587 lines
23 KiB
Python
587 lines
23 KiB
Python
# -*- 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.
|
|
"""completers for resource library."""
|
|
|
|
from __future__ import absolute_import
|
|
from __future__ import division
|
|
from __future__ import unicode_literals
|
|
|
|
import collections
|
|
|
|
from apitools.base.protorpclite import messages
|
|
|
|
from googlecloudsdk.api_lib.util import resource as resource_lib # pylint: disable=unused-import
|
|
from googlecloudsdk.command_lib.util import completers
|
|
from googlecloudsdk.command_lib.util.apis import arg_utils
|
|
from googlecloudsdk.command_lib.util.apis import registry
|
|
from googlecloudsdk.command_lib.util.concepts import resource_parameter_info
|
|
from googlecloudsdk.core import exceptions
|
|
from googlecloudsdk.core import log
|
|
from googlecloudsdk.core import properties
|
|
from googlecloudsdk.core import resources
|
|
|
|
import six
|
|
|
|
DEFAULT_ID_FIELD = 'name'
|
|
_PROJECTS_COLLECTION = 'cloudresourcemanager.projects'
|
|
_PROJECT_ID_FIELD = 'projectId'
|
|
|
|
|
|
class Error(exceptions.Error):
|
|
"""Base error class for this module."""
|
|
|
|
|
|
class ParentTranslator(object):
|
|
"""Translates parent collections for completers.
|
|
|
|
Attributes:
|
|
collection: str, the collection name.
|
|
param_translation: {str: str}, lookup from the params of the child
|
|
collection to the params of the special parent collection. If None,
|
|
then the collections match and translate methods are a no-op.
|
|
"""
|
|
|
|
def __init__(self, collection, param_translation=None):
|
|
self.collection = collection
|
|
self.param_translation = param_translation or {}
|
|
|
|
def ToChildParams(self, params):
|
|
"""Translate from original parent params to params that match the child."""
|
|
if self.param_translation:
|
|
for orig_param, new_param in six.iteritems(self.param_translation):
|
|
params[orig_param] = params.get(new_param)
|
|
del params[new_param]
|
|
return params
|
|
|
|
def MessageResourceMap(self, message, ref):
|
|
"""Get dict for translating parent params into the given message type."""
|
|
message_resource_map = {}
|
|
# Parse resource with any params in the translator that are needed for the
|
|
# request.
|
|
for orig_param, special_param in six.iteritems(self.param_translation):
|
|
try:
|
|
message.field_by_name(orig_param)
|
|
# The field is not found, meaning that the original param isn't in the
|
|
# message.
|
|
except KeyError:
|
|
continue
|
|
message_resource_map[orig_param] = getattr(ref, special_param, None)
|
|
return message_resource_map
|
|
|
|
def Parse(self, parent_params, parameter_info, aggregations_dict):
|
|
"""Parse the parent resource from parameter info and aggregations.
|
|
|
|
Args:
|
|
parent_params: [str], a list of params in the current collection's parent
|
|
collection.
|
|
parameter_info: the runtime ResourceParameterInfo object.
|
|
aggregations_dict: {str: str}, a dict of params to values that are
|
|
being aggregated from earlier updates.
|
|
|
|
Returns:
|
|
resources.Resource | None, the parsed parent reference or None if there
|
|
is not enough information to parse.
|
|
"""
|
|
param_values = {
|
|
self.param_translation.get(p, p): parameter_info.GetValue(p)
|
|
for p in parent_params}
|
|
for p, value in six.iteritems(aggregations_dict):
|
|
translated_name = self.param_translation.get(p, p)
|
|
if value and not param_values.get(translated_name, None):
|
|
param_values[translated_name] = value
|
|
try:
|
|
return resources.Resource(
|
|
resources.REGISTRY,
|
|
collection_info=resources.REGISTRY.GetCollectionInfo(self.collection),
|
|
subcollection='',
|
|
param_values=param_values,
|
|
endpoint_url=None)
|
|
# Not all completion list calls may need to have a parent, so even if we
|
|
# can't parse a parent, we log the error and attempt to send an update call
|
|
# without one. (Any error returned by the API will be raised.)
|
|
except resources.Error as e:
|
|
log.info(six.text_type(e).rstrip())
|
|
return None
|
|
|
|
|
|
# A map from parent params (in original resource parser order, joined with '.')
|
|
# to special collections. If the original params are different from the special
|
|
# collection, the param_translator is used to translate back and forth between
|
|
# the original params and the special collection.
|
|
_PARENT_TRANSLATORS = {
|
|
'projectsId': ParentTranslator(_PROJECTS_COLLECTION,
|
|
{'projectsId': _PROJECT_ID_FIELD}),
|
|
'projectId': ParentTranslator(_PROJECTS_COLLECTION)}
|
|
|
|
|
|
class CollectionConfig(collections.namedtuple(
|
|
'CollectionConfig',
|
|
[
|
|
# static params are used to build the List request when updating
|
|
# the cache (equivalent to completion_request_params in AttributeConfig
|
|
# objects)
|
|
'static_params',
|
|
# Configures the ID field that is used to parse the results of a List
|
|
# request when updating the cache. Equivalent to completion_id_field
|
|
# in AttributeConfig objects.
|
|
'id_field',
|
|
# Configures the param name for the completer.
|
|
'param_name']
|
|
)):
|
|
"""Stores data about special collections for configuring completion."""
|
|
|
|
|
|
# This maps special collections to configuration for CompleterInfo objects
|
|
# rather than using configuration from the parent resource's collection.
|
|
# Currently only covers projects.
|
|
_SPECIAL_COLLECTIONS_MAP = {
|
|
_PROJECTS_COLLECTION: CollectionConfig({'filter': 'lifecycleState:ACTIVE'},
|
|
_PROJECT_ID_FIELD,
|
|
_PROJECT_ID_FIELD)}
|
|
|
|
|
|
class ResourceArgumentCompleter(completers.ResourceCompleter):
|
|
"""A completer for an argument that's part of a resource argument."""
|
|
|
|
def __init__(self, resource_spec, collection_info, method,
|
|
static_params=None, id_field=None, param=None, **kwargs):
|
|
"""Initializes."""
|
|
self.resource_spec = resource_spec
|
|
self._method = method
|
|
self._static_params = static_params or {}
|
|
self.id_field = id_field or DEFAULT_ID_FIELD
|
|
collection_name = collection_info.full_name
|
|
api_version = collection_info.api_version
|
|
super(ResourceArgumentCompleter, self).__init__(
|
|
collection=collection_name,
|
|
api_version=api_version,
|
|
param=param,
|
|
parse_all=True,
|
|
**kwargs)
|
|
|
|
@property
|
|
def method(self):
|
|
"""Gets the list method for the collection.
|
|
|
|
Returns:
|
|
googlecloudsdk.command_lib.util.apis.registry.APIMethod, the method.
|
|
"""
|
|
return self._method
|
|
|
|
def _ParentParams(self):
|
|
"""Get the parent params of the collection."""
|
|
return self.collection_info.GetParams('')[:-1]
|
|
|
|
def _GetUpdaters(self):
|
|
"""Helper function to build dict of updaters."""
|
|
# Find the attribute that matches the final param of the collection for this
|
|
# completer.
|
|
final_param = self.collection_info.GetParams('')[-1]
|
|
for i, attribute in enumerate(self.resource_spec.attributes):
|
|
if self.resource_spec.ParamName(attribute.name) == final_param:
|
|
attribute_idx = i
|
|
break
|
|
else:
|
|
attribute_idx = 0
|
|
|
|
updaters = {}
|
|
for i, attribute in enumerate(
|
|
self.resource_spec.attributes[:attribute_idx]):
|
|
completer = CompleterForAttribute(self.resource_spec, attribute.name)
|
|
if completer:
|
|
updaters[self.resource_spec.ParamName(attribute.name)] = (completer,
|
|
True)
|
|
else:
|
|
updaters[self.resource_spec.ParamName(attribute.name)] = (None,
|
|
False)
|
|
return updaters
|
|
|
|
def ParameterInfo(self, parsed_args, argument):
|
|
"""Builds a ResourceParameterInfo object.
|
|
|
|
Args:
|
|
parsed_args: the namespace.
|
|
argument: unused.
|
|
|
|
Returns:
|
|
ResourceParameterInfo, the parameter info for runtime information.
|
|
"""
|
|
resource_info = parsed_args.CONCEPTS.ArgNameToConceptInfo(argument.dest)
|
|
|
|
updaters = self._GetUpdaters()
|
|
|
|
return resource_parameter_info.ResourceParameterInfo(
|
|
resource_info, parsed_args, argument, updaters=updaters,
|
|
collection=self.collection)
|
|
|
|
def ValidateAttributeSources(self, aggregations):
|
|
"""Validates that parent attributes values exitst before making request."""
|
|
parameters_needing_resolution = set([p.name for p in self.parameters[:-1]])
|
|
resolved_parameters = set([a.name for a in aggregations])
|
|
# attributes can also be resolved by completers
|
|
for attribute in self.resource_spec.attributes:
|
|
if CompleterForAttribute(self.resource_spec, attribute.name):
|
|
resolved_parameters.add(
|
|
self.resource_spec.attribute_to_params_map[attribute.name])
|
|
return parameters_needing_resolution.issubset(resolved_parameters)
|
|
|
|
def Update(self, parameter_info, aggregations):
|
|
if self.method is None:
|
|
return None
|
|
|
|
if not self.ValidateAttributeSources(aggregations):
|
|
return None
|
|
|
|
log.info(
|
|
'Cache query parameters={} aggregations={}'
|
|
'resource info={}'.format(
|
|
[(p, parameter_info.GetValue(p))
|
|
for p in self.collection_info.GetParams('')],
|
|
[(p.name, p.value) for p in aggregations],
|
|
parameter_info.resource_info.attribute_to_args_map))
|
|
parent_translator = self._GetParentTranslator(parameter_info, aggregations)
|
|
try:
|
|
query = self.BuildListQuery(parameter_info, aggregations,
|
|
parent_translator=parent_translator)
|
|
except Exception as e: # pylint: disable=broad-except
|
|
if properties.VALUES.core.print_completion_tracebacks.GetBool():
|
|
raise
|
|
log.info(six.text_type(e).rstrip())
|
|
raise Error('Could not build query to list completions: {} {}'.format(
|
|
type(e), six.text_type(e).rstrip()))
|
|
try:
|
|
response = self.method.Call(query)
|
|
response_collection = self.method.collection
|
|
items = [self._ParseResponse(r, response_collection,
|
|
parameter_info=parameter_info,
|
|
aggregations=aggregations,
|
|
parent_translator=parent_translator)
|
|
for r in response]
|
|
log.info('cache items={}'.format(
|
|
[i.RelativeName() for i in items]))
|
|
except Exception as e: # pylint: disable=broad-except
|
|
if properties.VALUES.core.print_completion_tracebacks.GetBool():
|
|
raise
|
|
log.info(six.text_type(e).rstrip())
|
|
# Give user more information if they hit an apitools validation error,
|
|
# which probably means that they haven't provided enough information
|
|
# for us to complete.
|
|
if isinstance(e, messages.ValidationError):
|
|
raise Error('Update query failed, may not have enough information to '
|
|
'list existing resources: {} {}'.format(
|
|
type(e), six.text_type(e).rstrip()))
|
|
raise Error('Update query [{}]: {} {}'.format(
|
|
query, type(e), six.text_type(e).rstrip()))
|
|
return [self.StringToRow(item.RelativeName()) for item in items]
|
|
|
|
def _ParseResponse(self, response, response_collection,
|
|
parameter_info=None, aggregations=None,
|
|
parent_translator=None):
|
|
"""Gets a resource ref from a single item in a list response."""
|
|
param_values = self._GetParamValuesFromParent(
|
|
parameter_info, aggregations=aggregations,
|
|
parent_translator=parent_translator)
|
|
param_names = response_collection.detailed_params
|
|
for param in param_names:
|
|
val = getattr(response, param, None)
|
|
if val is not None:
|
|
param_values[param] = val
|
|
|
|
line = getattr(response, self.id_field, '')
|
|
return resources.REGISTRY.Parse(
|
|
line, collection=response_collection.full_name, params=param_values)
|
|
|
|
def _GetParamValuesFromParent(self, parameter_info, aggregations=None,
|
|
parent_translator=None):
|
|
parent_ref = self.GetParent(parameter_info, aggregations=aggregations,
|
|
parent_translator=parent_translator)
|
|
if not parent_ref:
|
|
return {}
|
|
params = parent_ref.AsDict()
|
|
if parent_translator:
|
|
return parent_translator.ToChildParams(params)
|
|
return params
|
|
|
|
def _GetAggregationsValuesDict(self, aggregations):
|
|
"""Build a {str: str} dict of name to value for aggregations."""
|
|
aggregations_dict = {}
|
|
aggregations = [] if aggregations is None else aggregations
|
|
for aggregation in aggregations:
|
|
if aggregation.value:
|
|
aggregations_dict[aggregation.name] = aggregation.value
|
|
return aggregations_dict
|
|
|
|
def BuildListQuery(self, parameter_info, aggregations=None,
|
|
parent_translator=None):
|
|
"""Builds a list request to list values for the given argument.
|
|
|
|
Args:
|
|
parameter_info: the runtime ResourceParameterInfo object.
|
|
aggregations: a list of _RuntimeParameter objects.
|
|
parent_translator: a ParentTranslator object if needed.
|
|
|
|
Returns:
|
|
The apitools request.
|
|
"""
|
|
method = self.method
|
|
if method is None:
|
|
return None
|
|
message = method.GetRequestType()()
|
|
for field, value in six.iteritems(self._static_params):
|
|
arg_utils.SetFieldInMessage(message, field, value)
|
|
parent = self.GetParent(parameter_info, aggregations=aggregations,
|
|
parent_translator=parent_translator)
|
|
if not parent:
|
|
return message
|
|
message_resource_map = {}
|
|
|
|
if parent_translator:
|
|
message_resource_map = parent_translator.MessageResourceMap(
|
|
message, parent)
|
|
|
|
arg_utils.ParseResourceIntoMessage(
|
|
parent, method, message,
|
|
message_resource_map=message_resource_map, is_primary_resource=True)
|
|
return message
|
|
|
|
def _GetParentTranslator(self, parameter_info, aggregations=None):
|
|
"""Get a special parent translator if needed and available."""
|
|
aggregations_dict = self._GetAggregationsValuesDict(aggregations)
|
|
param_values = self._GetRawParamValuesForParent(
|
|
parameter_info, aggregations_dict=aggregations_dict)
|
|
try:
|
|
self._ParseDefaultParent(param_values)
|
|
# If there's no error, we don't need a translator.
|
|
return None
|
|
except resources.ParentCollectionResolutionException:
|
|
# Check the parent params against the _PARENT_TRANSLATORS dict, using the
|
|
# parent params (joined by '.' in original resource parser order) as a
|
|
# key.
|
|
key = '.'.join(self._ParentParams())
|
|
if key in _PARENT_TRANSLATORS:
|
|
return _PARENT_TRANSLATORS.get(key)
|
|
# Errors will be raised and logged later when actually parsing the parent.
|
|
except resources.Error:
|
|
return None
|
|
|
|
def _GetRawParamValuesForParent(self, parameter_info, aggregations_dict=None):
|
|
"""Get raw param values for the resource in prep for parsing parent."""
|
|
param_values = {p: parameter_info.GetValue(p) for p in self._ParentParams()}
|
|
for name, value in six.iteritems(aggregations_dict or {}):
|
|
if value and not param_values.get(name, None):
|
|
param_values[name] = value
|
|
final_param = self.collection_info.GetParams('')[-1]
|
|
if param_values.get(final_param, None) is None:
|
|
param_values[final_param] = 'fake' # Stripped when we get the parent.
|
|
return param_values
|
|
|
|
def _ParseDefaultParent(self, param_values):
|
|
"""Parse the parent for a resource using default collection."""
|
|
resource = resources.Resource(
|
|
resources.REGISTRY,
|
|
collection_info=self.collection_info,
|
|
subcollection='',
|
|
param_values=param_values,
|
|
endpoint_url=None)
|
|
return resource.Parent()
|
|
|
|
def GetParent(self, parameter_info, aggregations=None,
|
|
parent_translator=None):
|
|
"""Gets the parent reference of the parsed parameters.
|
|
|
|
Args:
|
|
parameter_info: the runtime ResourceParameterInfo object.
|
|
aggregations: a list of _RuntimeParameter objects.
|
|
parent_translator: a ParentTranslator for translating to a special
|
|
parent collection, if needed.
|
|
|
|
Returns:
|
|
googlecloudsdk.core.resources.Resource | None, the parent resource or None
|
|
if no parent was found.
|
|
"""
|
|
aggregations_dict = self._GetAggregationsValuesDict(aggregations)
|
|
param_values = self._GetRawParamValuesForParent(
|
|
parameter_info, aggregations_dict=aggregations_dict)
|
|
try:
|
|
if not parent_translator:
|
|
return self._ParseDefaultParent(param_values)
|
|
return parent_translator.Parse(self._ParentParams(), parameter_info,
|
|
aggregations_dict)
|
|
except resources.ParentCollectionResolutionException as e:
|
|
# We don't know the parent collection.
|
|
log.info(six.text_type(e).rstrip())
|
|
return None
|
|
# No resource could be parsed.
|
|
except resources.Error as e:
|
|
log.info(six.text_type(e).rstrip())
|
|
return None
|
|
|
|
def __eq__(self, other):
|
|
"""Overrides."""
|
|
# Not using type(self) because the class is created programmatically.
|
|
if not isinstance(other, ResourceArgumentCompleter):
|
|
return False
|
|
return (self.resource_spec == other.resource_spec and
|
|
self.collection == other.collection and
|
|
self.method == other.method)
|
|
|
|
|
|
def _MatchCollection(resource_spec, attribute):
|
|
"""Gets the collection for an attribute in a resource."""
|
|
resource_collection_info = resource_spec._collection_info # pylint: disable=protected-access
|
|
resource_collection = registry.APICollection(
|
|
resource_collection_info)
|
|
if resource_collection is None:
|
|
return None
|
|
if attribute == resource_spec.attributes[-1]:
|
|
return resource_collection.name
|
|
attribute_idx = resource_spec.attributes.index(attribute)
|
|
api_name = resource_collection_info.api_name
|
|
resource_collections = registry.GetAPICollections(
|
|
api_name,
|
|
resource_collection_info.api_version)
|
|
params = resource_collection.detailed_params[:attribute_idx + 1]
|
|
for c in resource_collections:
|
|
if c.detailed_params == params:
|
|
return c.name
|
|
|
|
|
|
def _GetCompleterCollectionInfo(resource_spec, attribute):
|
|
"""Gets collection info for an attribute in a resource."""
|
|
api_version = None
|
|
collection = _MatchCollection(resource_spec, attribute)
|
|
if collection:
|
|
# pylint: disable=protected-access
|
|
full_collection_name = (
|
|
resource_spec._collection_info.api_name + '.' + collection)
|
|
api_version = resource_spec._collection_info.api_version
|
|
# The CloudResourceManager projects collection can be used for "synthetic"
|
|
# project resources that don't have their own method.
|
|
elif attribute.name == 'project':
|
|
full_collection_name = 'cloudresourcemanager.projects'
|
|
else:
|
|
return None
|
|
return resources.REGISTRY.GetCollectionInfo(full_collection_name,
|
|
api_version=api_version)
|
|
|
|
|
|
class CompleterInfo(object):
|
|
"""Holds data that can be used to instantiate a resource completer."""
|
|
|
|
def __init__(self, static_params=None, id_field=None, collection_info=None,
|
|
method=None, param_name=None):
|
|
self.static_params = static_params
|
|
self.id_field = id_field
|
|
self.collection_info = collection_info
|
|
self.method = method
|
|
self.param_name = param_name
|
|
|
|
@classmethod
|
|
def FromResource(cls, resource_spec, attribute_name):
|
|
"""Gets the method, param_name, and other configuration for a completer.
|
|
|
|
Args:
|
|
resource_spec: concepts.ResourceSpec, the overall resource.
|
|
attribute_name: str, the name of the attribute whose argument will use
|
|
this completer.
|
|
|
|
Raises:
|
|
AttributeError: if the attribute doesn't belong to the resource.
|
|
|
|
Returns:
|
|
CompleterInfo, the instantiated object.
|
|
"""
|
|
for a in resource_spec.attributes:
|
|
if a.name == attribute_name:
|
|
attribute = a
|
|
break
|
|
else:
|
|
raise AttributeError(
|
|
'Attribute [{}] not found in resource.'.format(attribute_name))
|
|
param_name = resource_spec.ParamName(attribute_name)
|
|
static_params = attribute.completion_request_params
|
|
id_field = attribute.completion_id_field
|
|
collection_info = _GetCompleterCollectionInfo(resource_spec, attribute)
|
|
if collection_info.full_name in _SPECIAL_COLLECTIONS_MAP:
|
|
special_info = _SPECIAL_COLLECTIONS_MAP.get(collection_info.full_name)
|
|
method = registry.GetMethod(collection_info.full_name, 'list')
|
|
static_params = special_info.static_params
|
|
id_field = special_info.id_field
|
|
param_name = special_info.param_name
|
|
if not collection_info:
|
|
return CompleterInfo(static_params, id_field, None, None, param_name)
|
|
# If there is no appropriate list method for the collection, we can't auto-
|
|
# create a completer.
|
|
try:
|
|
method = registry.GetMethod(
|
|
collection_info.full_name, 'list',
|
|
api_version=collection_info.api_version)
|
|
except registry.UnknownMethodError:
|
|
if (collection_info.full_name != _PROJECTS_COLLECTION
|
|
and collection_info.full_name.split('.')[-1] == 'projects'):
|
|
# The CloudResourceManager projects methods can be used for "synthetic"
|
|
# project resources that don't have their own method.
|
|
# This is a bit of a hack, so if any resource arguments come up for
|
|
# which this doesn't work, a toggle should be added to the
|
|
# ResourceSpec class to disable this.
|
|
# Does not use param_name from the special collections map because
|
|
# the collection exists with the current params, it's just the list
|
|
# method that we're borrowing.
|
|
special_info = _SPECIAL_COLLECTIONS_MAP.get(_PROJECTS_COLLECTION)
|
|
method = registry.GetMethod(_PROJECTS_COLLECTION, 'list')
|
|
static_params = special_info.static_params
|
|
id_field = special_info.id_field
|
|
else:
|
|
method = None
|
|
except registry.Error:
|
|
method = None
|
|
return CompleterInfo(static_params, id_field, collection_info, method,
|
|
param_name)
|
|
|
|
def GetMethod(self):
|
|
"""Get the APIMethod for an attribute in a resource."""
|
|
return self.method
|
|
|
|
|
|
def CompleterForAttribute(resource_spec, attribute_name):
|
|
"""Gets a resource argument completer for a specific attribute."""
|
|
|
|
class Completer(ResourceArgumentCompleter):
|
|
"""A specific completer for this attribute and resource."""
|
|
|
|
def __init__(self, resource_spec=resource_spec,
|
|
attribute_name=attribute_name, **kwargs):
|
|
completer_info = CompleterInfo.FromResource(resource_spec, attribute_name)
|
|
|
|
super(Completer, self).__init__(
|
|
resource_spec,
|
|
completer_info.collection_info,
|
|
completer_info.method,
|
|
static_params=completer_info.static_params,
|
|
id_field=completer_info.id_field,
|
|
param=completer_info.param_name,
|
|
**kwargs)
|
|
|
|
@classmethod
|
|
def validate(cls):
|
|
"""Checks whether the completer is valid (has a list method)."""
|
|
return bool(
|
|
CompleterInfo.FromResource(resource_spec, attribute_name).GetMethod())
|
|
|
|
if not Completer.validate():
|
|
return None
|
|
|
|
return Completer
|