# -*- 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 command to get the status of Config Management Feature.""" from __future__ import absolute_import from __future__ import division from __future__ import unicode_literals from googlecloudsdk.api_lib.container.fleet import util from googlecloudsdk.calliope import base from googlecloudsdk.command_lib.container.fleet import api_util from googlecloudsdk.command_lib.container.fleet.config_management import utils from googlecloudsdk.command_lib.container.fleet.features import base as feature_base from googlecloudsdk.core import log NA = 'NA' DETAILED_HELP = { 'EXAMPLES': """\ Print the status of the Config Management feature: $ {command} Name | Status | Last_Synced_Token | Sync_Branch | Last_Synced_Time | Policy_Controller | Hierarchy_Controller | Version | Upgrades | Synced_To_Fleet_Default --------------- | ------ | ----------------- | ----------- | ----------------------------- | ----------------- | -------------------- | ------- | -------- | ---------------------------- managed-cluster | SYNCED | 2945500b7f | acme | 2020-03-23 11:12:31 -0700 PDT | NA | NA | 1.18.3 | auto | FLEET_DEFAULT_NOT_CONFIGURED View the status for the cluster named `managed-cluster-a`: $ {command} --flatten=acm_status --filter="acm_status.name:managed-cluster-a" Use a regular expression to list status for multiple clusters: $ {command} --flatten=acm_status --filter="acm_status.name ~ managed-cluster.*" List all clusters where current Config Sync `Status` is `SYNCED`: $ {command} --flatten=acm_status --filter="acm_status.config_sync:SYNCED" List all the clusters where sync_branch is `v1` and current Config Sync `Status` is not `SYNCED`: $ {command} --flatten=acm_status --filter="acm_status.sync_branch:v1 AND -acm_status.config_sync:SYNCED" """, } class ConfigmanagementFeatureState(object): """Feature state class stores ACM status.""" def __init__(self, cluster_name): self.name = cluster_name self.config_sync = NA self.last_synced_token = NA self.last_synced = NA self.sync_branch = NA self.policy_controller_state = NA self.hierarchy_controller_state = NA self.version = NA self.upgrades = NA self.synced_to_fleet_default = NA def update_sync_state(self, fs): """Update config_sync state for the membership that has ACM installed. Args: fs: ConfigManagementFeatureState """ if ( fs.configSyncState is None or fs.configSyncState.state.name != 'CONFIG_SYNC_INSTALLED' ): return if fs.configSyncState.syncState: if fs.configSyncState.syncState.syncToken: self.last_synced_token = fs.configSyncState.syncState.syncToken[:7] self.last_synced = fs.configSyncState.syncState.lastSyncTime if has_config_sync_git(fs): self.sync_branch = fs.membershipSpec.configSync.git.syncBranch def update_policy_controller_state(self, md): """Update policy controller state for the membership that has ACM installed. Args: md: MembershipFeatureState """ # Also surface top-level Feature Authorizer errors. if md.state.code.name != 'OK': self.policy_controller_state = 'ERROR: {}'.format(md.state.description) return fs = md.configmanagement if not ( fs.policyControllerState and fs.policyControllerState.deploymentState ): self.policy_controller_state = NA return pc_deployment_state = fs.policyControllerState.deploymentState expected_deploys = { 'GatekeeperControllerManager': ( pc_deployment_state.gatekeeperControllerManagerState ) } if ( fs.membershipSpec and fs.membershipSpec.version and fs.membershipSpec.version > '1.4.1' ): expected_deploys['GatekeeperAudit'] = pc_deployment_state.gatekeeperAudit for deployment_name, deployment_state in expected_deploys.items(): if not deployment_state: continue elif deployment_state.name != 'INSTALLED': self.policy_controller_state = '{} {}'.format( deployment_name, deployment_state ) return self.policy_controller_state = deployment_state.name def update_hierarchy_controller_state(self, fs): """Update hierarchy controller state for the membership that has ACM installed. The PENDING state is set separately after this logic. The PENDING state suggests the HC part in feature_spec and feature_state are inconsistent, but the HC status from feature_state is not ERROR. This suggests that HC might be still in the updating process, so we mark it as PENDING Args: fs: ConfigmanagementFeatureState """ if not (fs.hierarchyControllerState and fs.hierarchyControllerState.state): self.hierarchy_controller_state = NA return hc_deployment_state = fs.hierarchyControllerState.state hnc_state = 'NOT_INSTALLED' ext_state = 'NOT_INSTALLED' if hc_deployment_state.hnc: hnc_state = hc_deployment_state.hnc.name if hc_deployment_state.extension: ext_state = hc_deployment_state.extension.name # partial mapping from ('hnc_state', 'ext_state') to 'HC_STATE', # ERROR, PENDING, NA states are identified separately deploys_to_status = { ('INSTALLED', 'INSTALLED'): 'INSTALLED', ('INSTALLED', 'NOT_INSTALLED'): 'INSTALLED', ('NOT_INSTALLED', 'NOT_INSTALLED'): NA, } if (hnc_state, ext_state) in deploys_to_status: self.hierarchy_controller_state = deploys_to_status[ (hnc_state, ext_state) ] else: self.hierarchy_controller_state = 'ERROR' def update_pending_state(self, api, feature_spec_mc, feature_state_mc): """Update Config Management component states if spec does not match state. Args: api: GKE Hub API feature_spec_mc: MembershipConfig feature_state_mc: MembershipConfig """ feature_state_pending = ( feature_state_mc is None and feature_spec_mc is not None ) if feature_state_pending: self.last_synced_token = utils.STATUS_PENDING self.last_synced = utils.STATUS_PENDING self.sync_branch = utils.STATUS_PENDING if self.config_sync == NA: self.config_sync = utils.STATUS_PENDING if ( self.policy_controller_state.__str__() in ['INSTALLED', 'GatekeeperAudit NOT_INSTALLED', NA] and feature_state_pending ): self.policy_controller_state = utils.STATUS_PENDING hc_semantic_copy = ( lambda hc_spec: hc_spec if hc_spec is not None else api.ConfigManagementHierarchyControllerConfig() ) if ( self.hierarchy_controller_state.__str__() != utils.STATUS_ERROR and feature_state_pending or hc_semantic_copy(feature_spec_mc.hierarchyController) != hc_semantic_copy(feature_state_mc.hierarchyController) ): self.hierarchy_controller_state = utils.STATUS_PENDING @base.ReleaseTracks(base.ReleaseTrack.ALPHA, base.ReleaseTrack.BETA) class Status(feature_base.FeatureCommand, base.ListCommand): """Print the status of all clusters with Config Management enabled. The `Status` column indicates the status of the Config Sync component. `Status` displays `NOT_INSTALLED` when Config Sync is not installed. `Status` displays `NOT_CONFIGURED` when Config Sync is installed but git/oci is not configured. `Status` displays `SYNCED` when Config Sync is installed and git/oci is configured and the last sync was successful. `Status` displays `ERROR` when Config Sync encounters errors. `Status` displays `STOPPED` when Config Sync stops syncing configs to the cluster. `Status` displays `PENDING` when Config Sync has not reached the desired state. Otherwise, `Status` displays `UNSPECIFIED`. The `Synced_to_Fleet_Default` status indicates whether each membership's configuration has been synced with the [fleet-default membership configuration ](https://cloud.google.com/kubernetes-engine/fleet-management/docs/manage-features). `Synced_to_Fleet_Default` displays `FLEET_DEFAULT_NOT_CONFIGURED` when fleet-default membership configuration is not enabled. `Synced_to_Fleet_Default` for an individual membership may be `UNKNOWN` if configuration has yet to be applied to this membership since enabling fleet-default membership configuration. See the `enable` and `apply` commands for more details. """ detailed_help = DETAILED_HELP feature_name = 'configmanagement' @staticmethod def Args(parser): parser.display_info.AddFormat(""" multi(acm_status:format='table( name:label=Name:sort=1, config_sync:label=Status, last_synced_token:label="Last_Synced_Token", sync_branch:label="Sync_Branch", last_synced:label="Last_Synced_Time", policy_controller_state:label="Policy_Controller", hierarchy_controller_state:label="Hierarchy_Controller", version:label="Version", upgrades:label="Upgrades", synced_to_fleet_default:label="Synced_to_Fleet_Default" )' , acm_errors:format=list) """) def Run(self, _): memberships, unreachable = api_util.ListMembershipsFull() if unreachable: log.warning( 'Locations {} are currently unreachable. Status ' 'entries may be incomplete'.format(unreachable) ) if not memberships: return None self.f = self.GetFeature() acm_status = [] acm_errors = [] self.feature_spec_memberships = { util.MembershipPartialName(m): s for m, s in self.hubclient.ToPyDict(self.f.membershipSpecs).items() if s is not None and s.configmanagement is not None } feature_state_memberships = { util.MembershipPartialName(m): s for m, s in self.hubclient.ToPyDict(self.f.membershipStates).items() } # TODO(b/298461043): Refactor this method so it is more readable. for name in memberships: name = util.MembershipPartialName(name) cluster = ConfigmanagementFeatureState(name) cluster.synced_to_fleet_default = self.fleet_default_sync_status(name) if name not in feature_state_memberships: if name in self.feature_spec_memberships: # (b/187846229) Show PENDING if feature spec is aware of # this membership name but feature state is not cluster.update_pending_state( self.messages, self.feature_spec_memberships[name], None ) acm_status.append(cluster) continue md = feature_state_memberships[name] fs = md.configmanagement # (b/153587485) Show FeatureState.code if it's not OK # as it indicates an unreachable cluster or a dated syncState.code if md.state is None or md.state.code is None: cluster.config_sync = 'CODE_UNSPECIFIED' elif fs is None: cluster.config_sync = utils.STATUS_NOT_INSTALLED else: # operator errors could occur regardless of the deployment_state if has_operator_error(fs): append_error(name, fs.operatorState.errors, acm_errors) # We should update PoCo state regardless of operator state. cluster.update_policy_controller_state(md) if not has_operator_state(fs): if md.state.code.name != 'OK': cluster.config_sync = md.state.code.name else: cluster.config_sync = 'OPERATOR_STATE_UNSPECIFIED' else: # Set cluster.upgrades if ( fs.membershipSpec is not None and fs.membershipSpec.management is not None and fs.membershipSpec.management.name == utils.MANAGEMENT_AUTOMATIC ): cluster.upgrades = utils.UPGRADES_AUTO else: cluster.upgrades = utils.UPGRADES_MANUAL # Set cluster.version if fs.membershipSpec is not None: cluster.version = fs.membershipSpec.version # Set cluster.config_sync if fs.configSyncState.state is not None: cluster.config_sync = config_sync_state(fs) # Set cluster.last_synced_token, cluster.sync_branch and # cluster.last_synced_time cluster.update_sync_state(fs) # Add errors into acm_errors if fs.configSyncState.errors: append_error(name, fs.configSyncState.errors, acm_errors) if has_config_sync_sync_error(fs): append_error(name, fs.configSyncState.syncState.errors, acm_errors) # Set cluster.hierarchy_controller_state cluster.update_hierarchy_controller_state(fs) if name in self.feature_spec_memberships: cluster.update_pending_state( self.messages, self.feature_spec_memberships[name].configmanagement, fs.membershipSpec, ) acm_status.append(cluster) return {'acm_errors': acm_errors, 'acm_status': acm_status} def fleet_default_sync_status(self, membership): if not self.f.fleetDefaultMemberConfig: return 'FLEET_DEFAULT_NOT_CONFIGURED' if (membership not in self.feature_spec_memberships or self.feature_spec_memberships[membership].origin is None): return 'UNKNOWN' origin = self.feature_spec_memberships[membership].origin.type if origin == self.messages.Origin.TypeValueValuesEnum.FLEET: return 'YES' if (origin == self.messages.Origin.TypeValueValuesEnum.USER or origin == self.messages.Origin.TypeValueValuesEnum.FLEET_OUT_OF_SYNC): return 'NO' return 'UNKNOWN' def config_sync_state(fs): """Convert state to a string shown to the users. Args: fs: ConfigManagementFeatureState Returns: a string shown to the users representing the Config Sync state. """ if ( fs.configSyncState is not None and fs.configSyncState.clusterLevelStopSyncingState is not None ): if fs.configSyncState.clusterLevelStopSyncingState.name in [ utils.STATUS_STOPPED, utils.STATUS_PENDING, ]: return fs.configSyncState.clusterLevelStopSyncingState.name cs_installation_state = fs.configSyncState.state.name if cs_installation_state == 'CONFIG_SYNC_PENDING': return utils.STATUS_PENDING if cs_installation_state == 'CONFIG_SYNC_INSTALLED': if fs.configSyncState and fs.configSyncState.syncState: return fs.configSyncState.syncState.code.name return utils.STATUS_INSTALLED if cs_installation_state == 'CONFIG_SYNC_NOT_INSTALLED': return utils.STATUS_NOT_INSTALLED if cs_installation_state == 'CONFIG_SYNC_ERROR': return utils.STATUS_ERROR return 'UNSPECIFIED' def has_operator_state(fs): return fs and fs.operatorState and fs.operatorState.deploymentState def has_operator_error(fs): return fs and fs.operatorState and fs.operatorState.errors def has_config_sync_sync_error(fs): return ( fs and fs.configSyncState and fs.configSyncState.syncState and fs.configSyncState.syncState.errors ) def has_config_sync_git(fs): return ( fs.membershipSpec and fs.membershipSpec.configSync and fs.membershipSpec.configSync.git ) def append_error(cluster, state_errors, acm_errors): for error in state_errors: acm_errors.append({'cluster': cluster, 'error': error.errorMessage})