Convert json_home and version discovery to Flask
Move the JSON Home Document and Version Discovery Documents out of the webob-based mapper and into Flask. This change removes the keystone.version.controller and keystone.version.router modules as they have been moved into keystone.api.discovery. The keystone.api.discovery module is somewhat specialized as there are no "resources" and it must handle multiple types of responses based upon the ACCEPTS header (JSON Home or JSON). In lieu of the flask-RESTful mechanisms, keystone.api.discovery utilizes bare flask blueprint and functions. Minor scaffolding work has been done to ensure the discovery blueprint can be loaded via the loader loop in keystone.server.flask.application (a stub object in keystone.api.discovery). Partial-Bug: #1776504 Change-Id: Ib25380cefdbb7147661bb9853de7872a837322e0
This commit is contained in:
parent
ecf721a3c1
commit
3e3ba18bfa
16
keystone/api/__init__.py
Normal file
16
keystone/api/__init__.py
Normal file
@ -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,)
|
142
keystone/api/discovery.py
Normal file
142
keystone/api/discovery.py
Normal file
@ -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 = [
|
||||
@ -746,7 +746,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
|
||||
@ -822,8 +822,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)
|
||||
@ -852,17 +852,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
Block a user