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 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.controllers import versions
from barbican.api import hooks from barbican.api import hooks
from barbican.common import config from barbican.common import config
@ -44,16 +39,6 @@ if newrelic_loaded:
newrelic.agent.initialize('/etc/newrelic/newrelic.ini') 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): def build_wsgi_app(controller=None, transactional=False):
"""WSGI application creation helper """WSGI application creation helper
@ -66,21 +51,21 @@ def build_wsgi_app(controller=None, transactional=False):
# Create WSGI app # Create WSGI app
wsgi_app = pecan.Pecan( wsgi_app = pecan.Pecan(
controller or RootController(), controller or versions.AVAILABLE_VERSIONS[versions.DEFAULT_VERSION](),
hooks=request_hooks, hooks=request_hooks,
force_canonical=False force_canonical=False
) )
return wsgi_app return wsgi_app
def create_main_app(global_config, **local_conf): def main_app(func):
"""uWSGI factory method for the Barbican-API application.""" def _wrapper(global_config, **local_conf):
# Queuing initialization # Queuing initialization
queue.init(CONF, is_server_side=False) queue.init(CONF, is_server_side=False)
# Configure oslo logging and configuration services. # Configure oslo logging and configuration services.
log.setup(CONF, 'barbican') log.setup(CONF, 'barbican')
config.setup_remote_pydev_debug() config.setup_remote_pydev_debug()
# Initializing the database engine and session factory before the app # Initializing the database engine and session factory before the app
@ -88,20 +73,25 @@ def create_main_app(global_config, **local_conf):
# connections. # connections.
repositories.setup_database_engine_and_factory() repositories.setup_database_engine_and_factory()
# Setup app with transactional hook enabled wsgi_app = func(global_config, **local_conf)
wsgi_app = build_wsgi_app(transactional=True)
if newrelic_loaded: if newrelic_loaded:
wsgi_app = newrelic.agent.WSGIApplicationWrapper(wsgi_app) wsgi_app = newrelic.agent.WSGIApplicationWrapper(wsgi_app)
LOG = log.getLogger(__name__) LOG = log.getLogger(__name__)
LOG.info(u._LI('Barbican app created and initialized')) LOG.info(u._LI('Barbican app created and initialized'))
return wsgi_app 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): 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 return wsgi_app

View File

@ -11,8 +11,14 @@
# under the License. # under the License.
import pecan import pecan
from six.moves.urllib import parse
from barbican.api import controllers 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.common import utils
from barbican import i18n as u from barbican import i18n as u
from barbican import version from barbican import version
@ -20,21 +26,145 @@ from barbican import version
LOG = utils.getLogger(__name__) 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): 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) @pecan.expose(generic=True)
def index(self): def index(self):
pecan.abort(405) # HTTP 405 Method Not Allowed as default pecan.abort(405) # HTTP 405 Method Not Allowed as default
@index.when(method='GET', template='json') @index.when(method='GET', template='json')
@utils.allow_certain_content_types(MIME_TYPE_JSON, MIME_TYPE_JSON_HOME)
@controllers.handle_exceptions(u._('Version retrieval')) @controllers.handle_exceptions(u._('Version retrieval'))
def on_get(self): def on_get(self):
body = { return {'version': self.get_version_info(pecan.request)}
'v1': 'current',
'build': version.__version__
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' API_VERSION = 'v1'
def allow_all_content_types(f): def _do_allow_certain_content_types(func, content_types_list=[]):
# Pecan decorator to not limit content types for controller routes # Allows you to bypass pecan's content-type restrictions
cfg = pecan.util._cfg(f) cfg = pecan.util._cfg(func)
cfg.setdefault('content_types', {}) cfg.setdefault('content_types', {})
cfg['content_types'].update((value, '') cfg['content_types'].update((value, '')
for value in mimetypes.types_map.values()) for value in content_types_list)
return f 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): def hostname_for_refs(resource=None):

View File

@ -16,15 +16,32 @@ from barbican.api import controllers
from barbican.tests import utils from barbican.tests import utils
class WhenTestingVersionResource(utils.BarbicanAPIBaseTestCase): class WhenTestingVersionsResource(utils.BarbicanAPIBaseTestCase):
root_controller = controllers.versions.VersionController() 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('/') 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): def test_should_return_version_json(self):
resp = self.app.get('/') resp = self.app.get('/')
self.assertTrue('v1' in resp.json) versions_response = resp.json['versions']['values']
self.assertEqual(resp.json.get('v1'), 'current') 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) return self.controller.on_delete(*args, **kwargs)
class VersionResource(TestableResource): class VersionsResource(TestableResource):
controller_cls = versions.VersionController controller_cls = versions.VersionsController
class SecretsResource(TestableResource): class SecretsResource(TestableResource):
@ -203,32 +203,32 @@ class BaseTestCase(utils.BaseTestCase, utils.MockModelRepositoryMixin):
self.assertEqual(403, exception.status_int) self.assertEqual(403, exception.status_int)
class WhenTestingVersionResource(BaseTestCase): class WhenTestingVersionsResource(BaseTestCase):
"""RBAC tests for the barbican.api.resources.VersionResource class.""" """RBAC tests for the barbican.api.resources.VersionsResource class."""
def setUp(self): def setUp(self):
super(WhenTestingVersionResource, self).setUp() super(WhenTestingVersionsResource, self).setUp()
self.resource = VersionResource() self.resource = VersionsResource()
def test_rules_should_be_loaded(self): def test_rules_should_be_loaded(self):
self.assertIsNotNone(self.policy_enforcer.rules) 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, # Can't use base method that short circuits post-RBAC processing here,
# as version GET is trivial # as version GET is trivial
for role in ['admin', 'observer', 'creator', 'audit']: for role in ['admin', 'observer', 'creator', 'audit']:
self.req = self._generate_req(roles=[role] if role else []) self.req = self._generate_req(roles=[role] if role else [])
self._invoke_on_get() 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.req = self._generate_req(roles=[None, 'bunkrolehere'])
self._invoke_on_get() 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.req = self._generate_req()
self._invoke_on_get() 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', self.req = self._generate_req(roles=['admin', 'observer', 'creator',
'audit']) 'audit'])
self._invoke_on_get() self._invoke_on_get()

View File

@ -130,7 +130,7 @@ function configure_barbican {
## Set up keystone ## Set up keystone
# Turn on the middleware # 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 # Set the keystone parameters
iniset $BARBICAN_PASTE_CONF 'filter:keystone_authtoken' auth_protocol $KEYSTONE_AUTH_PROTOCOL 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 .. code-block:: ini
[pipeline:barbican_api] [pipeline:barbican_api]
pipeline = keystone_authtoken context apiapp pipeline = keystone_authtoken context apiapp_v1
2. Replace ``keystone_authtoken`` filter values to match your Keystone 2. Replace ``keystone_authtoken`` filter values to match your Keystone
setup setup

View File

@ -9,21 +9,20 @@ pipeline = versionapp
# Use this pipeline for Barbican API - DEFAULT no authentication # Use this pipeline for Barbican API - DEFAULT no authentication
[pipeline:barbican_api] [pipeline:barbican_api]
pipeline = unauthenticated-context apiapp pipeline = unauthenticated-context apiapp_v1
####pipeline = simple apiapp #pipeline = keystone_authtoken context apiapp_v1
#pipeline = keystone_authtoken context apiapp
#Use this pipeline to activate a repoze.profile middleware and HTTP port, #Use this pipeline to activate a repoze.profile middleware and HTTP port,
# to provide profiling information for the REST API processing. # to provide profiling information for the REST API processing.
[pipeline:barbican-profile] [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 #Use this pipeline for keystone auth
[pipeline:barbican-api-keystone] [pipeline:barbican-api-keystone]
pipeline = keystone_authtoken context apiapp pipeline = keystone_authtoken context apiapp_v1
[app:apiapp] [app:apiapp_v1]
paste.app_factory = barbican.api.app:create_main_app paste.app_factory = barbican.api.app:create_main_app_v1
[app:versionapp] [app:versionapp]
paste.app_factory = barbican.api.app:create_version_app 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) resp = self.client.get(url_without_version, use_auth=use_auth)
body = resp.json() body = resp.json()
self.assertEqual(resp.status_code, 200) self.assertEqual(resp.status_code, 300)
self.assertEqual(body.get('v1'), 'current') versions_response = body['versions']['values']
self.assertGreater(len(body.get('build')), 1) 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')