File: //snap/google-cloud-cli/current/lib/googlecloudsdk/command_lib/apigee/request.py
# -*- 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