1# -*- coding: utf-8 -*- #
2# Copyright 2018 Google LLC. All Rights Reserved.
3#
4# Licensed under the Apache License, Version 2.0 (the "License");
5# you may not use this file except in compliance with the License.
6# You may obtain a copy of the License at
7#
8#    http://www.apache.org/licenses/LICENSE-2.0
9#
10# Unless required by applicable law or agreed to in writing, software
11# distributed under the License is distributed on an "AS IS" BASIS,
12# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13# See the License for the specific language governing permissions and
14# limitations under the License.
15"""Provides common arguments for the Run command surface."""
16
17from __future__ import absolute_import
18from __future__ import division
19from __future__ import print_function
20from __future__ import unicode_literals
21
22import enum
23import os
24import re
25from apitools.base.py import exceptions as apitools_exceptions
26
27from googlecloudsdk.api_lib.container import kubeconfig
28from googlecloudsdk.api_lib.run import container_resource
29from googlecloudsdk.api_lib.run import global_methods
30from googlecloudsdk.api_lib.run import k8s_object
31from googlecloudsdk.api_lib.run import revision
32from googlecloudsdk.api_lib.run import service
33from googlecloudsdk.api_lib.run import traffic
34from googlecloudsdk.api_lib.services import enable_api
35from googlecloudsdk.api_lib.services import exceptions as services_exceptions
36from googlecloudsdk.calliope import actions
37from googlecloudsdk.calliope import arg_parsers
38from googlecloudsdk.calliope import base
39from googlecloudsdk.command_lib.functions.v2.deploy import env_vars_util
40from googlecloudsdk.command_lib.run import config_changes
41from googlecloudsdk.command_lib.run import exceptions as serverless_exceptions
42from googlecloudsdk.command_lib.run import platforms
43from googlecloudsdk.command_lib.run import pretty_print
44from googlecloudsdk.command_lib.run import resource_args
45from googlecloudsdk.command_lib.util.args import labels_util
46from googlecloudsdk.command_lib.util.args import map_util
47from googlecloudsdk.command_lib.util.args import repeated
48from googlecloudsdk.command_lib.util.concepts import concept_parsers
49from googlecloudsdk.core import config
50from googlecloudsdk.core import exceptions
51from googlecloudsdk.core import log
52from googlecloudsdk.core import properties
53from googlecloudsdk.core.console import console_io
54from googlecloudsdk.core.util import encoding
55from googlecloudsdk.core.util import files
56
57_VISIBILITY_MODES = {
58    'internal': 'Visible only within the cluster.',
59    'external': 'Visible from outside the cluster.',
60}
61
62_INGRESS_MODES = {
63    'all': 'Inbound requests from all sources are allowed.',
64    'internal':
65        'For Cloud Run (fully managed), only inbound requests from VPC networks'
66        ' in the same project are allowed. For Cloud Run for Anthos, only '
67        'inbound requests from the same cluster are allowed.',
68    'internal-and-cloud-load-balancing':
69        'Only supported for Cloud Run (fully managed). Only inbound requests'
70        ' from VPC networks in the same project or from Google Cloud Load '
71        'Balancing are allowed.'
72}
73
74_SANDBOX_CHOICES = {
75    'gvisor': 'Run the application in a gVisor sandbox.',
76    'minivm': 'Run the application in a mini VM sandbox.',
77}
78
79_DEFAULT_KUBECONFIG_PATH = '~/.kube/config'
80
81_FIFTEEN_MINUTES = 15 * 60
82
83
84def _StripKeys(d):
85  return {k.strip(): v for k, v in d.items()}
86
87
88def _MapLStrip(seq):
89  return [elem.lstrip() for elem in seq]
90
91
92class KubeconfigError(exceptions.Error):
93  pass
94
95
96class Product(enum.Enum):
97  RUN = 'Run'
98  EVENTS = 'Events'
99
100
101def AddImageArg(parser, required=True):
102  """Add an image resource arg."""
103  parser.add_argument(
104      '--image',
105      required=required,
106      help='Name of the container image to deploy (e.g. '
107      '`gcr.io/cloudrun/hello:latest`).')
108
109
110def AddConfigFlags(parser):
111  """Add config flags."""
112  build_config = parser.add_mutually_exclusive_group()
113  build_config.add_argument(
114      '--image',
115      help='Name of the container image to deploy (e.g. '
116      '`gcr.io/cloudrun/hello:latest`).')
117  build_config.add_argument(
118      '--config',
119      hidden=True,
120      default='cloudbuild.yaml',  # By default, find this in the current dir
121      help='The YAML or JSON file to use as the build configuration file.')
122  build_config.add_argument(
123      '--pack',
124      hidden=True,
125      type=arg_parsers.ArgDict(
126          spec={
127              'image': str,
128              'builder': str,
129              'env': str
130          },
131          required_keys=['image']),
132      action='append',
133      help='Uses CNCF buildpack (https://buildpacks.io/) to create image.  '
134      'The "image" key/value must be provided.  The image name must be in the '
135      'gcr.io/*, *.gcr.io, or pkg.dev namespaces. By default '
136      'gcr.io/buildpacks/builder will be used. To specify your own builder '
137      'image use the optional "builder" key/value argument.  To pass '
138      'environment variables to the builder use the optional "env" key/value '
139      'argument where value is a list of key values using '
140      'escaping (https://cloud.google.com/sdk/gcloud/reference/topic/escaping) '
141      'if neccessary.')
142
143
144_ARG_GROUP_HELP_TEXT = ('Only applicable if connecting to {platform_desc}. '
145                        'Specify {platform} to use:')
146
147
148def _GetOrAddArgGroup(parser, help_text):
149  """Create a new arg group or return existing group with given help text."""
150  for arg in parser.arguments:
151    if arg.is_group and arg.help == help_text:
152      return arg
153  return parser.add_argument_group(help_text)
154
155
156def GetManagedArgGroup(parser):
157  """Get an arg group for managed CR-only flags."""
158  return _GetOrAddArgGroup(
159      parser,
160      _ARG_GROUP_HELP_TEXT.format(
161          platform='`--platform={}`'.format(platforms.PLATFORM_MANAGED),
162          platform_desc=platforms.PLATFORM_SHORT_DESCRIPTIONS[
163              platforms.PLATFORM_MANAGED]))
164
165
166def GetGkeArgGroup(parser):
167  """Get an arg group for CRoGKE-only flags."""
168  return _GetOrAddArgGroup(
169      parser,
170      _ARG_GROUP_HELP_TEXT.format(
171          platform='`--platform={}`'.format(platforms.PLATFORM_GKE),
172          platform_desc=platforms.PLATFORM_SHORT_DESCRIPTIONS[
173              platforms.PLATFORM_GKE]))
174
175
176def GetKubernetesArgGroup(parser):
177  """Get an arg group for --platform=kubernetes only flags."""
178  return _GetOrAddArgGroup(
179      parser,
180      _ARG_GROUP_HELP_TEXT.format(
181          platform='`--platform={}`'.format(platforms.PLATFORM_KUBERNETES),
182          platform_desc=platforms.PLATFORM_SHORT_DESCRIPTIONS[
183              platforms.PLATFORM_KUBERNETES]))
184
185
186def GetClusterArgGroup(parser):
187  """Get an arg group for any generic cluster flags."""
188  return _GetOrAddArgGroup(
189      parser,
190      _ARG_GROUP_HELP_TEXT.format(
191          platform='`--platform={}` or `--platform={}`'.format(
192              platforms.PLATFORM_GKE, platforms.PLATFORM_KUBERNETES),
193          platform_desc='{} or {}'.format(
194              platforms.PLATFORM_SHORT_DESCRIPTIONS[platforms.PLATFORM_GKE],
195              platforms.PLATFORM_SHORT_DESCRIPTIONS[
196                  platforms.PLATFORM_KUBERNETES])))
197
198
199def AddPlatformAndLocationFlags(parser, managed_only=False, anthos_only=False):
200  """Adds flags used to determine the platform and the location of resource."""
201  assert not (managed_only and anthos_only)
202  AddPlatformArg(parser, managed_only, anthos_only)
203
204  if managed_only:
205    AddRegionArg(parser)
206    return None
207
208  # When multiple platforms are supported, add a arg group that covers the
209  # various ways to specify region/zone/cluster.
210  platform_helpers_group = parser.add_mutually_exclusive_group(
211      help='Arguments to locate resources, depending on the platform used.')
212
213  if not anthos_only:
214    # Add --region flag
215    managed_group = GetManagedArgGroup(platform_helpers_group)
216    AddRegionArg(managed_group)
217
218  # Add --cluster and --cluster-location (plus properties)
219  gke_group = GetGkeArgGroup(platform_helpers_group)
220  concept_parsers.ConceptParser([resource_args.CLUSTER_PRESENTATION
221                                ]).AddToParser(gke_group)
222
223  # Add --kubeconfig and --context
224  kubernetes_group = GetKubernetesArgGroup(platform_helpers_group)
225  AddKubeconfigFlags(kubernetes_group)
226
227
228def AddAllowUnauthenticatedFlag(parser):
229  """Add the --allow-unauthenticated flag."""
230  parser.add_argument(
231      '--allow-unauthenticated',
232      action=arg_parsers.StoreTrueFalseAction,
233      help='Whether to enable allowing unauthenticated access to the service. '
234      'This may take a few moments to take effect.')
235
236
237def AddAsyncFlag(parser, default_async_for_cluster=False):
238  """Add an async flag."""
239  if default_async_for_cluster:
240    modified_async_flag = base.Argument(
241        '--async',
242        action=arg_parsers.StoreTrueFalseAction,
243        dest='async_',
244        help="""\
245    Return immediately, without waiting for the operation in progress to
246    complete. Defaults to --no-async for Cloud Run (fully managed) and --async
247    for Cloud Run for Anthos.""")
248    modified_async_flag.AddToParser(parser)
249  else:
250    base.ASYNC_FLAG.AddToParser(parser)
251
252
253def AddEndpointVisibilityEnum(parser, deprecated=False):
254  """Add the --connectivity=[external|internal] flag."""
255  action = None
256  if deprecated:
257    action = actions.DeprecationAction(
258        '--connectivity',
259        warn='The {flag_name} flag is deprecated and will be removed in an '
260        'upcoming release. Please use the --ingress flag instead.')
261  parser.add_argument(
262      '--connectivity',
263      choices=_VISIBILITY_MODES,
264      help=('Defaults to \'external\'. If \'external\', the service can be '
265            'invoked through the internet, in addition to through the cluster '
266            'network.'),
267      action=action)
268
269
270def AddIngressFlag(parser):
271  """Adds the --ingress flag."""
272  parser.add_argument(
273      '--ingress',
274      choices=_INGRESS_MODES,
275      help='Set the ingress traffic sources allowed to call the service. For '
276      'Cloud Run (fully managed) the `--[no-]allow-unauthenticated` flag '
277      'separately controls the identities allowed to call the service.',
278      default='all')
279
280
281def AddServiceFlag(parser):
282  """Add a service resource flag."""
283  parser.add_argument(
284      '--service',
285      required=False,
286      help='Limit matched revisions to the given service.')
287
288
289def AddRegionArg(parser):
290  """Add a region arg."""
291  parser.add_argument(
292      '--region',
293      help='Region in which the resource can be found. '
294      'Alternatively, set the property [run/region].')
295
296
297def AddFunctionArg(parser):
298  """Add a function resource arg."""
299  parser.add_argument(
300      '--function',
301      hidden=True,
302      help="""\
303      Specifies that the deployed object is a function. If a value is
304      provided, that value is used as the entrypoint.
305      """)
306
307
308def AddNoTrafficFlag(parser):
309  """Add flag to deploy a revision with no traffic."""
310  parser.add_argument(
311      '--no-traffic',
312      default=False,
313      action='store_true',
314      help='True to avoid sending traffic to the revision being deployed. '
315      'Setting this flag assigns any traffic assigned to the LATEST revision '
316      'to the specific revision bound to LATEST before the deployment. The '
317      'effect is that the revision being deployed will not receive traffic.\n\n'
318      'After a deployment with this flag the LATEST revision will not receive '
319      'traffic on future deployments. To restore sending traffic to the LATEST '
320      'revision by default, run the `gcloud run services update-traffic` '
321      'command with `--to-latest`.')
322
323
324def AddDeployTagFlag(parser):
325  """Add flag to specify a tag for the new revision."""
326  parser.add_argument(
327      '--tag', help='Traffic tag to assign to the newly created revision.')
328
329
330def AddTrafficTagsFlags(parser):
331  """Add flags for updating traffic tags for a service."""
332  AddMapFlagsNoFile(
333      parser,
334      group_help=('Specify traffic tags. Traffic tags can be '
335                  'assigned to a revision by name or to the '
336                  'latest ready revision. Assigning a tag to a '
337                  'revision generates a URL prefixed with the '
338                  'tag that allows addressing that revision '
339                  'directly, regardless of the percent traffic '
340                  'specified. Keys are tags. Values are revision names or '
341                  '"LATEST" for the latest ready revision. For example, '
342                  '--set-tags=candidate=LATEST,current='
343                  'myservice-v1 assigns the tag "candidate" '
344                  'to the latest ready revision and the tag'
345                  ' "current" to the revision with name '
346                  '"myservice-v1" and clears any existing tags. '
347                  'Changing tags does not '
348                  'affect the traffic percentage assigned to '
349                  'revisions. When using a tags flag and '
350                  'one or more of --to-latest and --to-revisions in the same '
351                  'command, the tags change occurs first then the traffic '
352                  'percentage change occurs.'),
353      flag_name='tags',
354      key_metavar='TAG',
355      value_metavar='REVISION')
356
357
358def AddUpdateTrafficFlags(parser, release_track):
359  """Add flags for updating traffic assignments for a service."""
360
361  @staticmethod
362  def TrafficTargetKey(key):
363    return key
364
365  @staticmethod
366  def TrafficPercentageValue(value):
367    """Type validation for traffic percentage flag values."""
368    try:
369      result = int(value)
370    except (TypeError, ValueError):
371      raise serverless_exceptions.ArgumentError(
372          'Traffic percentage value %s is not an integer.' % value)
373
374    if result < 0 or result > 100:
375      raise serverless_exceptions.ArgumentError(
376          'Traffic percentage value %s is not between 0 and 100.' % value)
377    return result
378
379  group = parser.add_mutually_exclusive_group()
380
381  group.add_argument(
382      '--to-revisions',
383      metavar='REVISION-NAME=PERCENTAGE',
384      action=arg_parsers.UpdateAction,
385      type=arg_parsers.ArgDict(
386          key_type=TrafficTargetKey.__func__,
387          value_type=TrafficPercentageValue.__func__),
388      help='Comma separated list of traffic assignments in the form '
389      'REVISION-NAME=PERCENTAGE. REVISION-NAME must be the name for a '
390      'revision for the service as returned by \'gcloud beta run list '
391      'revisions\'. PERCENTAGE must be an integer percentage between '
392      '0 and 100 inclusive.  Ex service-nw9hs=10,service-nw9hs=20 '
393      'Up to 100 percent of traffic may be assigned. If 100 percent '
394      'of traffic is assigned,  the Service traffic is updated as '
395      'specified. If under 100 percent of traffic is assigned, the '
396      'Service traffic is updated as specified for revisions with '
397      'assignments and traffic is scaled up or down down proportionally '
398      'as needed for revision that are currently serving traffic but that do '
399      'not have new assignments. For example assume revision-1 is serving '
400      '40 percent of traffic and revision-2 is serving 60 percent. If '
401      'revision-1 is assigned 45 percent of traffic and no assignment is '
402      'made for revision-2, the service is updated with revsion-1 assigned '
403      '45 percent of traffic and revision-2 scaled down to 55 percent. '
404      'You can use "LATEST" as a special revision name to always put the given '
405      'percentage of traffic on the latest ready revision.')
406
407  if release_track and (base.ReleaseTrack.BETA == release_track or
408                        base.ReleaseTrack.ALPHA == release_track):
409    group.add_argument(
410        '--to-tags',
411        metavar='TAG=PERCENTAGE',
412        action=arg_parsers.UpdateAction,
413        type=arg_parsers.ArgDict(
414            key_type=TrafficTargetKey.__func__,
415            value_type=TrafficPercentageValue.__func__),
416        help='Comma separated list of traffic assignments in the form '
417        'TAG=PERCENTAGE. TAG must match a traffic tag on a revision of the '
418        'service. It may match a previously-set tag, or one assigned using'
419        ' the `--set-tags` or `--update-tags` flags on this command. '
420        'PERCENTAGE must be an integer percentage between '
421        '0 and 100 inclusive. '
422        'Up to 100 percent of traffic may be assigned. If 100 percent '
423        'of traffic is assigned, the service traffic is updated as '
424        'specified. If under 100 percent of traffic is assigned, the '
425        'service traffic is updated as specified to the given tags, and other '
426        'traffic is scaled up or down proportionally. For example, assume '
427        'the revision tagged `next` is serving 40 percent of traffic and the '
428        'revision tagged `current` is serving 60 percent. If '
429        '`next` is assigned 45 percent of traffic and no assignment is '
430        'made for `current`, the service is updated with `next` assigned '
431        '45 percent of traffic and `current` scaled down to 55 percent. ')
432
433  group.add_argument(
434      '--to-latest',
435      default=False,
436      action='store_true',
437      help='True to assign 100 percent of traffic to the \'latest\' '
438      'revision of this service. Note that when a new revision is '
439      'created, it will become the \'latest\' and traffic will be '
440      'directed to it. Defaults to False. Synonymous with '
441      '\'--to-revisions=LATEST=100\'.')
442
443
444def AddSetCloudSQLFlag(parser):
445  """Add only the --set-cloudsql-instances flag."""
446  parser.add_argument(
447      '--set-cloudsql-instances',
448      type=arg_parsers.ArgList(),
449      metavar='CLOUDSQL-INSTANCES',
450      help="""You can specify a name of a Cloud SQL instance if it's in the same
451      project and region as your Cloud Run resource; otherwise specify
452      <project>:<region>:<instance> for the instance.""")
453
454
455def AddCloudSQLFlags(parser):
456  """Add flags for setting CloudSQL stuff."""
457  repeated.AddPrimitiveArgs(
458      parser,
459      'Service',
460      'cloudsql-instances',
461      'Cloud SQL instances',
462      auto_group_help=False,
463      additional_help="""\
464      These flags modify the Cloud SQL instances this Service connects to.
465      You can specify a name of a Cloud SQL instance if it's in the same
466      project and region as your Cloud Run service; otherwise specify
467      <project>:<region>:<instance> for the instance.""")
468
469
470def AddMapFlagsNoFile(parser,
471                      flag_name,
472                      group_help='',
473                      long_name=None,
474                      key_type=None,
475                      value_type=None,
476                      key_metavar='KEY',
477                      value_metavar='VALUE'):
478  """Add flags like map_util.AddUpdateMapFlags but without the file one.
479
480  Args:
481    parser: The argument parser
482    flag_name: The name for the property to be used in flag names
483    group_help: Help text for the group of flags
484    long_name: The name for the property to be used in help text
485    key_type: A function to apply to map keys.
486    value_type: A function to apply to map values.
487    key_metavar: Metavariable to list for the key.
488    value_metavar: Metavariable to list for the value.
489  """
490  if not long_name:
491    long_name = flag_name
492
493  group = parser.add_mutually_exclusive_group(group_help)
494  update_remove_group = group.add_argument_group(
495      help=('Only --update-{0} and --remove-{0} can be used together. If both '
496            'are specified, --remove-{0} will be applied first.'
497           ).format(flag_name))
498  map_util.AddMapUpdateFlag(
499      update_remove_group,
500      flag_name,
501      long_name,
502      key_type=key_type,
503      value_type=value_type,
504      key_metavar=key_metavar,
505      value_metavar=value_metavar)
506  map_util.AddMapRemoveFlag(
507      update_remove_group,
508      flag_name,
509      long_name,
510      key_type=key_type,
511      key_metavar=key_metavar)
512  map_util.AddMapClearFlag(group, flag_name, long_name)
513  map_util.AddMapSetFlag(
514      group,
515      flag_name,
516      long_name,
517      key_type=key_type,
518      value_type=value_type,
519      key_metavar=key_metavar,
520      value_metavar=value_metavar)
521
522
523def AddSetEnvVarsFlag(parser):
524  """Add only the --set-env-vars flag."""
525  parser.add_argument(
526      '--set-env-vars',
527      metavar='KEY=VALUE',
528      action=arg_parsers.UpdateAction,
529      type=arg_parsers.ArgDict(
530          key_type=env_vars_util.EnvVarKeyType,
531          value_type=env_vars_util.EnvVarValueType),
532      help='List of key-value pairs to set as environment variables.')
533
534
535def AddMutexEnvVarsFlags(parser):
536  """Add flags for creating updating and deleting env vars."""
537  # TODO(b/119837621): Use env_vars_util.AddUpdateEnvVarsFlags when
538  # `gcloud run` supports an env var file.
539  AddMapFlagsNoFile(
540      parser,
541      flag_name='env-vars',
542      long_name='environment variables',
543      key_type=env_vars_util.EnvVarKeyType,
544      value_type=env_vars_util.EnvVarValueType)
545
546
547def AddMemoryFlag(parser):
548  parser.add_argument('--memory', help='Set a memory limit. Ex: 1Gi, 512Mi.')
549
550
551def AddCpuFlag(parser, managed_only=False):
552  help_msg = ('Set a CPU limit in Kubernetes cpu units.\n\n'
553              'Cloud Run (fully managed) supports values 1, 2 and 4.'
554              '  For Cloud Run (fully managed), 4 cpus also requires a minimum '
555              '2Gi `--memory` value.  Examples 2, 2.0, 2000m')
556  if not managed_only:
557    help_msg += ('\n\nCloud Run for Anthos and Knative-compatible Kubernetes '
558                 'clusters support fractional values.  Examples .5, 500m, 2')
559  parser.add_argument('--cpu', help=help_msg)
560
561
562def _ConcurrencyValue(value):
563  """Returns True if value is an int > 0 or 'default'."""
564  try:
565    return value == 'default' or int(value) > 0
566  except ValueError:
567    return False
568
569
570def AddConcurrencyFlag(parser):
571  parser.add_argument(
572      '--concurrency',
573      type=arg_parsers.CustomFunctionValidator(
574          _ConcurrencyValue, 'must be an integer greater than 0 or "default".'),
575      help='Set the maximum number of concurrent requests allowed per '
576      'container instance. If concurrency is unspecified, '
577      'any number of concurrent requests are allowed. To unset '
578      'this field, provide the special value `default`.')
579
580
581def AddTimeoutFlag(parser):
582  parser.add_argument(
583      '--timeout',
584      type=arg_parsers.Duration(lower_bound='1s'),
585      help='Set the maximum request execution time (timeout). It is specified '
586      'as a duration; for example, "10m5s" is ten minutes, and five seconds. '
587      'If you don\'t specify a unit, seconds is assumed. For example, "10" is '
588      '10 seconds.')
589
590
591def AddServiceAccountFlag(parser):
592  parser.add_argument(
593      '--service-account',
594      help='Service account associated with the revision of the service. '
595      'The service account represents the identity of '
596      'the running revision, and determines what permissions the revision has. '
597      'For the {} platform, this is the email address of an IAM '
598      'service account. For the Kubernetes-based platforms ({}, {}), this is '
599      'the name of a Kubernetes service account in the same namespace as the '
600      'service. If not provided, the revision will use the default service '
601      'account of the project, or default Kubernetes namespace service account '
602      'respectively.'.format(platforms.PLATFORM_MANAGED, platforms.PLATFORM_GKE,
603                             platforms.PLATFORM_KUBERNETES))
604
605
606def AddPlatformArg(parser, managed_only=False, anthos_only=False):
607  """Add a platform arg."""
608  assert not (managed_only and anthos_only)
609  choices = platforms.PLATFORMS
610  if managed_only:
611    choices = platforms.PLATFORMS_MANAGED_ONLY
612  if anthos_only:
613    choices = platforms.PLATFORMS_ANTHOS_ONLY
614  parser.add_argument(
615      '--platform',
616      choices=choices,
617      action=actions.StoreProperty(properties.VALUES.run.platform),
618      help='Target platform for running commands. '
619      'Alternatively, set the property [run/platform]. '
620      'If not specified, the user will be prompted to choose a platform.')
621
622
623def AddKubeconfigFlags(parser):
624  parser.add_argument(
625      '--kubeconfig',
626      help='The absolute path to your kubectl config file. If not specified, '
627      'the colon- or semicolon-delimited list of paths specified by '
628      '$KUBECONFIG will be used. If $KUBECONFIG is unset, this defaults to '
629      '`{}`.'.format(_DEFAULT_KUBECONFIG_PATH))
630  parser.add_argument(
631      '--context',
632      help='The name of the context in your kubectl config file to use for '
633      'connecting.')
634
635
636def AddRevisionSuffixArg(parser):
637  parser.add_argument(
638      '--revision-suffix',
639      help='Specify the suffix of the revision name. Revision names always '
640      'start with the service name automatically. For example, specifying '
641      '[--revision-suffix=v1] for a service named \'helloworld\', '
642      'would lead to a revision named \'helloworld-v1\'.')
643
644
645def AddSandboxArg(parser):
646  parser.add_argument(
647      '--sandbox',
648      choices=_SANDBOX_CHOICES,
649      help='Selects the sandbox where the application will run.')
650
651
652def AddVpcConnectorArg(parser):
653  parser.add_argument(
654      '--vpc-connector', help='Set a VPC connector for this resource.')
655
656
657def AddVpcConnectorArgs(parser):
658  AddVpcConnectorArg(parser)
659  parser.add_argument(
660      '--clear-vpc-connector',
661      action='store_true',
662      help='Remove the VPC connector for this resource.')
663
664
665def AddEgressSettingsFlag(parser):
666  """Adds a flag for configuring VPC egress for fully-managed."""
667  parser.add_argument(
668      '--vpc-egress',
669      help='The outbound traffic to send through the VPC connector'
670      ' for this resource. This resource must have a VPC connector to set'
671      ' VPC egress.',
672      choices={
673          container_resource.EGRESS_SETTINGS_PRIVATE_RANGES_ONLY:
674              'Default option. Sends outbound traffic to private IP addresses '
675              'defined by RFC1918 through the VPC connector.',
676          container_resource.EGRESS_SETTINGS_ALL:
677              'Sends all outbound traffic through the VPC connector.'
678      })
679
680
681def AddSecretsFlags(parser):
682  """Adds flags for creating, updating, and deleting secrets."""
683  AddMapFlagsNoFile(
684      parser,
685      group_help=('Specify secrets to mount or provide as environment '
686                  "variables. Keys starting with a forward slash '/' are mount "
687                  'paths. All other keys correspond to environment variables. '
688                  'The values associated with each of these should be in the '
689                  'form SECRET_NAME:KEY_IN_SECRET; you may omit the '
690                  'key within the secret to specify a mount of all keys '
691                  'within the secret. For example: '
692                  "'--update-secrets=/my/path=mysecret,"
693                  "ENV=othersecret:key.json' "
694                  "will create a volume with secret 'mysecret' "
695                  "and mount that volume at '/my/path'. Because no secret "
696                  "key was specified, all keys in 'mysecret' will be included. "
697                  'An environment variable named ENV will also be created '
698                  "whose value is the value of 'key.json' in 'othersecret'."),
699      flag_name='secrets')
700
701
702def AddConfigMapsFlags(parser):
703  """Adds flags for creating, updating, and deleting config maps."""
704  AddMapFlagsNoFile(
705      parser,
706      group_help=('Specify config map to mount or provide as environment '
707                  "variables. Keys starting with a forward slash '/' are mount "
708                  'paths. All other keys correspond to environment variables. '
709                  'The values associated with each of these should be in the '
710                  'form CONFIG_MAP_NAME:KEY_IN_CONFIG_MAP; you may omit the '
711                  'key within the config map to specify a mount of all keys '
712                  'within the config map. For example: '
713                  "'--update-config-maps=/my/path=myconfig,"
714                  "ENV=otherconfig:key.json' "
715                  "will create a volume with config map 'myconfig' "
716                  "and mount that volume at '/my/path'. Because no config map "
717                  "key was specified, all keys in 'myconfig' will be included. "
718                  'An environment variable named ENV will also be created '
719                  "whose value is the value of 'key.json' in 'otherconfig'."),
720      flag_name='config-maps')
721
722
723def AddLabelsFlag(parser, extra_message=''):
724  """Add only the --labels flag."""
725  labels_util.GetCreateLabelsFlag(
726      extra_message=extra_message, validate_keys=False,
727      validate_values=False).AddToParser(parser)
728
729
730def AddLabelsFlags(parser):
731  """Adds update command labels flags to an argparse parser.
732
733  Args:
734    parser: The argparse parser to add the flags to.
735  """
736  group = parser.add_group()
737  add_group = group.add_mutually_exclusive_group()
738  AddLabelsFlag(add_group, 'An alias to --update-labels.')
739  labels_util.GetUpdateLabelsFlag(
740      '', validate_keys=False, validate_values=False).AddToParser(add_group)
741  remove_group = group.add_mutually_exclusive_group()
742  labels_util.GetClearLabelsFlag().AddToParser(remove_group)
743  labels_util.GetRemoveLabelsFlag('').AddToParser(remove_group)
744
745
746class _ScaleValue(object):
747  """Type for min/max-instances flag values."""
748
749  def __init__(self, value):
750    self.restore_default = value == 'default'
751    if not self.restore_default:
752      try:
753        self.instance_count = int(value)
754      except (TypeError, ValueError):
755        raise serverless_exceptions.ArgumentError(
756            'Instance count value %s is not an integer '
757            'or \'default\'.' % value)
758
759      if self.instance_count < 0:
760        raise serverless_exceptions.ArgumentError(
761            'Instance count value %s is negative.' % value)
762
763
764def AddMinInstancesFlag(parser):
765  """Add min scaling flag."""
766  parser.add_argument(
767      '--min-instances',
768      type=_ScaleValue,
769      help=('The minimum number of container instances of the Service to run '
770            "or 'default' to remove any minimum."))
771
772
773def AddMaxInstancesFlag(parser):
774  """Add max scaling flag."""
775  parser.add_argument(
776      '--max-instances',
777      type=_ScaleValue,
778      help=('The maximum number of container instances of the Service to run. '
779            "Use 'default' to unset the limit and use the platform default."))
780
781
782def AddCommandFlag(parser):
783  """Add flags for specifying container's startup command."""
784  parser.add_argument(
785      '--command',
786      metavar='COMMAND',
787      type=arg_parsers.ArgList(),
788      action=arg_parsers.UpdateAction,
789      help='Entrypoint for the container image. If not specified, the '
790      'container image\'s default Entrypoint is run. '
791      'To reset this field to its default, pass an empty string.')
792
793
794def AddArgsFlag(parser):
795  """Add flags for specifying container's startup args."""
796  parser.add_argument(
797      '--args',
798      metavar='ARG',
799      type=arg_parsers.ArgList(),
800      action=arg_parsers.UpdateAction,
801      help='Comma-separated arguments passed to the command run by the '
802      'container image. If not specified and no \'--command\' is provided, the '
803      'container image\'s default Cmd is used. Otherwise, if not specified, no '
804      'arguments are passed. '
805      'To reset this field to its default, pass an empty string.')
806
807
808def AddClientNameAndVersionFlags(parser):
809  """Add flags for specifying the client name and version annotations."""
810  parser.add_argument(
811      '--client-name',
812      hidden=True,
813      help="Name of the client handling the deployment. Defaults to ``global'' "
814      'if this and --client-version are both unspecified.')
815  parser.add_argument(
816      '--client-version',
817      hidden=True,
818      help='Version of the client handling the deployment. Defaults to the '
819      'current gcloud version if this and --client-name are both unspecified.')
820
821
822def _PortValue(value):
823  """Returns True if port value is an int within range or 'default'."""
824  try:
825    return value == 'default' or (1 <= int(value) <= 65535)
826  except ValueError:
827    return False
828
829
830def AddPortFlag(parser):
831  """Add port flag to override $PORT."""
832  parser.add_argument(
833      '--port',
834      type=arg_parsers.CustomFunctionValidator(
835          _PortValue,
836          'must be an integer between 1 and 65535, inclusive, or "default".'),
837      help='Container port to receive requests at. Also sets the $PORT '
838      'environment variable. Must be a number between 1 and 65535, inclusive. '
839      'To unset this field, pass the special value "default".')
840
841
842def AddHttp2Flag(parser):
843  """Add http/2 flag to set the port name."""
844  parser.add_argument(
845      '--use-http2',
846      action=arg_parsers.StoreTrueFalseAction,
847      help='Whether to use HTTP/2 for connections to the service.')
848
849
850def AddParallelismFlag(parser):
851  """Add job parallelism/concurrency flag."""
852  parser.add_argument(
853      '--parallelism',
854      type=arg_parsers.BoundedInt(lower_bound=1),
855      default=1,
856      help='Number of instances that may run concurrently. '
857      'Must be less than or equal to the number of completions.')
858
859
860def AddCompletionsFlag(parser):
861  """Add job number of completions flag."""
862  parser.add_argument(
863      '--completions',
864      type=arg_parsers.BoundedInt(lower_bound=1),
865      default=1,
866      help='Number of instances that must run to completion for the job to be '
867      'considered done. Use this flag to trigger multiple runs of the job.')
868
869
870def AddMaxAttemptsFlag(parser):
871  """Add job max attempts flag to specify number of instance restarts."""
872  parser.add_argument(
873      '--max-attempts',
874      type=arg_parsers.BoundedInt(lower_bound=1),
875      default=6,
876      help='Number of times an instance will be allowed to run in case of '
877      'failure before being failed permanently. This applies per-instance, not '
878      'per-job. If set to 1, instances will only run once and never be '
879      'restarted on failure. If set to any other positive integer N, instances '
880      'will be allowed to restart N-1 times.')
881
882
883def AddWaitForCompletionFlag(parser):
884  """Add job flag to poll until completion on create."""
885  parser.add_argument(
886      '--wait-for-completion',
887      default=False,
888      action='store_true',
889      help='Wait until the job has completed running before finishing polling. '
890      'If not set, polling completes when the job has started.')
891
892
893def AddBinAuthzPolicyFlags(parser, with_clear=True):
894  """Add flags for BinAuthz."""
895  policy_group = parser
896  if with_clear:
897    policy_group = parser.add_mutually_exclusive_group()
898    policy_group.add_argument(
899        '--clear-binary-authorization',
900        default=False,
901        action='store_true',
902        help='Remove any previously set Binary Authorization policy.')
903  policy_group.add_argument(
904      '--binary-authorization',
905      metavar='POLICY',
906      # Don't actually validate the value here, let that happen server-side
907      # so the future change to support named policies will be backwards
908      # compatible with older gcloud versions.
909      help='Binary Authorization policy to check against. This must be set to '
910      '"default".')
911
912
913def AddBinAuthzBreakglassFlag(parser):
914  parser.add_argument(
915      '--breakglass',
916      metavar='JUSTIFICATION',
917      help='Justification to bypass Binary Authorization policy constraints '
918      'and allow the operation. See '
919      'https://cloud.google.com/binary-authorization/docs/using-breakglass '
920      'for more information.')
921
922
923def _HasChanges(args, flags):
924  """True iff any of the passed flags are set."""
925  return any(FlagIsExplicitlySet(args, flag) for flag in flags)
926
927
928def _HasEnvChanges(args):
929  """True iff any of the env var flags are set."""
930  env_flags = [
931      'update_env_vars', 'set_env_vars', 'remove_env_vars', 'clear_env_vars'
932  ]
933  return _HasChanges(args, env_flags)
934
935
936def _HasCloudSQLChanges(args):
937  """True iff any of the cloudsql flags are set."""
938  instances_flags = [
939      'add_cloudsql_instances', 'set_cloudsql_instances',
940      'remove_cloudsql_instances', 'clear_cloudsql_instances'
941  ]
942  return _HasChanges(args, instances_flags)
943
944
945def _EnabledCloudSqlApiRequired(args):
946  """True iff flags that add or set cloud sql instances are set."""
947  instances_flags = (
948      'add_cloudsql_instances',
949      'set_cloudsql_instances',
950  )
951  return _HasChanges(args, instances_flags)
952
953
954def _HasLabelChanges(args):
955  """True iff any of the label flags are set."""
956  label_flags = ['labels', 'update_labels', 'clear_labels', 'remove_labels']
957  return _HasChanges(args, label_flags)
958
959
960def _HasSecretsChanges(args):
961  """True iff any of the secret flags are set."""
962  secret_flags = [
963      'update_secrets', 'set_secrets', 'remove_secrets', 'clear_secrets'
964  ]
965  return _HasChanges(args, secret_flags)
966
967
968def _HasConfigMapsChanges(args):
969  """True iff any of the config maps flags are set."""
970  config_maps_flags = [
971      'update_config_maps', 'set_config_maps', 'remove_config_maps',
972      'clear_config_maps'
973  ]
974  return _HasChanges(args, config_maps_flags)
975
976
977def _HasTrafficTagsChanges(args):
978  """True iff any of the traffic tags flags are set."""
979  tags_flags = ['update_tags', 'set_tags', 'remove_tags', 'clear_tags']
980  return _HasChanges(args, tags_flags)
981
982
983def _HasTrafficChanges(args):
984  """True iff any of the traffic flags are set."""
985  traffic_flags = ['to_revisions', 'to_tags', 'to_latest']
986  return _HasChanges(args, traffic_flags) or _HasTrafficTagsChanges(args)
987
988
989def _GetEnvChanges(args):
990  """Return config_changes.EnvVarLiteralChanges for given args."""
991  return config_changes.EnvVarLiteralChanges(
992      updates=_StripKeys(
993          getattr(args, 'update_env_vars', None) or args.set_env_vars or {}),
994      removes=_MapLStrip(getattr(args, 'remove_env_vars', None) or []),
995      clear_others=bool(args.set_env_vars or args.clear_env_vars))
996
997
998def _GetScalingChanges(args):
999  """Returns the list of changes for scaling for given args."""
1000  result = []
1001  if 'min_instances' in args and args.min_instances is not None:
1002    scale_value = args.min_instances
1003    if scale_value.restore_default or scale_value.instance_count == 0:
1004      result.append(
1005          config_changes.DeleteTemplateAnnotationChange(
1006              revision.MIN_SCALE_ANNOTATION))
1007    else:
1008      result.append(
1009          config_changes.SetTemplateAnnotationChange(
1010              revision.MIN_SCALE_ANNOTATION, str(scale_value.instance_count)))
1011  if 'max_instances' in args and args.max_instances is not None:
1012    scale_value = args.max_instances
1013    if scale_value.restore_default:
1014      result.append(
1015          config_changes.DeleteTemplateAnnotationChange(
1016              revision.MAX_SCALE_ANNOTATION))
1017    else:
1018      result.append(
1019          config_changes.SetTemplateAnnotationChange(
1020              revision.MAX_SCALE_ANNOTATION, str(scale_value.instance_count)))
1021  return result
1022
1023
1024def _IsVolumeMountKey(key):
1025  """Returns True if the key refers to a volume mount."""
1026  return key.startswith('/')
1027
1028
1029def _GetSecretsChanges(args):
1030  """Return secret env var and volume changes for given args."""
1031  volume_kwargs = {}
1032  env_kwargs = {}
1033
1034  updates = _StripKeys(
1035      getattr(args, 'update_secrets', None) or args.set_secrets or {})
1036  volume_kwargs['updates'] = {
1037      k: v for k, v in updates.items() if _IsVolumeMountKey(k)
1038  }
1039  env_kwargs['updates'] = {
1040      k: v for k, v in updates.items() if not _IsVolumeMountKey(k)
1041  }
1042
1043  removes = _MapLStrip(getattr(args, 'remove_secrets', None) or [])
1044  volume_kwargs['removes'] = [k for k in removes if _IsVolumeMountKey(k)]
1045  env_kwargs['removes'] = [k for k in removes if not _IsVolumeMountKey(k)]
1046
1047  clear_others = bool(args.set_secrets or args.clear_secrets)
1048  env_kwargs['clear_others'] = clear_others
1049  volume_kwargs['clear_others'] = clear_others
1050
1051  secret_changes = []
1052  if any(env_kwargs.values()):
1053    secret_changes.append(config_changes.SecretEnvVarChanges(**env_kwargs))
1054  if any(volume_kwargs.values()):
1055    secret_changes.append(config_changes.SecretVolumeChanges(**volume_kwargs))
1056  return secret_changes
1057
1058
1059def _GetConfigMapsChanges(args):
1060  """Return config map env var and volume changes for given args."""
1061  volume_kwargs = {}
1062  env_kwargs = {}
1063
1064  updates = _StripKeys(
1065      getattr(args, 'update_config_maps', None) or args.set_config_maps or {})
1066  volume_kwargs['updates'] = {
1067      k: v for k, v in updates.items() if _IsVolumeMountKey(k)
1068  }
1069  env_kwargs['updates'] = {
1070      k: v for k, v in updates.items() if not _IsVolumeMountKey(k)
1071  }
1072
1073  removes = _MapLStrip(getattr(args, 'remove_config_maps', None) or [])
1074  volume_kwargs['removes'] = [k for k in removes if _IsVolumeMountKey(k)]
1075  env_kwargs['removes'] = [k for k in removes if not _IsVolumeMountKey(k)]
1076
1077  clear_others = bool(args.set_config_maps or args.clear_config_maps)
1078  env_kwargs['clear_others'] = clear_others
1079  volume_kwargs['clear_others'] = clear_others
1080
1081  config_maps_changes = []
1082  if any(env_kwargs.values()):
1083    config_maps_changes.append(
1084        config_changes.ConfigMapEnvVarChanges(**env_kwargs))
1085  if any(volume_kwargs.values()):
1086    config_maps_changes.append(
1087        config_changes.ConfigMapVolumeChanges(**volume_kwargs))
1088  return config_maps_changes
1089
1090
1091def PromptToEnableApi(service_name):
1092  """Prompts to enable the API and throws if the answer is no.
1093
1094  Args:
1095    service_name: str, The service token of the API to prompt for.
1096  """
1097  if not properties.VALUES.core.should_prompt_to_enable_api.GetBool():
1098    return
1099
1100  project = properties.VALUES.core.project.Get(required=True)
1101  # Don't prompt to enable an already enabled API
1102  if not enable_api.IsServiceEnabled(project, service_name):
1103    if console_io.PromptContinue(
1104        default=False,
1105        cancel_on_no=True,
1106        prompt_string=('API [{}] not enabled on project [{}]. '
1107                       'Would you like to enable and retry (this will take a '
1108                       'few minutes)?').format(service_name, project)):
1109      enable_api.EnableService(project, service_name)
1110
1111
1112_CLOUD_SQL_API_SERVICE_TOKEN = 'sql-component.googleapis.com'
1113_CLOUD_SQL_ADMIN_API_SERVICE_TOKEN = 'sqladmin.googleapis.com'
1114
1115
1116def _CheckCloudSQLApiEnablement():
1117  if not properties.VALUES.core.should_prompt_to_enable_api.GetBool():
1118    return
1119  try:
1120    PromptToEnableApi(_CLOUD_SQL_API_SERVICE_TOKEN)
1121    PromptToEnableApi(_CLOUD_SQL_ADMIN_API_SERVICE_TOKEN)
1122  except (services_exceptions.GetServicePermissionDeniedException,
1123          apitools_exceptions.HttpError):
1124    log.status.Print('Skipped validating Cloud SQL API and Cloud SQL Admin API'
1125                     ' enablement due to an issue contacting the Service Usage '
1126                     ' API. Please ensure the Cloud SQL API and Cloud SQL Admin'
1127                     ' API are activated (see '
1128                     'https://console.cloud.google.com/apis/dashboard).')
1129
1130
1131def _GetTrafficChanges(args):
1132  """Returns a changes for traffic assignment based on the flags."""
1133  # Check if args has tags changes again in case args does not include tags
1134  # flags. Tags will launch in the alpha release track only.
1135  if _HasTrafficTagsChanges(args):
1136    update_tags = args.update_tags or args.set_tags
1137    remove_tags = args.remove_tags
1138    clear_other_tags = bool(args.set_tags) or args.clear_tags
1139  else:
1140    update_tags = None
1141    remove_tags = None
1142    clear_other_tags = False
1143  by_tag = False
1144  if args.to_latest:
1145    # Mutually exlcusive flag with to-revisions, to-tags
1146    new_percentages = {traffic.LATEST_REVISION_KEY: 100}
1147  elif args.to_revisions:
1148    new_percentages = args.to_revisions
1149  elif FlagIsExplicitlySet(args, 'to_tags'):
1150    new_percentages = args.to_tags
1151    by_tag = True
1152  else:
1153    new_percentages = {}
1154
1155  return config_changes.TrafficChanges(new_percentages, by_tag, update_tags,
1156                                       remove_tags, clear_other_tags)
1157
1158
1159def _GetIngressChanges(args):
1160  """Returns changes to ingress traffic allowed based on the flags."""
1161  platform = platforms.GetPlatform()
1162  if platform == platforms.PLATFORM_MANAGED:
1163    return config_changes.SetAnnotationChange(service.INGRESS_ANNOTATION,
1164                                              args.ingress)
1165  elif args.ingress == service.INGRESS_INTERNAL:
1166    return config_changes.EndpointVisibilityChange(True)
1167  elif args.ingress == service.INGRESS_ALL:
1168    return config_changes.EndpointVisibilityChange(False)
1169  else:
1170    raise serverless_exceptions.ConfigurationError(
1171        'Ingress value `{}` is not supported on platform `{}`.'.format(
1172            args.ingress, platform))
1173
1174
1175def GetConfigurationChanges(args):
1176  """Returns a list of changes to Configuration, based on the flags set."""
1177  changes = []
1178
1179  # Set client name and version regardless of whether or not it was specified.
1180  if 'client_name' in args:
1181    is_either_specified = (
1182        args.IsSpecified('client_name') or args.IsSpecified('client_version'))
1183    changes.append(
1184        config_changes.SetClientNameAndVersionAnnotationChange(
1185            args.client_name if is_either_specified else 'gcloud',
1186            args.client_version
1187            if is_either_specified else config.CLOUD_SDK_VERSION))
1188
1189  # FlagIsExplicitlySet can't be used here because args.image is also set from
1190  # code in deploy.py.
1191  if hasattr(args, 'image') and args.image is not None:
1192    changes.append(config_changes.ImageChange(args.image))
1193
1194  changes.extend(_GetScalingChanges(args))
1195  if _HasEnvChanges(args):
1196    changes.append(_GetEnvChanges(args))
1197
1198  if _HasTrafficChanges(args):
1199    changes.append(_GetTrafficChanges(args))
1200
1201  if _HasCloudSQLChanges(args):
1202    region = GetRegion(args)
1203    project = (
1204        getattr(args, 'project', None) or
1205        properties.VALUES.core.project.Get(required=True))
1206    if _EnabledCloudSqlApiRequired(args):
1207      _CheckCloudSQLApiEnablement()
1208    changes.append(config_changes.CloudSQLChanges(project, region, args))
1209
1210  if _HasSecretsChanges(args):
1211    changes.extend(_GetSecretsChanges(args))
1212
1213  if _HasConfigMapsChanges(args):
1214    changes.extend(_GetConfigMapsChanges(args))
1215
1216  if 'no_traffic' in args and args.no_traffic:
1217    changes.append(config_changes.NoTrafficChange())
1218
1219  if 'cpu' in args and args.cpu:
1220    changes.append(config_changes.ResourceChanges(cpu=args.cpu))
1221  if 'memory' in args and args.memory:
1222    changes.append(config_changes.ResourceChanges(memory=args.memory))
1223  if 'concurrency' in args and args.concurrency:
1224    changes.append(
1225        config_changes.ConcurrencyChanges(concurrency=args.concurrency))
1226  if 'timeout' in args and args.timeout:
1227    changes.append(config_changes.TimeoutChanges(timeout=args.timeout))
1228  if 'service_account' in args and args.service_account:
1229    changes.append(
1230        config_changes.ServiceAccountChanges(
1231            service_account=args.service_account))
1232  if _HasLabelChanges(args):
1233    additions = (
1234        args.labels
1235        if FlagIsExplicitlySet(args, 'labels') else args.update_labels)
1236    diff = labels_util.Diff(
1237        additions=additions,
1238        subtractions=args.remove_labels if 'remove_labels' in args else [],
1239        clear=args.clear_labels if 'clear_labels' in args else False)
1240    if diff.MayHaveUpdates():
1241      changes.append(config_changes.LabelChanges(diff))
1242  if 'revision_suffix' in args and args.revision_suffix:
1243    changes.append(config_changes.RevisionNameChanges(args.revision_suffix))
1244  if 'sandbox' in args and args.sandbox:
1245    changes.append(config_changes.SandboxChange(args.sandbox))
1246  if 'vpc_connector' in args and args.vpc_connector:
1247    changes.append(config_changes.VpcConnectorChange(args.vpc_connector))
1248  if FlagIsExplicitlySet(args, 'vpc_egress'):
1249    changes.append(
1250        config_changes.SetTemplateAnnotationChange(
1251            container_resource.EGRESS_SETTINGS_ANNOTATION, args.vpc_egress))
1252  if 'clear_vpc_connector' in args and args.clear_vpc_connector:
1253    # MUST be after 'vpc_egress' change.
1254    changes.append(config_changes.ClearVpcConnectorChange())
1255  if 'connectivity' in args and args.connectivity:
1256    if args.connectivity == 'internal':
1257      changes.append(config_changes.EndpointVisibilityChange(True))
1258    elif args.connectivity == 'external':
1259      changes.append(config_changes.EndpointVisibilityChange(False))
1260  if FlagIsExplicitlySet(args, 'ingress'):
1261    changes.append(_GetIngressChanges(args))
1262  if 'command' in args and args.command is not None:
1263    # Allow passing an empty string here to reset the field
1264    changes.append(config_changes.ContainerCommandChange(args.command))
1265  if 'args' in args and args.args is not None:
1266    # Allow passing an empty string here to reset the field
1267    changes.append(config_changes.ContainerArgsChange(args.args))
1268  if FlagIsExplicitlySet(args, 'port'):
1269    changes.append(config_changes.ContainerPortChange(port=args.port))
1270  if FlagIsExplicitlySet(args, 'use_http2'):
1271    changes.append(config_changes.ContainerPortChange(use_http2=args.use_http2))
1272  if FlagIsExplicitlySet(args, 'tag'):
1273    # MUST be after 'revision_suffix' change
1274    changes.append(config_changes.TagOnDeployChange(args.tag))
1275  if FlagIsExplicitlySet(args, 'parallelism'):
1276    changes.append(config_changes.SpecChange('parallelism', args.parallelism))
1277  if FlagIsExplicitlySet(args, 'completions'):
1278    changes.append(config_changes.SpecChange('completions', args.completions))
1279  if FlagIsExplicitlySet(args, 'max_attempts'):
1280    changes.append(config_changes.JobMaxAttemptsChange(args.max_attempts))
1281  if FlagIsExplicitlySet(args, 'binary_authorization'):
1282    changes.append(
1283        config_changes.SetAnnotationChange(
1284            k8s_object.BINAUTHZ_POLICY_ANNOTATION,
1285            args.binary_authorization))
1286  if FlagIsExplicitlySet(args, 'clear_binary_authorization'):
1287    changes.append(
1288        config_changes.DeleteAnnotationChange(
1289            k8s_object.BINAUTHZ_POLICY_ANNOTATION))
1290  if FlagIsExplicitlySet(args, 'breakglass'):
1291    changes.append(
1292        config_changes.SetAnnotationChange(
1293            k8s_object.BINAUTHZ_BREAKGLASS_ANNOTATION,
1294            args.breakglass))
1295  return changes
1296
1297
1298def ValidateResource(resource_ref):
1299  """Validate resource name."""
1300  # Valid resource names comprise only alphanumeric characters and dashes. Must
1301  # not begin or end with a dash, and must not contain more than 63 characters.
1302  # Must be lowercase.
1303  k8s_resource_name_regex = re.compile(
1304      r'(?=^[a-z0-9-]{1,63}$)(?!^\-.*)(?!.*\-$)')
1305  if not k8s_resource_name_regex.match(resource_ref.Name()):
1306    raise serverless_exceptions.ArgumentError(
1307        'Invalid resource name [{}]. The name must use only lowercase '
1308        'alphanumeric characters and dashes, cannot begin or end with a dash, '
1309        'and cannot be longer than 63 characters.'.format(resource_ref.Name()))
1310
1311
1312def PromptForRegion():
1313  """Prompt for region from list of available regions.
1314
1315  This method is referenced by the declaritive iam commands as a fallthrough
1316  for getting the region.
1317
1318  Returns:
1319    The region specified by the user, str
1320  """
1321  if console_io.CanPrompt():
1322    client = global_methods.GetServerlessClientInstance()
1323    all_regions = global_methods.ListRegions(client)
1324    idx = console_io.PromptChoice(
1325        all_regions, message='Please specify a region:\n', cancel_option=True)
1326    region = all_regions[idx]
1327    log.status.Print('To make this the default region, run '
1328                     '`gcloud config set run/region {}`.\n'.format(region))
1329    return region
1330
1331
1332def GetRegion(args, prompt=False):
1333  """Prompt for region if not provided.
1334
1335  Region is decided in the following order:
1336  - region argument;
1337  - run/region gcloud config;
1338  - prompt user.
1339
1340  Args:
1341    args: Namespace, The args namespace.
1342    prompt: bool, whether to attempt to prompt.
1343
1344  Returns:
1345    A str representing region.
1346  """
1347  if getattr(args, 'region', None):
1348    return args.region
1349  if properties.VALUES.run.region.IsExplicitlySet():
1350    return properties.VALUES.run.region.Get()
1351  if prompt:
1352    region = PromptForRegion()
1353    if region:
1354      # set the region on args, so we're not embarassed the next time we call
1355      # GetRegion
1356      args.region = region
1357      return region
1358
1359
1360def GetAllowUnauthenticated(args, client=None, service_ref=None, prompt=False):
1361  """Return bool for the explicit intent to allow unauth invocations or None.
1362
1363  If --[no-]allow-unauthenticated is set, return that value. If not set,
1364  prompt for value if desired. If prompting not necessary or doable,
1365  return None, indicating that no action needs to be taken.
1366
1367  Args:
1368    args: Namespace, The args namespace
1369    client: from googlecloudsdk.command_lib.run import serverless_operations
1370      serverless_operations.ServerlessOperations object
1371    service_ref: service resource reference (e.g. args.CONCEPTS.service.Parse())
1372    prompt: bool, whether to attempt to prompt.
1373
1374  Returns:
1375    bool indicating whether to allow/unallow unauthenticated or None if N/A
1376  """
1377  if getattr(args, 'allow_unauthenticated', None) is not None:
1378    return args.allow_unauthenticated
1379
1380  if prompt:
1381    # Need to check if the user has permissions before we prompt
1382    assert client is not None and service_ref is not None
1383    if client.CanSetIamPolicyBinding(service_ref):
1384      return console_io.PromptContinue(
1385          prompt_string=('Allow unauthenticated invocations '
1386                         'to [{}]'.format(service_ref.servicesId)),
1387          default=False)
1388    else:
1389      pretty_print.Info(
1390          'This service will require authentication to be invoked.')
1391  return None
1392
1393
1394def GetKubeconfig(file_path=None):
1395  """Get config from kubeconfig file.
1396
1397  Get config from potentially 3 different places, falling back to the next
1398  option as necessary:
1399  1. file_path specified as argument by the user
1400  2. List of file paths specified in $KUBECONFIG
1401  3. Default config path (~/.kube/config)
1402
1403  Args:
1404    file_path: str, the path to the kubeconfig if provided by the user
1405
1406  Returns:
1407    dict: config object
1408
1409  Raises:
1410    KubeconfigError: if $KUBECONFIG is set but contains no valid paths
1411  """
1412  if file_path:
1413    return kubeconfig.Kubeconfig.LoadFromFile(files.ExpandHomeDir(file_path))
1414  if encoding.GetEncodedValue(os.environ, 'KUBECONFIG'):
1415    config_paths = encoding.GetEncodedValue(os.environ,
1416                                            'KUBECONFIG').split(os.pathsep)
1417    kube_config = None
1418    # Merge together all valid paths into single config
1419    for path in config_paths:
1420      try:
1421        other_config = kubeconfig.Kubeconfig.LoadFromFile(
1422            files.ExpandHomeDir(path))
1423        if not kube_config:
1424          kube_config = other_config
1425        else:
1426          kube_config.Merge(other_config)
1427      except kubeconfig.Error:
1428        pass
1429    if not kube_config:
1430      raise KubeconfigError('No valid file paths found in $KUBECONFIG')
1431    return kube_config
1432  return kubeconfig.Kubeconfig.LoadFromFile(
1433      files.ExpandHomeDir(_DEFAULT_KUBECONFIG_PATH))
1434
1435
1436def FlagIsExplicitlySet(args, flag):
1437  """Return True if --flag is explicitly passed by the user."""
1438  # hasattr check is to allow the same code to work for release tracks that
1439  # don't have the args at all yet.
1440  return hasattr(args, flag) and args.IsSpecified(flag)
1441
1442
1443def VerifyManagedFlags(args, release_track, product):
1444  """Raise ConfigurationError if args aren't valid for managed Cloud Run."""
1445
1446  if product == Product.EVENTS:
1447    raise serverless_exceptions.ConfigurationError(
1448        'The flag --platform={0} is not supported. '
1449        'Instead of using the flag --platform={0} in "gcloud events", '
1450        'run "gcloud eventarc".'
1451        .format(platforms.PLATFORM_MANAGED))
1452
1453  error_msg = ('The `{flag}` flag is not supported on the fully managed '
1454               'version of Cloud Run. Specify `--platform {platform}` or run '
1455               '`gcloud config set run/platform {platform}` to work with '
1456               '{platform_desc}.')
1457
1458  if FlagIsExplicitlySet(args, 'connectivity'):
1459    raise serverless_exceptions.ConfigurationError(
1460        error_msg.format(
1461            flag='--connectivity=[internal|external]',
1462            platform=platforms.PLATFORM_GKE,
1463            platform_desc=platforms.PLATFORM_SHORT_DESCRIPTIONS[
1464                platforms.PLATFORM_GKE]))
1465
1466  if FlagIsExplicitlySet(args, 'namespace'):
1467    raise serverless_exceptions.ConfigurationError(
1468        error_msg.format(
1469            flag='--namespace',
1470            platform=platforms.PLATFORM_GKE,
1471            platform_desc=platforms.PLATFORM_SHORT_DESCRIPTIONS[
1472                platforms.PLATFORM_GKE]))
1473
1474  if FlagIsExplicitlySet(args, 'cluster'):
1475    raise serverless_exceptions.ConfigurationError(
1476        error_msg.format(
1477            flag='--cluster',
1478            platform=platforms.PLATFORM_GKE,
1479            platform_desc=platforms.PLATFORM_SHORT_DESCRIPTIONS[
1480                platforms.PLATFORM_GKE]))
1481
1482  if FlagIsExplicitlySet(args, 'cluster_location'):
1483    raise serverless_exceptions.ConfigurationError(
1484        error_msg.format(
1485            flag='--cluster-location',
1486            platform=platforms.PLATFORM_GKE,
1487            platform_desc=platforms.PLATFORM_SHORT_DESCRIPTIONS[
1488                platforms.PLATFORM_GKE]))
1489
1490  if _HasSecretsChanges(args) and release_track != base.ReleaseTrack.ALPHA:
1491    raise serverless_exceptions.ConfigurationError(
1492        'The `--[update|set|remove|clear]-secrets` flags are only supported '
1493        'in the alpha release track on the fully managed version of Cloud '
1494        'Run. Use `gcloud alpha` to enable these flags.')
1495
1496  if _HasConfigMapsChanges(args):
1497    raise serverless_exceptions.ConfigurationError(
1498        error_msg.format(
1499            flag='--[update|set|remove|clear]-config-maps',
1500            platform=platforms.PLATFORM_GKE,
1501            platform_desc=platforms.PLATFORM_SHORT_DESCRIPTIONS[
1502                platforms.PLATFORM_GKE]))
1503
1504  if FlagIsExplicitlySet(args,
1505                         'use_http2') and release_track == base.ReleaseTrack.GA:
1506    raise serverless_exceptions.ConfigurationError(
1507        'The `--use-http2` flag is only supported in the beta release '
1508        'track on the fully managed version of Cloud Run. Use `gcloud alpha` '
1509        'to set `--use-http2` on Cloud Run (fully managed). Alternatively, '
1510        'specify `--platform gke` or run '
1511        '`gcloud config set run/platform gke`.')
1512
1513  if FlagIsExplicitlySet(args, 'broker'):
1514    raise serverless_exceptions.ConfigurationError(
1515        error_msg.format(
1516            flag='--broker',
1517            platform=platforms.PLATFORM_GKE,
1518            platform_desc=platforms.PLATFORM_SHORT_DESCRIPTIONS[
1519                platforms.PLATFORM_GKE]))
1520
1521  if FlagIsExplicitlySet(args, 'custom_type') and product == Product.EVENTS:
1522    raise serverless_exceptions.ConfigurationError(
1523        error_msg.format(
1524            flag='--custom-type',
1525            platform=platforms.PLATFORM_GKE,
1526            platform_desc=platforms.PLATFORM_SHORT_DESCRIPTIONS[
1527                platforms.PLATFORM_GKE]))
1528
1529  if FlagIsExplicitlySet(args, 'kubeconfig'):
1530    raise serverless_exceptions.ConfigurationError(
1531        error_msg.format(
1532            flag='--kubeconfig',
1533            platform=platforms.PLATFORM_KUBERNETES,
1534            platform_desc=platforms.PLATFORM_SHORT_DESCRIPTIONS[
1535                platforms.PLATFORM_KUBERNETES]))
1536
1537  if FlagIsExplicitlySet(args, 'context'):
1538    raise serverless_exceptions.ConfigurationError(
1539        error_msg.format(
1540            flag='--context',
1541            platform=platforms.PLATFORM_KUBERNETES,
1542            platform_desc=platforms.PLATFORM_SHORT_DESCRIPTIONS[
1543                platforms.PLATFORM_KUBERNETES]))
1544
1545  if (FlagIsExplicitlySet(args, 'timeout') and
1546      release_track == base.ReleaseTrack.GA):
1547    if args.timeout > _FIFTEEN_MINUTES:
1548      raise serverless_exceptions.ConfigurationError(
1549          'Timeout duration must be less than 15m. Timeouts above 15m are in '
1550          'Beta. Use "gcloud beta run ..." to set timeouts above 15m.')
1551
1552  if FlagIsExplicitlySet(args, 'trigger_filters'):
1553    raise serverless_exceptions.ConfigurationError(
1554        error_msg.format(
1555            flag='--trigger-filters',
1556            platform=platforms.PLATFORM_GKE,
1557            platform_desc=platforms.PLATFORM_SHORT_DESCRIPTIONS[
1558                platforms.PLATFORM_GKE]))
1559
1560
1561def VerifyGKEFlags(args, release_track, product):
1562  """Raise ConfigurationError if args includes OnePlatform only arguments."""
1563  error_msg = ('The `{flag}` flag is not supported with Cloud Run for Anthos '
1564               'deployed on Google Cloud. Specify `--platform {platform}` or '
1565               'run `gcloud config set run/platform {platform}` to work with '
1566               '{platform_desc}.')
1567
1568  if FlagIsExplicitlySet(args, 'allow_unauthenticated'):
1569    raise serverless_exceptions.ConfigurationError(
1570        'The `--[no-]allow-unauthenticated` flag is not supported with '
1571        'Cloud Run for Anthos deployed on Google Cloud. All deployed '
1572        'services allow unauthenticated requests. The `--connectivity` '
1573        'flag can limit which network a service is available on to reduce '
1574        'access.')
1575
1576  if (FlagIsExplicitlySet(args, 'connectivity') and
1577      FlagIsExplicitlySet(args, 'ingress')):
1578    raise serverless_exceptions.ConfigurationError(
1579        'Cannot specify both the `--connectivity` and `--ingress` flags.'
1580        ' `--connectivity` is deprecated in favor of `--ingress`.')
1581
1582  if FlagIsExplicitlySet(args, 'region'):
1583    raise serverless_exceptions.ConfigurationError(
1584        error_msg.format(
1585            flag='--region',
1586            platform=platforms.PLATFORM_MANAGED,
1587            platform_desc=platforms.PLATFORM_SHORT_DESCRIPTIONS[
1588                platforms.PLATFORM_MANAGED]))
1589
1590  if FlagIsExplicitlySet(args, 'sandbox'):
1591    raise serverless_exceptions.ConfigurationError(
1592        error_msg.format(
1593            flag='--sandbox',
1594            platform=platforms.PLATFORM_MANAGED,
1595            platform_desc=platforms.PLATFORM_SHORT_DESCRIPTIONS[
1596                platforms.PLATFORM_MANAGED]))
1597
1598  if FlagIsExplicitlySet(args, 'vpc_connector'):
1599    raise serverless_exceptions.ConfigurationError(
1600        error_msg.format(
1601            flag='--vpc-connector',
1602            platform=platforms.PLATFORM_MANAGED,
1603            platform_desc=platforms.PLATFORM_SHORT_DESCRIPTIONS[
1604                platforms.PLATFORM_MANAGED]))
1605
1606  if FlagIsExplicitlySet(args, 'clear_vpc_connector'):
1607    raise serverless_exceptions.ConfigurationError(
1608        error_msg.format(
1609            flag='--clear-vpc-connector',
1610            platform=platforms.PLATFORM_MANAGED,
1611            platform_desc=platforms.PLATFORM_SHORT_DESCRIPTIONS[
1612                platforms.PLATFORM_MANAGED]))
1613
1614  if FlagIsExplicitlySet(args, 'vpc_egress'):
1615    raise serverless_exceptions.ConfigurationError(
1616        error_msg.format(
1617            flag='--vpc-egress',
1618            platform=platforms.PLATFORM_MANAGED,
1619            platform_desc=platforms.PLATFORM_SHORT_DESCRIPTIONS[
1620                platforms.PLATFORM_MANAGED]))
1621
1622  if FlagIsExplicitlySet(args, 'binary_authorization'):
1623    raise serverless_exceptions.ConfigurationError(
1624        error_msg.format(
1625            flag='--binary-authorization',
1626            platform=platforms.PLATFORM_MANAGED,
1627            platform_desc=platforms.PLATFORM_SHORT_DESCRIPTIONS[
1628                platforms.PLATFORM_MANAGED]))
1629
1630  if FlagIsExplicitlySet(args, 'clear_binary_authorization'):
1631    raise serverless_exceptions.ConfigurationError(
1632        error_msg.format(
1633            flag='--clear-binary-authorization',
1634            platform=platforms.PLATFORM_MANAGED,
1635            platform_desc=platforms.PLATFORM_SHORT_DESCRIPTIONS[
1636                platforms.PLATFORM_MANAGED]))
1637
1638  if FlagIsExplicitlySet(args, 'breakglass'):
1639    raise serverless_exceptions.ConfigurationError(
1640        error_msg.format(
1641            flag='--breakglass',
1642            platform=platforms.PLATFORM_MANAGED,
1643            platform_desc=platforms.PLATFORM_SHORT_DESCRIPTIONS[
1644                platforms.PLATFORM_MANAGED]))
1645
1646  if FlagIsExplicitlySet(args, 'kubeconfig'):
1647    raise serverless_exceptions.ConfigurationError(
1648        error_msg.format(
1649            flag='--kubeconfig',
1650            platform=platforms.PLATFORM_KUBERNETES,
1651            platform_desc=platforms.PLATFORM_SHORT_DESCRIPTIONS[
1652                platforms.PLATFORM_KUBERNETES]))
1653
1654  if FlagIsExplicitlySet(args, 'context'):
1655    raise serverless_exceptions.ConfigurationError(
1656        error_msg.format(
1657            flag='--context',
1658            platform=platforms.PLATFORM_KUBERNETES,
1659            platform_desc=platforms.PLATFORM_SHORT_DESCRIPTIONS[
1660                platforms.PLATFORM_KUBERNETES]))
1661
1662
1663def VerifyKubernetesFlags(args, release_track, product):
1664  """Raise ConfigurationError if args includes OnePlatform or GKE only arguments."""
1665  error_msg = ('The `{flag}` flag is not supported with Cloud Run for Anthos '
1666               'deployed on VMware. Specify `--platform {platform}` or run '
1667               '`gcloud config set run/platform {platform}` to work with '
1668               '{platform_desc}.')
1669
1670  if FlagIsExplicitlySet(args, 'allow_unauthenticated'):
1671    raise serverless_exceptions.ConfigurationError(
1672        'The `--[no-]allow-unauthenticated` flag is not supported with '
1673        'Cloud Run for Anthos deployed on VMware. All deployed '
1674        'services allow unauthenticated requests. The `--connectivity` '
1675        'flag can limit which network a service is available on to reduce '
1676        'access.')
1677
1678  if (FlagIsExplicitlySet(args, 'connectivity') and
1679      FlagIsExplicitlySet(args, 'ingress')):
1680    raise serverless_exceptions.ConfigurationError(
1681        'Cannot specify both the `--connectivity` and `--ingress` flags.'
1682        ' `--connectivity` is deprecated in favor of `--ingress`.')
1683
1684  if FlagIsExplicitlySet(args, 'region'):
1685    raise serverless_exceptions.ConfigurationError(
1686        error_msg.format(
1687            flag='--region',
1688            platform=platforms.PLATFORM_MANAGED,
1689            platform_desc=platforms.PLATFORM_SHORT_DESCRIPTIONS[
1690                platforms.PLATFORM_MANAGED]))
1691
1692  if FlagIsExplicitlySet(args, 'sandbox'):
1693    raise serverless_exceptions.ConfigurationError(
1694        error_msg.format(
1695            flag='--sandbox',
1696            platform=platforms.PLATFORM_MANAGED,
1697            platform_desc=platforms.PLATFORM_SHORT_DESCRIPTIONS[
1698                platforms.PLATFORM_MANAGED]))
1699
1700  if FlagIsExplicitlySet(args, 'vpc_connector'):
1701    raise serverless_exceptions.ConfigurationError(
1702        error_msg.format(
1703            flag='--vpc-connector',
1704            platform='managed',
1705            platform_desc=platforms.PLATFORM_SHORT_DESCRIPTIONS['managed']))
1706
1707  if FlagIsExplicitlySet(args, 'clear_vpc_connector'):
1708    raise serverless_exceptions.ConfigurationError(
1709        error_msg.format(
1710            flag='--clear-vpc-connector',
1711            platform=platforms.PLATFORM_MANAGED,
1712            platform_desc=platforms.PLATFORM_SHORT_DESCRIPTIONS[
1713                platforms.PLATFORM_MANAGED]))
1714
1715  if FlagIsExplicitlySet(args, 'vpc_egress'):
1716    raise serverless_exceptions.ConfigurationError(
1717        error_msg.format(
1718            flag='--vpc-egress',
1719            platform=platforms.PLATFORM_MANAGED,
1720            platform_desc=platforms.PLATFORM_SHORT_DESCRIPTIONS[
1721                platforms.PLATFORM_MANAGED]))
1722
1723  if FlagIsExplicitlySet(args, 'binary_authorization'):
1724    raise serverless_exceptions.ConfigurationError(
1725        error_msg.format(
1726            flag='--binary-authorization',
1727            platform=platforms.PLATFORM_MANAGED,
1728            platform_desc=platforms.PLATFORM_SHORT_DESCRIPTIONS[
1729                platforms.PLATFORM_MANAGED]))
1730
1731  if FlagIsExplicitlySet(args, 'clear_binary_authorization'):
1732    raise serverless_exceptions.ConfigurationError(
1733        error_msg.format(
1734            flag='--clear-binary-authorization',
1735            platform=platforms.PLATFORM_MANAGED,
1736            platform_desc=platforms.PLATFORM_SHORT_DESCRIPTIONS[
1737                platforms.PLATFORM_MANAGED]))
1738
1739  if FlagIsExplicitlySet(args, 'breakglass'):
1740    raise serverless_exceptions.ConfigurationError(
1741        error_msg.format(
1742            flag='--breakglass',
1743            platform=platforms.PLATFORM_MANAGED,
1744            platform_desc=platforms.PLATFORM_SHORT_DESCRIPTIONS[
1745                platforms.PLATFORM_MANAGED]))
1746
1747  if FlagIsExplicitlySet(args, 'cluster'):
1748    raise serverless_exceptions.ConfigurationError(
1749        error_msg.format(
1750            flag='--cluster',
1751            platform=platforms.PLATFORM_GKE,
1752            platform_desc=platforms.PLATFORM_SHORT_DESCRIPTIONS[
1753                platforms.PLATFORM_GKE]))
1754
1755  if FlagIsExplicitlySet(args, 'cluster_location'):
1756    raise serverless_exceptions.ConfigurationError(
1757        error_msg.format(
1758            flag='--cluster-location',
1759            platform=platforms.PLATFORM_GKE,
1760            platform_desc=platforms.PLATFORM_SHORT_DESCRIPTIONS[
1761                platforms.PLATFORM_GKE]))
1762
1763
1764def GetAndValidatePlatform(args, release_track, product, allow_empty=False):
1765  """Returns the platform to run on and validates specified flags.
1766
1767  A given command may support multiple platforms, but not every flag is
1768  supported by every platform. This method validates that all specified flags
1769  are supported by the specified platform.
1770
1771  Args:
1772    args: Namespace, The args namespace.
1773    release_track: base.ReleaseTrack, calliope release track.
1774    product: Product, which product the command was executed for (e.g. Run or
1775      Events).
1776    allow_empty: bool, if True, allows the platform property to be unset and
1777      will not prompt.
1778
1779  Raises:
1780    ArgumentError if an unknown platform type is found.
1781  """
1782  platform = platforms.GetPlatform(prompt_if_unset=not allow_empty)
1783  if platform == platforms.PLATFORM_MANAGED:
1784    VerifyManagedFlags(args, release_track, product)
1785  elif platform == platforms.PLATFORM_GKE:
1786    VerifyGKEFlags(args, release_track, product)
1787  elif platform == platforms.PLATFORM_KUBERNETES:
1788    VerifyKubernetesFlags(args, release_track, product)
1789  elif allow_empty and platform is None:
1790    # No platform is allowed for commands that only support a single platform.
1791    # It's assumed that only valid flags exist for these commands, so verifying
1792    # supported flags are set is not necessary
1793    return platform
1794  if platform not in platforms.PLATFORMS:
1795    raise serverless_exceptions.ArgumentError(
1796        'Invalid target platform specified: [{}].\n'
1797        'Available platforms:\n{}'.format(
1798            platform, '\n'.join([
1799                '- {}: {}'.format(k, v) for k, v in platforms.PLATFORMS.items()
1800            ])))
1801  return platform
1802
1803
1804# TODO(b/165145546): Remove advanced build flags for 'gcloud run deploy'
1805def AddBuildTimeoutFlag(parser):
1806  parser.add_argument(
1807      '--build-timeout',
1808      hidden=True,
1809      help='Set the maximum request execution time (timeout) to build the '
1810      'resource. It is specified as a duration; for example, "10m5s" is ten '
1811      'minutes, and five seconds. If you don\'t specify a unit, seconds is '
1812      'assumed. For example, "10" is 10 seconds.',
1813      action=actions.StoreProperty(properties.VALUES.builds.timeout))
1814
1815
1816def AddSourceFlag(parser):
1817  """Add deploy source flags, an image or a source for build."""
1818  parser.add_argument(
1819      '--source',
1820      help='The location of the source to build. The location can be a '
1821      'directory on a local disk or a gzipped archive file (.tar.gz) in '
1822      'Google Cloud Storage. If the source is a local directory, this '
1823      'command skips the files specified in the `--ignore-file`. If '
1824      '`--ignore-file` is not specified, use`.gcloudignore` file. If a '
1825      '`.gcloudignore` file is absent and a `.gitignore` file is present in '
1826      'the local source directory, gcloud will use a generated Git-compatible '
1827      '`.gcloudignore` file that respects your .gitignored files. The global '
1828      '`.gitignore` is not respected. For more information on `.gcloudignore`, '
1829      'see `gcloud topic gcloudignore`.',
1830  )
1831