564 lines
20 KiB
Python
564 lines
20 KiB
Python
# -*- 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.
|
|
"""Library for obtaining API clients and messages.
|
|
|
|
This should only be called by api_lib.util.apis, core.resources, gcloud meta
|
|
commands, and module tests.
|
|
"""
|
|
|
|
from __future__ import absolute_import
|
|
from __future__ import division
|
|
from __future__ import unicode_literals
|
|
|
|
from googlecloudsdk.api_lib.util import apis_util
|
|
from googlecloudsdk.api_lib.util import resource as resource_util
|
|
from googlecloudsdk.core import exceptions
|
|
from googlecloudsdk.core import log
|
|
from googlecloudsdk.core import properties
|
|
from googlecloudsdk.core import transport
|
|
from googlecloudsdk.generated_clients.apis import apis_map
|
|
import six
|
|
from six.moves.urllib.parse import urljoin
|
|
from six.moves.urllib.parse import urlparse
|
|
from six.moves.urllib.parse import urlunparse
|
|
|
|
|
|
def _GetApiNameAndAlias(api_name):
|
|
# pylint:disable=protected-access
|
|
return (apis_util._API_NAME_ALIASES.get(api_name, api_name), api_name)
|
|
|
|
|
|
def _GetDefaultVersion(api_name):
|
|
api_name, _ = _GetApiNameAndAlias(api_name)
|
|
api_vers = apis_map.MAP.get(api_name, {})
|
|
for ver, api_def in six.iteritems(api_vers):
|
|
if api_def.default_version:
|
|
return ver
|
|
return None
|
|
|
|
|
|
def _GetApiNames():
|
|
"""Returns list of avaiblable apis, ignoring the version."""
|
|
return sorted(apis_map.MAP.keys())
|
|
|
|
|
|
def _GetVersions(api_name):
|
|
"""Return available versions for given api.
|
|
|
|
Args:
|
|
api_name: str, The API name (or the command surface name, if different).
|
|
|
|
Raises:
|
|
apis_util.UnknownAPIError: If api_name does not exist in the APIs map.
|
|
|
|
Returns:
|
|
list, of version names.
|
|
"""
|
|
api_name, _ = _GetApiNameAndAlias(api_name)
|
|
version_map = apis_map.MAP.get(api_name, None)
|
|
if version_map is None:
|
|
raise apis_util.UnknownAPIError(api_name)
|
|
return list(version_map.keys())
|
|
|
|
|
|
def GetApiDef(api_name, api_version):
|
|
"""Returns the APIDef for the specified API and version.
|
|
|
|
Args:
|
|
api_name: str, The API name (or the command surface name, if different).
|
|
api_version: str, The version of the API.
|
|
|
|
Raises:
|
|
apis_util.UnknownAPIError: If api_name does not exist in the APIs map.
|
|
apis_util.UnknownVersionError: If api_version does not exist for given
|
|
api_name in the APIs map.
|
|
|
|
Returns:
|
|
APIDef, The APIDef for the specified API and version.
|
|
"""
|
|
api_name, api_name_alias = _GetApiNameAndAlias(api_name)
|
|
if api_name not in apis_map.MAP:
|
|
raise apis_util.UnknownAPIError(api_name)
|
|
|
|
version_overrides = properties.VALUES.api_client_overrides.AllValues()
|
|
|
|
# First attempt to get api specific override, then full surface override.
|
|
version_override = version_overrides.get('{}/{}'.format(
|
|
api_name, api_version))
|
|
if not version_override:
|
|
version_override = version_overrides.get(api_name_alias, None)
|
|
|
|
api_version = version_override or api_version
|
|
|
|
api_versions = apis_map.MAP[api_name]
|
|
if api_version is None or api_version not in api_versions:
|
|
raise apis_util.UnknownVersionError(api_name, api_version)
|
|
else:
|
|
api_def = api_versions[api_version]
|
|
|
|
return api_def
|
|
|
|
|
|
def _GetClientClass(api_name, api_version):
|
|
"""Returns the client class for the API specified in the args.
|
|
|
|
Args:
|
|
api_name: str, The API name (or the command surface name, if different).
|
|
api_version: str, The version of the API.
|
|
|
|
Returns:
|
|
base_api.BaseApiClient, Client class for the specified API.
|
|
"""
|
|
api_def = GetApiDef(api_name, api_version)
|
|
return _GetClientClassFromDef(api_def)
|
|
|
|
|
|
def _GetClientClassFromDef(api_def):
|
|
"""Returns the apitools client class for the API definition specified in args.
|
|
|
|
Args:
|
|
api_def: apis_map.APIDef, The definition of the API.
|
|
|
|
Returns:
|
|
base_api.BaseApiClient, Client class for the specified API.
|
|
"""
|
|
client_full_classpath = api_def.apitools.client_full_classpath
|
|
module_path, client_class_name = client_full_classpath.rsplit('.', 1)
|
|
module_obj = __import__(module_path, fromlist=[client_class_name])
|
|
return getattr(module_obj, client_class_name)
|
|
|
|
|
|
def _GetClientInstance(api_name,
|
|
api_version,
|
|
no_http=False,
|
|
http_client=None,
|
|
check_response_func=None,
|
|
http_timeout_sec=None,
|
|
region=None):
|
|
"""Returns an instance of the API client specified in the args.
|
|
|
|
Args:
|
|
api_name: str, The API name (or the command surface name, if different).
|
|
api_version: str, The version of the API.
|
|
no_http: bool, True to not create an http object for this client.
|
|
http_client: bring your own http client to use. Incompatible with
|
|
no_http=True.
|
|
check_response_func: error handling callback to give to apitools.
|
|
http_timeout_sec: int, seconds of http timeout to set, defaults if None.
|
|
region: str, Region (or multi-region) for regionalized endpoints (REP).
|
|
|
|
Returns:
|
|
base_api.BaseApiClient, An instance of the specified API client.
|
|
"""
|
|
|
|
# pylint: disable=g-import-not-at-top
|
|
if no_http:
|
|
assert http_client is None
|
|
elif http_client is None:
|
|
# Normal gcloud authentication
|
|
# Import http only when needed, as it depends on credential infrastructure
|
|
# which is not needed in all cases.
|
|
from googlecloudsdk.core.credentials import transports
|
|
http_client = transports.GetApitoolsTransport(
|
|
response_encoding=transport.ENCODING,
|
|
timeout=http_timeout_sec if http_timeout_sec else 'unset')
|
|
|
|
client_class = _GetClientClass(api_name, api_version)
|
|
client_instance = client_class(
|
|
url=_GetEffectiveApiEndpoint(api_name, api_version, client_class, region),
|
|
get_credentials=False,
|
|
http=http_client)
|
|
if check_response_func is not None:
|
|
client_instance.check_response_func = check_response_func
|
|
api_key = properties.VALUES.core.api_key.Get()
|
|
if api_key:
|
|
client_instance.AddGlobalParam('key', api_key)
|
|
header = 'X-Google-Project-Override'
|
|
client_instance.additional_http_headers[header] = 'apikey'
|
|
return client_instance
|
|
|
|
|
|
def _GetGapicClientClass(api_name,
|
|
api_version,
|
|
transport_choice=apis_util.GapicTransport.GRPC):
|
|
"""Returns the GAPIC client class for the API def specified by the args.
|
|
|
|
Args:
|
|
api_name: str, The API name (or the command surface name, if different).
|
|
api_version: str, The version of the API.
|
|
transport_choice: apis_util.GapicTransport, The transport to be used by the
|
|
client.
|
|
"""
|
|
api_def = GetApiDef(api_name, api_version)
|
|
if transport_choice == apis_util.GapicTransport.GRPC_ASYNCIO:
|
|
client_full_classpath = api_def.gapic.async_client_full_classpath
|
|
elif transport_choice == apis_util.GapicTransport.REST:
|
|
client_full_classpath = api_def.gapic.rest_client_full_classpath
|
|
else:
|
|
client_full_classpath = api_def.gapic.client_full_classpath
|
|
|
|
module_path, client_class_name = client_full_classpath.rsplit('.', 1)
|
|
module_obj = __import__(module_path, fromlist=[client_class_name])
|
|
return getattr(module_obj, client_class_name)
|
|
|
|
|
|
def _GetGapicClientInstance(
|
|
api_name,
|
|
api_version,
|
|
credentials,
|
|
address_override_func=None,
|
|
transport_choice=apis_util.GapicTransport.GRPC,
|
|
attempt_direct_path=False,
|
|
redact_request_body_reason=None,
|
|
custom_interceptors=None,
|
|
region=None,
|
|
):
|
|
"""Returns an instance of the GAPIC API client specified in the args.
|
|
|
|
For apitools API clients, the API endpoint override is something like
|
|
http://compute.googleapis.com/v1/. For GAPIC API clients, the DEFAULT_ENDPOINT
|
|
is something like compute.googleapis.com. To use the same endpoint override
|
|
property for both, we use the netloc of the API endpoint override.
|
|
|
|
Args:
|
|
api_name: str, The API name (or the command surface name, if different).
|
|
api_version: str, The version of the API.
|
|
credentials: google.auth.credentials.Credentials, the credentials to use.
|
|
address_override_func: function, function to call to override the client
|
|
host. It takes a single argument which is the original host.
|
|
transport_choice: apis_util.GapicTransport, The transport to be used by the
|
|
client.
|
|
attempt_direct_path: bool, True if we want to attempt direct path gRPC where
|
|
possible.
|
|
redact_request_body_reason: str, the reason why the request body must be
|
|
redacted if --log-http is used. If None, the body is not redacted.
|
|
custom_interceptors: list[grpc interceptor], a list of custom interceptors
|
|
to add to the channel.
|
|
region: str, Region (or multi-region) for regionalized endpoints (REP).
|
|
|
|
Returns:
|
|
An instance of the specified GAPIC API client.
|
|
"""
|
|
def AddressOverride(address):
|
|
try:
|
|
endpoint_override = properties.VALUES.api_endpoint_overrides.Property(
|
|
api_name).Get()
|
|
except properties.NoSuchPropertyError:
|
|
endpoint_override = None
|
|
|
|
if endpoint_override:
|
|
address = urlparse(endpoint_override).netloc
|
|
|
|
if address_override_func:
|
|
address = address_override_func(address)
|
|
|
|
if endpoint_override is not None:
|
|
return address
|
|
|
|
# Note that regionalized endpoints are incompatible with mTLS / non-Google
|
|
# universe domains.
|
|
if _ShouldUseRegionalEndpoints(api_name, api_version, region):
|
|
return _GetRegionalizedEndpoint(address, region)
|
|
|
|
return UniversifyAddress(address)
|
|
|
|
client_class = _GetGapicClientClass(
|
|
api_name, api_version, transport_choice=transport_choice)
|
|
|
|
return client_class(
|
|
credentials,
|
|
address_override_func=AddressOverride,
|
|
mtls_enabled=_MtlsEnabled(api_name, api_version),
|
|
attempt_direct_path=attempt_direct_path,
|
|
redact_request_body_reason=redact_request_body_reason,
|
|
custom_interceptors=custom_interceptors,
|
|
)
|
|
|
|
|
|
def UniversifyAddress(address):
|
|
"""Update a URL based on the current universe domain."""
|
|
universe_domain_property = properties.VALUES.core.universe_domain
|
|
universe_domain = universe_domain_property.Get()
|
|
if (address is not None and
|
|
universe_domain_property.default != universe_domain):
|
|
address = address.replace(universe_domain_property.default,
|
|
universe_domain, 1)
|
|
return address
|
|
|
|
|
|
def _GetRegionalizedEndpoint(domain, region):
|
|
"""Returns regionalized domain.
|
|
|
|
Regional endpoints (REPs) have the following format:
|
|
{service}.{region}.rep.googleapis.com
|
|
|
|
Args:
|
|
domain: Domain for the API in form {service}.googleapis.com.
|
|
region: REP region (or multi-region).
|
|
|
|
Returns:
|
|
str, Regionalized domain.
|
|
"""
|
|
if region is None:
|
|
raise ValueError('Region must be provided.')
|
|
if domain.count('.') > 2:
|
|
raise ValueError(
|
|
'Base URLs should have form {service}.googleapis.com;'
|
|
f' received: [{domain}].')
|
|
service, base_domain = domain.split('.', 1)
|
|
return f'{service}.{region}.rep.{base_domain}'
|
|
|
|
|
|
class UnsupportedEndpointModeError(exceptions.Error):
|
|
"""Error when regional/endpoint_mode property is unsupported by a command."""
|
|
|
|
|
|
def _ShouldUseRegionalEndpoints(api_name, api_version, region):
|
|
"""Returns whether or not regional endpoint should be used.
|
|
|
|
Depending on the user-specified regional/endpoint_mode setting and the
|
|
developer-specified command annotation, this function calculates whether or
|
|
not to use regional endpoints. In case of an invalid combination, an error
|
|
will be raised. The full matrix is as follows:
|
|
|
|
+--------------+-------------------------------------------------------------+
|
|
| | Command compatibility |
|
|
| User mode +---------------+---------------+-----------------------------+
|
|
| | Only supports | Only supports | Supports both |
|
|
| | global | regional | |
|
|
+--------------+---------------+---------------+-----------------------------+
|
|
| regional | Error | Regional | Regional client |
|
|
| | | client | |
|
|
+--------------+---------------+---------------+-----------------------------+
|
|
| global | Global client | Error | Global client |
|
|
+--------------+---------------+---------------+-----------------------------+
|
|
| regional- | Global client | Regional | If region available: |
|
|
| preferred | | client | Regional client |
|
|
| | | | Otherwise: Global client |
|
|
+--------------+---------------+---------------+-----------------------------+
|
|
| (unset) | Global client | Regional | Global client |
|
|
| | | client | |
|
|
+--------------+---------------+---------------+-----------------------------+
|
|
|
|
Args:
|
|
api_name: str, The API name.
|
|
api_version: str, The version of the API.
|
|
region: The region (or multi-region).
|
|
Returns:
|
|
bool, Whether to use regional endpoints.
|
|
Raises:
|
|
UnsupportedEndpointModeError: If the command does not support the given
|
|
endpoint mode.
|
|
"""
|
|
mode = properties.VALUES.regional.endpoint_mode.Get()
|
|
compatibility = properties.VALUES.regional.endpoint_compatibility.Get()
|
|
if mode == properties.VALUES.regional.REGIONAL and compatibility is None:
|
|
raise UnsupportedEndpointModeError(
|
|
f'The regional/endpoint_mode property is currently set to [{mode}], but'
|
|
' this command does not support regional endpoints.')
|
|
if (
|
|
mode == properties.VALUES.regional.GLOBAL and
|
|
compatibility == properties.VALUES.regional.REQUIRED
|
|
):
|
|
raise UnsupportedEndpointModeError(
|
|
f'The regional/endpoint_mode property is currently set to [{mode}], but'
|
|
' this command only supports regional endpoints.')
|
|
|
|
# Global.
|
|
if (
|
|
compatibility is None
|
|
or mode == properties.VALUES.regional.GLOBAL
|
|
or (
|
|
mode is None
|
|
and compatibility == properties.VALUES.regional.SUPPORTED
|
|
)
|
|
):
|
|
return False
|
|
|
|
# Regional if available.
|
|
known_available_regions = GetApiDef(api_name, api_version).regional_endpoints
|
|
if (
|
|
mode == properties.VALUES.regional.REGIONAL_PREFERRED
|
|
and compatibility == properties.VALUES.regional.SUPPORTED
|
|
):
|
|
return region in known_available_regions
|
|
|
|
# Command requires or user explicitly requested regional endpoints.
|
|
if region not in known_available_regions:
|
|
# If the requested region isn't known to be available, it could be because
|
|
# this version of gcloud is older and doesn't know about it yet. We let the
|
|
# request through and fail open in this case rather than error out here.
|
|
log.info(
|
|
'API [%s] does not contain a known regional endpoint for region [%s].',
|
|
api_name, region)
|
|
return True
|
|
|
|
|
|
def _GetMtlsEndpoint(api_name, api_version, client_class=None):
|
|
"""Returns mtls endpoint."""
|
|
api_def = GetApiDef(api_name, api_version)
|
|
if api_def.apitools:
|
|
client_class = client_class or _GetClientClass(api_name, api_version)
|
|
else:
|
|
client_class = client_class or _GetGapicClientClass(api_name, api_version)
|
|
return api_def.mtls_endpoint_override or client_class.MTLS_BASE_URL
|
|
|
|
|
|
def _MtlsEnabled(api_name, api_version):
|
|
"""Checks if the API of the given version should use mTLS.
|
|
|
|
If context_aware/always_use_mtls_endpoint is True, then mTLS will always be
|
|
used.
|
|
|
|
If context_aware/use_client_certificate is True, then mTLS will be used only
|
|
if the API version is in the mTLS allowlist.
|
|
|
|
gcloud maintains a client-side allowlist for the mTLS feature
|
|
(go/gcloud-rollout-mtls).
|
|
|
|
Args:
|
|
api_name: str, The API name.
|
|
api_version: str, The version of the API.
|
|
|
|
Returns:
|
|
True if the given service and version is in the mTLS allowlist.
|
|
"""
|
|
if properties.VALUES.context_aware.always_use_mtls_endpoint.GetBool():
|
|
return True
|
|
|
|
if not properties.VALUES.context_aware.use_client_certificate.GetBool():
|
|
return False
|
|
|
|
api_def = GetApiDef(api_name, api_version)
|
|
return api_def.enable_mtls
|
|
|
|
|
|
def _BuildEndpointOverride(endpoint_override, base_url):
|
|
"""Constructs a normalized endpoint URI depending on the client base_url."""
|
|
url_base = urlparse(base_url)
|
|
url_endpoint_override = urlparse(endpoint_override)
|
|
if url_base.path == '/' or url_endpoint_override.path != '/':
|
|
return endpoint_override
|
|
return urljoin(
|
|
'{}://{}'.format(url_endpoint_override.scheme,
|
|
url_endpoint_override.netloc), url_base.path)
|
|
|
|
|
|
def _GetBaseUrlFromApi(api_name, api_version):
|
|
"""Returns base url for given api."""
|
|
if GetApiDef(api_name, api_version).apitools:
|
|
client_class = _GetClientClass(api_name, api_version)
|
|
else:
|
|
client_class = _GetGapicClientClass(api_name, api_version)
|
|
|
|
if hasattr(client_class, 'BASE_URL'):
|
|
client_base_url = client_class.BASE_URL
|
|
else:
|
|
try:
|
|
client_base_url = _GetResourceModule(api_name, api_version).BASE_URL
|
|
except AttributeError:
|
|
client_base_url = 'https://{}.googleapis.com/{}'.format(
|
|
api_name, api_version
|
|
)
|
|
return UniversifyAddress(client_base_url)
|
|
|
|
|
|
def _GetEffectiveApiEndpoint(
|
|
api_name, api_version, client_class=None, region=None):
|
|
"""Returns effective endpoint for given api."""
|
|
try:
|
|
endpoint_override = properties.VALUES.api_endpoint_overrides.Property(
|
|
api_name).Get()
|
|
except properties.NoSuchPropertyError:
|
|
endpoint_override = None
|
|
|
|
api_def = GetApiDef(api_name, api_version)
|
|
if api_def.apitools:
|
|
client_class = client_class or _GetClientClass(api_name, api_version)
|
|
else:
|
|
client_class = client_class or _GetGapicClientClass(api_name, api_version)
|
|
client_base_url = _GetBaseUrlFromApi(api_name, api_version)
|
|
if endpoint_override:
|
|
address = _BuildEndpointOverride(endpoint_override, client_base_url)
|
|
elif _MtlsEnabled(api_name, api_version):
|
|
address = UniversifyAddress(
|
|
_GetMtlsEndpoint(api_name, api_version, client_class)
|
|
)
|
|
# Note that regionalized endpoints are incompatible with mTLS / non-Google
|
|
# universe domains.
|
|
elif _ShouldUseRegionalEndpoints(api_name, api_version, region):
|
|
url_base = urlparse(client_base_url)
|
|
regionalized_domain = _GetRegionalizedEndpoint(url_base.netloc, region)
|
|
address = urlunparse(url_base._replace(netloc=regionalized_domain))
|
|
else:
|
|
address = client_base_url
|
|
|
|
return address
|
|
|
|
|
|
def _GetMessagesModule(api_name, api_version):
|
|
"""Returns the messages module for the API specified in the args.
|
|
|
|
Args:
|
|
api_name: str, The API name (or the command surface name, if different).
|
|
api_version: str, The version of the API.
|
|
|
|
Returns:
|
|
Module containing the definitions of messages for the specified API.
|
|
"""
|
|
api_def = GetApiDef(api_name, api_version)
|
|
# fromlist below must not be empty, see:
|
|
# http://stackoverflow.com/questions/2724260/why-does-pythons-import-require-fromlist.
|
|
return __import__(
|
|
api_def.apitools.messages_full_modulepath, fromlist=['something'])
|
|
|
|
|
|
def _GetResourceModule(api_name, api_version):
|
|
"""Imports and returns given api resources module."""
|
|
|
|
api_def = GetApiDef(api_name, api_version)
|
|
# fromlist below must not be empty, see:
|
|
# http://stackoverflow.com/questions/2724260/why-does-pythons-import-require-fromlist.
|
|
if api_def.apitools:
|
|
return __import__(
|
|
api_def.apitools.class_path + '.' + 'resources', fromlist=['something']
|
|
)
|
|
# ApiDef must be gapic-only:
|
|
return __import__(
|
|
api_def.gapic.class_path + '.' + 'resources', fromlist=['something']
|
|
)
|
|
|
|
|
|
def _GetApiCollections(api_name, api_version):
|
|
"""Yields all collections for for given api."""
|
|
|
|
try:
|
|
resources_module = _GetResourceModule(api_name, api_version)
|
|
except ImportError:
|
|
pass
|
|
else:
|
|
for collection in resources_module.Collections:
|
|
yield resource_util.CollectionInfo(
|
|
api_name,
|
|
api_version,
|
|
resources_module.BASE_URL,
|
|
resources_module.DOCS_URL,
|
|
collection.collection_name,
|
|
collection.path,
|
|
collection.flat_paths,
|
|
collection.params,
|
|
collection.enable_uri_parsing,
|
|
)
|