# -*- 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. """Completer extensions for the core.cache.completion_cache module.""" from __future__ import absolute_import from __future__ import division from __future__ import unicode_literals import abc import io from googlecloudsdk.api_lib.util import resource_search from googlecloudsdk.command_lib.util import parameter_info_lib from googlecloudsdk.core import log from googlecloudsdk.core import properties from googlecloudsdk.core import resources from googlecloudsdk.core.cache import completion_cache from googlecloudsdk.core.cache import resource_cache import six _PSEUDO_COLLECTION_PREFIX = 'cloud.sdk' def PseudoCollectionName(name): """Returns the pseudo collection name for name. Pseudo collection completion entities have no resource parser and/or URI. Args: name: The pseudo collection entity name. Returns: The pseudo collection name for name. """ return '.'.join([_PSEUDO_COLLECTION_PREFIX, name]) class Converter(completion_cache.Completer): """Converter mixin, based on core/resource_completion_style at instantiation. Attributes: _additional_params: A list of additional parameter names not int the parsed resource. _parse_all: If True, attempt to parse any string, otherwise, just parse strings beginning with 'http[s]://'. qualified_parameter_names: The list of parameter names that must be fully qualified. Use the name 'collection' to qualify collections. """ def __init__(self, additional_params=None, api=None, qualified_parameter_names=None, style=None, parse_all=False, **kwargs): super(Converter, self).__init__(**kwargs) if api: self.api = api elif self.collection: self.api = self.collection.split('.')[0] else: self.api = None self._additional_params = additional_params self.qualified_parameter_names = set(qualified_parameter_names or []) if style is None: style = properties.VALUES.core.resource_completion_style.Get() if style == 'gri' or properties.VALUES.core.enable_gri.GetBool(): self._string_to_row = self._GRI_StringToRow else: self._string_to_row = self._StringToRow if style == 'gri': self._row_to_string = self._GRI_RowToString else: self._row_to_string = self._FLAGS_RowToString self._parse_all = parse_all def StringToRow(self, string, parameter_info=None): """Returns the row representation of string.""" return self._string_to_row(string, parameter_info) def RowToString(self, row, parameter_info=None): """Returns the string representation of row.""" return self._row_to_string(row, parameter_info=parameter_info) def AddQualifiedParameterNames(self, qualified_parameter_names): """Adds qualified_parameter_names to the set of qualified parameters.""" self.qualified_parameter_names |= qualified_parameter_names def ParameterInfo(self, parsed_args, argument): """Returns the parameter info object. This is the default method that returns the parameter info by name convention object. Resource argument completers should override this method to provide the exact object, not the "best guess" of the default. Args: parsed_args: The command line parsed args object. argument: The argparse argument object attached to this completer. Returns: The parameter info object. """ return parameter_info_lib.ParameterInfoByConvention(parsed_args, argument, self.api) @staticmethod def _ConvertProjectNumberToID(row, parameter_info): """Convert project number into ID, if it's not one already. Get the project ID from command parameters and compare it to project IDs returned by list commands. If a project number is found instead, replace it with the project ID before storing it in completion cache. Idempotent. Does nothing if there's no project parameter, which is the case for resources without a parent project, e.g. organization resources. Args: row: a dict containing the values necessary for tab completion of resource args. parameter_info: Program state, contains the available information on the CLI command executed, such as param values, etc. Returns: None, modifies the provided dict in-place. """ project_key = [ k for k in row if k in ['project', 'projectId', 'projectsId'] ] project_key = project_key[0] if project_key else None if project_key and row[project_key].isnumeric(): row[project_key] = parameter_info.GetValue( project_key, check_properties=True) def _GRI_StringToRow(self, string, parameter_info=None): try: # '' is not parsable so treat it like None to match all. row = self.parse(string or None) if parameter_info: self._ConvertProjectNumberToID(row, parameter_info) row = list(row.values()) return row except resources.RequiredFieldOmittedException: fields = resources.GRI.FromString(string, self.collection).path_fields if len(fields) < self.columns: fields += [''] * (self.columns - len(fields)) return list(reversed(fields)) def _StringToRow(self, string, parameter_info=None): if string and (string.startswith('https://') or string.startswith('http://') or self._parse_all): try: row = self.parse(string or None) if parameter_info: self._ConvertProjectNumberToID(row, parameter_info) row = list(row.values()) return row except resources.RequiredFieldOmittedException: pass return [''] * (self.columns - 1) + [string] def _GRI_RowToString(self, row, parameter_info=None): # Clear out parameters that are the same as the corresponding # flag/property value, in highest to lowest significance order, stopping # at the first parameter that can't be cleared. parts = list(row) for column, parameter in enumerate(self.parameters): if parameter.name in self.qualified_parameter_names: continue value = parameter_info.GetValue(parameter.name) if parts[column] != value: break parts[column] = '' if 'collection' in self.qualified_parameter_names: collection = self.collection is_fully_qualified = True else: collection = None is_fully_qualified = True return six.text_type( resources.GRI( reversed(parts), collection=collection, is_fully_qualified=is_fully_qualified)) def _FLAGS_RowToString(self, row, parameter_info=None): parts = [row[self.columns - 1]] parameters = self.parameters name = 'collection' if name in self.qualified_parameter_names: # Treat 'collection' like a parameter. collection_parameter = resource_cache.Parameter(name=name) parameters = list(parameters) + [collection_parameter] for parameter in parameters: if parameter.column == self.columns - 1: continue check_properties = parameter.name not in self.qualified_parameter_names flag = parameter_info.GetFlag( parameter.name, row[parameter.column], check_properties=check_properties) if flag: parts.append(flag) for flag_name in set(self._additional_params or [] + parameter_info.GetAdditionalParams() or []): flag = parameter_info.GetFlag(flag_name, True) if flag: parts.append(flag) return ' '.join(parts) class ResourceCompleter(Converter): """A parsed resource parameter initializer. Attributes: collection_info: The resource registry collection info. parse: The resource URI parse function. Converts a URI string into a list of parsed parameters. """ def __init__(self, collection=None, api_version=None, param=None, **kwargs): """Constructor. Args: collection: The resource collection name. api_version: The API version for collection, None for the default version. param: The updated parameter column name. **kwargs: Base class kwargs. """ self.api_version = api_version if collection: self.collection_info = resources.REGISTRY.GetCollectionInfo( collection, api_version=api_version) params = self.collection_info.GetParams('') log.info('cache collection=%s api_version=%s params=%s' % ( collection, self.collection_info.api_version, params)) parameters = [resource_cache.Parameter(name=name, column=column) for column, name in enumerate(params)] parse = resources.REGISTRY.Parse def _Parse(string): return parse( string, collection=collection, enforce_collection=False, validate=False).AsDict() self.parse = _Parse else: params = [] parameters = [] super(ResourceCompleter, self).__init__( collection=collection, columns=len(params), column=params.index(param) if param else 0, parameters=parameters, **kwargs) class ListCommandCompleter(ResourceCompleter): """A parameterized completer that uses a gcloud list command for updates. Attributes: list_command: The gcloud list command that returns the list of current resource URIs. flags: The resource parameter flags that are referenced by list_command. parse_output: The completion items are written to the list_command standard output, one per line, if True. Otherwise the list_command return value is the list of items. """ def __init__(self, list_command=None, flags=None, parse_output=False, **kwargs): self._list_command = list_command self._flags = flags or [] self._parse_output = parse_output super(ListCommandCompleter, self).__init__(**kwargs) def GetListCommand(self, parameter_info): """Returns the list command argv given parameter_info.""" def _FlagName(flag): return flag.split('=')[0] list_command = self._list_command.split() flags = {_FlagName(f) for f in list_command if f.startswith('--')} if '--quiet' not in flags: flags.add('--quiet') list_command.append('--quiet') if '--uri' in flags and '--format' not in flags: flags.add('--format') list_command.append('--format=disable') for name in (self._flags + [parameter.name for parameter in self.parameters] + parameter_info.GetAdditionalParams()): flag = parameter_info.GetFlag( name, check_properties=False, for_update=True) if flag: flag_name = _FlagName(flag) if flag_name not in flags: flags.add(flag_name) list_command.append(flag) return list_command def GetAllItems(self, command, parameter_info): """Runs command and returns the list of completion items.""" try: if not self._parse_output: return parameter_info.Execute(command) log_out = log.out out = io.StringIO() log.out = out parameter_info.Execute(command) return out.getvalue().rstrip('\n').split('\n') finally: if self._parse_output: log.out = log_out def Update(self, parameter_info, aggregations): """Returns the current list of parsed resources from list_command.""" command = self.GetListCommand(parameter_info) for parameter in aggregations: flag = parameter_info.GetFlag( parameter.name, parameter.value, for_update=True) if flag and flag not in command: command.append(flag) log.info('cache update command: %s' % ' '.join(command)) try: items = list(self.GetAllItems(command, parameter_info) or []) except (Exception, SystemExit) as e: # pylint: disable=broad-except if properties.VALUES.core.print_completion_tracebacks.GetBool(): raise log.info(six.text_type(e).rstrip()) try: raise (type(e))('Update command [{}]: {}'.format( ' '.join(command), six.text_type(e).rstrip())) except TypeError: raise e return [self.StringToRow(item, parameter_info) for item in items] class ResourceSearchCompleter(ResourceCompleter): """A parameterized completer that uses Cloud Resource Search for updates.""" def Update(self, parameter_info, aggregations): """Returns the current list of parsed resources.""" query = '@type:{}'.format(self.collection) log.info('cloud resource search query: %s' % query) try: items = resource_search.List(query=query, uri=True) 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 (type(e))('Update resource query [{}]: {}'.format( query, six.text_type(e).rstrip())) return [self.StringToRow(item, parameter_info) for item in items] class ResourceParamCompleter(ListCommandCompleter): """A completer that produces a resource list for one resource param.""" def __init__(self, collection=None, param=None, **kwargs): super(ResourceParamCompleter, self).__init__( collection=collection, param=param, **kwargs) def RowToString(self, row, parameter_info=None): """Returns the string representation of row.""" return row[self.column] class MultiResourceCompleter(Converter): """A completer that composes multiple resource completers. Attributes: completers: The list of completers. """ def __init__(self, completers=None, qualified_parameter_names=None, **kwargs): """Constructor. Args: completers: The list of completers. qualified_parameter_names: The set of parameter names that must be qualified. **kwargs: Base class kwargs. """ self.completers = [completer_class(**kwargs) for completer_class in completers] name_count = {} if qualified_parameter_names: for name in qualified_parameter_names: name_count[name] = 1 for completer in self.completers: if completer.parameters: for parameter in completer.parameters: if parameter.name in name_count: name_count[parameter.name] += 1 else: name_count[parameter.name] = 1 qualified_parameter_names = { name for name, count in six.iteritems(name_count) if count != len(self.completers)} # The "collection" for a multi resource completer is the odered comma # separated list of collections. The api is the common API prefix (the first # dotted part of the collections). It names the property section used to # determine default values in the flag completion style. If there are # multiple apis then the combined collection is None which disables property # lookup. collections = [] apis = set() for completer in self.completers: completer.AddQualifiedParameterNames(qualified_parameter_names) apis.add(completer.collection.split('.')[0]) collections.append(completer.collection) collection = ','.join(collections) api = apis.pop() if len(apis) == 1 else None super(MultiResourceCompleter, self).__init__( collection=collection, api=api, **kwargs) def Complete(self, prefix, parameter_info): """Returns the union of completions from all completers.""" return sorted( {completions for completer in self.completers for completions in completer.Complete(prefix, parameter_info)}) def Update(self, parameter_info, aggregations): """Update handled by self.completers.""" del parameter_info del aggregations class NoCacheCompleter(six.with_metaclass(abc.ABCMeta, Converter)): """A completer that does not cache completions.""" def __init__(self, cache=None, **kwargs): del cache super(NoCacheCompleter, self).__init__(**kwargs) @abc.abstractmethod def Complete(self, prefix, parameter_info): """Returns the list of strings matching prefix. This method is normally provided by the cache, but must be specified here in order to bypass the cache. Args: prefix: The resource prefix string to match. parameter_info: A ParamaterInfo object for accessing parameter values in the program state. Returns: The list of strings matching prefix. """ del prefix del parameter_info def Update(self, parameter_info=None, aggregations=None): """Satisfies abc resolution and will never be called.""" del parameter_info, aggregations