279 lines
10 KiB
Python
279 lines
10 KiB
Python
# -*- coding: utf-8 -*- #
|
|
# Copyright 2024 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 interacting with the Connect Gateway API."""
|
|
|
|
from __future__ import absolute_import
|
|
from __future__ import division
|
|
from __future__ import unicode_literals
|
|
|
|
from typing import List, Union
|
|
|
|
from googlecloudsdk.api_lib.cloudresourcemanager import projects_api
|
|
from googlecloudsdk.api_lib.container import util as container_util
|
|
from googlecloudsdk.api_lib.container.fleet.connectgateway import client as gateway_client
|
|
from googlecloudsdk.api_lib.container.fleet.connectgateway import util as gateway_util
|
|
from googlecloudsdk.api_lib.services import enable_api
|
|
from googlecloudsdk.api_lib.util import apis
|
|
from googlecloudsdk.calliope import base
|
|
from googlecloudsdk.command_lib.container.fleet import api_util as hubapi_util
|
|
from googlecloudsdk.command_lib.container.fleet import base as hub_base
|
|
from googlecloudsdk.command_lib.container.fleet import gwkubeconfig_util as kconfig
|
|
from googlecloudsdk.command_lib.container.fleet import overrides
|
|
from googlecloudsdk.command_lib.container.fleet.memberships import errors as memberships_errors
|
|
from googlecloudsdk.command_lib.container.fleet.memberships import util
|
|
from googlecloudsdk.command_lib.projects import util as project_util
|
|
from googlecloudsdk.core import log
|
|
from googlecloudsdk.core import properties
|
|
from googlecloudsdk.core.util import platforms
|
|
|
|
KUBECONTEXT_FORMAT = 'connectgateway_{project}_{location}_{membership}'
|
|
SERVER_FORMAT = 'https://{service_name}/{version}/projects/{project_number}/locations/{location}/{collection}/{membership}'
|
|
REQUIRED_SERVER_PERMISSIONS = [
|
|
'gkehub.gateway.get',
|
|
]
|
|
REQUIRED_CLIENT_PERMISSIONS = [
|
|
'gkehub.memberships.get',
|
|
'gkehub.gateway.get',
|
|
'serviceusage.services.get',
|
|
]
|
|
|
|
|
|
class GetCredentialsCommand(hub_base.HubCommand, base.Command):
|
|
"""GetCredentialsCommand is a base class with util functions for Gateway credential generating commands."""
|
|
|
|
# TODO(b/368039642): Remove once we're sure server-side generation is stable
|
|
def RunGetCredentials(self, membership_id, arg_location, arg_namespace=None):
|
|
container_util.CheckKubectlInstalled()
|
|
project_id = hub_base.HubCommand.Project()
|
|
|
|
log.status.Print('Starting to build Gateway kubeconfig...')
|
|
log.status.Print('Current project_id: ' + project_id)
|
|
|
|
self.RunIamCheck(project_id, REQUIRED_CLIENT_PERMISSIONS)
|
|
try:
|
|
hub_endpoint_override = properties.VALUES.api_endpoint_overrides.Property(
|
|
'gkehub'
|
|
).Get()
|
|
except properties.NoSuchPropertyError:
|
|
hub_endpoint_override = None
|
|
# API enablement is only done once per environment, regardless of which
|
|
# region is being accessed.
|
|
CheckGatewayApiEnablement(
|
|
project_id,
|
|
util.GetConnectGatewayServiceName(hub_endpoint_override, None),
|
|
)
|
|
|
|
membership = self.ReadClusterMembership(
|
|
project_id, arg_location, membership_id
|
|
)
|
|
|
|
# Registered GKE clusters use a different URL scheme, having
|
|
# `gkeMemberships/` rather than the standard `memberships/` resource type.
|
|
collection = 'memberships'
|
|
# Probers use GKE clusters to emulate attached clusters, and so must be
|
|
# exempt from this.
|
|
if project_id == 'gkeconnect-prober':
|
|
pass
|
|
elif (
|
|
hasattr(membership, 'endpoint')
|
|
and hasattr(membership.endpoint, 'gkeCluster')
|
|
and membership.endpoint.gkeCluster
|
|
):
|
|
collection = 'gkeMemberships'
|
|
|
|
self.GenerateKubeconfig(
|
|
util.GetConnectGatewayServiceName(hub_endpoint_override, arg_location),
|
|
project_id,
|
|
arg_location,
|
|
collection,
|
|
membership_id,
|
|
arg_namespace,
|
|
)
|
|
msg = (
|
|
'A new kubeconfig entry "'
|
|
+ self.KubeContext(
|
|
project_id, arg_location, membership_id, arg_namespace
|
|
)
|
|
+ '" has been generated and set as the current context.'
|
|
)
|
|
log.status.Print(msg)
|
|
|
|
def RunServerSide(
|
|
self,
|
|
membership_id: str,
|
|
arg_location: str,
|
|
force_use_agent: bool = False,
|
|
arg_namespace: Union[str, None] = None,
|
|
):
|
|
"""RunServerSide generates credentials using server-side kubeconfig generation.
|
|
|
|
Args:
|
|
membership_id: The short name of the membership to generate credentials
|
|
for.
|
|
arg_location: The location of the membership to generate credentials for.
|
|
force_use_agent: Whether to force the use of Connect Agent in generated
|
|
credentials.
|
|
arg_namespace: The namespace to use in the kubeconfig context.
|
|
"""
|
|
log.status.Print('Fetching Gateway kubeconfig...')
|
|
container_util.CheckKubectlInstalled()
|
|
project_id = hub_base.HubCommand.Project()
|
|
project_number = hub_base.HubCommand.Project(number=True)
|
|
|
|
# Ensure at the minimum that the user has gkehub.gateway.get. This is
|
|
# because the user might have gkehub.gateway.generateCredentials but not
|
|
# gkehub.gateway.get, which would lead to unclear errors when using kubectl.
|
|
self.RunIamCheck(project_id, REQUIRED_SERVER_PERMISSIONS)
|
|
|
|
operating_system = None
|
|
if platforms.OperatingSystem.IsWindows():
|
|
operating_system = gateway_util.WindowsOperatingSystem(
|
|
self.ReleaseTrack()
|
|
)
|
|
|
|
with overrides.RegionalGatewayEndpoint(arg_location):
|
|
client = gateway_client.GatewayClient(self.ReleaseTrack())
|
|
resp = client.GenerateCredentials(
|
|
name=f'projects/{project_number}/locations/{arg_location}/memberships/{membership_id}',
|
|
force_use_agent=force_use_agent,
|
|
kubernetes_namespace=arg_namespace,
|
|
operating_system=operating_system,
|
|
)
|
|
|
|
new = kconfig.Kubeconfig.LoadFromBytes(resp.kubeconfig)
|
|
kubeconfig = kconfig.Kubeconfig.Default()
|
|
kubeconfig.Merge(new, overwrite=True)
|
|
# The returned kubeconfig should only have one context.
|
|
kubeconfig.SetCurrentContext(list(new.contexts.keys())[0])
|
|
kubeconfig.SaveToFile()
|
|
|
|
msg = (
|
|
f'A new kubeconfig entry "{kubeconfig.current_context}" has been'
|
|
' generated and set as the current context.'
|
|
)
|
|
log.status.Print(msg)
|
|
|
|
def KubeContext(self, project_id, location, membership, namespace=None):
|
|
kc = KUBECONTEXT_FORMAT.format(
|
|
project=project_id, location=location, membership=membership
|
|
)
|
|
if namespace:
|
|
kc += '_ns-' + namespace
|
|
return kc
|
|
|
|
def RunIamCheck(self, project_id: str, permissions: List[str]):
|
|
"""Run an IAM check, making sure the caller has the necessary permissions to use the Gateway API."""
|
|
project_ref = project_util.ParseProject(project_id)
|
|
result = projects_api.TestIamPermissions(project_ref, permissions)
|
|
granted_permissions = result.permissions
|
|
|
|
if not set(permissions).issubset(set(granted_permissions)):
|
|
raise memberships_errors.InsufficientPermissionsError()
|
|
|
|
def ReadClusterMembership(self, project_id, location, membership):
|
|
resource_name = hubapi_util.MembershipRef(project_id, location, membership)
|
|
# If membership doesn't exist, exception will be raised to caller.
|
|
return hubapi_util.GetMembership(resource_name)
|
|
|
|
def GenerateKubeconfig(
|
|
self,
|
|
service_name,
|
|
project_id,
|
|
location,
|
|
collection,
|
|
membership,
|
|
namespace=None,
|
|
):
|
|
project_number = project_util.GetProjectNumber(project_id)
|
|
kwargs = {
|
|
'membership': membership,
|
|
'location': location,
|
|
'project_id': project_id,
|
|
'server': SERVER_FORMAT.format(
|
|
service_name=service_name,
|
|
version=self.GetVersion(),
|
|
project_number=project_number,
|
|
location=location,
|
|
collection=collection,
|
|
membership=membership,
|
|
),
|
|
'auth_provider': 'gcp',
|
|
}
|
|
user_kwargs = {
|
|
'auth_provider': 'gcp',
|
|
}
|
|
|
|
cluster_kwargs = {}
|
|
context = self.KubeContext(project_id, location, membership, namespace)
|
|
cluster = self.KubeContext(project_id, location, membership)
|
|
kubeconfig = kconfig.Kubeconfig.Default()
|
|
# Use same key for context, cluster, and user.
|
|
kubeconfig.contexts[context] = kconfig.Context(
|
|
context, cluster, context, namespace
|
|
)
|
|
kubeconfig.users[context] = kconfig.User(context, **user_kwargs)
|
|
kubeconfig.clusters[cluster] = kconfig.Cluster(
|
|
cluster, kwargs['server'], **cluster_kwargs
|
|
)
|
|
kubeconfig.SetCurrentContext(context)
|
|
kubeconfig.SaveToFile()
|
|
return kubeconfig
|
|
|
|
@classmethod
|
|
def GetVersion(cls):
|
|
if cls.ReleaseTrack() is base.ReleaseTrack.ALPHA:
|
|
return 'v1alpha1'
|
|
elif cls.ReleaseTrack() is base.ReleaseTrack.BETA:
|
|
return 'v1beta1'
|
|
elif cls.ReleaseTrack() is base.ReleaseTrack.GA:
|
|
return 'v1'
|
|
else:
|
|
return ''
|
|
|
|
|
|
def CheckGatewayApiEnablement(project_id, service_name):
|
|
"""Checks if the Connect Gateway API is enabled for a given project.
|
|
|
|
Prompts the user to enable the API if the API is not enabled. Defaults to
|
|
"No". Throws an error if the user declines to enable the API.
|
|
|
|
Args:
|
|
project_id: The ID of the project on which to check/enable the API.
|
|
service_name: The name of the service to check/enable the API.
|
|
|
|
Raises:
|
|
memberships_errors.ServiceNotEnabledError: if the user declines to attempt
|
|
to enable the API.
|
|
exceptions.GetServicesPermissionDeniedException: if a 403 or 404 error is
|
|
returned by the Get request.
|
|
apitools_exceptions.HttpError: Another miscellaneous error with the
|
|
listing service.
|
|
api_exceptions.HttpException: API not enabled error if the user chooses to
|
|
not enable the API.
|
|
"""
|
|
if not enable_api.IsServiceEnabled(project_id, service_name):
|
|
try:
|
|
apis.PromptToEnableApi(
|
|
project_id,
|
|
service_name,
|
|
memberships_errors.ServiceNotEnabledError(
|
|
'Connect Gateway API', service_name, project_id
|
|
),
|
|
)
|
|
except apis.apitools_exceptions.RequestError:
|
|
# Since we are not actually calling the API, there is nothing to retry,
|
|
# so this signal to retry can be ignored
|
|
pass
|