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,760 @@
# -*- coding: utf-8 -*- #
# Copyright 2017 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.
"""Classes that generate and parse arguments for apitools messages."""
from __future__ import absolute_import
from __future__ import division
from __future__ import unicode_literals
import collections
from apitools.base.protorpclite import messages
from googlecloudsdk.calliope import base
from googlecloudsdk.calliope.concepts import util as resource_util
from googlecloudsdk.command_lib.util.apis import arg_utils
from googlecloudsdk.command_lib.util.apis import update
from googlecloudsdk.command_lib.util.apis import yaml_arg_schema
from googlecloudsdk.command_lib.util.apis import yaml_command_schema
from googlecloudsdk.command_lib.util.apis import yaml_command_schema_util as util
from googlecloudsdk.command_lib.util.args import labels_util
from googlecloudsdk.core import resources
from googlecloudsdk.core.resource import resource_property
class Error(Exception):
"""Base class for this module's exceptions."""
class ConflictingResourcesError(Error):
"""Error for whenever api method / primary resource cannot be determined."""
def _GetLabelsClass(message, api_field):
return arg_utils.GetFieldFromMessage(message, api_field).type
def _ParseLabelsIntoCreateMessage(message, args, api_field):
labels_cls = _GetLabelsClass(message, api_field)
labels_field = labels_util.ParseCreateArgs(args, labels_cls)
arg_utils.SetFieldInMessage(message, api_field, labels_field)
def _AddLabelsToUpdateMask(static_field, update_mask_path):
if (update_mask_path not in static_field) or (
not static_field[update_mask_path]):
static_field[update_mask_path] = 'labels'
return
if 'labels' in static_field[update_mask_path].split(','):
return
static_field[
update_mask_path] = static_field[update_mask_path] + ',' + 'labels'
def _RetrieveFieldValueFromMessage(message, api_field):
path = api_field.split('.')
for field_name in path:
try:
message = getattr(message, field_name)
except AttributeError:
raise AttributeError(
'The message does not have field specified in {}.'.format(api_field))
return message
def _ParseLabelsIntoUpdateMessage(message, args, api_field):
"""Find diff between existing labels and args, set labels into the message."""
diff = labels_util.Diff.FromUpdateArgs(args)
# Do nothing if 'labels' arguments weren't specified.
if not diff.MayHaveUpdates():
return False
existing_labels = _RetrieveFieldValueFromMessage(message, api_field)
label_cls = _GetLabelsClass(message, api_field)
update_result = diff.Apply(label_cls, existing_labels)
if update_result.needs_update:
arg_utils.SetFieldInMessage(message, api_field, update_result.labels)
return True
def _GetResources(params):
"""Retrieves all resource args from the arg_info tree.
Args:
params: an ArgGroup or list of args to parse through.
Returns:
YAMLConceptArgument (resource arg) list.
"""
if isinstance(params, yaml_arg_schema.YAMLConceptArgument):
return [params]
if isinstance(params, yaml_arg_schema.Argument):
return []
if isinstance(params, yaml_arg_schema.ArgumentGroup):
params = params.arguments
result = []
for param in params:
result.extend(_GetResources(param))
return result
def _GetPrimaryResource(resource_params, resource_collection):
"""Retrieves the primary resource arg.
Args:
resource_params: list of YAMLConceptParser
resource_collection: registry.APICollection, resource collection
associated with method
Returns:
YAMLConceptArgument (resource arg) or None.
"""
# No resource params occurs if resource args are added through a hook.
if not resource_params:
return None
primary_resources = [
arg for arg in resource_params
if arg.IsPrimaryResource(resource_collection)]
if not primary_resources:
if resource_collection:
full_name = resource_collection.full_name
api_version = resource_collection.api_version
else:
full_name = None
api_version = None
raise util.InvalidSchemaError(
'No resource args were found that correspond with [{name} {version}]. '
'Add resource arguments that corresponds with request.method '
'collection [{name} {version}]. HINT: Can set resource arg '
'is_primary_resource to True in yaml schema to receive more assistance '
'with validation.'.format(
name=full_name, version=api_version))
if len(primary_resources) > 1:
primary_resource_names = [arg.name for arg in primary_resources]
raise util.InvalidSchemaError(
'Only one resource arg can be listed as primary. Remove one of the '
'primary resource args [{}] or set is_primary_resource to False in '
'yaml schema.'.format(', '.join(primary_resource_names)))
return primary_resources[0]
def _GetMethodResourceArgs(resource_args, methods):
"""Gets list of primary resource args and methods associated with them.
Args:
resource_args: list[YAMLConceptArg], list of potential primary resource
args
methods: list[registry.APIMethod], The method to generate arguments for.
Returns:
list[YAMLMethod] (resource arg) or None.
"""
args = resource_args
# Handle methodless commands with primary resource arg
if not methods and (primary := _GetPrimaryResource(args, None)):
return [MethodResourceArg(primary_resource=primary, method=None)]
yaml_methods = []
for method in methods:
resource_arg = _GetPrimaryResource(
args, method.resource_argument_collection)
yaml_methods.append(MethodResourceArg(resource_arg, method))
return yaml_methods
def _NormalizeNames(attributes):
return [resource_util.NormalizeFormat(attr) for attr in attributes]
def _DoesDupResourceArgHaveSameAttributes(resource, resource_params):
"""Verify if there is a duplicated resource argument with the same attributes.
Args:
resource: yaml_arg_schema.Argument, resource to be verified.
resource_params: [yaml_arg_schema.Argument], list to check duplicate.
Returns:
True if there is a duplicate resource arg in the list with same attributes.
"""
for res_arg in resource_params:
if res_arg != resource and res_arg.name == resource.name:
# Normalize the attribute names to account for positional
# and non-positional.
return(_NormalizeNames(res_arg.attribute_names) ==
_NormalizeNames(resource.attribute_names))
return True
def _GetSharedFlags(resource_params):
"""Retrieves shared attributes between resource args.
Args:
resource_params: [yaml_arg_schema.Argument], yaml argument tree
Returns:
Map of attribute names to list of resources that contain that attribute.
"""
resource_names = set()
flags = collections.defaultdict(list)
for arg in resource_params:
# Presentation name is used to register the resource arg ie
# arg.CONCEPTS.presentation_name.Parse() retrieves and parses CLI input.
arg_name = arg.presentation_name
if arg_name in resource_names:
# If we found a duplicate resource arg, make sure it has same attributes.
# If it has different attributes, current resource arg will override
# previous one.
if (
arg_name in resource_names
and not _DoesDupResourceArgHaveSameAttributes(arg, resource_params)
):
raise util.InvalidSchemaError(
'More than one resource argument has the name [{}] with different '
'attributes. Remove the duplicate resource declarations.'.format(
arg_name
)
)
else:
resource_names.add(arg_name)
# iterate thorugh attributes flags
for flag_name in arg.attribute_to_flag_map.values():
if flag_name not in arg.ignored_flags:
flags[flag_name].append(arg_name)
# Shared attributes: attribute entries with more than 1 resource args.
return {
flag_name: resource_args
for flag_name, resource_args in flags.items()
if len(resource_args) > 1
}
def _GetCollectionName(method, is_parent=False):
collection_name = method.resource_argument_collection.full_name
if is_parent:
resource_collection, _, _ = collection_name.rpartition('.')
else:
resource_collection = collection_name
return resource_collection
class MethodResourceArg:
"""Method and the resource argument associated with it."""
def __init__(self, primary_resource, method):
self.primary_resource = primary_resource
self.method = method
def Parse(self, namespace):
if self.primary_resource:
return self.primary_resource.ParseResourceArg(namespace)
else:
return None
class DeclarativeArgumentGenerator(object):
"""An argument generator that operates off a declarative configuration.
When using this generator, you must provide attributes for the arguments that
should be generated. All resource arguments must be provided and arguments
will only be generated for API fields for which attributes were provided.
"""
def __init__(self, arg_info):
"""Creates a new Argument Generator.
Args:
arg_info: [yaml_arg_schema.Argument], Information about
request fields and how to map them into arguments.
"""
self.arg_info = arg_info
self.resource_args = _GetResources(self.arg_info)
def GenerateArgs(self, methods):
"""Generates all the CLI arguments required to call this method.
Args:
methods: list[APIMethod], list of methods to generate arguments for.
Returns:
{str, calliope.base.Action}, A map of field name to the argument.
"""
shared_flag_resource_dict = _GetSharedFlags(self.resource_args)
shared_resource_flag_list = list(shared_flag_resource_dict)
args = [arg.Generate(methods, shared_resource_flag_list)
for arg in self.arg_info]
primary_resource_args = _GetMethodResourceArgs(self.resource_args, methods)
primary_names = set(
arg.primary_resource and arg.primary_resource.name
for arg in primary_resource_args)
for flag_name, resource_args in shared_flag_resource_dict.items():
resource_names = list(set(resource_args))
resource_names.sort(
key=lambda name: '' if name in primary_names else name)
args.append(base.Argument(
flag_name,
help='For resources [{}], provides fallback value for resource '
'{attr} attribute. When the resource\'s full URI path is not '
'provided, {attr} will fallback to this flag value.'.format(
', '.join(resource_names),
attr=resource_util.StripPrefix(flag_name))))
return args
def GetPrimaryResource(self, methods, namespace):
"""Gets primary resource based on user input and returns single method.
This determines which api method to use to make api request. If there
is only one potential request method, return the one request method.
Args:
methods: list[APIMethod], The method to generate arguments for.
namespace: The argparse namespace.
Returns:
MethodResourceArg, gets the primary resource arg and method the
user specified in the namespace.
Raises:
ConflictingResourcesError: occurs when user specifies too many primary
resources.
"""
specified_methods = []
primary_resources = _GetMethodResourceArgs(self.resource_args, methods)
# Do not need to look at user specified args if there is only one primary
# resource arg or method.
if not primary_resources:
return MethodResourceArg(primary_resource=None, method=None)
elif len(primary_resources) == 1:
return primary_resources.pop()
for method_info in primary_resources:
method = method_info.method
primary_resource = method_info.primary_resource
# A primary resource can be None if added to a hook. If more than one
# collection is specified, we require that a primary resource is added.
# Otherwise, we cannot evaluate which method to use.
if not method or not primary_resource:
raise util.InvalidSchemaError(
'If more than one request collection is specified, a resource '
'argument that corresponds with the collection, must be '
'specified in YAML command.'
)
method_collection = _GetCollectionName(
method, is_parent=primary_resource.is_parent_resource)
specified_resource = method_info.Parse(namespace)
primary_collection = (
specified_resource and
specified_resource.GetCollectionInfo().full_name)
if method_collection == primary_collection:
specified_methods.append(method_info)
if len(specified_methods) > 1:
uris = []
for method_info in specified_methods:
if parsed := method_info.Parse(namespace):
uris.append(parsed.RelativeName())
args = ', '.join(uris)
raise ConflictingResourcesError(
f'User specified multiple primary resource arguments: [{args}]. '
'Unable to determine api request method.')
if len(specified_methods) == 1:
return specified_methods.pop()
else:
return MethodResourceArg(primary_resource=None, method=None)
def CreateRequest(self,
namespace,
method,
static_fields=None,
labels=None,
command_type=None,
existing_message=None):
"""Generates the request object for the method call from the parsed args.
Args:
namespace: The argparse namespace.
method: APIMethod, api method used to make request message.
static_fields: {str, value}, A mapping of API field name to value to
insert into the message. This is a convenient way to insert extra data
while the request is being constructed for fields that don't have
corresponding arguments.
labels: The labels section of the command spec.
command_type: Type of the command, i.e. CREATE, UPDATE.
existing_message: the apitools message returned from server, which is used
to construct the to-be-modified message when the command follows
get-modify-update pattern.
Returns:
The apitools message to be send to the method.
"""
new_message = method.GetRequestType()()
# If an apitools message is provided, use the existing one by default
# instead of creating an empty one.
if existing_message:
message = arg_utils.ParseExistingMessageIntoMessage(
new_message, existing_message, method)
else:
message = new_message
# Add labels into message
if labels:
if command_type == yaml_command_schema.CommandType.CREATE:
_ParseLabelsIntoCreateMessage(message, namespace, labels.api_field)
elif command_type == yaml_command_schema.CommandType.UPDATE:
need_update = _ParseLabelsIntoUpdateMessage(message, namespace,
labels.api_field)
if need_update:
update_mask_path = update.GetMaskFieldPath(method)
_AddLabelsToUpdateMask(static_fields, update_mask_path)
# Insert static fields into message.
arg_utils.ParseStaticFieldsIntoMessage(message, static_fields=static_fields)
# Parse api Fields into message.
for arg in self.arg_info:
arg.Parse(method, message, namespace)
return message
def GetResponseResourceRef(self, id_value, namespace, method):
"""Gets a resource reference for a resource returned by a list call.
It parses the namespace to find a reference to the parent collection and
then creates a reference to the child resource with the given id_value.
Args:
id_value: str, The id of the child resource that was returned.
namespace: The argparse namespace.
method: APIMethod, method used to make the api request
Returns:
resources.Resource, The parsed resource reference.
"""
methods = [method] if method else []
parent_ref = self.GetPrimaryResource(methods, namespace).Parse(namespace)
return resources.REGISTRY.Parse(
id_value,
collection=method.collection.full_name,
api_version=method.collection.api_version,
params=parent_ref.AsDict())
def Limit(self, namespace):
"""Gets the value of the limit flag (if present)."""
return getattr(namespace, 'limit', None)
def PageSize(self, namespace):
"""Gets the value of the page size flag (if present)."""
return getattr(namespace, 'page_size', None)
class AutoArgumentGenerator(object):
"""An argument generator to generate arguments for all fields in a message.
When using this generator, you don't provide any manual configuration for
arguments, it is all done automatically based on the request messages.
There are two modes for this generator. In 'raw' mode, no modifications are
done at all to the generated fields. In normal mode, certain list fields are
not generated and instead our global list flags are used (and orchestrate
the proper API fields automatically). In both cases, we generate additional
resource arguments for path parameters.
"""
FLAT_RESOURCE_ARG_NAME = 'resource'
IGNORABLE_LIST_FIELDS = {'filter', 'pageToken', 'orderBy'}
def __init__(self, method, raw=False):
"""Creates a new Argument Generator.
Args:
method: APIMethod, The method to generate arguments for.
raw: bool, True to do no special processing of arguments for list
commands. If False, typical List command flags will be added in and the
equivalent API fields will be ignored.
"""
self.method = method
self.raw = raw
self.is_atomic = self.method.detailed_params != self.method.params
self.ignored_fields = set()
if not raw and self.method.HasTokenizedRequest():
self.ignored_fields |= AutoArgumentGenerator.IGNORABLE_LIST_FIELDS
batch_page_size_field = self.method.BatchPageSizeField()
if batch_page_size_field:
self.ignored_fields.add(batch_page_size_field)
def GenerateArgs(self):
"""Generates all the CLI arguments required to call this method.
Returns:
{str, calliope.base.Action}, A map of field name to the argument.
"""
seen = set()
args = []
def _UpdateArgs(arguments):
"""Update args."""
for arg in arguments:
try:
name = arg.name
except IndexError:
# An argument group does not have a name.
pass
else:
if name in seen:
continue
seen.add(name)
args.append(arg)
# NOTICE: The call order is significant. Duplicate arg names are possible.
# The first of the duplicate args entered wins.
_UpdateArgs(self._GenerateResourceArg())
_UpdateArgs(self._GenerateArguments('', self.method.GetRequestType()))
_UpdateArgs(self._GenerateListMethodFlags())
return args
def CreateRequest(self, namespace):
"""Generates the request object for the method call from the parsed args.
Args:
namespace: The argparse namespace.
Returns:
The apitools message to be send to the method.
"""
request_type = self.method.GetRequestType()
# Recursively create the message and sub-messages.
fields = self._ParseArguments(namespace, '', request_type)
# For each actual method path field, add the attribute to the request.
ref = self._ParseResourceArg(namespace)
if ref:
relative_name = ref.RelativeName()
fields.update({f: getattr(ref, f, relative_name)
for f in self.method.params})
return request_type(**fields)
def Limit(self, namespace):
"""Gets the value of the limit flag (if present)."""
if not self.raw:
return getattr(namespace, 'limit', None)
else:
return None
def PageSize(self, namespace):
"""Gets the value of the page size flag (if present)."""
if not self.raw:
return getattr(namespace, 'page_size', None)
else:
return None
def _GenerateListMethodFlags(self):
"""Generates all the CLI flags for a List command.
Returns:
{str, calliope.base.Action}, A map of field name to the argument.
"""
flags = []
if not self.raw and self.method.IsList():
flags.append(base.FILTER_FLAG)
flags.append(base.SORT_BY_FLAG)
if self.method.HasTokenizedRequest() and self.method.ListItemField():
# We can use YieldFromList() with a limit.
flags.append(base.LIMIT_FLAG)
if self.method.BatchPageSizeField():
# API supports page size.
flags.append(base.PAGE_SIZE_FLAG)
return flags
def _GenerateArguments(self, prefix, message):
"""Gets the arguments to add to the parser that appear in the method body.
Args:
prefix: str, A string to prepend to the name of the flag. This is used
for flags representing fields of a submessage.
message: The apitools message to generate the flags for.
Returns:
{str, calliope.base.Argument}, A map of field name to argument.
"""
args = []
field_helps = arg_utils.FieldHelpDocs(message)
for field in message.all_fields():
field_help = field_helps.get(field.name, None)
name = self._GetArgName(field.name, field_help)
if not name:
continue
name = prefix + name
if field.variant == messages.Variant.MESSAGE:
sub_args = self._GenerateArguments(name + '.', field.type)
if sub_args:
help_text = (name + ': ' + field_help) if field_help else ''
group = base.ArgumentGroup(help=help_text)
args.append(group)
for arg in sub_args:
group.AddArgument(arg)
else:
attributes = yaml_arg_schema.Argument(name, name, field_help)
arg = arg_utils.GenerateFlag(field, attributes, fix_bools=False,
category='MESSAGE')
if not arg.kwargs.get('help'):
arg.kwargs['help'] = 'API doc needs help for field [{}].'.format(name)
args.append(arg)
return args
def _GenerateResourceArg(self):
"""Gets the flags to add to the parser that appear in the method path.
Returns:
{str, calliope.base.Argument}, A map of field name to argument.
"""
args = []
field_names = (self.method.request_collection.detailed_params
if self.method.request_collection else None)
if not field_names:
return args
field_helps = arg_utils.FieldHelpDocs(self.method.GetRequestType())
default_help = 'For substitution into: ' + self.method.detailed_path
# Make a dedicated positional in addition to the flags for each part of
# the URI path.
arg = base.Argument(
AutoArgumentGenerator.FLAT_RESOURCE_ARG_NAME,
nargs='?',
help='The GRI for the resource being operated on.')
args.append(arg)
for field in field_names:
arg = base.Argument(
'--' + field,
metavar=resource_property.ConvertToAngrySnakeCase(field),
category='RESOURCE',
help=field_helps.get(field, default_help))
args.append(arg)
return args
def _ParseArguments(self, namespace, prefix, message):
"""Recursively generates data for the request message and any sub-messages.
Args:
namespace: The argparse namespace containing the all the parsed arguments.
prefix: str, The flag prefix for the sub-message being generated.
message: The apitools class for the message.
Returns:
A dict of message field data that can be passed to an apitools Message.
"""
kwargs = {}
for field in message.all_fields():
arg_name = self._GetArgName(field.name)
if not arg_name:
continue
arg_name = prefix + arg_name
# Field is a sub-message, recursively generate it.
if field.variant == messages.Variant.MESSAGE:
sub_kwargs = self._ParseArguments(namespace, arg_name + '.', field.type)
if sub_kwargs:
# Only construct the sub-message if we have something to put in it.
value = field.type(**sub_kwargs)
kwargs[field.name] = value if not field.repeated else [value]
# Field is a scalar, just get the value.
else:
value = arg_utils.GetFromNamespace(namespace, arg_name)
if value is not None:
kwargs[field.name] = arg_utils.ConvertValue(field, value)
return kwargs
def _ParseResourceArg(self, namespace):
"""Gets the resource ref for the resource specified as the positional arg.
Args:
namespace: The argparse namespace.
Returns:
The parsed resource ref or None if no resource arg was generated for this
method.
"""
field_names = (self.method.request_collection.detailed_params
if self.method.request_collection else None)
if not field_names:
return
r = getattr(namespace, AutoArgumentGenerator.FLAT_RESOURCE_ARG_NAME)
enforce_collection = getattr(namespace, 'enforce_collection', True)
params = {}
defaults = {}
for f in field_names:
value = getattr(namespace, f)
if value:
params[f] = value
else:
default = arg_utils.DEFAULT_PARAMS.get(f, lambda: None)()
if default:
defaults[f] = default
if not r and not params and len(defaults) < len(field_names):
# No values were explicitly given and there are not enough defaults for
# the parse to work.
return None
defaults.update(params)
return resources.REGISTRY.Parse(
r, collection=self.method.request_collection.full_name,
enforce_collection=enforce_collection,
api_version=self.method.request_collection.api_version,
params=defaults)
def _GetArgName(self, field_name, field_help=None):
"""Gets the name of the argument to generate for the field.
Args:
field_name: str, The name of the field.
field_help: str, The help for the field in the API docs.
Returns:
str, The name of the argument to generate, or None if this field is output
only or should be ignored.
"""
if field_help and arg_utils.IsOutputField(field_help):
return None
if field_name in self.ignored_fields:
return None
if (field_name == self.method.request_field and
field_name.lower().endswith('request')):
return 'request'
return field_name

View File

@@ -0,0 +1,325 @@
# -*- 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 handling YAML schemas for gcloud export/import commands."""
from __future__ import absolute_import
from __future__ import division
from __future__ import unicode_literals
import collections
import io
import os
import re
import textwrap
from googlecloudsdk.core import log
from googlecloudsdk.core.resource import resource_projector
from googlecloudsdk.core.resource import yaml_printer
from googlecloudsdk.core.util import files
import six
_SPEC_DESCRIPTION = 'A gcloud export/import command YAML validation schema.'
_WIDTH = 80 # YAML list line width
_MINWRAP = 40 # The minimum text wrap width if nesting is too deep.
_INDENT = 2 # YAML nested object indentation
_DESCRIPTION_INDENT = len('description: ') - _INDENT
_YAML_WORKAROUND = '<YAML-WORKAROUND/>'
_OPTIONAL = 'Optional.'
_OUTPUT_ONLY = 'Output only.'
_REQUIRED = 'Required.'
def _WrapDescription(depth, text):
"""Returns description: |- text wrapped so it won't exceed _WIDTH at depth.
The YAML representer doesn't seem to take the length of the current tag
into account when deciding whether to inline strings or use |-. In this case
the tag is always "description: ". This function detects when YAML would fail
and adds temporary marker lines to produce the desired output. The marker
lines are removed prior to final output.
Args:
depth: The nested dict depth.
text: The text string to wrap.
Returns:
Text wrapped so it won't exceed _WIDTH at depth.
"""
width = _WIDTH - (depth * _INDENT)
lines = textwrap.wrap(text, max(_MINWRAP, width))
if len(lines) != 1:
return '\n'.join(lines)
line = lines[0]
nudge = width - (len(line) + _DESCRIPTION_INDENT)
if nudge < 0:
# nudge<0 means we are in the YAML bug zone and we nudge the representer to
# fall through to the |- form by adding enough spaces and a marker. The
# marker lines are removed prior to final output.
return line + '\n' + nudge * ' ' + _YAML_WORKAROUND
return line
def _NormalizeTypeName(name):
"""Returns the JSON schema normalized type name for name."""
s = six.text_type(name).lower()
if re.match(r'.?int64', s):
return 'integer'
if re.match(r'.?int32', s):
return 'integer'
if re.match(r'^int\d*$', s):
return 'integer'
if s == 'float':
return 'number'
if s == 'double':
return 'number'
if s == 'bool':
return 'boolean'
if s == 'bytes':
return 'string'
return s
def _IsProtobufStructField(field):
"""Returns whether the field represents a google.protobuf.Struct message.
google.protobuf.Struct is the following message:
message Struct {
// Unordered map of dynamically typed values.
map<string, Value> fields = 1;
}
In apitools, this corresponds to a message with an additionalProperties
field containing a list of AdditionalProperty messages each of which holds a
key/value pair, where the value is a extra_types.JsonValue message.
Args:
field: A message spec field dict.
Returns:
True iff the field is a google.protobuf.Struct.
"""
maybe_map_value_type = field.get(
'fields', {}).get(
'additionalProperties', {}).get(
'fields', {}).get(
'value', {}).get(
'type', '')
return maybe_map_value_type == 'JsonValue'
def _GetRequiredFields(fields):
"""Returns the list of required field names in fields.
Args:
fields: A message spec fields dict.
Returns:
The list of required field names in fields.
"""
required = []
for name, value in six.iteritems(fields):
description = value['description']
# NOTE: Protobufs use the special field name 'additionalProperties' to
# encode additional self-defining properties. This allows an API to extend
# proto data without changing the proto description, at the cost of being
# stringy-typed. JSON schema uses the 'additionalProperties' bool property
# to declare if only properties declared in the schema are allowed (false)
# or if properties not in the schema are allowed (true).
if name != 'additionalProperties' and description.startswith(_REQUIRED):
required.append(name)
return required
def _AddRequiredFields(spec, fields):
"""Adds required fields to spec."""
required = _GetRequiredFields(fields)
if required:
spec['required'] = sorted(required)
class ExportSchemasGenerator(object):
"""Recursively generates export JSON schemas for nested messages."""
def __init__(self, api, directory=None):
self._api = api
self._directory = directory
self._generated = set()
def _GetSchemaFileName(self, message_name):
"""Returns the schema file name given the message name."""
return message_name + '.yaml'
def _GetSchemaFilePath(self, message_name):
"""Returns the schema file path name given the message name."""
file_path = self._GetSchemaFileName(message_name)
if self._directory:
file_path = os.path.join(self._directory, file_path)
return file_path
def _WriteSchema(self, message_name, spec):
"""Writes the schema in spec to the _GetSchemaFilePath() file."""
tmp = io.StringIO()
tmp.write('$schema: "http://json-schema.org/draft-06/schema#"\n\n')
yaml_printer.YamlPrinter(
name='yaml',
projector=resource_projector.IdentityProjector(),
out=tmp).Print(spec)
content = re.sub('\n *{}\n'.format(_YAML_WORKAROUND), '\n', tmp.getvalue())
file_path = self._GetSchemaFilePath(message_name)
log.info('Generating JSON schema [{}].'.format(file_path))
with files.FileWriter(file_path) as w:
w.write(content)
def _AddFields(self, depth, parent, spec, fields):
"""Adds message fields to the YAML spec.
Args:
depth: The nested dict depth.
parent: The parent spec (nested ordered dict to add fields to) of spec.
spec: The nested ordered dict to add fields to.
fields: A message spec fields dict to add to spec.
"""
depth += 2
for name, value in sorted(six.iteritems(fields)):
description = value['description'].strip()
if description.startswith(_OPTIONAL):
description = description[len(_OPTIONAL):].strip()
elif description.startswith(_REQUIRED):
description = description[len(_REQUIRED):].strip()
if description.startswith(_OUTPUT_ONLY):
continue
d = collections.OrderedDict()
spec[name] = d
d['description'] = _WrapDescription(depth, description)
if value.get('repeated'):
d['type'] = 'array'
items = collections.OrderedDict(value.get('items', {}))
d['items'] = items
d = items
depth += 2
# When generating the message spec, we set 'type' to the name of the
# message type for message fields; otherwise it will be the apitools
# variant type.
is_message_field = isinstance(value.get('type'), str)
type_name = value.get('type', 'boolean')
subfields = value.get('fields', {})
if is_message_field:
if name == 'additionalProperties':
# This is proto 'additionalProperties', not JSON schema.
del spec[name]
properties = collections.OrderedDict()
self._AddFields(depth, d, properties, subfields)
if properties:
parent[name] = properties
elif _IsProtobufStructField(value):
# A google.protobuf.Struct field is a map of strings to arbitrary JSON
# values, so for the corresponding schema we accept an object type
# with no constraints on the (jsonschema) additionalProperties.
d['type'] = 'object'
else:
# Reference another schema file that we'll generate right now.
d['$ref'] = self._GetSchemaFileName(type_name)
self.Generate(type_name, subfields)
else:
if type_name in self._generated:
d['$ref'] = self._GetSchemaFileName(type_name)
else:
type_name = _NormalizeTypeName(type_name)
if type_name == 'enum':
# Convert enums from proto to JSON schema.
enum = value.get('choices')
d['type'] = 'string'
d['enum'] = sorted([n for n, _ in six.iteritems(enum)])
else:
d['type'] = type_name
def Generate(self, message_name, message_spec):
"""Recursively generates export/import YAML schemas for message_spec.
The message and nested messages are generated in separate schema files in
the destination directory. Pre-existing files are silently overwritten.
Args:
message_name: The API message name for message_spec.
message_spec: An arg_utils.GetRecursiveMessageSpec() message spec.
"""
# Check if this schema was already generated in this invocation.
if message_name in self._generated:
return
self._generated.add(message_name)
# Initialize the common spec properties.
spec = collections.OrderedDict()
spec['title'] = '{} {} {} export schema'.format(
self._api.name, self._api.version, message_name)
spec['description'] = _SPEC_DESCRIPTION
spec['type'] = 'object'
_AddRequiredFields(spec, message_spec)
spec['additionalProperties'] = False
properties = collections.OrderedDict()
spec['properties'] = properties
type_string = {'type': 'string'}
# COMMENT ignored by import commands
comment = collections.OrderedDict()
properties['COMMENT'] = comment
comment['type'] = 'object'
comment['description'] = 'User specified info ignored by gcloud import.'
comment['additionalProperties'] = False
comment_properties = collections.OrderedDict()
comment['properties'] = comment_properties
comment_properties['template-id'] = collections.OrderedDict(type_string)
comment_properties['region'] = collections.OrderedDict(type_string)
comment_properties['description'] = collections.OrderedDict(type_string)
comment_properties['date'] = collections.OrderedDict(type_string)
comment_properties['version'] = collections.OrderedDict(type_string)
# UNKNOWN marks incomplete export data
unknown = collections.OrderedDict()
properties['UNKNOWN'] = unknown
unknown['type'] = 'array'
unknown['description'] = 'Unknown API fields that cannot be imported.'
unknown['items'] = type_string
self._AddFields(1, spec, properties, message_spec)
self._WriteSchema(message_name, spec)
def GenerateExportSchemas(api, message_name, message_spec, directory=None):
"""Recursively generates export/import YAML schemas for message_spec in api.
The message and nested messages are generated in separate schema files in the
current directory. Pre-existing files are silently overwritten.
Args:
api: An API registry object.
message_name: The API message name for message_spec.
message_spec: An arg_utils.GetRecursiveMessageSpec() message spec.
directory: The path name of the directory to place the generated schemas,
None for the current directory.
"""
ExportSchemasGenerator(api, directory).Generate(message_name, message_spec)

View File

@@ -0,0 +1,621 @@
# -*- coding: utf-8 -*- #
# Copyright 2016 Google LLC. All Rights Reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
"""Utilities for the gcloud meta apis surface."""
from __future__ import absolute_import
from __future__ import division
from __future__ import unicode_literals
from apitools.base.protorpclite import messages
from apitools.base.py import exceptions as apitools_exc
from apitools.base.py import list_pager
from googlecloudsdk.api_lib.util import apis
from googlecloudsdk.api_lib.util import apis_internal
from googlecloudsdk.api_lib.util import resource
from googlecloudsdk.command_lib.util.apis import arg_utils
from googlecloudsdk.core import exceptions
from googlecloudsdk.core import log
from googlecloudsdk.generated_clients.apis import apis_map
import six
NAME_SEPARATOR = '.'
class Error(exceptions.Error):
pass
class UnknownAPIError(Error):
def __init__(self, api_name):
super(UnknownAPIError, self).__init__(
'API [{api}] does not exist or is not registered.'
.format(api=api_name)
)
class UnknownAPIVersionError(Error):
def __init__(self, api_name, version):
super(UnknownAPIVersionError, self).__init__(
'Version [{version}] does not exist for API [{api}].'
.format(version=version, api=api_name)
)
class NoDefaultVersionError(Error):
def __init__(self, api_name):
super(NoDefaultVersionError, self).__init__(
'API [{api}] does not have a default version. You must specify which '
'version to use.'.format(api=api_name)
)
class UnknownCollectionError(Error):
def __init__(self, api_name, api_version, collection):
super(UnknownCollectionError, self).__init__(
'Collection [{collection}] does not exist for [{api}] [{version}].'
.format(collection=collection, api=api_name, version=api_version)
)
class UnknownMethodError(Error):
def __init__(self, method, collection):
super(UnknownMethodError, self).__init__(
'Method [{method}] does not exist for collection [{collection}].'
.format(method=method, collection=collection)
)
class APICallError(Error):
pass
class API(object):
"""A data holder for returning API data for display."""
def __init__(self, name, version, is_default, client, base_url):
self.name = name
self.version = version
self.is_default = is_default
self._client = client
self.base_url = base_url
def GetMessagesModule(self):
return self._client.MESSAGES_MODULE
class APICollection(object):
"""A data holder for collection information for an API."""
def __init__(self, collection_info):
self.api_name = collection_info.api_name
self.api_version = collection_info.api_version
self.base_url = collection_info.base_url
self.docs_url = collection_info.docs_url
self.name = collection_info.name
self.full_name = collection_info.full_name
self.detailed_path = collection_info.GetPath('')
self.detailed_params = collection_info.GetParams('')
self.path = collection_info.path
self.params = collection_info.params
self.enable_uri_parsing = collection_info.enable_uri_parsing
class APIMethod(object):
"""A data holder for method information for an API collection."""
def __init__(self, service, name, api_collection, method_config,
disable_pagination=False):
self._service = service
self._method_name = name
self._disable_pagination = disable_pagination
self.collection = api_collection
self.name = method_config.method_id
dotted_path = self.collection.full_name + NAME_SEPARATOR
if self.name.startswith(dotted_path):
self.name = self.name[len(dotted_path):]
self.path = _RemoveVersionPrefix(
self.collection.api_version, method_config.relative_path)
self.params = method_config.ordered_params
if method_config.flat_path:
self.detailed_path = _RemoveVersionPrefix(
self.collection.api_version, method_config.flat_path)
self.detailed_params = resource.GetParamsFromPath(method_config.flat_path)
else:
self.detailed_path = self.path
self.detailed_params = self.params
self.http_method = method_config.http_method
self.request_field = method_config.request_field
self.request_type = method_config.request_type_name
self.response_type = method_config.response_type_name
self._request_collection = self._RequestCollection()
# Keep track of method query parameters
self.query_params = method_config.query_params
@property
def resource_argument_collection(self):
"""Gets the collection that should be used to represent the resource.
Most of the time this is the same as request_collection because all methods
in a collection operate on the same resource and so the API method takes
the same parameters that make up the resource.
One exception is List methods where the API parameters are for the parent
collection. Because people don't specify the resource directly for list
commands this also returns the parent collection for parsing purposes.
The other exception is Create methods. They reference the parent collection
list Like, but the difference is that we *do* want to specify the actual
resource on the command line, so the original resource collection is
returned here instead of the one that matches the API methods. When
generating the request, you must figure out how to generate the message
correctly from the parsed resource (as you cannot simply pass the reference
to the API).
Returns:
APICollection: The collection.
"""
if self.IsList():
return self._request_collection
return self.collection
@property
def request_collection(self):
"""Gets the API collection that matches the parameters of the API method."""
return self._request_collection
def GetRequestType(self):
"""Gets the apitools request class for this method."""
return self._service.GetRequestType(self._method_name)
def GetResponseType(self):
"""Gets the apitools response class for this method."""
return self._service.GetResponseType(self._method_name)
def GetEffectiveResponseType(self):
"""Gets the effective apitools response class for this method.
This will be different from GetResponseType for List methods if we are
extracting the list of response items from the overall response. This will
always match the type of response that Call() returns.
Returns:
The apitools Message object.
"""
if (item_field := self.ListItemField()) and self.HasTokenizedRequest():
return arg_utils.GetFieldFromMessage(
self.GetResponseType(), item_field).type
else:
return self.GetResponseType()
def GetMessageByName(self, name):
"""Gets a arbitrary apitools message class by name.
This method can be used to get arbitrary apitools messages from the
underlying service. Examples:
policy_type = method.GetMessageByName('Policy')
status_type = method.GetMessageByName('Status')
Args:
name: str, the name of the message to return.
Returns:
The apitools Message object.
"""
msgs = self._service.client.MESSAGES_MODULE
return getattr(msgs, name, None)
def IsList(self):
"""Determines whether this is a List method."""
return self._method_name == 'List'
def HasTokenizedRequest(self):
"""Determines whether this is a method that supports paging."""
return (not self._disable_pagination
and 'pageToken' in self._RequestFieldNames()
and 'nextPageToken' in self._ResponseFieldNames())
def BatchPageSizeField(self):
"""Gets the name of the page size field in the request if it exists."""
request_fields = self._RequestFieldNames()
if 'maxResults' in request_fields:
return 'maxResults'
if 'pageSize' in request_fields:
return 'pageSize'
return None
def ListItemField(self):
"""Gets the name of the field that contains the items in paginated response.
This will return None if the method is not a paginated or if a single
repeated field of items could not be found in the response type.
Returns:
str, The name of the field or None.
"""
if self._disable_pagination:
return None
response = self.GetResponseType()
found = [f for f in response.all_fields()
if f.variant == messages.Variant.MESSAGE and f.repeated]
if len(found) == 1:
return found[0].name
else:
return None
def _RequestCollection(self):
"""Gets the collection that matches the API parameters of this method.
Methods apply to elements of a collection. The resource argument is always
of the type of that collection. List is an exception where you are listing
items of that collection so the argument to be provided is that of the
parent collection. This method returns the collection that should be used
to parse the resource for this specific method.
Returns:
APICollection, The collection to use or None if no parent collection could
be found.
"""
if self.detailed_params == self.collection.detailed_params:
return self.collection
collections = GetAPICollections(
self.collection.api_name, self.collection.api_version)
for c in collections:
if (self.detailed_params == c.detailed_params
and c.detailed_path in self.detailed_path):
return c
# Fallback to collection that matches params only
for c in collections:
if self.detailed_params == c.detailed_params:
return c
return None
def _RequestFieldNames(self):
"""Gets the fields that are actually a part of the request message.
For APIs that use atomic names, this will only be the single name parameter
(and any other message fields) but not the detailed parameters.
Returns:
[str], The field names.
"""
return [f.name for f in self.GetRequestType().all_fields()]
def _ResponseFieldNames(self):
"""Gets the fields that are actually a part of the response message.
Returns:
[str], The field names.
"""
return [f.name for f in self.GetResponseType().all_fields()]
def Call(self, request, client=None, global_params=None, raw=False,
limit=None, page_size=None):
"""Executes this method with the given arguments.
Args:
request: The apitools request object to send.
client: base_api.BaseApiClient, An API client to use for making requests.
global_params: {str: str}, A dictionary of global parameters to send with
the request.
raw: bool, True to not do any processing of the response, False to maybe
do processing for List results.
limit: int, The max number of items to return if this is a List method.
page_size: int, The max number of items to return in a page if this API
supports paging.
Returns:
The response from the API.
"""
if client is None:
client = apis.GetClientInstance(
self.collection.api_name, self.collection.api_version)
service = _GetService(client, self.collection.name)
request_func = self._GetRequestFunc(
service, request, raw=raw, limit=limit, page_size=page_size)
try:
return request_func(global_params=global_params)
except apitools_exc.InvalidUserInputError as e:
log.debug('', exc_info=True)
raise APICallError(str(e))
def _GetRequestFunc(self, service, request, raw=False,
limit=None, page_size=None):
"""Gets a request function to call and process the results.
If this is a method with paginated response, it may flatten the response
depending on if the List Pager can be used.
Args:
service: The apitools service that will be making the request.
request: The apitools request object to send.
raw: bool, True to not do any processing of the response, False to maybe
do processing for List results.
limit: int, The max number of items to return if this is a List method.
page_size: int, The max number of items to return in a page if this API
supports paging.
Returns:
A function to make the request.
"""
if raw or self._disable_pagination:
return self._NormalRequest(service, request)
item_field = self.ListItemField()
if not item_field:
if self.IsList():
log.debug(
'Unable to flatten list response, raw results being returned.')
return self._NormalRequest(service, request)
if not self.HasTokenizedRequest():
# API doesn't do paging.
if self.IsList():
return self._FlatNonPagedRequest(service, request, item_field)
else:
return self._NormalRequest(service, request)
def RequestFunc(global_params=None):
return list_pager.YieldFromList(
service, request, method=self._method_name, field=item_field,
global_params=global_params, limit=limit,
current_token_attribute='pageToken',
next_token_attribute='nextPageToken',
batch_size_attribute=self.BatchPageSizeField(),
batch_size=page_size)
return RequestFunc
def _NormalRequest(self, service, request):
"""Generates a basic request function for the method.
Args:
service: The apitools service that will be making the request.
request: The apitools request object to send.
Returns:
A function to make the request.
"""
def RequestFunc(global_params=None):
method = getattr(service, self._method_name)
return method(request, global_params=global_params)
return RequestFunc
def _FlatNonPagedRequest(self, service, request, item_field):
"""Generates a request function for the method that extracts an item list.
List responses usually have a single repeated field that represents the
actual items being listed. This request function returns only those items
not the entire response.
Args:
service: The apitools service that will be making the request.
request: The apitools request object to send.
item_field: str, The name of the field that the list of items can be found
in.
Returns:
A function to make the request.
"""
def RequestFunc(global_params=None):
response = self._NormalRequest(service, request)(
global_params=global_params)
return getattr(response, item_field)
return RequestFunc
def _RemoveVersionPrefix(api_version, path):
"""Trims the version number off the front of a URL path if present."""
if not path:
return None
if path.startswith(api_version):
return path[len(api_version) + 1:]
return path
def _ValidateAndGetDefaultVersion(api_name, api_version):
"""Validates the API exists and gets the default version if not given."""
# pylint:disable=protected-access
api_name, _ = apis_internal._GetApiNameAndAlias(api_name)
api_vers = apis_map.MAP.get(api_name, {})
if not api_vers:
# No versions, this API is not registered.
raise UnknownAPIError(api_name)
if api_version:
if api_version not in api_vers:
raise UnknownAPIVersionError(api_name, api_version)
return api_version
for version, api_def in six.iteritems(api_vers):
if api_def.default_version:
return version
raise NoDefaultVersionError(api_name)
def GetAPI(api_name, api_version=None):
"""Get a specific API definition.
Args:
api_name: str, The name of the API.
api_version: str, The version string of the API.
Returns:
API, The API definition.
"""
api_version = _ValidateAndGetDefaultVersion(api_name, api_version)
# pylint: disable=protected-access
api_def = apis_internal.GetApiDef(api_name, api_version)
if api_def.apitools:
api_client = apis_internal._GetClientClassFromDef(api_def)
else:
api_client = apis_internal._GetGapicClientClass(api_name, api_version)
if hasattr(api_client, 'BASE_URL'):
base_url = api_client.BASE_URL
else:
try:
base_url = apis_internal._GetResourceModule(
api_name, api_version
).BASE_URL
except ImportError:
base_url = 'https://{}.googleapis.com/{}'.format(api_name, api_version)
return API(
api_name, api_version, api_def.default_version, api_client, base_url
)
def GetAllAPIs():
"""Gets all registered APIs.
Returns:
[API], A list of API definitions.
"""
all_apis = []
for api_name, versions in six.iteritems(apis_map.MAP):
for api_version, _ in six.iteritems(versions):
all_apis.append(GetAPI(api_name, api_version))
return all_apis
def _SplitFullCollectionName(collection):
return tuple(collection.split(NAME_SEPARATOR, 1))
def GetAPICollections(api_name=None, api_version=None):
"""Gets the registered collections for the given API version.
Args:
api_name: str, The name of the API or None for all apis.
api_version: str, The version string of the API or None to use the default
version.
Returns:
[APICollection], A list of the registered collections.
"""
if api_name:
all_apis = {api_name: _ValidateAndGetDefaultVersion(api_name, api_version)}
else:
all_apis = {x.name: x.version for x in GetAllAPIs() if x.is_default}
collections = []
for n, v in six.iteritems(all_apis):
# pylint:disable=protected-access
collections.extend(
[APICollection(c) for c in apis_internal._GetApiCollections(n, v)])
return collections
def GetAPICollection(full_collection_name, api_version=None):
"""Gets the given collection for the given API version.
Args:
full_collection_name: str, The collection to get including the api name.
api_version: str, The version string of the API or None to use the default
for this API.
Returns:
APICollection, The requested API collection.
Raises:
UnknownCollectionError: If the collection does not exist for the given API
and version.
"""
api_name, collection = _SplitFullCollectionName(full_collection_name)
api_version = _ValidateAndGetDefaultVersion(api_name, api_version)
collections = GetAPICollections(api_name, api_version)
for c in collections:
if c.name == collection:
return c
raise UnknownCollectionError(api_name, api_version, collection)
def GetMethod(full_collection_name, method, api_version=None,
disable_pagination=False):
"""Gets the specification for the given API method.
Args:
full_collection_name: str, The collection including the api name.
method: str, The name of the method.
api_version: str, The version string of the API or None to use the default
for this API.
disable_pagination: bool, Boolean for whether pagination should be disabled
Returns:
APIMethod, The method specification.
Raises:
UnknownMethodError: If the method does not exist on the collection.
"""
methods = GetMethods(
full_collection_name, api_version=api_version,
disable_pagination=disable_pagination)
for m in methods:
if m.name == method:
return m
raise UnknownMethodError(method, full_collection_name)
def _GetService(client, collection_name):
return getattr(client, collection_name.replace(NAME_SEPARATOR, '_'), None)
def _GetApiClient(api_name, api_version):
"""Gets the repesctive api client for the api."""
api_def = apis_internal.GetApiDef(api_name, api_version)
if api_def.apitools:
client = apis.GetClientInstance(api_name, api_version, no_http=True)
else:
client = apis.GetGapicClientInstance(api_name, api_version)
return client
def GetMethods(
full_collection_name, api_version=None, disable_pagination=False):
"""Gets all the methods available on the given collection.
Args:
full_collection_name: str, The collection including the api name.
api_version: str, The version string of the API or None to use the default
for this API.
disable_pagination: bool, Boolean for whether pagination should be disabled
Returns:
[APIMethod], The method specifications.
"""
api_collection = GetAPICollection(full_collection_name,
api_version=api_version)
client = _GetApiClient(api_collection.api_name, api_collection.api_version)
service = _GetService(client, api_collection.name)
if not service:
# This is a synthetic collection that does not actually have a backing API.
return []
method_names = service.GetMethodsList()
method_configs = [(name, service.GetMethodConfig(name))
for name in method_names]
return [APIMethod(service, name, api_collection, config, disable_pagination)
for name, config in method_configs]

View File

@@ -0,0 +1,163 @@
# -*- 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 handling YAML schemas for gcloud update commands."""
from __future__ import absolute_import
from __future__ import division
from __future__ import unicode_literals
from googlecloudsdk.command_lib.util.apis import arg_utils
from googlecloudsdk.command_lib.util.apis import yaml_arg_schema
from googlecloudsdk.core import exceptions
class NoFieldsSpecifiedError(exceptions.Error):
"""Raises when no arguments specified for update commands."""
def GetMaskString(args, spec, mask_path, is_dotted=True):
"""Gets the fieldMask that is required for update api calls.
Args:
args: The argparse parser.
spec: The CommandData class.
mask_path: string, the dotted path of mask in the api method
is_dotted: Boolean, True if the dotted path of the name is returned.
Returns:
A String, represents a mask specifying which fields in the resource should
be updated.
Raises:
NoFieldsSpecifiedError: this error would happen when no args are specified.
"""
if not args.GetSpecifiedArgs():
raise NoFieldsSpecifiedError(
'Must specify at least one valid parameter to update.')
field_set = set()
for param in _GetSpecParams(spec.arguments.params):
field_set.update(_GetMaskFields(param, args, mask_path, is_dotted))
# Sorts the list for better testing.
return ','.join(sorted(field_set))
def _GetMaskFields(param, args, mask_path, is_dotted):
"""Gets the fieldMask based on the yaml arg and the arguments specified.
Args:
param: yaml_arg_schema.YAMLArgument, the yaml argument added to parser
args: parser_extensions.Namespace, user specified arguments
mask_path: str, path to where update mask applies
is_dotted: bool, True if the dotted path of the name is returned
Returns:
Set of fields (str) to add to the update mask
"""
field_set = set()
if not param.IsApiFieldSpecified(args):
return field_set
for api_field in param.api_fields:
mask_field = _ExtractMaskField(mask_path, api_field, is_dotted)
if mask_field:
field_set.add(mask_field)
return field_set
def _GetSpecParams(params):
"""Recursively yields all the params in the spec.
Args:
params: List of Argument or ArgumentGroup objects.
Yields:
All the Argument objects in the command spec.
"""
for param in params:
if isinstance(param, yaml_arg_schema.ArgumentGroup):
for p in _GetSpecParams(param.arguments):
yield p
else:
yield param
def _ExtractMaskField(mask_path, api_field, is_dotted):
"""Extracts the api field name which constructs the mask used for request.
For most update requests, you have to specify which fields in the resource
should be updated. This information is stored as updateMask or fieldMask.
Because resource and mask are in the same path level in a request, this
function uses the mask_path as the guideline to extract the fields need to be
parsed in the mask.
Args:
mask_path: string, the dotted path of mask in an api method, e.g. updateMask
or updateRequest.fieldMask. The mask and the resource would always be in
the same level in a request.
api_field: string, the api field name in the resource to be updated and it
is specified in the YAML files, e.g. displayName or
updateRequest.instance.displayName.
is_dotted: Boolean, True if the dotted path of the name is returned.
Returns:
String, the field name of the resource to be updated..
"""
level = len(mask_path.split('.'))
api_field_list = api_field.split('.')
if is_dotted:
if 'additionalProperties' in api_field_list:
repeated_index = api_field_list.index('additionalProperties')
api_field_list = api_field_list[:repeated_index]
return '.'.join(api_field_list[level:])
else:
return api_field_list[level]
def GetMaskFieldPath(method):
"""Gets the dotted path of mask in the api method.
Args:
method: APIMethod, The method specification.
Returns:
String or None.
"""
possible_mask_fields = ('updateMask', 'fieldMask')
message = method.GetRequestType()()
# If the mask field is found in the request message of the method, return
# the mask name directly, e.g, updateMask
for mask in possible_mask_fields:
if hasattr(message, mask):
return mask
# If the mask field is found in the request field message, return the
# request field and the mask name, e.g, updateRequest.fieldMask.
if method.request_field:
request_field = method.request_field
request_message = None
if hasattr(message, request_field):
request_message = arg_utils.GetFieldFromMessage(message,
request_field).type
for mask in possible_mask_fields:
if hasattr(request_message, mask):
return '{}.{}'.format(request_field, mask)
return None

View File

@@ -0,0 +1,619 @@
# -*- coding: utf-8 -*- #
# Copyright 2023 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 update argument groups."""
from __future__ import absolute_import
from __future__ import division
from __future__ import unicode_literals
import abc
import enum
from googlecloudsdk.calliope import arg_parsers
from googlecloudsdk.calliope import arg_parsers_usage_text as usage_text
from googlecloudsdk.calliope import base
from googlecloudsdk.calliope.concepts import util as format_util
from googlecloudsdk.command_lib.util.apis import arg_utils
from googlecloudsdk.command_lib.util.apis import yaml_command_schema_util as util
import six
# TODO(b/280653078) The UX is still under review. These utilities are
# liable to change and should not be used in new surface yet.
# TODO(b/283949482): Place this file in util/args and replace the duplicate
# logic in the util files.
class Prefix(enum.Enum):
ADD = 'add'
UPDATE = 'update'
REMOVE = 'remove'
CLEAR = 'clear'
class _ConvertValueType(usage_text.DefaultArgTypeWrapper):
"""Wraps flag types in arg_utils.ConvertValue while maintaining help text.
Attributes:
arg_gen: UpdateBasicArgumentGenerator, update argument generator
"""
def __init__(self, arg_gen):
super(_ConvertValueType, self).__init__(arg_gen.flag_type)
self.field = arg_gen.field
self.repeated = arg_gen.repeated
self.processor = arg_gen.processor
self._choices = arg_gen.choices
@property
def choices(self):
"""Returns a map of choice values to choice help text.
Used in help text generation and not in actual input parsing.
"""
if self._choices:
return {choice.arg_value: choice.help_text for choice in self._choices}
else:
return None
def __call__(self, arg_value):
"""Converts arg_value into type arg_type."""
value = self.arg_type(arg_value)
return arg_utils.ConvertValue(
self.field,
value,
repeated=self.repeated,
processor=self.processor,
choices=util.Choice.ToChoiceMap(self._choices),
)
class UpdateArgumentGenerator(six.with_metaclass(abc.ABCMeta, object)):
"""Update flag generator.
To use this base class, provide required methods for parsing
(GetArgFromNamespace and GetFieldValueFromNamespace) and override
the flags that are needed to update the value. For example, if argument
group requires a set flag, we would override the `set_arg` property and
ApplySetFlag method.
"""
def _GetTextFormatOfEmptyValue(self, value):
if value:
return value
if isinstance(value, dict):
return 'empty map'
if isinstance(value, list):
return 'empty list'
if value is None:
return 'null'
return value
def _CreateFlag(
self, arg_name, flag_prefix=None, flag_type=None, action=None,
metavar=None, help_text=None
):
"""Creates a flag.
Args:
arg_name: str, root name of the arg
flag_prefix: Prefix | None, prefix for the flag name
flag_type: func, type that flag is used to convert user input
action: str, flag action
metavar: str, user specified metavar for flag
help_text: str, flag help text
Returns:
base.Argument with correct params
"""
flag_name = arg_utils.GetFlagName(
arg_name, flag_prefix and flag_prefix.value)
arg = base.Argument(flag_name, action=action, help=help_text)
if action == 'store_true':
return arg
arg.kwargs['type'] = flag_type
if flag_metavar := arg_utils.GetMetavar(metavar, flag_type, flag_name):
arg.kwargs['metavar'] = flag_metavar
return arg
# DEFAULT FLAGS GENERATED
@property
def set_arg(self):
"""Flag that sets field to specifed value."""
return None
@property
def clear_arg(self):
"""Flag that clears field."""
return None
@property
def update_arg(self):
"""Flag that updates value if part of existing field."""
return None
@property
def remove_arg(self):
"""Flag that removes value if part of existing field."""
return None
def Generate(self, additional_flags=None):
"""Returns ArgumentGroup with all flags specified in generator.
ArgumentGroup is returned where the set flag is mutually exclusive with
the rest of the update flags. In addition, remove and clear flags are
mutually exclusive. The following combinations are allowed
# sets the foo value to value1,value2
{command} --foo=value1,value2
# adds values value3
{command} --add-foo=value3
# clears values and sets foo to value4,value5
{command} --add-foo=value4,value5 --clear
# removes value4 and adds value6
{command} --add-foo=value6 --remove-foo=value4
# removes value6 and then re-adds it
{command} --add-foo=value6 --remove-foo=value6
Args:
additional_flags: [base.Argument], list of additional arguments needed
to udpate the value
Returns:
base.ArgumentGroup, argument group containing flags
"""
base_group = base.ArgumentGroup(
mutex=True,
required=False,
hidden=self.is_hidden,
help='Update {}.'.format(self.arg_name),
)
if self.set_arg:
base_group.AddArgument(self.set_arg)
update_group = base.ArgumentGroup(required=False)
if self.update_arg:
update_group.AddArgument(self.update_arg)
clear_group = base.ArgumentGroup(mutex=True, required=False)
if self.clear_arg:
clear_group.AddArgument(self.clear_arg)
if self.remove_arg:
clear_group.AddArgument(self.remove_arg)
if clear_group.arguments:
update_group.AddArgument(clear_group)
if update_group.arguments:
base_group.AddArgument(update_group)
if not additional_flags:
return base_group
wrapper_group = base.ArgumentGroup(
required=False,
hidden=self.is_hidden,
help='All arguments needed to update {}.'.format(self.arg_name),
)
wrapper_group.AddArgument(base_group)
for arg in additional_flags:
wrapper_group.AddArgument(arg)
return wrapper_group
# METHODS REQUIRED FOR PARSING NEW VALUE
@abc.abstractmethod
def GetArgFromNamespace(self, namespace, arg):
"""Retrieves namespace value associated with flag.
Args:
namespace: The parsed command line argument namespace.
arg: base.Argument, used to get namespace value
Returns:
value parsed from namespace
"""
pass
@abc.abstractmethod
def GetFieldValueFromMessage(self, existing_message):
"""Retrieves existing field from message.
Args:
existing_message: apitools message we need to get field value from
Returns:
field value from apitools message
"""
pass
# DEFAULTED METHODS FOR PARSING NEW VALUE
def ApplySetFlag(self, existing_val, unused_set_val):
"""Updates result to new value (No-op: implementation in subclass)."""
return existing_val
def ApplyClearFlag(self, existing_val, unused_clear_flag):
"""Clears existing value (No-op: implementation in subclass)."""
return existing_val
def ApplyRemoveFlag(self, existing_val, unused_remove_val):
"""Removes existing value (No-op: implementation in subclass)."""
return existing_val
def ApplyUpdateFlag(self, existing_val, unused_update_val):
"""Updates existing value (No-op: implementation in subclass)."""
return existing_val
def Parse(self, namespace, existing_message):
"""Parses update flags from namespace and returns updated message field.
Args:
namespace: The parsed command line argument namespace.
existing_message: Apitools message that exists for given resource.
Returns:
Modified existing apitools message field.
"""
result = self.GetFieldValueFromMessage(existing_message)
set_value, clear_value, remove_value, update_value = (
self.GetArgFromNamespace(namespace, self.set_arg),
self.GetArgFromNamespace(namespace, self.clear_arg),
self.GetArgFromNamespace(namespace, self.remove_arg),
self.GetArgFromNamespace(namespace, self.update_arg),
)
# Whether or not the flags are mutually exclusive are determined by the
# ArgumentGroup generated. We do not want to duplicate the mutex logic
# so instead we consistently apply all flags in same order, first by
# removing and then adding values.
# Remove values
result = self.ApplyClearFlag(result, clear_value)
result = self.ApplyRemoveFlag(result, remove_value)
# Add values
result = self.ApplySetFlag(result, set_value)
result = self.ApplyUpdateFlag(result, update_value)
return result
class UpdateBasicArgumentGenerator(UpdateArgumentGenerator):
"""Update flag generator for simple flags."""
@classmethod
def FromArgData(cls, arg_data, field):
"""Creates a flag generator from yaml arg data and request message.
Args:
arg_data: yaml_arg_schema.Argument, data about flag being generated
field: messages.Field, apitools field instance.
Returns:
UpdateArgumentGenerator, the correct version of flag generator
"""
flag_type, action = arg_utils.GenerateFlagType(field, arg_data)
is_repeated = (
field.repeated if arg_data.repeated is None else arg_data.repeated
)
field_type = arg_utils.GetFieldType(field)
if field_type == arg_utils.FieldType.MAP:
gen_cls = UpdateMapArgumentGenerator
elif is_repeated:
gen_cls = UpdateListArgumentGenerator
else:
gen_cls = UpdateDefaultArgumentGenerator
return gen_cls(
arg_name=arg_data.arg_name,
flag_type=flag_type,
field=field,
action=action,
is_hidden=arg_data.hidden,
help_text=arg_data.help_text,
api_field=arg_data.api_field,
repeated=arg_data.repeated,
processor=arg_data.processor,
choices=arg_data.choices,
metavar=arg_data.metavar,
)
def __init__(
self,
arg_name,
flag_type=None,
field=None,
action=None,
is_hidden=False,
help_text=None,
api_field=None,
repeated=False,
processor=None,
choices=None,
metavar=None,
):
super(UpdateBasicArgumentGenerator, self).__init__()
self.arg_name = format_util.NormalizeFormat(arg_name)
self.field = field
self.flag_type = flag_type
self.action = action
self.is_hidden = is_hidden
self.help_text = help_text
self.api_field = api_field
self.repeated = repeated
self.processor = processor
self.choices = choices
self.metavar = metavar
def GetArgFromNamespace(self, namespace, arg):
if arg is None:
return None
return arg_utils.GetFromNamespace(namespace, arg.name)
def GetFieldValueFromMessage(self, existing_message):
"""Retrieves existing field from message."""
if existing_message:
existing_value = arg_utils.GetFieldValueFromMessage(
existing_message, self.api_field
)
else:
existing_value = None
if isinstance(existing_value, list):
existing_value = existing_value.copy()
return existing_value
def _CreateBasicFlag(self, **kwargs):
return self._CreateFlag(arg_name=self.arg_name, **kwargs)
class UpdateDefaultArgumentGenerator(UpdateBasicArgumentGenerator):
"""Update flag generator for simple values."""
@property
def _empty_value(self):
return None
@property
def set_arg(self):
return self._CreateBasicFlag(
flag_type=_ConvertValueType(self),
action=self.action,
metavar=self.metavar,
help_text='Set {} to new value.'.format(self.arg_name),
)
@property
def clear_arg(self):
return self._CreateBasicFlag(
flag_prefix=Prefix.CLEAR,
action='store_true',
help_text='Clear {} value and set to {}.'.format(
self.arg_name, self._GetTextFormatOfEmptyValue(self._empty_value)),
)
def ApplySetFlag(self, existing_val, set_val):
if set_val is not None:
return set_val
return existing_val
def ApplyClearFlag(self, existing_val, clear_flag):
if clear_flag:
return self._empty_value
return existing_val
class UpdateListArgumentGenerator(UpdateBasicArgumentGenerator):
"""Update flag generator for list."""
@property
def _empty_value(self):
return []
@property
def set_arg(self):
return self._CreateBasicFlag(
flag_type=_ConvertValueType(self),
action=self.action,
metavar=self.metavar,
help_text='Set {} to new value.'.format(self.arg_name),
)
@property
def clear_arg(self):
return self._CreateBasicFlag(
flag_prefix=Prefix.CLEAR,
action='store_true',
help_text='Clear {} value and set to {}.'.format(
self.arg_name, self._GetTextFormatOfEmptyValue(self._empty_value)),
)
@property
def update_arg(self):
return self._CreateBasicFlag(
flag_prefix=Prefix.ADD,
flag_type=_ConvertValueType(self),
action=self.action,
help_text='Add new value to {} list.'.format(self.arg_name),
)
@property
def remove_arg(self):
return self._CreateBasicFlag(
flag_prefix=Prefix.REMOVE,
flag_type=_ConvertValueType(self),
action=self.action,
help_text='Remove existing value from {} list.'.format(self.arg_name),
)
def _ContainsVal(self, new_val, all_vals):
if isinstance(self.flag_type, util.EquitableType):
return any(
self.flag_type.Matches(new_val, val) for val in all_vals)
else:
return new_val in all_vals
def ApplySetFlag(self, existing_val, set_val):
if set_val is not None:
return set_val
return existing_val
def ApplyClearFlag(self, existing_val, clear_flag):
if clear_flag:
return self._empty_value
return existing_val
def ApplyRemoveFlag(self, existing_val, remove_val):
if remove_val is not None:
return [
x for x in existing_val if not self._ContainsVal(x, remove_val)]
return existing_val
def ApplyUpdateFlag(self, existing_val, update_val):
if update_val is not None:
new_vals = [
x for x in update_val if not self._ContainsVal(x, existing_val)]
return existing_val + new_vals
return existing_val
class UpdateMapArgumentGenerator(UpdateBasicArgumentGenerator):
"""Update flag generator for key-value pairs ie proto map fields."""
@property
def _empty_value(self):
return {}
@property
def _is_list_field(self):
return self.field.name == arg_utils.ADDITIONAL_PROPS
def _WrapOutput(self, output_list):
"""Wraps field AdditionalProperties in apitools message if needed.
Args:
output_list: list of apitools AdditionalProperties messages.
Returns:
apitools message instance.
"""
if self._is_list_field:
return output_list
message = self.field.type()
arg_utils.SetFieldInMessage(
message, arg_utils.ADDITIONAL_PROPS, output_list)
return message
def _GetPropsFieldValue(self, field):
"""Retrieves AdditionalProperties field value.
Args:
field: apitools instance that contains AdditionalProperties field
Returns:
list of apitools AdditionalProperties messages.
"""
if not field:
return []
if self._is_list_field:
return field
return arg_utils.GetFieldValueFromMessage(field, arg_utils.ADDITIONAL_PROPS)
@property
def set_arg(self):
return self._CreateBasicFlag(
flag_type=_ConvertValueType(self),
action=self.action,
metavar=self.metavar,
help_text='Set {} to new value.'.format(self.arg_name),
)
@property
def clear_arg(self):
return self._CreateBasicFlag(
flag_prefix=Prefix.CLEAR,
action='store_true',
help_text='Clear {} value and set to {}.'.format(
self.arg_name, self._GetTextFormatOfEmptyValue(self._empty_value)),
)
@property
def update_arg(self):
return self._CreateBasicFlag(
flag_prefix=Prefix.UPDATE,
flag_type=_ConvertValueType(self),
action=self.action,
help_text='Update {} value or add key value pair.'.format(
self.arg_name
),
)
@property
def remove_arg(self):
if self._is_list_field:
field = self.field
else:
field = arg_utils.GetFieldFromMessage(
self.field.type, arg_utils.ADDITIONAL_PROPS
)
key_field = arg_utils.GetFieldFromMessage(field.type, 'key')
key_type = key_field.type or arg_utils.TYPES.get(key_field.variant)
key_list = arg_parsers.ArgObject(
value_type=key_type, repeated=True)
return self._CreateBasicFlag(
flag_prefix=Prefix.REMOVE,
flag_type=key_list,
action='store',
help_text='Remove existing value from map {}.'.format(self.arg_name),
)
def ApplySetFlag(self, existing_val, set_val):
if set_val is not None:
return set_val
return existing_val
def ApplyClearFlag(self, existing_val, clear_flag):
if clear_flag:
return self._WrapOutput([])
return existing_val
def ApplyUpdateFlag(self, existing_val, update_val):
if update_val is not None:
output_list = self._GetPropsFieldValue(existing_val)
update_val_list = self._GetPropsFieldValue(update_val)
update_key_set = set([x.key for x in update_val_list])
deduped_list = [x for x in output_list if x.key not in update_key_set]
return self._WrapOutput(deduped_list + update_val_list)
return existing_val
def ApplyRemoveFlag(self, existing_val, remove_val):
if remove_val is not None:
output_list = self._GetPropsFieldValue(existing_val)
remove_val_set = set(remove_val)
return self._WrapOutput(
[x for x in output_list if x.key not in remove_val_set])
return existing_val

View File

@@ -0,0 +1,451 @@
# -*- coding: utf-8 -*- #
# Copyright 2023 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 update resource argument groups."""
from __future__ import absolute_import
from __future__ import division
from __future__ import unicode_literals
from googlecloudsdk.calliope import base
from googlecloudsdk.calliope.concepts import util as format_util
from googlecloudsdk.command_lib.util.apis import arg_utils
from googlecloudsdk.command_lib.util.apis import update_args
from googlecloudsdk.command_lib.util.apis import yaml_command_schema_util as util
from googlecloudsdk.command_lib.util.concepts import concept_parsers
from googlecloudsdk.core import resources
# TODO(b/280653078) The UX is still under review. These utilities are
# liable to change and should not be used in new surface yet.
# TODO(b/283949482): Place this file in util/args and replace the duplicate
# logic in the util files.
def _GetRelativeNameField(arg_data):
"""Gets message field where the resource's relative name is mapped."""
api_fields = [
key
for key, value in arg_data.resource_method_params.items()
if util.REL_NAME_FORMAT_KEY in value
]
if not api_fields:
return None
return api_fields[0]
def _GetSharedAttributeFlags(arg_data, shared_resource_flags):
"""Gets a list of all shared resource attributes."""
ignored_flags = set()
anchor_names = set(attr.name for attr in arg_data.anchors)
for attr_name, flag_name in arg_data.attribute_to_flag_map.items():
if (flag_name in arg_data.ignored_flags or
flag_name in shared_resource_flags or
attr_name in anchor_names):
continue
ignored_flags.add(flag_name)
return list(ignored_flags)
def _GetResourceArgGenerator(
arg_data, resource_collection, ignored_flags):
"""Gets a function to generate a resource arg."""
def ArgGen(name, group_help, flag_name_override):
group_help += '\n\n'
if arg_data.group_help:
group_help += arg_data.group_help
return arg_data.GenerateResourceArg(
resource_collection,
presentation_flag_name=name,
flag_name_override=flag_name_override,
shared_resource_flags=ignored_flags,
group_help=group_help)
return ArgGen
def _GenerateSharedFlags(
arg_data, resource_collection, shared_flag_names):
"""Generates a list of flags needed to generate more than one resource arg."""
arg_gen = _GetResourceArgGenerator(
arg_data, resource_collection, None)
# Generate a fake resource arg where none of the flags are filtered out.
resource_arg_info = arg_gen(
'--current', '', None).GetInfo('--current')
return [
arg for arg in resource_arg_info.GetAttributeArgs()
if arg.name in shared_flag_names
]
class UpdateResourceArgumentGenerator(update_args.UpdateArgumentGenerator):
"""Update flag generator for resource args."""
@classmethod
def FromArgData(
cls, arg_data, method_resource_collection, is_list_method=False,
shared_resource_flags=None
):
if arg_data.multitype and arg_data.repeated:
gen_cls = UpdateMultitypeListResourceArgumentGenerator
elif arg_data.multitype:
gen_cls = UpdateMultitypeResourceArgumentGenerator
elif arg_data.repeated:
gen_cls = UpdateListResourceArgumentGenerator
else:
gen_cls = UpdateDefaultResourceArgumentGenerator
presentation_flag_name = arg_data.GetPresentationFlagName(
method_resource_collection, is_list_method)
is_primary = arg_data.IsPrimaryResource(method_resource_collection)
if is_primary:
raise util.InvalidSchemaError(
'{} is a primary resource. Primary resources are required and '
'cannot be listed as clearable.'.format(presentation_flag_name)
)
api_field = _GetRelativeNameField(arg_data)
if not api_field:
raise util.InvalidSchemaError(
'{} does not specify the message field where the relative name is '
'mapped in resource_method_params. Message field name is needed '
'in order add update args. Please update '
'resource_method_params.'.format(presentation_flag_name)
)
shared_flags = shared_resource_flags or []
shared_attribute_flags = _GetSharedAttributeFlags(
arg_data, shared_flags)
# All of the flags the resource arg should not generate
all_shared_flags = shared_attribute_flags + shared_flags
return gen_cls(
presentation_flag_name=presentation_flag_name,
arg_gen=_GetResourceArgGenerator(
arg_data, method_resource_collection, all_shared_flags),
api_field=api_field,
repeated=arg_data.repeated,
collections=arg_data.collections,
is_primary=is_primary,
# attributes shared between update args we need to generate and
# add to the root.
shared_attribute_flags=_GenerateSharedFlags(
arg_data, method_resource_collection, shared_attribute_flags),
anchor_names=[attr.name for attr in arg_data.anchors],
)
def __init__(
self,
presentation_flag_name,
arg_gen=None,
is_hidden=False,
api_field=None,
repeated=False,
collections=None,
is_primary=None,
shared_attribute_flags=None,
anchor_names=None,
):
super(UpdateResourceArgumentGenerator, self).__init__()
self.arg_name = format_util.NormalizeFormat(
presentation_flag_name)
self.arg_gen = arg_gen
self.is_hidden = is_hidden
self.api_field = api_field
self.repeated = repeated
self.collections = collections or []
self.is_primary = is_primary
self.shared_attribute_flags = shared_attribute_flags or []
self.anchor_names = anchor_names or []
def _GetAnchorFlag(self, attr_name, flag_prefix_value):
if len(self.anchor_names) > 1:
base_name = attr_name
else:
base_name = self.arg_name
return arg_utils.GetFlagName(base_name, flag_prefix=flag_prefix_value)
def _CreateResourceFlag(self, flag_prefix=None, group_help=None):
prefix = flag_prefix and flag_prefix.value
flag_name = arg_utils.GetFlagName(
self.arg_name,
flag_prefix=prefix)
flag_name_override = {
anchor_name: self._GetAnchorFlag(anchor_name, prefix)
for anchor_name in self.anchor_names
}
return self.arg_gen(
flag_name, group_help=group_help, flag_name_override=flag_name_override)
def _RelativeName(self, value):
resource = None
for collection in self.collections:
try:
resource = resources.REGISTRY.ParseRelativeName(
value,
collection.full_name,
api_version=collection.api_version)
except resources.Error:
continue
return resource
def GetArgFromNamespace(self, namespace, arg):
"""Retrieves namespace value associated with flag.
Args:
namespace: The parsed command line argument namespace.
arg: base.Argument|concept_parsers.ConceptParser|None, used to get
namespace value
Returns:
value parsed from namespace
"""
if isinstance(arg, base.Argument):
return arg_utils.GetFromNamespace(namespace, arg.name)
if isinstance(arg, concept_parsers.ConceptParser):
all_anchors = list(arg.specs.keys())
if len(all_anchors) != 1:
raise ValueError(
'ConceptParser must contain exactly one spec for clearable '
'but found specs {}. {} cannot parse the namespace value if more '
'than or less than one spec is added to the '
'ConceptParser.'.format(all_anchors, type(self).__name__))
name = all_anchors[0]
value = arg_utils.GetFromNamespace(namespace.CONCEPTS, name)
if value:
value = value.Parse()
return value
return None
def GetFieldValueFromMessage(self, existing_message):
value = arg_utils.GetFieldValueFromMessage(existing_message, self.api_field)
if not value:
return None
if isinstance(value, list):
relative_names = (self._RelativeName(v) for v in value)
return [name for name in relative_names if name]
else:
return self._RelativeName(value)
def Generate(self):
return super(UpdateResourceArgumentGenerator, self).Generate(
self.shared_attribute_flags)
class UpdateDefaultResourceArgumentGenerator(UpdateResourceArgumentGenerator):
"""Update flag generator for resource args."""
@property
def _empty_value(self):
return None
@property
def set_arg(self):
return self._CreateResourceFlag(
group_help='Set {} to new value.'.format(self.arg_name))
@property
def clear_arg(self):
return self._CreateFlag(
self.arg_name,
flag_prefix=update_args.Prefix.CLEAR,
action='store_true',
help_text=(
f'Clear {self.arg_name} value and set '
f'to {self._GetTextFormatOfEmptyValue(self._empty_value)}.'),
)
def ApplySetFlag(self, output, set_val):
if set_val:
return set_val
return output
def ApplyClearFlag(self, output, clear_flag):
if clear_flag:
return self._empty_value
return output
class UpdateMultitypeResourceArgumentGenerator(UpdateResourceArgumentGenerator):
"""Update flag generator for multitype resource args."""
@property
def _empty_value(self):
return None
@property
def set_arg(self):
return self._CreateResourceFlag(
group_help='Set {} to new value.'.format(self.arg_name))
@property
def clear_arg(self):
return self._CreateFlag(
self.arg_name,
flag_prefix=update_args.Prefix.CLEAR,
action='store_true',
help_text=(
f'Clear {self.arg_name} value and set '
f'to {self._GetTextFormatOfEmptyValue(self._empty_value)}.'),
)
def ApplySetFlag(self, output, set_val):
if result := (set_val and set_val.result):
return result
else:
return output
def ApplyClearFlag(self, output, clear_flag):
if clear_flag:
return self._empty_value
else:
return output
class UpdateListResourceArgumentGenerator(UpdateResourceArgumentGenerator):
"""Update flag generator for list resource args."""
@property
def _empty_value(self):
return []
@property
def set_arg(self):
return self._CreateResourceFlag(
group_help=f'Set {self.arg_name} to new value.')
@property
def clear_arg(self):
return self._CreateFlag(
self.arg_name,
flag_prefix=update_args.Prefix.CLEAR,
action='store_true',
help_text=(
f'Clear {self.arg_name} value and set '
f'to {self._GetTextFormatOfEmptyValue(self._empty_value)}.'),
)
@property
def update_arg(self):
return self._CreateResourceFlag(
flag_prefix=update_args.Prefix.ADD,
group_help=f'Add new value to {self.arg_name} list.')
@property
def remove_arg(self):
return self._CreateResourceFlag(
flag_prefix=update_args.Prefix.REMOVE,
group_help=f'Remove value from {self.arg_name} list.')
def ApplySetFlag(self, output, set_val):
if set_val:
return set_val
return output
def ApplyClearFlag(self, output, clear_flag):
if clear_flag:
return self._empty_value
return output
def ApplyRemoveFlag(self, existing_val, remove_val):
value = existing_val or self._empty_value
if remove_val:
return [x for x in value if x not in remove_val]
else:
return value
def ApplyUpdateFlag(self, existing_val, update_val):
value = existing_val or self._empty_value
if update_val:
return existing_val + [x for x in update_val if x not in value]
else:
return value
class UpdateMultitypeListResourceArgumentGenerator(
UpdateResourceArgumentGenerator):
"""Update flag generator for multitype list resource args."""
@property
def _empty_value(self):
return []
@property
def set_arg(self):
return self._CreateResourceFlag(
group_help=f'Set {self.arg_name} to new value.')
@property
def clear_arg(self):
return self._CreateFlag(
self.arg_name,
flag_prefix=update_args.Prefix.CLEAR,
action='store_true',
help_text=(
f'Clear {self.arg_name} value and set '
f'to {self._GetTextFormatOfEmptyValue(self._empty_value)}.'),
)
@property
def update_arg(self):
return self._CreateResourceFlag(
flag_prefix=update_args.Prefix.ADD,
group_help=f'Add new value to {self.arg_name} list.')
@property
def remove_arg(self):
return self._CreateResourceFlag(
flag_prefix=update_args.Prefix.REMOVE,
group_help=f'Remove value from {self.arg_name} list.')
def ApplySetFlag(self, output, set_val):
resource_list = [val.result for val in set_val if val.result]
if resource_list:
return resource_list
else:
return output
def ApplyClearFlag(self, output, clear_flag):
if clear_flag:
return self._empty_value
else:
return output
def ApplyRemoveFlag(self, existing_val, remove_val):
value = existing_val or self._empty_value
if remove_resources := set(val.result for val in remove_val if val.result):
return [x for x in value if x not in remove_resources]
else:
return value
def ApplyUpdateFlag(self, existing_val, update_val):
value = existing_val or self._empty_value
if update_val:
return value + [
x.result for x in update_val if x.result not in value]
else:
return value

View File

@@ -0,0 +1,289 @@
# -*- coding: utf-8 -*- #
# Copyright 2017 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.
"""Data objects to support the yaml command schema."""
from __future__ import absolute_import
from __future__ import division
from __future__ import unicode_literals
from enum import Enum
from googlecloudsdk.calliope import base
from googlecloudsdk.command_lib.util.apis import yaml_command_schema_util as util
class CommandData(object):
"""A general holder object for yaml command schema."""
def __init__(self, name, data):
# pylint: disable=g-import-not-at-top
from googlecloudsdk.command_lib.util.apis import yaml_arg_schema
# pylint: enable=g-import-not-at-top
self.hidden = data.get('hidden', False)
self.auto_generated = data.get('auto_generated', False)
self.universe_compatible = data.get('universe_compatible', None)
self.release_tracks = [
base.ReleaseTrack.FromId(i) for i in data.get('release_tracks', [])
]
self.command_type = CommandType.ForName(data.get('command_type', name))
self.help_text = data['help_text']
self.request = None
self.response = None
request_data = None
if CommandType.HasRequestMethod(self.command_type):
request_data = data.get('request')
self.request = Request(self.command_type, request_data)
self.response = Response(data.get('response', {}))
async_data = data.get('async')
iam_data = data.get('iam')
update_data = data.get('update')
generic_data = data.get('generic')
import_data = data.get('import')
if self.command_type == CommandType.WAIT and not async_data:
raise util.InvalidSchemaError(
'Wait commands must include an async section.')
self.async_ = Async(async_data) if async_data else None
self.iam = IamData(iam_data) if iam_data else None
self.arguments = yaml_arg_schema.Arguments(data['arguments'], request_data)
self.input = Input(self.command_type, data.get('input', {}))
self.output = Output(data.get('output', {}))
self.update = UpdateData(update_data) if update_data else None
self.generic = GenericData(generic_data) if generic_data else None
self.import_ = ImportData(import_data, request_data,
async_data) if import_data else None
self.deprecated_data = data.get('deprecate')
class CommandType(Enum):
"""An enum for the types of commands the generator supports."""
DESCRIBE = 1
LIST = 2
DELETE = 3
IMPORT = 4
EXPORT = 5
CONFIG_EXPORT = 6
CREATE = 7
WAIT = 8
UPDATE = 9
GET_IAM_POLICY = 10
SET_IAM_POLICY = 11
ADD_IAM_POLICY_BINDING = 12
REMOVE_IAM_POLICY_BINDING = 13
GENERIC = 14
@property
def default_method(self):
"""Returns the default API method name for this type of command."""
return _DEFAULT_METHODS_BY_COMMAND_TYPE.get(self)
@classmethod
def ForName(cls, name):
try:
return CommandType[name.upper()]
except KeyError:
return CommandType.GENERIC
@classmethod
def HasRequestMethod(cls, name):
methodless_commands = {cls.CONFIG_EXPORT}
return name not in methodless_commands
_DEFAULT_METHODS_BY_COMMAND_TYPE = {
CommandType.DESCRIBE: 'get',
CommandType.LIST: 'list',
CommandType.DELETE: 'delete',
CommandType.IMPORT: 'patch',
CommandType.EXPORT: 'get',
CommandType.CONFIG_EXPORT: 'config_export',
CommandType.CREATE: 'create',
CommandType.WAIT: 'get',
CommandType.UPDATE: 'patch',
# IAM support currently implemented as subcommands
CommandType.GET_IAM_POLICY: 'getIamPolicy',
CommandType.SET_IAM_POLICY: 'setIamPolicy',
# For add/remove-iam-policy-binding commands, the actual API method to
# modify the iam support is 'setIamPolicy'.
CommandType.ADD_IAM_POLICY_BINDING: 'setIamPolicy',
CommandType.REMOVE_IAM_POLICY_BINDING: 'setIamPolicy',
# Generic commands are those that don't extend a specific calliope command
# base class.
CommandType.GENERIC: None,
}
class Request(object):
"""A holder object for api request information specified in yaml command."""
def __init__(self, command_type, data):
collection = data.get('collection')
if isinstance(collection, list):
self.collections = collection
else:
self.collections = [collection]
self.disable_resource_check = data.get('disable_resource_check')
self.display_resource_type = data.get('display_resource_type')
self.api_version = data.get('api_version')
self.method = data.get('method', command_type.default_method)
if not self.method:
raise util.InvalidSchemaError(
'request.method was not specified and there is no default for this '
'command type.')
self.disable_pagination = data.get('disable_pagination', False)
self.static_fields = data.get('static_fields', {})
self.modify_request_hooks = [
util.Hook.FromPath(p) for p in data.get('modify_request_hooks', [])]
self.create_request_hook = util.Hook.FromData(data, 'create_request_hook')
self.modify_method_hook = util.Hook.FromData(data, 'modify_method_hook')
self.issue_request_hook = util.Hook.FromData(data, 'issue_request_hook')
class Response(object):
"""A holder object for api response information specified in yaml command."""
def __init__(self, data):
self.id_field = data.get('id_field')
self.result_attribute = data.get('result_attribute')
self.error = ResponseError(data['error']) if 'error' in data else None
self.modify_response_hooks = [
util.Hook.FromPath(p) for p in data.get('modify_response_hooks', [])]
class ResponseError(object):
def __init__(self, data):
self.field = data.get('field', 'error')
self.code = data.get('code')
self.message = data.get('message')
class Async(object):
"""A holder object for api async information specified in yaml command."""
def __init__(self, data):
collection = data.get('collection')
if isinstance(collection, list):
self.collections = collection
else:
self.collections = [collection]
self.api_version = data.get('api_version')
self.method = data.get('method', 'get')
self.request_issued_message = data.get('request_issued_message')
self.response_name_field = data.get('response_name_field', 'name')
self.extract_resource_result = data.get('extract_resource_result', True)
self.operation_get_method_params = data.get(
'operation_get_method_params', {})
self.result_attribute = data.get('result_attribute')
self.state = AsyncStateField(data.get('state', {}))
self.error = AsyncErrorField(data.get('error', {}))
self.modify_request_hooks = [
util.Hook.FromPath(p) for p in data.get('modify_request_hooks', [])]
class IamData(object):
"""A holder object for IAM related information specified in yaml command."""
def __init__(self, data):
self.message_type_overrides = data.get('message_type_overrides', {})
self.set_iam_policy_request_path = data.get('set_iam_policy_request_path')
self.enable_condition = data.get('enable_condition', False)
self.hide_special_member_types = data.get('hide_special_member_types',
False)
self.policy_version = data.get('policy_version', None)
self.get_iam_policy_version_path = data.get(
'get_iam_policy_version_path',
'options.requestedPolicyVersion')
class AsyncStateField(object):
def __init__(self, data):
self.field = data.get('field', 'done')
self.success_values = data.get('success_values', [True])
self.error_values = data.get('error_values', [])
class AsyncErrorField(object):
def __init__(self, data):
self.field = data.get('field', 'error')
class Input(object):
def __init__(self, command_type, data):
self.confirmation_prompt = data.get('confirmation_prompt')
self.default_continue = data.get('default_continue', True)
if not self.confirmation_prompt and command_type is CommandType.DELETE:
self.confirmation_prompt = (
'You are about to delete {{{}}} [{{{}}}]'.format(
util.RESOURCE_TYPE_FORMAT_KEY, util.NAME_FORMAT_KEY))
class Output(object):
def __init__(self, data):
self.format = data.get('format')
self.flatten = data.get('flatten')
class UpdateData(object):
"""A holder object for yaml update command."""
def __init__(self, data):
self.mask_field = data.get('mask_field', None)
self.read_modify_update = data.get('read_modify_update', False)
self.disable_auto_field_mask = data.get('disable_auto_field_mask', False)
class GenericData(object):
"""A holder object for generic commands."""
def __init__(self, data):
self.disable_paging_flags = data.get('disable_paging_flags', False)
class ImportData(object):
"""A holder object for yaml import command."""
def __init__(self, data, orig_request, orig_async):
self.abort_if_equivalent = data.get('abort_if_equivalent', False)
self.create_if_not_exists = data.get('create_if_not_exists', False)
self.no_create_async = data.get('no_create_async', False)
# Populate create request data if any is specified.
create_request = data.get('create_request', None)
if create_request:
# Use original request data while overwriting specified fields.
overlayed_create_request = self._OverlayData(create_request, orig_request)
self.create_request = Request(CommandType.CREATE,
overlayed_create_request)
else:
self.create_request = None
# Populate create async data if any is specified.
create_async = data.get('create_async', None)
if create_async:
overlayed_create_async = self._OverlayData(create_async, orig_async)
self.create_async = Async(overlayed_create_async)
else:
self.create_async = None
def _OverlayData(self, create_data, orig_data):
"""Uses data from the original configuration unless explicitly defined."""
for k, v in orig_data.items():
create_data[k] = create_data.get(k) or v
return create_data

View File

@@ -0,0 +1,953 @@
# Copyright 2017 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.
title: command
description: The specification for a full calliope command
definitions:
python_hook:
type: string
# A Python hook points to a module, class, an optionally a sub attribute
# or function. The thing it points to will be called by the framework
# and must have the function signature required by that particular hook.
# If arguments are given (:foo=bar,baz=stuff), The function will first be
# called with those arguments, and the return value will be used as the
# callable hook.
# Examples:
# googlecloudsdk.module:classname
# googlecloudsdk.module:class.attribute
# googlecloudsdk.module:class.function:foo=bar,baz=stuff
pattern: "\\w+(\\.\\w+)+:\\w+(\\.\\w+)*(:\\w+=\\w+(,\\w+=\\w+)*)?"
yaml_reference:
type: string
# Examples:
# googlecloudsdk.module:attribute
# googlecloudsdk.module:attribute.attribute
pattern: "\\w+(\\.\\w+)+:\\w+(\\.\\w+)*"
attribute:
type: string
# Examples: a, a.b, a.c.b
pattern: "\\w+(\\.\\w+)*"
property:
type: string
# Examples: core/project, compute/zone
pattern: "\\w+/\\w+"
argparse_type:
# Something to be used as the type argument for an argparse argument.
oneOf:
# One of the builtin Python types.
- enum: [str, int, long, float, bool, bytes]
# The module path to a function used as the 'type' for the argument.
# The function takes a single parameter which is the parsed string value
# of the argument and returns the converted parsed value.
- $ref: "#/definitions/python_hook"
# Support for arg_parsers.arg_list
- enum: [arg_list, arg_object, arg_json, file_type]
choices:
# A list of valid choices for an argument.
type: array
items:
type: object
additionalProperties: false
required: [arg_value]
properties:
# The choice as it appears on the command line
# (should be lower-kebab-case).
arg_value: {type: [string, number, boolean]}
# The string representation of the API enum value that will be used
# when this choice is selected. Default is derived from arg_value by
# converting to upper case and replacing hyphens with underscores.
enum_value: {type: string}
# The help text to show next to the choice on the man page.
help_text: {type: string}
# Whether or not the choice should be hidden from help text.
hidden: {type: boolean}
flag_spec:
type: array
items:
type: object
additionalProperties: false
properties:
api_field: {type: string}
json_name: {type: string}
help_text: {type: string}
hidden: {type: boolean}
# TODO(b/291130667): enable mutex for arg_object
# one_of_index is to determine which items are in a mutex group.
# Not available for public use yet.
one_of_index: {type: number}
# TODO(b/304805943): enable resource pattern
# resource_pattern is a regex pattern used to verify resource strings.
# Not available for public use yet.
resource_pattern: {type: string}
type: {$ref: "#/definitions/argparse_type"}
spec: {$ref: "#/definitions/flag_spec"}
fallthrough:
required: [hook, hint]
additionalProperties: false
properties:
# A zero-argument Python function that can provide a fallback value for
# the attribute.
hook: {$ref: "#/definitions/python_hook"}
# Text to display to users if no fallthroughs are set (should be
# imperative, to follow "The attribute can be set in the following ways").
hint: {type: string}
value_fallthrough:
required: [value]
additionalProperties: false
properties:
# A fixed value to use for fallthrough.
value: {type: string}
# Text to display to users if no fallthroughs are set (should be
# imperative, to follow "The attribute can be set in the following ways").
hint: {type: string}
arg_fallthrough:
required: [arg_name]
additionalProperties: false
properties:
# Name of a flag or positional whose value can be used as a default.
arg_name: {type: string}
# Whether the other arg is positional. Defaults to False.
is_positional: {type: boolean}
resource_attribute:
# An individual resource arg attribute.
required: [parameter_name, attribute_name, help]
additionalProperties: false
properties:
# The API parameter name that this attribute maps to.
parameter_name: {type: string}
# The name as it should appear on the command line.
attribute_name: {type: string}
# The help text for this attribute.
help: {type: string}
# A property to act as the fallthrough for this attribute when it is not
# provided on the command line.
property: {$ref: "#definitions/property"}
# A list of Python hooks that will act as fallthroughs if the attribute is
# not provided on the command line.
fallthroughs:
type: array
items:
oneOf:
- $ref: "#/definitions/fallthrough"
- $ref: "#/definitions/value_fallthrough"
# The module path to a class that implements remote completion
# for this resource.
completer: {$ref: "#/definitions/python_hook"}
# The ID field of the return value in the response for completions.
completion_id_field: {type: string}
# Field names to use as static field values in any request to
# complete this resource.
completion_request_params:
type: array
items:
type: object
additionalProperties: false
properties:
fieldName: {type: string}
value: {type: string}
resource_spec:
type: object
additionalProperties: false
required: [name, collection, attributes]
properties:
# The name of the resource argument, used for messaging.
name: {type: string}
# The pluralized name if default rules won't work.
plural_name: {type: string}
# The full collection name of the resource argument.
collection: {type: string}
# The api version this spec refers to. If empty, it applies to any
# version.
api_version: {type: string}
# TODO(b/274890004): remove request_id_field from resource_spec
# The field in the create request that should hold the name of the
# resource being created (since create requests don't have the resource
# id as an API parameter.
request_id_field: {$ref: "#/definitions/attribute"}
# The set of attributes that map to the API parameters for this resource's
# collection.
attributes:
type: array
items: {$ref: "#/definitions/resource_attribute"}
# If False, allows the resource to create automatic completers for
# each argument. True by default.
disable_auto_completers: {type: boolean}
multitype_resource_spec:
type: object
additionalProperties: false
required: [name, resources]
properties:
# The name of the resource argument, used for messaging.
name: {type: string}
# The pluralized name if default rules won't work.
plural_name: {type: string}
# All resource specs contained in this one.
# The parsed result will have a "type_" attribute whose name is either the
# name of the resource spec (if all sub-resource specs are unique) or the
# collection name otherwise (such as 'projects.zones.instances'). Note:
# All resources used in this spec must have names.
resources:
type: array
items: {$ref: "#/definitions/resource_spec"}
argument:
oneOf:
- $ref: "#/definitions/arg"
- $ref: "#/definitions/resource_arg"
- $ref: "#/definitions/arg_group"
resource_arg:
description: Describes how to generate the resource arg and its flags
type: object
additionalProperties: false
required: [help_text, resource_spec]
properties:
# The help text for the overall resource arg group.
help_text: {type: string}
# Set to False to force the anchor resource argument to be a flag
# even though it would normally be a positional. This should normally
# not be required for well formed API methods.
is_positional: {type: boolean}
# Set to True if the resource spec is for the parent of the request
# collection instead of for the collection itself. This allows for
# create commands that do not require a name for the new resource.
# Set the `resource_type` property on `request` to include the
# resource type in log messages.
is_parent_resource: {type: boolean}
# Set to True if this resource is the resource being operated on ie
# being described, created, updated, etc. This helps determine which
# resource arg should be validated against the method and included
# in the display name, among other things. Only one resource arg can
# set is_primary_resource to true. If command does not set any resource
# args' is_primary_resource to true, then gcloud will try to assume
# which resource is primary based off of request.method.collection
is_primary_resource: {type: boolean}
# The name to use for the resource arg. If set this is the name that
# will be used for the resource arg in the command. If not set,
# command will use the name from the spec.
arg_name: {type: string}
# List of resource arg attributes to not generate flags for. These
# attributes will need to have fallthroughs in the resource arg spec
# in order to be populated.
removed_flags:
type: array
items: {type: string}
# The field in the create request that should hold the name of the
# resource being created (since create requests don't have the resource
# id as an API parameter.
request_id_field: {$ref: "#/definitions/attribute"}
# a map of attribute names to a list of fallthroughs *specific to
# this command* that will be used if the attribute is not directly
# provided on the command line. Anything in this map takes precedence
# over what's specified in the resource spec by default.
command_level_fallthroughs:
type: object
patternProperties:
.*:
type: array
items: {$ref: "#/definitions/arg_fallthrough"}
# The resource arg specification itself. This corresponds to the
# ResourceSpec and ResourceParameterAttributeConfig objects and does
# not include anything that would go in a presentation spec. It is
# expected that this entire section be imported from a resources.yaml
# file in command_lib.
# (ex !REF googlecloudsdk.command_lib.pubsub.resources:topic)
resource_spec:
oneOf:
# A regular resource arg
- $ref: "#/definitions/resource_spec"
# A multitype resource arg
- $ref: "#/definitions/multitype_resource_spec"
# A mapping of method parameter to format string that can include resource reference params(s)
# (or '__relative_name__' to use the full resource name or '__resource_id__' for resource id)
# for references or APIs that use non-standard naming. For example, if a resource ref has a
# project, location, and instance attribute, but the request URI has the structure
# /projects/{project}/regions/{region}/instances/{instance}, you would use
# resource_method_params:
# region: '{location}'
# To map the resource attribute to the correct method parameter.
resource_method_params:
type: object
patternProperties:
.*: {type: string}
# False if the resource reference should not be auto-parsed into the
# request message. NOTE: if you mark this False, you need to add a hook
# that handles the result of parsing the resource argument for the
# command. For example, if the resource is multi-type, you may want to
# define an modify_request_hook that modifies the request based on
# the parsed type of the resource.
parse_resource_into_request: {type: boolean}
# Use ref.RelativeName() if true, ref.Name() otherwise. Default True.
use_relative_name: {type: boolean}
# If true, will disable validating that the resource arg schema matches the
# request method schema. This is useful for API's that have multiple services
# (and multiple resource collections) that use the same actual resource objects.
override_resource_collection: {type: boolean}
# Whether the flag is required or not.
required: {type: boolean}
# Whether or not the resource arg is plural and should be generated as a list.
repeated: {type: boolean}
# Whether to add clearable update arguments
clearable: {type: boolean}
arg:
required: [help_text]
additionalProperties: false
properties:
# The name of the API field the value of this argument goes into.
# At least one of [api_field, arg_name] must be given.
api_field: {type: string}
# Disables yaml_command_test check for unused arguments. Some arguments get used
# in such a way that static analysis can't catch it.
disable_unused_arg_check: {type: boolean}
# The help text for this individual flag.
help_text: {type: string}
# The name of the argument as it should appear in the CLI. If
# not given it will match the API field name. This does not
# include '--' even if it will be a flag.
# At least one of [api_field, arg_name] must be given.
arg_name: {type: string}
# The name of the argument as it should appear in the JSON. If
# not given it will match the API field name.
json_name: {type: string}
# The module path to a class that implements remote completion
# for this argument.
completer: {$ref: "#/definitions/python_hook"}
# True if this should be forced to be a positional argument
# instead of a flag.
is_positional: {type: boolean}
# The argparse type to use for this argument. See inline options for
# this value.
type:
oneOf:
# A builtin type or hook to a type function.
- $ref: "#/definitions/argparse_type"
# First class support for making this an ArgDict.
- type: object
additionalProperties: false
properties:
arg_dict:
type: object
additionalProperties: false
required: [spec]
properties:
# Set to True to make the ArgDict correspond to a repeated
# key/value pair. For this to work, the spec must contain
# exactly two items. If False, the keys in the dict will be
# the arg names, and the values will be the values for those
# fields. The arg is repeated by specifying the flag multiple
# times.
flatten: {type: boolean}
spec:
type: array
items:
type: object
additionalProperties: false
required: [api_field]
properties:
api_field: {type: string}
# Only applicable for non-flat dicts.
arg_name: {type: string}
type: {$ref: "#/definitions/argparse_type"}
# Only applicable for non-flat dicts.
required: {type: boolean}
choices: {$ref: "#/definitions/choices"}
# custom spec for arg_object type
spec: {$ref: "#/definitions/flag_spec"}
# The argparse action to use for this argument. See inline options for
# this value.
action:
oneOf:
# One of the builtin argparse actions.
- enum: [store, store_true, store_true_false, append]
# The module path to a class that implements argparse's Action class.
- $ref: "#/definitions/python_hook"
# Mark this flag as being deprecated. All the sub elements here
# are passed as kwargs to the calliope.actions.DeprecationAction
# action.
- type: object
additionalProperties: false
properties:
deprecated:
type: object
additionalProperties: false
properties:
# If False, warning message will be printed, otherwise, error message will be
# printed.
removed: {type: boolean}
# Warning message. 'flag_name' template will be replaced with value of flag_name
# parameter.
warn: {type: string}
# Error message. 'flag_name' template will be replaced with value of flag_name
# parameter.
error: {type: string}
# Can be set to False if you want to accept a single value even though the
# API field is repeated. If the API field is not repeated, this attribute
# has no effect.
repeated: {type: boolean}
# The metavar for the generated argument. This will be generated
# automatically if not provided.
metavar: {type: string}
# Overrides the choices that are generated for the argument.
choices: {$ref: "#/definitions/choices"}
# TODO(b/80307139): Combine default and fallback.
# The default value of the argument when not specified. Mutually exclusive
# with fallback. If provided and None, the default of the argument is
# explicitly set to None. If not provided, no default is set on the
# argument.
default: {type: [string, number, boolean, array, 'null']}
# The module path to a function to call (with no arguments) if no value is
# provided; the result of this function will be used for this argument
# (like default, but with a computed value). Mutually exclusive with
# default.
fallback: {$ref: "#/definitions/python_hook"}
# The module path to a function to process the parsed value
# before inserting it into the request message. It takes a
# single value which is the parsed argument's value and returns
# the value that should be inserted into the request.
processor: {$ref: "#/definitions/python_hook"}
# Whether the flag is required or not.
required: {type: boolean}
# Whether the flag is hidden or not.
hidden: {type: boolean}
# TODO(b/280653078): Not ready for public use yet
# Whether to add update flags for update commands.
# Must be used with read_modify_update
clearable: {type: boolean}
arg_group:
type: object
additionalProperties: false
properties:
group:
required: [params]
type: object
additionalProperties: false
properties:
# The help text for the group.
help_text: {type: string}
# Whether the group is required or not.
required: {type: boolean}
# Whether the group is mutually exclusive or not.
mutex: {type: boolean}
# Whether the group is hidden or not.
hidden: {type: boolean}
# Whether the group has an arg_object flag or not in addition to the params.
settable: {type: boolean}
# Whether the group has a clearable flag or not in addition to the params.
clearable: {type: boolean}
# Api field associated with the group.
# Only needed when clearable or settable flags are set to true.
api_field: {type: string}
# The name of the argument group as it should appear in the CLI.
# Only needed when clearable or settable flags are set to true.
arg_name: {type: string}
# How the argument group should be represented in JSON.
json_name: {type: string}
# The group arguments.
params:
type: array
items: {$ref: "#/definitions/argument"}
minItems: 1
type: object
additionalProperties: false
required: [help_text, arguments]
properties:
# Determines whether the command is visible in help text or not.
hidden: {type: boolean}
# Determines whether the command is auto generated or not.
auto_generated: {type: boolean}
# Determines whether the command is available in univserse or not.
universe_compatible: {type: boolean}
# Determines which release tracks this command implementation will apply to.
release_tracks:
type: array
items: {enum: [ALPHA, BETA, GA]}
# The type of command to generate. This is inferred based on the name
# of the command file (convert to uppercase and remove .yaml extension).
command_type:
enum:
- DESCRIBE
- LIST
- DELETE
- CREATE
- UPDATE
- WAIT
- CONFIG_EXPORT
- GET_IAM_POLICY
- SET_IAM_POLICY
- GENERIC
# Corresponds to the detailed_help attribute on command classes. Typically
# looks something like this (brief and description are required):
# help_text:
# brief: Delete a Compute Engine virtual machine.
# description: |
# This command stops, and deletes the given virtual machine instance. Any
# boot disks associated with this instance are also deleted.
# examples: |
# To delete an instance:
#
# $ {command} INSTANCE_NAME --zone ZONE
help_text:
type: object
additionalProperties: false
required: [brief, description]
patternProperties:
brief: {type: string}
description: {type: string}
examples: {type: string}
.*: {type: string}
# Determines whether the command is deprecated.
# If is_removed is false, a warning will be logged when *command* is run,
# otherwise an *exception* will be thrown containing error message.
# Command help output will be modified to include warning/error message
# depending on value of is_removed.
# Command help text will automatically hidden from the reference
# documentation if is_removed is True.
deprecate:
type: object
additionalProperties: false
required: [is_removed]
properties:
is_removed: {type: boolean}
warning: {type: string}
error: {type: string}
# Attributes about the API request the command will make.
request: &request
description: Describes information about the API that this command calls
type: object
additionalProperties: false
required: [collection]
oneOf:
- required: [modify_request_hooks]
- required: [create_request_hook]
- required: [issue_request_hook]
- allOf:
- not: {required: [modify_request_hooks]}
- not: {required: [create_request_hook]}
- not: {required: [issue_request_hook]}
properties:
# The full collection name of the resource the command operates on.
# A list of collections is used whenever the request is a multitype ie
# a resource with different parent paths.
collection: {type: [string, array]}
# If true, will disable validating that a resource arg is specified if
# the collection requires a params from a resource.
disable_resource_check: {type: boolean}
# The type of the resource the command operates on to be used for logging.
display_resource_type: {type: string}
# The API version to use, defaults to API set as gcloud default.
api_version: {type: string}
# The name of the method to call to perform the operation. Optional if
# your command is one of the supported command types (not Generic).
method: {type: string}
# Used for methods that return paginated results. If set to True, makes a single api
# request rather than utilizing YieldFromList method. YieldFromList method makes
# continuous api request until the limit is reached or all results are returned.
# Defaults to False
disable_pagination: {type: boolean}
# The module path to a function to call to modify the API method
# to call to perform the operation. The provided or default `method`
# will be used to parse the arguments, then this function will be called
# with two arguments: a resource ref to the parsed resource and the parsed
# args namespace. The returned string will be the method used to create
# and issue the request.
modify_method_hook: {$ref: "#/definitions/python_hook"}
# TODO(b/272076207): Remove resource_method_params when surfaces are updated
# A mapping of method parameter to format string that can include resource reference params(s)
# (or '__relative_name__' to use the full resource name or '__resource_id__' for resource id)
# for references or APIs that use non-standard naming. For example, if a resource ref has a
# project, location, and instance attribute, but the request URI has the structure
# /projects/{project}/regions/{region}/instances/{instance}, you would use
# resource_method_params:
# region: '{location}'
# To map the resource attribute to the correct method parameter.
resource_method_params:
type: object
patternProperties:
.*: {type: string}
# TODO(b/272076207): Remove parse_resource_into_request when surfaces are updated
# False if the resource reference should not be auto-parsed into the
# request message. NOTE: if you mark this False, you need to add a hook
# that handles the result of parsing the resource argument for the
# command. For example, if the resource is multi-type, you may want to
# define an modify_request_hook that modifies the request based on
# the parsed type of the resource.
parse_resource_into_request: {type: boolean}
# A mapping of request field names to static values to insert into the
# request. Specifying simple values here accomplishes the same as a
# a custom request generator, but is simpler. For example, if you had an
# an API that could work in several modes, but we only use one in the
# command, you could set:
# static_fields:
# requests.features.type: TYPE1
# To always populate that field with the correct mode.
static_fields:
type: object
patternProperties:
.*: {}
# The module path to a function to call to modify the API request
# before it is invoked. All registered arguments will be processed
# into the request, before this is called. This is a function
# that takes 3 arguments: a resource ref to the parsed resource,
# the parsed args namespace, and the created request. It must return
# an apitools request message that will be passed to the API method
# defined in this section.
modify_request_hooks:
type: array
items: {$ref: "#/definitions/python_hook"}
# The module path to a function to call to create the API request
# rather than having it done automatically. This is a function
# that takes 2 arguments: a resource ref to the parsed resource, and
# the parsed args namespace. It must return an apitools request
# message that will be passed to the API method defined in this
# section.
create_request_hook: {$ref: "#/definitions/python_hook"}
# The module path to a function to call to issue an API request
# rather than having it done automatically. This is a function
# that takes 2 arguments: a resource ref to the parsed resource, and
# the parsed args namespace. It should create a request, make the
# API call, and return the response.
issue_request_hook: {$ref: "#/definitions/python_hook"}
# TODO(b/272076207): Remove use_relative_name when surfaces are updated
# Use ref.RelativeName() if true, ref.Name() otherwise. Default True.
use_relative_name: {type: boolean}
# Configuration for handling the API response.
response:
type: object
additionalProperties: false
properties:
# TODO(b/80311963): Support all resource paths here.
# The field in the response that is the id of the resource (just the
# name, not the relative name or URI). This is used to construct a
# URI for resources out of the result of a list command. If not
# provided, there won't be a --uri flag on the list command.
id_field: {type: string, pattern: "\\w+"}
# The attribute of the API response to return (instead of the entire
# response.
result_attribute: {$ref: "#/definitions/attribute"}
# If given, the response will be searched for error information and an
# exception raised if found. This is useful for batch request methods
# that return 200 even though something may have failed.
error:
type: object
additionalProperties: false
properties:
# The dotted path of the field whose presence indicates that an
# error has occured. This is 'error' by default if the error section
# is declared.
field: {$ref: "#/definitions/attribute"}
# If the error message is found, extract the error code from this
# field within that message.
code: {$ref: "#/definitions/attribute"}
# If the error message is found, extract the error message from this
# field within that message.
message: {$ref: "#/definitions/attribute"}
# The list of module paths to functions to call to modify the response
# returned by the API request. These functions take 2 arguments: the API
# response first and the parsed args namespace second. They must return a
# modified response. The functions are called in a chain, one after
# another, after result_attribute and error are processed, but before
# id_field.
modify_response_hooks:
type: array
items: {$ref: "#/definitions/python_hook"}
# If present, indicates that this API method uses operations and the --async
# flag will be set up.
async: &async
description: Describes how to poll and report the result of the operation
type: object
additionalProperties: false
required: [collection]
properties:
# The full collection name of the operation collection for this API.
# A list of collections is used whenever the request is a multitype ie
# a resource with different parent paths.
collection: {type: [string, array]}
# The API version to use, defaults to API version specified in the request
# section.
api_version: {type: string}
# The API method to call to get the operation ('get' by default).
method: {type: string}
# The message to print when the request is issued. The default depends on
# the command type.
request_issued_message: {type: string}
# The field in the operation message that corresponds to the operation's
# full name ('name' by default). Not used by Wait commands.
response_name_field: {type: string}
# Override whether the command should get the resulting resource after
# the operation is done, or just return the operation result. This is
# True by default (except for Delete and Wait commands).
extract_resource_result: {type: boolean}
# A mapping of method parameter to operation reference field for
# references or APIs that use non-standard naming.
operation_get_method_params:
type: object
patternProperties:
.*: {type: string}
# The attribute of the result to return from polling. If
# extract_resource_result is False, this is an attribute on the
# operation, if True it is on the resource itself.
result_attribute: {$ref: "#/definitions/attribute"}
# The module path to a function to call to modify the API request
# before it is invoked. All registered arguments will be processed
# into the request, before this is called. This is a function
# that takes 3 arguments: a resource ref to the parsed resource,
# the parsed args namespace, and the created request. It must return
# an apitools request message that will be passed to the API method
# defined in this section.
modify_request_hooks:
type: array
items: {$ref: "#/definitions/python_hook"}
state:
type: object
additionalProperties: false
required: [field, success_values]
properties:
# The field to check for status. Polling continues until it matches
# something in success_values or error_values. ('done' by default).
field: {type: string}
# Values that indicate that the operation is done and finished
# successfully. ('True' by default).
success_values: {type: array, items: {type: [boolean, string]}}
# Values that indicate that the operation is done but finished
# unsuccessfully. (Nothing by default).
error_values: {type: array, items: {type: [boolean, string]}}
error:
type: object
additionalProperties: false
required: [field]
properties:
# If this field is set when the operation completes, it will be
# used to generate an error message and polling stops. ('error' by
# default).
field: {type: string}
# Enumerates the API fields (other than those specifying the resource to act
# on) that arguments should be generated.
arguments:
type: object
additionalProperties: false
properties:
# The module path to a function to call to generate extra arguments
# that for whatever reason cannot be defined here. It is a function that
# takes no arguments and returns a list of calliope.base.Action
# objects that will be added to the parser. These arguments will not
# be processed automatically so you will need to implement
# request.modify_request_hooks to insert something into the request based
# on these arguments.
additional_arguments_hook: {$ref: "#/definitions/python_hook"}
# TODO(b/272076207): Remove resource when surfaces are updated
# This section declares how to generate the arguments and flags that
# correspond to the resource being operated on by this command.
resource:
description: Describes how to generate the resource arg and its flags
type: object
additionalProperties: false
required: [help_text, spec]
properties:
# The help text for the overall resource arg group.
help_text: {type: string}
# Set to False to force the anchor resource argument to be a flag
# even though it would normally be a positional. This should normally
# not be required for well formed API methods.
is_positional: {type: boolean}
# Set to True if the resource spec is for the parent of the request
# collection instead of for the collection itself. This allows for
# create commands that do not require a name for the new resource.
# Set the `resource_type` property on `request` to include the
# resource type in log messages.
is_parent_resource: {type: boolean}
# The name to use for the resource arg. If set this is the name that
# will be used for the resource arg in the command. If not set,
# command will use the name from the spec.
arg_name: {type: string}
# List of resource arg attributes to not generate flags for. These
# attributes will need to have fallthroughs in the resource arg spec
# in order to be populated.
removed_flags:
type: array
items: {type: string}
# a map of attribute names to a list of fallthroughs *specific to
# this command* that will be used if the attribute is not directly
# provided on the command line. Anything in this map takes precedence
# over what's specified in the resource spec by default.
command_level_fallthroughs:
type: object
patternProperties:
.*:
type: array
items: {$ref: "#/definitions/arg_fallthrough"}
# The resource arg specification itself. This corresponds to the
# ResourceSpec and ResourceParameterAttributeConfig objects and does
# not include anything that would go in a presentation spec. It is
# expected that this entire section be imported from a resources.yaml
# file in command_lib.
# (ex !REF googlecloudsdk.command_lib.pubsub.resources:topic)
spec:
oneOf:
# A regular resource arg
- $ref: "#/definitions/resource_spec"
# A multitype resource arg
- $ref: "#/definitions/multitype_resource_spec"
# A hook to get the display name of the parsed resource. Used for
# logging. Takes two arguments: the resource ref and a parsed args
# namespace.
# Example:
# def DisplayNameHook(resource_ref, args):
# if args.CONCEPTS.xyz.Parse().type_.name == 'parentType':
# return 'name not specified'
# return resource_ref.Name()
display_name_hook: {$ref: "#/definitions/python_hook"}
# If true, will disable validating that the resource arg schema matches the
# request method schema. This is useful for API's that have multiple services
# (and multiple resource collections) that use the same actual resource objects.
override_resource_collection: {type: boolean}
# Enumerates the API fields that need to be set and information
# about each argument to generate.
params:
type: array
items: {$ref: "#/definitions/argument"}
labels:
type: object
additionalProperties: false
description: Handles resource labels for resource create/update (go/api-label)
required: [api_field]
properties:
api_field: {type: string}
# Removes arguments from the parser
exclude:
type: array
items:
type: string
# Information about how to collect input from the user.
input:
type: object
additionalProperties: false
properties:
# An optional confirmation prompt to show before actually performing the
# operation. The string may have named format substitutions in it which
# will be replaced with attributes of the resource being operated on.
confirmation_prompt: {type: string}
default_continue: {type: boolean}
# Information about how to show output to the user.
output:
type: object
additionalProperties: false
properties:
# Corresponds to the default output format setting on the command.
format: {type: string}
# Corresponds to the default output flatten setting on the command.
flatten: {type: array}
# Additional information for handling the IAM Commands.
iam:
type: object
additionalProperties: false
properties:
# Path to the policy/updateMask fields in the SetIamRequest message for
# APIs that use non-standard naming.
# Example: 'setMyAPIIamPolicyRequest'
set_iam_policy_request_path: {type: string}
# A mapping of policy message type names to apitools message types for
# APIs that use non-standard naming.
# Example: policy: MyApiPolicy
message_type_overrides:
type: object
patternProperties:
.*: {type: string}
# The indicator for IAM condition. If True, the command can accept condition
# as part of a IAM policy binding. Default is False.
enable_condition: {type: boolean}
# Whether to hide special member types `allUsers` and `allAuthenticatedUsers` as potential
# values for the `member` flag. Default is False.
hide_special_member_types: {type: boolean}
# Override policy version. Default is None.
policy_version: {type: number}
# Path to the policy_version field for APIs that use non-standard mapping.
get_iam_policy_version_path: {type: string}
# Additional information for handling the update commands.
update:
type: object
additionalProperties: false
properties:
# True when the update command requires an extra get api call.
# False by default.
read_modify_update: {type: boolean}
# Disable the field mask auto generation in the update commands.
# False by default.
disable_auto_field_mask: {type: boolean}
# Additional information for generic commands
generic:
type: object
additionalProperties: false
properties:
# Used for commands where pagination is enabled. When pagination is enabled, list
# flags are automatically added. Setting disable_paging_flags prevents list
# flags from being automatically added. This allows users to opt out of flags or
# add their own flags
disable_paging_flags: {type: boolean}
# Additional information for handling the import commands.
import:
type: object
additionalProperties: false
properties:
# True when the import command should return early if it detects the imported resource
# has no changes from the resource on the service.
abort_if_equivalent: {type: boolean}
# True when the import command should create the resource if it does not already exist
# on the service.
create_if_not_exists: {type: boolean}
# Attributes about the create request the command will make if the resource does not already
# exist on the service. The create request will use all configuration from the parent
# request configuration unless explicitly specified here.
create_request: *request
# Attributes about the asynchronous behavior for the above create command. The create request
# will use all settings from the parent asynchronous configuration unless explicitly
# specified here.
create_async: *async
# If set, the create command will not use operations, and will not use the settings
# form the parent asynchronous configuration.
no_create_async: {type: boolean}

View File

@@ -0,0 +1,82 @@
# -*- 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.
"""Helpers for loading YAML data."""
from __future__ import absolute_import
from __future__ import division
from __future__ import unicode_literals
import re
from googlecloudsdk.core import exceptions
from googlecloudsdk.core import yaml
from googlecloudsdk.core.util import pkg_resources
_RESOURCE_FILE_NAME = 'resources.yaml'
_RESOURCE_FILE_PREFIX = 'googlecloudsdk.command_lib.'
_RESOURCE_PATH_PATTERN = r'^(?P<surface_name>\S+)\.(?P<resource_name>\w+)$'
class Error(exceptions.Error):
"""Base class for errors in this module."""
class InvalidResourcePathError(Error):
"""Raised when a resources.yaml is not found by the given resource_path."""
class YAMLData(object):
"""A general data holder object for data parsed from a YAML file."""
def __init__(self, data):
self._data = data
def GetData(self):
return self._data
class ResourceYAMLData(YAMLData):
"""A data holder object for data parsed from a resources.yaml file."""
@classmethod
def FromPath(cls, resource_path):
"""Constructs a ResourceYAMLData from a standard resource_path.
Args:
resource_path: string, the dotted path of the resources.yaml file, e.g.
iot.device or compute.instance.
Returns:
A ResourceYAMLData object.
Raises:
InvalidResourcePathError: invalid resource_path string.
"""
match = re.search(_RESOURCE_PATH_PATTERN, resource_path)
if not match:
raise InvalidResourcePathError(
'Invalid resource_path: [{}].'.format(resource_path))
surface_name = match.group('surface_name')
resource_name = match.group('resource_name')
# Gets the directory name of the targeted YAML file.
# Example: googlecloudsdk.command_lib.iot.
dir_name = _RESOURCE_FILE_PREFIX + surface_name + '.'
resource_file = pkg_resources.GetResource(dir_name, _RESOURCE_FILE_NAME)
# Loads the data from YAML file.
resource_data = yaml.load(resource_file)[resource_name]
return cls(resource_data)
def GetArgName(self):
return self._data.get('name', None)