blob: 51409a5711ca8f8daebc2d69b4b7e3105c32a91b [file] [log] [blame]
# Copyright 2016 Google Inc. 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.
"""Hook into the live Discovery service and get API configuration info."""
# pylint: disable=g-bad-name
from __future__ import absolute_import
import json
import logging
from . import api_config
from . import directory_list_generator
from . import discovery_generator
from . import util
_logger = logging.getLogger(__name__)
class DiscoveryService(object):
"""Implements the local discovery service.
This has a static minimal version of the discoverable part of the
discovery .api file.
It only handles returning the discovery doc and directory, and ignores
directory parameters to filter the results.
The discovery docs/directory are created by calling a Cloud Endpoints
discovery service to generate the discovery docs/directory from an .api
file/set of .api files.
"""
_GET_REST_API = 'apisdev.getRest'
_GET_RPC_API = 'apisdev.getRpc'
_LIST_API = 'apisdev.list'
API_CONFIG = {
'name': 'discovery',
'version': 'v1',
'api_version': 'v1',
'path_version': 'v1',
'methods': {
'discovery.apis.getRest': {
'path': 'apis/{api}/{version}/rest',
'httpMethod': 'GET',
'rosyMethod': _GET_REST_API,
},
'discovery.apis.getRpc': {
'path': 'apis/{api}/{version}/rpc',
'httpMethod': 'GET',
'rosyMethod': _GET_RPC_API,
},
'discovery.apis.list': {
'path': 'apis',
'httpMethod': 'GET',
'rosyMethod': _LIST_API,
},
}
}
def __init__(self, config_manager, backend):
"""Initializes an instance of the DiscoveryService.
Args:
config_manager: An instance of ApiConfigManager.
backend: An _ApiServer instance for API config generation.
"""
self._config_manager = config_manager
self._backend = backend
def _send_success_response(self, response, start_response):
"""Sends an HTTP 200 json success response.
This calls start_response and returns the response body.
Args:
response: A string containing the response body to return.
start_response: A function with semantics defined in PEP-333.
Returns:
A string, the response body.
"""
headers = [('Content-Type', 'application/json; charset=UTF-8')]
return util.send_wsgi_response('200 OK', headers, response, start_response)
def _get_rest_doc(self, request, start_response):
"""Sends back HTTP response with API directory.
This calls start_response and returns the response body. It will return
the discovery doc for the requested api/version.
Args:
request: An ApiRequest, the transformed request sent to the Discovery API.
start_response: A function with semantics defined in PEP-333.
Returns:
A string, the response body.
"""
api = request.body_json['api']
version = request.body_json['version']
generator = discovery_generator.DiscoveryGenerator(request=request)
services = [s for s in self._backend.api_services if
s.api_info.name == api and s.api_info.api_version == version]
doc = generator.pretty_print_config_to_json(services)
if not doc:
error_msg = ('Failed to convert .api to discovery doc for '
'version %s of api %s') % (version, api)
_logger.error('%s', error_msg)
return util.send_wsgi_error_response(error_msg, start_response)
return self._send_success_response(doc, start_response)
def _generate_api_config_with_root(self, request):
"""Generate an API config with a specific root hostname.
This uses the backend object and the ApiConfigGenerator to create an API
config specific to the hostname of the incoming request. This allows for
flexible API configs for non-standard environments, such as localhost.
Args:
request: An ApiRequest, the transformed request sent to the Discovery API.
Returns:
A string representation of the generated API config.
"""
actual_root = self._get_actual_root(request)
generator = api_config.ApiConfigGenerator()
api = request.body_json['api']
version = request.body_json['version']
lookup_key = (api, version)
service_factories = self._backend.api_name_version_map.get(lookup_key)
if not service_factories:
return None
service_classes = [service_factory.service_class
for service_factory in service_factories]
config_dict = generator.get_config_dict(
service_classes, hostname=actual_root)
# Save to cache
for config in config_dict.get('items', []):
lookup_key_with_root = (
config.get('name', ''), config.get('version', ''), actual_root)
self._config_manager.save_config(lookup_key_with_root, config)
return config_dict
def _get_actual_root(self, request):
url = request.server
# Append the port if not the default
if ((request.url_scheme == 'https' and request.port != '443') or
(request.url_scheme != 'https' and request.port != '80')):
url += ':%s' % request.port
return url
def _list(self, request, start_response):
"""Sends HTTP response containing the API directory.
This calls start_response and returns the response body.
Args:
request: An ApiRequest, the transformed request sent to the Discovery API.
start_response: A function with semantics defined in PEP-333.
Returns:
A string containing the response body.
"""
configs = []
generator = directory_list_generator.DirectoryListGenerator(request)
for config in self._config_manager.configs.values():
if config != self.API_CONFIG:
configs.append(config)
directory = generator.pretty_print_config_to_json(configs)
if not directory:
_logger.error('Failed to get API directory')
# By returning a 404, code explorer still works if you select the
# API in the URL
return util.send_wsgi_not_found_response(start_response)
return self._send_success_response(directory, start_response)
def handle_discovery_request(self, path, request, start_response):
"""Returns the result of a discovery service request.
This calls start_response and returns the response body.
Args:
path: A string containing the API path (the portion of the path
after /_ah/api/).
request: An ApiRequest, the transformed request sent to the Discovery API.
start_response: A function with semantics defined in PEP-333.
Returns:
The response body. Or returns False if the request wasn't handled by
DiscoveryService.
"""
if path == self._GET_REST_API:
return self._get_rest_doc(request, start_response)
elif path == self._GET_RPC_API:
error_msg = ('RPC format documents are no longer supported with the '
'Endpoints Framework for Python. Please use the REST '
'format.')
_logger.error('%s', error_msg)
return util.send_wsgi_error_response(error_msg, start_response)
elif path == self._LIST_API:
return self._list(request, start_response)
return False