File: //lib/google-cloud-sdk/lib/googlecloudsdk/command_lib/util/apis/yaml_command_schema.py
# -*- 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 arg_utils
from googlecloudsdk.command_lib.util.apis import resource_arg_schema
from googlecloudsdk.command_lib.util.apis import yaml_command_schema_util as util
NAME_FORMAT_KEY = '__name__'
REL_NAME_FORMAT_KEY = '__relative_name__'
RESOURCE_TYPE_FORMAT_KEY = '__resource_type__'
class CommandData(object):
"""A general holder object for yaml command schema."""
def __init__(self, name, data):
self.hidden = data.get('hidden', False)
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']
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')
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 = Arguments(data['arguments'])
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.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.
Attributes:
default_method: str, The name of the API method to use by default for this
type of command.
"""
DESCRIBE = 'get'
LIST = 'list'
DELETE = 'delete'
IMPORT = 'patch'
EXPORT = 'get'
CREATE = 'create'
WAIT = 'get'
UPDATE = 'patch'
# IAM support currently implemented as subcommands
GET_IAM_POLICY = 'getIamPolicy'
SET_IAM_POLICY = 'setIamPolicy'
# For add/remove-iam-policy-binding commands, the actual API method to modify
# the iam support is 'setIamPolicy'.
ADD_IAM_POLICY_BINDING = 'setIamPolicy'
REMOVE_IAM_POLICY_BINDING = 'setIamPolicy'
# Generic commands are those that don't extend a specific calliope command
# base class.
GENERIC = None
def __init__(self, default_method):
# Set the value to a unique object so multiple enums can have the same
# default method.
self._value_ = object()
self.default_method = default_method
@classmethod
def ForName(cls, name):
try:
return CommandType[name.upper()]
except KeyError:
return CommandType.GENERIC
class Request(object):
"""A holder object for api request information specified in yaml command."""
def __init__(self, command_type, data):
self.collection = data['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.resource_method_params = data.get('resource_method_params', {})
self.parse_resource_into_request = data.get(
'parse_resource_into_request', True)
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')
self.use_relative_name = data.get('use_relative_name', True)
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):
self.collection = data['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)
resource_get_method = data.get('resource_get_method')
if not self.extract_resource_result and resource_get_method:
raise util.InvalidSchemaError(
'async.resource_get_method was specified but extract_resource_result '
'is False')
self.resource_get_method = resource_get_method or 'get'
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.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 Arguments(object):
"""Everything about cli arguments are registered in this section."""
def __init__(self, data):
self.resource = resource_arg_schema.YAMLConceptArgument.FromData(
data.get('resource'))
self.additional_arguments_hook = util.Hook.FromData(
data, 'additional_arguments_hook')
self.params = [
Argument.FromData(param_data) for param_data in data.get('params', [])]
self.labels = Labels(data.get('labels')) if data.get('labels') else None
self.exclude = data.get('exclude', [])
class Labels(object):
"""Everything about labels of GCP resources."""
def __init__(self, data):
self.api_field = data['api_field']
class Argument(object):
"""Encapsulates data used to generate arguments.
Most of the attributes of this object correspond directly to the schema and
have more complete docs there.
Attributes:
api_field: The name of the field in the request that this argument values
goes.
arg_name: The name of the argument that will be generated. Defaults to the
api_field if not set.
help_text: The help text for the generated argument.
metavar: The metavar for the generated argument. This will be generated
automatically if not provided.
completer: A completer for this argument.
is_positional: Whether to make the argument positional or a flag.
type: The type to use on the argparse argument.
choices: A static map of choice to value the user types.
default: The default for the argument.
fallback: A function to call and use as the default for the argument.
processor: A function to call to process the value of the argument before
inserting it into the request.
required: True to make this a required flag.
hidden: True to make the argument hidden.
action: An override for the argparse action to use for this argument.
repeated: False to accept only one value when the request field is actually
repeated.
generate: False to not generate this argument. This can be used to create
placeholder arg specs for defaults that don't actually need to be
generated.
"""
@classmethod
def FromData(cls, data):
"""Gets the arg definition from the spec data.
Args:
data: The spec data.
Returns:
Argument, the parsed argument.
Raises:
InvalidSchemaError: if the YAML command is malformed.
"""
group = data.get('group')
if group:
return ArgumentGroup.FromData(group)
api_field = data.get('api_field')
arg_name = data.get('arg_name', api_field)
if not arg_name:
raise util.InvalidSchemaError(
'An argument must have at least one of [api_field, arg_name].')
is_positional = data.get('is_positional')
flag_name = arg_name if is_positional else '--' + arg_name
if data.get('default') and data.get('fallback'):
raise util.InvalidSchemaError(
'An argument may have at most one of [default, fallback].')
try:
help_text = data['help_text']
except KeyError:
raise util.InvalidSchemaError('An argument must have help_text.')
choices = data.get('choices')
return cls(
api_field,
arg_name,
help_text,
metavar=data.get('metavar'),
completer=util.Hook.FromData(data, 'completer'),
is_positional=is_positional,
type=util.ParseType(data.get('type')),
choices=[util.Choice(d) for d in choices] if choices else None,
default=data.get('default', arg_utils.UNSPECIFIED),
fallback=util.Hook.FromData(data, 'fallback'),
processor=util.Hook.FromData(data, 'processor'),
required=data.get('required', False),
hidden=data.get('hidden', False),
action=util.ParseAction(data.get('action'), flag_name),
repeated=data.get('repeated'),
)
# pylint:disable=redefined-builtin, type param needs to match the schema.
def __init__(self, api_field=None, arg_name=None, help_text=None,
metavar=None, completer=None, is_positional=None, type=None,
choices=None, default=arg_utils.UNSPECIFIED, fallback=None,
processor=None, required=False, hidden=False, action=None,
repeated=None, generate=True):
self.api_field = api_field
self.arg_name = arg_name
self.help_text = help_text
self.metavar = metavar
self.completer = completer
self.is_positional = is_positional
self.type = type
self.choices = choices
self.default = default
self.fallback = fallback
self.processor = processor
self.required = required
self.hidden = hidden
self.action = action
self.repeated = repeated
self.generate = generate
def Generate(self, message):
"""Generates and returns the base argument.
Args:
message: The API message, None for non-resource args.
Returns:
The base argument.
"""
if self.api_field:
field = arg_utils.GetFieldFromMessage(message, self.api_field)
else:
field = None
return arg_utils.GenerateFlag(field, self)
def Parse(self, message, namespace):
"""Sets the argument message value, if any, from the parsed args.
Args:
message: The API message, None for non-resource args.
namespace: The parsed command line argument namespace.
"""
if self.api_field is None:
return
value = arg_utils.GetFromNamespace(
namespace, self.arg_name, fallback=self.fallback)
if value is None:
return
field = arg_utils.GetFieldFromMessage(message, self.api_field)
value = arg_utils.ConvertValue(
field, value, repeated=self.repeated, processor=self.processor,
choices=util.Choice.ToChoiceMap(self.choices))
arg_utils.SetFieldInMessage(message, self.api_field, value)
class ArgumentGroup(object):
"""Encapsulates data used to generate argument groups.
Most of the attributes of this object correspond directly to the schema and
have more complete docs there.
Attributes:
help_text: Optional help text for the group.
required: True to make the group required.
mutex: True to make the group mutually exclusive.
hidden: True to make the group hidden.
arguments: The list of arguments in the group.
"""
@classmethod
def FromData(cls, data):
"""Gets the arg group definition from the spec data.
Args:
data: The group spec data.
Returns:
ArgumentGroup, the parsed argument group.
Raises:
InvalidSchemaError: if the YAML command is malformed.
"""
return cls(
help_text=data.get('help_text'),
required=data.get('required', False),
mutex=data.get('mutex', False),
hidden=data.get('hidden', False),
arguments=[Argument.FromData(item) for item in data.get('params')],
)
def __init__(self, help_text=None, required=False, mutex=False, hidden=False,
arguments=None):
self.help_text = help_text
self.required = required
self.mutex = mutex
self.hidden = hidden
self.arguments = arguments
def Generate(self, message):
"""Generates and returns the base argument group.
Args:
message: The API message, None for non-resource args.
Returns:
The base argument group.
"""
group = base.ArgumentGroup(
mutex=self.mutex, required=self.required, help=self.help_text)
for arg in self.arguments:
group.AddArgument(arg.Generate(message))
return group
def Parse(self, message, namespace):
"""Sets argument group message values, if any, from the parsed args.
Args:
message: The API message, None for non-resource args.
namespace: The parsed command line argument namespace.
"""
for arg in self.arguments:
arg.Parse(message, namespace)
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(
RESOURCE_TYPE_FORMAT_KEY, 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 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