File: //snap/google-cloud-cli/396/lib/googlecloudsdk/command_lib/container/gkemulticloud/kubeconfig.py
# -*- 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')