feat: Add new gcloud commands, API clients, and third-party libraries across various services.

This commit is contained in:
2026-01-01 20:26:35 +01:00
parent 5e23cbece0
commit a19e592eb7
25221 changed files with 8324611 additions and 0 deletions

View File

@@ -0,0 +1,30 @@
# -*- coding: utf-8 -*- #
# Copyright 2021 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.
"""Command group for Fleet memberships."""
from __future__ import absolute_import
from __future__ import division
from __future__ import unicode_literals
from googlecloudsdk.calliope import base as calliope_base
@calliope_base.ReleaseTracks(calliope_base.ReleaseTrack.ALPHA,
calliope_base.ReleaseTrack.BETA,
calliope_base.ReleaseTrack.GA)
class Memberships(calliope_base.Group):
"""Manage memberships of all your GKE and other Kubernetes clusters with fleets."""
category = calliope_base.COMPUTE_CATEGORY

View File

@@ -0,0 +1,37 @@
# -*- 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.
"""Command group for Membership-Bindings."""
from __future__ import absolute_import
from __future__ import division
from __future__ import unicode_literals
from googlecloudsdk.calliope import base
class Membership(base.Group):
"""Membership Bindings for permissions.
This command group allows for manipulation of bindings associated with
membership-scope.
## EXAMPLES
Manage Bindings:
$ {command} --help
"""
category = base.COMPUTE_CATEGORY

View File

@@ -0,0 +1,88 @@
# -*- 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.
"""Command to create a Membership Binding."""
from __future__ import absolute_import
from __future__ import division
from __future__ import unicode_literals
from googlecloudsdk.api_lib.container.fleet import client
from googlecloudsdk.api_lib.container.fleet import util
from googlecloudsdk.calliope import base
from googlecloudsdk.command_lib.container.fleet import resources
from googlecloudsdk.command_lib.util.args import labels_util
class Create(base.CreateCommand):
"""Create a Membership Binding.
This command can fail for the following reasons:
* The Membership Binding already exists.
* The caller does not have permission to access the given Membership.
* The Scope or the Membership does not exist.
* The caller did not specify the location (--location) if referring to
location other than global.
## EXAMPLES
To create a membership binding `BINDING_NAME` in global membership
`MEMBERSHIP_NAME` for scope `SCOPE_NAME`, run:
$ {command} BINDING_NAME --membership=MEMBERSHIP_NAME --scope=SCOPE_NAME
To create a Binding `BINDING_NAME` associated with regional membership
`MEMBERSHIP_NAME`, provide the location LOCATION_NAME for the Membership where
the Binding belongs along with membership name and associated
Scope `SCOPE_NAME`.
$ {command} BINDING_NAME --membership=MEMBERSHIP_NAME --scope=SCOPE_NAME
--location=LOCATION_NAME
"""
@classmethod
def Args(cls, parser):
resources.AddMembershipBindingResourceArg(
parser,
api_version=util.VERSION_MAP[cls.ReleaseTrack()],
binding_help=(
'Name of the membership Binding to be created.'
'Must comply with RFC 1123 (up to 63 characters, '
"alphanumeric and '-')"
),
)
group = parser.add_mutually_exclusive_group(required=True)
resources.AddScopeResourceArg(
parser,
flag_name='--scope',
api_version=util.VERSION_MAP[cls.ReleaseTrack()],
scope_help='The Fleet Scope to bind the membership to.',
group=group,
)
labels_util.AddCreateLabelsFlags(parser)
def Run(self, args):
fleetclient = client.FleetClient(release_track=self.ReleaseTrack())
scope = None
if args.CONCEPTS.scope.Parse() is not None:
scope = args.CONCEPTS.scope.Parse().RelativeName()
labels_diff = labels_util.Diff(additions=args.labels)
labels = labels_diff.Apply(
fleetclient.messages.MembershipBinding.LabelsValue, None
).GetOrNone()
return fleetclient.CreateMembershipBinding(
resources.MembershipBindingResourceName(args),
scope=scope,
labels=labels,
)

View File

@@ -0,0 +1,63 @@
# -*- 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.
"""Command to delete a Membership Binding."""
from __future__ import absolute_import
from __future__ import division
from __future__ import unicode_literals
from googlecloudsdk.api_lib.container.fleet import client
from googlecloudsdk.api_lib.container.fleet import util
from googlecloudsdk.calliope import base
from googlecloudsdk.command_lib.container.fleet import resources
class Delete(base.DeleteCommand):
"""Delete a Membership Binding.
This command can fail for the following reasons:
* The Membership specified does not exist.
* The Membership Binding specified does not exist.
* The caller does not have permission to access the given Membership.
* The caller did not specify the location (--location) if referring to
location other than global.
## EXAMPLES
To delete Membership Binding `BINDING_NAME` in global Membership
`MEMBERSHIP_NAME` for a global membership:
$ {command} BINDING_NAME --membership=MEMBERSHIP_NAME
To delete a Binding `BINDING_NAME` associated with regional membership
`MEMBERSHIP_NAME`, provide the location LOCATION_NAME for the Membership where
the Binding belongs along with the membership name.
$ {command} BINDING_NAME --membership=MEMBERSHIP_NAME --location=LOCATION_NAME
"""
@classmethod
def Args(cls, parser):
resources.AddMembershipBindingResourceArg(
parser,
api_version=util.VERSION_MAP[cls.ReleaseTrack()],
binding_help=('Name of the Membership Binding to be deleted.'
'Must comply with RFC 1123 (up to 63 characters, '
'alphanumeric and \'-\')'))
def Run(self, args):
fleetclient = client.FleetClient(release_track=self.ReleaseTrack())
return fleetclient.DeleteMembershipBinding(
resources.MembershipBindingResourceName(args))

View File

@@ -0,0 +1,63 @@
# -*- 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.
"""Command to show fleet information."""
from __future__ import absolute_import
from __future__ import division
from __future__ import unicode_literals
from googlecloudsdk.api_lib.container.fleet import client
from googlecloudsdk.api_lib.container.fleet import util
from googlecloudsdk.calliope import base
from googlecloudsdk.command_lib.container.fleet import resources
class Describe(base.DescribeCommand):
"""Show Membership-Binding info.
This command can fail for the following reasons:
* The Membership specified does not exist.
* The Membership Binding specified does not exist in the project.
* The caller does not have permission to access the Membership Binding.
* The caller did not specify the location (--location) if referring to
location other than global.
## EXAMPLES
To print metadata for the membership Binding `BINDING_NAME` in a global
membership `MEMBERSHIP_NAME`, run:
$ {command} BINDING_NAME --membership=MEMBERSHIP_NAME
To print metadata for the Binding `BINDING_NAME` associated with regional
membership `MEMBERSHIP_NAME`, provide the location LOCATION_NAME for the
Membership where the Binding belongs along with membership name.
$ {command} BINDING_NAME --membership=MEMBERSHIP_NAME --location=LOCATION_NAME
"""
@classmethod
def Args(cls, parser):
resources.AddMembershipBindingResourceArg(
parser,
api_version=util.VERSION_MAP[cls.ReleaseTrack()],
binding_help=('Name of the Membership Binding to be described.'
'Must comply with RFC 1123 (up to 63 characters, '
'alphanumeric and \'-\')'))
def Run(self, args):
fleetclient = client.FleetClient(release_track=self.ReleaseTrack())
return fleetclient.GetMembershipBinding(
resources.MembershipBindingResourceName(args))

View File

@@ -0,0 +1,75 @@
# -*- 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.
"""Command to show bindings in a membership."""
from __future__ import absolute_import
from __future__ import division
from __future__ import unicode_literals
from googlecloudsdk.api_lib.container.fleet import client
from googlecloudsdk.calliope import base
from googlecloudsdk.calliope import exceptions as calliope_exceptions
from googlecloudsdk.command_lib.container.fleet import util
from googlecloudsdk.core import properties
class List(base.ListCommand):
"""List Bindings in a Membership.
This command can fail for the following reasons:
* The Membership specified does not exist.
* The user does not have access to the Membership specified.
* The caller did not specify the location (--location) if referring to
location other than global.
## EXAMPLES
The following command lists Bindings in global Membership `MEMBERSHIP_NAME`:
$ {command} --membership=MEMBERSHIP_NAME
To list all the bindings associated with regional membership
`MEMBERSHIP_NAME`, provide the location LOCATION_NAME for the Membership where
the Binding belongs along with membership name.
$ {command} --membership=MEMBERSHIP_NAME --location=LOCATION_NAME
"""
@staticmethod
def Args(parser):
# Table formatting
parser.display_info.AddFormat(util.B_LIST_FORMAT)
parser.add_argument(
'--membership',
type=str,
required=True,
help='Name of the Membership to list Bindings from.')
parser.add_argument(
'--location',
type=str,
default='global',
help='Name of the Membership location to list Bindings from.')
def Run(self, args):
fleetclient = client.FleetClient(release_track=self.ReleaseTrack())
project = args.project
if project is None:
project = properties.VALUES.core.project.Get()
if args.IsKnownAndSpecified('membership'):
return fleetclient.ListMembershipBindings(project, args.membership,
args.location)
raise calliope_exceptions.RequiredArgumentException(
'membership', 'Membership parent is required.')

View File

@@ -0,0 +1,106 @@
# -*- 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.
"""Command to update Membership Binding information."""
from __future__ import absolute_import
from __future__ import division
from __future__ import unicode_literals
from googlecloudsdk.api_lib.container.fleet import client
from googlecloudsdk.api_lib.container.fleet import util
from googlecloudsdk.calliope import base
from googlecloudsdk.command_lib.container.fleet import resources
from googlecloudsdk.command_lib.util.args import labels_util
class Update(base.UpdateCommand):
"""Update the Binding in a Membership.
This command can fail for the following reasons:
* The Membership specified does not exist.
* The Binding does not exist in the Membership.
* The caller does not have permission to access the Membership/Binding.
* The Scope specified does not exist.
* The caller did not specify the location (--location) if referring to
location other than global.
## EXAMPLES
To update the binding `BINDING_NAME` in global membership `MEMBERSHIP_NAME`
in the active project:
$ {command} BINDING_NAME --membership=MEMBERSHIP_NAME
To update a Binding `BINDING_NAME` associated with regional membership
`MEMBERSHIP_NAME`, provide the location LOCATION_NAME for the Membership where
the Binding belongs along with membership name and associated
Scope `SCOPE_NAME`.
$ {command} BINDING_NAME --membership=MEMBERSHIP_NAME --scope=SCOPE_NAME
--location=LOCATION_NAME
"""
@classmethod
def Args(cls, parser):
resources.AddMembershipBindingResourceArg(
parser,
api_version=util.VERSION_MAP[cls.ReleaseTrack()],
binding_help=(
'Name of the Membership Binding to be updated.'
'Must comply with RFC 1123 (up to 63 characters, '
"alphanumeric and '-')"
),
)
group = parser.add_mutually_exclusive_group(required=True)
resources.AddScopeResourceArg(
parser,
flag_name='--scope',
api_version=util.VERSION_MAP[cls.ReleaseTrack()],
scope_help='The Fleet Scope to bind the membership to.',
group=group,
)
labels_util.AddUpdateLabelsFlags(parser)
def Run(self, args):
fleetclient = client.FleetClient(release_track=self.ReleaseTrack())
mask = []
current_binding = fleetclient.GetMembershipBinding(
resources.MembershipBindingResourceName(args)
)
# update GCP labels for namespace resource
labels_diff = labels_util.Diff.FromUpdateArgs(args)
new_labels = labels_diff.Apply(
fleetclient.messages.MembershipBinding.LabelsValue,
current_binding.labels,
).GetOrNone()
if new_labels:
mask.append('labels')
for flag in ['fleet', 'scope']:
if args.IsKnownAndSpecified(flag):
mask.append(flag)
scope = None
if args.CONCEPTS.scope.Parse() is not None:
scope = args.CONCEPTS.scope.Parse().RelativeName()
# if there's nothing to update, then return
if not mask:
return
return fleetclient.UpdateMembershipBinding(
resources.MembershipBindingResourceName(args),
scope=scope,
labels=new_labels,
mask=','.join(mask))

View File

@@ -0,0 +1,50 @@
- release_tracks: [ALPHA, BETA, GA]
hidden: true
help_text:
brief: Create a new membership for a cluster.
description: |
Create a Fleet Membership resource corresponding to the cluster.
This command does not install the Connect Agent or in-cluster Kubernetes Resources that are
required to connect the cluster with Google. For complete cluster registration, consider using
the command: `{parent_command} register --help`.
examples: |
Create a new membership `MEMBERSHIP_NAME` in the active project's fleet:
$ {command} MEMBERSHIP_NAME
Create a new membership `MEMBERSHIP_NAME` for a GKE cluster:
$ {command} MEMBERSHIP_NAME --gke-cluster-self-link=my-gke-cluster-self-link
request:
collection: gkehub.projects.locations.memberships
ALPHA:
api_version: v1alpha
BETA:
api_version: v1beta
GA:
api_version: v1
async:
collection: gkehub.projects.locations.operations
arguments:
resource:
help_text: The cluster membership to create.
spec: !REF googlecloudsdk.command_lib.container.fleet.resources:membership
params:
- api_field: membership.externalId
arg_name: external-id
help_text: External-id of the membership resource.
- api_field: membership.endpoint.gkeCluster.resourceLink
arg_name: gke-cluster-self-link
help_text: |
GKE cluster self-link of the cluster represented by this membership;
for example,
'//container.googleapis.com/projects/my-project/locations/us-central1-a/clusters/my-cluster.'
This is only valid if the represented cluster is a GKE cluster. The
provided self-link will be validated to confirm that it maps to the
cluster represented by this membership.
labels:
api_field: membership.labels

View File

@@ -0,0 +1,38 @@
- release_tracks: [ALPHA, BETA, GA]
help_text:
brief: Delete a membership.
description: |
This command deletes the Fleet Membership resource corresponding to the cluster.
This command is intended to delete stale Fleet Membership resources as doing so on a fully
registered cluster will skip uninstalling the Connect Agent and and orphan in-cluster
resources and agent connections to Google. To completely unregister the cluster, consider
using the command: `{parent_command} unregister`.
examples: |
First retrieve the ID of the membership using the command below. The output of this command
lists all the fleet's members, with their unique IDs in the Names column:
$ {parent_command} list
Delete a membership from the active project's fleet:
$ {command} MEMBERSHIP_NAME
request:
collection: gkehub.projects.locations.memberships
modify_request_hooks:
- googlecloudsdk.command_lib.container.fleet.memberships.util:SetMembershipLocation
ALPHA:
api_version: v1alpha
BETA:
api_version: v1beta
GA:
api_version: v1
async:
collection: gkehub.projects.locations.operations
arguments:
resource:
help_text: The cluster membership to delete.
spec: !REF googlecloudsdk.command_lib.container.fleet.resources:membership

View File

@@ -0,0 +1,30 @@
- release_tracks: [ALPHA, BETA, GA]
help_text:
brief: Describe a membership.
description: Describe a membership in a fleet.
examples: |
First retrieve the ID of the membership using the command below. The output of this command
lists all the fleet's members, with their unique IDs in the NAME column:
$ {parent_command} list
Then describe it:
$ {command} MEMBERSHIP
request:
collection: gkehub.projects.locations.memberships
modify_request_hooks:
- googlecloudsdk.command_lib.container.fleet.memberships.util:SetMembershipLocation
ALPHA:
api_version: v1alpha
BETA:
api_version: v1beta
GA:
api_version: v1
arguments:
resource:
help_text: The cluster membership to describe.
spec: !REF googlecloudsdk.command_lib.container.fleet.resources:membership

View File

@@ -0,0 +1,372 @@
# -*- 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.
"""Generate RBAC policy files for Connect Gateway users."""
from __future__ import absolute_import
from __future__ import division
from __future__ import print_function
from __future__ import unicode_literals
import sys
import textwrap
from googlecloudsdk.calliope import base
from googlecloudsdk.command_lib.container.fleet import kube_util
from googlecloudsdk.command_lib.container.fleet import rbac_util
from googlecloudsdk.core import exceptions
from googlecloudsdk.core import log
from googlecloudsdk.core import properties
from googlecloudsdk.core.console import console_io
from googlecloudsdk.core.util import files as file_utils
@base.ReleaseTracks(
base.ReleaseTrack.ALPHA, base.ReleaseTrack.BETA, base.ReleaseTrack.GA
)
class GenerateGatewayRbac(base.Command):
# pylint: disable=line-too-long
r"""Generate RBAC policy files for connected clusters by the user.
{command} generates RBAC policies to be used by Connect Gateway API.
Upon success, this command will write the output RBAC policy to the designated
local file in dry run mode.
Override RBAC policy: Y to override previous RBAC policy, N to stop. If
overriding the --role, Y will clean up the previous RBAC policy and then apply
the new one.
## EXAMPLES
The current implementation supports multiple modes:
Dry run mode to generate the RBAC policy file, and write to local directory:
$ {command} --membership=my-cluster
--users=foo@example.com,test-acct@test-project.iam.gserviceaccount.com
--role=clusterrole/cluster-admin --rbac-output-file=./rbac.yaml
Dry run mode to generate the RBAC policy, and print on screen:
$ {command} --membership=my-cluster
--users=foo@example.com,test-acct@test-project.iam.gserviceaccount.com
--role=clusterrole/cluster-admin
Anthos support mode, generate the RBAC policy file with read-only permission
for TSE/Eng to debug customers' clusters:
$ {command} --membership=my-cluster --anthos-support
Apply mode, generate the RBAC policy and apply it to the specified cluster:
$ {command} --membership=my-cluster
--users=foo@example.com,test-acct@test-project.iam.gserviceaccount.com
--role=clusterrole/cluster-admin --context=my-cluster-context
--kubeconfig=/home/user/custom_kubeconfig --apply
Revoke mode, revoke the RBAC policy for the specified users:
$ {command} --membership=my-cluster
--users=foo@example.com,test-acct@test-project.iam.gserviceaccount.com
--role=clusterrole/cluster-admin --context=my-cluster-context
--kubeconfig=/home/user/custom_kubeconfig --revoke
The role to be granted to the users can either be cluster-scoped or
namespace-scoped. To grant a namespace-scoped role to the users in dry run
mode, run:
$ {command} --membership=my-cluster
--users=foo@example.com,test-acct@test-project.iam.gserviceaccount.com
--role=role/mynamespace/namespace-reader
The users provided can be using a Google identity (only email) or using
external identity providers (starting with
"principal://iam.googleapis.com"):
$ {command} --membership=my-cluster
--users=foo@example.com,principal://iam.googleapis.com/locations/global/workforcePools/pool/subject/user
--role=clusterrole/cluster-admin --context=my-cluster-context
--kubeconfig=/home/user/custom_kubeconfig --apply
The groups can be provided as a Google identity (only email) or an
external identity (starting with
"principalSet://iam.googleapis.com"):
$ {command} --membership=my-cluster
--groups=group@example.com,principalSet://iam.googleapis.com/locations/global/workforcePools/pool/group/ExampleGroup
--role=clusterrole/cluster-admin --context=my-cluster-context
--kubeconfig=/home/user/custom_kubeconfig --apply
"""
@classmethod
def Args(cls, parser):
parser.add_argument(
'--membership',
type=str,
help=textwrap.dedent("""\
Membership name to assign RBAC policy with.
"""),
)
parser.add_argument(
'--role',
type=str,
help=textwrap.dedent("""\
Namespace scoped role or cluster role.
"""),
)
parser.add_argument(
'--rbac-output-file',
type=str,
help=textwrap.dedent("""\
If specified, this command will execute in dry run mode and write to
the file specified with this flag: the generated RBAC policy will not
be applied to Kubernetes clusters,instead it will be written to the
designated local file.
"""),
)
parser.add_argument(
'--apply',
action='store_true',
help=textwrap.dedent("""\
If specified, this command will generate RBAC policy and apply to the
specified cluster.
"""),
)
parser.add_argument(
'--context',
type=str,
help=textwrap.dedent("""\
The cluster context as it appears in the kubeconfig file. You can get
this value from the command line by running command:
`kubectl config current-context`.
"""),
)
parser.add_argument(
'--kubeconfig',
type=str,
help=textwrap.dedent("""\
The kubeconfig file containing an entry for the cluster. Defaults to
$KUBECONFIG if it is set in the environment, otherwise defaults to
$HOME/.kube/config.
"""),
)
parser.add_argument(
'--revoke',
action='store_true',
help=textwrap.dedent("""\
If specified, this command will revoke the RBAC policy for the
specified users.
"""),
)
identities = parser.add_mutually_exclusive_group(required=True)
identities.add_argument(
'--groups',
type=str,
help=textwrap.dedent("""\
Group email address or third-party IAM group principal.
"""),
)
identities.add_argument(
'--users',
type=str,
help=textwrap.dedent("""\
User's email address, service account email address, or third-party IAM subject principal.
"""),
)
identities.add_argument(
'--anthos-support',
action='store_true',
help=textwrap.dedent("""\
If specified, this command will generate RBAC policy
file for anthos support.
"""),
)
def Run(self, args):
log.status.Print('Validating input arguments.')
project_id = properties.VALUES.core.project.GetOrFail()
# Validate the args value before generate the RBAC policy file.
rbac_util.ValidateArgs(args)
# Revoke RBAC policy for specified user from cluster.
if args.revoke:
sys.stdout.write(
'Revoking the RBAC policy from cluster with kubeconfig: {}, context:'
' {}\n'.format(args.kubeconfig, args.context)
)
with kube_util.KubernetesClient(
kubeconfig=getattr(args, 'kubeconfig', None),
context=getattr(args, 'context', None),
) as kube_client:
# Check Admin permissions.
kube_client.CheckClusterAdminPermissions()
identities_list = list()
if args.users:
identities_list = [(user, True) for user in args.users.split(',')]
elif args.anthos_support:
identities_list.append(
(rbac_util.GetAnthosSupportUser(project_id), True)
)
elif args.groups:
identities_list = [(group, False) for group in args.groups.split(',')]
for identity, is_user in identities_list:
message = 'The RBAC policy for {} will be cleaned up.'.format(
identity
)
console_io.PromptContinue(message=message, cancel_on_no=True)
log.status.Print('--------------------------------------------')
log.status.Print(
'Start cleaning up RBAC policy for: {}'.format(identity)
)
rbac = kube_client.GetRBACForOperations(
args.membership,
args.role,
project_id,
identity,
is_user,
args.anthos_support,
)
if kube_client.CleanUpRbacPolicy(rbac):
log.status.Print(
'Finished cleaning up the previous RBAC policy for: {}'.format(
identity
)
)
return
# Generate the RBAC policy file from args.
generated_rbac = rbac_util.GenerateRBAC(args, project_id)
if args.rbac_output_file:
sys.stdout.write(
'Generated RBAC policy is written to file: {} \n'.format(
args.rbac_output_file
)
)
else:
sys.stdout.write('Generated RBAC policy is: \n')
sys.stdout.write('--------------------------------------------\n')
# Write the generated RBAC policy file to the file provided with
# "--rbac-output-file" specified or print on the screen.
final_rbac_policy = ''
for user in sorted(generated_rbac.keys()):
final_rbac_policy += generated_rbac.get(user)
log.WriteToFileOrStdout(
args.rbac_output_file if args.rbac_output_file else '-',
final_rbac_policy,
overwrite=True,
binary=False,
private=True,
)
# Apply generated RBAC policy to cluster.
if args.apply:
sys.stdout.write(
'Applying the generate RBAC policy to cluster with kubeconfig: {},'
' context: {}\n'.format(args.kubeconfig, args.context)
)
with kube_util.KubernetesClient(
kubeconfig=getattr(args, 'kubeconfig', None),
context=getattr(args, 'context', None),
) as kube_client:
# Check Admin permissions.
kube_client.CheckClusterAdminPermissions()
for identity, is_user in generated_rbac.keys():
with file_utils.TemporaryDirectory() as tmp_dir:
file = tmp_dir + '/rbac.yaml'
current_rbac_policy = generated_rbac.get((identity, is_user))
file_utils.WriteFileContents(file, current_rbac_policy)
rbac = kube_client.GetRBACForOperations(
args.membership,
args.role,
project_id,
identity,
is_user,
args.anthos_support,)
# Check whether there are existing RBAC policy for this user, if not,
# will directly apply the new RBAC policy.
if not kube_client.GetRbacPolicy(rbac):
# Check whether there are role confliction, which required clean up.
need_clean_up = False
# Override when proposed RBAC policy has diff with existing one.
override_check = False
# Checking RBAC policy diff, return None, None if there are no diff.
diff, err = kube_client.GetRbacPolicyDiff(file)
if diff is not None:
override_check = True
log.status.Print(
'The new RBAC policy has diff with previous: \n {}'.format(
diff
)
)
if err is not None:
# 'Invalid value' means the clusterrole/role permission has been
# changed. This need to clean up old RBAC policy and then apply
# the new one.
if 'Invalid value' in err:
rbac_policy_name = kube_client.RbacPolicyName(
'permission', project_id, args.membership, identity
)
rbac_permission_policy = kube_client.GetRbacPermissionPolicy(
rbac_policy_name, args.role
)
log.status.Print(
'The existing RBAC policy has conflicts with proposed'
' one:\n{}'.format(rbac_permission_policy)
)
need_clean_up = True
override_check = True
else:
raise exceptions.Error(
'Error when getting diff for RBAC policy files for:'
' {}, with error: {}'.format(identity, err)
)
if override_check:
message = 'The RBAC file will be overridden.'
console_io.PromptContinue(message=message, cancel_on_no=True)
if need_clean_up:
log.status.Print('--------------------------------------------')
log.status.Print(
'Start cleaning up previous RBAC policy for: {}'.format(
identity
)
)
if kube_client.CleanUpRbacPolicy(rbac):
log.status.Print(
'Finished cleaning up the previous RBAC policy for: {}'
.format(identity)
)
try:
log.status.Print(
'Writing RBAC policy for user: {} to cluster.'.format(identity)
)
kube_client.ApplyRbacPolicy(file)
except Exception as e:
log.status.Print(
'Error in applying the RBAC policy to cluster: {}'.format(e)
)
raise
log.status.Print('Successfully applied the RBAC policy to cluster.')

View File

@@ -0,0 +1,103 @@
# -*- 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.
"""Fetch Hub-registered cluster credentials for Connect Gateway."""
from __future__ import absolute_import
from __future__ import division
from __future__ import unicode_literals
import textwrap
from googlecloudsdk.api_lib.container.fleet import util as fleet_util
from googlecloudsdk.command_lib.container.fleet import gateway
from googlecloudsdk.command_lib.container.fleet import resources
class GetCredentials(gateway.GetCredentialsCommand):
"""Fetch credentials for a fleet-registered cluster to be used in Connect Gateway.
{command} updates the `kubeconfig` file with the appropriate credentials and
endpoint information to send `kubectl` commands to a fleet-registered and
-connected cluster through the Connect Gateway service.
It takes a project, passed through by set defaults or flags. 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.
Upon success, this command will switch the current context to the target
cluster if other contexts are already present in the `kubeconfig` file.
## EXAMPLES
Get the Gateway kubeconfig for a globally registered cluster:
$ {command} my-cluster
$ {command} my-cluster --location=global
Get the Gateway kubeconfig for a cluster registered in us-central1:
$ {command} my-cluster --location=us-central1
"""
@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, attempts to automatically choose the correct region.
"""),
membership_required=True,
positional=True,
)
# TODO(b/368039642): Remove once we're sure server-side generation is stable
parser.add_argument(
'--use-client-side-generation',
action='store_true',
required=False,
hidden=True,
help=textwrap.dedent("""\
Generate the kubeconfig locally rather than generating
it using an API call.
"""),
)
parser.add_argument(
'--force-use-agent',
action='store_true',
required=False,
hidden=True,
help=textwrap.dedent("""\
Force the use of Connect Agent-based transport.
"""),
)
def Run(self, args):
membership_name = resources.ParseMembershipArg(args)
location = fleet_util.MembershipLocation(membership_name)
membership_id = fleet_util.MembershipShortname(membership_name)
if args.use_client_side_generation:
self.RunGetCredentials(membership_id, location)
else:
self.RunServerSide(
membership_id, location, force_use_agent=args.force_use_agent
)

View File

@@ -0,0 +1,37 @@
- release_tracks: [ALPHA, BETA, GA]
help_text:
brief: List memberships.
description: List memberships in a fleet.
examples: |
List memberships in the active project's fleet:
$ {command}
request:
collection: gkehub.projects.locations.memberships
modify_request_hooks:
- googlecloudsdk.command_lib.container.fleet.memberships.util:SetParentCollection
ALPHA:
api_version: v1alpha
BETA:
api_version: v1beta
GA:
api_version: v1
arguments:
resource:
help_text: The project and location to list cluster memberships for.
spec: !REF googlecloudsdk.command_lib.container.fleet.resources:locationDefaultToAll
override_resource_collection: true
response:
id_field: name
output:
format: |
table(
name.basename():label=NAME,
unique_id:label=UNIQUE_ID,
name.scope().segment(-3):label=LOCATION
)

View File

@@ -0,0 +1,757 @@
# -*- 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 register command for registering a clusters with the fleet."""
from __future__ import absolute_import
from __future__ import division
from __future__ import unicode_literals
import json
import textwrap
from apitools.base.py import exceptions as apitools_exceptions
from googlecloudsdk.api_lib.container import api_adapter as gke_api_adapter
from googlecloudsdk.api_lib.util import exceptions as core_api_exceptions
from googlecloudsdk.calliope import base
from googlecloudsdk.command_lib.container import flags
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
from googlecloudsdk.core.util import files
import six
SERVICE_ACCOUNT_KEY_FILE_FLAG = '--service-account-key-file'
DOCKER_CREDENTIAL_FILE_FLAG = '--docker-credential-file'
def _ValidateConnectAgentCredentialFlags(args):
enable_workload_identity = getattr(args, 'enable_workload_identity', False)
if not args.service_account_key_file and not enable_workload_identity:
raise exceptions.Error(
'--enable-workload-identity --service-account-key-file',
'One of (--enable-workload-identity | --service-account-key-file) ' +
'must be specified for Connect agent authentication.')
@base.DefaultUniverseOnly
class Register(base.CreateCommand):
r"""Register a cluster with a fleet.
This command registers a cluster with the fleet by:
1. Creating a Fleet Membership resource corresponding to the cluster.
2. Adding in-cluster Kubernetes Resources that make the cluster exclusive
to one fleet.
3. Installing the Connect agent into this cluster (optional for GKE).
A successful registration implies that the cluster is now exclusive to a
single Fleet. If the cluster is already registered to another Fleet, the
registration will not be successful.
To register a GKE cluster, use `--gke-cluster` or `--gke-uri` flag (no
`--kubeconfig` flag is required). Connect agent will not be installed by
default for GKE clusters. To install it, specify `--install-connect-agent`.
The default value for `--location` is the same as the cluster's region or zone,
can be specified as `global`.
Anthos clusters on VMware, bare metal, AWS, and Azure are registered
with a fleet when the clusters are created. To register Amazon EKS
clusters, see
[Attach your EKS cluster](https://cloud.google.com/anthos/clusters/docs/multi-cloud/attached/eks/how-to/attach-cluster).
To regiser Microsoft Azure clusters, see
[Attach your AKS cluster](https://cloud.google.com/anthos/clusters/docs/multi-cloud/attached/aks/how-to/attach-cluster).
To register a third-party cluster, use --context flag (with an optional
--kubeconfig flag). Connect agent will always be installed for these
clusters.
If Connect agent is to be installed, its authentication needs to be configured
by `--enable-workload-identity` or `--service-account-key-file`. For the
latter case, the corresponding service account must have been granted
`gkehub.connect` permissions. For more information about Connect agent, go to:
https://cloud.google.com/anthos/multicluster-management/connect/overview/
Rerunning this command against the same cluster with the same MEMBERSHIP_NAME
and target fleet is successful, and will upgrade the Connect agent if it is
supposed to be installed and a newer version is available. Rerunning with
`--enable-workload-identity` ensures that Workload Identity is enabled on the
cluster.
## EXAMPLES
Register a non-GKE cluster referenced from a specific
kubeconfig file, and install the Connect agent:
$ {command} my-cluster \
--context=my-cluster-context \
--kubeconfig=/home/user/custom_kubeconfig \
--service-account-key-file=/tmp/keyfile.json
Register a non-GKE cluster referenced from the default
kubeconfig file, and install the Connect agent:
$ {command} my-cluster \
--context=my-cluster-context \
--service-account-key-file=/tmp/keyfile.json
Register a non-GKE cluster, and install a specific version
of the Connect agent:
$ {command} my-cluster \
--context=my-cluster-context \
--version=gkeconnect_20190802_02_00 \
--service-account-key-file=/tmp/keyfile.json
Register a non-GKE cluster and output a manifest that can be used to
install the Connect agent by kubectl:
$ {command} my-cluster \
--context=my-cluster-context \
--manifest-output-file=/tmp/manifest.yaml \
--service-account-key-file=/tmp/keyfile.json
Register a GKE cluster referenced from a GKE URI:
$ {command} my-cluster \
--gke-uri=my-cluster-gke-uri
Register a GKE cluster referenced from a GKE URI, and install the Connect
agent using service account key file:
$ {command} my-cluster \
--gke-uri=my-cluster-gke-uri \
--install-connect-agent \
--service-account-key-file=/tmp/keyfile.json
Register a GKE cluster and output a manifest that can be used to
install the Connect agent by kubectl:
$ {command} my-cluster \
--gke-uri=my-cluster-gke-uri \
--enable-workload-identity \
--install-connect-agent \
--manifest-output-file=/tmp/manifest.yaml
Register a GKE cluster first, and install the Connect agent later.
$ {command} my-cluster \
--gke-cluster=my-cluster-region-or-zone/my-cluster
$ {command} my-cluster \
--gke-cluster=my-cluster-region-or-zone/my-cluster \
--install-connect-agent \
--enable-workload-identity
Register a GKE cluster, and install a specific version of the Connect
agent:
$ {command} my-cluster \
--gke-cluster=my-cluster-region-or-zone/my-cluster \
--install-connect-agent \
--version=20220819-00-00 \
--service-account-key-file=/tmp/keyfile.json
Register a GKE cluster and output a manifest that can be used to install the
Connect agent:
$ {command} my-cluster \
--gke-uri=my-cluster-gke-uri \
--install-connect-agent \
--manifest-output-file=/tmp/manifest.yaml \
--service-account-key-file=/tmp/keyfile.json
"""
@classmethod
def Args(cls, parser):
resources.AddMembershipResourceArg(
parser,
membership_help=textwrap.dedent("""\
The membership name that you choose to uniquely represents the cluster
being registered in the fleet.
"""),
location_help=textwrap.dedent("""\
The location for the membership resource, e.g. `us-central1`.
If not specified, defaults to `global`. Not supported for GKE clusters,
whose membership location will be the location of the cluster.
"""),
membership_required=True,
positional=True)
hub_util.AddClusterConnectionCommonArgs(parser)
parser.add_argument(
'--install-connect-agent',
action='store_true',
help=textwrap.dedent("""\
If set to True for a GKE cluster, Connect agent will be installed in
the cluster. No-op for Non-GKE clusters, where Connect agent will
always be installed.
"""),
default=False,
)
parser.add_argument(
'--manifest-output-file',
type=str,
help=textwrap.dedent("""\
The full path of the file into which the Connect agent installation
manifest should be stored. If this option is provided, then the
manifest will be written to this file and will not be deployed into
the cluster by gcloud, and it will need to be deployed manually.
"""),
)
parser.add_argument(
'--proxy',
type=str,
help=textwrap.dedent("""\
The proxy address in the format of http[s]://{hostname}. The proxy
must support the HTTP CONNECT method in order for this connection to
succeed.
"""),
)
parser.add_argument(
'--version',
type=str,
hidden=True,
help=textwrap.dedent("""\
The version of the Connect agent to install/upgrade if not using the
latest connect version.
"""),
)
parser.add_argument(
DOCKER_CREDENTIAL_FILE_FLAG,
type=str,
hidden=True,
help=textwrap.dedent("""\
The credentials to be used if a private registry is provided and auth
is required. The contents of the file will be stored into a Secret and
referenced from the imagePullSecrets of the Connect agent workload.
"""),
)
parser.add_argument(
'--docker-registry',
type=str,
hidden=True,
help=textwrap.dedent("""\
The registry to pull GKE Connect agent image if not using gcr.io/gkeconnect.
"""),
)
parser.add_argument(
'--internal-ip',
help='Whether to use the internal IP address of the cluster endpoint.',
action='store_true')
if cls.ReleaseTrack() is not base.ReleaseTrack.GA:
parser.add_argument(
'--cross-connect-subnetwork',
hidden=True,
help='full path of cross connect subnet whose endpoint to persist')
parser.add_argument(
'--private-endpoint-fqdn',
help='whether to persist the private fqdn',
hidden=True,
default=None,
action='store_true')
credentials = parser.add_mutually_exclusive_group()
credentials.add_argument(
SERVICE_ACCOUNT_KEY_FILE_FLAG,
type=str,
help=textwrap.dedent("""\
The JSON file of a Google Cloud service account private key. This
service account key is stored as a secret named ``creds-gcp'' in
gke-connect namespace. To update the ``creds-gcp'' secret in
gke-connect namespace with a new service account key file, run the
following command:
kubectl delete secret creds-gcp -n gke-connect
kubectl create secret generic creds-gcp -n gke-connect --from-file=creds-gcp.json=/path/to/file
"""),
)
# Optional groups with required arguments are "modal,"
# meaning that if any of the required arguments is specified,
# all are required.
workload_identity = credentials.add_group(help='Workload Identity')
workload_identity.add_argument(
'--enable-workload-identity',
required=True,
action='store_true',
help=textwrap.dedent("""\
Enable Workload Identity when registering the cluster with a fleet.
Ensure that GKE Workload Identity is enabled on your GKE cluster, it
is a requirement for using Workload Identity with memberships. Refer
to the `Enable GKE Workload Identity` section in
https://cloud.google.com/kubernetes-engine/docs/how-to/workload-identity#enable
--service_account_key_file flag should not be set if this is set.
"""),
)
workload_identity_mutex = workload_identity.add_group(mutex=True)
workload_identity_mutex.add_argument(
'--public-issuer-url',
type=str,
help=textwrap.dedent("""\
Skip auto-discovery and register the cluster with this issuer URL.
Use this option when the OpenID Provider Configuration and associated
JSON Web Key Set for validating the cluster's service account JWTs
are served at a public endpoint different from the cluster API server.
Requires --enable-workload-identity.
"""),
)
workload_identity_mutex.add_argument(
'--has-private-issuer',
action='store_true',
help=textwrap.dedent("""\
Set to true for clusters where no publicly-routable OIDC discovery
endpoint for the Kubernetes service account token issuer exists.
When set to true, the gcloud command-line tool will read the
private issuer URL and JSON Web Key Set (JWKS) (public keys) for
validating service account tokens from the cluster's API server
and upload both when creating the Membership. Google Cloud Platform
will then use the JWKS, instead of a public OIDC endpoint,
to validate service account tokens issued by this cluster.
Note the JWKS establishes the uniqueness of issuers in this
configuration, but issuer claims in tokens are still compared to the
issuer URL associated with the Membership when validating tokens.
Note the cluster's OIDC discovery endpoints
(https://[KUBE-API-ADDRESS]/.well-known/openid-configuration and
https://[KUBE-API-ADDRESS]/openid/v1/jwks) must still be
network-accessible to the gcloud client running this command.
"""),
)
def Run(self, args):
project = arg_utils.GetFromNamespace(args, '--project', use_defaults=True)
# This incidentally verifies that the kubeconfig and context args are valid.
if self.ReleaseTrack() is base.ReleaseTrack.BETA or self.ReleaseTrack(
) is base.ReleaseTrack.ALPHA:
api_adapter = gke_api_adapter.NewAPIAdapter('v1beta1')
else:
api_adapter = gke_api_adapter.NewAPIAdapter('v1')
location = 'global'
# Allow attempting to override location for register
# e.g. in case of global GKE cluster memberships
if args.location:
location = args.location
elif hub_util.LocationFromGKEArgs(args):
location = hub_util.LocationFromGKEArgs(args)
gke_cluster_resource_link, gke_cluster_uri = gke_util.GetGKEClusterResoureLinkAndURI(
gke_uri=args.GetValue('gke_uri'),
gke_cluster=args.GetValue('gke_cluster'))
manifest_path = args.GetValue('manifest_output_file')
if (
gke_cluster_resource_link
and manifest_path
and not args.GetValue('install_connect_agent')
):
raise exceptions.Error(
'For GKE clusters, "manifest-output-file" should be specified'
' together with "install-connect-agent". '
)
# Register GKE cluster with simple Add-to-Hub API call. Connect agent will
# not get installed. And Kubernetes Client is not needed.
if gke_cluster_resource_link and not args.GetValue('install_connect_agent'):
return self._RegisterGKEWithoutConnectAgent(
gke_cluster_resource_link, gke_cluster_uri, project, location, args
)
# Register non-GKE cluster, or GKE with --install-connect-agent.
# It will require a kube client.
_ValidateConnectAgentCredentialFlags(args)
with kube_util.KubernetesClient(
api_adapter=api_adapter,
gke_uri=getattr(args, 'gke_uri', None),
gke_cluster=getattr(args, 'gke_cluster', None),
kubeconfig=getattr(args, 'kubeconfig', None),
internal_ip=getattr(args, 'internal_ip', False),
cross_connect_subnetwork=getattr(args, 'cross_connect_subnetwork',
None),
private_endpoint_fqdn=getattr(args, 'private_endpoint_fqdn', None),
context=getattr(args, 'context', None),
public_issuer_url=getattr(args, 'public_issuer_url', None),
enable_workload_identity=getattr(args, 'enable_workload_identity',
False),
) as kube_client:
kube_client.CheckClusterAdminPermissions()
kube_util.ValidateClusterIdentifierFlags(kube_client, args)
if self.ReleaseTrack() is not base.ReleaseTrack.GA:
flags.VerifyGetCredentialsFlags(args)
uuid = kube_util.GetClusterUUID(kube_client)
# Read the service account files provided in the arguments early, in order
# to catch invalid files before performing mutating operations.
# Service Account key file is required if Workload Identity is not
# enabled.
# If Workload Identity is enabled, then the Connect agent uses
# a Kubernetes Service Account token instead and hence a Google Cloud
# Platform Service Account key is not required.
service_account_key_data = ''
if args.service_account_key_file:
try:
service_account_key_data = hub_util.Base64EncodedFileContents(
args.service_account_key_file)
except files.Error as e:
raise exceptions.Error('Could not process {}: {}'.format(
SERVICE_ACCOUNT_KEY_FILE_FLAG, e))
docker_credential_data = None
if args.docker_credential_file:
try:
file_content = files.ReadBinaryFileContents(
files.ExpandHomeDir(args.docker_credential_file))
docker_credential_data = six.ensure_str(
file_content, encoding='utf-8')
except files.Error as e:
raise exceptions.Error('Could not process {}: {}'.format(
DOCKER_CREDENTIAL_FILE_FLAG, e))
gke_cluster_self_link = kube_client.processor.gke_cluster_self_link
issuer_url = None
private_keyset_json = None
if args.enable_workload_identity:
# public_issuer_url can be None or given by user or gke_cluster_uri
# (incase of a gke cluster).
# args.public_issuer_url takes precedence over gke_cluster_uri.
public_issuer_url = (
args.public_issuer_url
or kube_client.processor.gke_cluster_uri
or None
)
try:
openid_config_json = six.ensure_str(
kube_client.GetOpenIDConfiguration(issuer_url=public_issuer_url),
encoding='utf-8')
except Exception as e: # pylint: disable=broad-except
raise exceptions.Error(
'Error getting the OpenID Provider Configuration: '
'{}'.format(e))
# Extract the issuer URL from the discovery doc.
issuer_url = json.loads(openid_config_json).get('issuer')
if not issuer_url:
raise exceptions.Error(
'Invalid OpenID Config: '
'missing issuer: {}'.format(openid_config_json))
# Ensure public_issuer_url (only non-empty) matches what came back in
# the discovery doc.
if public_issuer_url and (public_issuer_url != issuer_url):
raise exceptions.Error('--public-issuer-url {} did not match issuer '
'returned in discovery doc: {}'.format(
public_issuer_url, issuer_url))
# In the private issuer case, we set private_keyset_json,
# which is used later to upload the JWKS
# in the Fleet Membership.
if args.has_private_issuer:
private_keyset_json = kube_client.GetOpenIDKeyset()
# Attempt to create a membership.
already_exists = False
# For backward compatiblity, check if a membership was previously created
# using the cluster uuid.
parent = api_util.ParentRef(project, location)
resource_name = api_util.MembershipRef(project, location, uuid)
obj = self._CheckMembershipWithUUID(resource_name, args.MEMBERSHIP_NAME)
# get api version version to pass into create/update membership
api_server_version = kube_util.GetClusterServerVersion(kube_client)
if obj:
# The membership exists and has the same description.
already_exists = True
else:
# Attempt to create a new membership using MEMBERSHIP_NAME.
membership_id = args.MEMBERSHIP_NAME
resource_name = api_util.MembershipRef(project, location,
args.MEMBERSHIP_NAME)
try:
self._VerifyClusterExclusivity(kube_client, parent, membership_id)
obj = api_util.CreateMembership(project, args.MEMBERSHIP_NAME,
location, gke_cluster_self_link, uuid,
self.ReleaseTrack(), issuer_url,
private_keyset_json,
api_server_version)
# Generate CRD Manifest should only be called afer create/update.
self._InstallOrUpdateExclusivityArtifacts(kube_client, resource_name)
except apitools_exceptions.HttpConflictError as e:
# If the error is not due to the object already existing, re-raise.
error = core_api_exceptions.HttpErrorPayload(e)
if error.status_description != 'ALREADY_EXISTS':
raise
obj = api_util.GetMembership(resource_name, self.ReleaseTrack())
if not obj.externalId:
raise exceptions.Error(
'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))
if obj.externalId != uuid:
raise exceptions.Error(
'membership {0} already exists in the project'
' with another cluster. If this operation is'
' intended, please run `gcloud container '
'fleet memberships delete {0}` and register '
'again.'.format(membership_id))
# The membership exists with same MEMBERSHIP_NAME.
already_exists = True
# In case of an existing membership, check with the user to upgrade the
# Connect-Agent.
if already_exists:
# Update Membership when required. Scenarios that require updates:
# 1. membership.authority is set, but there is now no issuer URL.
# This means the user is disabling Workload Identity.
# 2. membership.authority is not set, but there is now an
# issuer URL. This means the user is enabling Workload Identity.
# 3. membership.authority is set, but the issuer URL is different
# from that set in membership.authority.issuer. This is technically
# an error, but we defer to validation in the API.
# 4. membership.authority.oidcJwks is set, but the private keyset
# we got from the cluster differs from the keyset in the membership.
# This means the user is updating the public keys, and we should
# update to the latest keyset in the membership.
if ( # scenario 1, disabling WI
(obj.authority and not issuer_url) or
# scenario 2, enabling WI
(issuer_url and not obj.authority) or
(obj.authority and
# scenario 3, issuer changed
((obj.authority.issuer != issuer_url) or
# scenario 4, JWKS changed
(private_keyset_json and obj.authority.oidcJwks and
(obj.authority.oidcJwks.decode('utf-8') != private_keyset_json))
))):
console_io.PromptContinue(
message=hub_util.GenerateWIUpdateMsgString(
obj, issuer_url, resource_name, args.MEMBERSHIP_NAME),
cancel_on_no=True)
try:
api_util.UpdateMembership(
resource_name,
obj,
'authority',
self.ReleaseTrack(),
issuer_url=issuer_url,
oidc_jwks=private_keyset_json)
# Generate CRD Manifest should only be called afer create/update.
self._InstallOrUpdateExclusivityArtifacts(kube_client,
resource_name)
log.status.Print(
'Updated the membership [{}] for the cluster [{}]'.format(
resource_name, args.MEMBERSHIP_NAME))
except Exception as e:
raise exceptions.Error(
'Error in updating the membership [{}]:{}'.format(
resource_name, e))
else:
console_io.PromptContinue(
message='A membership [{}] for the cluster [{}] already exists. '
'Continuing will reinstall the Connect agent deployment to use a '
'new image (if one is available).'.format(resource_name,
args.MEMBERSHIP_NAME),
cancel_on_no=True)
else:
log.status.Print(
'Created a new membership [{}] for the cluster [{}]'.format(
resource_name, args.MEMBERSHIP_NAME))
# Attempt to update the existing agent deployment, or install a new agent
# if necessary.
try:
agent_util.DeployConnectAgent(kube_client, args,
service_account_key_data,
docker_credential_data, resource_name,
self.ReleaseTrack())
except Exception as e:
log.status.Print('Error in installing the Connect agent: {}'.format(e))
# In case of a new membership, we need to clean up membership and
# resources if we failed to install the Connect agent.
if not already_exists:
api_util.DeleteMembership(resource_name, self.ReleaseTrack())
exclusivity_util.DeleteMembershipResources(kube_client)
raise
log.status.Print(
'Finished registering the cluster [{}] with the fleet.'.format(
args.MEMBERSHIP_NAME))
return obj
def _CheckMembershipWithUUID(self, resource_name, membership_name):
"""Checks for an existing Membership with UUID.
In the past, by default we used Cluster UUID to create a membership. Now
we use user supplied membership_name. This check ensures that we don't
reregister a cluster.
Args:
resource_name: The full membership resource name using the cluster uuid.
membership_name: User supplied membership_name.
Returns:
The Membership resource or None.
Raises:
exceptions.Error: If it fails to getMembership.
"""
obj = None
try:
obj = api_util.GetMembership(resource_name, self.ReleaseTrack())
if (hasattr(obj, 'description') and obj.description != membership_name):
# A membership exists, but does not have the same membership_name.
# This is possible if two different users attempt to register the same
# cluster, or if the user is upgrading and has passed a different
# membership_name. Treat this as an error: even in the upgrade case,
# this is useful to prevent the user from upgrading the wrong cluster.
raise exceptions.Error(
'There is an existing membership, [{}], that conflicts with [{}]. '
'Please delete it before continuing:\n\n'
' gcloud {}container fleet memberships delete {}'.format(
obj.description, membership_name,
hub_util.ReleaseTrackCommandPrefix(self.ReleaseTrack()),
resource_name))
except apitools_exceptions.HttpNotFoundError:
# We couldn't find a membership with uuid, so it's safe to create a
# new one.
pass
return obj
def _VerifyClusterExclusivity(self, kube_client, parent, membership_id):
"""Verifies that the cluster can be registered to the project.
Args:
kube_client: a KubernetesClient
parent: the parent collection the user is attempting to register the
cluster with.
membership_id: the ID of the membership to be created for the cluster.
Raises:
apitools.base.py.HttpError: if the API request returns an HTTP error.
exceptions.Error: if the cluster is in an invalid exclusivity state.
"""
cr_manifest = ''
# The cluster has been registered.
if kube_client.MembershipCRDExists():
cr_manifest = kube_client.GetMembershipCR()
res = api_util.ValidateExclusivity(cr_manifest, parent, membership_id,
self.ReleaseTrack())
if res.status.code:
raise exceptions.Error(
'Error validating cluster\'s exclusivity state '
'with the Fleet under parent collection [{}]: {}. '
'Cannot proceed with the cluster registration.'.format(
parent, res.status.message))
def _InstallOrUpdateExclusivityArtifacts(self, kube_client, membership_ref):
"""Install the exclusivity artifacts for the cluster.
Update the exclusivity artifacts if a new version is available if the
cluster has already being registered.
Args:
kube_client: a KubernetesClient
membership_ref: the full resource name of the membership the cluster is
registered with.
Raises:
apitools.base.py.HttpError: if the API request returns an HTTP error.
exceptions.Error: if the kubectl interation with the cluster failed.
"""
crd_manifest = kube_client.GetMembershipCRD()
cr_manifest = kube_client.GetMembershipCR() if crd_manifest else ''
res = api_util.GenerateExclusivityManifest(crd_manifest, cr_manifest,
membership_ref)
kube_client.ApplyMembership(res.crdManifest, res.crManifest)
def _RegisterGKEWithoutConnectAgent(
self, gke_cluster_resource_link, gke_cluster_uri, project, location, args
):
"""Register a GKE cluster without installing Connect agent."""
issuer_url = None
if args.enable_workload_identity:
issuer_url = gke_cluster_uri
try:
obj = api_util.CreateMembership(
project=project,
membership_id=args.MEMBERSHIP_NAME,
location=location,
gke_cluster_self_link=gke_cluster_resource_link,
external_id=None,
release_track=self.ReleaseTrack(),
issuer_url=issuer_url,
oidc_jwks=None,
api_server_version=None)
except apitools_exceptions.HttpConflictError as create_exc:
error = core_api_exceptions.HttpErrorPayload(create_exc)
if error.status_description != 'ALREADY_EXISTS':
# If the error is not due to the object already existing, re-raise.
raise
resource_name = api_util.MembershipRef(project, location,
args.MEMBERSHIP_NAME)
obj = api_util.GetMembership(resource_name, self.ReleaseTrack())
if obj.endpoint.gkeCluster.resourceLink == gke_cluster_resource_link:
log.status.Print(
'Membership [{}] already registered with the cluster [{}] in the'
' Fleet.'.format(
resource_name, obj.endpoint.gkeCluster.resourceLink
)
)
if issuer_url:
# Update the membership if there is no issuer or the issuer has
# changed.
if not obj.authority or (obj.authority.issuer != issuer_url):
try:
api_util.UpdateMembership(
name=resource_name,
membership=obj,
update_mask='authority',
issuer_url=issuer_url,
oidc_jwks=None,
release_track=self.ReleaseTrack())
log.status.Print(
'Enabled Workload Identity for the cluster [{}]'.format(
obj.endpoint.gkeCluster.resourceLink))
except Exception as update_exc:
raise exceptions.Error(
'Error in updating the membership [{}]: {}'.format(
resource_name, update_exc))
else:
raise exceptions.Error(
'membership [{}] already exists in the Fleet '
'with another cluster link [{}]. If this operation is '
'intended, please delete the membership and register '
'again.'.format(resource_name,
obj.endpoint.gkeCluster.resourceLink))
log.status.Print('Finished registering to the Fleet.')
return obj

View File

@@ -0,0 +1,38 @@
# -*- coding: utf-8 -*- #
# Copyright 2023 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.
"""Command group for Membership support access."""
from __future__ import absolute_import
from __future__ import division
from __future__ import unicode_literals
from googlecloudsdk.calliope import base
@base.DefaultUniverseOnly
class Membership(base.Group):
"""Membership used for support access.
This command group allows for manipulation of support access for the specified
membership.
## EXAMPLES
Manage support access:
$ {command} --help
"""
category = base.COMPUTE_CATEGORY

View File

@@ -0,0 +1,63 @@
# -*- coding: utf-8 -*- #
# Copyright 2023 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 command for describing the anthos support access RBACs."""
from __future__ import absolute_import
from __future__ import division
from __future__ import unicode_literals
import textwrap
from googlecloudsdk.api_lib.container.fleet import client
from googlecloudsdk.calliope import base
from googlecloudsdk.command_lib.container.fleet import resources
ROLE_BINDING_ID = 'gke-fleet-support-access'
RESOURCE_NAME_FORMAT = '{membership_name}/rbacrolebindings/{rbacrolebinding_id}'
@base.DefaultUniverseOnly
class Describe(base.DescribeCommand):
"""Describe support access for the specified membership.
## EXAMPLES
To describe support access for membership `my-membership` run:
$ {command} my-membership
"""
@classmethod
def Args(cls, parser):
resources.AddMembershipResourceArg(
parser,
membership_help=textwrap.dedent("""\
The membership name that you want to describe support access for.
"""),
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)
def Run(self, args):
membership_name = resources.ParseMembershipArg(args)
name = RESOURCE_NAME_FORMAT.format(
membership_name=membership_name, rbacrolebinding_id=ROLE_BINDING_ID)
fleet_client = client.FleetClient(release_track=self.ReleaseTrack())
return fleet_client.GetMembershipRbacRoleBinding(name)

View File

@@ -0,0 +1,63 @@
# -*- coding: utf-8 -*- #
# Copyright 2023 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 disable command for removing anthos support RBACs from the cluster."""
from __future__ import absolute_import
from __future__ import division
from __future__ import unicode_literals
import textwrap
from googlecloudsdk.api_lib.container.fleet import client
from googlecloudsdk.calliope import base
from googlecloudsdk.command_lib.container.fleet import resources
ROLE_BINDING_ID = 'gke-fleet-support-access'
RESOURCE_NAME_FORMAT = '{membership_name}/rbacrolebindings/{rbacrolebinding_id}'
@base.DefaultUniverseOnly
class Disable(base.DeleteCommand):
"""Disable support access for the specified membership.
## EXAMPLES
To disable support access for membership `my-membership` run:
$ {command} my-membership
"""
@classmethod
def Args(cls, parser):
resources.AddMembershipResourceArg(
parser,
membership_help=textwrap.dedent("""\
The membership name that you want to disable support access for.
"""),
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)
def Run(self, args):
membership_name = resources.ParseMembershipArg(args)
name = RESOURCE_NAME_FORMAT.format(
membership_name=membership_name, rbacrolebinding_id=ROLE_BINDING_ID)
fleet_client = client.FleetClient(release_track=self.ReleaseTrack())
return fleet_client.DeleteMembershipRbacRoleBinding(name)

View File

@@ -0,0 +1,101 @@
# -*- coding: utf-8 -*- #
# Copyright 2023 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 enable command for adding anthos support RBACs to the cluster."""
from __future__ import absolute_import
from __future__ import division
from __future__ import unicode_literals
import textwrap
from googlecloudsdk.api_lib.cloudresourcemanager import projects_api
from googlecloudsdk.api_lib.container.fleet import client
from googlecloudsdk.calliope import base
from googlecloudsdk.command_lib.container.fleet import resources
from googlecloudsdk.command_lib.container.fleet import util
from googlecloudsdk.command_lib.container.fleet.memberships import errors
from googlecloudsdk.command_lib.projects import util as projects_util
from googlecloudsdk.core import properties
ANTHOS_SUPPORT_USER = 'service-{project_number}@gcp-sa-{instance_name}anthossupport.iam.gserviceaccount.com'
ROLE_TYPE = 'ANTHOS_SUPPORT'
ROLE_BINDING_ID = 'gke-fleet-support-access'
RESOURCE_NAME_FORMAT = '{membership_name}/rbacrolebindings/{rbacrolebinding_id}'
@base.DefaultUniverseOnly
class Enable(base.CreateCommand):
"""Enable support access for the specified membership.
## EXAMPLES
To enable support access for membership `my-membership` run:
$ {command} my-membership
"""
def GetAnthosSupportUser(self, project_id):
"""Get P4SA account name for Anthos Support when user not specified.
Args:
project_id: the project ID of the resource.
Returns:
the P4SA account name for Anthos Support.
"""
project_number = projects_api.Get(
projects_util.ParseProject(project_id)
).projectNumber
hub_endpoint_override = util.APIEndpoint()
if hub_endpoint_override == util.PROD_API:
return ANTHOS_SUPPORT_USER.format(
project_number=project_number, instance_name=''
)
elif hub_endpoint_override == util.STAGING_API:
return ANTHOS_SUPPORT_USER.format(
project_number=project_number, instance_name='staging-'
)
elif hub_endpoint_override == util.AUTOPUSH_API:
return ANTHOS_SUPPORT_USER.format(
project_number=project_number, instance_name='autopush-'
)
else:
raise errors.UnknownApiEndpointOverrideError('gkehub')
@classmethod
def Args(cls, parser):
resources.AddMembershipResourceArg(
parser,
membership_help=textwrap.dedent("""\
The membership name that you want to enable support access for.
"""),
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)
def Run(self, args):
project_id = properties.VALUES.core.project.GetOrFail()
membership_name = resources.ParseMembershipArg(args)
user = self.GetAnthosSupportUser(project_id)
name = RESOURCE_NAME_FORMAT.format(
membership_name=membership_name, rbacrolebinding_id=ROLE_BINDING_ID)
fleet_client = client.FleetClient(release_track=self.ReleaseTrack())
return fleet_client.CreateMembershipRbacRoleBinding(
name, ROLE_TYPE, user, None)

View File

@@ -0,0 +1,128 @@
# -*- coding: utf-8 -*- #
# Copyright 2023 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.
"""Generates YAML for anthos support RBAC policies."""
from __future__ import absolute_import
from __future__ import division
from __future__ import unicode_literals
import sys
import textwrap
from googlecloudsdk.api_lib.cloudresourcemanager import projects_api
from googlecloudsdk.api_lib.container.fleet import client
from googlecloudsdk.calliope import base
from googlecloudsdk.command_lib.container.fleet import resources
from googlecloudsdk.command_lib.container.fleet import util
from googlecloudsdk.command_lib.container.fleet.memberships import errors
from googlecloudsdk.command_lib.projects import util as projects_util
from googlecloudsdk.core import log
from googlecloudsdk.core import properties
ANTHOS_SUPPORT_USER = 'service-{project_number}@gcp-sa-{instance_name}anthossupport.iam.gserviceaccount.com'
ROLE_TYPE = 'ANTHOS_SUPPORT'
ROLE_BINDING_ID = 'gke-fleet-support-access'
RESOURCE_NAME_FORMAT = '{membership_name}/rbacrolebindings/{rbacrolebinding_id}'
@base.DefaultUniverseOnly
class GetYaml(base.Command):
"""Generates YAML for anthos support RBAC policies.
## EXAMPLES
To generate the YAML for support access RBAC policies with membership
`my-membership`, run:
$ {command} my-membership
"""
def GetAnthosSupportUser(self, project_id):
"""Gets P4SA account name for Anthos Support when user not specified.
Args:
project_id: the project ID of the resource.
Returns:
The P4SA account name for Anthos Support.
"""
project_number = projects_api.Get(
projects_util.ParseProject(project_id)
).projectNumber
hub_endpoint_override = util.APIEndpoint()
if hub_endpoint_override == util.PROD_API:
return ANTHOS_SUPPORT_USER.format(
project_number=project_number, instance_name=''
)
elif hub_endpoint_override == util.STAGING_API:
return ANTHOS_SUPPORT_USER.format(
project_number=project_number, instance_name='staging-'
)
elif hub_endpoint_override == util.AUTOPUSH_API:
return ANTHOS_SUPPORT_USER.format(
project_number=project_number, instance_name='autopush-'
)
else:
raise errors.UnknownApiEndpointOverrideError('gkehub')
@classmethod
def Args(cls, parser):
resources.AddMembershipResourceArg(
parser,
membership_help=textwrap.dedent("""\
The membership name that you want to generate support access RBAC
policies for.
"""),
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(
'--rbac-output-file',
type=str,
help=textwrap.dedent("""\
If specified, the generated RBAC policy will be written to the
designated local file.
"""))
def Run(self, args):
project_id = properties.VALUES.core.project.GetOrFail()
user = self.GetAnthosSupportUser(project_id)
name = RESOURCE_NAME_FORMAT.format(
membership_name=resources.ParseMembershipArg(args),
rbacrolebinding_id=ROLE_BINDING_ID)
fleet_client = client.FleetClient(release_track=self.ReleaseTrack())
response = fleet_client.GenerateMembershipRbacRoleBindingYaml(
name, ROLE_TYPE, user, None)
if args.rbac_output_file:
sys.stdout.write('Generated RBAC policy is written to file: {} \n'.format(
args.rbac_output_file))
else:
sys.stdout.write('Generated RBAC policy is: \n')
sys.stdout.write('--------------------------------------------\n')
# Write the generated RBAC policy file to the file provided with
# "--rbac-output-file" specified or print on the screen.
log.WriteToFileOrStdout(
args.rbac_output_file if args.rbac_output_file else '-',
response.roleBindingsYaml,
overwrite=True,
binary=False,
private=True)

View File

@@ -0,0 +1,280 @@
# -*- 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))

View File

@@ -0,0 +1,57 @@
- release_tracks: [ALPHA, BETA, GA]
help_text:
brief: Update a membership.
description: Update an existing membership in a fleet.
examples: |
First retrieve the ID of the membership using the command below. The output of this command
lists all the fleet's members, with their unique IDs in the NAME column:
$ {parent_command} list
Update a membership for a cluster:
$ {command} MEMBERSHIP_ID
request:
collection: gkehub.projects.locations.memberships
issue_request_hook: googlecloudsdk.command_lib.container.fleet.memberships.util:ExecuteUpdateMembershipRequest
ALPHA:
api_version: v1alpha
BETA:
api_version: v1beta
GA:
api_version: v1
arguments:
resource:
spec: !REF googlecloudsdk.command_lib.container.fleet.resources:membership
help_text: membership resource representing a cluster in Fleet.
params:
- arg_name: async
type: bool
help_text: Return immediately, without waiting for the operation in progress to complete.
- api_field: membership.externalId
hidden: true
arg_name: external-id
help_text: Update external-id of the membership resource.
- api_field: membership.infrastructureType
release_tracks: [ALPHA, BETA]
hidden: true
arg_name: infra-type
choices:
- arg_value: on-prem
enum_value: ON_PREM
help_text: |
Specifies a cluster is running on an on-prem environment.
- arg_value: multi-cloud
enum_value: MULTI_CLOUD
help_text: |
Specified a cluster is running on public cloud infrastructure.
help_text: |
Specifies the infrastructure type that the cluster is running on.
labels:
api_field: membership.labels
update:
read_modify_update: false