1# -*- coding: utf-8 -*- #
2# Copyright 2017 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"""Helpers and common arguments for Composer commands."""
16
17from __future__ import absolute_import
18from __future__ import division
19from __future__ import unicode_literals
20
21import argparse
22import ipaddress
23import re
24
25from googlecloudsdk.calliope import actions
26from googlecloudsdk.calliope import arg_parsers
27from googlecloudsdk.calliope import base
28from googlecloudsdk.calliope import exceptions
29from googlecloudsdk.command_lib.composer import parsers
30from googlecloudsdk.command_lib.composer import util as command_util
31from googlecloudsdk.command_lib.util.args import labels_util
32from googlecloudsdk.core import properties
33
34import six
35
36
37AIRFLOW_VERSION_TYPE = arg_parsers.RegexpValidator(
38    r'^(\d+\.\d+(?:\.\d+)?)', 'must be in the form X.Y[.Z].')
39
40IMAGE_VERSION_TYPE = arg_parsers.RegexpValidator(
41    r'^composer-(\d+\.\d+\.\d+(?:-[a-z]+\.\d+)?|latest)-airflow-(\d+\.\d+(?:\.\d+)?)',
42    'must be in the form \'composer-A.B.C[-D.E]-airflow-X.Y[.Z]\' or '
43    '\'latest\' can be provided in place of the Cloud Composer version '
44    'string. For example: \'composer-latest-airflow-1.10.0\'.')
45
46# TODO(b/118349075): Refactor global Argument definitions to be factory methods.
47ENVIRONMENT_NAME_ARG = base.Argument(
48    'name', metavar='NAME', help='The name of an environment.')
49
50MULTI_ENVIRONMENT_NAME_ARG = base.Argument(
51    'name', metavar='NAME', nargs='+', help='The name of an environment.')
52
53MULTI_OPERATION_NAME_ARG = base.Argument(
54    'name', metavar='NAME', nargs='+', help='The name or UUID of an operation.')
55
56OPERATION_NAME_ARG = base.Argument(
57    'name', metavar='NAME', help='The name or UUID of an operation.')
58
59LOCATION_FLAG = base.Argument(
60    '--location',
61    required=False,
62    help='The Cloud Composer location (e.g., us-central1).',
63    action=actions.StoreProperty(properties.VALUES.composer.location))
64
65_ENV_VAR_NAME_ERROR = (
66    'Only upper and lowercase letters, digits, and underscores are allowed. '
67    'Environment variable names may not start with a digit.')
68
69_INVALID_IPV4_CIDR_BLOCK_ERROR = ('Invalid format of IPV4 CIDR block.')
70_INVALID_GKE_MASTER_IPV4_CIDR_BLOCK_ERROR = (
71    'Not a valid IPV4 CIDR block value for the kubernetes master')
72_INVALID_WEB_SERVER_IPV4_CIDR_BLOCK_ERROR = (
73    'Not a valid IPV4 CIDR block value for the Airflow web server')
74_INVALID_CLOUD_SQL_IPV4_CIDR_BLOCK_ERROR = (
75    'Not a valid IPV4 CIDR block value for the Cloud SQL instance')
76
77AIRFLOW_CONFIGS_FLAG_GROUP_DESCRIPTION = (
78    'Group of arguments for modifying the Airflow configuration.')
79
80CLEAR_AIRFLOW_CONFIGS_FLAG = base.Argument(
81    '--clear-airflow-configs',
82    action='store_true',
83    help="""\
84    Removes all Airflow config overrides from the environment.
85    """)
86
87UPDATE_AIRFLOW_CONFIGS_FLAG = base.Argument(
88    '--update-airflow-configs',
89    metavar='KEY=VALUE',
90    type=arg_parsers.ArgDict(key_type=str, value_type=str),
91    action=arg_parsers.UpdateAction,
92    help="""\
93    A list of Airflow config override KEY=VALUE pairs to set. If a config
94    override exists, its value is updated; otherwise, a new config override
95    is created.
96
97    KEYs should specify the configuration section and property name,
98    separated by a hyphen, for example `core-print_stats_interval`. The
99    section may not contain a closing square brace or period. The property
100    name must be non-empty and may not contain an equals sign, semicolon,
101    or period. By convention, property names are spelled with
102    `snake_case.` VALUEs may contain any character.
103    """)
104
105REMOVE_AIRFLOW_CONFIGS_FLAG = base.Argument(
106    '--remove-airflow-configs',
107    metavar='KEY',
108    type=arg_parsers.ArgList(),
109    action=arg_parsers.UpdateAction,
110    help="""\
111    A list of Airflow config override keys to remove.
112    """)
113
114ENV_VARIABLES_FLAG_GROUP_DESCRIPTION = (
115    'Group of arguments for modifying environment variables.')
116
117UPDATE_ENV_VARIABLES_FLAG = base.Argument(
118    '--update-env-variables',
119    metavar='NAME=VALUE',
120    type=arg_parsers.ArgDict(key_type=str, value_type=str),
121    action=arg_parsers.UpdateAction,
122    help="""\
123    A list of environment variable NAME=VALUE pairs to set and provide to the
124    Airflow scheduler, worker, and webserver processes. If an environment
125    variable exists, its value is updated; otherwise, a new environment
126    variable is created.
127
128    NAMEs are the environment variable names and may contain upper and
129    lowercase letters, digits, and underscores; they must not begin with a
130    digit.
131
132    User-specified environment variables should not be used to set Airflow
133    configuration properties. Instead use the `--update-airflow-configs` flag.
134    """)
135
136REMOVE_ENV_VARIABLES_FLAG = base.Argument(
137    '--remove-env-variables',
138    metavar='NAME',
139    type=arg_parsers.ArgList(),
140    action=arg_parsers.UpdateAction,
141    help="""\
142    A list of environment variables to remove.
143
144    Environment variables that have system-provided defaults cannot be unset
145    with the `--remove-env-variables` or `--clear-env-variables` flags; only
146    the user-supplied overrides will be removed.
147    """)
148
149CLEAR_ENV_VARIABLES_FLAG = base.Argument(
150    '--clear-env-variables',
151    action='store_true',
152    help="""\
153    Removes all environment variables from the environment.
154
155    Environment variables that have system-provided defaults cannot be unset
156    with the `--remove-env-variables` or `--clear-env-variables` flags; only
157    the user-supplied overrides will be removed.
158    """)
159
160ENV_UPGRADE_GROUP_DESCRIPTION = (
161    'Group of arguments for performing in-place environment upgrades.')
162
163UPDATE_AIRFLOW_VERSION_FLAG = base.Argument(
164    '--airflow-version',
165    type=AIRFLOW_VERSION_TYPE,
166    metavar='AIRFLOW_VERSION',
167    help="""\
168    Upgrade the environment to a later Airflow version in-place.
169
170    Must be of the form `X.Y[.Z]`.
171
172    The Airflow version is a semantic version. The patch version can be omitted
173    and the current version will be selected. The version numbers that are used
174    will be stored.
175    """)
176
177UPDATE_IMAGE_VERSION_FLAG = base.Argument(
178    '--image-version',
179    type=IMAGE_VERSION_TYPE,
180    metavar='IMAGE_VERSION',
181    help="""\
182    Upgrade the environment to a later version in-place.
183
184    The image version encapsulates the versions of both Cloud Composer and
185    Apache Airflow. Must be of the form `composer-A.B.C[-D.E]-airflow-X.Y[.Z]`.
186
187    The Cloud Composer and Airflow versions are semantic versions.
188    `latest` can be provided instead of an explicit Cloud Composer
189    version number indicating that the server will replace `latest`
190    with the current Cloud Composer version. For the Apache Airflow
191    portion, the patch version can be omitted and the current
192    version will be selected. The version numbers that are used will
193    be stored.
194    """)
195
196UPDATE_PYPI_FROM_FILE_FLAG = base.Argument(
197    '--update-pypi-packages-from-file',
198    help="""\
199    The path to a file containing a list of PyPI packages to install in
200    the environment. Each line in the file should contain a package
201    specification in the format of the update-pypi-package argument
202    defined above. The path can be a local file path or a Google Cloud Storage
203    file path (Cloud Storage file path starts with 'gs://').
204    """)
205
206LABELS_FLAG_GROUP_DESCRIPTION = (
207    'Group of arguments for modifying environment labels.')
208
209GENERAL_REMOVAL_FLAG_GROUP_DESCRIPTION = 'Arguments available for item removal.'
210
211PYPI_PACKAGES_FLAG_GROUP_DESCRIPTION = (
212    'Group of arguments for modifying the PyPI package configuration.')
213
214AUTOSCALING_FLAG_GROUP_DESCRIPTION = (
215    'Group of arguments for modifying GKE cluster autoscaling.')
216
217CLEAR_PYPI_PACKAGES_FLAG = base.Argument(
218    '--clear-pypi-packages',
219    action='store_true',
220    help="""\
221    Removes all PyPI packages from the environment.
222
223    PyPI packages that are required by the environment's core software
224    cannot be uninstalled with the `--remove-pypi-packages` or
225    `--clear-pypi-packages` flags.
226    """)
227
228UPDATE_PYPI_PACKAGE_FLAG = base.Argument(
229    '--update-pypi-package',
230    metavar='PACKAGE[EXTRAS_LIST]VERSION_SPECIFIER',
231    action='append',
232    default=[],
233    help="""\
234    A PyPI package to add to the environment. If a package exists, its
235    value is updated; otherwise, a new package is installed.
236
237    The value takes the form of: `PACKAGE[EXTRAS_LIST]VERSION_SPECIFIER`,
238    as one would specify in a pip requirements file.
239
240    PACKAGE is specified as a package name, such as `numpy.` EXTRAS_LIST is
241    a comma-delimited list of PEP 508 distribution extras that may be
242    empty, in which case the enclosing square brackets may be omitted.
243    VERSION_SPECIFIER is an optional PEP 440 version specifier. If both
244    EXTRAS_LIST and VERSION_SPECIFIER are omitted, the `=` and
245    everything to the right may be left empty.
246
247    This is a repeated argument that can be specified multiple times to
248    update multiple packages. If PACKAGE appears more than once, the last
249    value will be used.
250    """)
251
252REMOVE_PYPI_PACKAGES_FLAG = base.Argument(
253    '--remove-pypi-packages',
254    metavar='PACKAGE',
255    type=arg_parsers.ArgList(),
256    action=arg_parsers.UpdateAction,
257    help="""\
258    A list of PyPI package names to remove.
259
260    PyPI packages that are required by the environment's core software
261    cannot be uninstalled with the `--remove-pypi-packages` or
262    `--clear-pypi-packages` flags.
263    """)
264
265ENABLE_IP_ALIAS_FLAG = base.Argument(
266    '--enable-ip-alias',
267    default=None,
268    action='store_true',
269    help="""\
270    Enable use of alias IPs (https://cloud.google.com/compute/docs/alias-ip/)
271    for Pod IPs. This will require at least two secondary ranges in the
272    subnetwork, one for the pod IPs and another to reserve space for the
273    services range.
274    """)
275
276CLUSTER_SECONDARY_RANGE_NAME_FLAG = base.Argument(
277    '--cluster-secondary-range-name',
278    default=None,
279    help="""\
280    Secondary range to be used as the source for pod IPs. Alias ranges will be
281    allocated from this secondary range. NAME must be the name of an existing
282    secondary range in the cluster subnetwork.
283
284    Cannot be specified unless '--enable-ip-alias' is also specified.
285    """)
286
287SERVICES_SECONDARY_RANGE_NAME_FLAG = base.Argument(
288    '--services-secondary-range-name',
289    default=None,
290    help="""\
291    Secondary range to be used for services (e.g. ClusterIPs). NAME must be the
292    name of an existing secondary range in the cluster subnetwork.
293
294    Cannot be specified unless '--enable-ip-alias' is also specified.
295    """)
296
297MAX_PODS_PER_NODE = base.Argument(
298    '--max-pods-per-node',
299    type=int,
300    help="""\
301    Maximum number of pods that can be assigned to a single node, can be used to
302    limit the size of IP range assigned to the node in VPC native cluster setup.
303
304    Cannot be specified unless '--enable-ip-alias' is also specified.
305    """)
306
307WEB_SERVER_ALLOW_IP = base.Argument(
308    '--web-server-allow-ip',
309    type=arg_parsers.ArgDict(spec={
310        'ip_range': str,
311        'description': str
312    }),
313    action='append',
314    help="""\
315    Specifies a list of IPv4 or IPv6 ranges that will be allowed to access the
316    Airflow web server. By default, all IPs are allowed to access the web
317    server.
318
319    This is a repeated argument that can be specified multiple times to specify
320    multiple IP ranges.
321    (e.g. --web-server-allow-ip=ip_range=130.211.160.0/28,description="office network"
322    --web-server-allow-ip=ip_range=130.211.114.0/28,description="legacy network")
323
324    *ip_range*::: IPv4 or IPv6 range of addresses allowed to access the Airflow
325    web server.
326
327    *description*::: An optional description of the IP range.
328    """)
329
330WEB_SERVER_DENY_ALL = base.Argument(
331    '--web-server-deny-all',
332    action='store_true',
333    help="""\
334    Denies all incoming traffic to the Airflow web server.
335    """)
336
337WEB_SERVER_ALLOW_ALL = base.Argument(
338    '--web-server-allow-all',
339    action='store_true',
340    help="""\
341    Allows all IP addresses to access the Airflow web server.
342    """)
343
344UPDATE_WEB_SERVER_ALLOW_IP = base.Argument(
345    '--update-web-server-allow-ip',
346    type=arg_parsers.ArgDict(spec={
347        'ip_range': str,
348        'description': str
349    }),
350    action='append',
351    help="""\
352    Specifies a list of IPv4 or IPv6 ranges that will be allowed to access the
353    Airflow web server. By default, all IPs are allowed to access the web
354    server.
355
356    *ip_range*::: IPv4 or IPv6 range of addresses allowed to access the Airflow
357    web server.
358
359    *description*::: An optional description of the IP range.
360    """)
361
362CLOUD_SQL_MACHINE_TYPE = base.Argument(
363    '--cloud-sql-machine-type',
364    type=str,
365    help="""\
366    Cloud SQL machine type used by the Airflow database.
367    """)
368
369WEB_SERVER_MACHINE_TYPE = base.Argument(
370    '--web-server-machine-type',
371    type=str,
372    help="""\
373    machine type used by the Airflow web server. The list of available machine
374    types is available here: https://cloud.google.com/composer/pricing.
375    """)
376
377SCHEDULER_CPU = base.Argument(
378    '--scheduler-cpu',
379    hidden=True,
380    type=float,
381    default=None,
382    help="""\
383    CPU allocated to Airflow scheduler.
384    """)
385
386WORKER_CPU = base.Argument(
387    '--worker-cpu',
388    hidden=True,
389    type=float,
390    default=None,
391    help="""\
392    CPU allocated to each Airflow worker
393    """)
394
395MIN_WORKERS = base.Argument(
396    '--min-workers',
397    hidden=True,
398    type=int,
399    default=None,
400    help="""\
401    Minimum number of workers in the Environment.
402    """)
403
404MAX_WORKERS = base.Argument(
405    '--max-workers',
406    hidden=True,
407    type=int,
408    default=None,
409    help="""\
410    Maximum number of workers in the Environment.
411    """)
412
413
414def _IsValidIpv4CidrBlock(ipv4_cidr_block):
415  """Validates that IPV4 CIDR block arg has valid format.
416
417  Intended to be used as an argparse validator.
418
419  Args:
420    ipv4_cidr_block: str, the IPV4 CIDR block string to validate
421
422  Returns:
423    bool, True if and only if the IPV4 CIDR block is valid
424  """
425  return ipaddress.IPv4Network(ipv4_cidr_block) is not None
426
427
428IPV4_CIDR_BLOCK_FORMAT_VALIDATOR = arg_parsers.CustomFunctionValidator(
429    _IsValidIpv4CidrBlock, _INVALID_IPV4_CIDR_BLOCK_ERROR)
430
431CLUSTER_IPV4_CIDR_FLAG = base.Argument(
432    '--cluster-ipv4-cidr',
433    default=None,
434    type=IPV4_CIDR_BLOCK_FORMAT_VALIDATOR,
435    help="""\
436    IP address range for the pods in this cluster in CIDR notation
437    (e.g. 10.0.0.0/14).
438
439    Cannot be specified unless '--enable-ip-alias' is also specified.
440    """)
441
442SERVICES_IPV4_CIDR_FLAG = base.Argument(
443    '--services-ipv4-cidr',
444    default=None,
445    type=IPV4_CIDR_BLOCK_FORMAT_VALIDATOR,
446    help="""\
447    IP range for the services IPs.
448
449    Can be specified as a netmask size (e.g. '/20') or as in CIDR notion
450    (e.g. '10.100.0.0/20'). If given as a netmask size, the IP range will
451    be chosen automatically from the available space in the network.
452
453    If unspecified, the services CIDR range will be chosen with a default
454    mask size.
455
456    Cannot be specified unless '--enable-ip-alias' is also specified.
457    """)
458
459ENABLE_PRIVATE_ENVIRONMENT_FLAG = base.Argument(
460    '--enable-private-environment',
461    default=None,
462    action='store_true',
463    help="""\
464    Environment cluster is created with no public IP addresses on the cluster
465    nodes.
466
467    If not specified, cluster nodes will be assigned public IP addresses.
468
469    Cannot be specified unless '--enable-ip-alias' is also specified.
470    """)
471
472ENABLE_PRIVATE_ENDPOINT_FLAG = base.Argument(
473    '--enable-private-endpoint',
474    default=None,
475    action='store_true',
476    help="""\
477    Environment cluster is managed using the private IP address of the master
478    API endpoint. Therefore access to the master endpoint must be from
479    internal IP addresses.
480
481    If not specified, the master API endpoint will be accessible by its public
482    IP address.
483
484    Cannot be specified unless '--enable-private-environment' is also
485    specified.
486    """)
487
488
489def _GetIpv4CidrMaskSize(ipv4_cidr_block):
490  """Returns the size of IPV4 CIDR block mask in bits.
491
492  Args:
493    ipv4_cidr_block: str, the IPV4 CIDR block string to check.
494
495  Returns:
496    int, the size of the block mask if ipv4_cidr_block is a valid CIDR block
497    string, otherwise None.
498  """
499  network = ipaddress.IPv4Network(ipv4_cidr_block)
500  if network is None:
501    return None
502
503  return 32 - (network.num_addresses.bit_length() - 1)
504
505
506def _IsValidMasterIpv4CidrBlockWithMaskSize(ipv4_cidr_block, min_mask_size,
507                                            max_mask_size):
508  """Validates that IPV4 CIDR block arg for the cluster master is a valid value.
509
510  Args:
511    ipv4_cidr_block: str, the IPV4 CIDR block string to validate.
512    min_mask_size: int, minimum allowed netmask size for CIDR block.
513    max_mask_size: int, maximum allowed netmask size for CIDR block.
514
515  Returns:
516    bool, True if and only if the IPV4 CIDR block is valid and has the mask
517    size between min_mask_size and max_mask_size.
518  """
519  is_valid = _IsValidIpv4CidrBlock(ipv4_cidr_block)
520  if not is_valid:
521    return False
522
523  mask_size = _GetIpv4CidrMaskSize(ipv4_cidr_block)
524  return min_mask_size <= mask_size and mask_size <= max_mask_size
525
526
527_IS_VALID_MASTER_IPV4_CIDR_BLOCK = (
528    lambda cidr: _IsValidMasterIpv4CidrBlockWithMaskSize(cidr, 23, 28))
529
530MASTER_IPV4_CIDR_BLOCK_FORMAT_VALIDATOR = arg_parsers.CustomFunctionValidator(
531    _IS_VALID_MASTER_IPV4_CIDR_BLOCK, _INVALID_GKE_MASTER_IPV4_CIDR_BLOCK_ERROR)
532
533MASTER_IPV4_CIDR_FLAG = base.Argument(
534    '--master-ipv4-cidr',
535    default=None,
536    type=MASTER_IPV4_CIDR_BLOCK_FORMAT_VALIDATOR,
537    help="""\
538    IPv4 CIDR range to use for the cluste master network. This should have a
539    size of the netmask between 23 and 28.
540
541    Cannot be specified unless '--enable-private-environment' is also
542    specified.
543    """)
544
545_IS_VALID_WEB_SERVER_IPV4_CIDR_BLOCK = (
546    lambda cidr: _IsValidMasterIpv4CidrBlockWithMaskSize(cidr, 24, 29))
547
548WEB_SERVER_IPV4_CIDR_BLOCK_FORMAT_VALIDATOR = arg_parsers.CustomFunctionValidator(
549    _IS_VALID_WEB_SERVER_IPV4_CIDR_BLOCK,
550    _INVALID_WEB_SERVER_IPV4_CIDR_BLOCK_ERROR)
551
552WEB_SERVER_IPV4_CIDR_FLAG = base.Argument(
553    '--web-server-ipv4-cidr',
554    default=None,
555    type=WEB_SERVER_IPV4_CIDR_BLOCK_FORMAT_VALIDATOR,
556    help="""\
557    IPv4 CIDR range to use for the Airflow web server network. This should have
558    a size of the netmask between 24 and 29.
559
560    Cannot be specified unless '--enable-private-environment' is also
561    specified.
562    """)
563
564_IS_VALID_CLOUD_SQL_IPV4_CIDR_BLOCK = (
565    lambda cidr: _IsValidMasterIpv4CidrBlockWithMaskSize(cidr, 0, 24))
566
567CLOUD_SQL_IPV4_CIDR_BLOCK_FORMAT_VALIDATOR = arg_parsers.CustomFunctionValidator(
568    _IS_VALID_CLOUD_SQL_IPV4_CIDR_BLOCK,
569    _INVALID_CLOUD_SQL_IPV4_CIDR_BLOCK_ERROR)
570
571CLOUD_SQL_IPV4_CIDR_FLAG = base.Argument(
572    '--cloud-sql-ipv4-cidr',
573    default=None,
574    type=CLOUD_SQL_IPV4_CIDR_BLOCK_FORMAT_VALIDATOR,
575    help="""\
576    IPv4 CIDR range to use for the Cloud SQL network. This should have a size
577    of the netmask not greater than 24.
578
579    Cannot be specified unless '--enable-private-environment' is also
580    specified.
581    """)
582
583MAINTENANCE_WINDOW_START_FLAG = base.Argument(
584    '--maintenance-window-start',
585    type=arg_parsers.Datetime.Parse,
586    required=True,
587    help="""\
588    Start time of the mantenance window in the form of the full date. Only the
589    time of the day is used as a reference for a starting time of the window
590    with a provided recurrence.
591    See $ gcloud topic datetimes for information on time formats.
592    """)
593
594MAINTENANCE_WINDOW_END_FLAG = base.Argument(
595    '--maintenance-window-end',
596    type=arg_parsers.Datetime.Parse,
597    required=True,
598    help="""\
599    End time of the mantenance window in the form of the full date. Only the
600    time of the day is used as a reference for an ending time of the window
601    with a provided recurrence. Specified date must take place after the one
602    specified as a start date, the difference between will be used as a length
603    of a single maintenance window.
604    See $ gcloud topic datetimes for information on time formats.
605    """)
606
607MAINTENANCE_WINDOW_RECURRENCE_FLAG = base.Argument(
608    '--maintenance-window-recurrence',
609    type=str,
610    required=True,
611    help="""\
612    An RFC 5545 RRULE, specifying how the maintenance window will recur. The
613    minimum requirement for the length of the maintenance window is 12 hours a
614    week. Only FREQ=DAILY and FREQ=WEEKLY rules are supported.
615    """)
616
617MAINTENANCE_WINDOW_FLAG_GROUP_DESCRIPTION = (
618    'Group of arguments for setting the maintenance window value.')
619
620
621def GetAndValidateKmsEncryptionKey(args):
622  """Validates the KMS key name.
623
624  Args:
625    args: list of all the arguments
626
627  Returns:
628    string, a fully qualified KMS resource name
629
630  Raises:
631    exceptions.InvalidArgumentException: key name not fully specified
632  """
633  kms_ref = args.CONCEPTS.kms_key.Parse()
634  if kms_ref:
635    return kms_ref.RelativeName()
636  for keyword in ['kms-key', 'kms-keyring', 'kms-location', 'kms-project']:
637    if getattr(args, keyword.replace('-', '_'), None):
638      raise exceptions.InvalidArgumentException(
639          '--kms-key', 'Encryption key not fully specified.')
640
641
642def AddImportSourceFlag(parser, folder):
643  """Adds a --source flag for a storage import command to a parser.
644
645  Args:
646    parser: argparse.ArgumentParser, the parser to which to add the flag
647    folder: str, the top-level folder in the bucket into which the import
648        command will write. Should not contain any slashes. For example, 'dags'.
649  """
650  base.Argument(
651      '--source',
652      required=True,
653      help="""\
654      Path to a local directory/file or Cloud Storage bucket/object to be
655      imported into the {}/ subdirectory in the environment's Cloud Storage
656      bucket. Cloud Storage paths must begin with 'gs://'.
657      """.format(folder)).AddToParser(parser)
658
659
660def AddImportDestinationFlag(parser, folder):
661  """Adds a --destination flag for a storage import command to a parser.
662
663  Args:
664    parser: argparse.ArgumentParser, the parser to which to add the flag
665    folder: str, the top-level folder in the bucket into which the import
666        command will write. Should not contain any slashes. For example, 'dags'.
667  """
668  base.Argument(
669      '--destination',
670      metavar='DESTINATION',
671      required=False,
672      help="""\
673      An optional subdirectory under the {}/ directory in the environment's
674      Cloud Storage bucket into which to import files. May contain forward
675      slashes to delimit multiple levels of subdirectory nesting, but should not
676      contain leading or trailing slashes. If the DESTINATION does not exist, it
677      will be created.
678      """.format(folder)).AddToParser(parser)
679
680
681def AddExportSourceFlag(parser, folder):
682  """Adds a --source flag for a storage export command to a parser.
683
684  Args:
685    parser: argparse.ArgumentParser, the parser to which to add the flag
686    folder: str, the top-level folder in the bucket from which the export
687        command will read. Should not contain any slashes. For example, 'dags'.
688  """
689  base.Argument(
690      '--source',
691      help="""\
692      An optional relative path to a file or directory to be exported from the
693      {}/ subdirectory in the environment's Cloud Storage bucket.
694      """.format(folder)).AddToParser(parser)
695
696
697def AddExportDestinationFlag(parser):
698  """Adds a --destination flag for a storage export command to a parser.
699
700  Args:
701    parser: argparse.ArgumentParser, the parser to which to add the flag
702  """
703  base.Argument(
704      '--destination',
705      metavar='DESTINATION',
706      required=True,
707      help="""\
708      The path to an existing local directory or a Cloud Storage
709      bucket/directory into which to export files.
710      """).AddToParser(parser)
711
712
713def AddDeleteTargetPositional(parser, folder):
714  base.Argument(
715      'target',
716      nargs='?',
717      help="""\
718      A relative path to a file or subdirectory to delete within the
719      {folder} Cloud Storage subdirectory. If not specified, the entire contents
720      of the {folder} subdirectory will be deleted.
721      """.format(folder=folder)).AddToParser(parser)
722
723
724def _IsValidEnvVarName(name):
725  """Validates that a user-provided arg is a valid environment variable name.
726
727  Intended to be used as an argparse validator.
728
729  Args:
730    name: str, the environment variable name to validate
731
732  Returns:
733    bool, True if and only if the name is valid
734  """
735  return re.match('^[a-zA-Z_][a-zA-Z0-9_]*$', name) is not None
736
737
738ENV_VAR_NAME_FORMAT_VALIDATOR = arg_parsers.CustomFunctionValidator(
739    _IsValidEnvVarName, _ENV_VAR_NAME_ERROR)
740CREATE_ENV_VARS_FLAG = base.Argument(
741    '--env-variables',
742    metavar='NAME=VALUE',
743    type=arg_parsers.ArgDict(
744        key_type=ENV_VAR_NAME_FORMAT_VALIDATOR, value_type=str),
745    action=arg_parsers.UpdateAction,
746    help='A comma-delimited list of environment variable `NAME=VALUE` '
747    'pairs to provide to the Airflow scheduler, worker, and webserver '
748    'processes. NAME may contain upper and lowercase letters, digits, '
749    'and underscores, but they may not begin with a digit. '
750    'To include commas as part of a `VALUE`, see `{top_command} topics'
751    ' escaping` for information about overriding the delimiter.')
752
753
754def IsValidUserPort(val):
755  """Validates that a user-provided arg is a valid user port.
756
757  Intended to be used as an argparse validator.
758
759  Args:
760    val: str, a string specifying a TCP port number to be validated
761
762  Returns:
763    int, the provided port number
764
765  Raises:
766    ArgumentTypeError: if the provided port is not an integer outside the
767        system port range
768  """
769  port = int(val)
770  if 1024 <= port and port <= 65535:
771    return port
772  raise argparse.ArgumentTypeError('PORT must be in range [1024, 65535].')
773
774
775def ValidateDiskSize(parameter_name, disk_size):
776  """Validates that a disk size is a multiple of some number of GB.
777
778  Args:
779    parameter_name: parameter_name, the name of the parameter, formatted as
780        it would be in help text (e.g., '--disk-size' or 'DISK_SIZE')
781    disk_size: int, the disk size in bytes
782
783  Raises:
784    exceptions.InvalidArgumentException: the disk size was invalid
785  """
786  gb_mask = (1 << 30) - 1
787  if disk_size & gb_mask:
788    raise exceptions.InvalidArgumentException(
789        parameter_name, 'Must be an integer quantity of GB.')
790
791
792def _AddPartialDictUpdateFlagsToGroup(update_type_group,
793                                      clear_flag,
794                                      remove_flag,
795                                      update_flag,
796                                      group_help_text=None):
797  """Adds flags related to a partial update of arg represented by a dictionary.
798
799  Args:
800    update_type_group: argument group, the group to which flags should be added.
801    clear_flag: flag, the flag to clear dictionary.
802    remove_flag: flag, the flag to remove values from dictionary.
803    update_flag: flag, the flag to add or update values in dictionary.
804    group_help_text: (optional) str, the help info to apply to the created
805        argument group. If not provided, then no help text will be applied to
806        group.
807  """
808  group = update_type_group.add_argument_group(help=group_help_text)
809  remove_group = group.add_mutually_exclusive_group(
810      help=GENERAL_REMOVAL_FLAG_GROUP_DESCRIPTION)
811  clear_flag.AddToParser(remove_group)
812  remove_flag.AddToParser(remove_group)
813  update_flag.AddToParser(group)
814
815
816def AddNodeCountUpdateFlagToGroup(update_type_group):
817  """Adds flag related to setting node count.
818
819  Args:
820    update_type_group: argument group, the group to which flag should be added.
821  """
822  update_type_group.add_argument(
823      '--node-count',
824      metavar='NODE_COUNT',
825      type=arg_parsers.BoundedInt(lower_bound=3),
826      help='The new number of nodes running the environment. Must be >= 3.')
827
828
829def AddIpAliasEnvironmentFlags(update_type_group, support_max_pods_per_node):
830  """Adds flags related to IP aliasing to parser.
831
832  IP alias flags are related to similar flags found within GKE SDK:
833    /third_party/py/googlecloudsdk/command_lib/container/flags.py
834
835  Args:
836    update_type_group: argument group, the group to which flag should be added.
837    support_max_pods_per_node: bool, if specifying maximum number of pods is
838                         supported.
839  """
840  group = update_type_group.add_group(help='IP Alias (VPC-native)')
841  ENABLE_IP_ALIAS_FLAG.AddToParser(group)
842  CLUSTER_IPV4_CIDR_FLAG.AddToParser(group)
843  SERVICES_IPV4_CIDR_FLAG.AddToParser(group)
844  CLUSTER_SECONDARY_RANGE_NAME_FLAG.AddToParser(group)
845  SERVICES_SECONDARY_RANGE_NAME_FLAG.AddToParser(group)
846  if support_max_pods_per_node:
847    MAX_PODS_PER_NODE.AddToParser(group)
848
849
850def AddPrivateIpEnvironmentFlags(update_type_group):
851  """Adds flags related to private clusters to parser.
852
853  Private cluster flags are related to similar flags found within GKE SDK:
854    /third_party/py/googlecloudsdk/command_lib/container/flags.py
855
856  Args:
857    update_type_group: argument group, the group to which flag should be added.
858  """
859  group = update_type_group.add_group(help='Private Clusters')
860  ENABLE_PRIVATE_ENVIRONMENT_FLAG.AddToParser(group)
861  ENABLE_PRIVATE_ENDPOINT_FLAG.AddToParser(group)
862  MASTER_IPV4_CIDR_FLAG.AddToParser(group)
863  WEB_SERVER_IPV4_CIDR_FLAG.AddToParser(group)
864  CLOUD_SQL_IPV4_CIDR_FLAG.AddToParser(group)
865
866
867def AddPypiUpdateFlagsToGroup(update_type_group):
868  """Adds flag related to setting Pypi updates.
869
870  Args:
871    update_type_group: argument group, the group to which flag should be added.
872  """
873  group = update_type_group.add_mutually_exclusive_group(
874      PYPI_PACKAGES_FLAG_GROUP_DESCRIPTION)
875  UPDATE_PYPI_FROM_FILE_FLAG.AddToParser(group)
876  _AddPartialDictUpdateFlagsToGroup(
877      group, CLEAR_PYPI_PACKAGES_FLAG, REMOVE_PYPI_PACKAGES_FLAG,
878      UPDATE_PYPI_PACKAGE_FLAG)
879
880
881def AddEnvVariableUpdateFlagsToGroup(update_type_group):
882  """Adds flags related to updating environent variables.
883
884  Args:
885    update_type_group: argument group, the group to which flags should be added.
886  """
887  _AddPartialDictUpdateFlagsToGroup(update_type_group, CLEAR_ENV_VARIABLES_FLAG,
888                                    REMOVE_ENV_VARIABLES_FLAG,
889                                    UPDATE_ENV_VARIABLES_FLAG,
890                                    ENV_VARIABLES_FLAG_GROUP_DESCRIPTION)
891
892
893def AddAirflowConfigUpdateFlagsToGroup(update_type_group):
894  """Adds flags related to updating Airflow configurations.
895
896  Args:
897    update_type_group: argument group, the group to which flags should be added.
898  """
899  _AddPartialDictUpdateFlagsToGroup(update_type_group,
900                                    CLEAR_AIRFLOW_CONFIGS_FLAG,
901                                    REMOVE_AIRFLOW_CONFIGS_FLAG,
902                                    UPDATE_AIRFLOW_CONFIGS_FLAG,
903                                    AIRFLOW_CONFIGS_FLAG_GROUP_DESCRIPTION)
904
905
906def AddEnvUpgradeFlagsToGroup(update_type_group):
907  """Adds flag group to perform in-place env upgrades.
908
909  Args:
910    update_type_group: argument group, the group to which flags should be added.
911  """
912  upgrade_group = update_type_group.add_argument_group(
913      ENV_UPGRADE_GROUP_DESCRIPTION, mutex=True)
914  UPDATE_AIRFLOW_VERSION_FLAG.AddToParser(upgrade_group)
915  UPDATE_IMAGE_VERSION_FLAG.AddToParser(upgrade_group)
916
917
918def AddLabelsUpdateFlagsToGroup(update_type_group):
919  """Adds flags related to updating environment labels.
920
921  Args:
922    update_type_group: argument group, the group to which flags should be added.
923  """
924  labels_update_group = update_type_group.add_argument_group(
925      LABELS_FLAG_GROUP_DESCRIPTION)
926  labels_util.AddUpdateLabelsFlags(labels_update_group)
927
928
929def AddAutoscalingUpdateFlagsToGroup(update_type_group):
930  """Adds flags related to updating autoscaling.
931
932  Args:
933    update_type_group: argument group, the group to which flags should be added.
934  """
935  update_group = update_type_group.add_argument_group(
936      AUTOSCALING_FLAG_GROUP_DESCRIPTION, hidden=True)
937  SCHEDULER_CPU.AddToParser(update_group)
938  WORKER_CPU.AddToParser(update_group)
939  MIN_WORKERS.AddToParser(update_group)
940  MAX_WORKERS.AddToParser(update_group)
941
942
943def AddMaintenanceWindowFlagsGroup(update_type_group):
944  """Adds flag group for maintenance window.
945
946  Args:
947    update_type_group: argument group, the group to which flags should be added.
948  """
949  group = update_type_group.add_group(MAINTENANCE_WINDOW_FLAG_GROUP_DESCRIPTION)
950  MAINTENANCE_WINDOW_START_FLAG.AddToParser(group)
951  MAINTENANCE_WINDOW_END_FLAG.AddToParser(group)
952  MAINTENANCE_WINDOW_RECURRENCE_FLAG.AddToParser(group)
953
954
955def FallthroughToLocationProperty(location_refs, flag_name, failure_msg):
956  """Provides a list containing composer/location if `location_refs` is empty.
957
958  This intended to be used as a fallthrough for a plural Location resource arg.
959  The built-in fallthrough for plural resource args doesn't play well with
960  properties, as it will iterate over each character in the string and parse
961  it as the resource type. This function will parse the entire property and
962  return a singleton list if `location_refs` is empty.
963
964  Args:
965    location_refs: [core.resources.Resource], a possibly empty list of location
966      resource references
967    flag_name: str, if `location_refs` is empty, and the composer/location
968      property is also missing, an error message will be reported that will
969      advise the user to set this flag name
970    failure_msg: str, an error message to accompany the advisory described in
971      the docs for `flag_name`.
972
973  Returns:
974    [core.resources.Resource]: a non-empty list of location resourc references.
975    If `location_refs` was non-empty, it will be the same list, otherwise it
976    will be a singleton list containing the value of the [composer/location]
977    property.
978
979  Raises:
980    exceptions.RequiredArgumentException: both the user-provided locations
981        and property fallback were empty
982  """
983  if location_refs:
984    return location_refs
985
986  fallthrough_location = parsers.GetLocation(required=False)
987  if fallthrough_location:
988    return [parsers.ParseLocation(fallthrough_location)]
989  else:
990    raise exceptions.RequiredArgumentException(flag_name, failure_msg)
991
992
993def ValidateIpRanges(ip_ranges):
994  """Validates list of IP ranges.
995
996  Raises exception when any of the given strings is not a valid IPv4
997  or IPv6 network IP range.
998  Args:
999    ip_ranges: [string], list of IP ranges to validate
1000  """
1001  for ip_range in ip_ranges:
1002    if six.PY2:
1003      ip_range = ip_range.decode()
1004    try:
1005      ipaddress.ip_network(ip_range)
1006    except:
1007      raise command_util.InvalidUserInputError(
1008          'Invalid IP range: [{}].'.format(ip_range))
1009