Merge "Convert json_home and version discovery to Flask"
This commit is contained in:
commit
1efa27e4ed
|
@ -0,0 +1,16 @@
|
|||
# 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.
|
||||
|
||||
from keystone.api import discovery
|
||||
|
||||
__all__ = ('discovery',)
|
||||
__apis__ = (discovery,)
|
|
@ -0,0 +1,142 @@
|
|||
# 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.
|
||||
|
||||
import flask
|
||||
from flask import request
|
||||
from oslo_serialization import jsonutils
|
||||
from six.moves import http_client
|
||||
|
||||
from keystone.common import json_home
|
||||
from keystone.common import wsgi
|
||||
import keystone.conf
|
||||
from keystone import exception
|
||||
|
||||
|
||||
CONF = keystone.conf.CONF
|
||||
MEDIA_TYPE_JSON = 'application/vnd.openstack.identity-%s+json'
|
||||
_VERSIONS = []
|
||||
_DISCOVERY_BLUEPRINT = flask.Blueprint('Discovery', __name__)
|
||||
|
||||
|
||||
def register_version(version):
|
||||
_VERSIONS.append(version)
|
||||
|
||||
|
||||
def _get_versions_list(identity_url):
|
||||
versions = {}
|
||||
if 'v3' in _VERSIONS:
|
||||
versions['v3'] = {
|
||||
'id': 'v3.10',
|
||||
'status': 'stable',
|
||||
'updated': '2018-02-28T00:00:00Z',
|
||||
'links': [
|
||||
{
|
||||
'rel': 'self',
|
||||
'href': identity_url,
|
||||
}
|
||||
],
|
||||
'media-types': [
|
||||
{
|
||||
'base': 'application/json',
|
||||
'type': MEDIA_TYPE_JSON % 'v3'
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
return versions
|
||||
|
||||
|
||||
class MimeTypes(object):
|
||||
JSON = 'application/json'
|
||||
JSON_HOME = 'application/json-home'
|
||||
|
||||
|
||||
def _v3_json_home_content():
|
||||
# TODO(morgan): Eliminate this, we should never be disabling an API version
|
||||
# now, JSON Home should never be empty.
|
||||
if 'v3' not in _VERSIONS:
|
||||
# No V3 Support, so return an empty JSON Home document.
|
||||
return {'resources': {}}
|
||||
return json_home.JsonHomeResources.resources()
|
||||
|
||||
|
||||
def v3_mime_type_best_match():
|
||||
if not request.accept_mimetypes:
|
||||
return MimeTypes.JSON
|
||||
|
||||
return request.accept_mimetypes.best_match(
|
||||
[MimeTypes.JSON, MimeTypes.JSON_HOME])
|
||||
|
||||
|
||||
@_DISCOVERY_BLUEPRINT.route('/')
|
||||
def get_versions():
|
||||
if v3_mime_type_best_match() == MimeTypes.JSON_HOME:
|
||||
# RENDER JSON-Home form, we have a clever client who will
|
||||
# understand the JSON-Home document.
|
||||
v3_json_home = _v3_json_home_content()
|
||||
json_home.translate_urls(v3_json_home, '/v3')
|
||||
return flask.Response(response=jsonutils.dumps(v3_json_home),
|
||||
mimetype=MimeTypes.JSON_HOME)
|
||||
else:
|
||||
# NOTE(morgan): wsgi.Application.base_url will eventually need to
|
||||
# be moved to a better "common" location. For now, we'll just lean
|
||||
# on it for the sake of leaning on common code where possible.
|
||||
identity_url = '%s/v3/' % wsgi.Application.base_url(
|
||||
context={'environment': request.environ})
|
||||
versions = _get_versions_list(identity_url)
|
||||
return flask.Response(
|
||||
response=jsonutils.dumps(
|
||||
{'versions': {
|
||||
'values': list(versions.values())}}),
|
||||
mimetype=MimeTypes.JSON,
|
||||
status=http_client.MULTIPLE_CHOICES)
|
||||
|
||||
|
||||
@_DISCOVERY_BLUEPRINT.route('/v3')
|
||||
def get_version_v3():
|
||||
if 'v3' not in _VERSIONS:
|
||||
raise exception.VersionNotFound(version='v3')
|
||||
|
||||
if v3_mime_type_best_match() == MimeTypes.JSON_HOME:
|
||||
# RENDER JSON-Home form, we have a clever client who will
|
||||
# understand the JSON-Home document.
|
||||
content = _v3_json_home_content()
|
||||
return flask.Response(response=jsonutils.dumps(content),
|
||||
mimetype=MimeTypes.JSON_HOME)
|
||||
else:
|
||||
# NOTE(morgan): wsgi.Application.base_url will eventually need to
|
||||
# be moved to a better "common" location. For now, we'll just lean
|
||||
# on it for the sake of leaning on common code where possible.
|
||||
identity_url = '%s/v3/' % wsgi.Application.base_url(
|
||||
context={'environment': request.environ})
|
||||
versions = _get_versions_list(identity_url)
|
||||
return flask.Response(
|
||||
response=jsonutils.dumps({'version': versions['v3']}),
|
||||
mimetype=MimeTypes.JSON)
|
||||
|
||||
|
||||
class DiscoveryAPI(object):
|
||||
# NOTE(morgan): The Discovery Bits are so special they cannot conform to
|
||||
# Flask-RESTful-isms. We are using straight flask Blueprint(s) here so that
|
||||
# we have a lot more control over what the heck is going on. This is just
|
||||
# a stub object to ensure we can load discovery in the same manner we
|
||||
# handle the rest of keystone.api
|
||||
|
||||
@staticmethod
|
||||
def instantiate_and_register_to_app(flask_app):
|
||||
# This is a lot more magical than the normal setup as the discovery
|
||||
# API does not lean on flask-restful. We're statically building a
|
||||
# single blueprint here.
|
||||
flask_app.register_blueprint(_DISCOVERY_BLUEPRINT)
|
||||
|
||||
|
||||
APIs = (DiscoveryAPI,)
|
|
@ -72,7 +72,7 @@ class Routers(wsgi.RoutersBase):
|
|||
PATH_ENDPOINT_GROUP_PROJECTS = PATH_ENDPOINT_GROUPS + (
|
||||
'/projects/{project_id}')
|
||||
|
||||
_path_prefixes = (PATH_PREFIX, 'regions', 'endpoints', 'services')
|
||||
_path_prefixes = ('OS-EP-FILTER', 'regions', 'endpoints', 'services')
|
||||
|
||||
def append_v3_routers(self, mapper, routers):
|
||||
regions_controller = controllers.RegionV3()
|
||||
|
|
|
@ -23,7 +23,7 @@ from oslo_middleware import healthcheck
|
|||
import routes
|
||||
import werkzeug.wsgi
|
||||
|
||||
|
||||
import keystone.api
|
||||
from keystone.application_credential import routers as app_cred_routers
|
||||
from keystone.assignment import routers as assignment_routers
|
||||
from keystone.auth import routers as auth_routers
|
||||
|
@ -42,8 +42,6 @@ from keystone.resource import routers as resource_routers
|
|||
from keystone.revoke import routers as revoke_routers
|
||||
from keystone.token import _simple_cert as simple_cert_ext
|
||||
from keystone.trust import routers as trust_routers
|
||||
from keystone.version import controllers as version_controllers
|
||||
from keystone.version import routers as version_routers
|
||||
|
||||
|
||||
LOG = log.getLogger(__name__)
|
||||
|
@ -105,72 +103,53 @@ class KeystoneDispatcherMiddleware(werkzeug.wsgi.DispatcherMiddleware):
|
|||
a non-native flask Mapper.
|
||||
"""
|
||||
|
||||
@property
|
||||
def config(self):
|
||||
return self.app.config
|
||||
|
||||
def __call__(self, environ, start_response):
|
||||
script = environ.get('PATH_INFO', '')
|
||||
original_script_name = environ.get('SCRIPT_NAME', '')
|
||||
last_element = ''
|
||||
path_info = ''
|
||||
# NOTE(morgan): Special Case root documents per version, these *are*
|
||||
# special and should never fall through to the legacy dispatcher, they
|
||||
# must be handled by the version dispatchers.
|
||||
if script not in ('/v3', '/', '/v2.0'):
|
||||
while '/' in script:
|
||||
if script in self.mounts:
|
||||
LOG.debug('Dispatching request to legacy mapper: %s',
|
||||
script)
|
||||
app = self.mounts[script]
|
||||
# NOTE(morgan): Simply because we're doing something "odd"
|
||||
# here and internally routing magically to another "wsgi"
|
||||
# router even though we're already deep in the stack we
|
||||
# need to re-add the last element pulled off. This is 100%
|
||||
# legacy and only applies to the "apps" that make up each
|
||||
# keystone subsystem.
|
||||
#
|
||||
# This middleware is only used in support of the transition
|
||||
# process from webob and home-rolled WSGI framework to
|
||||
# Flask
|
||||
if script.rindex('/') > 0:
|
||||
script, last_element = script.rsplit('/', 1)
|
||||
last_element = '/%s' % last_element
|
||||
environ['SCRIPT_NAME'] = original_script_name + script
|
||||
# Ensure there is only 1 slash between these items, the
|
||||
# mapper gets horribly confused if we have // in there,
|
||||
# which occasionally. As this is temporary to dispatch
|
||||
# to the Legacy mapper, fix the string until we no longer
|
||||
# need this logic.
|
||||
environ['PATH_INFO'] = '%s/%s' % (last_element.rstrip('/'),
|
||||
path_info.strip('/'))
|
||||
break
|
||||
script, last_item = script.rsplit('/', 1)
|
||||
path_info = '/%s%s' % (last_item, path_info)
|
||||
else:
|
||||
app = self.mounts.get(script, self.app)
|
||||
if app != self.app:
|
||||
LOG.debug('Dispatching (fallthrough) request to legacy '
|
||||
'mapper: %s', script)
|
||||
else:
|
||||
LOG.debug('Dispatching back to Flask native app.')
|
||||
while '/' in script:
|
||||
if script in self.mounts:
|
||||
LOG.debug('Dispatching request to legacy mapper: %s',
|
||||
script)
|
||||
app = self.mounts[script]
|
||||
# NOTE(morgan): Simply because we're doing something "odd"
|
||||
# here and internally routing magically to another "wsgi"
|
||||
# router even though we're already deep in the stack we
|
||||
# need to re-add the last element pulled off. This is 100%
|
||||
# legacy and only applies to the "apps" that make up each
|
||||
# keystone subsystem.
|
||||
#
|
||||
# This middleware is only used in support of the transition
|
||||
# process from webob and home-rolled WSGI framework to
|
||||
# Flask
|
||||
if script.rindex('/') > 0:
|
||||
script, last_element = script.rsplit('/', 1)
|
||||
last_element = '/%s' % last_element
|
||||
environ['SCRIPT_NAME'] = original_script_name + script
|
||||
environ['PATH_INFO'] = path_info
|
||||
# Ensure there is only 1 slash between these items, the
|
||||
# mapper gets horribly confused if we have // in there,
|
||||
# which occasionally. As this is temporary to dispatch
|
||||
# to the Legacy mapper, fix the string until we no longer
|
||||
# need this logic.
|
||||
environ['PATH_INFO'] = '%s/%s' % (last_element.rstrip('/'),
|
||||
path_info.strip('/'))
|
||||
break
|
||||
script, last_item = script.rsplit('/', 1)
|
||||
path_info = '/%s%s' % (last_item, path_info)
|
||||
else:
|
||||
# Special casing for version discovery docs.
|
||||
# REMOVE THIS SPECIAL CASE WHEN VERSION DISCOVERY GOES FLASK NATIVE
|
||||
app = self.mounts.get(script, self.app)
|
||||
if script == '/':
|
||||
# ROOT Version Discovery Doc
|
||||
LOG.debug('Dispatching to legacy root mapper for root version '
|
||||
'discovery document: `%s`', script)
|
||||
environ['SCRIPT_NAME'] = '/'
|
||||
environ['PATH_INFO'] = '/'
|
||||
elif script == '/v3':
|
||||
LOG.debug('Dispatching to legacy mapper for v3 version '
|
||||
'discovery document: `%s`', script)
|
||||
# V3 Version Discovery Doc
|
||||
environ['SCRIPT_NAME'] = '/v3'
|
||||
environ['PATH_INFO'] = '/'
|
||||
if app != self.app:
|
||||
LOG.debug('Dispatching (fallthrough) request to legacy '
|
||||
'mapper: %s', script)
|
||||
else:
|
||||
LOG.debug('Dispatching to flask native app for version '
|
||||
'discovery document: `%s`', script)
|
||||
LOG.debug('Dispatching back to Flask native app.')
|
||||
environ['SCRIPT_NAME'] = original_script_name + script
|
||||
environ['PATH_INFO'] = path_info
|
||||
|
||||
# NOTE(morgan): remove extra trailing slashes so the mapper can do the
|
||||
# right thing and get the requests mapped to the right place. For
|
||||
|
@ -183,6 +162,18 @@ class KeystoneDispatcherMiddleware(werkzeug.wsgi.DispatcherMiddleware):
|
|||
return app(environ, start_response)
|
||||
|
||||
|
||||
class _ComposibleRouterStub(keystone_wsgi.ComposableRouter):
|
||||
def __init__(self, routers):
|
||||
self._routers = routers
|
||||
|
||||
|
||||
def _add_vary_x_auth_token_header(response):
|
||||
# Add the expected Vary Header, this is run after every request in the
|
||||
# response-phase
|
||||
response.headers['Vary'] = 'X-Auth-Token'
|
||||
return response
|
||||
|
||||
|
||||
@fail_gracefully
|
||||
def application_factory(name='public'):
|
||||
if name not in ('admin', 'public'):
|
||||
|
@ -192,6 +183,12 @@ def application_factory(name='public'):
|
|||
# NOTE(morgan): The Flask App actually dispatches nothing until we migrate
|
||||
# some routers to Flask-Blueprints, it is simply a placeholder.
|
||||
app = flask.Flask(name)
|
||||
app.after_request(_add_vary_x_auth_token_header)
|
||||
|
||||
# NOTE(morgan): Configure the Flask Environment for our needs.
|
||||
app.config.update(
|
||||
# We want to bubble up Flask Exceptions (for now)
|
||||
PROPAGATE_EXCEPTIONS=True)
|
||||
|
||||
# TODO(morgan): Convert Subsystems over to Flask-Native, for now, we simply
|
||||
# dispatch to another "application" [e.g "keystone"]
|
||||
|
@ -215,29 +212,24 @@ def application_factory(name='public'):
|
|||
_routers.append(routers_instance)
|
||||
routers_instance.append_v3_routers(mapper, sub_routers)
|
||||
|
||||
# Add in the v3 version api
|
||||
sub_routers.append(version_routers.VersionV3('public', _routers))
|
||||
version_controllers.register_version('v3')
|
||||
# TODO(morgan): Remove "API version registration". For now this is kept
|
||||
# for ease of conversion (minimal changes)
|
||||
keystone.api.discovery.register_version('v3')
|
||||
|
||||
# NOTE(morgan): We add in all the keystone.api blueprints here, this
|
||||
# replaces (as they are implemented) the legacy dispatcher work.
|
||||
for api in keystone.api.__apis__:
|
||||
for api_bp in api.APIs:
|
||||
api_bp.instantiate_and_register_to_app(app)
|
||||
|
||||
# Build and construct the dispatching for the Legacy dispatching model
|
||||
sub_routers.append(_ComposibleRouterStub(_routers))
|
||||
legacy_dispatcher = keystone_wsgi.ComposingRouter(mapper, sub_routers)
|
||||
|
||||
for pfx in itertools.chain(*[rtr.Routers._path_prefixes for
|
||||
rtr in ALL_API_ROUTERS]):
|
||||
dispatch_map['/v3/%s' % pfx] = legacy_dispatcher
|
||||
|
||||
# NOTE(morgan) Move the version routers to Flask Native First! It will
|
||||
# not work well due to how the dispatcher works unless this is first,
|
||||
# otherwise nothing falls through to the native flask app.
|
||||
dispatch_map['/v3'] = legacy_dispatcher
|
||||
|
||||
# NOTE(morgan): The Root Version Discovery Document is special and needs
|
||||
# it's own mapper/router since the v3 one assumes it owns the root due
|
||||
# to legacy paste-isms where /v3 would be routed to APP=/v3, PATH=/
|
||||
root_version_disc_mapper = routes.Mapper()
|
||||
root_version_disc_router = version_routers.Versions(name)
|
||||
root_dispatcher = keystone_wsgi.ComposingRouter(
|
||||
root_version_disc_mapper, [root_version_disc_router])
|
||||
dispatch_map['/'] = root_dispatcher
|
||||
|
||||
application = KeystoneDispatcherMiddleware(
|
||||
app,
|
||||
dispatch_map)
|
||||
|
|
|
@ -119,4 +119,4 @@ class APIBase(object):
|
|||
explicitly via normal instantiation where more values may be passed
|
||||
via :meth:`__init__`.
|
||||
"""
|
||||
flask_app.register(cls())
|
||||
flask_app.register_blueprint(cls().blueprint)
|
||||
|
|
|
@ -16,6 +16,7 @@ import os
|
|||
import oslo_i18n
|
||||
from oslo_log import log
|
||||
import stevedore
|
||||
from werkzeug.contrib import fixers
|
||||
|
||||
|
||||
# NOTE(dstanek): i18n.enable_lazy() must be called before
|
||||
|
@ -85,7 +86,7 @@ def _get_config_files(env=None):
|
|||
return files
|
||||
|
||||
|
||||
def setup_app_middleware(application):
|
||||
def setup_app_middleware(app):
|
||||
# NOTE(morgan): Load the middleware, in reverse order, we wrap the app
|
||||
# explicitly; reverse order to ensure the first element in _APP_MIDDLEWARE
|
||||
# processes the request first.
|
||||
|
@ -121,8 +122,11 @@ def setup_app_middleware(application):
|
|||
# local_conf, this is all a hold-over from paste-ini and pending
|
||||
# reworking/removal(s)
|
||||
factory_func = loaded.driver.factory({}, **mw.conf)
|
||||
application = factory_func(application)
|
||||
return application
|
||||
app = factory_func(app)
|
||||
|
||||
# Apply werkzeug speficic middleware
|
||||
app = fixers.ProxyFix(app)
|
||||
return app
|
||||
|
||||
|
||||
def initialize_application(name, post_log_configured_function=lambda: None,
|
||||
|
|
|
@ -39,6 +39,7 @@ from sqlalchemy import exc
|
|||
import testtools
|
||||
from testtools import testcase
|
||||
|
||||
import keystone.api
|
||||
from keystone.common import context
|
||||
from keystone.common import json_home
|
||||
from keystone.common import provider_api
|
||||
|
@ -49,9 +50,8 @@ from keystone import exception
|
|||
from keystone.identity.backends.ldap import common as ks_ldap
|
||||
from keystone import notifications
|
||||
from keystone.resource.backends import base as resource_base
|
||||
from keystone.server import flask as keystone_flask
|
||||
from keystone.tests.unit import ksfixtures
|
||||
from keystone.version import controllers
|
||||
from keystone.version import service
|
||||
|
||||
|
||||
keystone.conf.configure()
|
||||
|
@ -693,7 +693,7 @@ class TestCase(BaseTestCase):
|
|||
self.addCleanup(notifications.clear_subscribers)
|
||||
self.addCleanup(notifications.reset_notifier)
|
||||
|
||||
self.addCleanup(setattr, controllers, '_VERSIONS', [])
|
||||
self.addCleanup(setattr, keystone.api.discovery, '_VERSIONS', [])
|
||||
|
||||
def config(self, config_files):
|
||||
sql.initialize()
|
||||
|
@ -782,7 +782,9 @@ class TestCase(BaseTestCase):
|
|||
self.addCleanup(self.cleanup_instance(*fixtures_to_cleanup))
|
||||
|
||||
def loadapp(self, name='public'):
|
||||
return service.loadapp(name=name)
|
||||
app = keystone_flask.application.application_factory(name)
|
||||
app.config.update(PROPAGATE_EXCEPTIONS=True, testing=True)
|
||||
return keystone_flask.setup_app_middleware(app)
|
||||
|
||||
def assertCloseEnoughForGovernmentWork(self, a, b, delta=3):
|
||||
"""Assert that two datetimes are nearly equal within a small delta.
|
||||
|
|
|
@ -23,9 +23,9 @@ from six.moves import http_client
|
|||
from testtools import matchers as tt_matchers
|
||||
import webob
|
||||
|
||||
from keystone.api import discovery
|
||||
from keystone.common import json_home
|
||||
from keystone.tests import unit
|
||||
from keystone.version import controllers
|
||||
|
||||
|
||||
v3_MEDIA_TYPES = [
|
||||
|
@ -750,7 +750,7 @@ class VersionTestCase(unit.TestCase):
|
|||
self._paste_in_port(expected['version'], 'http://localhost/v3/')
|
||||
self.assertEqual(expected, data)
|
||||
|
||||
@mock.patch.object(controllers, '_VERSIONS', ['v3'])
|
||||
@mock.patch.object(discovery, '_VERSIONS', ['v3'])
|
||||
def test_v2_disabled(self):
|
||||
# NOTE(morgan): This test should be kept, v2.0 is removed and should
|
||||
# never return, this prevents regression[s]/v2.0 discovery doc
|
||||
|
@ -826,8 +826,8 @@ class VersionTestCase(unit.TestCase):
|
|||
self.assertThat(resp.status, tt_matchers.Equals('200 OK'))
|
||||
return resp.headers['Content-Type']
|
||||
|
||||
JSON = controllers.MimeTypes.JSON
|
||||
JSON_HOME = controllers.MimeTypes.JSON_HOME
|
||||
JSON = discovery.MimeTypes.JSON
|
||||
JSON_HOME = discovery.MimeTypes.JSON_HOME
|
||||
|
||||
JSON_MATCHER = tt_matchers.Equals(JSON)
|
||||
JSON_HOME_MATCHER = tt_matchers.Equals(JSON_HOME)
|
||||
|
@ -856,17 +856,13 @@ class VersionTestCase(unit.TestCase):
|
|||
# If request some unknown mime-type, get JSON.
|
||||
self.assertThat(make_request(self.getUniqueString()), JSON_MATCHER)
|
||||
|
||||
@mock.patch.object(controllers, '_VERSIONS', [])
|
||||
@mock.patch.object(discovery, '_VERSIONS', [])
|
||||
def test_no_json_home_document_returned_when_v3_disabled(self):
|
||||
json_home_document = controllers.request_v3_json_home('some_prefix')
|
||||
json_home_document = discovery._v3_json_home_content()
|
||||
json_home.translate_urls(json_home_document, '/v3')
|
||||
expected_document = {'resources': {}}
|
||||
self.assertEqual(expected_document, json_home_document)
|
||||
|
||||
def test_extension_property_method_returns_none(self):
|
||||
extension_obj = controllers.Extensions()
|
||||
extensions_property = extension_obj.extensions
|
||||
self.assertIsNone(extensions_property)
|
||||
|
||||
|
||||
class VersionSingleAppTestCase(unit.TestCase):
|
||||
"""Test running with a single application loaded.
|
||||
|
|
|
@ -1,193 +0,0 @@
|
|||
# Copyright 2012 OpenStack Foundation
|
||||
#
|
||||
# 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.
|
||||
|
||||
from six.moves import http_client
|
||||
|
||||
from keystone.common import extension
|
||||
from keystone.common import json_home
|
||||
from keystone.common import wsgi
|
||||
from keystone import exception
|
||||
|
||||
|
||||
MEDIA_TYPE_JSON = 'application/vnd.openstack.identity-%s+json'
|
||||
|
||||
_VERSIONS = []
|
||||
|
||||
# NOTE(blk-u): latest_app will be set by keystone.version.service.loadapp(). It
|
||||
# gets set to the application that was just loaded. loadapp() gets called once
|
||||
# for the public app if this is the public instance or loadapp() gets called
|
||||
# for the admin app if it's the admin instance. This is used to fetch the /v3
|
||||
# JSON Home response. The /v3 JSON Home response is the same whether it's the
|
||||
# admin or public service so either admin or public works.
|
||||
latest_app = None
|
||||
|
||||
|
||||
def request_v3_json_home(new_prefix):
|
||||
if 'v3' not in _VERSIONS:
|
||||
# No V3 support, so return an empty JSON Home document.
|
||||
return {'resources': {}}
|
||||
v3_json_home = json_home.JsonHomeResources.resources()
|
||||
json_home.translate_urls(v3_json_home, new_prefix)
|
||||
|
||||
return v3_json_home
|
||||
|
||||
|
||||
class Extensions(wsgi.Application):
|
||||
"""Base extensions controller to be extended by public and admin API's."""
|
||||
|
||||
# extend in subclass to specify the set of extensions
|
||||
@property
|
||||
def extensions(self):
|
||||
return None
|
||||
|
||||
def get_extensions_info(self, request):
|
||||
return {'extensions': {'values': list(self.extensions.values())}}
|
||||
|
||||
def get_extension_info(self, request, extension_alias):
|
||||
try:
|
||||
return {'extension': self.extensions[extension_alias]}
|
||||
except KeyError:
|
||||
raise exception.NotFound(target=extension_alias)
|
||||
|
||||
|
||||
class AdminExtensions(Extensions):
|
||||
@property
|
||||
def extensions(self):
|
||||
return extension.ADMIN_EXTENSIONS
|
||||
|
||||
|
||||
class PublicExtensions(Extensions):
|
||||
@property
|
||||
def extensions(self):
|
||||
return extension.PUBLIC_EXTENSIONS
|
||||
|
||||
|
||||
def register_version(version):
|
||||
_VERSIONS.append(version)
|
||||
|
||||
|
||||
class MimeTypes(object):
|
||||
JSON = 'application/json'
|
||||
JSON_HOME = 'application/json-home'
|
||||
|
||||
|
||||
def v3_mime_type_best_match(request):
|
||||
|
||||
# accept_header is a WebOb MIMEAccept object so supports best_match.
|
||||
accept_header = request.accept
|
||||
|
||||
if not accept_header:
|
||||
return MimeTypes.JSON
|
||||
|
||||
SUPPORTED_TYPES = [MimeTypes.JSON, MimeTypes.JSON_HOME]
|
||||
return accept_header.best_match(SUPPORTED_TYPES)
|
||||
|
||||
|
||||
class Version(wsgi.Application):
|
||||
|
||||
def __init__(self, version_type, routers=None):
|
||||
self.endpoint_url_type = version_type
|
||||
self._routers = routers
|
||||
|
||||
super(Version, self).__init__()
|
||||
|
||||
def _get_identity_url(self, context, version):
|
||||
"""Return a URL to keystone's own endpoint."""
|
||||
url = self.base_url(context, self.endpoint_url_type)
|
||||
return '%s/%s/' % (url, version)
|
||||
|
||||
def _get_versions_list(self, context):
|
||||
"""The list of versions is dependent on the context."""
|
||||
versions = {}
|
||||
if 'v2.0' in _VERSIONS:
|
||||
versions['v2.0'] = {
|
||||
'id': 'v2.0',
|
||||
'status': 'deprecated',
|
||||
'updated': '2016-08-04T00:00:00Z',
|
||||
'links': [
|
||||
{
|
||||
'rel': 'self',
|
||||
'href': self._get_identity_url(context, 'v2.0'),
|
||||
}, {
|
||||
'rel': 'describedby',
|
||||
'type': 'text/html',
|
||||
'href': 'https://docs.openstack.org/'
|
||||
}
|
||||
],
|
||||
'media-types': [
|
||||
{
|
||||
'base': 'application/json',
|
||||
'type': MEDIA_TYPE_JSON % 'v2.0'
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
if 'v3' in _VERSIONS:
|
||||
versions['v3'] = {
|
||||
'id': 'v3.10',
|
||||
'status': 'stable',
|
||||
'updated': '2018-02-28T00:00:00Z',
|
||||
'links': [
|
||||
{
|
||||
'rel': 'self',
|
||||
'href': self._get_identity_url(context, 'v3'),
|
||||
}
|
||||
],
|
||||
'media-types': [
|
||||
{
|
||||
'base': 'application/json',
|
||||
'type': MEDIA_TYPE_JSON % 'v3'
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
return versions
|
||||
|
||||
def get_versions(self, request):
|
||||
|
||||
req_mime_type = v3_mime_type_best_match(request)
|
||||
if req_mime_type == MimeTypes.JSON_HOME:
|
||||
v3_json_home = request_v3_json_home('/v3')
|
||||
return wsgi.render_response(
|
||||
body=v3_json_home,
|
||||
headers=(('Content-Type', MimeTypes.JSON_HOME),))
|
||||
|
||||
versions = self._get_versions_list(request.context_dict)
|
||||
return wsgi.render_response(
|
||||
status=(http_client.MULTIPLE_CHOICES,
|
||||
http_client.responses[http_client.MULTIPLE_CHOICES]),
|
||||
body={
|
||||
'versions': {
|
||||
'values': list(versions.values())
|
||||
}
|
||||
})
|
||||
|
||||
def _get_json_home_v3(self):
|
||||
return json_home.JsonHomeResources.resources()
|
||||
|
||||
def get_version_v3(self, request):
|
||||
versions = self._get_versions_list(request.context_dict)
|
||||
if 'v3' in _VERSIONS:
|
||||
req_mime_type = v3_mime_type_best_match(request)
|
||||
|
||||
if req_mime_type == MimeTypes.JSON_HOME:
|
||||
return wsgi.render_response(
|
||||
body=self._get_json_home_v3(),
|
||||
headers=(('Content-Type', MimeTypes.JSON_HOME),))
|
||||
|
||||
return wsgi.render_response(body={
|
||||
'version': versions['v3']
|
||||
})
|
||||
else:
|
||||
raise exception.VersionNotFound(version='v3')
|
|
@ -1,80 +0,0 @@
|
|||
# Copyright 2012 OpenStack Foundation
|
||||
#
|
||||
# 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.
|
||||
"""
|
||||
The only types of routers in this file should be ``ComposingRouters``.
|
||||
|
||||
The routers for the backends should be in the backend-specific router modules.
|
||||
For example, the ``ComposableRouter`` for ``identity`` belongs in::
|
||||
|
||||
keystone.identity.routers
|
||||
|
||||
"""
|
||||
|
||||
|
||||
from keystone.common import wsgi
|
||||
from keystone.version import controllers
|
||||
|
||||
|
||||
class Extension(wsgi.ComposableRouter):
|
||||
def __init__(self, is_admin=True):
|
||||
if is_admin:
|
||||
self.controller = controllers.AdminExtensions()
|
||||
else:
|
||||
self.controller = controllers.PublicExtensions()
|
||||
|
||||
def add_routes(self, mapper):
|
||||
extensions_controller = self.controller
|
||||
mapper.connect('/extensions',
|
||||
controller=extensions_controller,
|
||||
action='get_extensions_info',
|
||||
conditions=dict(method=['GET']))
|
||||
mapper.connect('/extensions/{extension_alias}',
|
||||
controller=extensions_controller,
|
||||
action='get_extension_info',
|
||||
conditions=dict(method=['GET']))
|
||||
|
||||
|
||||
class VersionV2(wsgi.ComposableRouter):
|
||||
def __init__(self, description):
|
||||
self.description = description
|
||||
|
||||
def add_routes(self, mapper):
|
||||
version_controller = controllers.Version(self.description)
|
||||
mapper.connect('/',
|
||||
controller=version_controller,
|
||||
action='get_version_v2')
|
||||
|
||||
|
||||
class VersionV3(wsgi.ComposableRouter):
|
||||
def __init__(self, description, routers):
|
||||
self.description = description
|
||||
self._routers = routers
|
||||
|
||||
def add_routes(self, mapper):
|
||||
version_controller = controllers.Version(self.description,
|
||||
routers=self._routers)
|
||||
mapper.connect('/',
|
||||
controller=version_controller,
|
||||
action='get_version_v3')
|
||||
|
||||
|
||||
class Versions(wsgi.ComposableRouter):
|
||||
def __init__(self, description):
|
||||
self.description = description
|
||||
|
||||
def add_routes(self, mapper):
|
||||
version_controller = controllers.Version(self.description)
|
||||
mapper.connect('/',
|
||||
controller=version_controller,
|
||||
action='get_versions')
|
|
@ -1,33 +0,0 @@
|
|||
# Copyright 2012 OpenStack Foundation
|
||||
#
|
||||
# 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.
|
||||
|
||||
from oslo_log import log
|
||||
|
||||
import keystone.conf
|
||||
from keystone.server import flask as keystone_flask
|
||||
from keystone.server.flask import application
|
||||
from keystone.version import controllers
|
||||
|
||||
|
||||
CONF = keystone.conf.CONF
|
||||
LOG = log.getLogger(__name__)
|
||||
|
||||
|
||||
def loadapp(name):
|
||||
# NOTE(blk-u): Save the application being loaded in the controllers module.
|
||||
# This is similar to how public_app_factory() and v3_app_factory()
|
||||
# register the version with the controllers module.
|
||||
controllers.latest_app = keystone_flask.setup_app_middleware(
|
||||
application.application_factory(name))
|
||||
return controllers.latest_app
|
Loading…
Reference in New Issue