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