feat: Add new gcloud commands, API clients, and third-party libraries across various services.

This commit is contained in:
2026-01-01 20:26:35 +01:00
parent 5e23cbece0
commit a19e592eb7
25221 changed files with 8324611 additions and 0 deletions

View File

@@ -0,0 +1,67 @@
# -*- coding: utf-8 -*- #
# Copyright 2019 Google LLC. All Rights Reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
"""utilities to define common arguments."""
from __future__ import absolute_import
from __future__ import division
from __future__ import unicode_literals
from googlecloudsdk.calliope import actions
from googlecloudsdk.calliope import base
from googlecloudsdk.command_lib.resource_manager import completers as resource_manager_completers
from googlecloudsdk.core import properties
def ProjectArgument(help_text_to_prepend=None, help_text_to_overwrite=None):
"""Creates project argument.
Args:
help_text_to_prepend: str, help text to prepend to the generic --project
help text.
help_text_to_overwrite: str, help text to overwrite the generic --project
help text.
Returns:
calliope.base.Argument, The argument for project.
"""
if help_text_to_overwrite:
help_text = help_text_to_overwrite
else:
help_text = """\
The Google Cloud project ID to use for this invocation. If
omitted, then the current project is assumed; the current project can
be listed using `gcloud config list --format='text(core.project)'`
and can be set using `gcloud config set project PROJECTID`.
`--project` and its fallback `{core_project}` property play two roles
in the invocation. It specifies the project of the resource to
operate on. It also specifies the project for API enablement check,
quota, and billing. To specify a different project for quota and
billing, use `--billing-project` or `{billing_project}` property.
""".format(
core_project=properties.VALUES.core.project,
billing_project=properties.VALUES.billing.quota_project)
if help_text_to_prepend:
help_text = '\n\n'.join((help_text_to_prepend, help_text))
return base.Argument(
'--project',
metavar='PROJECT_ID',
dest='project',
category=base.COMMONLY_USED_FLAGS,
suggestion_aliases=['--application'],
completer=resource_manager_completers.ProjectCompleter,
action=actions.StoreProperty(properties.VALUES.core.project),
help=help_text)

View File

@@ -0,0 +1,530 @@
# -*- coding: utf-8 -*- #
# Copyright 2016 Google LLC. All Rights Reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
"""Module for labels API support.
Typical usage (create command):
# When defining arguments
labels_util.AddCreateLabelsFlags(parser)
# When running the command
new_resource.labels = labels_util.ParseCreateArgs(args, labels_cls)
Create(..., new_resource)
Typical usage (update command):
# When defining arguments
labels_util.AddUpdateLabelsFlags(parser)
# When running the command
labels_diff = labels_util.Diff.FromUpdateArgs(args)
if labels_diff.MayHaveUpdates():
orig_resource = Get(...) # to prevent unnecessary Get calls
labels_update = labels_diff.Apply(labels_cls, orig_resource.labels)
if labels_update.needs_update:
new_resource.labels = labels_update.labels
field_mask.append('labels')
Update(..., new_resource)
# Or alternatively, when running the command
labels_update = labels_util.ProcessUpdateArgsLazy(
args, labels_cls, lambda: Get(...).labels)
if labels_update.needs_update:
new_resource.labels = labels_update.labels
field_mask.append('labels')
Update(..., new_resource)
"""
from __future__ import absolute_import
from __future__ import division
from __future__ import unicode_literals
from googlecloudsdk.calliope import arg_parsers
from googlecloudsdk.calliope import base
from googlecloudsdk.calliope import exceptions as calliope_exceptions
import six
def _IsLower(c):
"""Returns True if c is lower case or a caseless ideograph."""
return c.isalpha() and (c.islower() or not c.isupper())
def _IsValueOrSubsequent(c):
"""Returns True if c is a valid value or subsequent (not first) character."""
return c in ('_', '-') or c.isdigit() or _IsLower(c)
def IsValidLabelValue(value):
r"""Implements the PCRE r'[\p{Ll}\p{Lo}\p{N}_-]{0,63}'.
Only hyphens (-), underscores (_), lowercase characters, and numbers are
allowed. International characters are allowed.
Args:
value: The label value, a string.
Returns:
True is the value is valid; False if not.
"""
if value is None or len(value) > 63:
return False
return all(_IsValueOrSubsequent(c) for c in value)
def IsValidLabelKey(key):
r"""Implements the PCRE r'[\p{Ll}\p{Lo}][\p{Ll}\p{Lo}\p{N}_-]{0,62}'.
The key must start with a lowercase character and must be a valid label value.
Args:
key: The label key, a string.
Returns:
True if the key is valid; False if not.
"""
if not key or not _IsLower(key[0]):
return False
return IsValidLabelValue(key)
_KEY_FORMAT_ERROR = (
'Only hyphens (-), underscores (_), lowercase characters, and numbers are '
'allowed. Keys must start with a lowercase character. International '
'characters are allowed. Key length must not exceed 63 characters.')
KEY_FORMAT_HELP = (
'Keys must start with a lowercase character and contain only hyphens '
'(`-`), underscores (```_```), lowercase characters, and numbers.')
_VALUE_FORMAT_ERROR = (
'Only hyphens (-), underscores (_), lowercase characters, and numbers are '
'allowed. International characters are allowed.')
VALUE_FORMAT_HELP = (
'Values must contain only hyphens (`-`), underscores (```_```), lowercase '
'characters, and numbers.')
KEY_FORMAT_VALIDATOR = arg_parsers.CustomFunctionValidator(
IsValidLabelKey, _KEY_FORMAT_ERROR)
VALUE_FORMAT_VALIDATOR = arg_parsers.CustomFunctionValidator(
IsValidLabelValue, _VALUE_FORMAT_ERROR)
def GetCreateLabelsFlag(extra_message='', labels_name='labels',
validate_keys=True, validate_values=True):
"""Makes the base.Argument for --labels flag."""
key_type = KEY_FORMAT_VALIDATOR if validate_keys else None
value_type = VALUE_FORMAT_VALIDATOR if validate_values else None
format_help = []
if validate_keys:
format_help.append(KEY_FORMAT_HELP)
if validate_values:
format_help.append(VALUE_FORMAT_HELP)
help_parts = ['List of label KEY=VALUE pairs to add.']
if format_help:
help_parts.append(' '.join(format_help))
if extra_message:
help_parts.append(extra_message)
return base.Argument(
'--{}'.format(labels_name),
metavar='KEY=VALUE',
type=arg_parsers.ArgDict(
key_type=key_type, value_type=value_type),
action=arg_parsers.UpdateAction,
help=('\n\n'.join(help_parts)))
def GetClearLabelsFlag(labels_name='labels'):
return base.Argument(
'--clear-{}'.format(labels_name),
action='store_true',
help="""\
Remove all labels. If `--update-{labels}` is also specified then
`--clear-{labels}` is applied first.
For example, to remove all labels:
$ {{command}} --clear-{labels}
To remove all existing labels and create two new labels,
``foo'' and ``baz'':
$ {{command}} --clear-{labels} --update-{labels} foo=bar,baz=qux
""".format(labels=labels_name))
def GetUpdateLabelsFlag(extra_message, labels_name='labels',
validate_keys=True, validate_values=True):
"""Makes a base.Argument for the `--update-labels` flag."""
key_type = KEY_FORMAT_VALIDATOR if validate_keys else None
value_type = VALUE_FORMAT_VALIDATOR if validate_values else None
format_help = []
if validate_keys:
format_help.append(KEY_FORMAT_HELP)
if validate_values:
format_help.append(VALUE_FORMAT_HELP)
help_parts = [
('List of label KEY=VALUE pairs to update. If a label exists, its value '
'is modified. Otherwise, a new label is created.')]
if format_help:
help_parts.append(' '.join(format_help))
if extra_message:
help_parts.append(extra_message)
return base.Argument(
'--update-{}'.format(labels_name),
metavar='KEY=VALUE',
type=arg_parsers.ArgDict(
key_type=key_type, value_type=value_type),
action=arg_parsers.UpdateAction,
help='\n\n'.join(help_parts))
def GetRemoveLabelsFlag(extra_message, labels_name='labels'):
return base.Argument(
'--remove-{}'.format(labels_name),
metavar='KEY',
type=arg_parsers.ArgList(),
action=arg_parsers.UpdateAction,
help="""\
List of label keys to remove. If a label does not exist it is
silently ignored. If `--update-{labels}` is also specified then
`--update-{labels}` is applied first.""".format(labels=labels_name) +
extra_message)
def AddCreateLabelsFlags(parser):
"""Adds create command labels flags to an argparse parser.
Args:
parser: The argparse parser to add the flags to.
"""
GetCreateLabelsFlag().AddToParser(parser)
def AddUpdateLabelsFlags(
parser, extra_update_message='', extra_remove_message='',
enable_clear=True):
"""Adds update command labels flags to an argparse parser.
Args:
parser: The argparse parser to add the flags to.
extra_update_message: str, extra message to append to help text for
--update-labels flag.
extra_remove_message: str, extra message to append to help text for
--delete-labels flag.
enable_clear: bool, whether to include the --clear-labels flag.
"""
GetUpdateLabelsFlag(extra_update_message).AddToParser(parser)
if enable_clear:
remove_group = parser.add_mutually_exclusive_group()
GetClearLabelsFlag().AddToParser(remove_group)
GetRemoveLabelsFlag(extra_remove_message).AddToParser(remove_group)
else:
GetRemoveLabelsFlag(extra_remove_message).AddToParser(parser)
def GetUpdateLabelsDictFromArgs(args):
"""Returns the update labels dict from the parsed args.
Args:
args: The parsed args.
Returns:
The update labels dict from the parsed args.
"""
return args.labels if hasattr(args, 'labels') else args.update_labels
def GetRemoveLabelsListFromArgs(args):
"""Returns the remove labels list from the parsed args.
Args:
args: The parsed args.
Returns:
The remove labels list from the parsed args.
"""
return args.remove_labels
def GetAndValidateOpsFromArgs(parsed_args):
"""Validates and returns labels specific args for update.
At least one of --update-labels, --clear-labels or --remove-labels must be
provided. The --clear-labels flag *must* be a declared argument, whether it
was specified on the command line or not.
Args:
parsed_args: The parsed args.
Returns:
(update_labels, remove_labels)
update_labels contains values from --labels and --update-labels flags
respectively.
remove_labels contains values from --remove-labels flag
Raises:
RequiredArgumentException: if all labels arguments are absent.
AttributeError: if the --clear-labels flag is absent.
"""
diff = Diff.FromUpdateArgs(parsed_args)
if not diff.MayHaveUpdates():
raise calliope_exceptions.RequiredArgumentException(
'LABELS',
'At least one of --update-labels, --remove-labels, or --clear-labels '
'must be specified.')
return diff
def _PackageLabels(labels_cls, labels):
# Sorted for test stability
return labels_cls(additionalProperties=[
labels_cls.AdditionalProperty(key=key, value=value)
for key, value in sorted(six.iteritems(labels))])
def _GetExistingLabelsDict(labels):
if not labels:
return {}
return {l.key: l.value for l in labels.additionalProperties}
def ValidateAndParseLabels(
list_of_labels=None, delimiter='=', validate_keys=True, validate_values=True
):
"""Validates and returns labels in dictionary format.
Args:
list_of_labels: List of labels in format ["K1=V1", "K2=V2", ...].
delimiter: delimiters which separates key and its corresponding values.
validate_keys: if true, performs regex validation.
validate_values: if true, performs regex validation.
Returns:
None: if list_of_labels is empty.
Otheriwse: dictionary of labels {"K1": "V1", "K2": "V2", ...}.
Raises:
InvalidArgumentException: if invalid format.
"""
if not list_of_labels:
return None
dict_of_labels = {}
for label in list_of_labels:
try:
key, value = label.split(delimiter)
except ValueError:
raise calliope_exceptions.InvalidArgumentException(
'--labels', 'Invalid label format: {}'.format(label)
)
if validate_keys and not IsValidLabelKey(key):
raise calliope_exceptions.InvalidArgumentException(
'--labels',
'Invalid key format: {key}\n{_KEY_FORMAT_ERROR}'.format(
key=key, _KEY_FORMAT_ERROR=_KEY_FORMAT_ERROR
),
)
if validate_values and not IsValidLabelValue(value):
raise calliope_exceptions.InvalidArgumentException(
'--labels',
'Invalid value format: {value}\n{_VALUE_FORMAT_ERROR}'.format(
value=value, _VALUE_FORMAT_ERROR=_VALUE_FORMAT_ERROR
),
)
if key in dict_of_labels:
raise calliope_exceptions.InvalidArgumentException(
'--labels', 'Duplicate key: {}'.format(key)
)
dict_of_labels[key] = value
return dict_of_labels
class UpdateResult(object):
"""Result type for Diff application.
Attributes:
needs_update: bool, whether the diff resulted in any changes to the existing
labels proto.
_labels: LabelsValue, the new populated LabelsValue object. If needs_update
is False, this is identical to the original LabelValue object.
"""
def __init__(self, needs_update, labels):
self.needs_update = needs_update
self._labels = labels
@property
def labels(self):
"""Returns the new labels.
Raises:
ValueError: if needs_update is False.
"""
if not self.needs_update:
raise ValueError(
'If no update is needed (self.needs_update == False), '
'checking labels is unnecessary.')
return self._labels
def GetOrNone(self):
"""Returns the new labels if an update is needed or None otherwise.
NOTE: If this function returns None, make sure not to include the labels
field in the field mask of the update command. Otherwise, it's possible to
inadvertently clear the labels on the resource.
"""
try:
return self.labels
except ValueError:
return None
class Diff(object):
"""A change to the labels on a resource."""
def __init__(self, additions=None, subtractions=None, clear=False):
"""Initialize a Diff.
Only one of [subtractions, clear] may be specified.
Args:
additions: {str: str}, any label values to be updated
subtractions: List[str], any labels to be removed
clear: bool, whether to clear the labels
Returns:
Diff.
Raises:
ValueError: if both subtractions and clear are specified.
"""
self._additions = additions or {}
self._subtractions = subtractions or []
self._clear = clear
if self._subtractions and self._clear:
raise ValueError('Only one of [subtractions, clear] may be specified.')
def _RemoveLabels(self, existing_labels, new_labels):
"""Remove labels."""
del existing_labels # Unused in _RemoveLabels; needed by subclass
new_labels = new_labels.copy()
for key in self._subtractions:
new_labels.pop(key, None)
return new_labels
def _ClearLabels(self, existing_labels):
del existing_labels # Unused in _ClearLabels; needed by subclass
return {}
def _AddLabels(self, new_labels):
new_labels = new_labels.copy()
new_labels.update(self._additions)
return new_labels
def Apply(self, labels_cls, labels=None):
"""Apply this Diff to the (possibly non-existing) labels.
First, makes any additions. Then, removes any labels.
Args:
labels_cls: type, the LabelsValue class for the resource.
labels: LabelsValue, the existing LabelsValue object for the original
resource (or None, which is treated the same as empty labels)
Returns:
labels_cls, the instantiated LabelsValue message with the new set up
labels, or None if there are no changes.
"""
# Add pre-existing labels.
existing_labels = _GetExistingLabelsDict(labels)
new_labels = existing_labels.copy()
if self._clear:
new_labels = self._ClearLabels(existing_labels)
if self._additions:
new_labels = self._AddLabels(new_labels)
if self._subtractions:
new_labels = self._RemoveLabels(existing_labels, new_labels)
needs_update = new_labels != existing_labels
return UpdateResult(needs_update, _PackageLabels(labels_cls, new_labels))
def MayHaveUpdates(self):
"""Returns true if this Diff is non-empty."""
return any([self._additions, self._subtractions, self._clear])
@classmethod
def FromUpdateArgs(cls, args, enable_clear=True):
"""Initializes a Diff based on the arguments in AddUpdateLabelsFlags."""
if enable_clear:
clear = args.clear_labels
else:
clear = None
return cls(args.update_labels, args.remove_labels, clear)
def ProcessUpdateArgsLazy(args, labels_cls, orig_labels_thunk):
"""Returns the result of applying the diff constructed from args.
Lazily fetches the original labels value if needed.
Args:
args: argparse.Namespace, the parsed arguments with update_labels,
remove_labels, and clear_labels
labels_cls: type, the LabelsValue class for the new labels.
orig_labels_thunk: callable, a thunk which will return the original labels
object (of type LabelsValue) when evaluated.
Returns:
UpdateResult: the result of applying the diff.
"""
diff = Diff.FromUpdateArgs(args)
orig_labels = orig_labels_thunk() if diff.MayHaveUpdates() else None
return diff.Apply(labels_cls, orig_labels)
def ParseCreateArgs(args, labels_cls, labels_dest='labels'):
"""Initializes labels based on args and the given class."""
labels = getattr(args, labels_dest)
if labels is None:
return None
return _PackageLabels(labels_cls, labels)
class ExplicitNullificationDiff(Diff):
"""A change to labels for resources where API requires explicit nullification.
That is, to clear a label {'foo': 'bar'}, you must pass {'foo': None} to the
API.
"""
def _RemoveLabels(self, existing_labels, new_labels):
"""Remove labels."""
new_labels = new_labels.copy()
for key in self._subtractions:
if key in existing_labels:
new_labels[key] = None
elif key in new_labels:
del new_labels[key]
return new_labels
def _ClearLabels(self, existing_labels):
return {key: None for key in existing_labels}

View File

@@ -0,0 +1,350 @@
# -*- coding: utf-8 -*- #
# Copyright 2018 Google LLC. All Rights Reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
"""Utilities for updating primitive dict args."""
from __future__ import absolute_import
from __future__ import division
from __future__ import unicode_literals
import os
import dotenv
from googlecloudsdk.calliope import arg_parsers
from googlecloudsdk.calliope import base
from googlecloudsdk.core import yaml
def MapUpdateFlag(
flag_name,
long_name,
key_type,
value_type,
key_metavar='KEY',
value_metavar='VALUE',
):
return base.Argument(
'--update-{}'.format(flag_name),
metavar='{}={}'.format(key_metavar, value_metavar),
action=arg_parsers.UpdateAction,
type=arg_parsers.ArgDict(key_type=key_type, value_type=value_type),
help='List of key-value pairs to set as {}.'.format(long_name),
)
def AddMapUpdateFlag(
group,
flag_name,
long_name,
key_type,
value_type,
key_metavar='KEY',
value_metavar='VALUE',
):
return MapUpdateFlag(
flag_name,
long_name,
key_type,
value_type,
key_metavar=key_metavar,
value_metavar=value_metavar,
).AddToParser(group)
def MapRemoveFlag(flag_name, long_name, key_type, key_metavar='KEY'):
return base.Argument(
'--remove-{}'.format(flag_name),
metavar=key_metavar,
action=arg_parsers.UpdateAction,
type=arg_parsers.ArgList(element_type=key_type),
help='List of {} to be removed.'.format(long_name),
)
def AddMapRemoveFlag(group, flag_name, long_name, key_type, key_metavar='KEY'):
return MapRemoveFlag(
flag_name, long_name, key_type, key_metavar=key_metavar
).AddToParser(group)
def MapClearFlag(flag_name, long_name):
return base.Argument(
'--clear-{}'.format(flag_name),
action='store_true',
help='Remove all {}.'.format(long_name),
)
def AddMapClearFlag(group, flag_name, long_name):
return MapClearFlag(flag_name, long_name).AddToParser(group)
def MapSetFlag(
flag_name,
long_name,
key_type,
value_type,
key_metavar='KEY',
value_metavar='VALUE',
):
return base.Argument(
'--set-{}'.format(flag_name),
metavar='{}={}'.format(key_metavar, value_metavar),
action=arg_parsers.UpdateAction,
type=arg_parsers.ArgDict(key_type=key_type, value_type=value_type),
help=(
'List of key-value pairs to set as {0}. All existing {0} will be '
'removed first.'
).format(long_name),
)
def AddMapSetFlag(
group,
flag_name,
long_name,
key_type,
value_type,
key_metavar='KEY',
value_metavar='VALUE',
):
return MapSetFlag(
flag_name,
long_name,
key_type,
value_type,
key_metavar=key_metavar,
value_metavar=value_metavar,
).AddToParser(group)
class ArgDictWithYAMLOrEnv(object):
"""Interpret a YAML or .env file as a dict."""
def __init__(self, key_type=None, value_type=None):
"""Initialize an ArgDictFile.
Args:
key_type: (str)->str, A function to apply to each of the dict keys.
value_type: (str)->str, A function to apply to each of the dict values.
"""
self.key_type = key_type
self.value_type = value_type
def __call__(self, file_path):
"""Interpret a YAML or .env file as a dict.Try to parse the file as a .env file first, if it fails, try to parse it as a YAML file.
Args:
file_path: The path to the file to parse.
Returns:
A dict with the parsed values.
"""
map_dict = {}
if not os.path.exists(file_path):
raise arg_parsers.ArgumentTypeError(
'File [{}] does not exist.'.format(file_path)
)
if file_path.endswith('.env'):
map_file_dict = dotenv.dotenv_values(dotenv_path=file_path)
if not map_file_dict:
raise arg_parsers.ArgumentTypeError(
'Invalid .env file [{}], expected map-like data.'.format(file_path)
)
else:
map_file_dict = yaml.load_path(file_path)
if not yaml.dict_like(map_file_dict):
raise arg_parsers.ArgumentTypeError(
'Invalid YAML/JSON data in [{}], expected map-like data.'.format(
file_path
)
)
for key, value in map_file_dict.items():
if self.key_type:
try:
key = self.key_type(key)
except ValueError:
raise arg_parsers.ArgumentTypeError('Invalid key [{0}]'.format(key))
if self.value_type:
try:
value = self.value_type(value)
except ValueError:
raise arg_parsers.ArgumentTypeError(
'Invalid value [{0}]'.format(value)
)
map_dict[key] = value
return map_dict
class ArgDictFile(object):
"""Interpret a YAML file as a dict."""
def __init__(self, key_type=None, value_type=None):
"""Initialize an ArgDictFile.
Args:
key_type: (str)->str, A function to apply to each of the dict keys.
value_type: (str)->str, A function to apply to each of the dict values.
"""
self.key_type = key_type
self.value_type = value_type
def __call__(self, file_path):
map_file_dict = yaml.load_path(file_path)
map_dict = {}
if not yaml.dict_like(map_file_dict):
raise arg_parsers.ArgumentTypeError(
'Invalid YAML/JSON data in [{}], expected map-like data.'.format(
file_path
)
)
for key, value in map_file_dict.items():
if self.key_type:
try:
key = self.key_type(key)
except ValueError:
raise arg_parsers.ArgumentTypeError('Invalid key [{0}]'.format(key))
if self.value_type:
try:
value = self.value_type(value)
except ValueError:
raise arg_parsers.ArgumentTypeError(
'Invalid value [{0}]'.format(value)
)
map_dict[key] = value
return map_dict
def AddMapSetFileFlagWithYAMLOrEnv(
group, flag_name, long_name, key_type, value_type
):
group.add_argument(
'--{}-file'.format(flag_name),
metavar='FILE_PATH',
type=ArgDictWithYAMLOrEnv(key_type=key_type, value_type=value_type),
help=(
'Path to a local YAML file with definitions for all {0}. All '
'existing {0} will be removed before the new {0} are added.'
).format(long_name),
)
def AddMapSetFileFlag(group, flag_name, long_name, key_type, value_type):
group.add_argument(
'--{}-file'.format(flag_name),
metavar='FILE_PATH',
type=ArgDictFile(key_type=key_type, value_type=value_type),
help=(
'Path to a local YAML file with definitions for all {0}. All '
'existing {0} will be removed before the new {0} are added.'
).format(long_name),
)
def AddUpdateMapFlags(
parser, flag_name, long_name=None, key_type=None, value_type=None
):
"""Add flags for updating values of a map-of-atomic-values property.
Args:
parser: The argument parser
flag_name: The name for the property to be used in flag names
long_name: The name for the property to be used in help text
key_type: A function to apply to map keys.
value_type: A function to apply to map values.
"""
if not long_name:
long_name = flag_name
group = parser.add_mutually_exclusive_group()
update_remove_group = group.add_argument_group(
help=(
'Only --update-{0} and --remove-{0} can be used together. If both '
'are specified, --remove-{0} will be applied first.'
).format(flag_name)
)
AddMapUpdateFlag(
update_remove_group,
flag_name,
long_name,
key_type=key_type,
value_type=value_type,
)
AddMapRemoveFlag(update_remove_group, flag_name, long_name, key_type=key_type)
AddMapClearFlag(group, flag_name, long_name)
AddMapSetFlag(
group, flag_name, long_name, key_type=key_type, value_type=value_type
)
AddMapSetFileFlag(
group, flag_name, long_name, key_type=key_type, value_type=value_type
)
def GetMapFlagsFromArgs(flag_name, args):
"""Get the flags for updating this map and return their values in a dict.
Args:
flag_name: The base name of the flags
args: The argparse namespace
Returns:
A dict of the flag values
"""
specified_args = args.GetSpecifiedArgs()
return {
'set_flag_value': specified_args.get('--set-{}'.format(flag_name)),
'update_flag_value': specified_args.get('--update-{}'.format(flag_name)),
'clear_flag_value': specified_args.get('--clear-{}'.format(flag_name)),
'remove_flag_value': specified_args.get('--remove-{}'.format(flag_name)),
'file_flag_value': specified_args.get('--{}-file'.format(flag_name)),
}
def ApplyMapFlags(
old_map,
set_flag_value,
update_flag_value,
clear_flag_value,
remove_flag_value,
file_flag_value,
):
"""Determine the new map property from an existing map and parsed arguments.
Args:
old_map: the existing map
set_flag_value: The value from the --set-* flag
update_flag_value: The value from the --update-* flag
clear_flag_value: The value from the --clear-* flag
remove_flag_value: The value from the --remove-* flag
file_flag_value: The value from the --*-file flag
Returns:
A new map with the changes applied.
"""
if clear_flag_value:
return {}
if set_flag_value:
return set_flag_value
if file_flag_value:
return file_flag_value
if update_flag_value or remove_flag_value:
old_map = old_map or {}
remove_flag_value = remove_flag_value or []
new_map = {k: v for k, v in old_map.items() if k not in remove_flag_value}
new_map.update(update_flag_value or {})
return new_map
return old_map

View File

@@ -0,0 +1,326 @@
# -*- coding: utf-8 -*- #
# Copyright 2018 Google LLC. All Rights Reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
"""Utilities for updating primitive repeated args.
This code:
from googlecloudsdk.command_lib.util import repeated
class UpdateFoo(base.UpdateCommand)
@staticmethod
def Args(parser):
# add "foo" resource arg
repeated.AddPrimitiveArgs(
parser, 'foo', 'baz-bars', 'baz bars',
additional_help='The baz bars allow you to do a thing.')
def Run(self, args):
client = foos_api.Client()
foo_ref = args.CONCEPTS.foo.Parse()
foo_result = repeated.CachedResult.FromFunc(client.Get, foo_ref)
new_baz_bars = repeated.ParsePrimitiveArgs(
args, 'baz_bars', foo_result.GetAttrThunk('bazBars'))
if new_baz_bars is not None:
pass # code to update the baz_bars
Makes a command that works like so:
$ cli-tool foos update --set-baz-bars qux,quux
[...]
$ cli-tool foos update --help
[...]
These flags modify the member baz bars of this foo. The baz bars allow you
to do a thing. At most one of these can be specified:
--add-baz-bars=[BAZ_BAR,...]
Append the given values to the current baz bars.
--clear-baz-bars
Empty the current baz bars.
--remove-baz-bars=[BAZ_BAR,...]
Remove the given values from the current baz bars.
--set-baz-bars=[BAZ_BAR,...]
Completely replace the current access levels with the given values.
[...]
"""
from __future__ import absolute_import
from __future__ import division
from __future__ import print_function
from __future__ import unicode_literals
import functools
from googlecloudsdk.calliope import arg_parsers
from googlecloudsdk.calliope import base
from six.moves import map # pylint: disable=redefined-builtin
class CachedResult(object):
"""Memoizer for a function call."""
def __init__(self, thunk):
self.thunk = thunk
self._result = None
@classmethod
def FromFunc(cls, func, *args, **kwargs):
return cls(functools.partial(func, *args, **kwargs))
def Get(self):
"""Get the result of the function call (cached)."""
if self._result is None:
self._result = self.thunk()
return self._result
def GetAttrThunk(self, attr, transform=None):
"""Returns a thunk that gets the given attribute of the result of Get().
Examples:
>>> class A(object):
... b = [1, 2, 3]
>>> CachedResult([A()].pop).GetAttrThunk('b')()
[1, 2, 3]
>>> CachedResult([A()].pop).GetAttrThunk('b', lambda x: x+1)
[2, 3, 4]
Args:
attr: str, the name of the attribute. Attribute should be iterable.
transform: func, one-arg function that, if given, will be applied to
every member of the attribute (which must be iterable) before returning
it.
Returns:
zero-arg function which, when called, returns the attribute (possibly
transformed) of the result (which is cached).
"""
if transform:
return lambda: list(map(transform, getattr(self.Get(), attr)))
else:
return lambda: getattr(self.Get(), attr)
def ParseResourceNameArgs(args, arg_name, current_value_thunk, resource_parser):
"""Parse the modification to the given repeated resource name field.
To be used in combination with AddPrimitiveArgs. This variant assumes the
repeated field contains resource names and will use the given resource_parser
to convert the arguments to relative names.
Args:
args: argparse.Namespace of parsed arguments
arg_name: string, the (plural) suffix of the argument (snake_case).
current_value_thunk: zero-arg function that returns the current value of the
attribute to be updated. Will be called lazily if required.
resource_parser: one-arg function that returns a resource reference that
corresponds to the resource name list to be updated.
Raises:
ValueError: if more than one arg is set.
Returns:
List of str: the new value for the field, or None if no change is required.
"""
underscored_name = arg_name.replace('-', '_')
remove = _ConvertValuesToRelativeNames(
getattr(args, 'remove_' + underscored_name), resource_parser)
add = _ConvertValuesToRelativeNames(
getattr(args, 'add_' + underscored_name), resource_parser)
clear = getattr(args, 'clear_' + underscored_name)
# 'set' is allowed to be None, as it is deprecated.
set_ = _ConvertValuesToRelativeNames(
getattr(args, 'set_' + underscored_name, None), resource_parser)
return _ModifyCurrentValue(remove, add, clear, set_, current_value_thunk)
def _ConvertValuesToRelativeNames(names, resource_parser):
if names:
names = [resource_parser(name).RelativeName() for name in names]
return names
def ParsePrimitiveArgs(args, arg_name, current_value_thunk):
"""Parse the modification to the given repeated field.
To be used in combination with AddPrimitiveArgs; see module docstring.
Args:
args: argparse.Namespace of parsed arguments
arg_name: string, the (plural) suffix of the argument (snake_case).
current_value_thunk: zero-arg function that returns the current value of the
attribute to be updated. Will be called lazily if required.
Raises:
ValueError: if more than one arg is set.
Returns:
List of str: the new value for the field, or None if no change is required.
"""
underscored_name = arg_name.replace('-', '_')
remove = getattr(args, 'remove_' + underscored_name)
add = getattr(args, 'add_' + underscored_name)
clear = getattr(args, 'clear_' + underscored_name)
set_ = getattr(args, 'set_' + underscored_name, None)
return _ModifyCurrentValue(remove, add, clear, set_, current_value_thunk)
def _ModifyCurrentValue(remove, add, clear, set_, current_value_thunk):
"""Performs the modification of the current value based on the args.
Args:
remove: list[str], items to be removed from the current value.
add: list[str], items to be added to the current value.
clear: bool, whether or not to clear the current value.
set_: list[str], items to replace the current value.
current_value_thunk: zero-arg function that returns the current value of the
attribute to be updated. Will be called lazily if required.
Raises:
ValueError: if more than one arg is set.
Returns:
List of str: the new value for the field, or None if no change is required.
"""
if sum(map(bool, (remove, add, clear, set_))) > 1:
raise ValueError('At most one arg can be set.')
if remove is not None:
current_value = current_value_thunk()
new_value = [x for x in current_value if x not in remove]
elif add is not None:
current_value = current_value_thunk()
new_value = current_value + [x for x in add if x not in current_value]
elif clear:
return []
elif set_ is not None:
return set_
else:
return None
if new_value != current_value:
return new_value
else:
return None
def AddPrimitiveArgs(parser,
resource_name,
arg_name,
property_name,
additional_help='',
metavar=None,
is_dict_args=False,
auto_group_help=True,
include_set=True):
"""Add arguments for updating a field to the given parser.
Adds `--{add,remove,set,clear-<resource>` arguments.
Args:
parser: calliope.parser_extensions.ArgumentInterceptor, the parser to add
arguments to.
resource_name: str, the (singular) name of the resource being modified (in
whatever format you'd like it to appear in help text).
arg_name: str, the (plural) argument suffix to use (hyphen-case).
property_name: str, the description of the property being modified (plural;
in whatever format you'd like it to appear in help text)
additional_help: str, additional help text describing the property.
metavar: str, the name of the metavar to use (if different from
arg_name.upper()).
is_dict_args: boolean, True when the primitive args are dict args.
auto_group_help: bool, True to generate a summary help.
include_set: bool, True to include the (deprecated) set argument.
"""
properties_name = property_name
if auto_group_help:
group_help = 'These flags modify the member {} of this {}.'.format(
properties_name, resource_name)
if additional_help:
group_help += ' ' + additional_help
else:
group_help = additional_help
group = parser.add_mutually_exclusive_group(group_help)
metavar = metavar or arg_name.upper()
args = [
_GetAppendArg(arg_name, metavar, properties_name, is_dict_args),
_GetRemoveArg(arg_name, metavar, properties_name, is_dict_args),
_GetClearArg(arg_name, properties_name),
]
if include_set:
args.append(_GetSetArg(arg_name, metavar, properties_name, is_dict_args))
for arg in args:
arg.AddToParser(group)
def _GetAppendArg(arg_name, metavar, prop_name, is_dict_args):
list_name = '--add-{}'.format(arg_name)
list_help = 'Append the given values to the current {}.'.format(prop_name)
dict_name = '--update-{}'.format(arg_name)
dict_help = 'Update the given key-value pairs in the current {}.'.format(
prop_name)
return base.Argument(
dict_name if is_dict_args else list_name,
type=_GetArgType(is_dict_args),
metavar=metavar,
help=_GetArgHelp(dict_help, list_help, is_dict_args))
def _GetRemoveArg(arg_name, metavar, prop_name, is_dict_args):
list_help = 'Remove the given values from the current {}.'.format(prop_name)
dict_help = ('Remove the key-value pairs from the current {} with the given '
'keys.').format(prop_name)
return base.Argument(
'--remove-{}'.format(arg_name),
metavar=metavar,
type=_GetArgType(is_dict_args),
help=_GetArgHelp(dict_help, list_help, is_dict_args))
def _GetSetArg(arg_name, metavar, prop_name, is_dict_args):
list_help = 'Completely replace the current {} with the given values.'.format(
prop_name)
dict_help = ('Completely replace the current {} with the given key-value '
'pairs.').format(prop_name)
return base.Argument(
'--set-{}'.format(arg_name),
type=_GetArgType(is_dict_args),
metavar=metavar,
help=_GetArgHelp(dict_help, list_help, is_dict_args))
def _GetClearArg(arg_name, prop_name):
return base.Argument(
'--clear-{}'.format(arg_name),
action='store_true',
help='Empty the current {}.'.format(prop_name))
def _GetArgType(is_dict_args):
return arg_parsers.ArgDict() if is_dict_args else arg_parsers.ArgList()
def _GetArgHelp(dict_help, list_help, is_dict_args):
return dict_help if is_dict_args else list_help

View File

@@ -0,0 +1,94 @@
# -*- coding: utf-8 -*- #
# Copyright 2016 Google LLC. All Rights Reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
"""Module for resource_args API support."""
from __future__ import absolute_import
from __future__ import division
from __future__ import unicode_literals
from googlecloudsdk.calliope.concepts import concepts
from googlecloudsdk.command_lib.util.apis import yaml_data
from googlecloudsdk.command_lib.util.concepts import concept_parsers
from googlecloudsdk.command_lib.util.concepts import presentation_specs
import six
def AddResourceArgToParser(parser,
resource_path,
help_text,
name=None,
required=True):
"""Adds a resource argument in a python command.
Args:
parser: the parser for the command.
resource_path: string, the resource_path which refers to the resources.yaml.
help_text: string, the help text of the resource argument.
name: string, the default is the name specified in the resources.yaml file.
required: boolean, the default is True because in most cases resource arg is
required.
"""
resource_yaml_data = yaml_data.ResourceYAMLData.FromPath(resource_path)
resource_spec = concepts.ResourceSpec.FromYaml(resource_yaml_data.GetData())
concept_parsers.ConceptParser.ForResource(
name=(name if name else resource_yaml_data.GetArgName()),
resource_spec=resource_spec,
group_help=help_text,
required=required).AddToParser(parser)
def GetResourcePresentationSpec(name, verb, resource_data,
attribute_overrides=None,
help_text='The {name} {verb}.',
required=False, prefixes=True,
positional=False):
"""Build ResourcePresentationSpec for a Resource.
Args:
name: string, name of resource anchor argument.
verb: string, the verb to describe the resource, such as 'to create'.
resource_data: dict, the parsed data from a resources.yaml file under
command_lib/.
attribute_overrides: dict{string:string}, map of resource attribute names to
override in the generated resrouce spec.
help_text: string, the help text for the entire resource arg group. Should
have 2 format format specifiers (`{name}`, `{verb}`) to insert the
name and verb repectively.
required: bool, whether or not this resource arg is required.
prefixes: bool, if True the resource name will be used as a prefix for
the flags in the resource group.
positional: bool, if True, means that the resource arg is a positional
rather than a flag.
Returns:
ResourcePresentationSpec, presentation spec for resource.
"""
arg_name = name if positional else '--' + name
arg_help = help_text.format(verb=verb, name=name)
if attribute_overrides:
for attribute_name, value in six.iteritems(attribute_overrides):
for attr in resource_data['attributes']:
if attr['attribute_name'] == attribute_name:
attr['attribute_name'] = value
return presentation_specs.ResourcePresentationSpec(
arg_name,
concepts.ResourceSpec.FromYaml(resource_data),
arg_help,
required=required,
prefixes=prefixes
)

View File

@@ -0,0 +1,71 @@
# -*- coding: utf-8 -*- #
# Copyright 2018 Google LLC. All Rights Reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
"""Utilities for creating/parsing arguments for update commands."""
from __future__ import absolute_import
from __future__ import division
from __future__ import unicode_literals
from googlecloudsdk.calliope import base
class UpdateResult(object):
"""Result type for applying updates.
Attributes:
needs_update: bool, whether the args require any changes to the existing
resource.
value: the value to put
"""
def __init__(self, needs_update, value):
self.needs_update = needs_update
self.value = value
@classmethod
def MakeWithUpdate(cls, value):
return cls(True, value)
@classmethod
def MakeNoUpdate(cls):
return cls(False, None)
def AddClearableField(parser, arg_name, property_name, resource, full_help):
"""Add arguments corresponding to a field that can be cleared."""
args = [
base.Argument(
'--{}'.format(arg_name),
help='Set the {} for the {}.'.format(property_name, resource)),
base.Argument(
'--clear-{}'.format(arg_name),
help='Clear the {} from the {}.'.format(property_name, resource),
action='store_true')
]
group = parser.add_mutually_exclusive_group(help=full_help)
for arg in args:
arg.AddToParser(group)
def ParseClearableField(args, arg_name):
clear = getattr(args, 'clear_' + arg_name)
set_ = getattr(args, arg_name, None)
if clear:
return UpdateResult.MakeWithUpdate(None)
elif set_:
return UpdateResult.MakeWithUpdate(set_)
else:
return UpdateResult.MakeNoUpdate()