Merge "Move the Capsule API from Experimental to V1"

This commit is contained in:
Zuul 2018-02-26 06:28:28 +00:00 committed by Gerrit Code Review
commit 0cf31381e7
15 changed files with 37 additions and 306 deletions

View File

@ -141,7 +141,6 @@ function upload_sandbox_image {
function create_zun_accounts { function create_zun_accounts {
create_service_user "zun" "admin" create_service_user "zun" "admin"
create_service_user "zun-experimental" "admin"
if is_service_enabled zun-api; then if is_service_enabled zun-api; then
@ -154,18 +153,11 @@ function create_zun_accounts {
local zun_service=$(get_or_create_service "zun" \ local zun_service=$(get_or_create_service "zun" \
"container" "Container As Service") "container" "Container As Service")
local zun_experimental_service=$(get_or_create_service "zun-experimental" \
"container-experimental" "Container As Service - Experimental")
get_or_create_endpoint $zun_service \ get_or_create_endpoint $zun_service \
"$REGION_NAME" \ "$REGION_NAME" \
"$zun_api_url/v1" \ "$zun_api_url/v1" \
"$zun_api_url/v1" \ "$zun_api_url/v1" \
"$zun_api_url/v1" "$zun_api_url/v1"
get_or_create_endpoint $zun_experimental_service \
"$REGION_NAME" \
"$zun_api_url/experimental" \
"$zun_api_url/experimental" \
"$zun_api_url/experimental"
fi fi
} }

View File

@ -46,32 +46,7 @@ The diagram below is an overview of the structure of ``capsule``.
| | | |
+-----------------------------------------------------------+ +-----------------------------------------------------------+
Capsule API is currently in experimental phase, so you have to Capsule API is currently in v1 phase now.
specify ``--experimental-api`` option in each of the commands below. They will
be moved to stable API once they become stable.
.. note::
Please make sure that every capsule commands have ``--experimental-api``
flags in client side.
Experimental API is a separated API. After users deploy Zun by devstack,
a separated set of API endpoints and service type will be created in
service catalog. Zun stable API endpoints will have service name ``zun`` and
service type ``container``, while Zun experimental API endpoints will have
service name ``zun-experimental`` and service type ``container-experimental``.
We can see the service and endpoint information as below::
+------------------+------------------------+---------+-----------+--------------------------------------+
| Service Name | Service Type | Enabled | Interface | URL |
+------------------+------------------------+---------+-----------+--------------------------------------+
| zun | container | True | public | http://***/container/v1 |
| zun | container | True | internal | http://***/container/v1 |
| zun | container | True | admin | http://***/container/v1 |
| zun-experimental | container-experimental | True | public | http://***/container/experimental |
| zun-experimental | container-experimental | True | internal | http://***/container/experimental |
| zun-experimental | container-experimental | True | admin | http://***/container/experimental |
+------------------+------------------------+---------+-----------+--------------------------------------+
Now basic capsule functions are supported. Capsule API methods: Now basic capsule functions are supported. Capsule API methods:
@ -175,7 +150,7 @@ Create capsule, it will create capsule based on capsule.yaml:
.. code-block:: console .. code-block:: console
$ source ~/devstack/openrc demo demo $ source ~/devstack/openrc demo demo
$ zun --experimental-api capsule-create -f capsule.yaml $ zun capsule-create -f capsule.yaml
If you want to get access to the port, you need to set the security group If you want to get access to the port, you need to set the security group
rules for it. rules for it.
@ -193,21 +168,21 @@ Delete capsule:
.. code-block:: console .. code-block:: console
$ zun --experimental-api capsule-delete <uuid> $ zun capsule-delete <uuid>
$ zun --experimental-api capsule-delete <capsule-name> $ zun capsule-delete <capsule-name>
List capsule: List capsule:
.. code-block:: console .. code-block:: console
$ zun --experimental-api capsule-list $ zun capsule-list
Describe capsule: Describe capsule:
.. code-block:: console .. code-block:: console
$ zun --experimental-api capsule-describe <uuid> $ zun capsule-describe <uuid>
$ zun --experimental-api capsule-describe <capsule-name> $ zun capsule-describe <capsule-name>
To DO To DO
--------- ---------

View File

@ -1,153 +0,0 @@
# Copyright 2017 ARM Holdings.
#
# 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.
"""
Experimental of the Zun API
NOTE: IN PROGRESS AND NOT FULLY IMPLEMENTED.
"""
from oslo_log import log as logging
import pecan
from zun.api.controllers import base as controllers_base
from zun.api.controllers.experimental import capsules as capsule_controller
from zun.api.controllers import link
from zun.api.controllers import versions as ver
from zun.api import http_error
from zun.common.i18n import _
LOG = logging.getLogger(__name__)
BASE_VERSION = 1
MIN_VER_STR = '%s %s' % (ver.Version.service_string, ver.BASE_VER)
MAX_VER_STR = '%s %s' % (ver.Version.service_string, ver.CURRENT_MAX_VER)
MIN_VER = ver.Version({ver.Version.string: MIN_VER_STR},
MIN_VER_STR, MAX_VER_STR)
MAX_VER = ver.Version({ver.Version.string: MAX_VER_STR},
MIN_VER_STR, MAX_VER_STR)
class MediaType(controllers_base.APIBase):
"""A media type representation."""
fields = (
'base',
'type',
)
class Experimental(controllers_base.APIBase):
"""The representation of the version experimental of the API."""
fields = (
'id',
'media_types',
'links',
'capsules'
)
@staticmethod
def convert():
experimental = Experimental()
experimental.id = "experimental"
experimental.links = [link.make_link('self', pecan.request.host_url,
'experimental', '',
bookmark=True),
link.make_link('describedby',
'https://docs.openstack.org',
'developer/zun/dev',
'api-spec-v1.html',
bookmark=True,
type='text/html')]
experimental.media_types = \
[MediaType(base='application/json',
type='application/vnd.openstack.'
'zun.experimental+json')]
experimental.capsules = [link.make_link('self',
pecan.request.host_url,
'experimental/capsules', '',
bookmark=True),
link.make_link('bookmark',
pecan.request.host_url,
'capsules', '',
bookmark=True)]
return experimental
class Controller(controllers_base.Controller):
"""Version expereimental API controller root."""
capsules = capsule_controller.CapsuleController()
@pecan.expose('json')
def get(self):
return Experimental.convert()
def _check_version(self, version, headers=None):
if headers is None:
headers = {}
# ensure that major version in the URL matches the header
if version.major != BASE_VERSION:
raise http_error.HTTPNotAcceptableAPIVersion(_(
"Mutually exclusive versions requested. Version %(ver)s "
"requested but not supported by this service. "
"The supported version range is: "
"[%(min)s, %(max)s].") % {'ver': version,
'min': MIN_VER_STR,
'max': MAX_VER_STR},
headers=headers,
max_version=str(MAX_VER),
min_version=str(MIN_VER))
# ensure the minor version is within the supported range
if version < MIN_VER or version > MAX_VER:
raise http_error.HTTPNotAcceptableAPIVersion(_(
"Version %(ver)s was requested but the minor version is not "
"supported by this service. The supported version range is: "
"[%(min)s, %(max)s].") % {'ver': version, 'min': MIN_VER_STR,
'max': MAX_VER_STR},
headers=headers,
max_version=str(MAX_VER),
min_version=str(MIN_VER))
@pecan.expose()
def _route(self, args):
version = ver.Version(
pecan.request.headers, MIN_VER_STR, MAX_VER_STR)
# Always set the basic version headers
pecan.response.headers[ver.Version.min_string] = MIN_VER_STR
pecan.response.headers[ver.Version.max_string] = MAX_VER_STR
pecan.response.headers[ver.Version.string] = " ".join(
[ver.Version.service_string, str(version)])
pecan.response.headers["vary"] = ver.Version.string
# assert that requested version is supported
self._check_version(version, pecan.response.headers)
pecan.request.version = version
if pecan.request.body:
msg = ("Processing request: url: %(url)s, %(method)s, "
"body: %(body)s" %
{'url': pecan.request.url,
'method': pecan.request.method,
'body': pecan.request.body})
LOG.debug(msg)
return super(Controller, self)._route(args)
__all__ = (Controller)

View File

@ -1,43 +0,0 @@
# Copyright 2017 ARM Holdings.
#
# 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 pecan
from zun.api.controllers import base
from zun.api.controllers import link
class Collection(base.APIBase):
@property
def collection(self):
return getattr(self, self._type)
def has_next(self, limit):
"""Return whether collection has more items."""
return len(self.collection) and len(self.collection) == limit
def get_next(self, limit, url=None, **kwargs):
"""Return a link to the next subset of the collection."""
if not self.has_next(limit):
return None
resource_url = url or self._type
q_args = ''.join(['%s=%s&' % (key, kwargs[key]) for key in kwargs])
next_args = '?%(args)slimit=%(limit)d&marker=%(marker)s' % {
'args': q_args, 'limit': limit,
'marker': self.collection[-1]['uuid']}
return link.make_link('next', pecan.request.host_url,
resource_url, next_args)['href']

View File

@ -14,7 +14,6 @@ import pecan
from pecan import rest from pecan import rest
from zun.api.controllers import base from zun.api.controllers import base
from zun.api.controllers import experimental
from zun.api.controllers import link from zun.api.controllers import link
from zun.api.controllers import v1 from zun.api.controllers import v1
from zun.api.controllers import versions from zun.api.controllers import versions
@ -70,14 +69,13 @@ class Root(base.APIBase):
class RootController(rest.RestController): class RootController(rest.RestController):
_versions = ['v1', 'experimental'] _versions = ['v1']
"""All supported API versions""" """All supported API versions"""
_default_version = 'v1' _default_version = 'v1'
"""The default API version""" """The default API version"""
v1 = v1.Controller() v1 = v1.Controller()
experimental = experimental.Controller()
@pecan.expose('json') @pecan.expose('json')
def get(self): def get(self):

View File

@ -22,8 +22,8 @@ from oslo_log import log as logging
import pecan import pecan
from zun.api.controllers import base as controllers_base from zun.api.controllers import base as controllers_base
from zun.api.controllers.experimental import capsules as capsule_controller
from zun.api.controllers import link from zun.api.controllers import link
from zun.api.controllers.v1 import capsules as capsule_controller
from zun.api.controllers.v1 import containers as container_controller from zun.api.controllers.v1 import containers as container_controller
from zun.api.controllers.v1 import hosts as host_controller from zun.api.controllers.v1 import hosts as host_controller
from zun.api.controllers.v1 import images as image_controller from zun.api.controllers.v1 import images as image_controller
@ -66,7 +66,8 @@ class V1(controllers_base.APIBase):
'services', 'services',
'containers', 'containers',
'images', 'images',
'hosts' 'hosts',
'capsules'
) )
@staticmethod @staticmethod
@ -106,6 +107,12 @@ class V1(controllers_base.APIBase):
pecan.request.host_url, pecan.request.host_url,
'hosts', '', 'hosts', '',
bookmark=True)] bookmark=True)]
v1.capsules = [link.make_link('self', pecan.request.host_url,
'capsules', ''),
link.make_link('bookmark',
pecan.request.host_url,
'capsules', '',
bookmark=True)]
return v1 return v1

View File

@ -13,14 +13,15 @@
# under the License. # under the License.
from oslo_log import log as logging from oslo_log import log as logging
import pecan import pecan
import six import six
from zun.api.controllers import base from zun.api.controllers import base
from zun.api.controllers.experimental import collection
from zun.api.controllers.experimental.schemas import capsules as schema
from zun.api.controllers.experimental.views import capsules_view as view
from zun.api.controllers import link from zun.api.controllers import link
from zun.api.controllers.v1 import collection
from zun.api.controllers.v1.schemas import capsules as schema
from zun.api.controllers.v1.views import capsules_view as view
from zun.api import utils as api_utils from zun.api import utils as api_utils
from zun.common import consts from zun.common import consts
from zun.common import exception from zun.common import exception

View File

@ -1,19 +0,0 @@
[pipeline:main]
pipeline = cors request_id authtoken api_experimental
[app:api_experimental]
paste.app_factory = zun.api.app:app_factory
[filter:authtoken]
acl_public_routes = /experimental
paste.filter_factory = zun.api.middleware.auth_token:AuthTokenMiddleware.factory
[filter:request_id]
paste.filter_factory = oslo_middleware:RequestId.factory
[filter:cors]
paste.filter_factory = oslo_middleware.cors:filter_factory
oslo_config_project = zun
latent_allow_methods = GET, PUT, POST, DELETE
latent_allow_headers = X-Auth-Token, X-Identity-Status, X-Roles, X-Service-Catalog, X-User-Id, X-Tenant-Id, X-OpenStack-Request-ID
latent_expose_headers = X-Auth-Token, X-Subject-Token, X-Service-Token, X-OpenStack-Request-ID

View File

@ -67,20 +67,8 @@ class TestRootController(api_base.FunctionalTest):
'images': [{'href': 'http://localhost/v1/images/', 'images': [{'href': 'http://localhost/v1/images/',
'rel': 'self'}, 'rel': 'self'},
{'href': 'http://localhost/images/', {'href': 'http://localhost/images/',
'rel': 'bookmark'}]} 'rel': 'bookmark'}],
'capsules': [{'href': 'http://localhost/v1/capsules/',
self.experimental_expected = {
'media_types':
[{'base': 'application/json',
'type': 'application/vnd.openstack.zun.experimental+json'}],
'links': [{'href': 'http://localhost/experimental/',
'rel': 'self'},
{'href':
'https://docs.openstack.org/developer'
'/zun/dev/api-spec-v1.html',
'type': 'text/html', 'rel': 'describedby'}],
'id': 'experimental',
'capsules': [{'href': 'http://localhost/experimental/capsules/',
'rel': 'self'}, 'rel': 'self'},
{'href': 'http://localhost/capsules/', {'href': 'http://localhost/capsules/',
'rel': 'bookmark'}]} 'rel': 'bookmark'}]}
@ -98,10 +86,6 @@ class TestRootController(api_base.FunctionalTest):
response = self.app.get('/v1/') response = self.app.get('/v1/')
self.assertEqual(self.v1_expected, response.json) self.assertEqual(self.v1_expected, response.json)
def test_experimental_controller(self):
response = self.app.get('/experimental/')
self.assertEqual(self.experimental_expected, response.json)
def test_get_not_found(self): def test_get_not_found(self):
response = self.app.get('/a/bogus/url', expect_errors=True) response = self.app.get('/a/bogus/url', expect_errors=True)
assert response.status_int == 404 assert response.status_int == 404
@ -159,14 +143,3 @@ class TestRootController(api_base.FunctionalTest):
response = app.get('/v1/containers', expect_errors=True) response = app.get('/v1/containers', expect_errors=True)
self.assertEqual(401, response.status_int) self.assertEqual(401, response.status_int)
def test_auth_with_experimental_access(self):
paste_file = \
"zun/tests/unit/api/controllers/auth-experimental-access.ini"
app = self.make_app(paste_file)
response = app.get('/', expect_errors=True)
self.assertEqual(401, response.status_int)
response = app.get('/experimental/')
self.assertEqual(self.experimental_expected, response.json)

View File

@ -42,7 +42,7 @@ class TestCapsuleController(api_base.FunctionalTest):
' "name": "capsule-example"}' ' "name": "capsule-example"}'
' }' ' }'
'}') '}')
response = self.post('/capsules/', response = self.post('/v1/capsules/',
params=params, params=params,
content_type='application/json') content_type='application/json')
return_value = response.json return_value = response.json
@ -79,7 +79,7 @@ class TestCapsuleController(api_base.FunctionalTest):
' "name": "capsule-example"}' ' "name": "capsule-example"}'
' }' ' }'
'}') '}')
response = self.post('/capsules/', response = self.post('/v1/capsules/',
params=params, params=params,
content_type='application/json') content_type='application/json')
return_value = response.json return_value = response.json
@ -156,7 +156,7 @@ class TestCapsuleController(api_base.FunctionalTest):
'"name": "capsule-example"}}}') '"name": "capsule-example"}}}')
mock_check_template.side_effect = exception.InvalidCapsuleTemplate( mock_check_template.side_effect = exception.InvalidCapsuleTemplate(
"Container image is needed") "Container image is needed")
self.assertRaises(AppError, self.post, '/capsules/', self.assertRaises(AppError, self.post, '/v1/capsules/',
params=params, content_type='application/json') params=params, content_type='application/json')
self.assertFalse(mock_capsule_create.called) self.assertFalse(mock_capsule_create.called)
@ -193,7 +193,7 @@ class TestCapsuleController(api_base.FunctionalTest):
' "name": "capsule-example"}' ' "name": "capsule-example"}'
' }' ' }'
'}') '}')
response = self.post('/capsules/', response = self.post('/v1/capsules/',
params=params, params=params,
content_type='application/json') content_type='application/json')
return_value = response.json return_value = response.json
@ -247,7 +247,7 @@ class TestCapsuleController(api_base.FunctionalTest):
' "name": "capsule-example"}' ' "name": "capsule-example"}'
' }' ' }'
'}') '}')
response = self.post('/capsules/', response = self.post('/v1/capsules/',
params=params, params=params,
content_type='application/json') content_type='application/json')
return_value = response.json return_value = response.json
@ -310,7 +310,7 @@ class TestCapsuleController(api_base.FunctionalTest):
' "name": "capsule-example"}' ' "name": "capsule-example"}'
' }' ' }'
'}') '}')
response = self.post('/capsules/', response = self.post('/v1/capsules/',
params=params, params=params,
content_type='application/json') content_type='application/json')
return_value = response.json return_value = response.json
@ -347,7 +347,7 @@ class TestCapsuleController(api_base.FunctionalTest):
test_capsule_obj = objects.Capsule(self.context, **test_capsule) test_capsule_obj = objects.Capsule(self.context, **test_capsule)
mock_capsule_get_by_uuid.return_value = test_capsule_obj mock_capsule_get_by_uuid.return_value = test_capsule_obj
response = self.get('/capsules/%s/' % test_capsule['uuid']) response = self.get('/v1/capsules/%s/' % test_capsule['uuid'])
context = mock_capsule_get_by_uuid.call_args[0][0] context = mock_capsule_get_by_uuid.call_args[0][0]
self.assertIs(False, context.all_projects) self.assertIs(False, context.all_projects)
@ -399,7 +399,7 @@ class TestCapsuleController(api_base.FunctionalTest):
mock_capsule_delete.return_value = True mock_capsule_delete.return_value = True
capsule_uuid = test_capsule.get('uuid') capsule_uuid = test_capsule.get('uuid')
response = self.app.delete('/capsules/%s' % capsule_uuid) response = self.app.delete('/v1/capsules/%s' % capsule_uuid)
self.assertTrue(mock_capsule_delete.called) self.assertTrue(mock_capsule_delete.called)
self.assertEqual(204, response.status_int) self.assertEqual(204, response.status_int)
@ -431,7 +431,7 @@ class TestCapsuleController(api_base.FunctionalTest):
capsule_uuid = test_capsule.get('uuid') capsule_uuid = test_capsule.get('uuid')
response = self.app.delete( response = self.app.delete(
'/capsules/%s/?all_projects=1' % capsule_uuid) '/v1/capsules/%s/?all_projects=1' % capsule_uuid)
self.assertTrue(mock_capsule_delete.called) self.assertTrue(mock_capsule_delete.called)
self.assertEqual(204, response.status_int) self.assertEqual(204, response.status_int)
@ -463,7 +463,7 @@ class TestCapsuleController(api_base.FunctionalTest):
mock_capsule_delete.return_value = True mock_capsule_delete.return_value = True
capsule_name = test_capsule.get('meta_name') capsule_name = test_capsule.get('meta_name')
response = self.app.delete('/capsules/%s/' % response = self.app.delete('/v1/capsules/%s/' %
capsule_name) capsule_name)
self.assertTrue(mock_capsule_delete.called) self.assertTrue(mock_capsule_delete.called)
@ -487,7 +487,7 @@ class TestCapsuleController(api_base.FunctionalTest):
mock_capsule_list.return_value = [test_capsule_obj] mock_capsule_list.return_value = [test_capsule_obj]
mock_container_show.return_value = test_container_obj mock_container_show.return_value = test_container_obj
response = self.app.get('/capsules/') response = self.app.get('/v1/capsules/')
mock_capsule_list.assert_called_once_with(mock.ANY, mock_capsule_list.assert_called_once_with(mock.ANY,
1000, None, 'id', 'asc', 1000, None, 'id', 'asc',
@ -517,7 +517,7 @@ class TestCapsuleController(api_base.FunctionalTest):
mock_capsule_list.return_value = [test_capsule_obj] mock_capsule_list.return_value = [test_capsule_obj]
mock_container_show.return_value = test_container_obj mock_container_show.return_value = test_container_obj
response = self.app.get('/capsules/?all_projects=1') response = self.app.get('/v1/capsules/?all_projects=1')
mock_capsule_list.assert_called_once_with(mock.ANY, mock_capsule_list.assert_called_once_with(mock.ANY,
1000, None, 'id', 'asc', 1000, None, 'id', 'asc',
@ -544,7 +544,7 @@ class TestCapsuleController(api_base.FunctionalTest):
test_capsule_obj = objects.Capsule(self.context, **test_capsule) test_capsule_obj = objects.Capsule(self.context, **test_capsule)
mock_capsule_list.return_value = [test_capsule_obj] mock_capsule_list.return_value = [test_capsule_obj]
response = self.app.get('/capsules/') response = self.app.get('/v1/capsules/')
mock_capsule_list.assert_called_once_with(mock.ANY, mock_capsule_list.assert_called_once_with(mock.ANY,
1000, None, 'id', 'asc', 1000, None, 'id', 'asc',
@ -582,7 +582,7 @@ class TestCapsuleController(api_base.FunctionalTest):
mock_container_show.return_value = capsule_list[-1] mock_container_show.return_value = capsule_list[-1]
mock_capsule_save.return_value = True mock_capsule_save.return_value = True
response = self.app.get('/capsules/?limit=3&marker=%s' response = self.app.get('/v1/capsules/?limit=3&marker=%s'
% capsule_list[2].uuid) % capsule_list[2].uuid)
self.assertEqual(200, response.status_int) self.assertEqual(200, response.status_int)