285 lines
12 KiB
Python
285 lines
12 KiB
Python
# -*- coding: utf-8 -*- #
|
|
# Copyright 2025 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 to view the Config Management Feature."""
|
|
|
|
import copy
|
|
import textwrap
|
|
|
|
from googlecloudsdk.api_lib.container.fleet import transforms
|
|
from googlecloudsdk.api_lib.container.fleet import util
|
|
from googlecloudsdk.api_lib.util import exceptions as gcloud_exception
|
|
from googlecloudsdk.calliope import base
|
|
from googlecloudsdk.command_lib.container.fleet import resources
|
|
from googlecloudsdk.command_lib.container.fleet.config_management import utils
|
|
from googlecloudsdk.command_lib.container.fleet.features import base as features_base
|
|
from googlecloudsdk.command_lib.container.fleet.membershipfeatures import base as membershipfeatures_base
|
|
from googlecloudsdk.core import exceptions
|
|
from googlecloudsdk.core import log
|
|
|
|
|
|
@base.ReleaseTracks(base.ReleaseTrack.BETA, base.ReleaseTrack.GA)
|
|
class Describe(
|
|
# MembershipFeature must be inherited before Feature so that this class can
|
|
# use the prior's MembershipFeatureResourceName() method.
|
|
membershipfeatures_base.MembershipFeatureCommand,
|
|
features_base.DescribeCommand
|
|
):
|
|
"""Describe the Config Management feature."""
|
|
feature_name = utils.CONFIG_MANAGEMENT_FEATURE_NAME
|
|
mf_name = utils.CONFIG_MANAGEMENT_FEATURE_NAME
|
|
|
|
|
|
@base.ReleaseTracks(base.ReleaseTrack.ALPHA)
|
|
class DescribeAlpha(Describe):
|
|
"""Describe the Config Management feature."""
|
|
|
|
detailed_help = {
|
|
'EXAMPLES': """
|
|
To describe the entire Config Management feature, run:
|
|
|
|
$ {command}
|
|
|
|
To describe select membership configurations, run:
|
|
|
|
$ {command} --memberships=example-membership-1,example-membership-2
|
|
|
|
To list the membership configurations, run:
|
|
|
|
$ {command} --view=list
|
|
|
|
MEMBERSHIP | LOCATION | STATUS | INSTALL_STATE | STOP_STATE | SYNC_STATE | VERSION | SYNCED_TO_FLEET_DEFAULT
|
|
-------------------- | ----------- | ------ | ------------------------- | ----------- | ------------- | ------- | ----------------------------
|
|
example-membership-1 | asia-east1 | OK | CONFIG_SYNC_NOT_INSTALLED | | NOT_INSTALLED | | FLEET_DEFAULT_NOT_CONFIGURED
|
|
example-membership-2 | us-central1 | OK | CONFIG_SYNC_INSTALLED | NOT_STOPPED | SYNCED | 1.22.0 | FLEET_DEFAULT_NOT_CONFIGURED
|
|
example-membership-3 | us-central1 | ERROR | CONFIG_SYNC_INSTALLED | NOT_STOPPED | ERROR | 1.21.3 | FLEET_DEFAULT_NOT_CONFIGURED
|
|
""",
|
|
}
|
|
|
|
@classmethod
|
|
def Args(cls, parser):
|
|
cls.parser = parser
|
|
view_group = parser.add_group()
|
|
view_group.add_argument(
|
|
'--view',
|
|
help='View of the feature.',
|
|
choices={
|
|
'full': 'Default view. Prints the entire feature.',
|
|
'list': """
|
|
List of membership configurations. Default format is a table summary.
|
|
|
|
The `SYNCED_TO_FLEET_DEFAULT` column may display `UNKNOWN` for any membership
|
|
whose configuration has not been updated since the
|
|
[fleet-default membership configuration](https://cloud.google.com/kubernetes-engine/fleet-management/docs/manage-features)
|
|
enablement.
|
|
|
|
To view the underlying configurations instead of the table summary for select
|
|
memberships, run:
|
|
|
|
$ {command} --view=list --format=yaml --memberships=example-membership-1,example-membership-2
|
|
""",
|
|
'config': textwrap.dedent(
|
|
# TODO(b/461540636): Need newline at end of example to ensure
|
|
# formatting of subsequent choices.
|
|
"""\
|
|
Membership configuration in a format that is passable to the
|
|
`--config` flag on the `update` command. Note that the
|
|
configuration includes any deprecated fields that are set.
|
|
Errors if `--memberships` does not specify exactly one
|
|
membership.
|
|
|
|
To describe the configuration for a given membership, run:
|
|
|
|
$ {command} --view=config --memberships=example-membership-1
|
|
"""
|
|
),
|
|
},
|
|
default='full',
|
|
required=True,
|
|
)
|
|
# Sole purpose of list_group and memberships_group is to add help text.
|
|
list_group = view_group.add_group(
|
|
category=base.LIST_COMMAND_FLAGS,
|
|
help=(
|
|
'List command flags.'
|
|
' Only specify when `--view=list`.'
|
|
' Does not include support for `--limit`.'
|
|
),
|
|
)
|
|
filter_with_examples = copy.deepcopy(base.FILTER_FLAG)
|
|
filter_with_examples.kwargs['help'] = (
|
|
filter_with_examples.kwargs['help'].rstrip() + """
|
|
|
|
To filter for memberships with an overall status of `ERROR`, use the
|
|
``COLUMN~VALUE'' pattern and run:
|
|
|
|
$ {command} --view=list --filter=STATUS~ERROR
|
|
|
|
To filter for memberships that are synced to the
|
|
fleet-default membership configuration, run:
|
|
|
|
$ {command} --view=list --filter="spec.origin.type.synced_to_fleet_default()~YES"
|
|
|
|
`SYNCED_TO_FLEET_DEFAULT` is the only column that requires filtering on the
|
|
underlying configuration field instead of the column name. An alternative is to
|
|
`--sort-by=SYNCED_TO_FLEET_DEFAULT` and filter by eye.
|
|
|
|
To filter on a configuration field not in the table summary, in this case the
|
|
Config Sync repo, run:
|
|
|
|
$ {command} --view=list --format=yaml --filter="spec.configmanagement.configSync.git.syncRepo~https://github.com/GoogleCloudPlatform/anthos-config-management-samples.git"
|
|
"""
|
|
)
|
|
filter_with_examples.AddToParser(list_group)
|
|
sort_by_with_examples = copy.deepcopy(base.SORT_BY_FLAG)
|
|
sort_by_with_examples.kwargs['help'] = (
|
|
sort_by_with_examples.kwargs['help'].rstrip() + """
|
|
|
|
The default table summary sorts by `LOCATION` then `MEMBERSHIP`.
|
|
|
|
To sort the table by `VERSION` instead, run:
|
|
|
|
$ {command} --view=list --sort-by=VERSION
|
|
|
|
To sort by a configuration field not in the table summary, in this case the
|
|
Config Sync repo, and print its values in a table, run:
|
|
|
|
$ {command} --view=list --sort-by="spec.configmanagement.configSync.git.syncRepo" --format="table(MEMBERSHIP,LOCATION,spec.configmanagement.configSync.git.syncRepo:label=REPO)"
|
|
"""
|
|
)
|
|
sort_by_with_examples.AddToParser(list_group)
|
|
memberships_group = parser.add_group(
|
|
help=(
|
|
'Memberships to print configurations for.'
|
|
' Errors if a specified membership does not have a configuration'
|
|
' for this feature.'
|
|
)
|
|
)
|
|
resources.AddMembershipResourceArg(memberships_group, plural=True)
|
|
|
|
@staticmethod
|
|
def enforce_flag_combinations(args):
|
|
if (args.filter or args.sort_by) and args.view != 'list':
|
|
raise exceptions.Error(
|
|
'--filter and --sort-by can only be specified when --view=list.'
|
|
)
|
|
|
|
@gcloud_exception.CatchHTTPErrorRaiseHTTPException('{message}')
|
|
def Run(self, args):
|
|
# Defensive programming to protect against command instance reuse in tests.
|
|
self.parser.display_info.AddFormat('default')
|
|
self.args = args
|
|
self.enforce_flag_combinations(args)
|
|
# Feature must exist for any invocation of this command.
|
|
feature = self.GetFeature()
|
|
memberships = []
|
|
if args.memberships or args.view == 'config':
|
|
memberships = features_base.ParseMembershipsPlural(
|
|
args,
|
|
prompt=True,
|
|
# Verify the specified Memberships exist to distinguish them from
|
|
# Memberships that have no specs in error messages.
|
|
# Because search raises an internal error if the Membership location
|
|
# is unreachable, the command does not need to log the Feature's
|
|
# unreachable field.
|
|
search=True,
|
|
)
|
|
self.filter_feature_for_memberships(feature, memberships)
|
|
if args.view == 'config':
|
|
if len(memberships) != 1:
|
|
raise exceptions.Error(
|
|
'Specify exactly one membership when --view=config.'
|
|
)
|
|
target_membership = memberships[0]
|
|
# Call v2 API to prevent v1 vs. v2 misalignment with --config on update.
|
|
membership_feature = self.GetMembershipFeature(target_membership)
|
|
# Unlikely given Membership path exists in Feature.
|
|
# Defensive programming for readable error msg and because update command
|
|
# does not accept empty config in the case of None.
|
|
if (not membership_feature.spec or
|
|
not membership_feature.spec.configmanagement):
|
|
raise exceptions.Error(
|
|
'MembershipFeature'
|
|
f" '{self.MembershipFeatureResourceName(target_membership)}'"
|
|
' missing expected configuration.'
|
|
)
|
|
return membership_feature.spec.configmanagement
|
|
if args.view == 'list':
|
|
# Limit transforms to --view=list for simplicity; relax if necessary.
|
|
self.parser.display_info.AddTransforms(transforms.get_transforms(
|
|
self.hubclient, feature.fleetDefaultMemberConfig is not None,
|
|
))
|
|
self.parser.display_info.AddFormat("""table(
|
|
name.segment(-3):label=MEMBERSHIP:sort=2,
|
|
name.segment(-5):label=LOCATION:sort=1,
|
|
state.state.code:label=STATUS,
|
|
state.configmanagement.configSyncState.state:label=INSTALL_STATE,
|
|
state.configmanagement.configSyncState.clusterLevelStopSyncingState:label=STOP_STATE,
|
|
state.configmanagement.configSyncState.syncState.code:label=SYNC_STATE,
|
|
state.configmanagement.membershipSpec.version:label=VERSION,
|
|
spec.origin.type.synced_to_fleet_default():label=SYNCED_TO_FLEET_DEFAULT
|
|
)""")
|
|
return self.construct_membership_features_list(feature)
|
|
return feature
|
|
|
|
def construct_membership_features_list(self, feature):
|
|
if feature.unreachable:
|
|
log.warning('Membership configuration list may be incomplete.'
|
|
' Unreachable locations: %s', feature.unreachable)
|
|
if not feature.membershipSpecs:
|
|
return []
|
|
states = {}
|
|
if feature.membershipStates:
|
|
states = {
|
|
util.MembershipPartialName(entry.key): entry.value
|
|
for entry in feature.membershipStates.additionalProperties
|
|
}
|
|
project_id = self.Project(number=False)
|
|
project_number = self.Project(number=True)
|
|
return [
|
|
# Mimic v2 MembershipFeature. Leaves room for alternate
|
|
# ListMembershipFeatures API call implementation. Uses dictionary
|
|
# instead of generated API message class to avoid potential v1 vs. v2
|
|
# configuration discrepancy errors. (Since we don't promise the output
|
|
# of --view=list passes into other commands as input, any v1 vs. v2
|
|
# discrepancy is forgivable.)
|
|
{
|
|
# Use project name instead of number for readability, to follow
|
|
# resource name convention, and to match the v2 API output for
|
|
# MembershipFeature. Do not use functions like
|
|
# util.MembershipFeatureResourceName() that parse the resources
|
|
# REGISTRY due to poor performance, which doesn't scale with the
|
|
# number of Memberships.
|
|
'name': self.MembershipFeatureResourceName(
|
|
entry.key.replace(
|
|
f'projects/{project_number}/',
|
|
f'projects/{project_id}/',
|
|
1
|
|
)
|
|
),
|
|
'spec': entry.value,
|
|
**(
|
|
{'state': states[util.MembershipPartialName(entry.key)]}
|
|
if util.MembershipPartialName(entry.key) in states
|
|
else {}
|
|
)
|
|
}
|
|
for entry in feature.membershipSpecs.additionalProperties
|
|
]
|
|
|
|
def Epilog(self, resources_were_displayed):
|
|
# Reference Epilog method on ListCommand.
|
|
if not resources_were_displayed and self.args.view == 'list':
|
|
log.status.Print('Listed 0 items.')
|