286 lines
9.1 KiB
Python
286 lines
9.1 KiB
Python
# -*- 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
|