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 {
create_service_user "zun" "admin"
create_service_user "zun-experimental" "admin"
if is_service_enabled zun-api; then
@ -154,18 +153,11 @@ function create_zun_accounts {
local zun_service=$(get_or_create_service "zun" \
"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 \
"$REGION_NAME" \
"$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
}

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
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 |
+------------------+------------------------+---------+-----------+--------------------------------------+
Capsule API is currently in v1 phase now.
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
$ 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
rules for it.
@ -193,21 +168,21 @@ Delete capsule:
.. code-block:: console
$ zun --experimental-api capsule-delete <uuid>
$ zun --experimental-api capsule-delete <capsule-name>
$ zun capsule-delete <uuid>
$ zun capsule-delete <capsule-name>
List capsule:
.. code-block:: console
$ zun --experimental-api capsule-list
$ zun capsule-list
Describe capsule:
.. code-block:: console
$ zun --experimental-api capsule-describe <uuid>
$ zun --experimental-api capsule-describe <capsule-name>
$ zun capsule-describe <uuid>
$ zun capsule-describe <capsule-name>
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 zun.api.controllers import base
from zun.api.controllers import experimental
from zun.api.controllers import link
from zun.api.controllers import v1
from zun.api.controllers import versions
@ -70,14 +69,13 @@ class Root(base.APIBase):
class RootController(rest.RestController):
_versions = ['v1', 'experimental']
_versions = ['v1']
"""All supported API versions"""
_default_version = 'v1'
"""The default API version"""
v1 = v1.Controller()
experimental = experimental.Controller()
@pecan.expose('json')
def get(self):

View File

@ -22,8 +22,8 @@ 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.v1 import capsules as capsule_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 images as image_controller
@ -66,7 +66,8 @@ class V1(controllers_base.APIBase):
'services',
'containers',
'images',
'hosts'
'hosts',
'capsules'
)
@staticmethod
@ -106,6 +107,12 @@ class V1(controllers_base.APIBase):
pecan.request.host_url,
'hosts', '',
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

View File

@ -13,14 +13,15 @@
# under the License.
from oslo_log import log as logging
import pecan
import six
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.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.common import consts
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/',
'rel': 'self'},
{'href': 'http://localhost/images/',
'rel': 'bookmark'}]}
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': 'bookmark'}],
'capsules': [{'href': 'http://localhost/v1/capsules/',
'rel': 'self'},
{'href': 'http://localhost/capsules/',
'rel': 'bookmark'}]}
@ -98,10 +86,6 @@ class TestRootController(api_base.FunctionalTest):
response = self.app.get('/v1/')
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):
response = self.app.get('/a/bogus/url', expect_errors=True)
assert response.status_int == 404
@ -159,14 +143,3 @@ class TestRootController(api_base.FunctionalTest):
response = app.get('/v1/containers', expect_errors=True)
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"}'
' }'
'}')
response = self.post('/capsules/',
response = self.post('/v1/capsules/',
params=params,
content_type='application/json')
return_value = response.json
@ -79,7 +79,7 @@ class TestCapsuleController(api_base.FunctionalTest):
' "name": "capsule-example"}'
' }'
'}')
response = self.post('/capsules/',
response = self.post('/v1/capsules/',
params=params,
content_type='application/json')
return_value = response.json
@ -156,7 +156,7 @@ class TestCapsuleController(api_base.FunctionalTest):
'"name": "capsule-example"}}}')
mock_check_template.side_effect = exception.InvalidCapsuleTemplate(
"Container image is needed")
self.assertRaises(AppError, self.post, '/capsules/',
self.assertRaises(AppError, self.post, '/v1/capsules/',
params=params, content_type='application/json')
self.assertFalse(mock_capsule_create.called)
@ -193,7 +193,7 @@ class TestCapsuleController(api_base.FunctionalTest):
' "name": "capsule-example"}'
' }'
'}')
response = self.post('/capsules/',
response = self.post('/v1/capsules/',
params=params,
content_type='application/json')
return_value = response.json
@ -247,7 +247,7 @@ class TestCapsuleController(api_base.FunctionalTest):
' "name": "capsule-example"}'
' }'
'}')
response = self.post('/capsules/',
response = self.post('/v1/capsules/',
params=params,
content_type='application/json')
return_value = response.json
@ -310,7 +310,7 @@ class TestCapsuleController(api_base.FunctionalTest):
' "name": "capsule-example"}'
' }'
'}')
response = self.post('/capsules/',
response = self.post('/v1/capsules/',
params=params,
content_type='application/json')
return_value = response.json
@ -347,7 +347,7 @@ class TestCapsuleController(api_base.FunctionalTest):
test_capsule_obj = objects.Capsule(self.context, **test_capsule)
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]
self.assertIs(False, context.all_projects)
@ -399,7 +399,7 @@ class TestCapsuleController(api_base.FunctionalTest):
mock_capsule_delete.return_value = True
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.assertEqual(204, response.status_int)
@ -431,7 +431,7 @@ class TestCapsuleController(api_base.FunctionalTest):
capsule_uuid = test_capsule.get('uuid')
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.assertEqual(204, response.status_int)
@ -463,7 +463,7 @@ class TestCapsuleController(api_base.FunctionalTest):
mock_capsule_delete.return_value = True
capsule_name = test_capsule.get('meta_name')
response = self.app.delete('/capsules/%s/' %
response = self.app.delete('/v1/capsules/%s/' %
capsule_name)
self.assertTrue(mock_capsule_delete.called)
@ -487,7 +487,7 @@ class TestCapsuleController(api_base.FunctionalTest):
mock_capsule_list.return_value = [test_capsule_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,
1000, None, 'id', 'asc',
@ -517,7 +517,7 @@ class TestCapsuleController(api_base.FunctionalTest):
mock_capsule_list.return_value = [test_capsule_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,
1000, None, 'id', 'asc',
@ -544,7 +544,7 @@ class TestCapsuleController(api_base.FunctionalTest):
test_capsule_obj = objects.Capsule(self.context, **test_capsule)
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,
1000, None, 'id', 'asc',
@ -582,7 +582,7 @@ class TestCapsuleController(api_base.FunctionalTest):
mock_container_show.return_value = capsule_list[-1]
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)
self.assertEqual(200, response.status_int)