def get_actions_for_missing_state(self) -> Sequence[DAction]: config = self.info.config actions: MutableSequence[DAction] = [ DAction(name=f"create-project", description= f"Create GCP project '{self.info.config['project_id']}'") ] if 'billing_account_id' in config: desired_billing_account = config['billing_account_id'] actions.append( DAction(name='set-billing-account', description= f"Set billing account to '{desired_billing_account}'")) if 'apis' in config: apis = config['apis'] if 'disabled' in apis: for api_name in apis['disabled']: actions.append( DAction(name=f"disable-api-{api_name}", description=f"Disable API '{api_name}'", args=['disable_api', api_name])) if 'enabled' in apis: for api_name in apis['enabled']: actions.append( DAction(name=f"enable-api-{api_name}", description=f"Enable API '{api_name}'", args=['enable_api', api_name])) return actions
def get_actions_for_discovered_state(self, state: dict) -> Sequence[DAction]: actions: MutableSequence[DAction] = [] config = self.info.config # update project organization if requested to (and if necessary) if 'organization_id' in config: actual_parent: dict = state['parent'] if 'parent' in state else {} actual_parent_id: int = int( actual_parent['id']) if 'id' in actual_parent else None desired_parent_id: int = config['organization_id'] if desired_parent_id != actual_parent_id: actions.append( DAction( name='set-parent', description=f"Set organization to '{desired_parent_id}'" )) # update project billing account if requested to (and if necessary) if 'billing_account_id' in config and config[ 'billing_account_id'] != state['billing_account_id']: actions.append( DAction( name='set-billing-account', description= f"Set billing account to '{config['billing_account_id']}'") ) # enable/disable project APIs if requested to (and if necessary) if 'apis' in config: apis = config['apis'] # fetch currently enabled project APIs actual_enabled_api_names: Sequence[str] = sorted( state['apis']['enabled']) if 'disabled' in apis: # disable APIs that are currently enabled, but user requested them to be disabled for api_name in [ api_name for api_name in apis['disabled'] if api_name in actual_enabled_api_names ]: actions.append( DAction(name=f"disable-api-{api_name}", description=f"Disable API '{api_name}'", args=['disable_api', api_name])) if 'enabled' in apis: # enable APIs that are currently not enabled, but user requested them to be enabled for api_name in [ api_name for api_name in apis['enabled'] if api_name not in actual_enabled_api_names ]: actions.append( DAction(name=f"enable-api-{api_name}", description=f"Enable API '{api_name}'", args=['enable_api', api_name])) return actions
def get_actions_for_discovered_state(self, state: dict) -> Sequence[DAction]: actions: MutableSequence[DAction] = [] if 'display_name' in self.info.config and self.info.config['display_name'] != state['displayName']: sa_email = self.info.config["email"] actions.append(DAction(name=f"update-display-name", description=f"Update display name of service account '{sa_email}'", args=["update_display_name", state['etag']])) return actions
def get_actions_for_missing_state(self) -> Sequence[DAction]: return [ DAction(name=a['name'], description=a['description'], image=a['image'], entrypoint=a['entrypoint'], args=a['args']) for a in missing_state_actions or [] ]
def get_actions_for_missing_state(self) -> Sequence[DAction]: manifest = self.info.config['manifest'] metadata = manifest['metadata'] return [ DAction(name='create', description= f"Create {manifest['kind'].lower()} '{metadata['name']}'") ]
def get_actions_for_discovered_state(self, state: dict) -> Sequence[DAction]: return [ DAction(name=a['name'], description=a['description'], image=a['image'], entrypoint=a['entrypoint'], args=a['args']) for a in existing_state_actions or [] ]
def get_actions_for_missing_state(self) -> Sequence[DAction]: actions: MutableSequence[DAction] = [] cfg: dict = self.info.config enabled_apis = self.svc.find_gcp_project_enabled_apis(project_id=cfg['project_id']) if enabled_apis is None \ or 'sqladmin.googleapis.com' not in enabled_apis \ or 'sql-component.googleapis.com' not in enabled_apis: # if the SQL Admin API is not enabled, there can be no SQL instances; we will, however, have to enable # that API for the project later on. actions.append(DAction(name='enable-sql-apis', description=f"Enable Cloud SQL APIs for project '{cfg['project_id']}'")) actions.append(DAction(name=f"create-sql-instance", description=f"Create SQL instance '{cfg['name']}'")) # no need to evaluate scripts, since instance does not yet exist; the create action will do it once it creates # the instance successfully return actions
def get_actions_for_discovered_state(self, state: dict) -> Sequence[DAction]: actions: MutableSequence[DAction] = [] differences: list = collect_differences(desired=self.info.config['manifest'], actual=state) if 'apiVersion' in differences: differences.remove('apiVersion') if 'kind' in differences: differences.remove('kind') if differences: if self.info.verbose: print("Found state differences: ", file=sys.stderr) pprint(differences, stream=sys.stderr) kind: str = self.info.config['manifest']['kind'] name: str = self.info.config['manifest']['metadata']['name'] actions.append(DAction(name='update', description=f"Update {kind.lower()} '{name}'", args=['update'])) return actions
def get_actions_for_discovered_state(self, state: dict) -> Sequence[DAction]: if 'bindings' not in state: raise Exception( f"illegal state: IAM policy could not be fetched! (permissions problem, or missing project?)" ) update_needed: bool = False final_bindings: List[dict] = deepcopy(state['bindings']) for desired_binding in self.info.config['bindings']: desired_role: str = desired_binding['role'] desired_members: Sequence[str] = desired_binding['members'] role_found: bool = False for actual_binding in final_bindings: if desired_role == actual_binding['role']: role_found = True actual_members: List[str] = actual_binding['members'] missing_members = [ member for member in desired_members if member not in actual_members ] if len(missing_members) > 0: print( f"Subjects {missing_members} missing from role '{desired_role}'", file=sys.stderr) update_needed = True if not role_found: print(f"No policy for role '{desired_role}' was found", file=sys.stderr) update_needed = True if update_needed: return [ DAction(name=f"update-policy", description=f"Update IAM policy", args=["update_policy", state['etag']]) ] else: return []
def get_actions_for_missing_state(self) -> Sequence[DAction]: sa_email = self.info.config["email"] return [DAction(name=f"create-service-account", description=f"Create service account '{sa_email}'")]
def get_actions_for_missing_state(self) -> Sequence[DAction]: type = "global" if 'region' not in self.info.config else "regional" return [DAction(name=f"create", description=f"Create {type} IP address '{self.info.config['name']}'")]
def get_actions_for_discovered_state(self, state: dict) -> Sequence[DAction]: actions: MutableSequence[DAction] = [] actual = state actual_settings = actual['settings'] cfg = self.info.config # validate instance is RUNNING if actual['state'] != "RUNNABLE": raise Exception(f"illegal state: instance exists, but not running ('{actual['state']}')") # validate instance region zone = cfg['zone'] region = region_from_zone(zone) if actual['region'] != region: raise Exception( f"illegal config: SQL instance is in region '{actual['region']}' instead of '{region}'. " f"Unfortunately, changing SQL instances regions is not allowed in Google Cloud SQL.") # validate instance preferred zone if actual_settings['locationPreference']['zone'] != zone: actions.append(DAction(name='update-zone', description=f"Update SQL instance preferred zone to '{zone}'")) # validate instance machine type machine_type = cfg['machine-type'] if actual_settings['tier'] != machine_type: actions.append(DAction(name='update-machine-type', description=f"Update SQL instance machine type to '{machine_type}'")) # validate backup configuration if 'backup' in cfg: desired_backup: dict = cfg['backup'] if desired_backup['enabled']: # Verify that actual backup configuration IS enabled: if 'backupConfiguration' not in actual_settings: actions.append(DAction(name='update-backup', description=f"Enable SQL instance backups")) else: actual_backup = actual_settings['backupConfiguration'] if not actual_backup['enabled'] or not actual_backup['binaryLogEnabled']: actions.append(DAction(name='update-backup', description=f"Enable SQL instance backup/binary-logging")) elif 'time' in desired_backup: desired_time = desired_backup['time'] if desired_time != actual_backup['startTime']: actions.append( DAction(name='update-backup', description=f"Update SQL instance backup schedule to '{desired_time}'")) elif 'time' in desired_backup: raise Exception(f"illegal config: cannot specify backup time when backup is disabled") elif 'backupConfiguration' in actual_settings: # Verify that actual backup configuration IS NOT enabled: actual_backup = actual_settings['backupConfiguration'] if actual_backup['enabled'] or actual_backup['binaryLogEnabled']: actions.append(DAction(name='update-backup', description=f"Disable SQL instance backups/binary-logging")) # validate data-disk size if "data-disk-size-gb" in cfg: desired_data_disk_size_gb: int = cfg["data-disk-size-gb"] actual_disk_size: int = int(actual_settings['dataDiskSizeGb']) if actual_disk_size != desired_data_disk_size_gb: if desired_data_disk_size_gb < actual_disk_size: raise Exception( f"illegal config: cannot reduce disk size from {actual_disk_size}gb to " f"{desired_data_disk_size_gb}gb (not allowed by Cloud SQL APIs).") else: actions.append( DAction(name='update-data-disk-size', description=f"Update SQL instance data disk size from {actual_disk_size}gb to " f"{desired_data_disk_size_gb}gb")) # validate data-disk type if "data-disk-type" in cfg: desired_data_disk_type: str = cfg["data-disk-type"] if actual_settings['dataDiskType'] != desired_data_disk_type: actions.append(DAction(name='update-data-disk-type', description=f"Update SQL instance data disk type to '{desired_data_disk_type}'")) # validate MySQL flags if 'flags' in cfg: desired_flags: dict = cfg['flags'] actual_flags = actual_settings['databaseFlags'] if 'databaseFlags' in actual_settings else [] if actual_flags != desired_flags: actions.append(DAction(name='update-flags', description=f"Update SQL instance flags")) # validate SSL connections requirement if "require-ssl" in cfg: desired_require_ssl: bool = cfg["require-ssl"] if actual_settings['ipConfiguration']['requireSsl'] != desired_require_ssl: actions.append( DAction(name='update-require-ssl', description=f"Update SQL instance to {'' if desired_require_ssl else 'not '}require " f"SSL connections")) # validate authorized networks if "authorized-networks" in cfg: desired_authorized_networks: list = cfg["authorized-networks"] # validate network names are unique names = set() for desired_network in desired_authorized_networks: if desired_network['name'] in names: raise Exception(f"illegal config: network '{desired_network['name']}' defined more than once") else: names.add(desired_network['name']) actual_auth_networks = actual_settings['ipConfiguration']['authorizedNetworks'] if len(actual_auth_networks) != len(desired_authorized_networks): actions.append( DAction(name='update-authorized-networks', description=f"Update SQL instance authorized networks")) else: # validate each network for desired_network in desired_authorized_networks: try: actual_network = next(n for n in actual_auth_networks if n['name'] == desired_network['name']) desired_expiry = desired_network['expirationTime'] \ if 'expirationTime' in desired_network else None actual_expiry = actual_network['expirationTime'] if 'expirationTime' in actual_network else None if desired_network['value'] != actual_network['value'] or desired_expiry != actual_expiry: actions.append(DAction(name='update-authorized-networks', description=f"Update SQL instance authorized networks " f"(found stale network: {desired_network['name']})")) break except StopIteration: actions.append(DAction(name='update-authorized-networks', description=f"Update SQL instance authorized networks")) break # validate maintenance window if "maintenance" in cfg: desired_maintenance: dict = cfg["maintenance"] actual_maintenance_window = actual_settings['maintenanceWindow'] if desired_maintenance is None: actions.append(DAction(name='update-maintenance-window', description=f"Disable SQL instance maintenance window")) else: desired_day = desired_maintenance['day'] desired_hour: int = desired_maintenance['hour'] if desired_day != actual_maintenance_window['day'] or desired_hour != actual_maintenance_window['hour']: actions.append(DAction(name='update-maintenance-window', description=f"Update SQL instance maintenance window")) # validate storage auto-resize if "storage-auto-resize" in cfg: desired_storage_auto_resize: dict = cfg["storage-auto-resize"] if not desired_storage_auto_resize['enabled'] and 'limit' in desired_storage_auto_resize: raise Exception(f"illegal config: cannot specify storage auto-resize limit when it's disabled") elif desired_storage_auto_resize['enabled'] != actual_settings['storageAutoResize']: raise Exception(f"illegal config: currently it's impossible to switch storage auto-resize " f"(Google APIs seem to reject this change)") elif 'limit' in desired_storage_auto_resize \ and desired_storage_auto_resize['limit'] != int(actual_settings['storageAutoResizeLimit']): actions.append(DAction(name='update-storage-auto-resize', description=f"Update SQL instance storage auto-resizing")) # validate labels if "labels" in cfg: desired_labels: dict = cfg["labels"] actual_labels: dict = actual_settings['userLabels'] if 'userLabels' in actual_settings else {} if len(desired_labels.keys()) != len(actual_labels.keys()): actions.append(DAction(name='update-labels', description=f"Update SQL instance user-labels")) else: for key, value in desired_labels.items(): if key not in actual_labels or value != actual_labels[key]: actions.append(DAction(name='update-labels', description=f"Update SQL instance user-labels")) break # create missing users if 'users' in cfg: actual_users: list = actual['users'] for user in cfg['users']: found: bool = False for actual_user in actual_users: if user['name'] == actual_user['name']: found: bool = True break if not found: actions.append(DAction(name='add-user', description=f"Create new user '{user['name']}'", args=["add_user", user['name']])) # check for scripts that need to be executed if "scripts" in cfg: evaluator: ScriptEvaluator = ScriptEvaluator(svc=self.svc, project_id=cfg['project_id'], instance_name=cfg['name'], root_password=cfg['root-password'], zone=zone, scripts_data=self.info.config['scripts'], context=cfg['scripts_ctx'] if 'scripts_ctx' in cfg else {}) with evaluator as evaluator: for script in evaluator.get_scripts_to_execute(): actions.append( DAction(name='execute-script', description=f"Execute '{script.name}' SQL scripts", args=['execute_scripts', script.name])) return actions
def get_actions_for_discovered_state(self, state: dict) -> Sequence[DAction]: cluster_name = self.info.config['name'] actions: MutableSequence[DAction] = [] actual_cluster = state # validate cluster is RUNNING if actual_cluster['status'] != "RUNNING": raise Exception(f"Cluster exists, but not running ('{actual_cluster['status']}')") # validate cluster primary zone & locations desired_cluster_zone = self.info.config['zone'] actual_cluster_locations: Sequence[str] = actual_cluster['locations'] if [desired_cluster_zone] != actual_cluster_locations: raise Exception( f"Cluster locations are {actual_cluster_locations} instead of {[desired_cluster_zone]}. " f"Updating this is not allowed in GKE APIs unfortunately.") # validate cluster master version & cluster node pools version actual_cluster_master_version: str = actual_cluster["currentMasterVersion"] actual_cluster_node_version: str = actual_cluster["currentNodeVersion"] desired_version = self.info.config['version'] if desired_version != actual_cluster_master_version: actions.append( DAction(name='update-cluster-master-version', description=f"Update master version for cluster '{cluster_name}'")) if desired_version != actual_cluster_node_version: raise Exception( f"Cluster node version is '{actual_cluster_node_version}' instead of '{desired_version}'. " f"Updating this is not allowed in GKE APIs unfortunately.") # ensure master authorized networks is disabled if 'masterAuthorizedNetworksConfig' in actual_cluster: if 'enabled' in actual_cluster['masterAuthorizedNetworksConfig']: if actual_cluster['masterAuthorizedNetworksConfig']['enabled']: actions.append( DAction(name='disable-master-authorized-networks', description=f"Disable master authorized networks for cluster '{cluster_name}'")) # ensure that legacy ABAC is disabled if 'legacyAbac' in actual_cluster: if 'enabled' in actual_cluster['legacyAbac']: if actual_cluster['legacyAbac']['enabled']: actions.append( DAction(name='disable-legacy-abac', description=f"Disable legacy ABAC for cluster '{cluster_name}'")) # ensure monitoring service is set to GKE's monitoring service actual_monitoring_service = \ actual_cluster["monitoringService"] if "monitoringService" in actual_cluster else None if actual_monitoring_service != "monitoring.googleapis.com": actions.append( DAction(name='enable-monitoring-service', description=f"Enable GCP monitoring for cluster '{cluster_name}'")) # ensure logging service is set to GKE's logging service actual_logging_service = \ actual_cluster["loggingService"] if "loggingService" in actual_cluster else None if actual_logging_service != "logging.googleapis.com": actions.append( DAction(name='enable-logging-service', description=f"Enable GCP logging for cluster '{cluster_name}'")) # infer actual addons status actual_addons: dict = actual_cluster["addonsConfig"] if "addonsConfig" in actual_cluster else {} # ensure HTTP load-balancing addon is ENABLED http_lb_addon: dict = actual_addons["httpLoadBalancing"] if "httpLoadBalancing" in actual_addons else {} if 'disabled' in http_lb_addon and http_lb_addon["disabled"]: actions.append( DAction(name='enable-http-load-balancer-addon', description=f"Enable HTTP load-balancing addon for cluster '{cluster_name}'", args=['set_addon_status', 'httpLoadBalancing', 'enabled'])) # ensure Kubernetes Dashboard addon is DISABLED k8s_dashboard_addon = actual_addons["kubernetesDashboard"] if "kubernetesDashboard" in actual_addons else {} if "disabled" in k8s_dashboard_addon and not k8s_dashboard_addon["disabled"]: actions.append( DAction(name='disable-k8s-dashboard-addon', description=f"Disable legacy Kubernetes Dashboard addon for cluster '{cluster_name}'", args=['set_addon_status', 'kubernetesDashboard', 'disabled'])) # ensure Horizontal Pod Auto-scaling addon is ENABLED horiz_pod_auto_scaling_addon = \ actual_addons["horizontalPodAutoscaling"] if "horizontalPodAutoscaling" in actual_addons else {} if "disabled" in horiz_pod_auto_scaling_addon and horiz_pod_auto_scaling_addon["disabled"]: actions.append( DAction(name='enable-k8s-horiz-pod-auto-scaling-addon', description=f"Enable horizontal Pod auto-scaling addon for cluster '{cluster_name}'", args=['set_addon_status', 'horizontalPodAutoscaling', 'enabled'])) # ensure alpha features are DISABLED if 'enableKubernetesAlpha' in actual_cluster and actual_cluster["enableKubernetesAlpha"]: raise Exception(f"Cluster alpha features are enabled instead of disabled. " f"Updating this is not allowed in GKE APIs unfortunately.") # validate node pools state desired_node_pools: Sequence[dict] = self.info.config['node_pools'] for pool in desired_node_pools: pool_name = pool['name'] actual_pool = self.svc.get_gke_cluster_node_pool(project_id=self.info.config['project_id'], zone=desired_cluster_zone, name=self.info.config['name'], pool_name=pool_name) if actual_pool is None: actions.append(DAction(name='create-node-pool', description=f"Create node pool '{pool_name}' in cluster '{cluster_name}'", args=['create_node_pool', pool_name])) continue # ensure the node pool is RUNNING if actual_pool['status'] != "RUNNING": raise Exception(f"Node pool '{pool_name}' exists, but not running ('{actual_pool['status']}')") # validate node pool version if desired_version != actual_pool["version"]: actions.append( DAction(name='update-node-pool-version', description=f"Update version of node pool '{pool_name}' in cluster '{cluster_name}'", args=['update_node_pool_version', pool_name])) # infer node pool management features management: dict = actual_pool["management"] if "management" in actual_pool else {} # ensure auto-repair is ENABLED if "autoRepair" not in management or not management["autoRepair"]: actions.append(DAction( name='enable-node-pool-autorepair', description=f"Enable auto-repair for node pool '{pool_name}' in cluster '{cluster_name}'", args=['enable_node_pool_autorepair', pool_name])) # ensure auto-upgrade is DISABLED if "autoUpgrade" in management and management["autoUpgrade"]: actions.append(DAction( name='disable-node-pool-autoupgrade', description=f"Disable auto-upgrade for node pool '{pool_name}' in cluster '{cluster_name}'", args=['disable_node_pool_autoupgrade', pool_name])) # validate auto-scaling desired_pool_min_size: int = pool['min_size'] if 'min_size' in pool else 1 desired_pool_max_size: int = pool['max_size'] if 'max_size' in pool else desired_pool_min_size actual_autoscaling: dict = actual_pool["autoscaling"] if "autoscaling" in actual_pool else {} actual_autoscaling_enabled: bool = "enabled" in actual_autoscaling and actual_autoscaling["enabled"] actual_autoscaling_min_size: int = actual_autoscaling["minNodeCount"] \ if "minNodeCount" in actual_autoscaling else None actual_autoscaling_max_size: int = actual_autoscaling["maxNodeCount"] \ if "maxNodeCount" in actual_autoscaling else None if not actual_autoscaling_enabled \ or actual_autoscaling_min_size != desired_pool_min_size \ or actual_autoscaling_max_size != desired_pool_max_size: actions.append( DAction( name='configure-node-pool-autoscaling', description=f"Configure auto-scaling of node pool '{pool_name}' in cluster '{cluster_name}'", args=['configure_node_pool_autoscaling', pool_name, str(desired_pool_min_size), str(desired_pool_max_size)])) # infer node VM configuration pool_cfg: dict = actual_pool['config'] if 'config' in actual_pool else {} # validate node pool service account if 'service_account' in pool: desired_service_account: str = pool['service_account'] actual_service_account: str = pool_cfg[ 'serviceAccount'] if 'serviceAccount' in pool_cfg else 'default' if desired_service_account != actual_service_account: raise Exception( f"Node pool '{pool_name}' service account is '{actual_service_account}' instead of " f"'{desired_service_account}' (updating the service account is not allowed in GKE APIs)") # validate node pool OAuth scopes if 'oauth_scopes' in pool: desired_oauth_scopes: Sequence[str] = pool['oauth_scopes'] actual_oauth_scopes: Sequence[str] = pool_cfg["oauthScopes"] if 'oauthScopes' in pool_cfg else [] if desired_oauth_scopes != actual_oauth_scopes: raise Exception( f"Node pool '{pool_name}' OAuth scopes are {actual_oauth_scopes} instead of " f"{desired_oauth_scopes} (updating OAuth scopes is not allowed in GKE APIs unfortunately)") # validate node pool preemptible usage if 'preemptible' in pool: desired_preemptible: bool = pool['preemptible'] actual_preemptible: bool = pool_cfg['preemptible'] if 'preemptible' in pool_cfg else False if desired_preemptible != actual_preemptible: raise Exception(f"GKE node pools APIs do not allow enabling/disabling preemptibles usage mode " f"(required for node pool '{pool_name}' in cluster '{cluster_name}')") # validate machine type if 'machine_type' in pool: desired_machine_type: str = pool['machine_type'] actual_machine_type: str = pool_cfg["machineType"] if "machineType" in pool_cfg else 'n1-standard-1' if desired_machine_type != actual_machine_type: raise Exception( f"Node pool '{pool_name}' uses '{actual_machine_type}' instead of '{desired_machine_type}'. " f"Updating this is not allowed in GKE APIs unfortunately.") # validate machine disk type if 'disk_size_gb' in pool: desired_disk_size_gb: int = pool['disk_size_gb'] actual_disk_size_gb: int = pool_cfg["diskSizeGb"] if "diskSizeGb" in pool_cfg else 100 if desired_disk_size_gb != actual_disk_size_gb: raise Exception( f"Node pool '{pool_name}' allocates {actual_disk_size_gb}GB disk space instead of " f"{desired_disk_size_gb}GB. Updating this is not allowed in GKE APIs unfortunately.") # validate network tags if 'tags' in pool: desired_tags: Sequence[str] = pool['tags'] actual_tags: Sequence[str] = pool_cfg["tags"] if "tags" in pool_cfg else [] if desired_tags != actual_tags: raise Exception( f"Node pool '{pool_name}' network tags are '{actual_tags}' instead of '{desired_tags}'. " f"Updating this is not allowed in GKE APIs unfortunately.") # validate GCE metadata if 'metadata' in pool: desired_metadata: Mapping[str, str] = pool['metadata'] actual_metadata: Mapping[str, str] = pool_cfg["metadata"] if "metadata" in pool_cfg else {} if desired_metadata != actual_metadata: raise Exception( f"Node pool '{pool_name}' GCE metadata is '{actual_metadata}' instead of '{desired_metadata}'. " f"Updating this is not allowed in GKE APIs unfortunately.") # validate Kubernetes labels if 'labels' in pool: desired_labels: Mapping[str, str] = pool['labels'] actual_labels: Mapping[str, str] = pool_cfg["labels"] if "labels" in pool_cfg else {} if desired_labels != actual_labels: raise Exception( f"Node pool '{pool_name}' Kubernetes labels are '{actual_labels}' instead of " f"'{desired_labels}'. Updating this is not allowed in GKE APIs unfortunately.") if not actions: # if no actions returned, we are VALID - create authentication for dependant resources self.authenticate(properties=state) return actions
def get_actions_for_missing_state(self) -> Sequence[DAction]: return [DAction(name=f"create-cluster", description=f"Create cluster '{self.info.config['name']}'")]