237 lines
8.4 KiB
Python
237 lines
8.4 KiB
Python
# -*- coding: utf-8 -*- # Lint as: python3
|
|
# Copyright 2020 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.
|
|
"""Generalized Apigee Management API request handler.
|
|
|
|
The Apigee Management APIs were designed before One Platform, and include some
|
|
design decisions incompatible with apitools (see b/151099218). So the gcloud
|
|
apigee surface must make its own HTTPS requests instead of relying on an
|
|
apitools-generated client.
|
|
"""
|
|
|
|
from __future__ import absolute_import
|
|
from __future__ import division
|
|
from __future__ import unicode_literals
|
|
|
|
import collections
|
|
import json
|
|
|
|
from googlecloudsdk.command_lib.apigee import defaults
|
|
from googlecloudsdk.command_lib.apigee import errors
|
|
from googlecloudsdk.command_lib.apigee import resource_args
|
|
from googlecloudsdk.core import properties
|
|
from googlecloudsdk.core.credentials import requests
|
|
from six.moves import urllib
|
|
|
|
|
|
APIGEE_GLOBAL_HOST = "apigee.googleapis.com"
|
|
APIGEE_LEP_HOST = "%s-apigee.googleapis.com"
|
|
ERROR_FIELD = "error"
|
|
MESSAGE_FIELD = "message"
|
|
|
|
|
|
def _ResourceIdentifier(identifiers, entity_path):
|
|
"""Returns an OrderedDict uniquely identifying the resource to be accessed.
|
|
|
|
Args:
|
|
identifiers: a collection that maps entity type names to identifiers.
|
|
entity_path: a list of entity type names from least to most specific.
|
|
|
|
Raises:
|
|
MissingIdentifierError: an entry in entity_path is missing from
|
|
`identifiers`.
|
|
"""
|
|
resource_identifier = collections.OrderedDict()
|
|
|
|
for entity_name in entity_path:
|
|
entity = resource_args.ENTITIES[entity_name]
|
|
id_key = entity.plural + "Id"
|
|
if id_key not in identifiers or identifiers[id_key] is None:
|
|
raise errors.MissingIdentifierError(entity.singular)
|
|
resource_identifier[entity] = identifiers[id_key]
|
|
return resource_identifier
|
|
|
|
|
|
def _Communicate(url, method, body, headers):
|
|
"""Returns HTTP status, reason, and response body for a given HTTP request."""
|
|
response = requests.GetSession().request(
|
|
method, url, data=body, headers=headers, stream=True)
|
|
status = response.status_code
|
|
reason = response.reason
|
|
data = response.content
|
|
return status, reason, data
|
|
|
|
|
|
def _DecodeResponse(response):
|
|
"""Returns decoded string.
|
|
|
|
Args:
|
|
response: the raw string or bytes of JSON data
|
|
|
|
Raises:
|
|
ValueError: failure to load/decode JSON data
|
|
"""
|
|
# In older versions of Python 3, the built-in JSON library will only
|
|
# accept strings, not bytes.
|
|
if not isinstance(response, str) and hasattr(response, "decode"):
|
|
response = response.decode()
|
|
return response
|
|
|
|
|
|
def _GetResourceType(entity_collection, entity_path):
|
|
"""Gets resource type from the inputed data."""
|
|
return entity_collection or entity_path[-1]
|
|
|
|
|
|
def _BuildErrorIdentifier(resource_identifier):
|
|
"""Builds error identifier from inputed data."""
|
|
return collections.OrderedDict([
|
|
(key.singular, value) for key, value in resource_identifier.items()
|
|
])
|
|
|
|
|
|
def _ExtractErrorMessage(response):
|
|
"""Extracts error message from response, returns None if message not found."""
|
|
json_response = json.loads(response)
|
|
if ERROR_FIELD in json_response and isinstance(
|
|
json_response[ERROR_FIELD],
|
|
dict) and MESSAGE_FIELD in json_response[ERROR_FIELD]:
|
|
return json_response[ERROR_FIELD][MESSAGE_FIELD]
|
|
return None
|
|
|
|
|
|
def _GetApigeeHostByOrganization(organization):
|
|
"""Returns the Apigee host based on the organization."""
|
|
location = defaults.GetOrganizationLocation(organization)
|
|
return _GetApigeeHostByLocation(location)
|
|
|
|
|
|
def _GetApigeeHostByLocation(location=None):
|
|
"""Returns the Apigee host based on the location."""
|
|
if location is None or location == "global" or not location:
|
|
return APIGEE_GLOBAL_HOST
|
|
|
|
return APIGEE_LEP_HOST % location
|
|
|
|
|
|
def ResponseToApiRequest(identifiers,
|
|
entity_path,
|
|
entity_collection=None,
|
|
method="GET",
|
|
query_params=None,
|
|
accept_mimetype=None,
|
|
body=None,
|
|
body_mimetype="application/json",
|
|
method_override=None,
|
|
location=None):
|
|
"""Makes a request to the Apigee API and returns the response.
|
|
|
|
Args:
|
|
identifiers: a collection that maps entity type names to identifiers.
|
|
entity_path: a list of entity type names from least to most specific.
|
|
entity_collection: if provided, the final entity type; the request will not
|
|
be specific as to which entity of that type is being referenced.
|
|
method: an HTTP method string specifying what to do with the accessed
|
|
entity. If the method begins with a colon, it will be interpreted as a
|
|
Cloud custom method (https://cloud.google.com/apis/design/custom_methods)
|
|
and appended to the request URL with the POST HTTP method.
|
|
query_params: any extra query parameters to be sent in the request.
|
|
accept_mimetype: the mimetype to expect in the response body. If not
|
|
provided, the response will be parsed as JSON.
|
|
body: data to send in the request body.
|
|
body_mimetype: the mimetype of the body data, if not JSON.
|
|
method_override: the HTTP method to use for the request, when method starts
|
|
with a colon.
|
|
location: the location of the apigee organization.
|
|
|
|
Returns:
|
|
an object containing the API's response. If accept_mimetype was set, this
|
|
will be raw bytes. Otherwise, it will be a parsed JSON object.
|
|
|
|
Raises:
|
|
MissingIdentifierError: an entry in entity_path is missing from
|
|
`identifiers`.
|
|
RequestError: if the request itself fails.
|
|
"""
|
|
headers = {}
|
|
if body:
|
|
headers["Content-Type"] = body_mimetype
|
|
if accept_mimetype:
|
|
headers["Accept"] = accept_mimetype
|
|
|
|
resource_identifier = _ResourceIdentifier(identifiers, entity_path)
|
|
url_path_elements = ["v1"]
|
|
for key, value in resource_identifier.items():
|
|
url_path_elements += [key.plural, urllib.parse.quote(value)]
|
|
if entity_collection:
|
|
collection_name = resource_args.ENTITIES[entity_collection].plural
|
|
url_path_elements.append(urllib.parse.quote(collection_name))
|
|
|
|
query_string = urllib.parse.urlencode(query_params) if query_params else ""
|
|
|
|
endpoint_override = properties.VALUES.api_endpoint_overrides.apigee.Get()
|
|
if location:
|
|
scheme = "https"
|
|
# Construct the host based on the location.
|
|
host = _GetApigeeHostByLocation(location)
|
|
elif endpoint_override:
|
|
endpoint = urllib.parse.urlparse(endpoint_override)
|
|
scheme = endpoint.scheme
|
|
host = endpoint.netloc
|
|
else:
|
|
scheme = "https"
|
|
# Construct the host based on the organization location.
|
|
organization = identifiers.get("organizationsId", None)
|
|
host = _GetApigeeHostByOrganization(organization)
|
|
|
|
url_path = "/".join(url_path_elements)
|
|
if method and method[0] == ":":
|
|
url_path += method
|
|
method = "POST"
|
|
if method_override:
|
|
method = method_override
|
|
url = urllib.parse.urlunparse((scheme, host, url_path, "", query_string, ""))
|
|
|
|
status, reason, response = _Communicate(url, method, body, headers)
|
|
|
|
if status >= 400:
|
|
resource_type = _GetResourceType(entity_collection, entity_path)
|
|
if status == 404:
|
|
exception_class = errors.EntityNotFoundError
|
|
elif status in (401, 403):
|
|
exception_class = errors.UnauthorizedRequestError
|
|
else:
|
|
exception_class = errors.RequestError
|
|
error_identifier = _BuildErrorIdentifier(resource_identifier)
|
|
|
|
try:
|
|
user_help = _ExtractErrorMessage(_DecodeResponse(response))
|
|
except ValueError:
|
|
user_help = None
|
|
|
|
raise exception_class(resource_type, error_identifier, method,
|
|
reason, response, user_help=user_help)
|
|
|
|
if accept_mimetype is None:
|
|
try:
|
|
response = _DecodeResponse(response)
|
|
response = json.loads(response)
|
|
except ValueError as error:
|
|
resource_type = _GetResourceType(entity_collection, entity_path)
|
|
error_identifier = _BuildErrorIdentifier(resource_identifier)
|
|
raise errors.ResponseNotJSONError(error, resource_type, error_identifier,
|
|
response)
|
|
|
|
return response
|