HEX
Server: Apache/2.4.65 (Ubuntu)
System: Linux ielts-store-v2 6.8.0-1036-gcp #38~22.04.1-Ubuntu SMP Thu Aug 14 01:19:18 UTC 2025 x86_64
User: root (0)
PHP: 7.2.34-54+ubuntu20.04.1+deb.sury.org+1
Disabled: pcntl_alarm,pcntl_fork,pcntl_waitpid,pcntl_wait,pcntl_wifexited,pcntl_wifstopped,pcntl_wifsignaled,pcntl_wifcontinued,pcntl_wexitstatus,pcntl_wtermsig,pcntl_wstopsig,pcntl_signal,pcntl_signal_get_handler,pcntl_signal_dispatch,pcntl_get_last_error,pcntl_strerror,pcntl_sigprocmask,pcntl_sigwaitinfo,pcntl_sigtimedwait,pcntl_exec,pcntl_getpriority,pcntl_setpriority,pcntl_async_signals,
Upload Files
File: //snap/google-cloud-cli/current/lib/googlecloudsdk/command_lib/app/deploy_util.py
# -*- coding: utf-8 -*- #
# Copyright 2016 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.
"""Utilities for `gcloud app` deployment.

Mostly created to selectively enable Cloud Endpoints in the beta/preview release
tracks.
"""

from __future__ import absolute_import
from __future__ import division
from __future__ import unicode_literals

import enum
import os
import re

from apitools.base.py import exceptions as apitools_exceptions
from googlecloudsdk.api_lib import scheduler
from googlecloudsdk.api_lib import tasks
from googlecloudsdk.api_lib.app import build as app_cloud_build
from googlecloudsdk.api_lib.app import deploy_app_command_util
from googlecloudsdk.api_lib.app import deploy_command_util
from googlecloudsdk.api_lib.app import env
from googlecloudsdk.api_lib.app import metric_names
from googlecloudsdk.api_lib.app import runtime_builders
from googlecloudsdk.api_lib.app import util
from googlecloudsdk.api_lib.app import version_util
from googlecloudsdk.api_lib.app import yaml_parsing
from googlecloudsdk.api_lib.datastore import index_api
from googlecloudsdk.api_lib.storage import storage_util
from googlecloudsdk.api_lib.tasks import app_deploy_migration_util
from googlecloudsdk.api_lib.util import exceptions as core_api_exceptions
from googlecloudsdk.calliope import actions
from googlecloudsdk.calliope import base
from googlecloudsdk.command_lib.app import create_util
from googlecloudsdk.command_lib.app import deployables
from googlecloudsdk.command_lib.app import exceptions
from googlecloudsdk.command_lib.app import flags
from googlecloudsdk.command_lib.app import output_helpers
from googlecloudsdk.command_lib.app import source_files_util
from googlecloudsdk.command_lib.app import staging
from googlecloudsdk.core import exceptions as core_exceptions
from googlecloudsdk.core import log
from googlecloudsdk.core import metrics
from googlecloudsdk.core import properties
from googlecloudsdk.core.configurations import named_configs
from googlecloudsdk.core.console import console_io
from googlecloudsdk.core.console import progress_tracker
from googlecloudsdk.core.util import files
from googlecloudsdk.core.util import times
import six

_TASK_CONSOLE_LINK = """\
https://console.cloud.google.com/appengine/taskqueues/cron?project={}
"""

# The regex for runtimes prior to runtime builders support. Used to deny the
# use of pinned runtime builders when this feature is disabled.
ORIGINAL_RUNTIME_RE_STRING = r'[a-z][a-z0-9\-]{0,29}'
ORIGINAL_RUNTIME_RE = re.compile(ORIGINAL_RUNTIME_RE_STRING + r'\Z')

# Max App Engine file size; see https://cloud.google.com/appengine/docs/quotas
_MAX_FILE_SIZE_STANDARD = 32 * 1024 * 1024

# 1rst gen runtimes that still need the _MAX_FILE_SIZE_STANDARD check:
_RUNTIMES_WITH_FILE_SIZE_LIMITS = [
    'java7', 'java8', 'java8g', 'python27', 'go19', 'php55'
]


class Error(core_exceptions.Error):
  """Base error for this module."""


class VersionPromotionError(Error):

  def __init__(self, err_str):
    super(VersionPromotionError, self).__init__(
        'Your deployment has succeeded, but promoting the new version to '
        'default failed. '
        'You may not have permissions to change traffic splits. '
        'Changing traffic splits requires the Owner, Editor, App Engine Admin, '
        'or App Engine Service Admin role. '
        'Please contact your project owner and use the '
        '`gcloud app services set-traffic --splits <version>=1` command to '
        'redirect traffic to your newly deployed version.\n\n'
        'Original error: ' + err_str)


class StoppedApplicationError(Error):
  """Error if deployment fails because application is stopped/disabled."""

  def __init__(self, app):
    super(StoppedApplicationError, self).__init__(
        'Unable to deploy to application [{}] with status [{}]: Deploying '
        'to stopped apps is not allowed.'.format(app.id, app.servingStatus))


class InvalidRuntimeNameError(Error):
  """Error for runtime names that are not allowed in the given environment."""

  def __init__(self, runtime, allowed_regex):
    super(InvalidRuntimeNameError,
          self).__init__('Invalid runtime name: [{}]. '
                         'Must match regular expression [{}].'.format(
                             runtime, allowed_regex))


class RequiredFileMissingError(Error):
  """Error for skipped/ignored files that must be uploaded."""

  def __init__(self, filename):
    super(RequiredFileMissingError, self).__init__(
        'Required file is not uploaded: [{}]. '
        'This file should not be added to an ignore list ('
        'https://cloud.google.com/sdk/gcloud/reference/topic/gcloudignore)'
        .format(filename))


class FlexImageBuildOptions(enum.Enum):
  """Enum declaring different options for building image for flex deploys."""
  ON_CLIENT = 1
  ON_SERVER = 2
  BUILDPACK_ON_CLIENT = 3


def GetFlexImageBuildOption(default_strategy=FlexImageBuildOptions.ON_CLIENT):
  """Determines where the build should be performed."""
  trigger_build_server_side = (
      properties.VALUES.app.trigger_build_server_side.GetBool(required=False))
  use_flex_with_buildpacks = (
      properties.VALUES.app.use_flex_with_buildpacks.GetBool(required=False))

  if trigger_build_server_side:
    result = FlexImageBuildOptions.ON_SERVER
  elif (trigger_build_server_side is None and not use_flex_with_buildpacks):
    result = default_strategy
  elif use_flex_with_buildpacks:
    result = FlexImageBuildOptions.BUILDPACK_ON_CLIENT
  else:
    result = FlexImageBuildOptions.ON_CLIENT

  log.debug('Flex image build option: %s', result)
  return result


class DeployOptions(object):
  """Values of options that affect deployment process in general.

  No deployment details (e.g. sources for a specific deployment).

  Attributes:
    promote: True if the deployed version should receive all traffic.
    stop_previous_version: Stop previous version
    runtime_builder_strategy: runtime_builders.RuntimeBuilderStrategy, when to
      use the new CloudBuild-based runtime builders (alternative is old
      externalized runtimes).
    parallel_build: bool, whether to use parallel build and deployment path.
      Only supported in v1beta and v1alpha App Engine Admin API.
    flex_image_build_option: FlexImageBuildOptions, whether a flex deployment
      should upload files so that the server can build the image, or build the
      image on client, or build the image on client using the buildpacks.
  """

  def __init__(self,
               promote,
               stop_previous_version,
               runtime_builder_strategy,
               parallel_build=False,
               flex_image_build_option=FlexImageBuildOptions.ON_CLIENT):
    self.promote = promote
    self.stop_previous_version = stop_previous_version
    self.runtime_builder_strategy = runtime_builder_strategy
    self.parallel_build = parallel_build
    self.flex_image_build_option = flex_image_build_option

  @classmethod
  def FromProperties(cls,
                     runtime_builder_strategy,
                     parallel_build=False,
                     flex_image_build_option=FlexImageBuildOptions.ON_CLIENT):
    """Initialize DeloyOptions using user properties where necessary.

    Args:
      runtime_builder_strategy: runtime_builders.RuntimeBuilderStrategy, when to
        use the new CloudBuild-based runtime builders (alternative is old
        externalized runtimes).
      parallel_build: bool, whether to use parallel build and deployment path.
        Only supported in v1beta and v1alpha App Engine Admin API.
      flex_image_build_option: FlexImageBuildOptions, whether a flex deployment
        should upload files so that the server can build the image or build the
        image on client or build the image on client using the buildpacks.

    Returns:
      DeployOptions, the deploy options.
    """
    promote = properties.VALUES.app.promote_by_default.GetBool()
    stop_previous_version = (
        properties.VALUES.app.stop_previous_version.GetBool())
    return cls(promote, stop_previous_version, runtime_builder_strategy,
               parallel_build, flex_image_build_option)


class ServiceDeployer(object):
  """Coordinator (reusable) for deployment of one service at a time.

  Attributes:
    api_client: api_lib.app.appengine_api_client.AppengineClient, App Engine
      Admin API client.
    deploy_options: DeployOptions, the options to use for services deployed by
      this ServiceDeployer.
  """

  def __init__(self, api_client, deploy_options):
    self.api_client = api_client
    self.deploy_options = deploy_options

  def _ValidateRuntime(self, service_info):
    """Validates explicit runtime builders are not used without the feature on.

    Args:
      service_info: yaml_parsing.ServiceYamlInfo, service configuration to be
        deployed

    Raises:
      InvalidRuntimeNameError: if the runtime name is invalid for the deployment
        (see above).
    """
    runtime = service_info.runtime
    if runtime == 'custom':
      return

    # This may or may not be accurate, but it only matters for custom runtimes,
    # which are handled above.
    needs_dockerfile = True
    strategy = self.deploy_options.runtime_builder_strategy
    use_runtime_builders = deploy_command_util.ShouldUseRuntimeBuilders(
        service_info, strategy, needs_dockerfile)
    if not use_runtime_builders and not ORIGINAL_RUNTIME_RE.match(runtime):
      raise InvalidRuntimeNameError(runtime, ORIGINAL_RUNTIME_RE_STRING)

  def _PossiblyBuildAndPush(self, new_version, service, upload_dir,
                            source_files, image, code_bucket_ref, gcr_domain,
                            flex_image_build_option):
    """Builds and Pushes the Docker image if necessary for this service.

    Args:
      new_version: version_util.Version describing where to deploy the service
      service: yaml_parsing.ServiceYamlInfo, service configuration to be
        deployed
      upload_dir: str, path to the service's upload directory
      source_files: [str], relative paths to upload.
      image: str or None, the URL for the Docker image to be deployed (if image
        already exists).
      code_bucket_ref: cloud_storage.BucketReference where the service's files
        have been uploaded
      gcr_domain: str, Cloud Registry domain, determines the physical location
        of the image. E.g. `us.gcr.io`.
      flex_image_build_option: FlexImageBuildOptions, whether a flex deployment
        should upload files so that the server can build the image or build the
        image on client or build the image on client using the buildpacks.

    Returns:
      BuildArtifact, a wrapper which contains either the build ID for
        an in-progress build, or the name of the container image for a serial
        build. Possibly None if the service does not require an image.
    Raises:
      RequiredFileMissingError: if a required file is not uploaded.
    """
    build = None
    if image:
      if service.RequiresImage() and service.parsed.skip_files.regex:
        log.warning('Deployment of service [{0}] will ignore the skip_files '
                    'field in the configuration file, because the image has '
                    'already been built.'.format(new_version.service))
      return app_cloud_build.BuildArtifact.MakeImageArtifact(image)
    elif service.RequiresImage():
      if not _AppYamlInSourceFiles(source_files, service.GetAppYamlBasename()):
        raise RequiredFileMissingError(service.GetAppYamlBasename())

      if flex_image_build_option == FlexImageBuildOptions.ON_SERVER:
        cloud_build_options = {
            'appYamlPath': service.GetAppYamlBasename(),
        }
        timeout = properties.VALUES.app.cloud_build_timeout.Get()
        if timeout:
          build_timeout = int(
              times.ParseDuration(timeout, default_suffix='s').total_seconds)
          cloud_build_options['cloudBuildTimeout'] = six.text_type(
              build_timeout) + 's'
        build = app_cloud_build.BuildArtifact.MakeBuildOptionsArtifact(
            cloud_build_options)
      else:
        build = deploy_command_util.BuildAndPushDockerImage(
            new_version.project, service, upload_dir, source_files,
            new_version.id, code_bucket_ref, gcr_domain,
            self.deploy_options.runtime_builder_strategy,
            self.deploy_options.parallel_build, flex_image_build_option ==
            FlexImageBuildOptions.BUILDPACK_ON_CLIENT)

    return build

  def _PossiblyPromote(self, all_services, new_version, wait_for_stop_version):
    """Promotes the new version to default (if specified by the user).

    Args:
      all_services: dict of service ID to service_util.Service objects
        corresponding to all pre-existing services (used to determine how to
        promote this version to receive all traffic, if applicable).
      new_version: version_util.Version describing where to deploy the service
      wait_for_stop_version: bool, indicating whether to wait for stop operation
        to finish.

    Raises:
      VersionPromotionError: if the version could not successfully promoted
    """
    if self.deploy_options.promote:
      try:
        version_util.PromoteVersion(all_services, new_version, self.api_client,
                                    self.deploy_options.stop_previous_version,
                                    wait_for_stop_version)
      except apitools_exceptions.HttpError as err:
        err_str = six.text_type(core_api_exceptions.HttpException(err))
        raise VersionPromotionError(err_str)
    elif self.deploy_options.stop_previous_version:
      log.info('Not stopping previous version because new version was '
               'not promoted.')

  def _PossiblyUploadFiles(self, image, service_info, upload_dir, source_files,
                           code_bucket_ref, flex_image_build_option):
    """Uploads files for this deployment is required for this service.

    Uploads if flex_image_build_option is FlexImageBuildOptions.ON_SERVER,
    or if the deployment is non-hermetic and the image is not provided.

    Args:
      image: str or None, the URL for the Docker image to be deployed (if image
        already exists).
      service_info: yaml_parsing.ServiceYamlInfo, service configuration to be
        deployed
      upload_dir: str, path to the service's upload directory
      source_files: [str], relative paths to upload.
      code_bucket_ref: cloud_storage.BucketReference where the service's files
        have been uploaded
      flex_image_build_option: FlexImageBuildOptions, whether a flex deployment
        should upload files so that the server can build the image or build the
        image on client or build the image on client using the buildpacks.

    Returns:
      Dictionary mapping source files to Google Cloud Storage locations.

    Raises:
      RequiredFileMissingError: if a required file is not uploaded.
    """
    manifest = None
    # "Non-hermetic" services require file upload outside the Docker image
    # unless an image was already built.
    if (not image and
        (flex_image_build_option == FlexImageBuildOptions.ON_SERVER or
         not service_info.is_hermetic)):
      if (service_info.env == env.FLEX and not _AppYamlInSourceFiles(
          source_files, service_info.GetAppYamlBasename())):
        raise RequiredFileMissingError(service_info.GetAppYamlBasename())

      limit = None
      if (service_info.env == env.STANDARD and
          service_info.runtime in _RUNTIMES_WITH_FILE_SIZE_LIMITS):
        limit = _MAX_FILE_SIZE_STANDARD
      manifest = deploy_app_command_util.CopyFilesToCodeBucket(
          upload_dir, source_files, code_bucket_ref, max_file_size=limit)
    return manifest

  def Deploy(self,
             service,
             new_version,
             code_bucket_ref,
             image,
             all_services,
             gcr_domain,
             disable_build_cache,
             wait_for_stop_version,
             flex_image_build_option=FlexImageBuildOptions.ON_CLIENT,
             ignore_file=None,
             service_account=None):
    """Deploy the given service.

    Performs all deployment steps for the given service (if applicable):
    * Enable endpoints (for beta deployments)
    * Build and push the Docker image (Flex only, if image_url not provided)
    * Upload files (non-hermetic deployments and flex deployments with
      flex_image_build_option=FlexImageBuildOptions.ON_SERVER)
    * Create the new version
    * Promote the version to receive all traffic (if --promote given (default))
    * Stop the previous version (if new version promoted and
      --stop-previous-version given (default))

    Args:
      service: deployables.Service, service to be deployed.
      new_version: version_util.Version describing where to deploy the service
      code_bucket_ref: cloud_storage.BucketReference where the service's files
        will be uploaded
      image: str or None, the URL for the Docker image to be deployed (if image
        already exists).
      all_services: dict of service ID to service_util.Service objects
        corresponding to all pre-existing services (used to determine how to
        promote this version to receive all traffic, if applicable).
      gcr_domain: str, Cloud Registry domain, determines the physical location
        of the image. E.g. `us.gcr.io`.
      disable_build_cache: bool, disable the build cache.
      wait_for_stop_version: bool, indicating whether to wait for stop operation
        to finish.
      flex_image_build_option: FlexImageBuildOptions, whether a flex deployment
        should upload files so that the server can build the image or build the
        image on client or build the image on client using the buildpacks.
      ignore_file: custom ignore_file name. Override .gcloudignore file to
        customize files to be skipped.
      service_account: identity this version runs as. If not set, Admin API will
        fallback to use the App Engine default appspot SA.
    """
    log.status.Print('Beginning deployment of service [{service}]...'.format(
        service=new_version.service))
    if (service.service_info.env == env.MANAGED_VMS and
        flex_image_build_option == FlexImageBuildOptions.ON_SERVER):
      # Server-side builds are not supported for Managed VMs.
      flex_image_build_option = FlexImageBuildOptions.ON_CLIENT

    service_info = service.service_info
    self._ValidateRuntime(service_info)

    source_files = source_files_util.GetSourceFiles(
        service.upload_dir,
        service_info.parsed.skip_files.regex,
        service_info.HasExplicitSkipFiles(),
        service_info.runtime,
        service_info.env,
        service.source,
        ignore_file=ignore_file)

    # Tar-based upload for flex
    build = self._PossiblyBuildAndPush(new_version, service_info,
                                       service.upload_dir, source_files, image,
                                       code_bucket_ref, gcr_domain,
                                       flex_image_build_option)

    # Manifest-based incremental source upload for all envs
    manifest = self._PossiblyUploadFiles(image, service_info,
                                         service.upload_dir, source_files,
                                         code_bucket_ref,
                                         flex_image_build_option)

    del source_files  # Free some memory

    extra_config_settings = {}
    if disable_build_cache:
      extra_config_settings['no-cache'] = 'true'

    # Actually create the new version of the service.
    metrics.CustomTimedEvent(metric_names.DEPLOY_API_START)
    self.api_client.DeployService(new_version.service, new_version.id,
                                  service_info, manifest, build,
                                  extra_config_settings, service_account)
    metrics.CustomTimedEvent(metric_names.DEPLOY_API)
    self._PossiblyPromote(all_services, new_version, wait_for_stop_version)


def ArgsDeploy(parser):
  """Get arguments for this command.

  Args:
    parser: argparse.ArgumentParser, the parser for this command.
  """
  flags.SERVER_FLAG.AddToParser(parser)
  flags.IGNORE_CERTS_FLAG.AddToParser(parser)
  flags.DOCKER_BUILD_FLAG.AddToParser(parser)
  flags.IGNORE_FILE_FLAG.AddToParser(parser)
  parser.add_argument(
      '--version',
      '-v',
      type=flags.VERSION_TYPE,
      help='The version of the app that will be created or replaced by this '
      'deployment.  If you do not specify a version, one will be generated for '
      'you.')
  parser.add_argument(
      '--bucket',
      type=storage_util.BucketReference.FromArgument,
      help=('The Google Cloud Storage bucket used to stage files associated '
            'with the deployment. If this argument is not specified, the '
            "application's default code bucket is used."))
  parser.add_argument(
      '--service-account',
      help=('The service account that this deployed version will run as. '
            'If this argument is not specified, the App Engine default '
            'service account will be used for your current deployed version.'))
  parser.add_argument(
      'deployables',
      nargs='*',
      help="""\
      The yaml files for the services or configurations you want to deploy.
      If not given, defaults to `app.yaml` in the current directory.
      If that is not found, attempts to automatically generate necessary
      configuration files (such as app.yaml) in the current directory.""")
  parser.add_argument(
      '--stop-previous-version',
      action=actions.StoreBooleanProperty(
          properties.VALUES.app.stop_previous_version),
      help="""\
      Stop the previously running version when deploying a new version that
      receives all traffic.

      Note that if the version is running on an instance
      of an auto-scaled service in the App Engine Standard
      environment, using `--stop-previous-version` will not work
      and the previous version will continue to run because auto-scaled service
      instances are always running.""")
  parser.add_argument(
      '--image-url',
      help='(App Engine flexible environment only.) Deploy with a specific '
      'Docker image. Docker url must be from one of the valid Artifact '
      'Registry hostnames.')
  parser.add_argument(
      '--appyaml',
      help='Deploy with a specific app.yaml that will replace '
      'the one defined in the DEPLOYABLE.')
  parser.add_argument(
      '--promote',
      action=actions.StoreBooleanProperty(
          properties.VALUES.app.promote_by_default),
      help='Promote the deployed version to receive all traffic.')
  parser.add_argument(
      '--cache',
      action='store_true',
      default=True,
      help='Enable caching mechanisms involved in the deployment process, '
      'particularly in the build step.')
  staging_group = parser.add_mutually_exclusive_group(hidden=True)
  staging_group.add_argument(
      '--skip-staging',
      action='store_true',
      default=False,
      help='THIS ARGUMENT NEEDS HELP TEXT.')
  staging_group.add_argument(
      '--staging-command', help='THIS ARGUMENT NEEDS HELP TEXT.')


def _MakeStager(skip_staging, use_beta_stager, staging_command, staging_area):
  """Creates the appropriate stager for the given arguments/release track.

  The stager is responsible for invoking the right local staging depending on
  env and runtime.

  Args:
    skip_staging: bool, if True use a no-op Stager. Takes precedence over other
      arguments.
    use_beta_stager: bool, if True, use a stager that includes beta staging
      commands.
    staging_command: str, path to an executable on disk. If given, use this
      command explicitly for staging. Takes precedence over later arguments.
    staging_area: str, the path to the staging area

  Returns:
    staging.Stager, the appropriate stager for the command
  """
  if skip_staging:
    return staging.GetNoopStager(staging_area)
  elif staging_command:
    command = staging.ExecutableCommand.FromInput(staging_command)
    return staging.GetOverrideStager(command, staging_area)
  elif use_beta_stager:
    return staging.GetBetaStager(staging_area)
  else:
    return staging.GetStager(staging_area)


def RunDeploy(
    args,
    api_client,
    use_beta_stager=False,
    runtime_builder_strategy=runtime_builders.RuntimeBuilderStrategy.NEVER,
    parallel_build=True,
    flex_image_build_option=FlexImageBuildOptions.ON_CLIENT,
):
  """Perform a deployment based on the given args.

  Args:
    args: argparse.Namespace, An object that contains the values for the
      arguments specified in the ArgsDeploy() function.
    api_client: api_lib.app.appengine_api_client.AppengineClient, App Engine
      Admin API client.
    use_beta_stager: Use the stager registry defined for the beta track rather
      than the default stager registry.
    runtime_builder_strategy: runtime_builders.RuntimeBuilderStrategy, when to
      use the new CloudBuild-based runtime builders (alternative is old
      externalized runtimes).
    parallel_build: bool, whether to use parallel build and deployment path.
      Only supported in v1beta and v1alpha App Engine Admin API.
    flex_image_build_option: FlexImageBuildOptions, whether a flex deployment
      should upload files so that the server can build the image or build the
      image on client or build the image on client using the buildpacks.

  Returns:
    A dict on the form `{'versions': new_versions, 'configs': updated_configs}`
    where new_versions is a list of version_util.Version, and updated_configs
    is a list of config file identifiers, see yaml_parsing.ConfigYamlInfo.
  """
  project = properties.VALUES.core.project.Get(required=True)
  deploy_options = DeployOptions.FromProperties(
      runtime_builder_strategy=runtime_builder_strategy,
      parallel_build=parallel_build,
      flex_image_build_option=flex_image_build_option)

  with files.TemporaryDirectory() as staging_area:
    stager = _MakeStager(args.skip_staging, use_beta_stager,
                         args.staging_command, staging_area)
    services, configs = deployables.GetDeployables(
        args.deployables, stager, deployables.GetPathMatchers(), args.appyaml)

    wait_for_stop_version = _CheckIfConfigsContainDispatch(configs)

    service_infos = [d.service_info for d in services]

    flags.ValidateImageUrl(args.image_url, service_infos)

    # pylint: disable=protected-access
    log.debug('API endpoint: [{endpoint}], API version: [{version}]'.format(
        endpoint=api_client.client.url, version=api_client.client._VERSION))
    app = _PossiblyCreateApp(api_client, project)
    _RaiseIfStopped(api_client, app)

    # Call _PossiblyRepairApp when --bucket param is unspecified
    if not args.bucket:
      app = _PossiblyRepairApp(api_client, app)

    # Tell the user what is going to happen, and ask them to confirm.
    version_id = args.version or util.GenerateVersionId()
    deployed_urls = output_helpers.DisplayProposedDeployment(
        app, project, services, configs, version_id, deploy_options.promote,
        args.service_account, api_client.client._VERSION)
    console_io.PromptContinue(cancel_on_no=True)
    if service_infos:
      # Do generic app setup if deploying any services.
      # All deployment paths for a service involve uploading source to GCS.
      metrics.CustomTimedEvent(metric_names.GET_CODE_BUCKET_START)
      code_bucket_ref = args.bucket or flags.GetCodeBucket(app, project)
      metrics.CustomTimedEvent(metric_names.GET_CODE_BUCKET)
      log.debug('Using bucket [{b}].'.format(b=code_bucket_ref.ToUrl()))

      # Prepare Flex if any service is going to deploy an image.
      if any([s.RequiresImage() for s in service_infos]):
        deploy_command_util.PossiblyEnableFlex(project)

      all_services = dict([(s.id, s) for s in api_client.ListServices()])
    else:
      code_bucket_ref = None
      all_services = {}
    new_versions = []
    deployer = ServiceDeployer(api_client, deploy_options)

    # Track whether a service has been deployed yet, for metrics.
    service_deployed = False
    for service in services:
      if not service_deployed:
        metrics.CustomTimedEvent(metric_names.FIRST_SERVICE_DEPLOY_START)
      new_version = version_util.Version(project, service.service_id,
                                         version_id)
      deployer.Deploy(
          service,
          new_version,
          code_bucket_ref,
          args.image_url,
          all_services,
          app.gcrDomain,
          disable_build_cache=(not args.cache),
          wait_for_stop_version=wait_for_stop_version,
          flex_image_build_option=flex_image_build_option,
          ignore_file=args.ignore_file,
          service_account=args.service_account)
      new_versions.append(new_version)
      log.status.Print('Deployed service [{0}] to [{1}]'.format(
          service.service_id, deployed_urls[service.service_id]))
      if not service_deployed:
        metrics.CustomTimedEvent(metric_names.FIRST_SERVICE_DEPLOY)
      service_deployed = True

  # Deploy config files.
  if configs:
    metrics.CustomTimedEvent(metric_names.UPDATE_CONFIG_START)
    for config in configs:
      message = 'Updating config [{config}]'.format(config=config.name)
      with progress_tracker.ProgressTracker(message):
        if config.name == 'dispatch':
          api_client.UpdateDispatchRules(config.GetRules())
        elif config.name == yaml_parsing.ConfigYamlInfo.INDEX:
          index_api.CreateMissingIndexesViaDatastoreApi(project, config.parsed)
        elif config.name == yaml_parsing.ConfigYamlInfo.QUEUE:
          RunDeployCloudTasks(config)
        elif config.name == yaml_parsing.ConfigYamlInfo.CRON:
          RunDeployCloudScheduler(config)
        else:
          raise ValueError(
              'Unknown config [{config}]'.format(config=config.name)
          )
    metrics.CustomTimedEvent(metric_names.UPDATE_CONFIG)

  updated_configs = [c.name for c in configs]

  PrintPostDeployHints(new_versions, updated_configs)

  # Return all the things that were deployed.
  return {'versions': new_versions, 'configs': updated_configs}


def RunDeployCloudTasks(config):
  """Perform a deployment using Cloud Tasks API based on the given args.

  Args:
    config: A yaml_parsing.ConfigYamlInfo object for the parsed YAML file we are
      going to process.

  Returns:
    A list of config file identifiers, see yaml_parsing.ConfigYamlInfo.
  """
  # TODO(b/169069379): Upgrade to use GA once the relevant code is promoted
  tasks_api = tasks.GetApiAdapter(base.ReleaseTrack.BETA)
  queues_data = app_deploy_migration_util.FetchCurrentQueuesData(tasks_api)
  app_deploy_migration_util.ValidateQueueYamlFileConfig(config)
  app_deploy_migration_util.DeployQueuesYamlFile(tasks_api, config, queues_data)


def RunDeployCloudScheduler(config):
  """Perform a deployment using Cloud Scheduler APIs based on the given args.

  Args:
    config: A yaml_parsing.ConfigYamlInfo object for the parsed YAML file we are
      going to process.

  Returns:
    A list of config file identifiers, see yaml_parsing.ConfigYamlInfo.
  """
  # TODO(b/169069379): Upgrade to use GA once the relevant code is promoted
  scheduler_api = scheduler.GetApiAdapter(
      base.ReleaseTrack.BETA, legacy_cron=True)
  jobs_data = app_deploy_migration_util.FetchCurrentJobsData(scheduler_api)
  app_deploy_migration_util.ValidateCronYamlFileConfig(config)
  app_deploy_migration_util.DeployCronYamlFile(scheduler_api, config, jobs_data)


# TODO(b/30632016): Move to Epilog() when we have a good way to pass
# information about the deployed versions
def PrintPostDeployHints(new_versions, updated_configs):
  """Print hints for user at the end of a deployment."""
  if yaml_parsing.ConfigYamlInfo.CRON in updated_configs:
    log.status.Print('\nCron jobs have been updated.')
    if yaml_parsing.ConfigYamlInfo.QUEUE not in updated_configs:
      log.status.Print('\nVisit the Cloud Platform Console Task Queues page '
                       'to view your queues and cron jobs.')
      log.status.Print(
          _TASK_CONSOLE_LINK.format(properties.VALUES.core.project.Get()))
  if yaml_parsing.ConfigYamlInfo.DISPATCH in updated_configs:
    log.status.Print('\nCustom routings have been updated.')
  if yaml_parsing.ConfigYamlInfo.QUEUE in updated_configs:
    log.status.Print('\nTask queues have been updated.')
    log.status.Print('\nVisit the Cloud Platform Console Task Queues page '
                     'to view your queues and cron jobs.')
  if yaml_parsing.ConfigYamlInfo.INDEX in updated_configs:
    log.status.Print('\nIndexes are being rebuilt. This may take a moment.')

  if not new_versions:
    return
  elif len(new_versions) > 1:
    service_hint = ' -s <service>'
  elif new_versions[0].service == 'default':
    service_hint = ''
  else:
    service = new_versions[0].service
    service_hint = ' -s {svc}'.format(svc=service)

  proj_conf = named_configs.ActivePropertiesFile.Load().Get('core', 'project')
  project = properties.VALUES.core.project.Get()
  if proj_conf != project:
    project_hint = ' --project=' + project
  else:
    project_hint = ''
  log.status.Print('\nYou can stream logs from the command line by running:\n'
                   '  $ gcloud app logs tail' + (service_hint or ' -s default'))
  log.status.Print('\nTo view your application in the web browser run:\n'
                   '  $ gcloud app browse' + service_hint + project_hint)


def _PossiblyCreateApp(api_client, project):
  """Returns an app resource, and creates it if the stars are aligned.

  App creation happens only if the current project is app-less, we are running
  in interactive mode and the user explicitly wants to.

  Args:
    api_client: Admin API client.
    project: The GCP project/app id.

  Returns:
    An app object (never returns None).

  Raises:
    MissingApplicationError: If an app does not exist and cannot be created.
  """
  try:
    return api_client.GetApplication()
  except apitools_exceptions.HttpNotFoundError:
    # Invariant: GCP Project does exist but (singleton) GAE app is not yet
    # created.
    #
    # Check for interactive mode, since this action is irreversible and somewhat
    # surprising. CreateAppInteractively will provide a cancel option for
    # interactive users, and MissingApplicationException includes instructions
    # for non-interactive users to fix this.
    log.debug('No app found:', exc_info=True)
    if console_io.CanPrompt():

      # Equivalent to running `gcloud app create`
      create_util.CreateAppInteractively(api_client, project)
      # App resource must be fetched again
      return api_client.GetApplication()
    raise exceptions.MissingApplicationError(project)
  except apitools_exceptions.HttpForbiddenError:
    active_account = properties.VALUES.core.account.Get()
    # pylint: disable=protected-access
    raise core_api_exceptions.HttpException(
        ('Permissions error fetching application [{}]. Please '
         'make sure that you have permission to view applications on the '
         'project and that {} has the App Engine Deployer '
         '(roles/appengine.deployer) role.'.format(api_client._FormatApp(),
                                                   active_account)))


def _PossiblyRepairApp(api_client, app):
  """Repairs the app if necessary and returns a healthy app object.

  An app is considered unhealthy if the codeBucket field is missing.
  This may include more conditions in the future.

  Args:
    api_client: Admin API client.
    app: App object (with potentially missing resources).

  Returns:
    An app object (either the same or a new one), which contains the right
    resources, including code bucket.
  """
  if not app.codeBucket:
    message = 'Initializing App Engine resources'
    api_client.RepairApplication(progress_message=message)
    app = api_client.GetApplication()
  return app


def _RaiseIfStopped(api_client, app):
  """Checks if app is disabled and raises error if so.

  Deploying to a disabled app is not allowed.

  Args:
    api_client: Admin API client.
    app: App object (including status).

  Raises:
    StoppedApplicationError: if the app is currently disabled.
  """
  if api_client.IsStopped(app):
    raise StoppedApplicationError(app)


def _CheckIfConfigsContainDispatch(configs):
  """Checks if list of configs contains dispatch config.

  Args:
    configs: list of configs

  Returns:
    bool, indicating if configs contain dispatch config.
  """
  for config in configs:
    if config.name == 'dispatch':
      return True

  return False


def GetRuntimeBuilderStrategy(release_track):
  """Gets the appropriate strategy to use for runtime builders.

  Depends on the release track (beta or GA; alpha is not supported) and whether
  the hidden `app/use_runtime_builders` configuration property is set (in which
  case it overrides).

  Args:
    release_track: the base.ReleaseTrack that determines the default strategy.

  Returns:
    The RuntimeBuilderStrategy to use.

  Raises:
    ValueError: if the release track is not supported (and there is no property
      override set).
  """
  # Use Get(), not GetBool, since GetBool() doesn't differentiate between "None"
  # and "False"
  if properties.VALUES.app.use_runtime_builders.Get() is not None:
    if properties.VALUES.app.use_runtime_builders.GetBool():
      return runtime_builders.RuntimeBuilderStrategy.ALWAYS
    else:
      return runtime_builders.RuntimeBuilderStrategy.NEVER

  if release_track is base.ReleaseTrack.GA:
    return runtime_builders.RuntimeBuilderStrategy.ALLOWLIST_GA
  elif release_track is base.ReleaseTrack.BETA:
    return runtime_builders.RuntimeBuilderStrategy.ALLOWLIST_BETA
  else:
    raise ValueError('Unrecognized release track [{}]'.format(release_track))


def _AppYamlInSourceFiles(source_files, app_yaml_path):
  if not source_files:
    return False

  # TODO(b/171495697) until the bug is fixed, the app yaml has to be located in
  #  the root of the app code, hence we're searching only the filename
  app_yaml_filename = os.path.basename(app_yaml_path)
  return any([f == app_yaml_filename for f in source_files])