422 lines
14 KiB
Python
422 lines
14 KiB
Python
# -*- coding: utf-8 -*- #
|
|
# Copyright 2022 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 generating kubeconfig entries."""
|
|
|
|
from __future__ import absolute_import
|
|
from __future__ import division
|
|
from __future__ import unicode_literals
|
|
|
|
import base64
|
|
import os
|
|
import subprocess
|
|
|
|
from googlecloudsdk.api_lib.container import kubeconfig as kubeconfig_util
|
|
from googlecloudsdk.api_lib.container import util
|
|
from googlecloudsdk.command_lib.container.fleet import gateway
|
|
from googlecloudsdk.command_lib.container.fleet import gwkubeconfig_util
|
|
from googlecloudsdk.command_lib.container.gkemulticloud import errors
|
|
from googlecloudsdk.core import config
|
|
from googlecloudsdk.core import log
|
|
from googlecloudsdk.core import properties
|
|
from googlecloudsdk.core.util import platforms
|
|
from googlecloudsdk.core.util import semver
|
|
|
|
COMMAND_DESCRIPTION = """
|
|
Fetch credentials for a running {cluster_type}.
|
|
|
|
This command updates a kubeconfig file with appropriate credentials and
|
|
endpoint information to point kubectl at a specific {cluster_type}.
|
|
|
|
By default, credentials are written to ``HOME/.kube/config''.
|
|
You can provide an alternate path by setting the ``KUBECONFIG'' environment
|
|
variable. If ``KUBECONFIG'' contains multiple paths, the first one is used.
|
|
|
|
This command enables switching to a specific cluster, when working
|
|
with multiple clusters. It can also be used to access a previously created
|
|
cluster from a new workstation.
|
|
|
|
By default, the command will configure kubectl to automatically refresh its
|
|
credentials using the same identity as the gcloud command-line tool.
|
|
If you are running kubectl as part of an application, it is recommended to use
|
|
[application default credentials](https://cloud.google.com/docs/authentication/production).
|
|
To configure a kubeconfig file to use application default credentials, set
|
|
the ``container/use_application_default_credentials''
|
|
[Google Cloud CLI property](https://cloud.google.com/sdk/docs/properties) to
|
|
``true'' before running the command.
|
|
|
|
See [](https://cloud.google.com/kubernetes-engine/docs/kubectl) for
|
|
kubectl documentation.
|
|
"""
|
|
|
|
COMMAND_EXAMPLE = """
|
|
To get credentials of a cluster named ``my-cluster'' managed in location ``us-west1'',
|
|
run:
|
|
|
|
$ {command} my-cluster --location=us-west1
|
|
"""
|
|
|
|
NOT_RUNNING_MSG = """\
|
|
Cluster {} is not RUNNING. The Kubernetes API may or may not be available. \
|
|
Check the cluster status for more information."""
|
|
|
|
STILL_PROVISIONING_MSG = 'Is it still PROVISIONING?'
|
|
|
|
|
|
def GenerateContext(kind, project_id, location, cluster_id):
|
|
"""Generates a kubeconfig context for an Anthos Multi-Cloud cluster.
|
|
|
|
Args:
|
|
kind: str, kind of the cluster e.g. aws, azure.
|
|
project_id: str, project ID accociated with the cluster.
|
|
location: str, Google location of the cluster.
|
|
cluster_id: str, ID of the cluster.
|
|
|
|
Returns:
|
|
The context for the kubeconfig entry.
|
|
"""
|
|
template = 'gke_{kind}_{project_id}_{location}_{cluster_id}'
|
|
return template.format(
|
|
kind=kind, project_id=project_id, location=location, cluster_id=cluster_id
|
|
)
|
|
|
|
|
|
def GenerateAuthProviderCmdArgs(kind, cluster_id, location, project):
|
|
"""Generates command arguments for kubeconfig's authorization provider.
|
|
|
|
Args:
|
|
kind: str, kind of the cluster e.g. aws, azure.
|
|
cluster_id: str, ID of the cluster.
|
|
location: str, Google location of the cluster.
|
|
project: str, Google Cloud project of the cluster.
|
|
|
|
Returns:
|
|
The command arguments for kubeconfig's authorization provider.
|
|
"""
|
|
template = (
|
|
'container {kind} clusters print-access-token '
|
|
'{cluster_id} --project={project} --location={location} '
|
|
'--format=json --exec-credential'
|
|
)
|
|
return template.format(
|
|
kind=kind, cluster_id=cluster_id, location=location, project=project
|
|
)
|
|
|
|
|
|
def GenerateAttachedKubeConfig(cluster, cluster_id, context, cmd_path):
|
|
"""Generates a kubeconfig entry for an Anthos Multi-cloud attached cluster.
|
|
|
|
Args:
|
|
cluster: object, Anthos Multi-cloud cluster.
|
|
cluster_id: str, the cluster ID.
|
|
context: str, context for the kubeconfig entry.
|
|
cmd_path: str, authentication provider command path.
|
|
"""
|
|
kubeconfig = kubeconfig_util.Kubeconfig.Default()
|
|
# Use the same key for context, cluster, and user.
|
|
kubeconfig.contexts[context] = kubeconfig_util.Context(
|
|
context, context, context
|
|
)
|
|
|
|
_CheckPreqs()
|
|
_ConnectGatewayKubeconfig(kubeconfig, cluster, cluster_id, context, cmd_path)
|
|
|
|
kubeconfig.SetCurrentContext(context)
|
|
kubeconfig.SaveToFile()
|
|
log.status.Print(
|
|
'A new kubeconfig entry "{}" has been generated and set as the '
|
|
'current context.'.format(context)
|
|
)
|
|
|
|
|
|
def GenerateKubeconfig(
|
|
cluster, cluster_id, context, cmd_path, cmd_args, private_ep=False
|
|
):
|
|
"""Generates a kubeconfig entry for an Anthos Multi-cloud cluster.
|
|
|
|
Args:
|
|
cluster: object, Anthos Multi-cloud cluster.
|
|
cluster_id: str, the cluster ID.
|
|
context: str, context for the kubeconfig entry.
|
|
cmd_path: str, authentication provider command path.
|
|
cmd_args: str, authentication provider command arguments.
|
|
private_ep: bool, whether to use private VPC for authentication.
|
|
|
|
Raises:
|
|
Error: don't have the permission to open kubeconfig file.
|
|
"""
|
|
kubeconfig = kubeconfig_util.Kubeconfig.Default()
|
|
# Use the same key for context, cluster, and user.
|
|
kubeconfig.contexts[context] = kubeconfig_util.Context(
|
|
context, context, context
|
|
)
|
|
|
|
# Only default to use Connect Gateway for 1.21+.
|
|
version = _GetSemver(cluster, cluster_id)
|
|
if private_ep or version < semver.SemVer('1.21.0'):
|
|
_CheckPreqs(private_endpoint=True)
|
|
_PrivateVPCKubeconfig(
|
|
kubeconfig, cluster, cluster_id, context, cmd_path, cmd_args
|
|
)
|
|
else:
|
|
_CheckPreqs()
|
|
_ConnectGatewayKubeconfig(
|
|
kubeconfig, cluster, cluster_id, context, cmd_path
|
|
)
|
|
|
|
kubeconfig.SetCurrentContext(context)
|
|
kubeconfig.SaveToFile()
|
|
log.status.Print(
|
|
'A new kubeconfig entry "{}" has been generated and set as the '
|
|
'current context.'.format(context)
|
|
)
|
|
|
|
|
|
def _CheckPreqs(private_endpoint=False):
|
|
"""Checks the prerequisites to run get-credentials commands."""
|
|
util.CheckKubectlInstalled()
|
|
if not private_endpoint:
|
|
project_id = properties.VALUES.core.project.GetOrFail()
|
|
gateway.CheckGatewayApiEnablement(
|
|
project_id, _GetConnectGatewayEndpoint()
|
|
)
|
|
|
|
|
|
def _ConnectGatewayKubeconfig(
|
|
kubeconfig, cluster, cluster_id, context, cmd_path
|
|
):
|
|
"""Generates the Connect Gateway kubeconfig entry.
|
|
|
|
Args:
|
|
kubeconfig: object, Kubeconfig object.
|
|
cluster: object, Anthos Multi-cloud cluster.
|
|
cluster_id: str, the cluster ID.
|
|
context: str, context for the kubeconfig entry.
|
|
cmd_path: str, authentication provider command path.
|
|
|
|
Raises:
|
|
errors.MissingClusterField: cluster is missing required fields.
|
|
"""
|
|
if cluster.fleet is None or cluster.fleet.membership is None:
|
|
raise errors.MissingClusterField(
|
|
cluster_id, 'Fleet membership', STILL_PROVISIONING_MSG
|
|
)
|
|
server = 'https://{}/v1/{}'.format(
|
|
_GetConnectGatewayEndpoint(), cluster.fleet.membership
|
|
)
|
|
user_kwargs = {'auth_provider': 'gcp', 'auth_provider_cmd_path': cmd_path}
|
|
kubeconfig.users[context] = kubeconfig_util.User(context, **user_kwargs)
|
|
kubeconfig.clusters[context] = gwkubeconfig_util.Cluster(context, server)
|
|
|
|
|
|
def _PrivateVPCKubeconfig(
|
|
kubeconfig, cluster, cluster_id, context, cmd_path, cmd_args
|
|
):
|
|
"""Generates the kubeconfig entry to connect using private VPC.
|
|
|
|
Args:
|
|
kubeconfig: object, Kubeconfig object.
|
|
cluster: object, Anthos Multi-cloud cluster.
|
|
cluster_id: str, the cluster ID.
|
|
context: str, context for the kubeconfig entry.
|
|
cmd_path: str, authentication provider command path.
|
|
cmd_args: str, authentication provider command arguments.
|
|
"""
|
|
user = {}
|
|
user['exec'] = _ExecAuthPlugin(cmd_path, cmd_args)
|
|
kubeconfig.users[context] = {'name': context, 'user': user}
|
|
|
|
cluster_kwargs = {}
|
|
if cluster.clusterCaCertificate is None:
|
|
log.warning('Cluster is missing certificate authority data.')
|
|
else:
|
|
cluster_kwargs['ca_data'] = _GetCaData(cluster.clusterCaCertificate)
|
|
if cluster.endpoint is None:
|
|
raise errors.MissingClusterField(
|
|
cluster_id, 'endpoint', STILL_PROVISIONING_MSG
|
|
)
|
|
kubeconfig.clusters[context] = kubeconfig_util.Cluster(
|
|
context, 'https://{}'.format(cluster.endpoint), **cluster_kwargs
|
|
)
|
|
|
|
|
|
def ValidateClusterVersion(cluster, cluster_id):
|
|
"""Validates the cluster version.
|
|
|
|
Args:
|
|
cluster: object, Anthos Multi-cloud cluster.
|
|
cluster_id: str, the cluster ID.
|
|
|
|
Raises:
|
|
UnsupportedClusterVersion: cluster version is not supported.
|
|
MissingClusterField: expected cluster field is missing.
|
|
"""
|
|
version = _GetSemver(cluster, cluster_id)
|
|
if version < semver.SemVer('1.20.0'):
|
|
raise errors.UnsupportedClusterVersion(
|
|
'The command get-credentials is supported in cluster version 1.20 '
|
|
'and newer. For older versions, use get-kubeconfig.'
|
|
)
|
|
|
|
|
|
def _GetCaData(pem):
|
|
# Field certificate-authority-data in kubeconfig
|
|
# expects a base64 encoded string of a PEM.
|
|
return base64.b64encode(pem.encode('utf-8')).decode('utf-8')
|
|
|
|
|
|
def _GetSemver(cluster, cluster_id):
|
|
if cluster.controlPlane is None or cluster.controlPlane.version is None:
|
|
raise errors.MissingClusterField(cluster_id, 'version')
|
|
version = cluster.controlPlane.version
|
|
# The dev version e.g. 1.21-next does not conform to semantic versioning.
|
|
# Replace the -next suffix before parsing semver for version comparison.
|
|
if version.endswith('-next'):
|
|
v = version.replace('-next', '.0', 1)
|
|
return semver.SemVer(v)
|
|
return semver.SemVer(version)
|
|
|
|
|
|
def _GetConnectGatewayEndpoint():
|
|
"""Gets the corresponding Connect Gateway endpoint for Multicloud environment.
|
|
|
|
http://g3doc/cloud/kubernetes/multicloud/g3doc/oneplatform/team/how-to/hub
|
|
|
|
Returns:
|
|
The Connect Gateway endpoint.
|
|
|
|
Raises:
|
|
Error: Unknown API override.
|
|
"""
|
|
# TODO(b/196964566): Use per-region URL for Connect Gatway once GA e.g.
|
|
# us-west1-connectgateway.googleapis.com.
|
|
endpoint = properties.VALUES.api_endpoint_overrides.gkemulticloud.Get()
|
|
# Multicloud overrides prod endpoint at run time with the regionalized version
|
|
# so we can't simply check that endpoint is not overridden.
|
|
if (
|
|
endpoint is None
|
|
or endpoint.endswith('gkemulticloud.googleapis.com/')
|
|
or endpoint.endswith('preprod-gkemulticloud.sandbox.googleapis.com/')
|
|
):
|
|
return 'connectgateway.googleapis.com'
|
|
if 'staging-gkemulticloud' in endpoint:
|
|
return 'staging-connectgateway.sandbox.googleapis.com'
|
|
if endpoint.startswith('http://localhost') or endpoint.endswith(
|
|
'gkemulticloud.sandbox.googleapis.com/'
|
|
):
|
|
return 'autopush-connectgateway.sandbox.googleapis.com'
|
|
raise errors.UnknownApiEndpointOverrideError('gkemulticloud')
|
|
|
|
|
|
def ExecCredential(expiration_timestamp=None, access_token=None):
|
|
"""Generates a Kubernetes execCredential object."""
|
|
return {
|
|
'kind': 'ExecCredential',
|
|
'apiVersion': 'client.authentication.k8s.io/v1',
|
|
'status': {
|
|
'expirationTimestamp': expiration_timestamp,
|
|
'token': access_token,
|
|
},
|
|
}
|
|
|
|
|
|
def _ExecAuthPlugin(cmd_path, cmd_args):
|
|
"""Generates and returns an exec auth plugin config.
|
|
|
|
Args:
|
|
cmd_path: str, exec command path.
|
|
cmd_args: str, exec command arguments.
|
|
|
|
Returns:
|
|
dict, valid exec auth plugin config entry.
|
|
"""
|
|
if cmd_path is None:
|
|
bin_name = 'gcloud'
|
|
if platforms.OperatingSystem.IsWindows():
|
|
bin_name = 'gcloud.cmd'
|
|
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,
|
|
)
|
|
cmd_path = command
|
|
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(kubeconfig_util.SDK_BIN_PATH_NOT_FOUND)
|
|
raise kubeconfig_util.Error(kubeconfig_util.SDK_BIN_PATH_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,
|
|
)
|
|
# update command if sdk_path_bin_name works
|
|
cmd_path = sdk_path_bin_name
|
|
except Exception: # pylint: disable=broad-except
|
|
log.critical(kubeconfig_util.SDK_BIN_PATH_NOT_FOUND)
|
|
|
|
cfg = {
|
|
'command': cmd_path,
|
|
'apiVersion': 'client.authentication.k8s.io/v1',
|
|
'provideClusterInfo': True,
|
|
'args': cmd_args.split(' '),
|
|
'interactiveMode': 'Never',
|
|
}
|
|
|
|
endpoint = properties.VALUES.api_endpoint_overrides.gkemulticloud.Get()
|
|
if endpoint:
|
|
cfg['env'] = [{
|
|
'name': (
|
|
properties.VALUES.api_endpoint_overrides.gkemulticloud.EnvironmentName()
|
|
),
|
|
'value': endpoint,
|
|
}]
|
|
return cfg
|
|
|
|
|
|
def CheckClusterHasNodePools(cluster_client, cluster_ref):
|
|
"""Checks and gives a warning if the cluster does not have a node pool."""
|
|
try:
|
|
if not cluster_client.HasNodePools(cluster_ref):
|
|
log.warning(
|
|
'Cluster does not have a node pool. To use Connect Gateway, '
|
|
'ensure you have at least one Linux node pool running.'
|
|
)
|
|
# pylint: disable=bare-except, this function is just a warning and should not
|
|
# add new failures.
|
|
except:
|
|
pass
|
|
|
|
|
|
def ConnectGatewayInNodePools(cluster, cluster_id):
|
|
version = _GetSemver(cluster, cluster_id)
|
|
return version < semver.SemVer('1.25.0')
|