559 lines
18 KiB
Python
559 lines
18 KiB
Python
# -*- coding: utf-8 -*- #
|
|
# Copyright 2015 Google LLC. All Rights Reserved.
|
|
#
|
|
# Licensed under the Apache License, Version 2.0 (the "License");
|
|
# you may not use this file except in compliance with the License.
|
|
# You may obtain a copy of the License at
|
|
#
|
|
# http://www.apache.org/licenses/LICENSE-2.0
|
|
#
|
|
# Unless required by applicable law or agreed to in writing, software
|
|
# distributed under the License is distributed on an "AS IS" BASIS,
|
|
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
# See the License for the specific language governing permissions and
|
|
# limitations under the License.
|
|
|
|
"""Utilities for loading and parsing kubeconfig."""
|
|
|
|
from __future__ import absolute_import
|
|
from __future__ import division
|
|
from __future__ import unicode_literals
|
|
|
|
import os
|
|
import subprocess
|
|
|
|
from googlecloudsdk.core import config
|
|
from googlecloudsdk.core import exceptions as core_exceptions
|
|
from googlecloudsdk.core import log
|
|
from googlecloudsdk.core import properties
|
|
from googlecloudsdk.core import yaml
|
|
from googlecloudsdk.core.util import encoding
|
|
from googlecloudsdk.core.util import files as file_utils
|
|
from googlecloudsdk.core.util import platforms
|
|
|
|
|
|
class Error(core_exceptions.Error):
|
|
"""Class for errors raised by kubeconfig utilities."""
|
|
|
|
|
|
class MissingEnvVarError(Error):
|
|
"""An exception raised when required environment variables are missing."""
|
|
|
|
|
|
GKE_GCLOUD_AUTH_PLUGIN_CACHE_FILE_NAME = 'gke_gcloud_auth_plugin_cache'
|
|
|
|
|
|
class Kubeconfig(object):
|
|
"""Interface for interacting with a kubeconfig file."""
|
|
|
|
def __init__(self, raw_data, filename):
|
|
self._filename = filename
|
|
self._data = raw_data
|
|
self.clusters = {}
|
|
self.users = {}
|
|
self.contexts = {}
|
|
|
|
entry = None
|
|
try:
|
|
for cluster in self._data['clusters']:
|
|
entry = cluster
|
|
self.clusters[cluster['name']] = cluster
|
|
for user in self._data['users']:
|
|
entry = user
|
|
self.users[user['name']] = user
|
|
for context in self._data['contexts']:
|
|
entry = context
|
|
self.contexts[context['name']] = context
|
|
except KeyError as error:
|
|
raise Error(
|
|
'expected key {0} not found for entry {1}'.format(error, entry)
|
|
)
|
|
# WARNING: this will clear the ~/.kube/config and re-create it
|
|
# with only one entry for current context.
|
|
|
|
@property
|
|
def current_context(self):
|
|
return self._data['current-context']
|
|
|
|
@property
|
|
def filename(self):
|
|
return self._filename
|
|
|
|
def Clear(self, key):
|
|
self.contexts.pop(key, None)
|
|
self.clusters.pop(key, None)
|
|
self.users.pop(key, None)
|
|
if self._data.get('current-context') == key:
|
|
self._data['current-context'] = ''
|
|
|
|
def SaveToFile(self):
|
|
"""Save kubeconfig to file.
|
|
|
|
Raises:
|
|
Error: don't have the permission to open kubeconfig or plugin cache file.
|
|
"""
|
|
self._data['clusters'] = list(self.clusters.values())
|
|
self._data['users'] = list(self.users.values())
|
|
self._data['contexts'] = list(self.contexts.values())
|
|
with file_utils.FileWriter(self._filename, private=True) as fp:
|
|
yaml.dump(self._data, fp)
|
|
|
|
# GKE_GCLOUD_AUTH_PLUGIN_CACHE_FILE_NAME is used by GKE_GCLOUD_AUTH_PLUGIN
|
|
# Erase cache file everytime kubeconfig is updated. This allows for a reset
|
|
# of the cache. Previously, credentials were cached in the kubeconfig file
|
|
# and updating the kubeconfig allowed for a "reset" of the cache.
|
|
dirname = os.path.dirname(self._filename)
|
|
gke_gcloud_auth_plugin_file_path = os.path.join(
|
|
dirname, GKE_GCLOUD_AUTH_PLUGIN_CACHE_FILE_NAME
|
|
)
|
|
if os.path.exists(gke_gcloud_auth_plugin_file_path):
|
|
file_utils.WriteFileAtomically(gke_gcloud_auth_plugin_file_path, '')
|
|
|
|
def SetCurrentContext(self, context):
|
|
self._data['current-context'] = context
|
|
|
|
@classmethod
|
|
def _Validate(cls, data):
|
|
"""Make sure we have the main fields of a kubeconfig."""
|
|
if not data:
|
|
raise Error('empty file')
|
|
try:
|
|
for key in ('clusters', 'users', 'contexts'):
|
|
if not isinstance(data[key], list):
|
|
raise Error(
|
|
'invalid type for {0}: {1}'.format(data[key], type(data[key]))
|
|
)
|
|
except KeyError as error:
|
|
raise Error('expected key {0} not found'.format(error))
|
|
|
|
@classmethod
|
|
def LoadFromFile(cls, filename):
|
|
try:
|
|
data = yaml.load_path(filename)
|
|
except yaml.Error as error:
|
|
raise Error(
|
|
'unable to load kubeconfig for {0}: {1}'.format(
|
|
filename, error.inner_error
|
|
)
|
|
)
|
|
cls._Validate(data)
|
|
return cls(data, filename)
|
|
|
|
@classmethod
|
|
def LoadOrCreate(cls, path):
|
|
"""Read in the kubeconfig, and if it doesn't exist create one there."""
|
|
if os.path.isdir(path):
|
|
raise IsADirectoryError(
|
|
'{0} is a directory. File must be provided.'.format(path)
|
|
)
|
|
if os.path.isfile(path):
|
|
try:
|
|
return cls.LoadFromFile(path)
|
|
except (Error, IOError) as error:
|
|
log.debug(
|
|
'unable to load default kubeconfig: {0}; recreating {1}'.format(
|
|
error, path
|
|
)
|
|
)
|
|
file_utils.MakeDir(os.path.dirname(path))
|
|
kubeconfig = cls(EmptyKubeconfig(), path)
|
|
kubeconfig.SaveToFile()
|
|
return kubeconfig
|
|
|
|
@classmethod
|
|
def Default(cls):
|
|
return cls.LoadOrCreate(Kubeconfig.DefaultPath())
|
|
|
|
@staticmethod
|
|
def DefaultPath():
|
|
"""Return default path for kubeconfig file."""
|
|
|
|
kubeconfig = encoding.GetEncodedValue(os.environ, 'KUBECONFIG')
|
|
if kubeconfig:
|
|
kubeconfigs = kubeconfig.split(os.pathsep)
|
|
for kubeconfig in kubeconfigs:
|
|
# KUBEONCIFG=$KUBECONFIG:~/.kube/config might be ':~/.kube/config'
|
|
if kubeconfig:
|
|
return os.path.abspath(kubeconfig)
|
|
|
|
# This follows the same resolution process as kubectl for the config file.
|
|
home_dir = encoding.GetEncodedValue(os.environ, 'HOME')
|
|
if not home_dir and platforms.OperatingSystem.IsWindows():
|
|
home_drive = encoding.GetEncodedValue(os.environ, 'HOMEDRIVE')
|
|
home_path = encoding.GetEncodedValue(os.environ, 'HOMEPATH')
|
|
if home_drive and home_path:
|
|
home_dir = os.path.join(home_drive, home_path)
|
|
if not home_dir:
|
|
home_dir = encoding.GetEncodedValue(os.environ, 'USERPROFILE')
|
|
|
|
if not home_dir:
|
|
raise MissingEnvVarError(
|
|
'environment variable {vars} or KUBECONFIG must be set to store '
|
|
'credentials for kubectl'.format(
|
|
vars='HOMEDRIVE/HOMEPATH, USERPROFILE, HOME,'
|
|
if platforms.OperatingSystem.IsWindows()
|
|
else 'HOME'
|
|
)
|
|
)
|
|
return os.path.join(home_dir, '.kube', 'config')
|
|
|
|
def Merge(self, kubeconfig):
|
|
"""Merge another kubeconfig into self.
|
|
|
|
In case of overlapping keys, the value in self is kept and the value in
|
|
the other kubeconfig is lost.
|
|
|
|
Args:
|
|
kubeconfig: a Kubeconfig instance
|
|
"""
|
|
self.SetCurrentContext(self.current_context or kubeconfig.current_context)
|
|
self.clusters = dict(
|
|
list(kubeconfig.clusters.items()) + list(self.clusters.items())
|
|
)
|
|
self.users = dict(list(kubeconfig.users.items()) + list(self.users.items()))
|
|
self.contexts = dict(
|
|
list(kubeconfig.contexts.items()) + list(self.contexts.items())
|
|
)
|
|
|
|
|
|
def Cluster(name, server, ca_path=None, ca_data=None, has_dns_endpoint=False):
|
|
"""Generate and return a cluster kubeconfig object."""
|
|
cluster = {
|
|
'server': server,
|
|
}
|
|
if ca_path and ca_data:
|
|
raise Error('cannot specify both ca_path and ca_data')
|
|
if ca_path:
|
|
cluster['certificate-authority'] = ca_path
|
|
elif ca_data is not None and not has_dns_endpoint:
|
|
cluster['certificate-authority-data'] = ca_data
|
|
elif not has_dns_endpoint:
|
|
cluster['insecure-skip-tls-verify'] = True
|
|
return {'name': name, 'cluster': cluster}
|
|
|
|
|
|
def User(
|
|
name,
|
|
auth_provider=None,
|
|
auth_provider_cmd_path=None,
|
|
auth_provider_cmd_args=None,
|
|
auth_provider_expiry_key=None,
|
|
auth_provider_token_key=None,
|
|
cert_path=None,
|
|
cert_data=None,
|
|
key_path=None,
|
|
key_data=None,
|
|
impersonate_service_account=None,
|
|
iam_token=None,
|
|
):
|
|
"""Generates and returns a user kubeconfig object.
|
|
|
|
Args:
|
|
name: str, nickname for this user entry.
|
|
auth_provider: str, authentication provider.
|
|
auth_provider_cmd_path: str, authentication provider command path.
|
|
auth_provider_cmd_args: str, authentication provider command args.
|
|
auth_provider_expiry_key: str, authentication provider expiry key.
|
|
auth_provider_token_key: str, authentication provider token key.
|
|
cert_path: str, path to client certificate file.
|
|
cert_data: str, base64 encoded client certificate data.
|
|
key_path: str, path to client key file.
|
|
key_data: str, base64 encoded client key data.
|
|
impersonate_service_account: str, service account to impersonate.
|
|
iam_token: str, IAM token to use for authentication.
|
|
|
|
Returns:
|
|
dict, valid kubeconfig user entry.
|
|
|
|
Raises:
|
|
Error: if no auth info is provided (auth_provider or cert AND key)
|
|
"""
|
|
# TODO(b/70856999) Figure out what the correct behavior for client certs is.
|
|
if not (
|
|
auth_provider or (cert_path and key_path) or (cert_data and key_data)
|
|
):
|
|
raise Error('either auth_provider or cert & key must be provided')
|
|
user = {}
|
|
use_exec_auth = _UseExecAuth()
|
|
|
|
if auth_provider:
|
|
# Setup authprovider
|
|
# if certain 'auth_provider_' fields are "present" OR
|
|
# if use_exec_auth is set to False
|
|
# pylint: disable=line-too-long
|
|
if (
|
|
auth_provider_cmd_path
|
|
or auth_provider_cmd_args
|
|
or auth_provider_expiry_key
|
|
or auth_provider_token_key
|
|
or not use_exec_auth
|
|
):
|
|
# auth-provider is being deprecated in favor of "exec" in k8s 1.25.
|
|
user['auth-provider'] = _AuthProvider(
|
|
name=auth_provider,
|
|
cmd_path=auth_provider_cmd_path,
|
|
cmd_args=auth_provider_cmd_args,
|
|
expiry_key=auth_provider_expiry_key,
|
|
token_key=auth_provider_token_key,
|
|
)
|
|
else:
|
|
user['exec'] = _ExecAuthPlugin(impersonate_service_account)
|
|
|
|
if cert_path and cert_data:
|
|
raise Error('cannot specify both cert_path and cert_data')
|
|
if cert_path:
|
|
user['client-certificate'] = cert_path
|
|
elif cert_data:
|
|
user['client-certificate-data'] = cert_data
|
|
|
|
if key_path and key_data:
|
|
raise Error('cannot specify both key_path and key_data')
|
|
if key_path:
|
|
user['client-key'] = key_path
|
|
elif key_data:
|
|
user['client-key-data'] = key_data
|
|
|
|
if iam_token:
|
|
user['token'] = iam_token
|
|
log.status.Print(f'Added IAM token to kubeconfig entry for user {name}.')
|
|
|
|
return {'name': name, 'user': user}
|
|
|
|
|
|
def _UseExecAuth():
|
|
"""Returns a bool noting if ExecAuth should be enabled.
|
|
|
|
Returns:
|
|
bool, which notes if ExecAuth should be enabled
|
|
"""
|
|
# Enable ExecAuth for all users
|
|
use_exec_auth = True
|
|
|
|
use_gke_gcloud_auth_plugin = encoding.GetEncodedValue(
|
|
os.environ, 'USE_GKE_GCLOUD_AUTH_PLUGIN'
|
|
)
|
|
# if use_gke_gcloud_auth_plugin is explicitly set(True/False), take action.
|
|
# if use_gke_gcloud_auth_plugin is NOT explicitly set, do nothing
|
|
if (
|
|
use_gke_gcloud_auth_plugin
|
|
and use_gke_gcloud_auth_plugin.lower() == 'true'
|
|
):
|
|
use_exec_auth = True
|
|
elif (
|
|
use_gke_gcloud_auth_plugin
|
|
and use_gke_gcloud_auth_plugin.lower() == 'false'
|
|
):
|
|
use_exec_auth = False
|
|
|
|
return use_exec_auth
|
|
|
|
|
|
SDK_BIN_PATH_NOT_FOUND = """\
|
|
Path to sdk installation not found. Please switch to application default
|
|
credentials using one of
|
|
|
|
$ gcloud config set container/use_application_default_credentials true
|
|
$ export CLOUDSDK_CONTAINER_USE_APPLICATION_DEFAULT_CREDENTIALS=true"""
|
|
|
|
GKE_GCLOUD_AUTH_INSTALL_HINT = """\
|
|
Install gke-gcloud-auth-plugin for use with kubectl by following \
|
|
https://cloud.google.com/kubernetes-engine/docs/how-to/cluster-access-for-kubectl#install_plugin"""
|
|
|
|
GKE_GCLOUD_AUTH_PLUGIN_NOT_FOUND = """\
|
|
ACTION REQUIRED: gke-gcloud-auth-plugin, \
|
|
which is needed for continued use of kubectl, was not found or is not executable. \
|
|
""" + GKE_GCLOUD_AUTH_INSTALL_HINT
|
|
|
|
|
|
def _ExecAuthPlugin(impersonate_service_account=None):
|
|
"""Generate and return an exec auth plugin config.
|
|
|
|
Constructs an exec auth plugin config entry readable by kubectl.
|
|
This tells kubectl to call out to gke-gcloud-auth-plugin and
|
|
parse the output to retrieve access tokens to authenticate to
|
|
the kubernetes master.
|
|
|
|
Kubernetes GKE Auth Provider plugin is defined at
|
|
https://kubernetes.io/docs/reference/access-authn-authz/authentication/#client-go-credential-plugins
|
|
|
|
GKE GCloud Exec Auth Plugin code is at
|
|
https://github.com/kubernetes/cloud-provider-gcp/tree/master/cmd/gke-gcloud-auth-plugin
|
|
|
|
Args:
|
|
impersonate_service_account: str, service account to impersonate.
|
|
|
|
Returns:
|
|
dict, valid exec auth plugin config entry.
|
|
Raises:
|
|
Error: Only one of --dns-endpoint or USE_APPLICATION_DEFAULT_CREDENTIALS
|
|
should be set at a time.
|
|
"""
|
|
|
|
use_application_default_credentials = (
|
|
properties.VALUES.container.use_app_default_credentials.GetBool()
|
|
)
|
|
command = _GetGkeGcloudPluginCommandAndPrintWarning()
|
|
|
|
exec_cfg = {
|
|
'command': command,
|
|
'apiVersion': 'client.authentication.k8s.io/v1beta1',
|
|
'installHint': GKE_GCLOUD_AUTH_INSTALL_HINT,
|
|
'provideClusterInfo': True,
|
|
}
|
|
|
|
args = []
|
|
if use_application_default_credentials:
|
|
args.append('--use_application_default_credentials')
|
|
if impersonate_service_account:
|
|
args.append('--impersonate_service_account=' + impersonate_service_account)
|
|
|
|
if args:
|
|
exec_cfg['args'] = args
|
|
|
|
return exec_cfg
|
|
|
|
|
|
def _AuthProvider(
|
|
name='gcp', cmd_path=None, cmd_args=None, expiry_key=None, token_key=None
|
|
):
|
|
"""Generates and returns an auth provider config.
|
|
|
|
Constructs an auth provider config entry readable by kubectl. This tells
|
|
kubectl to call out to a specific gcloud command and parse the output to
|
|
retrieve access tokens to authenticate to the kubernetes master.
|
|
Kubernetes gcp auth provider plugin at
|
|
https://github.com/kubernetes/kubernetes/tree/master/staging/src/k8s.io/client-go/plugin/pkg/client/auth/gcp
|
|
|
|
Args:
|
|
name: auth provider name
|
|
cmd_path: str, authentication provider command path.
|
|
cmd_args: str, authentication provider command arguments.
|
|
expiry_key: str, authentication provider expiry key.
|
|
token_key: str, authentication provider token key.
|
|
|
|
Returns:
|
|
dict, valid auth provider config entry.
|
|
Raises:
|
|
Error: Path to sdk installation not found. Please switch to application
|
|
default credentials using one of
|
|
|
|
$ gcloud config set container/use_application_default_credentials true
|
|
$ export CLOUDSDK_CONTAINER_USE_APPLICATION_DEFAULT_CREDENTIALS=true.
|
|
"""
|
|
provider = {'name': name}
|
|
if (
|
|
name == 'gcp'
|
|
and not properties.VALUES.container.use_app_default_credentials.GetBool()
|
|
):
|
|
bin_name = 'gcloud'
|
|
if platforms.OperatingSystem.IsWindows():
|
|
bin_name = 'gcloud.cmd'
|
|
|
|
if cmd_path is None:
|
|
sdk_bin_path = config.Paths().sdk_bin_path
|
|
if sdk_bin_path is None:
|
|
log.error(SDK_BIN_PATH_NOT_FOUND)
|
|
raise Error(SDK_BIN_PATH_NOT_FOUND)
|
|
cmd_path = os.path.join(sdk_bin_path, bin_name)
|
|
try:
|
|
# Print warning if gke-gcloud-auth-plugin is not present or executable
|
|
_GetGkeGcloudPluginCommandAndPrintWarning()
|
|
except Exception: # pylint: disable=broad-except
|
|
# Catch all exceptions to avoid any failures in this code path and
|
|
# ignore the exceptions, as no action needs to be taken.
|
|
pass
|
|
|
|
cfg = {
|
|
# Command for gcloud credential helper
|
|
'cmd-path': cmd_path,
|
|
# Args for gcloud credential helper
|
|
'cmd-args': (
|
|
cmd_args if cmd_args else 'config config-helper --format=json'
|
|
),
|
|
# JSONpath to the field that is the raw access token
|
|
'token-key': token_key if token_key else '{.credential.access_token}',
|
|
# JSONpath to the field that is the expiration timestamp
|
|
'expiry-key': (
|
|
expiry_key if expiry_key else '{.credential.token_expiry}'
|
|
),
|
|
# Note: we're omitting 'time-fmt' field, which if provided, is a
|
|
# format string of the golang reference time. It can be safely omitted
|
|
# because config-helper's default time format is RFC3339, which is the
|
|
# same default kubectl assumes.
|
|
}
|
|
provider['config'] = cfg
|
|
return provider
|
|
|
|
|
|
def _GetGkeGcloudPluginCommandAndPrintWarning():
|
|
"""Get Gke Gcloud Plugin Command to be used.
|
|
|
|
Returns Gke Gcloud Plugin Command to be used. Also,
|
|
prints warning if plugin is not present or doesn't work correctly.
|
|
|
|
Returns:
|
|
string, Gke Gcloud Plugin Command to be used.
|
|
"""
|
|
bin_name = 'gke-gcloud-auth-plugin'
|
|
if platforms.OperatingSystem.IsWindows():
|
|
bin_name = 'gke-gcloud-auth-plugin.exe'
|
|
command = bin_name
|
|
|
|
# Check if command is in PATH and executable. Else, print critical(RED)
|
|
# warning as kubectl will break if command is not executable.
|
|
try:
|
|
subprocess.run(
|
|
[command, '--version'],
|
|
timeout=5,
|
|
check=False,
|
|
stdout=subprocess.DEVNULL,
|
|
stderr=subprocess.DEVNULL,
|
|
)
|
|
except Exception: # pylint: disable=broad-except
|
|
# Provide SDK Full path if command is not in PATH. This helps work
|
|
# around scenarios where cloud-sdk install location is not in PATH
|
|
# as sdk was installed using other distributions methods Eg: brew
|
|
try:
|
|
# config.Paths().sdk_bin_path throws an exception in some test envs,
|
|
# but is commonly defined in prod environments
|
|
sdk_bin_path = config.Paths().sdk_bin_path
|
|
if sdk_bin_path is None:
|
|
log.critical(GKE_GCLOUD_AUTH_PLUGIN_NOT_FOUND)
|
|
else:
|
|
sdk_path_bin_name = os.path.join(sdk_bin_path, command)
|
|
subprocess.run(
|
|
[sdk_path_bin_name, '--version'],
|
|
timeout=5,
|
|
check=False,
|
|
stdout=subprocess.DEVNULL,
|
|
stderr=subprocess.DEVNULL,
|
|
)
|
|
command = sdk_path_bin_name # update command if sdk_path_bin_name works
|
|
except Exception: # pylint: disable=broad-except
|
|
log.critical(GKE_GCLOUD_AUTH_PLUGIN_NOT_FOUND)
|
|
|
|
return command
|
|
|
|
|
|
def Context(name, cluster, user):
|
|
"""Generate and return a context kubeconfig object."""
|
|
return {
|
|
'name': name,
|
|
'context': {
|
|
'cluster': cluster,
|
|
'user': user,
|
|
},
|
|
}
|
|
|
|
|
|
def EmptyKubeconfig():
|
|
return {
|
|
'apiVersion': 'v1',
|
|
'contexts': [],
|
|
'clusters': [],
|
|
'current-context': '',
|
|
'kind': 'Config',
|
|
'preferences': {},
|
|
'users': [],
|
|
}
|