Files

281 lines
12 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.
"""The unregister-cluster command for removing clusters from the fleet."""
from __future__ import absolute_import
from __future__ import division
from __future__ import unicode_literals
import textwrap
from apitools.base.py import exceptions as apitools_exceptions
from googlecloudsdk.api_lib.container.fleet import util
from googlecloudsdk.calliope import base as calliope_base
from googlecloudsdk.command_lib.container.fleet import agent_util
from googlecloudsdk.command_lib.container.fleet import api_util
from googlecloudsdk.command_lib.container.fleet import exclusivity_util
from googlecloudsdk.command_lib.container.fleet import kube_util
from googlecloudsdk.command_lib.container.fleet import resources
from googlecloudsdk.command_lib.container.fleet import util as hub_util
from googlecloudsdk.command_lib.container.fleet.memberships import gke_util
from googlecloudsdk.command_lib.util.apis import arg_utils
from googlecloudsdk.core import exceptions
from googlecloudsdk.core import log
from googlecloudsdk.core.console import console_io
class Unregister(calliope_base.DeleteCommand):
r"""Unregister a cluster from a fleet.
This command unregisters a cluster with the fleet by:
1. Deleting the Fleet Membership resource for this cluster (a.k.a
`{parent_command} delete`).
2. Removing the corresponding in-cluster Kubernetes Resources that make the
cluster exclusive to one fleet (a.k.a `kubectl delete memberships
membership`).
3. [Optional for GKE cluster] Uninstalling the Connect agent from this
cluster (a.k.a `kubectl delete on the gke-connect namespace`).
The unregister command makes additional internal checks to ensure that all
three steps can be safely done to properly clean-up in-Fleet and in-cluster
resources.
To unregister a GKE cluster use `--gke-cluster` or `--gke-uri` flag (no
`--kubeconfig` flag is required).
To unregister a third-party cluster use `--context` flag (with an optional
-`-kubeconfig`s flag).
To only delete the Fleet Membership resource, consider using the command:
`{parent_command} delete`. This command is intended to delete stale Fleet
Membership resources as doing so on a fully registered cluster will skip some
of the steps above and orphan in-cluster resources and agent connections to
Google.
## EXAMPLES
Unregister a third-party cluster referenced from a specific kubeconfig file:
$ {command} my-membership \
--context=my-cluster-context \
--kubeconfig=/home/user/custom_kubeconfig
Unregister a third-party cluster referenced from the default kubeconfig file:
$ {command} my-membership --context=my-cluster-context
Unregister a GKE cluster referenced from a GKE URI:
$ {command} my-membership \
--gke-uri=my-cluster-gke-uri
Unregister a GKE cluster referenced from a GKE Cluster location and name:
$ {command} my-membership \
--gke-cluster=my-cluster-region-or-zone/my-cluster
Unregister a GKE cluster and uninstall Connect agent:
$ {command} my-membership \
--gke-cluster=my-cluster-region-or-zone/my-cluster
--uninstall-connect-agent
"""
@classmethod
def Args(cls, parser):
resources.AddMembershipResourceArg(
parser,
membership_help=textwrap.dedent("""\
The membership name that you choose to uniquely represent the cluster
being registered in the fleet.
"""),
location_help=textwrap.dedent("""\
The location of the membership resource, e.g. `us-central1`.
If not specified, defaults to `global`.
"""),
membership_required=True,
positional=True)
parser.add_argument(
'--uninstall-connect-agent',
action='store_true',
help=textwrap.dedent("""\
If set to True for a GKE cluster, Connect agent will be uninstalled
from the cluster. No-op for third-party clusters, where Connect agent
will always be uninstalled.
"""),
default=False,
)
hub_util.AddClusterConnectionCommonArgs(parser)
def Run(self, args):
project = arg_utils.GetFromNamespace(args, '--project', use_defaults=True)
gke_cluster_resource_link, gke_cluster_uri = (
gke_util.GetGKEClusterResoureLinkAndURI(
gke_uri=args.GetValue('gke_uri'),
gke_cluster=args.GetValue('gke_cluster'),
)
)
location = 'global'
membership_id = args.MEMBERSHIP_NAME
if resources.MembershipLocationSpecified(args):
membership = resources.MembershipResourceName(args)
membership_id = util.MembershipShortname(membership)
location = util.MembershipLocation(membership)
# If this is a gke cluster and location is ambiguous, then we'll check for
# the existence of a regional membership. Non-gke clusters can only be
# registered as 'global' memberships right now.
elif gke_cluster_resource_link:
cluster_location = hub_util.LocationFromGKEArgs(args)
regional_name = 'projects/{}/locations/{}/memberships/{}'.format(
project, cluster_location, membership_id
)
try:
regional_obj = api_util.GetMembership(
regional_name, self.ReleaseTrack()
)
if (
regional_obj.endpoint
and regional_obj.endpoint.gkeCluster.resourceLink
== gke_cluster_resource_link
):
location = cluster_location
except apitools_exceptions.HttpError:
pass
# Unregister GKE cluster with simple Add-to-Hub API call. Connect agent will
# not be uninstalled. And Kubernetes Client is not needed.
if gke_cluster_resource_link and not args.GetValue(
'uninstall_connect_agent'):
return self._UnregisterGKE(gke_cluster_resource_link, gke_cluster_uri,
project, location, membership_id, args)
# Unregister non-GKE, or GKE with --uninstall-connect-agent.
# It will require a kube client.
kube_client = kube_util.KubernetesClient(
gke_uri=getattr(args, 'gke_uri', None),
gke_cluster=getattr(args, 'gke_cluster', None),
kubeconfig=getattr(args, 'kubeconfig', None),
context=getattr(args, 'context', None),
public_issuer_url=getattr(args, 'public_issuer_url', None),
enable_workload_identity=getattr(args, 'enable_workload_identity',
False),
)
kube_client.CheckClusterAdminPermissions()
kube_util.ValidateClusterIdentifierFlags(kube_client, args)
# Delete membership from Fleet API.
try:
name = 'projects/{}/locations/{}/memberships/{}'.format(
project, location, membership_id)
obj = api_util.GetMembership(name, self.ReleaseTrack())
if not obj.externalId:
console_io.PromptContinue(
'invalid membership {0} does not have '
'external_id field set. We cannot determine '
'if registration is requested against a '
'valid existing Membership. Consult the '
'documentation on container fleet memberships '
'update for more information or run gcloud '
'container fleet memberships delete {0} if you '
'are sure that this is an invalid or '
'otherwise stale Membership'.format(membership_id),
cancel_on_no=True)
uuid = kube_util.GetClusterUUID(kube_client)
if obj.externalId != uuid:
raise exceptions.Error(
'Membership [{}] is not associated with the cluster you are trying'
' to unregister. Please double check the cluster identifier that you'
' have supplied.'.format(name))
api_util.DeleteMembership(name, self.ReleaseTrack())
except apitools_exceptions.HttpUnauthorizedError as e:
raise exceptions.Error(
'You are not authorized to unregister clusters from project [{}]. '
'Underlying error: {}'.format(project, e))
except apitools_exceptions.HttpNotFoundError:
log.status.Print(
'Membership [{}] for the cluster was not found on the fleet. '
'It may already have been deleted, or it may never have existed.'
.format(name))
# Get namespace for the connect resource label.
selector = '{}={}'.format(agent_util.CONNECT_RESOURCE_LABEL, project)
namespaces = kube_client.NamespacesWithLabelSelector(selector)
if not namespaces:
log.status.Print('There\'s no namespace for the label [{}]. '
'If [gke-connect] is labeled with another project, '
'You\'ll have to manually delete the namespace. '
'You can find all namespaces by running:\n'
' `kubectl get ns -l {}`'.format(
agent_util.CONNECT_RESOURCE_LABEL,
agent_util.CONNECT_RESOURCE_LABEL))
# Delete in-cluster membership resources.
try:
parent = api_util.ParentRef(project, location)
cr_manifest = kube_client.GetMembershipCR()
res = api_util.ValidateExclusivity(cr_manifest, parent, membership_id,
self.ReleaseTrack())
if res.status.code:
console_io.PromptContinue(
'Error validating cluster\'s exclusivity state with the Fleet under '
'parent collection [{}]: {}. The cluster you are trying to unregister'
' is not associated with the membership [{}]. Continuing will delete'
' membership related resources from your cluster, and the cluster'
' will lose its association to the Fleet in project [{}] and can be'
' registered into a different project. '.format(
parent, res.status.message, membership_id, project),
cancel_on_no=True)
exclusivity_util.DeleteMembershipResources(kube_client)
except exceptions.Error as e:
log.status.Print(
'{} error in deleting in-cluster membership resources. '
'You can manually delete these membership related '
'resources from your cluster by running the command:\n'
' `kubectl delete memberships membership`.\nBy doing so, '
'the cluster will lose its association to the Fleet in '
'project [{}] and can be registered into a different '
'project. '.format(e, project))
# Delete the connect agent.
agent_util.DeleteConnectNamespace(kube_client, args)
def _UnregisterGKE(self, gke_cluster_resource_link, gke_cluster_uri, project,
location, membership_id, args):
"""Register a GKE cluster without installing Connect agent."""
try:
name = 'projects/{}/locations/{}/memberships/{}'.format(
project, location, membership_id)
obj = api_util.GetMembership(name, self.ReleaseTrack())
if obj.endpoint.gkeCluster.resourceLink != gke_cluster_resource_link:
raise exceptions.Error(
'membership [{0}] is associated with a different GKE cluster link '
'{1}. You may be unregistering the wrong membership.'.format(
name, obj.endpoint.gkeCluster.resourceLink))
api_util.DeleteMembership(name, self.ReleaseTrack())
except apitools_exceptions.HttpUnauthorizedError as e:
raise exceptions.Error(
'You are not authorized to unregister clusters from project [{}]. '
'Underlying error: {}'.format(project, e))
except apitools_exceptions.HttpNotFoundError:
log.status.Print(
'Membership [{}] for the cluster was not found on the fleet. '
'It may already have been deleted, or it may never have existed.'
.format(name))