Display all versions info in versions controller
This patch enables the "versions controller" or "/" resource to display information relevant to all the versions of the Barbican API (which is only v1 at the moment). This is done in the same fashion Keystone displays it, and it has the purpose of enabling more automatic discovery as described in the blueprint. Accessing the root resource with the "build" query parameter, such as: $ curl http://localhost:9311/?build will display the build information. On the other hand, this introduces the V1Controller, which is now the root controller (which requires authentication) for Barbican. Accessing the "/v1" resource will display the version information in the way it's required by keystone. The json-home implementation is left for a subsequent CR. Partially implements blueprint fix-version-api Change-Id: Ie7e706adcf1b5d74f64776b888a06638247b4e87
This commit is contained in:
parent
47ec34f112
commit
3b20d84312
@ -26,11 +26,6 @@ except ImportError:
|
||||
|
||||
from oslo_log import log
|
||||
|
||||
from barbican.api.controllers import cas
|
||||
from barbican.api.controllers import containers
|
||||
from barbican.api.controllers import orders
|
||||
from barbican.api.controllers import secrets
|
||||
from barbican.api.controllers import transportkeys
|
||||
from barbican.api.controllers import versions
|
||||
from barbican.api import hooks
|
||||
from barbican.common import config
|
||||
@ -44,16 +39,6 @@ if newrelic_loaded:
|
||||
newrelic.agent.initialize('/etc/newrelic/newrelic.ini')
|
||||
|
||||
|
||||
class RootController(object):
|
||||
def __init__(self):
|
||||
# Adding the controllers at runtime to due config loading issues
|
||||
self.secrets = secrets.SecretsController()
|
||||
self.orders = orders.OrdersController()
|
||||
self.containers = containers.ContainersController()
|
||||
self.transport_keys = transportkeys.TransportKeysController()
|
||||
self.cas = cas.CertificateAuthoritiesController()
|
||||
|
||||
|
||||
def build_wsgi_app(controller=None, transactional=False):
|
||||
"""WSGI application creation helper
|
||||
|
||||
@ -66,42 +51,47 @@ def build_wsgi_app(controller=None, transactional=False):
|
||||
|
||||
# Create WSGI app
|
||||
wsgi_app = pecan.Pecan(
|
||||
controller or RootController(),
|
||||
controller or versions.AVAILABLE_VERSIONS[versions.DEFAULT_VERSION](),
|
||||
hooks=request_hooks,
|
||||
force_canonical=False
|
||||
)
|
||||
return wsgi_app
|
||||
|
||||
|
||||
def create_main_app(global_config, **local_conf):
|
||||
def main_app(func):
|
||||
def _wrapper(global_config, **local_conf):
|
||||
# Queuing initialization
|
||||
queue.init(CONF, is_server_side=False)
|
||||
|
||||
# Configure oslo logging and configuration services.
|
||||
log.setup(CONF, 'barbican')
|
||||
|
||||
config.setup_remote_pydev_debug()
|
||||
|
||||
# Initializing the database engine and session factory before the app
|
||||
# starts ensures we don't lose requests due to lazy initialiation of db
|
||||
# connections.
|
||||
repositories.setup_database_engine_and_factory()
|
||||
|
||||
wsgi_app = func(global_config, **local_conf)
|
||||
|
||||
if newrelic_loaded:
|
||||
wsgi_app = newrelic.agent.WSGIApplicationWrapper(wsgi_app)
|
||||
LOG = log.getLogger(__name__)
|
||||
LOG.info(u._LI('Barbican app created and initialized'))
|
||||
return wsgi_app
|
||||
return _wrapper
|
||||
|
||||
|
||||
@main_app
|
||||
def create_main_app_v1(global_config, **local_conf):
|
||||
"""uWSGI factory method for the Barbican-API application."""
|
||||
|
||||
# Queuing initialization
|
||||
queue.init(CONF, is_server_side=False)
|
||||
|
||||
# Configure oslo logging and configuration services.
|
||||
log.setup(CONF, 'barbican')
|
||||
config.setup_remote_pydev_debug()
|
||||
|
||||
# Initializing the database engine and session factory before the app
|
||||
# starts ensures we don't lose requests due to lazy initialiation of db
|
||||
# connections.
|
||||
repositories.setup_database_engine_and_factory()
|
||||
|
||||
# Setup app with transactional hook enabled
|
||||
wsgi_app = build_wsgi_app(transactional=True)
|
||||
|
||||
if newrelic_loaded:
|
||||
wsgi_app = newrelic.agent.WSGIApplicationWrapper(wsgi_app)
|
||||
|
||||
LOG = log.getLogger(__name__)
|
||||
LOG.info(u._LI('Barbican app created and initialized'))
|
||||
|
||||
return wsgi_app
|
||||
return build_wsgi_app(versions.V1Controller(), transactional=True)
|
||||
|
||||
|
||||
def create_admin_app(global_config, **local_conf):
|
||||
wsgi_app = pecan.make_app(versions.VersionController())
|
||||
wsgi_app = pecan.make_app(versions.VersionsController())
|
||||
return wsgi_app
|
||||
|
||||
|
||||
|
@ -11,8 +11,14 @@
|
||||
# under the License.
|
||||
|
||||
import pecan
|
||||
from six.moves.urllib import parse
|
||||
|
||||
from barbican.api import controllers
|
||||
from barbican.api.controllers import cas
|
||||
from barbican.api.controllers import containers
|
||||
from barbican.api.controllers import orders
|
||||
from barbican.api.controllers import secrets
|
||||
from barbican.api.controllers import transportkeys
|
||||
from barbican.common import utils
|
||||
from barbican import i18n as u
|
||||
from barbican import version
|
||||
@ -20,21 +26,145 @@ from barbican import version
|
||||
LOG = utils.getLogger(__name__)
|
||||
|
||||
|
||||
class VersionController(object):
|
||||
MIME_TYPE_JSON = 'application/json'
|
||||
MIME_TYPE_JSON_HOME = 'application/json-home'
|
||||
MEDIA_TYPE_JSON = 'application/vnd.openstack.keymanagement-%s+json'
|
||||
|
||||
|
||||
def _version_not_found():
|
||||
"""Throw exception indicating version not found."""
|
||||
pecan.abort(404, u._("The version you requested wasn't found"))
|
||||
|
||||
|
||||
def _get_versioned_url(full_url, version):
|
||||
parsed_url, _ = parse.urldefrag(full_url)
|
||||
|
||||
if version[-1] != '/':
|
||||
version += '/'
|
||||
return parse.urljoin(parsed_url, version)
|
||||
|
||||
|
||||
class BaseVersionController(object):
|
||||
"""Base class for the version-specific controllers"""
|
||||
|
||||
@classmethod
|
||||
def get_version_info(cls, request):
|
||||
return {
|
||||
'id': cls.version_id,
|
||||
'status': 'stable',
|
||||
'updated': cls.last_updated,
|
||||
'links': [
|
||||
{
|
||||
'rel': 'self',
|
||||
'href': _get_versioned_url(request.url,
|
||||
cls.version_string),
|
||||
}, {
|
||||
'rel': 'describedby',
|
||||
'type': 'text/html',
|
||||
'href': 'http://docs.openstack.org/'
|
||||
}
|
||||
],
|
||||
'media-types': [
|
||||
{
|
||||
'base': MIME_TYPE_JSON,
|
||||
'type': MEDIA_TYPE_JSON % cls.version_string
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
|
||||
class V1Controller(BaseVersionController):
|
||||
"""Root controller for the v1 API"""
|
||||
|
||||
version_string = 'v1'
|
||||
|
||||
# NOTE(jaosorior): We might start using decimals in the future, meanwhile
|
||||
# this is the same as the version string.
|
||||
version_id = 'v1'
|
||||
|
||||
last_updated = '2015-04-28T00:00:00Z'
|
||||
|
||||
def __init__(self):
|
||||
LOG.debug('=== Creating VersionController ===')
|
||||
LOG.debug('=== Creating V1Controller ===')
|
||||
self.secrets = secrets.SecretsController()
|
||||
self.orders = orders.OrdersController()
|
||||
self.containers = containers.ContainersController()
|
||||
self.transport_keys = transportkeys.TransportKeysController()
|
||||
self.cas = cas.CertificateAuthoritiesController()
|
||||
|
||||
self.__controllers = [
|
||||
self.secrets,
|
||||
self.orders,
|
||||
self.containers,
|
||||
self.transport_keys,
|
||||
self.cas,
|
||||
]
|
||||
|
||||
@pecan.expose(generic=True)
|
||||
def index(self):
|
||||
pecan.abort(405) # HTTP 405 Method Not Allowed as default
|
||||
|
||||
@index.when(method='GET', template='json')
|
||||
@utils.allow_certain_content_types(MIME_TYPE_JSON, MIME_TYPE_JSON_HOME)
|
||||
@controllers.handle_exceptions(u._('Version retrieval'))
|
||||
def on_get(self):
|
||||
body = {
|
||||
'v1': 'current',
|
||||
'build': version.__version__
|
||||
return {'version': self.get_version_info(pecan.request)}
|
||||
|
||||
|
||||
AVAILABLE_VERSIONS = {
|
||||
V1Controller.version_string: V1Controller,
|
||||
}
|
||||
|
||||
DEFAULT_VERSION = V1Controller.version_string
|
||||
|
||||
|
||||
class VersionsController(object):
|
||||
|
||||
def __init__(self):
|
||||
LOG.debug('=== Creating VersionsController ===')
|
||||
|
||||
@pecan.expose(generic=True)
|
||||
def index(self, **kwargs):
|
||||
pecan.abort(405) # HTTP 405 Method Not Allowed as default
|
||||
|
||||
@index.when(method='GET', template='json')
|
||||
@utils.allow_certain_content_types(MIME_TYPE_JSON, MIME_TYPE_JSON_HOME)
|
||||
def on_get(self, **kwargs):
|
||||
"""The list of versions is dependent on the context."""
|
||||
self._redirect_to_default_json_home_if_needed(pecan.request)
|
||||
|
||||
if 'build' in kwargs:
|
||||
return {'build': version.__version__}
|
||||
|
||||
versions_info = [version_class.get_version_info(pecan.request)
|
||||
for version_class in AVAILABLE_VERSIONS.itervalues()]
|
||||
|
||||
version_output = {
|
||||
'versions': {
|
||||
'values': versions_info
|
||||
}
|
||||
}
|
||||
LOG.info(u._LI('Retrieved version'))
|
||||
return body
|
||||
|
||||
# Since we are returning all the versions available, the proper status
|
||||
# code is Multiple Choices (300)
|
||||
pecan.response.status = 300
|
||||
return version_output
|
||||
|
||||
def _redirect_to_default_json_home_if_needed(self, request):
|
||||
if self._mime_best_match(request.accept) == MIME_TYPE_JSON_HOME:
|
||||
url = _get_versioned_url(request.url, DEFAULT_VERSION)
|
||||
LOG.debug("Redirecting Request to " + url)
|
||||
# NOTE(jaosorior): This issues an "external" redirect because of
|
||||
# two reasons:
|
||||
# * This module doesn't require authorization, and accessing
|
||||
# specific version info needs that.
|
||||
# * The resource is a separate app_factory and won't be found
|
||||
# internally
|
||||
pecan.redirect(url, request=request)
|
||||
|
||||
def _mime_best_match(self, accept):
|
||||
if not accept:
|
||||
return MIME_TYPE_JSON
|
||||
|
||||
SUPPORTED_TYPES = [MIME_TYPE_JSON, MIME_TYPE_JSON_HOME]
|
||||
return accept.best_match(SUPPORTED_TYPES)
|
||||
|
@ -35,13 +35,23 @@ CONF = config.CONF
|
||||
API_VERSION = 'v1'
|
||||
|
||||
|
||||
def allow_all_content_types(f):
|
||||
# Pecan decorator to not limit content types for controller routes
|
||||
cfg = pecan.util._cfg(f)
|
||||
def _do_allow_certain_content_types(func, content_types_list=[]):
|
||||
# Allows you to bypass pecan's content-type restrictions
|
||||
cfg = pecan.util._cfg(func)
|
||||
cfg.setdefault('content_types', {})
|
||||
cfg['content_types'].update((value, '')
|
||||
for value in mimetypes.types_map.values())
|
||||
return f
|
||||
for value in content_types_list)
|
||||
return func
|
||||
|
||||
|
||||
def allow_certain_content_types(*content_types_list):
|
||||
def _wrapper(func):
|
||||
return _do_allow_certain_content_types(func, content_types_list)
|
||||
return _wrapper
|
||||
|
||||
|
||||
def allow_all_content_types(f):
|
||||
return _do_allow_certain_content_types(f, mimetypes.types_map.values())
|
||||
|
||||
|
||||
def hostname_for_refs(resource=None):
|
||||
|
@ -16,15 +16,32 @@ from barbican.api import controllers
|
||||
from barbican.tests import utils
|
||||
|
||||
|
||||
class WhenTestingVersionResource(utils.BarbicanAPIBaseTestCase):
|
||||
root_controller = controllers.versions.VersionController()
|
||||
class WhenTestingVersionsResource(utils.BarbicanAPIBaseTestCase):
|
||||
root_controller = controllers.versions.VersionsController()
|
||||
|
||||
def test_should_return_200_on_get(self):
|
||||
def test_should_return_multiple_choices_on_get(self):
|
||||
resp = self.app.get('/')
|
||||
self.assertEqual(200, resp.status_int)
|
||||
self.assertEqual(300, resp.status_int)
|
||||
|
||||
def test_should_return_multiple_choices_on_get_if_json_accept_header(self):
|
||||
headers = {'Accept': 'application/json'}
|
||||
resp = self.app.get('/', headers=headers)
|
||||
self.assertEqual(300, resp.status_int)
|
||||
|
||||
def test_should_redirect_if_json_home_accept_header_present(self):
|
||||
headers = {'Accept': 'application/json-home'}
|
||||
resp = self.app.get('/', headers=headers)
|
||||
self.assertEqual(302, resp.status_int)
|
||||
|
||||
def test_should_return_version_json(self):
|
||||
resp = self.app.get('/')
|
||||
|
||||
self.assertTrue('v1' in resp.json)
|
||||
self.assertEqual(resp.json.get('v1'), 'current')
|
||||
versions_response = resp.json['versions']['values']
|
||||
v1_info = versions_response[0]
|
||||
|
||||
# NOTE(jaosorior): I used assertIn instead of assertEqual because we
|
||||
# might start using decimal numbers in the future. So when that happens
|
||||
# this test will still be valid.
|
||||
self.assertIn('v1', v1_info['id'])
|
||||
self.assertEqual(len(v1_info['media-types']), 1)
|
||||
self.assertEqual(v1_info['media-types'][0]['base'], 'application/json')
|
||||
|
@ -69,8 +69,8 @@ class TestableResource(object):
|
||||
return self.controller.on_delete(*args, **kwargs)
|
||||
|
||||
|
||||
class VersionResource(TestableResource):
|
||||
controller_cls = versions.VersionController
|
||||
class VersionsResource(TestableResource):
|
||||
controller_cls = versions.VersionsController
|
||||
|
||||
|
||||
class SecretsResource(TestableResource):
|
||||
@ -203,32 +203,32 @@ class BaseTestCase(utils.BaseTestCase, utils.MockModelRepositoryMixin):
|
||||
self.assertEqual(403, exception.status_int)
|
||||
|
||||
|
||||
class WhenTestingVersionResource(BaseTestCase):
|
||||
"""RBAC tests for the barbican.api.resources.VersionResource class."""
|
||||
class WhenTestingVersionsResource(BaseTestCase):
|
||||
"""RBAC tests for the barbican.api.resources.VersionsResource class."""
|
||||
def setUp(self):
|
||||
super(WhenTestingVersionResource, self).setUp()
|
||||
super(WhenTestingVersionsResource, self).setUp()
|
||||
|
||||
self.resource = VersionResource()
|
||||
self.resource = VersionsResource()
|
||||
|
||||
def test_rules_should_be_loaded(self):
|
||||
self.assertIsNotNone(self.policy_enforcer.rules)
|
||||
|
||||
def test_should_pass_get_version(self):
|
||||
def test_should_pass_get_versions(self):
|
||||
# Can't use base method that short circuits post-RBAC processing here,
|
||||
# as version GET is trivial
|
||||
for role in ['admin', 'observer', 'creator', 'audit']:
|
||||
self.req = self._generate_req(roles=[role] if role else [])
|
||||
self._invoke_on_get()
|
||||
|
||||
def test_should_pass_get_version_with_bad_roles(self):
|
||||
def test_should_pass_get_versions_with_bad_roles(self):
|
||||
self.req = self._generate_req(roles=[None, 'bunkrolehere'])
|
||||
self._invoke_on_get()
|
||||
|
||||
def test_should_pass_get_version_with_no_roles(self):
|
||||
def test_should_pass_get_versions_with_no_roles(self):
|
||||
self.req = self._generate_req()
|
||||
self._invoke_on_get()
|
||||
|
||||
def test_should_pass_get_version_multiple_roles(self):
|
||||
def test_should_pass_get_versions_multiple_roles(self):
|
||||
self.req = self._generate_req(roles=['admin', 'observer', 'creator',
|
||||
'audit'])
|
||||
self._invoke_on_get()
|
||||
|
@ -130,7 +130,7 @@ function configure_barbican {
|
||||
## Set up keystone
|
||||
|
||||
# Turn on the middleware
|
||||
iniset $BARBICAN_PASTE_CONF 'pipeline:barbican_api' pipeline 'keystone_authtoken context apiapp'
|
||||
iniset $BARBICAN_PASTE_CONF 'pipeline:barbican_api' pipeline 'keystone_authtoken context apiapp_v1'
|
||||
|
||||
# Set the keystone parameters
|
||||
iniset $BARBICAN_PASTE_CONF 'filter:keystone_authtoken' auth_protocol $KEYSTONE_AUTH_PROTOCOL
|
||||
|
@ -32,7 +32,7 @@ the get version call.
|
||||
.. code-block:: ini
|
||||
|
||||
[pipeline:barbican_api]
|
||||
pipeline = keystone_authtoken context apiapp
|
||||
pipeline = keystone_authtoken context apiapp_v1
|
||||
|
||||
2. Replace ``keystone_authtoken`` filter values to match your Keystone
|
||||
setup
|
||||
|
@ -9,21 +9,20 @@ pipeline = versionapp
|
||||
|
||||
# Use this pipeline for Barbican API - DEFAULT no authentication
|
||||
[pipeline:barbican_api]
|
||||
pipeline = unauthenticated-context apiapp
|
||||
####pipeline = simple apiapp
|
||||
#pipeline = keystone_authtoken context apiapp
|
||||
pipeline = unauthenticated-context apiapp_v1
|
||||
#pipeline = keystone_authtoken context apiapp_v1
|
||||
|
||||
#Use this pipeline to activate a repoze.profile middleware and HTTP port,
|
||||
# to provide profiling information for the REST API processing.
|
||||
[pipeline:barbican-profile]
|
||||
pipeline = unauthenticated-context egg:Paste#cgitb egg:Paste#httpexceptions profile apiapp
|
||||
pipeline = unauthenticated-context egg:Paste#cgitb egg:Paste#httpexceptions profile apiapp_v1
|
||||
|
||||
#Use this pipeline for keystone auth
|
||||
[pipeline:barbican-api-keystone]
|
||||
pipeline = keystone_authtoken context apiapp
|
||||
pipeline = keystone_authtoken context apiapp_v1
|
||||
|
||||
[app:apiapp]
|
||||
paste.app_factory = barbican.api.app:create_main_app
|
||||
[app:apiapp_v1]
|
||||
paste.app_factory = barbican.api.app:create_main_app_v1
|
||||
|
||||
[app:versionapp]
|
||||
paste.app_factory = barbican.api.app:create_version_app
|
||||
|
@ -35,6 +35,13 @@ class VersionDiscoveryTestCase(base.TestCase):
|
||||
resp = self.client.get(url_without_version, use_auth=use_auth)
|
||||
body = resp.json()
|
||||
|
||||
self.assertEqual(resp.status_code, 200)
|
||||
self.assertEqual(body.get('v1'), 'current')
|
||||
self.assertGreater(len(body.get('build')), 1)
|
||||
self.assertEqual(resp.status_code, 300)
|
||||
versions_response = body['versions']['values']
|
||||
v1_info = versions_response[0]
|
||||
|
||||
# NOTE(jaosorior): I used assertIn instead of assertEqual because we
|
||||
# might start using decimal numbers in the future. So when that happens
|
||||
# this test will still be valid.
|
||||
self.assertIn('v1', v1_info['id'])
|
||||
self.assertEqual(len(v1_info['media-types']), 1)
|
||||
self.assertEqual(v1_info['media-types'][0]['base'], 'application/json')
|
||||
|
Loading…
Reference in New Issue
Block a user