# -*- coding: utf-8 -*- # # Copyright 2024 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. """Parsers given command arguments for the Cloud Run V2 command surface into configuration changes.""" from googlecloudsdk.calliope import base from googlecloudsdk.command_lib.run import exceptions from googlecloudsdk.command_lib.run import flags from googlecloudsdk.command_lib.run.config_changes import GenerateVolumeName from googlecloudsdk.command_lib.run.v2 import config_changes from googlecloudsdk.command_lib.util.args import repeated from googlecloudsdk.core import config from googlecloudsdk.core import log from googlecloudsdk.core import properties def SecretsFlags(): """Creates flags for creating, updating, and deleting secrets.""" return flags.MapFlagsNoFile( group_help=( 'Specify secrets to provide as environment variables. ' "For example: '--set-secrets=ENV=mysecret:latest," "OTHER_ENV=othersecret:1' " 'will create an environment variable named ENV whose value is the ' "latest version of secret 'mysecret' and an environment variable " "OTHER_ENV whose value is version of 1 of secret 'othersecret'." ), flag_name='secrets', ) def AddSecretsFlags(parser): """Adds flags for creating, updating, and deleting secrets.""" SecretsFlags().AddToParser(parser) def AddCloudSQLFlags(parser): """Add flags for setting CloudSQL stuff.""" repeated.AddPrimitiveArgs( parser, 'WorkerPool', 'cloudsql-instances', 'Cloud SQL instances', auto_group_help=False, additional_help="""\ These flags modify the Cloud SQL instances this WorkerPool connects to. You can specify a name of a Cloud SQL instance if it's in the same project and region as your Cloud Run worker pool; otherwise specify :: for the instance.""", ) def _PrependClientNameAndVersionChange(args, changes): """Set client name and version regardless of whether or not it was specified.""" if 'client_name' in args: is_either_specified = args.IsSpecified('client_name') or args.IsSpecified( 'client_version' ) changes.insert( 0, config_changes.SetClientNameAndVersionChange( client_name=args.client_name if is_either_specified else 'gcloud', client_version=args.client_version if is_either_specified else config.CLOUD_SDK_VERSION, ), ) def _GetResourceLimitsChanges(args, non_ingress_type=False): """Returns the resource limits changes for the given args.""" changes = [] if 'memory' in args and args.memory: changes.append( config_changes.ResourceLimitsChange( memory=args.memory, non_ingress_type=non_ingress_type ) ) if 'cpu' in args and args.cpu: changes.append( config_changes.ResourceLimitsChange( cpu=args.cpu, non_ingress_type=non_ingress_type ) ) if 'gpu' in args and args.gpu: changes.append( config_changes.ResourceLimitsChange( gpu=args.gpu, non_ingress_type=non_ingress_type ) ) if args.gpu == '0': changes.append(config_changes.GpuTypeChange(gpu_type='')) return changes def _GetLabelChanges(args): """Returns the label changes for the given args.""" additions = {} if flags.FlagIsExplicitlySet(args, 'labels'): additions = args.labels elif flags.FlagIsExplicitlySet(args, 'update_labels'): additions = args.update_labels subtractions = ( args.remove_labels if flags.FlagIsExplicitlySet(args, 'remove_labels') else [] ) return config_changes.LabelChange( additions=additions, subtractions=subtractions, clear_labels=args.clear_labels if 'clear_labels' in args else False, ) def _HasNetworkChanges(args): """Returns true iff any network changes are specified.""" network_flags = [ 'vpc_egress', 'network', 'subnet', 'network_tags', 'clear_network', 'clear_network_tags', ] return flags.HasChanges(args, network_flags) def _GetNetworkChange(args): return config_changes.VpcAccessChanges( vpc_egress=args.vpc_egress, network=args.network, subnet=args.subnet, network_tags=args.network_tags if flags.FlagIsExplicitlySet(args, 'network_tags') else [], clear_network=args.clear_network if 'clear_network' in args else False, clear_network_tags=args.clear_network_tags if 'clear_network_tags' in args else False, ) def _HasCmekKeyChanges(args): """Returns true iff any CMEK key changes are specified.""" cmek_key_flags = [ 'key', 'post_key_revocation_action_type', 'encryption_key_shutdown_hours', 'clear_key', 'clear_post_key_revocation_action_type', 'clear_encryption_key_shutdown_hours', ] return flags.HasChanges(args, cmek_key_flags) def _GetCmekKeyChange(args): return config_changes.CmekKeyChanges( key=args.key if flags.FlagIsExplicitlySet(args, 'key') else None, post_key_revocation_action_type=args.post_key_revocation_action_type if flags.FlagIsExplicitlySet(args, 'post_key_revocation_action_type') else None, encryption_key_shutdown_hours=int(args.encryption_key_shutdown_hours) if flags.FlagIsExplicitlySet(args, 'encryption_key_shutdown_hours') else None, clear_key=flags.FlagIsExplicitlySet(args, 'clear_key'), clear_post_key_revocation_action_type=flags.FlagIsExplicitlySet( args, 'clear_post_key_revocation_action_type' ), clear_encryption_key_shutdown_hours=flags.FlagIsExplicitlySet( args, 'clear_encryption_key_shutdown_hours' ), ) def _GetSecretsChanges(args, non_ingress_type=False, container_name=None): """Returns the secrets changes for the given args.""" changes = [] updates = flags.StripKeys( getattr(args, 'update_secrets', None) or args.set_secrets or {} ) for key in updates: # Secrets volume mount is not supported for Worker Pools yet. if key.startswith('/'): raise exceptions.ConfigurationError( 'Secrets volume mount is not supported for Worker Pools yet.' ) removes = flags.MapLStrip(getattr(args, 'remove_secrets', None) or []) for key in removes: # Secrets volume mount is not supported for Worker Pools yet. if key.startswith('/'): raise exceptions.ConfigurationError( 'Secrets volume mount is not supported for Worker Pools yet.' ) clear_others = bool(args.set_secrets or args.clear_secrets) if updates or removes or clear_others: changes.append( config_changes.SecretsEnvVarChanges( updates=updates, removes=removes, clear_others=clear_others, container_name=container_name, non_ingress_type=non_ingress_type, ) ) return changes def _GetCloudSQLChanges(args): """Returns the Cloud SQL changes for the given args.""" region = flags.GetRegion(args) project = getattr( args, 'project', None ) or properties.VALUES.core.project.Get(required=True) if flags.EnabledCloudSqlApiRequired(args): flags.CheckCloudSQLApiEnablement() # At most one of the cloud sql flags can be set. change = [] if ( flags.FlagIsExplicitlySet(args, 'add_cloudsql_instances') and args.add_cloudsql_instances ): change.append( config_changes.AddCloudSQLChanges( project=project, region=region, add_cloudsql_instances=args.add_cloudsql_instances, ) ) elif ( flags.FlagIsExplicitlySet(args, 'remove_cloudsql_instances') and args.remove_cloudsql_instances ): change.append( config_changes.RemoveCloudSQLChanges( project=project, region=region, remove_cloudsql_instances=args.remove_cloudsql_instances, ) ) elif ( flags.FlagIsExplicitlySet(args, 'clear_cloudsql_instances') and args.clear_cloudsql_instances ): change.append(config_changes.ClearCloudSQLChanges()) elif ( flags.FlagIsExplicitlySet(args, 'set_cloudsql_instances') and args.set_cloudsql_instances ): change.append( config_changes.SetCloudSQLChanges( project=project, region=region, set_cloudsql_instances=args.set_cloudsql_instances, ) ) return change def _GetContainerConfigurationChanges( container_args, container_name=None, non_ingress_type=True ): """Returns per-container configuration changes.""" changes = [] # FlagIsExplicitlySet can't be used here because args.image is also set from # code in deploy.py. if hasattr(container_args, 'image') and container_args.image is not None: changes.append( config_changes.ImageChange( container_args.image, container_name=container_name, non_ingress_type=non_ingress_type, ) ) if flags.HasEnvChanges(container_args): changes.append( _GetEnvChanges( container_args, container_name=container_name, non_ingress_type=non_ingress_type, ) ) if container_args.IsSpecified('cpu'): changes.append( config_changes.ResourceLimitsChange( cpu=container_args.cpu, container_name=container_name, non_ingress_type=non_ingress_type, ) ) if container_args.IsSpecified('memory'): changes.append( config_changes.ResourceLimitsChange( memory=container_args.memory, container_name=container_name, non_ingress_type=non_ingress_type, ) ) if container_args.IsSpecified('command'): # Allow passing an empty string here to reset the field changes.append( config_changes.ContainerCommandChange( container_args.command, container_name=container_name, non_ingress_type=non_ingress_type, ) ) if container_args.IsSpecified('args'): # Allow passing an empty string here to reset the field changes.append( config_changes.ContainerArgsChange( container_args.args, container_name=container_name, non_ingress_type=non_ingress_type, ) ) if flags.FlagIsExplicitlySet( container_args, 'remove_volume_mount' ) or flags.FlagIsExplicitlySet(container_args, 'clear_volume_mounts'): changes.append( config_changes.RemoveVolumeMountChange( removed_mounts=container_args.remove_volume_mount, clear_mounts=container_args.clear_volume_mounts, container_name=container_name, non_ingress_type=non_ingress_type, ) ) if flags.HasSecretsChanges(container_args): changes.extend( _GetSecretsChanges( container_args, container_name=container_name, non_ingress_type=non_ingress_type, ) ) if flags.FlagIsExplicitlySet(container_args, 'add_volume_mount'): changes.append( config_changes.AddVolumeMountChange( new_mounts=container_args.add_volume_mount, container_name=container_name, non_ingress_type=non_ingress_type, ) ) return changes def _GetTemplateConfigurationChanges( args, release_track, non_ingress_type=False ): """Returns a list of changes shared by multiple resources, based on the flags set.""" changes = [] # Revision name suffix if flags.FlagIsExplicitlySet(args, 'revision_suffix'): changes.append(config_changes.RevisionNameChange(args.revision_suffix)) if flags.FlagIsExplicitlySet(args, 'mesh'): changes.append( config_changes.MeshChange( project=properties.VALUES.core.project.Get(required=True), mesh_name=args.mesh, ) ) if _HasNetworkChanges(args): changes.append(_GetNetworkChange(args)) if _HasCmekKeyChanges(args): changes.append(_GetCmekKeyChange(args)) # Service account if 'service_account' in args and args.service_account: changes.append( config_changes.ServiceAccountChange( service_account=args.service_account ) ) # FlagIsExplicitlySet can't be used here because args.image is also set from # code in deploy.py once deploy from source is supported. if hasattr(args, 'image') and args.image is not None: changes.append( config_changes.ImageChange( args.image, non_ingress_type=non_ingress_type ) ) if 'command' in args and args.command is not None: changes.append( config_changes.ContainerCommandChange( args.command, non_ingress_type=non_ingress_type ) ) if 'args' in args and args.args is not None: changes.append( config_changes.ContainerArgsChange( args.args, non_ingress_type=non_ingress_type ) ) if flags.HasEnvChanges(args): changes.append(_GetEnvChanges(args, non_ingress_type=non_ingress_type)) # Add cpu, memory and gpu limits changes. changes.extend( _GetResourceLimitsChanges(args, non_ingress_type=non_ingress_type) ) if 'gpu_type' in args and args.gpu_type: changes.append(config_changes.GpuTypeChange(gpu_type=args.gpu_type)) if flags.FlagIsExplicitlySet(args, 'gpu_zonal_redundancy'): changes.append( config_changes.GpuZonalRedundancyChange( gpu_zonal_redundancy=args.gpu_zonal_redundancy ) ) # Cloud SQL changes if flags.HasCloudSQLChanges(args): changes.extend(_GetCloudSQLChanges(args)) # Volumes / Volume Mounts / Secrets changes if flags.FlagIsExplicitlySet( args, 'remove_volume_mount' ) or flags.FlagIsExplicitlySet(args, 'clear_volume_mounts'): changes.append( config_changes.RemoveVolumeMountChange( removed_mounts=args.remove_volume_mount, clear_mounts=args.clear_volume_mounts, non_ingress_type=non_ingress_type, ) ) if ( flags.FlagIsExplicitlySet(args, 'remove_volume') and args.remove_volume ) or ( flags.FlagIsExplicitlySet(args, 'clear_volumes') and args.clear_volumes ): changes.append( config_changes.RemoveVolumeChange( args.remove_volume, args.clear_volumes ) ) if flags.HasSecretsChanges(args): changes.extend(_GetSecretsChanges(args, non_ingress_type=non_ingress_type)) if flags.FlagIsExplicitlySet(args, 'add_volume') and args.add_volume: # Volume names must be generated before calling AddVolumeChange _ValidateAndMaybeGenerateVolumeNames(args, release_track) changes.append( config_changes.AddVolumeChange(args.add_volume, release_track) ) _MaybeAddVolumeMountChange(args, changes, release_track) if ( flags.FlagIsExplicitlySet(args, 'add_volume_mount') and args.add_volume_mount ): changes.append( config_changes.AddVolumeMountChange( new_mounts=args.add_volume_mount, non_ingress_type=non_ingress_type, ) ) if flags.FlagIsExplicitlySet(args, 'remove_containers'): changes.append( config_changes.RemoveContainersChange(args.remove_containers) ) # Add an empty ContainerDependenciesChange to update dependencies. changes.append(config_changes.ContainerDependenciesChange()) # Per container changes if flags.FlagIsExplicitlySet(args, 'containers'): for container_name, container_args in args.containers.items(): changes.extend( _GetContainerConfigurationChanges( container_args, container_name=container_name ) ) # Dependencies if flags.FlagIsExplicitlySet(args, 'containers'): # TODO: b/393482156 - Add support for per container config changes. dependency_changes = { container_name: container_args.depends_on for container_name, container_args in args.containers.items() if container_args.IsSpecified('depends_on') } if dependency_changes: changes.append( config_changes.ContainerDependenciesChange(dependency_changes) ) return changes def _GetEnvChanges(args, **kwargs): """Returns the env var literal changes for the given args.""" return config_changes.EnvVarLiteralChanges( updates=flags.StripKeys( getattr(args, 'update_env_vars', None) or args.set_env_vars or args.env_vars_file or {} ), removes=flags.MapLStrip(getattr(args, 'remove_env_vars', None) or []), clear_others=bool( args.set_env_vars or args.env_vars_file or args.clear_env_vars ), **kwargs, ) def _HasWorkerPoolScalingChanges(args): """Returns true iff any worker pool scaling changes are specified.""" scaling_flags = [ 'instances', 'scaling', ] return flags.HasChanges(args, scaling_flags) def _GetWorkerPoolInstancesChange(args): """Return the changes for instances for Worker Pools for the given args.""" # --scaling flag is deprecated for Worker Pools in favor of --instances. We # will allow the --scaling flag to be used for instance counts, # to minimize disruption to existing users. However, if the user specifies # the automatic scaling parameter, we will throw an error. if ( 'scaling' in args and args.scaling is not None and args.scaling.auto_scaling ): raise exceptions.ConfigurationError( 'automtatic scaling is deprecated for Worker Pools. Please use a' ' positive number for --instances flag instead to specify a fixed' ' instance count.' ) # If the newer --instances flag is specified, use that. # Otherwise, check for --scaling flag for existing users. instances = None if 'instances' in args and args.instances is not None: instances = args.instances elif 'scaling' in args and args.scaling is not None: log.warning( 'The --scaling flag is deprecated for Worker Pools. Please use the' ' --instances flag instead to specify a fixed instance count.' ) instances = flags.InstanceValue(value=args.scaling.instance_count) return config_changes.WorkerPoolInstancesChange(instances=instances) def _HasBinaryAuthorizationChanges(args): """Returns true iff any binary authorization changes are specified.""" bin_auth_flags = [ 'binary_authorization', 'clear_binary_authorization', 'breakglass', ] return flags.HasChanges(args, bin_auth_flags) def _GetBinaryAuthorizationChanges(args): """Returns the binary authorization changes for the given args.""" changes = [] if flags.FlagIsExplicitlySet(args, 'binary_authorization'): changes.append( config_changes.BinaryAuthorizationChange( policy=args.binary_authorization ) ) if flags.FlagIsExplicitlySet(args, 'clear_binary_authorization'): changes.append( config_changes.BinaryAuthorizationChange( clear_binary_authorization=True ) ) if flags.FlagIsExplicitlySet(args, 'breakglass'): changes.append( config_changes.BinaryAuthorizationChange( breakglass_justification=args.breakglass ) ) return changes def _GetInstanceSplitChanges(args): """Returns the instance split changes for the given args.""" if args.to_latest: # Mutually exclusive flag with to-revisions return config_changes.InstanceSplitChange(to_latest=True) elif args.to_revisions: return config_changes.InstanceSplitChange(to_revisions=args.to_revisions) def _ValidateAndMaybeGenerateVolumeNames(args, release_track): """Validates used of the volumes shortcut and generates volume names when needed. Specifically, it checks that the 'mount-path' parameter is not being used with the --containers flag and that the volume type is an allowed type. If validation succeeds and the volume also needs a name, one is generated. Args: args: The argparse namespace containing the parsed command line arguments. release_track: The current release track (e.g., base.ReleaseTrack.ALPHA). """ uses_containers_flag = flags.FlagIsExplicitlySet(args, 'containers') if release_track != base.ReleaseTrack.GA: for volume in args.add_volume: # If mount-path is specified, the user is attempting to use the volumes # shortcut. if 'mount-path' in volume: # The volumes shortcut is not compatible with the --containers flag. if uses_containers_flag: raise exceptions.ConfigurationError( 'When using the --containers flag, "mount-path" cannot be' ' specified under the --add-volume flag. Instead, specify' ' "mount-path" using the --add-volume-mount flag after the' ' --container flag of the container the volume should be' ' mounted to.' ) # Generate a name if the user has not specified one. if 'name' not in volume: volume['name'] = GenerateVolumeName(volume['type']) def _MaybeAddVolumeMountChange(args, changes, release_track): """Adds a VolumeMountChange to the list of changes if applicable. This function checks if new volume mounts should be added based on the `--add-volume` flag in ALPHA release track. If a volume in `args.add_volume` has a 'mount-path', a corresponding AddVolumeMountChange is appended to the `changes` list. Args: args: The argparse namespace containing the parsed command line arguments. changes: A list of configuration changes to append to. release_track: The current release track (e.g., base.ReleaseTrack.ALPHA). """ if release_track != base.ReleaseTrack.GA: new_volume_mounts = [] for volume in args.add_volume: if 'mount-path' in volume and 'name' in volume: volume_mount_args = { 'volume': volume['name'], 'mount-path': volume['mount-path'], } new_volume_mounts.append(volume_mount_args) if new_volume_mounts: changes.append( config_changes.AddVolumeMountChange( new_mounts=new_volume_mounts, ) ) def GetWorkerPoolConfigurationChanges(args, release_track): """Returns a list of changes to the worker pool config, based on the flags set.""" changes = [] # Description if flags.FlagIsExplicitlySet(args, 'description'): changes.append(config_changes.DescriptionChange(args.description)) # Labels if flags.HasLabelChanges(args): changes.append(_GetLabelChanges(args)) # Binary authorization if _HasBinaryAuthorizationChanges(args): changes.extend(_GetBinaryAuthorizationChanges(args)) # Template changes changes.extend( _GetTemplateConfigurationChanges( args, release_track, non_ingress_type=True ) ) # Worker pool scaling if _HasWorkerPoolScalingChanges(args): changes.append(_GetWorkerPoolInstancesChange(args)) if flags.HasInstanceSplitChanges(args): changes.append(_GetInstanceSplitChanges(args)) if 'no_promote' in args and args.no_promote: changes.append(config_changes.NoPromoteChange()) _PrependClientNameAndVersionChange(args, changes) return changes