File: //snap/google-cloud-cli/current/lib/googlecloudsdk/api_lib/util/resource.py
# -*- coding: utf-8 -*- #
# Copyright 2019 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 cloud resources."""
from __future__ import absolute_import
from __future__ import division
from __future__ import unicode_literals
import re
from googlecloudsdk.core import exceptions
class CollectionInfo(object):
"""Holds information about a resource collection.
Attributes:
api_name: str, name of the api of resources parsed by this parser.
api_version: str, version id for this api.
path: str, Atomic URI template for this resource.
flat_paths: {name->path}, Named detailed URI templates for this resource.
If there is an entry ''->path it replaces path and corresponding param
attributes for resources parsing. path and params are not used in this
case. Also note that key in this dictionary is referred as
subcollection, as it extends 'name' attribute.
params: list(str), description of parameters in the path.
name: str, collection name for this resource without leading api_name.
base_url: str, URL for service providing these resources.
docs_url: str, URL to the API reference docs for this API.
enable_uri_parsing: bool, whether to register a parser to build up a
search tree to match URLs against URL templates.
"""
def __init__(self,
api_name,
api_version,
base_url,
docs_url,
name,
path,
flat_paths,
params,
enable_uri_parsing=True):
self.api_name = api_name
self.api_version = api_version
self.base_url = base_url
self.docs_url = docs_url
self.name = name
self.path = path
self.flat_paths = flat_paths
self.params = params
self.enable_uri_parsing = enable_uri_parsing
@property
def full_name(self):
return self.api_name + '.' + self.name
def GetSubcollection(self, collection_name):
name = self.full_name
# collection_name could be equal to name in which case subcollection is
# empty string or have additional suffix .subcollection.
if collection_name.startswith(name):
return collection_name[len(name) + 1:]
raise KeyError('{0} does not exist in {1}'.format(collection_name, name))
def GetPathRegEx(self, subcollection):
"""Returns regex for matching path template."""
path = self.GetPath(subcollection)
parts = []
prev_end = 0
for match in re.finditer('({[^}]+}/)|({[^}]+})$', path):
parts.append(path[prev_end:match.start()])
parts.append('([^/]+)')
if match.group(1):
parts.append('/')
prev_end = match.end()
if prev_end == len(path):
parts[-1] = '(.*)$'
return ''.join(parts)
def GetParams(self, subcollection):
"""Returns ordered list of parameters for given subcollection.
Args:
subcollection: str, key name for flat_paths. If self.flat_paths is empty
use '' (or any other falsy value) for subcollection to get default path
parameters.
Returns:
Paramaters present in specified subcollection path.
Raises:
KeyError if given subcollection does not exists.
"""
# If default collection requested and we are not using custom paths.
if not subcollection and not self.flat_paths:
return self.params
return GetParamsFromPath(self.flat_paths[subcollection])
def GetPath(self, subcollection):
"""Returns uri template path for given subcollection."""
# If default collection requested and we are not using custom paths.
if not subcollection and not self.flat_paths:
return self.path
return self.flat_paths[subcollection]
def __eq__(self, other):
return (self.api_name == other.api_name and
self.api_version == other.api_version and self.name == other.name)
def __ne__(self, other):
return not self == other
@classmethod
def _CmpHelper(cls, x, y):
"""Just a helper equivalent to the cmp() function in Python 2."""
return (x > y) - (x < y)
def __lt__(self, other):
return self._CmpHelper((self.api_name, self.api_version, self.name),
(other.api_name, other.api_version, other.name)) < 0
def __gt__(self, other):
return self._CmpHelper((self.api_name, self.api_version, self.name),
(other.api_name, other.api_version, other.name)) > 0
def __le__(self, other):
return not self.__gt__(other)
def __ge__(self, other):
return not self.__lt__(other)
def __str__(self):
return self.full_name
class InvalidEndpointException(exceptions.Error):
"""Exception for when an API endpoint is malformed."""
def __init__(self, url):
super(InvalidEndpointException, self).__init__(
"URL does not start with 'http://' or 'https://' [{0}]".format(url))
def SplitEndpointUrl(url):
"""Returns api_name, api_version, resource_path tuple for an API URL.
Supports the following formats:
# Google API production/staging endpoints.
http(s)://www.googleapis.com/{api}/{version}/{resource-path}
http(s)://stagingdomain/{api}/{version}/{resource-path}
http(s)://{api}.googleapis.com/{api}/{version}/{resource-path}
http(s)://{api}.googleapis.com/apis/{kube-api.name}/{version}/{resource-path}
http(s)://{api}.googleapis.com/{version}/{resource-path}
http(s)://{api}.googleapis.com/{api}/{resource-path}
http(s)://{api}.googleapis.com/{resource-path}
http(s)://googledomain/{api}
# Non-Google API endpoints.
http(s)://someotherdomain/{api}/{version}/{resource-path}
Args:
url: str, The resource url.
Returns:
(str, str, str): The API name, version, resource_path.
Raises: InvalidEndpointException.
"""
tokens = _StripUrl(url).split('/')
version_index = _GetApiVersionIndex(tokens)
domain = tokens[0]
if version_index == -1:
api = _ExtractApiNameFromDomain(domain)
if len(tokens) > 1 and tokens[1] == api:
# domain/{api}/{resource-path}
# Some older APIs have this form e.g. compute.googleapis.com/compute/...
resource_index = 2
else:
# domain/{resource-path}
resource_index = 1
return api, None, '/'.join(tokens[resource_index:])
version = tokens[version_index]
resource_path = '/'.join(tokens[version_index + 1:])
if version_index == 1:
# domain/{version}/{resource-path}
return _ExtractApiNameFromDomain(domain), version, resource_path
if version_index > 1:
# If domain is not a kubernetes api name, use
# domain/{api}/{version}/{resource-path}
api_name = _FindKubernetesApiName(domain) or tokens[version_index - 1]
return api_name, version, resource_path
raise InvalidEndpointException(url)
def GetParamsFromPath(path):
"""Extract parameters from uri template path.
See https://tools.ietf.org/html/rfc6570. This function makes simplifying
assumption that all parameter names are surrounded by /{ and }/.
Args:
path: str, uri template path.
Returns:
list(str), list of parameters in the template path.
"""
path = path.split(':')[0]
parts = path.split('/')
params = []
for part in parts:
if part.startswith('{') and part.endswith('}'):
part = part[1:-1]
if part.startswith('+'):
params.append(part[1:])
else:
params.append(part)
return params
def _StripUrl(url):
"""Strip a http: or https: prefix, then strip leading and trailing slashes."""
if url.startswith('https://') or url.startswith('http://'):
return url[url.index(':') + 1:].strip('/')
raise InvalidEndpointException(url)
def IsApiVersion(token):
"""Check if the token parsed from Url is API version."""
versions = ('alpha', 'beta', 'v1', 'v2', 'v3', 'v4', 'dogfood', 'head')
for api_version in versions:
if api_version in token:
return True
return False
def _GetApiVersionIndex(tokens):
for idx, token in enumerate(tokens):
if IsApiVersion(token):
return idx
return -1
def _ExtractApiNameFromDomain(domain):
# Example: sql.googleapis.com -> sql
return domain.split('.')[0]
def _FindKubernetesApiName(domain):
"""Find the name of the kubernetes api.
Determines the kubernetes api name from the domain of the resource uri.
The domain may from a global resource or a regionalized resource.
Args:
domain: Domain from the resource uri.
Returns:
Api name. Returns None if the domain is not a kubernetes api domain.
"""
# Examples:
# sql.googleapis.com -> sql
# us-central1-run.googleapis.com - > run
k8s_api_names = ('run',)
domain_first_part = domain.split('.')[0]
for api_name in k8s_api_names:
if (api_name == domain_first_part or
domain_first_part.endswith('-' + api_name)):
return api_name
return None