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:
Juan Antonio Osorio Robles 2015-04-29 11:11:43 +03:00
parent 47ec34f112
commit 3b20d84312
9 changed files with 233 additions and 80 deletions

View File

@ -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,21 +51,21 @@ 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):
"""uWSGI factory method for the Barbican-API application."""
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
@ -88,20 +73,25 @@ def create_main_app(global_config, **local_conf):
# connections.
repositories.setup_database_engine_and_factory()
# Setup app with transactional hook enabled
wsgi_app = build_wsgi_app(transactional=True)
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."""
# Setup app with transactional hook enabled
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

View File

@ -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,
}
LOG.info(u._LI('Retrieved version'))
return body
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
}
}
# 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)

View File

@ -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):

View File

@ -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')

View File

@ -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()

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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')