File: //snap/google-cloud-cli/current/lib/googlecloudsdk/api_lib/util/resource_search.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.
"""The Cloud Resource Search lister."""
from __future__ import absolute_import
from __future__ import division
from __future__ import unicode_literals
from apitools.base.py import list_pager
from googlecloudsdk.api_lib.util import apis
from googlecloudsdk.core import exceptions
from googlecloudsdk.core import log
from googlecloudsdk.core.resource import resource_expr_rewrite
class Error(exceptions.Error):
"""Base exception for this module."""
class CollectionNotIndexed(Error):
"""The collection is not indexed."""
class QueryOperatorNotSupported(Error):
"""An operator in a query is not supported."""
PAGE_SIZE = 500
# TODO(b/38192518): Move this to the resource parser yaml config.
# The Cloud Resource Search type dict indexed by Cloud SDK collection.
# The cloudresourcesearch API does not allow dotted resource type names, so
# the flat namespace will get crowded and unwieldy as it grows. For example,
# how many APIs have an "instance" resource? Sorry, "Instance" already snapped
# up by compute. Instead we map the well-defined hierarchical Cloud SDK
# collection names onto the supported resource types.
RESOURCE_TYPES = {
'cloudresourcemanager.projects': 'Project',
'compute.disks': 'Disk',
'compute.healthChecks': 'HealthCheck',
'compute.httpHealthChecks': 'HttpHealthCheck',
'compute.httpsHealthChecks': 'HttpsHealthCheck',
'compute.images': 'Image',
'compute.instanceGroups': 'InstanceTemplate',
'compute.instances': 'Instance',
'compute.subnetworks': 'Subnetwork',
}
CLOUD_RESOURCE_SEARCH_COLLECTION = 'resources'
class QueryRewriter(resource_expr_rewrite.Backend):
"""Resource filter expression rewriter."""
def RewriteGlobal(self, call):
"""Rewrites global restriction in call.
Args:
call: A list of resource_lex._TransformCall objects. In this case the list
has one element that is a global restriction with a global_restriction
property that is the restriction substring to match.
Returns:
The global restriction rewrite which is simply the global_restriction
string.
"""
return call.global_restriction
def RewriteTerm(self, key, op, operand, key_type):
"""Rewrites <key op operand>."""
del key_type # unused in RewriteTerm
if op in ('~',):
raise QueryOperatorNotSupported(
'The [{}] operator is not supported in cloud resource search '
'queries.'.format(op))
# The query API does not support name:(value1 ... valueN), but it does
# support OR, so we split multiple operands into separate name:valueI
# expressions joined by OR.
values = operand if isinstance(operand, list) else [operand]
if key == 'project':
# The query API does not have a 'project' attribute, but It does have
# 'selfLink'. The rewrite provides 'project' and massages the query to
# operate on 'selfLink'. The resource parser would be perfection here,
# but a bit heavweight. We'll stick with this shortcut until proven wrong.
key = 'selfLink'
values = ['/projects/{}/'.format(value) for value in values]
elif key == '@type':
# Cloud SDK maps @type:collection => @type:resource-type. This isolates
# the user from yet another esoteric namespace.
collections = values
values = []
for collection in collections:
if collection.startswith(CLOUD_RESOURCE_SEARCH_COLLECTION + '.'):
values.append(collection[len(CLOUD_RESOURCE_SEARCH_COLLECTION) + 1:])
else:
try:
values.append(RESOURCE_TYPES[collection])
except KeyError:
raise CollectionNotIndexed(
'Collection [{}] not indexed for search.'.format(collection))
parts = ['{key}{op}{value}'.format(key=key, op=op, value=self.Quote(value))
for value in values]
expr = ' OR '.join(parts)
if len(parts) > 1:
# This eliminates AND/OR precedence ambiguity.
expr = '( ' + expr + ' )'
return expr
def List(limit=None, page_size=None, query=None, sort_by=None, uri=False):
"""Yields the list of Cloud Resources for collection.
Not all collections are indexed for search.
Args:
limit: The max number of resources to return. None for unlimited.
page_size: The max number of resources per response page. The defsult is
PAGE_SIE.
query: A resource filter expression. Use @type:collection to filter
resources by collection. Use the resources._RESOURCE_TYPE_ collection to
specify CloudResourceSearch resource types. By default all indexed
resources are in play.
sort_by: A list of field names to sort by. Prefix a name with ~ to reverse
the sort for that name.
uri: Return the resource URI if true.
Raises:
CollectionNotIndexed: If the collection is not indexed for search.
QueryOperatorNotSupported: If the query contains an unsupported operator.
HttpError: request/response errors.
Yields:
The list of Cloud Resources for collection.
"""
_, remote_query = QueryRewriter().Rewrite(query)
log.info('Resource search query="%s" remote_query="%s"', query, remote_query)
if page_size is None:
page_size = PAGE_SIZE
if sort_by:
order_by = ','.join([name[1:] + ' desc' if name.startswith('~') else name
for name in sort_by])
else:
order_by = None
client = apis.GetClientInstance('cloudresourcesearch', 'v1')
for result in list_pager.YieldFromList(
service=client.ResourcesService(client),
method='Search',
request=client.MESSAGES_MODULE.CloudresourcesearchResourcesSearchRequest(
orderBy=order_by,
query=remote_query,
),
field='results',
limit=limit,
batch_size=page_size,
batch_size_attribute='pageSize'):
yield result.resourceUrl if uri else result.resource