support new HTTP microversion header
According to API working group guidelines: https://review.openstack.org/#/c/243414 microversion headers should be of the form: OpenStack-API-Version: [SERVICE_TYPE] 2.114 i.e OpenStack-API-Version: volume 3.22 Two extra headers are always returned in the response: OpenStack-API-Version: [SERVICE_TYPE] version_number Vary: OpenStack-API-Version note: Servers must be prepared to deal with multiple OpenStack-API-Version headers. This could happen when a client designed to address multiple services always sends the headers it thinks it needs. Most Python frameworks will handle this by setting the value of the header to the values of all matching headers, joined by a ',' (comma). For example ``compute 2.11,identity 2.114``. Closes-Bug: #1551941 Change-Id: I658e54966c390b41e3b551dd9827606c2e013511
This commit is contained in:
parent
6fa468270c
commit
ef7ed8dcb2
@ -16,7 +16,7 @@ user documentation.
|
||||
|
||||
A user can specify a header in the API request::
|
||||
|
||||
OpenStack-Volume-microversion: <version>
|
||||
OpenStack-API-Version: volume <version>
|
||||
|
||||
where ``<version>`` is any valid api version for this API.
|
||||
|
||||
|
@ -68,7 +68,9 @@ VER_METHOD_ATTR = 'versioned_methods'
|
||||
|
||||
# Name of header used by clients to request a specific version
|
||||
# of the REST API
|
||||
API_VERSION_REQUEST_HEADER = 'OpenStack-Volume-microversion'
|
||||
API_VERSION_REQUEST_HEADER = 'OpenStack-API-Version'
|
||||
|
||||
VOLUME_SERVICE = 'volume'
|
||||
|
||||
|
||||
class Request(webob.Request):
|
||||
@ -298,11 +300,20 @@ class Request(webob.Request):
|
||||
hdr_string = self.headers[API_VERSION_REQUEST_HEADER]
|
||||
# 'latest' is a special keyword which is equivalent to requesting
|
||||
# the maximum version of the API supported
|
||||
if hdr_string == 'latest':
|
||||
hdr_string_list = hdr_string.split(",")
|
||||
volume_version = None
|
||||
for hdr in hdr_string_list:
|
||||
if VOLUME_SERVICE in hdr:
|
||||
service, volume_version = hdr.split()
|
||||
break
|
||||
if not volume_version:
|
||||
raise exception.VersionNotFoundForAPIMethod(
|
||||
version=volume_version)
|
||||
if volume_version == 'latest':
|
||||
self.api_version_request = api_version.max_api_version()
|
||||
else:
|
||||
self.api_version_request = api_version.APIVersionRequest(
|
||||
hdr_string)
|
||||
volume_version)
|
||||
|
||||
# Check that the version requested is within the global
|
||||
# minimum/maximum of supported API versions
|
||||
@ -1159,6 +1170,7 @@ class Resource(wsgi.Application):
|
||||
|
||||
if not request.api_version_request.is_null():
|
||||
response.headers[API_VERSION_REQUEST_HEADER] = (
|
||||
VOLUME_SERVICE + ' ' +
|
||||
request.api_version_request.get_string())
|
||||
response.headers['Vary'] = API_VERSION_REQUEST_HEADER
|
||||
|
||||
|
@ -21,11 +21,13 @@ from cinder.api.openstack import api_version_request
|
||||
from cinder.api.openstack import wsgi
|
||||
from cinder.api.v1 import router
|
||||
from cinder.api import versions
|
||||
from cinder import exception
|
||||
from cinder import test
|
||||
from cinder.tests.unit.api import fakes
|
||||
|
||||
|
||||
version_header_name = 'OpenStack-Volume-microversion'
|
||||
VERSION_HEADER_NAME = 'OpenStack-API-Version'
|
||||
VOLUME_SERVICE = 'volume '
|
||||
|
||||
|
||||
@ddt.ddt
|
||||
@ -35,11 +37,27 @@ class VersionsControllerTestCase(test.TestCase):
|
||||
super(VersionsControllerTestCase, self).setUp()
|
||||
self.wsgi_apps = (versions.Versions(), router.APIRouter())
|
||||
|
||||
@ddt.data('1.0', '2.0', '3.0')
|
||||
def test_versions_root(self, version):
|
||||
req = fakes.HTTPRequest.blank('/', base_url='http://localhost')
|
||||
def build_request(self, base_url='http://localhost/v3',
|
||||
header_version=None):
|
||||
req = fakes.HTTPRequest.blank('/', base_url=base_url)
|
||||
req.method = 'GET'
|
||||
req.content_type = 'application/json'
|
||||
if header_version:
|
||||
req.headers = {VERSION_HEADER_NAME: VOLUME_SERVICE +
|
||||
header_version}
|
||||
|
||||
return req
|
||||
|
||||
def check_response(self, response, version):
|
||||
self.assertEqual(VOLUME_SERVICE + version,
|
||||
response.headers[VERSION_HEADER_NAME])
|
||||
self.assertEqual(VOLUME_SERVICE + version,
|
||||
response.headers[VERSION_HEADER_NAME])
|
||||
self.assertEqual(VERSION_HEADER_NAME, response.headers['Vary'])
|
||||
|
||||
@ddt.data('1.0', '2.0', '3.0')
|
||||
def test_versions_root(self, version):
|
||||
req = self.build_request(base_url='http://localhost')
|
||||
|
||||
response = req.get_response(versions.Versions())
|
||||
self.assertEqual(300, response.status_int)
|
||||
@ -64,28 +82,23 @@ class VersionsControllerTestCase(test.TestCase):
|
||||
v3.get('min_version'))
|
||||
|
||||
def test_versions_v1_no_header(self):
|
||||
req = fakes.HTTPRequest.blank('/', base_url='http://localhost/v1')
|
||||
req.method = 'GET'
|
||||
req.content_type = 'application/json'
|
||||
req = self.build_request(base_url='http://localhost/v1')
|
||||
|
||||
response = req.get_response(router.APIRouter())
|
||||
self.assertEqual(200, response.status_int)
|
||||
|
||||
def test_versions_v2_no_header(self):
|
||||
req = fakes.HTTPRequest.blank('/', base_url='http://localhost/v2')
|
||||
req.method = 'GET'
|
||||
req.content_type = 'application/json'
|
||||
req = self.build_request(base_url='http://localhost/v2')
|
||||
|
||||
response = req.get_response(router.APIRouter())
|
||||
self.assertEqual(200, response.status_int)
|
||||
|
||||
@ddt.data('1.0')
|
||||
def test_versions_v1(self, version):
|
||||
req = fakes.HTTPRequest.blank('/', base_url='http://localhost/v1')
|
||||
req.method = 'GET'
|
||||
req.content_type = 'application/json'
|
||||
req = self.build_request(base_url='http://localhost/v1',
|
||||
header_version=version)
|
||||
if version is not None:
|
||||
req.headers = {version_header_name: version}
|
||||
req.headers = {VERSION_HEADER_NAME: VOLUME_SERVICE + version}
|
||||
|
||||
response = req.get_response(router.APIRouter())
|
||||
self.assertEqual(200, response.status_int)
|
||||
@ -94,19 +107,16 @@ class VersionsControllerTestCase(test.TestCase):
|
||||
|
||||
ids = [v['id'] for v in version_list]
|
||||
self.assertEqual({'v1.0'}, set(ids))
|
||||
self.assertEqual('1.0', response.headers[version_header_name])
|
||||
self.assertEqual(version, response.headers[version_header_name])
|
||||
self.assertEqual(version_header_name, response.headers['Vary'])
|
||||
|
||||
self.check_response(response, version)
|
||||
|
||||
self.assertEqual('', version_list[0].get('min_version'))
|
||||
self.assertEqual('', version_list[0].get('version'))
|
||||
|
||||
@ddt.data('2.0')
|
||||
def test_versions_v2(self, version):
|
||||
req = fakes.HTTPRequest.blank('/', base_url='http://localhost/v2')
|
||||
req.method = 'GET'
|
||||
req.content_type = 'application/json'
|
||||
req.headers = {version_header_name: version}
|
||||
req = self.build_request(base_url='http://localhost/v2',
|
||||
header_version=version)
|
||||
|
||||
response = req.get_response(router.APIRouter())
|
||||
self.assertEqual(200, response.status_int)
|
||||
@ -115,19 +125,15 @@ class VersionsControllerTestCase(test.TestCase):
|
||||
|
||||
ids = [v['id'] for v in version_list]
|
||||
self.assertEqual({'v2.0'}, set(ids))
|
||||
self.assertEqual('2.0', response.headers[version_header_name])
|
||||
self.assertEqual(version, response.headers[version_header_name])
|
||||
self.assertEqual(version_header_name, response.headers['Vary'])
|
||||
|
||||
self.check_response(response, version)
|
||||
|
||||
self.assertEqual('', version_list[0].get('min_version'))
|
||||
self.assertEqual('', version_list[0].get('version'))
|
||||
|
||||
@ddt.data('3.0', 'latest')
|
||||
def test_versions_v3_0_and_latest(self, version):
|
||||
req = fakes.HTTPRequest.blank('/', base_url='http://localhost/v3')
|
||||
req.method = 'GET'
|
||||
req.content_type = 'application/json'
|
||||
req.headers = {version_header_name: version}
|
||||
req = self.build_request(header_version=version)
|
||||
|
||||
response = req.get_response(router.APIRouter())
|
||||
self.assertEqual(200, response.status_int)
|
||||
@ -136,8 +142,7 @@ class VersionsControllerTestCase(test.TestCase):
|
||||
|
||||
ids = [v['id'] for v in version_list]
|
||||
self.assertEqual({'v3.0'}, set(ids))
|
||||
self.assertEqual('3.0', response.headers[version_header_name])
|
||||
self.assertEqual(version_header_name, response.headers['Vary'])
|
||||
self.check_response(response, '3.0')
|
||||
|
||||
self.assertEqual(api_version_request._MAX_API_VERSION,
|
||||
version_list[0].get('version'))
|
||||
@ -145,20 +150,14 @@ class VersionsControllerTestCase(test.TestCase):
|
||||
version_list[0].get('min_version'))
|
||||
|
||||
def test_versions_version_latest(self):
|
||||
req = fakes.HTTPRequest.blank('/', base_url='http://localhost/v3')
|
||||
req.method = 'GET'
|
||||
req.content_type = 'application/json'
|
||||
req.headers = {version_header_name: 'latest'}
|
||||
req = self.build_request(header_version='latest')
|
||||
|
||||
response = req.get_response(router.APIRouter())
|
||||
|
||||
self.assertEqual(200, response.status_int)
|
||||
|
||||
def test_versions_version_invalid(self):
|
||||
req = fakes.HTTPRequest.blank('/', base_url='http://localhost/v3')
|
||||
req.method = 'GET'
|
||||
req.content_type = 'application/json'
|
||||
req.headers = {version_header_name: '2.0.1'}
|
||||
req = self.build_request(header_version='2.0.1')
|
||||
|
||||
for app in self.wsgi_apps:
|
||||
response = req.get_response(app)
|
||||
@ -177,8 +176,7 @@ class VersionsControllerTestCase(test.TestCase):
|
||||
def index(self, req):
|
||||
return 'off'
|
||||
|
||||
req = fakes.HTTPRequest.blank('/tests', base_url='http://localhost/v3')
|
||||
req.headers = {version_header_name: '3.5'}
|
||||
req = self.build_request(header_version='3.5')
|
||||
app = fakes.TestRouter(Controller())
|
||||
|
||||
response = req.get_response(app)
|
||||
@ -186,13 +184,40 @@ class VersionsControllerTestCase(test.TestCase):
|
||||
self.assertEqual(404, response.status_int)
|
||||
|
||||
def test_versions_version_not_acceptable(self):
|
||||
req = fakes.HTTPRequest.blank('/', base_url='http://localhost/v3')
|
||||
req.method = 'GET'
|
||||
req.content_type = 'application/json'
|
||||
req.headers = {version_header_name: '4.0'}
|
||||
req = self.build_request(header_version='4.0')
|
||||
|
||||
response = req.get_response(router.APIRouter())
|
||||
|
||||
self.assertEqual(406, response.status_int)
|
||||
self.assertEqual('4.0', response.headers[version_header_name])
|
||||
self.assertEqual(version_header_name, response.headers['Vary'])
|
||||
self.assertEqual('4.0', response.headers[VERSION_HEADER_NAME])
|
||||
self.assertEqual(VERSION_HEADER_NAME, response.headers['Vary'])
|
||||
|
||||
@ddt.data(['volume 3.0, compute 2.22', True],
|
||||
['volume 3.0, compute 2.22, identity 2.3', True],
|
||||
['compute 2.22, identity 2.3', False])
|
||||
@ddt.unpack
|
||||
def test_versions_multiple_services_header(
|
||||
self, service_list, should_pass):
|
||||
req = self.build_request()
|
||||
req.headers = {VERSION_HEADER_NAME: service_list}
|
||||
|
||||
try:
|
||||
response = req.get_response(router.APIRouter())
|
||||
except exception.VersionNotFoundForAPIMethod:
|
||||
if should_pass:
|
||||
raise
|
||||
elif not should_pass:
|
||||
return
|
||||
|
||||
self.assertEqual(200, response.status_int)
|
||||
body = jsonutils.loads(response.body)
|
||||
version_list = body['versions']
|
||||
|
||||
ids = [v['id'] for v in version_list]
|
||||
self.assertEqual({'v3.0'}, set(ids))
|
||||
self.check_response(response, '3.0')
|
||||
|
||||
self.assertEqual(api_version_request._MAX_API_VERSION,
|
||||
version_list[0].get('version'))
|
||||
self.assertEqual(api_version_request._MIN_API_VERSION,
|
||||
version_list[0].get('min_version'))
|
||||
|
@ -9,9 +9,11 @@ to the API while preserving backward compatibility. The basic idea is
|
||||
that a user has to explicitly ask for their request to be treated with
|
||||
a particular version of the API. So breaking changes can be added to
|
||||
the API without breaking users who don't specifically ask for it. This
|
||||
is done with an HTTP header ``OpenStack-Volume-microversion`` which
|
||||
is done with an HTTP header ``OpenStack-API-Version`` which
|
||||
is a monotonically increasing semantic version number starting from
|
||||
``3.0``.
|
||||
``3.0``. Each service that uses microversions will share this header, so
|
||||
the Volume service will need to specifiy ``volume``:
|
||||
``OpenStack-API-Version: volume 3.0``
|
||||
|
||||
If a user makes a request without specifying a version, they will get
|
||||
the ``DEFAULT_API_VERSION`` as defined in
|
||||
@ -157,7 +159,7 @@ In the controller class::
|
||||
....
|
||||
|
||||
This method would only be available if the caller had specified an
|
||||
``OpenStack-Volume-microversion`` of >= ``3.4``. If they had specified a
|
||||
``OpenStack-API-Version`` of >= ``3.4``. If they had specified a
|
||||
lower version (or not specified it and received the default of ``3.1``)
|
||||
the server would respond with ``HTTP/404``.
|
||||
|
||||
@ -171,7 +173,7 @@ In the controller class::
|
||||
....
|
||||
|
||||
This method would only be available if the caller had specified an
|
||||
``OpenStack-Volume-microversion`` of <= ``3.4``. If ``3.5`` or later
|
||||
``OpenStack-API-Version`` of <= ``3.4``. If ``3.5`` or later
|
||||
is specified the server will respond with ``HTTP/404``.
|
||||
|
||||
Changing a method's behaviour
|
||||
@ -294,11 +296,11 @@ these unit tests are not replicated in .../v3, and only new functionality
|
||||
needs to be place in the .../v3/directory.
|
||||
|
||||
Testing a microversioned API method is very similar to a normal controller
|
||||
method test, you just need to add the ``OpenStack-Volume-microversion``
|
||||
method test, you just need to add the ``OpenStack-API-Version``
|
||||
header, for example::
|
||||
|
||||
req = fakes.HTTPRequest.blank('/testable/url/endpoint')
|
||||
req.headers = {'OpenStack-Volume-microversion': '3.2'}
|
||||
req.headers = {'OpenStack-API-Version': 'volume 3.2'}
|
||||
req.api_version_request = api_version.APIVersionRequest('3.6')
|
||||
|
||||
controller = controller.TestableController()
|
||||
|
Loading…
Reference in New Issue
Block a user