📁 //snap/google-cloud-cli/current/lib/surface/run/deploy.py
# -*- coding: utf-8 -*- #
# Copyright 2018 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.
"""Deploy a container to Cloud Run."""
import contextlib
import copy
import enum
import json
import logging
import os
import os.path
import re
import tempfile
from typing import Any, Dict, Tuple
from googlecloudsdk.api_lib.run import api_enabler
from googlecloudsdk.api_lib.run import constants
from googlecloudsdk.api_lib.run import container_resource
from googlecloudsdk.api_lib.run import k8s_object
from googlecloudsdk.api_lib.run import service as service_lib
from googlecloudsdk.api_lib.run import traffic
from googlecloudsdk.calliope import base
from googlecloudsdk.calliope import exceptions as c_exceptions
from googlecloudsdk.command_lib.artifacts import docker_util
from googlecloudsdk.command_lib.run import artifact_registry
from googlecloudsdk.command_lib.run import build_util
from googlecloudsdk.command_lib.run import config_changes
from googlecloudsdk.command_lib.run import connection_context
from googlecloudsdk.command_lib.run import container_parser
from googlecloudsdk.command_lib.run import domain_mapping_util
from googlecloudsdk.command_lib.run import exceptions
from googlecloudsdk.command_lib.run import flags
from googlecloudsdk.command_lib.run import iap_util
from googlecloudsdk.command_lib.run import local_build
from googlecloudsdk.command_lib.run import messages_util
from googlecloudsdk.command_lib.run import platforms
from googlecloudsdk.command_lib.run import pretty_print
from googlecloudsdk.command_lib.run import resource_args
from googlecloudsdk.command_lib.run import resource_change_validators
from googlecloudsdk.command_lib.run import serverless_operations
from googlecloudsdk.command_lib.run import stages
from googlecloudsdk.command_lib.run import validators
from googlecloudsdk.command_lib.run.sourcedeploys import source_container_context
from googlecloudsdk.command_lib.run.sourcedeploys import sources
from googlecloudsdk.command_lib.util.args import map_util
from googlecloudsdk.command_lib.util.concepts import concept_parsers
from googlecloudsdk.command_lib.util.concepts import presentation_specs
from googlecloudsdk.core import properties
from googlecloudsdk.core.console import progress_tracker
from googlecloudsdk.core.resource import resource_printer
_GPU_BUILD_MACHINE_TYPE = 'E2_HIGHCPU_8'
class BuildType(enum.Enum):
DOCKERFILE = 'Dockerfile'
BUILDPACKS = 'Buildpacks'
def ContainerArgGroup(release_track=base.ReleaseTrack.GA):
"""Returns an argument group with all per-container deploy args."""
help_text = """
Container Flags
The following flags apply to a single container. If the --container flag is
specified these flags may only be specified after a --container flag. Otherwise
they will apply to the primary ingress container.
"""
group = base.ArgumentGroup(help=help_text)
group.AddArgument(flags.PortArg())
group.AddArgument(flags.Http2Flag())
group.AddArgument(flags.MutexEnvVarsFlags(release_track))
group.AddArgument(flags.MemoryFlag())
group.AddArgument(flags.CpuFlag())
group.AddArgument(flags.ArgsFlag())
group.AddArgument(flags.WorkdirFlag())
if release_track != base.ReleaseTrack.GA:
group.AddArgument(flags.SandboxLauncherFlag())
if release_track == base.ReleaseTrack.ALPHA:
group.AddArgument(flags.SandboxFlags())
group.AddArgument(flags.SecretsFlags())
group.AddArgument(flags.DependsOnFlag())
group.AddArgument(flags.AddVolumeMountFlag())
group.AddArgument(flags.RemoveVolumeMountFlag())
group.AddArgument(flags.ClearVolumeMountsFlag())
group.AddArgument(flags.AddCommandAndFunctionFlag())
group.AddArgument(flags.BaseImageArg())
group.AddArgument(flags.AutomaticUpdatesFlag())
group.AddArgument(flags.BuildServiceAccountMutexGroup())
group.AddArgument(flags.BuildWorkerPoolMutexGroup())
group.AddArgument(flags.MutexBuildEnvVarsFlags())
group.AddArgument(
flags.SourceAndImageFlags(
mutex=False,
no_build_enabled=True,
local_build_enabled=(release_track == base.ReleaseTrack.ALPHA),
release_track=release_track,
)
)
group.AddArgument(flags.StartupProbeFlag())
group.AddArgument(flags.LivenessProbeFlag())
group.AddArgument(flags.ReadinessProbeFlag())
group.AddArgument(flags.GpuFlag())
return group
@base.UniverseCompatible
@base.ReleaseTracks(base.ReleaseTrack.GA)
class Deploy(base.Command):
"""Create or update a Cloud Run service."""
detailed_help = {
'DESCRIPTION': (
"""\
Creates or updates a Cloud Run service.
"""
),
'EXAMPLES': (
"""\
To deploy a container to the service `my-backend` on Cloud Run:
$ {command} my-backend --image=us-docker.pkg.dev/project/image
You may also omit the service name. Then a prompt will be displayed
with a suggested default value:
$ {command} --image=us-docker.pkg.dev/project/image
To deploy to Cloud Run on Kubernetes Engine, you need to specify a cluster:
$ {command} --image=us-docker.pkg.dev/project/image --cluster=my-cluster
"""
),
}
@classmethod
def CommonArgs(cls, parser):
flags.AddAllowUnauthenticatedFlag(parser)
flags.AddAllowUnencryptedBuildFlag(parser)
flags.AddBinAuthzPolicyFlags(parser)
flags.AddBinAuthzBreakglassFlag(parser)
flags.AddCloudSQLFlags(parser)
flags.AddCmekKeyFlag(parser)
flags.AddCmekKeyRevocationActionTypeFlag(parser)
flags.AddConcurrencyUtilizationFlag(parser)
flags.AddCpuThrottlingFlag(parser)
flags.AddCpuUtilizationFlag(parser)
flags.AddCustomAudiencesFlag(parser)
flags.AddDefaultUrlFlag(parser)
flags.AddDeployHealthCheckFlag(parser)
flags.AddDescriptionFlag(parser)
flags.AddEgressSettingsFlag(parser)
flags.AddEncryptionKeyShutdownHoursFlag(parser)
flags.AddGpuTypeFlag(parser)
flags.GpuZonalRedundancyFlag(parser)
flags.AddRevisionSuffixArg(parser)
flags.AddSandboxArg(parser)
flags.AddSessionAffinityFlag(parser)
flags.AddStartupCpuBoostFlag(parser)
flags.AddVpcConnectorArgs(parser)
flags.AddVpcNetworkGroupFlagsForUpdate(parser)
flags.RemoveContainersFlag().AddToParser(parser)
flags.AddVolumesFlags(parser, cls.ReleaseTrack())
flags.AddServiceMinMaxInstancesFlag(parser)
flags.AddInvokerIamCheckFlag(parser)
flags.AddScalingFlag(parser, release_track=cls.ReleaseTrack())
# Flags specific to connecting to a cluster
flags.AddEndpointVisibilityEnum(parser)
flags.CONFIG_MAP_FLAGS.AddToParser(parser)
# Flags not specific to any platform
service_presentation = presentation_specs.ResourcePresentationSpec(
'SERVICE',
resource_args.GetServiceResourceSpec(
prompt=True,
allow_project_prompt=True,
),
'Service to deploy to.',
required=True,
prefixes=False,
)
if cls.ReleaseTrack() != base.ReleaseTrack.GA:
flags.AddDryRunFlag(parser)
flags.AddPlatformAndLocationFlags(parser)
flags.AddConcurrencyFlag(parser)
flags.AddTimeoutFlag(parser)
flags.AddAsyncFlag(parser)
flags.AddLabelsFlags(parser)
flags.AddGeneralAnnotationFlags(parser)
flags.AddMinInstancesFlag(parser)
flags.AddMaxInstancesFlag(parser)
flags.AddNoTrafficFlag(parser)
flags.AddDeployTagFlag(parser)
flags.AddServiceAccountFlag(parser)
flags.AddClientNameAndVersionFlags(parser)
flags.AddIngressFlag(parser)
flags.AddRegionsArg(parser)
flags.AddIapFlag(parser)
concept_parsers.ConceptParser([service_presentation]).AddToParser(parser)
# No output by default, can be overridden by --format
parser.display_info.AddFormat('none')
@classmethod
def Args(cls, parser):
cls.CommonArgs(parser)
container_args = ContainerArgGroup(cls.ReleaseTrack())
container_parser.AddContainerFlags(
parser, container_args, cls.ReleaseTrack()
)
def GetAllowUnauth(self, args, operations, service_ref, service_exists):
"""Returns allow_unauth value for a service change.
Args:
args: argparse.Namespace, Command line arguments
operations: serverless_operations.ServerlessOperations, Serverless client.
service_ref: protorpc.messages.Message, A resource reference object for
the service See googlecloudsdk.core.resources.Registry.ParseResourceId
for details.
service_exists: True if the service being changed already exists.
Returns:
allow_unauth value where
True means to enable unauthenticated access for the service.
False means to disable unauthenticated access for the service.
None means to retain the current value for the service.
"""
if (
flags.FlagIsExplicitlySet(args, 'preset')
and args.preset['name'] in flags.PRIVATE_ACCESS_PRESETS
):
return False
if (
flags.FlagIsExplicitlySet(args, 'preset')
and args.preset['name'] in flags.PUBLIC_ACCESS_PRESETS
):
return True
if self._IsMultiRegion():
regions = self._GetRegionsForMultiRegion()
allow_unauth = flags.GetAllowUnauthenticated(
args,
operations,
service_ref,
not service_exists,
region_override=regions[0] if regions else None,
)
# Avoid failure removing a policy binding for a service that
# doesn't exist.
if not service_exists and not allow_unauth:
return None
return allow_unauth
allow_unauth = None
if platforms.GetPlatform() == platforms.PLATFORM_MANAGED:
allow_unauth = flags.GetAllowUnauthenticated(
args, operations, service_ref, not service_exists
)
# Avoid failure removing a policy binding for a service that
# doesn't exist.
if not service_exists and not allow_unauth:
allow_unauth = None
return allow_unauth
def _ValidateAndGetContainers(self, args):
if flags.FlagIsExplicitlySet(args, 'containers'):
containers = args.containers
# TODO(b/436350694): Change this to use preset metadata once it is
# implemented.
if (
flags.FlagIsExplicitlySet(args, 'preset')
and args.preset['name'] in flags.INGRESS_CONTAINER_PRESETS
):
specified_args = getattr(
containers[args.preset['name']], '_specified_args'
)
specified_args['image'] = '--image'
setattr(
containers[args.preset['name']], '_specified_args', specified_args
)
setattr(containers[args.preset['name']], 'image', 'imageplaceholder')
setattr(containers[args.preset['name']], 'automatic_updates', None)
else:
containers = {'': args}
validators.ValidateIngressContainer(containers)
validators.ValidateContainerLimit(containers)
return containers
def _ValidateAndGetSourceContainer(
self,
containers: Dict[str, Any],
enable_unified_build: bool = False,
) -> Tuple[Dict[str, Any], Any, Any]:
"""Validates the source container flags and returns the source containers.
Args:
containers: A map of container name to container arguments.
enable_unified_build: Whether to use unified build. When unified build is
enabled, multiple source containers are allowed and no legacy build is
allowed.
Returns:
A tuple of (source_containers, legacy_build_container,
local_build_container). source_containers is a map of container name to
container arguments. legacy_build_container is the legacy source
container arguments. local_build_container is the local build source
container arguments.
The container arguments will have a local_context attribute added to them
to store local context.
"""
# get potential source containers before validation
source_containers = {
name: container
for name, container in containers.items()
if (
not container.IsSpecified('image')
or flags.FlagIsExplicitlySet(container, 'source')
)
}
validators.ValidateImplicitSourceContainers(source_containers)
validators.ValidateUnifiedBuildProperty(
source_containers, containers, self.ReleaseTrack()
)
validators.ValidateNoAutomaticUpdatesForContainers(
source_containers, containers, self.ReleaseTrack()
)
validators.ValidateNoBuildFromSource(source_containers, self.ReleaseTrack())
# add local context to each source container for easier local context
# passing between functions
for name, container_args in source_containers.items():
container_args.local_context = (
source_container_context.SourceContainerContext(
name=name,
source=container_args.source,
upload_through_run_api=sources.ShouldUploadThroughRunApi(
container_args, self.ReleaseTrack()
),
)
)
local_build_container = validators.GetValidLocalBuildSourceContainer(
self.ReleaseTrack(), source_containers
)
legacy_build_container = self._GetLegacyBuildContainer(
source_containers, enable_unified_build
)
# If legacy build is used, only one source container is allowed.
if legacy_build_container and len(source_containers) > 1:
raise c_exceptions.InvalidArgumentException(
'--source',
'At most one source container is allowed when building from source.',
)
return (
source_containers,
legacy_build_container,
local_build_container,
)
def _GetLegacyBuildContainer(
self, source_containers: Dict[str, Any], enable_unified_build: bool
):
"""Returns the legacy source container."""
if enable_unified_build:
return None
legacy_build_source_container = []
for _, container in source_containers.items():
if not getattr(container, 'no_build', False) and not getattr(
container, 'local_build', False
):
legacy_build_source_container.append(container)
if len(legacy_build_source_container) > 1:
raise c_exceptions.InvalidArgumentException(
'--source', 'Multiple source build containers are not supported.'
)
return (
legacy_build_source_container[0]
if legacy_build_source_container
else None
)
def _SetIngressContainerName(
self, service: Any, source_containers: Dict[str, Any]
):
"""Sets the ingress container name."""
if not service:
return
ingress_container_name = service.template.container.name
# When a container has empty name (--container not used), it is considered
# as the ingress container.
if '' in source_containers and ingress_container_name:
container_args = source_containers.pop('')
container_args.local_context.name = ingress_container_name
source_containers[ingress_container_name] = container_args
def _GetBaseImageForSourceContainer(self, container_args, service):
"""Returns the base image for the container.
Args:
container_args: command line arguments for container that is build from
source
service: existing Cloud run service which could be None.
Returns:
base_image: string. Base image of the container.
"""
base_image = getattr(container_args, 'base_image', None)
clear_base_image = getattr(container_args, 'clear_base_image', None)
if base_image:
return base_image
# If service exists, check existing base_image annotation and populate
# the value if --clear-base-image is not set
if (
base_image is None
and service is not None
and service_lib.RUN_FUNCTIONS_BUILD_BASE_IMAGE in service.annotations
and not clear_base_image
):
return service.annotations[service_lib.RUN_FUNCTIONS_BUILD_BASE_IMAGE]
return service_lib.DEFAULT_BASE_IMAGE
def _GetAutomaticUpdates(self, container_args, service):
"""Returns the automatic updates for the container."""
automatic_updates = getattr(container_args, 'automatic_updates', None)
clear_base_image = getattr(container_args, 'clear_base_image', None)
base_image = self._GetBaseImageForSourceContainer(container_args, service)
if automatic_updates is not None:
return automatic_updates
if clear_base_image:
# Clear base image will disable automatic updates.
return False
if service is None:
# When it's new service, when users provide
# --base-image, it implies automatic updates = True.
return base_image != service_lib.DEFAULT_BASE_IMAGE
# TODO(b/365567914): Delete elif statement once the new annotations are in.
if (
service_lib.RUN_FUNCTIONS_BUILD_ENABLE_AUTOMATIC_UPDATES
in service.annotations
):
# When there is existing service, use the annotation value.
# Because the annotation is sticky. Users need
# to explicitly set --automatic-updates to change it.
automatic_updates_annotation = service.annotations[
service_lib.RUN_FUNCTIONS_BUILD_ENABLE_AUTOMATIC_UPDATES
]
return True if automatic_updates_annotation.lower() == 'true' else False
elif (
service_lib.RUN_FUNCTIONS_ENABLE_AUTOMATIC_UPDATES_DEPRECATED
in service.annotations
):
automatic_updates_annotation = service.annotations[
service_lib.RUN_FUNCTIONS_ENABLE_AUTOMATIC_UPDATES_DEPRECATED
]
return True if automatic_updates_annotation.lower() == 'true' else False
return automatic_updates
def _GetMachineType(self, container_args, service):
"""Gets the default pool build machine type to use based on the user flags and annotations.
User flags take precedence over annotations for an existing service.
Args:
container_args: base.ArgumentGroup, Container arguments using source
build.
service: Service, existing Cloud run service.
Returns:
build_machine_type or
None meaning default machine type should be used
"""
gpu_type = getattr(container_args, 'gpu_type', None)
gpu_count = getattr(container_args, 'gpu', None)
if gpu_count is not None and gpu_count == '0':
return None
if gpu_type is not None or gpu_count is not None:
return _GPU_BUILD_MACHINE_TYPE
if service is not None:
if k8s_object.GPU_TYPE_NODE_SELECTOR in service.template.node_selector:
accelerator_value = service.template.node_selector[
k8s_object.GPU_TYPE_NODE_SELECTOR
]
if accelerator_value:
return _GPU_BUILD_MACHINE_TYPE
return None
def _BuildFromSource(
self,
args,
legacy_build_container,
legacy_build_context,
service_ref,
conn_context,
platform,
skip_activation_prompt,
service,
):
# Get run functions annotations to make values sticky
(
annotated_build_service_account,
annotated_build_worker_pool,
annotated_build_env_vars,
annotated_build_image_uri,
) = (
service.run_functions_annotations
if service
else (None, None, None, None)
)
name, container_args = (
legacy_build_container.local_context.name,
legacy_build_container,
)
# If service exists and it's GCF's worker container, use the name.
if not name and service:
name = service.template.container.name or ''
pack = None
changes = []
source = container_args.source
source_bucket = (
self._GetSourceBucketFromBuildSourceLocation(service.source_location)
if service
else None
)
logging.debug('Existing source_bucket: %s', source_bucket)
# We cannot use flag.isExplicitlySet(args, 'function') because it will
# return False when user provide --function after --container.
is_function = container_args.function
# Get the AR repo from flags or annotations if they exist, otherwise return
# the repository to create by default.
docker_string, repo_to_create = self._GetArtifactRegistryRepository(
args,
conn_context,
platform,
skip_activation_prompt,
container_args,
annotated_build_image_uri,
service_ref,
)
# The image is built with latest tag. After build, the image digest
# from the build result will be added to the image of the service spec.
container_args.image = '{repo}/{service}'.format(
repo=docker_string, service=service_ref.servicesId
)
# Use GCP Buildpacks if Dockerfile doesn't exist
docker_file = source + '/Dockerfile'
base_image = self._GetBaseImageForSourceContainer(container_args, service)
automatic_updates = self._GetAutomaticUpdates(container_args, service)
build_machine_type = self._GetMachineType(container_args, service)
if os.path.exists(docker_file):
build_type = BuildType.DOCKERFILE
# TODO(b/310727875): check --function is not provided
# Check whether base_image is provided by user
if flags.FlagIsExplicitlySet(container_args, 'base_image'):
raise c_exceptions.InvalidArgumentException(
'--base-image',
'Base image is not supported for services built from Dockerfile.',
)
# Base image is sticky annotation.
# Check whether there was base_image provided in previous revision
# even if users are not providing it in this deployment.
if base_image != service_lib.DEFAULT_BASE_IMAGE:
raise c_exceptions.RequiredArgumentException(
'--clear-base-image',
'Base image is not supported for services built from Dockerfile. To'
' continue the deployment, please use --clear-base-image to clear'
' the base image.',
)
else:
pack, changes = build_util.CreateBuildPack(container_args)
build_type = BuildType.BUILDPACKS
image = None if pack else container_args.image
if base_image is not None and base_image != service_lib.DEFAULT_BASE_IMAGE:
image = pack[0].get('image') if pack else image
build_service_account = build_util.GetBuildServiceAccount(
args, annotated_build_service_account, container_args, service, changes
)
operation_message = (
'Building using {build_type} and deploying container to'
).format(build_type=build_type.value)
build_worker_pool = build_util.GetBuildWorkerPool(
args, annotated_build_worker_pool, service, changes
)
old_build_env_vars = (
json.loads(annotated_build_env_vars)
if annotated_build_env_vars
else None
)
build_env_var_flags = map_util.GetMapFlagsFromArgs('build-env-vars', args)
build_env_vars = map_util.ApplyMapFlags(
old_build_env_vars, **build_env_var_flags
)
legacy_build_context.build_image = image
legacy_build_context.build_pack = pack
legacy_build_context.build_source = source
legacy_build_context.repo_to_create = repo_to_create
legacy_build_context.is_function = is_function
legacy_build_context.base_image = base_image
legacy_build_context.build_service_account = build_service_account
legacy_build_context.build_env_vars = build_env_vars
legacy_build_context.build_worker_pool = build_worker_pool
legacy_build_context.build_machine_type = build_machine_type
legacy_build_context.deploy_from_source_container_name = name
legacy_build_context.enable_automatic_updates = automatic_updates
legacy_build_context.source_bucket = source_bucket
return (operation_message, changes)
def _GetArtifactRegistryRepository(
self,
args,
conn_context,
platform,
skip_activation_prompt,
container_args,
annotated_build_image_uri,
service_ref,
):
"""Gets the AR repo from flags or annotations if they exist, otherwise return the repository to create.
Args:
args: argparse.Namespace, Command line arguments
conn_context: ConnectionInfo object, context to get project location.
platform: properties.VALUES.run.platform, platform to run on and to check
if it is GKE.
skip_activation_prompt: bool, True if the user has already activated the
required APIs.
container_args: base.ArgumentGroup, Container arguments using source
build.
annotated_build_image_uri: str, build image uri from service annotations.
service_ref: ServiceRef, reference to the existing Cloud run service.
Returns:
A string location of the AR repository and the docker_util.DockerRepo
object to create by default if none provided.
"""
repo_to_create = None
if container_args.image:
docker_string = artifact_registry.ValidateAndGetArRepository(
container_args.image,
skip_activation_prompt,
enable_by_default=True,
)
validators.ValidateServiceNameFromImage(
container_args.image, service_ref.servicesId
)
return docker_string, repo_to_create
elif annotated_build_image_uri:
docker_string = artifact_registry.ValidateAndGetArRepository(
annotated_build_image_uri,
skip_activation_prompt,
enable_by_default=True,
)
return docker_string, repo_to_create
else:
ar_repo = docker_util.DockerRepo(
project_id=properties.VALUES.core.project.Get(required=True),
location_id=artifact_registry.RepoRegion(
args,
cluster_location=(
conn_context.cluster_location
if platform == platforms.PLATFORM_GKE
else None
),
),
repo_id='cloud-run-source-deploy',
)
if artifact_registry.ShouldCreateRepository(
ar_repo,
skip_activation_prompt=skip_activation_prompt,
enable_by_default=True,
):
repo_to_create = ar_repo
docker_string = ar_repo.GetDockerString()
return docker_string, repo_to_create
def _GetRegionsForMultiRegion(self):
if self.__multi_region_regions:
return self.__multi_region_regions.split(',')
return None
def _IsMultiRegion(self):
return bool(self.__multi_region_regions)
def _GetSourceBucketFromZipDeploySourceLocation(self, service):
"""Returns the source bucket from the zip deploy source annotation run.googleapis.com/source.
If the annotations has more than one entry, return None.
Args:
service: existing Cloud run service.
"""
if not service:
return None
source_location_map = service.source_deploy_no_build_source_location_map
logging.debug('source_location map: %s', source_location_map)
if not source_location_map:
return None
try:
container_to_source_map = json.loads(source_location_map)
if (
not isinstance(container_to_source_map, dict)
or len(container_to_source_map) != 1
):
logging.debug(
'source_location is not a valid map or has more than one entry'
)
return None
x = re.search(
r'gs://([^/]+)/.*', next(iter(container_to_source_map.values()))
)
if x:
return x.group(1)
logging.debug('source_location does not match pattern gs://([^/]+)/.*')
return None
except (json.JSONDecodeError, TypeError) as e:
logging.debug('failed to parse source location: %s', e)
return None
def _GetSourceBucketFromBuildSourceLocation(self, source_location):
logging.debug('source_location: %s', source_location)
if not source_location:
return None
x = re.search(r'gs://([^/]+)/.*', source_location)
if x:
return x.group(1)
logging.debug('source_location does not match pattern gs://([^/]+)/.*')
return None
def _GetBaseChanges(self, args):
"""Returns the service config changes with some default settings."""
changes = flags.GetServiceConfigurationChanges(args, self.ReleaseTrack())
changes.insert(
0,
config_changes.DeleteAnnotationChange(
k8s_object.BINAUTHZ_BREAKGLASS_ANNOTATION
),
)
changes.append(
config_changes.SetLaunchStageAnnotationChange(self.ReleaseTrack())
)
if self._IsMultiRegion():
changes.append(
# TODO(b/440397578): Use the array here once we merge beta and GA.
config_changes.SetRegionsAnnotationChange(
regions=self.__multi_region_regions
)
)
return changes
def _ConnectionContext(self, args, custom_check_response_func=None):
# Obtaining the connection context prompts the user to select a region if
# one hasn't been provided. We want to do this prior to preparing a source
# deploy so that we can use that region for the Artifact Registry repo.
return connection_context.GetConnectionContext(
args,
flags.Product.RUN,
self.ReleaseTrack(),
is_multiregion=self._IsMultiRegion(),
custom_check_response_func=custom_check_response_func,
)
def _GetTracker(
self,
args,
service,
changes,
source_containers,
repo_to_create,
allow_unauth,
has_latest,
iap,
requires_build,
):
deployment_stages = stages.ServiceStages(
include_iam_policy_set=allow_unauth is not None,
include_route=has_latest,
include_validate_service=requires_build,
include_upload_source=bool(source_containers),
include_build=requires_build,
include_create_repo=repo_to_create is not None,
# Include IAP stage if enabling IAP, or if disabling IAP on an existing
# service (to clean up IAM bindings).
include_iap=(iap or (iap is not None and service is not None)),
include_domain_mapping=getattr(args, 'domain', None) is not None,
regions_list=self._GetRegionsForMultiRegion(),
)
dry_run = getattr(args, 'dry_run', False)
if dry_run:
header = 'Validating'
elif requires_build:
header = 'Building and deploying'
else:
header = 'Deploying'
if service is None:
header += ' new'
if self._IsMultiRegion():
header += ' Multi-Region service'
else:
header += ' service'
# new services default cpu boost on the client
if not flags.FlagIsExplicitlySet(args, 'cpu_boost'):
changes.append(config_changes.StartupCpuBoostChange(cpu_boost=True))
if self._IsMultiRegion():
failure_message = (
'Multi-region deployment failed. Some regions might already be'
' serving traffic.'
)
else:
failure_message = 'Deployment failed'
header += '...'
return progress_tracker.StagedProgressTracker(
header,
deployment_stages,
failure_message=failure_message,
suppress_output=args.async_ or dry_run,
)
def _GetRequiredApis(self, require_build):
apis = []
if self.ReleaseTrack() == base.ReleaseTrack.GA or require_build:
apis.append(api_enabler.get_run_api())
if require_build:
apis.append('artifactregistry.googleapis.com')
apis.append('cloudbuild.googleapis.com')
return apis
def _RequireBuild(self, source_containers):
"""Returns whether the deployment requires a build."""
for _, container_args in source_containers.items():
if not getattr(container_args, 'local_build', False) and not getattr(
container_args, 'no_build', False
):
return True
return False
def _GetSourceContainersContextList(
self, source_containers, legacy_build_container
) -> None | list[source_container_context.SourceContainerContext]:
"""Returns the source containers context list."""
if not source_containers:
return None
if legacy_build_container:
return None
return [
container_args.local_context
for _, container_args in source_containers.items()
]
def _DisplaySuccessMessage(
self, service, args, allow_unauth, operations, service_ref, records=None
):
if getattr(args, 'dry_run', False):
pretty_print.Success(
'Service [{}] has been validated.'.format(service.name)
)
return
if self._IsMultiRegion() and not args.async_:
pretty_print.Success(
messages_util.GetSuccessMessageForMultiRegionSynchronousDeploy(
service, self._GetRegionsForMultiRegion()
)
)
elif args.async_:
pretty_print.Success(
'Service [{{bold}}{serv}{{reset}}] is deploying '
'asynchronously.'.format(serv=service.name)
)
else:
requires_authentication = False
if platforms.GetPlatform() == platforms.PLATFORM_MANAGED and not (
getattr(args, 'dry_run', False)
or service.annotations.get(container_resource.DISABLE_IAM_ANNOTATION)
== container_resource.DISABLE_IAM_ANNOTATION_VALUE_DISABLED
or service.annotations.get(service_lib.INGRESS_ANNOTATION)
in [
service_lib.INGRESS_INTERNAL,
service_lib.INGRESS_INTERNAL_AND_CLOUD_LOAD_BALANCING,
]
or service.annotations.get(container_resource.DISABLE_URL_ANNOTATION)
== 'true'
):
if (
allow_unauth is not None
and not allow_unauth
# allow_unauth is set to None when --no-allow-unauthenticated flag
# is used, so check flag value too
or getattr(args, 'allow_unauthenticated', None) is not None
and not getattr(args, 'allow_unauthenticated')
):
requires_authentication = True
elif allow_unauth is None:
requires_authentication = not operations.IsUnauthenticated(
service_ref
)
project = properties.VALUES.core.project.Get(required=True)
region = flags.GetRegion(args)
pretty_print.Success(
messages_util.GetSuccessMessageForSynchronousDeploy(
service,
args.no_traffic,
show_proxy_message=requires_authentication,
project=project,
region=region,
)
)
if records:
pretty_print.Info(
'\nConfigure your DNS record for the domain mapping to take effect '
'and certificate to start provisioning:\n'
)
resource_printer.Print(
records,
'table(name:label=NAME, type:label="RECORD TYPE", '
'rrdata:label=CONTENTS)',
)
def _GetIap(self, args, service):
"""Returns the IAP status of the service."""
# Determine the desired IAP state based on flags.
if flags.FlagIsExplicitlySet(args, 'iap'):
iap = args.iap
elif flags.FlagIsExplicitlySet(args, 'public') and args.public:
iap = False
else:
# Neither flag is specified, so we don't want to change the IAP state.
return None
# Optimization: If we want to disable IAP (either explicitly or via
# --public), and the service does not exist yet (service is None), we can
# skip the call to disable it because it is already disabled by default.
# This also prevents the tracker from being shown for this operation.
if not iap and not service:
return None
return iap
def _VerifyDomain(self, domain_mapping_ref):
domain_mapping_util.VerifyDomain(domain_mapping_ref)
def Run(self, args):
"""Deploy a container to Cloud Run."""
flags.ValidatePublicFlags(args)
self.__multi_region_regions = flags.GetMultiRegion(args)
platform = flags.GetAndValidatePlatform(
args, self.ReleaseTrack(), flags.Product.RUN
)
containers = self._ValidateAndGetContainers(args)
source_containers, legacy_build_container, local_build_container = (
self._ValidateAndGetSourceContainer(containers)
)
service_ref = args.CONCEPTS.service.Parse()
flags.ValidateResource(service_ref)
domain_mapping_ref = None
if getattr(args, 'domain', None):
if not platforms.IsManaged():
raise exceptions.ArgumentError(
'The --domain flag is only supported for Cloud Run (fully managed).'
)
endpoint_mode = properties.VALUES.regional.endpoint_mode.Get()
if endpoint_mode in (
properties.VALUES.regional.REGIONAL,
properties.VALUES.regional.REGIONAL_PREFERRED,
):
raise exceptions.ArgumentError(
'The `--domain` flag is not supported when regional endpoints are'
' enabled.'
)
domain_mapping_ref = args.CONCEPTS.domain.Parse()
self._VerifyDomain(domain_mapping_ref)
project_id = properties.VALUES.core.project.Get(required=True)
require_build = self._RequireBuild(source_containers)
required_apis = self._GetRequiredApis(require_build)
custom_check_response = None
skip_activation_prompt = False
# For source-based deployments, optimize deployment latency by avoiding
# upfront API enablement checks. Instead, attach a response check callback
# that dynamically intercepts 403 service-disabled errors to batch-prompt
# and enable required APIs (e.g. Cloud Run, Cloud Build, Artifact Registry)
# on demand and retry the request. For non-build deployments, check and
# enable required APIs upfront.
if require_build and platform == platforms.PLATFORM_MANAGED:
custom_check_response = api_enabler.check_response_and_enable_apis(
project_id, required_apis
)
elif (
required_apis
and platform == platforms.PLATFORM_MANAGED
and not require_build
):
skip_activation_prompt = api_enabler.check_and_enable_apis(
project_id, required_apis
)
conn_context = self._ConnectionContext(
args, custom_check_response_func=custom_check_response
)
operation_message = 'Deploying container to'
if local_build_container and len(source_containers) == 1:
operation_message = 'Deploying locally-built source to'
elif len(source_containers) == len(containers):
operation_message = 'Deploying sources to'
kms_key = getattr(args, 'key', None)
legacy_build_changes = []
legacy_build_context = (
source_container_context.LegacyBuildSourceContainerContext(
upload_through_run_api=False
if not legacy_build_container
else legacy_build_container.local_context.upload_through_run_api,
)
)
local_build_changes = []
service = None
with serverless_operations.Connect(
conn_context,
skip_activation_prompt=skip_activation_prompt,
custom_check_response_func=custom_check_response,
) as operations, (
tempfile.TemporaryDirectory()
if local_build_container
else contextlib.nullcontext()
) as local_build_dir:
service = operations.GetService(service_ref)
# set the ingress container for easier READ-and-UPDATE later
self._SetIngressContainerName(service, source_containers)
if local_build_container:
maker_path = local_build.GetUniversalMakerPath()
with progress_tracker.StagedProgressTracker(
'Building application locally...',
[stages._LocalBuildStage()], # pylint: disable=protected-access
failure_message='Local build failed',
suppress_output=args.async_,
) as local_tracker:
local_tracker.StartStage(stages.LOCAL_BUILD_STAGE_KEY)
try:
local_build_changes, local_build_base_image, inferred_runtime = (
local_build.PerformLocalBuild(
local_build_container.local_context.name,
local_build_container,
local_build_dir,
maker_path,
tracker=local_tracker,
)
)
local_build_container.local_context.base_image = (
local_build_base_image
)
local_build_container.local_context.source = local_build_dir
if not local_tracker.IsComplete(stages.LOCAL_BUILD_STAGE_KEY):
local_tracker.CompleteStage(stages.LOCAL_BUILD_STAGE_KEY)
except Exception as e:
if not local_tracker.IsComplete(stages.LOCAL_BUILD_STAGE_KEY):
local_tracker.FailStage(stages.LOCAL_BUILD_STAGE_KEY, e)
raise e
if flags.FlagIsExplicitlySet(local_build_container, 'base_image'):
local_build.WarnOrPromptOnBaseImageMismatch(
inferred_runtime, local_build_container.base_image
)
# source containers processing.
# Legacy build is mutually exclusive with new ways of source deployment.
if source_containers and not legacy_build_container:
for _, container_args in source_containers.items():
container_args.local_context.source_bucket = (
self._GetSourceBucketFromZipDeploySourceLocation(service)
)
if not container_args.local_context.base_image:
container_args.local_context.base_image = container_args.base_image
container_args.image = 'scratch'
# legacy submit build container processing
elif legacy_build_container:
(
operation_message,
legacy_build_changes,
) = self._BuildFromSource(
args,
legacy_build_container,
legacy_build_context,
service_ref,
conn_context,
platform,
skip_activation_prompt,
service,
)
build_util.ValidateBuildServiceAccountAndPromptWarning(
project_id=properties.VALUES.core.project.Get(required=True),
region=flags.GetRegion(args),
build_service_account=legacy_build_context.build_service_account,
skip_build_sa_permission_check=require_build,
enable_by_default=True,
)
# Deploy a container with an image
changes = self._GetBaseChanges(args)
# Add legacy submitBuild changes
changes.extend(legacy_build_changes)
# Add local build changes
changes.extend(local_build_changes)
allow_unauth = self.GetAllowUnauth(args, operations, service_ref, service)
resource_change_validators.ValidateClearVpcConnector(service, args)
if service: # Service has been deployed before
if (
legacy_build_context.is_function
and service.template.container.command
):
clear_command = flags.PromptForClearCommand()
if clear_command:
changes.append(config_changes.ContainerCommandChange([]))
else:
raise c_exceptions.ConflictingArgumentsException(
'--command',
'--function',
)
else:
if (
flags.FlagIsExplicitlySet(args, 'ssh')
and args.ssh
and not flags.FlagIsExplicitlySet(args, 'execution_environment')
):
changes.append(config_changes.SandboxChange(constants.GEN2))
messages_util.MaybeLogDefaultGpuTypeMessage(args, service)
warning_msg = messages_util.GetAutoscalingAnnotationsWarning(service)
if warning_msg:
pretty_print.Info(f'{{bold}}[Warning]{{reset}} {warning_msg}\n')
pretty_print.Info(
messages_util.GetStartDeployMessage(
conn_context, service_ref, operation_message
)
)
has_latest = (
service is None or traffic.LATEST_REVISION_KEY in service.spec_traffic
)
iap = self._GetIap(args, service)
if iap:
if iap_util.IsOrglessProject(
project_id
) and not iap_util.IsIapAlreadyEnabled(self.ReleaseTrack()):
pretty_print.Info(
'\n {bold}**[Warning]**{reset} Deploying services with IAP'
' enabled in a project outside of an Organization and may require'
' initial setup via the Cloud Console. Please use the Cloud Run'
' UI to enable IAP for the first time in the project. '
'https://cloud.google.com/run/docs/securing/identity-aware-proxy-cloud-run#custom-oauth-client\n'
)
def _ReleaseService(changes_):
kwargs = {}
if getattr(args, 'dry_run', False):
kwargs['dry_run'] = True
with self._GetTracker(
args,
service,
changes_,
source_containers,
legacy_build_context.repo_to_create,
allow_unauth,
has_latest,
iap,
legacy_build_container is not None,
) as tracker:
released_service = operations.ReleaseService(
service_ref,
changes_,
self.ReleaseTrack(),
tracker,
asyn=args.async_,
allow_unauthenticated=allow_unauth,
multiregion_regions=self._GetRegionsForMultiRegion(),
prefetch=service,
build_image=legacy_build_context.build_image,
build_pack=legacy_build_context.build_pack,
build_region=flags.GetFirstRegion(args),
build_source=legacy_build_context.build_source,
repo_to_create=legacy_build_context.repo_to_create,
skip_activation_prompt=skip_activation_prompt,
generate_name=(
flags.FlagIsExplicitlySet(args, 'revision_suffix')
or flags.FlagIsExplicitlySet(args, 'tag')
),
base_image=legacy_build_context.base_image,
deploy_from_source_container_name=legacy_build_context.deploy_from_source_container_name,
build_service_account=legacy_build_context.build_service_account,
build_worker_pool=legacy_build_context.build_worker_pool,
build_machine_type=legacy_build_context.build_machine_type,
build_env_vars=legacy_build_context.build_env_vars,
enable_automatic_updates=legacy_build_context.enable_automatic_updates,
is_verbose=properties.VALUES.core.verbosity.Get() == 'debug',
source_bucket=legacy_build_context.source_bucket,
kms_key=kms_key,
iap_enabled=iap,
upload_through_run_api=legacy_build_context.upload_through_run_api,
source_containers_context=self._GetSourceContainersContextList(
source_containers, legacy_build_container
),
**kwargs,
)
records = []
if domain_mapping_ref:
mapping = domain_mapping_util.GetDomainMapping(
operations, domain_mapping_ref
)
if not mapping:
mapping = operations.CreateDomainMapping(
domain_mapping_ref,
service_ref.servicesId,
[],
force_override=False,
tracker=tracker,
stage_key=stages.DOMAIN_MAPPING_READY,
show_ready_message=False,
)
records = mapping.records
for record in records:
record.name = record.name or mapping.route_name
return released_service, records
try:
service, records = _ReleaseService(changes)
except exceptions.HttpError as e:
if flags.ShouldRetryNoZonalRedundancy(args, str(e)):
changes.append(
config_changes.GpuZonalRedundancyChange(
gpu_zonal_redundancy=False
)
)
service, records = _ReleaseService(changes)
else:
raise e
self._DisplaySuccessMessage(
service, args, allow_unauth, operations, service_ref, records
)
return service
@base.ReleaseTracks(base.ReleaseTrack.BETA)
@base.RegionalEndpointsSupported
class BetaDeploy(Deploy):
"""Create or update a Cloud Run service."""
detailed_help = copy.deepcopy(Deploy.detailed_help)
def _VerifyDomain(self, domain_mapping_ref):
if not domain_mapping_util.IsCustomUrl(domain_mapping_ref.Name()):
domain_mapping_util.VerifyDomain(domain_mapping_ref)
@classmethod
def Args(cls, parser):
cls.CommonArgs(parser)
flags.AddSshFlag(parser)
# Flags specific to managed CR
flags.SERVICE_MESH_FLAG.AddToParser(parser)
flags.IdentityCertificateFlag().AddToParser(parser)
flags.IdentityTypeFlag(release_track=cls.ReleaseTrack()).AddToParser(parser)
flags.FunctionalTypeFlag(resource='service').AddToParser(parser)
container_args = ContainerArgGroup(cls.ReleaseTrack())
container_parser.AddContainerFlags(
parser, container_args, cls.ReleaseTrack()
)
concept_parsers.ConceptParser([
presentation_specs.ResourcePresentationSpec(
'--domain',
resource_args.GetDomainResourceSpec(),
'The domain name to map to this Cloud Run service. '
'For example, example.com or subdomain.example.com.',
prefixes=False,
flag_name_overrides={'project': ''},
)
]).AddToParser(parser)
@base.ReleaseTracks(base.ReleaseTrack.ALPHA)
class AlphaDeploy(BetaDeploy):
"""Create or update a Cloud Run service."""
@classmethod
def Args(cls, parser):
cls.CommonArgs(parser)
# Flags specific to managed CR
flags.AddRuntimeFlag(parser)
flags.AddPublicFlag(parser)
flags.SERVICE_MESH_FLAG.AddToParser(parser)
flags.IDENTITY_FLAG.AddToParser(parser)
flags.IdentityCertificateFlag().AddToParser(parser)
flags.IdentityTypeFlag(release_track=cls.ReleaseTrack()).AddToParser(parser)
flags.FunctionalTypeFlag(resource='service').AddToParser(parser)
flags.MESH_DATAPLANE_FLAG.AddToParser(parser)
flags.AMBIENT_NETWORKING_FLAG.AddToParser(parser)
flags.AMBIENT_SCOPE_FLAG.AddToParser(parser)
container_args = ContainerArgGroup(cls.ReleaseTrack())
container_parser.AddContainerFlags(
parser, container_args, cls.ReleaseTrack()
)
flags.AddOverflowScalingFlag(parser)
flags.AddPresetFlags(parser)
flags.AddSshFlag(parser)
flags.AddGracePeriodFlag(parser)
concept_parsers.ConceptParser([
presentation_specs.ResourcePresentationSpec(
'--domain',
resource_args.GetDomainResourceSpec(),
'The domain name to map to this Cloud Run service. '
'For example, example.com or subdomain.example.com.',
prefixes=False,
flag_name_overrides={'project': ''},
)
]).AddToParser(parser)
AlphaDeploy.__doc__ = Deploy.__doc__