diff --git a/zun/api/controllers/v1/containers.py b/zun/api/controllers/v1/containers.py index 856a5959a..41a4896f1 100644 --- a/zun/api/controllers/v1/containers.py +++ b/zun/api/controllers/v1/containers.py @@ -75,7 +75,7 @@ class ContainerCollection(collection.Collection): context = pecan.request.context collection = ContainerCollection() collection.containers = \ - [view.format_container(context, url, p.as_dict()) + [view.format_container(context, url, p) for p in rpc_containers] collection.next = collection.get_next(limit, url=url, **kwargs) return collection @@ -267,7 +267,7 @@ class ContainersController(base.Controller): raise exception.ServerNotUsable return view.format_container(context, pecan.request.host_url, - container.as_dict()) + container) def _generate_name_for_container(self): """Generate a random name like: zeta-22-container.""" @@ -413,6 +413,12 @@ class ContainersController(base.Controller): exposed_ports = utils.build_exposed_ports(exposed_ports) container_dict['exposed_ports'] = exposed_ports + registry = container_dict.pop('registry', None) + if registry: + api_utils.version_check('registry', '1.31') + registry = utils.get_registry(registry) + container_dict['registry_id'] = registry.id + container_dict['status'] = consts.CREATING extra_spec = {} extra_spec['hints'] = container_dict.get('hints', None) @@ -435,7 +441,7 @@ class ContainersController(base.Controller): new_container.uuid) pecan.response.status = 202 return view.format_container(context, pecan.request.host_url, - new_container.as_dict()) + new_container) def _check_container_quotas(self, context, container_delta_dict, update_container=False): @@ -706,7 +712,7 @@ class ContainersController(base.Controller): compute_api = pecan.request.compute_api container = compute_api.container_update(context, container, patch) return view.format_container(context, pecan.request.host_url, - container.as_dict()) + container) @base.Controller.api_version("1.1", "1.13") @pecan.expose('json') @@ -727,7 +733,7 @@ class ContainersController(base.Controller): context = pecan.request.context container.save(context) return view.format_container(context, pecan.request.host_url, - container.as_dict()) + container) @base.Controller.api_version("1.19") @pecan.expose('json') @@ -752,7 +758,7 @@ class ContainersController(base.Controller): compute_api.resize_container(context, container, kwargs) pecan.response.status = 202 return view.format_container(context, pecan.request.host_url, - container.as_dict()) + container) @pecan.expose('json') @exception.wrap_pecan_controller_exception diff --git a/zun/api/controllers/v1/registries.py b/zun/api/controllers/v1/registries.py index ecf38a77e..9a1fc4b7c 100644 --- a/zun/api/controllers/v1/registries.py +++ b/zun/api/controllers/v1/registries.py @@ -39,14 +39,6 @@ RESOURCE_NAME = 'registry' COLLECTION_NAME = 'registries' -def _get_registry(registry_ident): - registry = api_utils.get_resource('Registry', registry_ident) - if not registry: - raise exception.RegistryNotFound(registry=registry_ident) - - return registry - - def check_policy_on_registry(registry, action): context = pecan.request.context policy.enforce(context, action, registry, action=action) @@ -160,7 +152,7 @@ class RegistryController(base.Controller): context = pecan.request.context if context.is_admin: context.all_projects = True - registry = _get_registry(registry_ident) + registry = utils.get_registry(registry_ident) policy_action = policies.REGISTRY % 'get_one' check_policy_on_registry(registry.as_dict(), policy_action) return RegistryItem.render_response(registry) @@ -181,7 +173,7 @@ class RegistryController(base.Controller): # Set the HTTP Location Header pecan.response.location = link.build_url(COLLECTION_NAME, new_registry.uuid) - pecan.response.status = 202 + pecan.response.status = 201 return RegistryItem.render_response(new_registry) @pecan.expose('json') @@ -193,7 +185,7 @@ class RegistryController(base.Controller): :param registry_ident: UUID or name of a registry. :param registry_dict: a json document to apply to this registry. """ - registry = _get_registry(registry_ident) + registry = utils.get_registry(registry_ident) context = pecan.request.context policy_action = policies.REGISTRY % 'update' check_policy_on_registry(registry.as_dict(), policy_action) @@ -220,7 +212,7 @@ class RegistryController(base.Controller): context = pecan.request.context if context.is_admin: context.all_projects = True - registry = _get_registry(registry_ident) + registry = utils.get_registry(registry_ident) policy_action = policies.REGISTRY % 'delete' check_policy_on_registry(registry.as_dict(), policy_action) registry.destroy(context) diff --git a/zun/api/controllers/v1/schemas/containers.py b/zun/api/controllers/v1/schemas/containers.py index 93c2bae67..4ef3826d7 100644 --- a/zun/api/controllers/v1/schemas/containers.py +++ b/zun/api/controllers/v1/schemas/containers.py @@ -41,6 +41,7 @@ _legacy_container_properties = { 'privileged': parameter_types.boolean, 'healthcheck': parameter_types.healthcheck, 'exposed_ports': parameter_types.exposed_ports, + 'registry': parameter_types.container_registry, } legacy_container_create = { diff --git a/zun/api/controllers/v1/schemas/parameter_types.py b/zun/api/controllers/v1/schemas/parameter_types.py index 2d0257818..3205ce1de 100644 --- a/zun/api/controllers/v1/schemas/parameter_types.py +++ b/zun/api/controllers/v1/schemas/parameter_types.py @@ -572,3 +572,10 @@ registry_password = { 'type': ['string'], 'minLength': 1, } + +container_registry = { + 'type': ['string'], + 'minLength': 2, + 'maxLength': 255, + 'pattern': '^[a-zA-Z0-9][a-zA-Z0-9_.-]+$' +} diff --git a/zun/api/controllers/v1/views/capsules_view.py b/zun/api/controllers/v1/views/capsules_view.py index 020c61b46..2ba4838b6 100644 --- a/zun/api/controllers/v1/views/capsules_view.py +++ b/zun/api/controllers/v1/views/capsules_view.py @@ -55,7 +55,7 @@ def format_capsule(url, capsule, context): bookmark=True)]) elif key == 'containers': containers = [] - for c in value: + for c in capsule.containers: container = containers_view.format_container( context, None, c) containers.append(container) diff --git a/zun/api/controllers/v1/views/containers_view.py b/zun/api/controllers/v1/views/containers_view.py index 86af94dbb..e0b5de60b 100644 --- a/zun/api/controllers/v1/views/containers_view.py +++ b/zun/api/controllers/v1/views/containers_view.py @@ -49,6 +49,7 @@ _basic_keys = ( 'privileged', 'healthcheck', 'cpu_policy', + 'registry_id', ) @@ -69,8 +70,14 @@ def format_container(context, url, container): 'bookmark', url, 'containers', value, bookmark=True)]) + elif key == 'registry_id': + if value: + # the value is an internal id so replace it with the + # user-facing uuid + value = container.registry.uuid + yield ('registry_id', value) else: yield (key, value) return dict(itertools.chain.from_iterable( - transform(k, v) for k, v in container.items())) + transform(k, v) for k, v in container.as_dict().items())) diff --git a/zun/api/controllers/versions.py b/zun/api/controllers/versions.py index 2a656c4bd..f4d5e88e3 100644 --- a/zun/api/controllers/versions.py +++ b/zun/api/controllers/versions.py @@ -63,10 +63,11 @@ REST_API_VERSION_HISTORY = """REST API Version History: * 1.28 - Add support cpuset * 1.29 - Add enable_cpu_pinning to compute_node * 1.30 - Introduce API resource for representing private registry + * 1.31 - Add 'registry_id' to containers """ BASE_VER = '1.1' -CURRENT_MAX_VER = '1.30' +CURRENT_MAX_VER = '1.31' class Version(object): diff --git a/zun/api/rest_api_version_history.rst b/zun/api/rest_api_version_history.rst index 0df1c5c1b..33fe6b4f1 100644 --- a/zun/api/rest_api_version_history.rst +++ b/zun/api/rest_api_version_history.rst @@ -54,7 +54,7 @@ user documentation. 1.5 --- - Add a new attribure 'runtime' to the request to create a container. + Add a new attribute 'runtime' to the request to create a container. Users can use this attribute to choose runtime for their containers. The specified runtime should be configured by admin to run with Zun. The default runtime for Zun is runc. @@ -235,3 +235,9 @@ user documentation. ---- Introduce API endpoint for create/read/update/delete private registry. + +1.31 +---- + + Add 'registry_id' to container resource. + This attribute indicate the registry from which the container pulls images. diff --git a/zun/common/utils.py b/zun/common/utils.py index 4f3a4cacf..8e45924ea 100644 --- a/zun/common/utils.py +++ b/zun/common/utils.py @@ -488,6 +488,14 @@ def get_image(image_id): return image +def get_registry(registry_id): + registry = api_utils.get_resource('Registry', registry_id) + if not registry: + raise exception.RegistryNotFound(registry=registry_id) + + return registry + + def check_for_restart_policy(container_dict): """Check for restart policy input diff --git a/zun/db/api.py b/zun/db/api.py index 5e0d9a6df..13144f52e 100644 --- a/zun/db/api.py +++ b/zun/db/api.py @@ -1179,6 +1179,18 @@ def create_registry(context, values): return _get_dbdriver_instance().create_registry(context, values) +@profiler.trace("db") +def get_registry_by_id(context, registry_id): + """Return a registry. + + :param context: The security context + :param registry_id: The id of a registry. + :returns: A registry. + """ + return _get_dbdriver_instance().get_registry_by_id( + context, registry_id) + + @profiler.trace("db") def get_registry_by_uuid(context, registry_uuid): """Return a registry. diff --git a/zun/db/sqlalchemy/alembic/versions/1bc34e18180b_add_registry_id_to_container.py b/zun/db/sqlalchemy/alembic/versions/1bc34e18180b_add_registry_id_to_container.py new file mode 100644 index 000000000..a86073af2 --- /dev/null +++ b/zun/db/sqlalchemy/alembic/versions/1bc34e18180b_add_registry_id_to_container.py @@ -0,0 +1,36 @@ +# 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. + +"""add registry_id to container + +Revision ID: 1bc34e18180b +Revises: 5ffc1cabe6b4 +Create Date: 2019-01-06 21:45:57.505152 + +""" + +# revision identifiers, used by Alembic. +revision = '1bc34e18180b' +down_revision = '5ffc1cabe6b4' +branch_labels = None +depends_on = None + +from alembic import op +import sqlalchemy as sa + + +def upgrade(): + op.add_column('container', + sa.Column('registry_id', sa.Integer(), + nullable=True)) + op.create_foreign_key( + None, 'container', 'registry', ['registry_id'], ['id']) diff --git a/zun/db/sqlalchemy/api.py b/zun/db/sqlalchemy/api.py index 8df29ef1d..83c96dd83 100644 --- a/zun/db/sqlalchemy/api.py +++ b/zun/db/sqlalchemy/api.py @@ -1426,6 +1426,17 @@ class Connection(object): ref.update(values) return ref + def get_registry_by_id(self, context, registry_id): + query = model_query(models.Registry) + query = self._add_project_filters(context, query) + query = query.filter_by(id=registry_id) + try: + result = query.one() + result['password'] = crypt.decrypt(result['password']) + return result + except NoResultFound: + raise exception.RegistryNotFound(registry=registry_id) + def get_registry_by_uuid(self, context, registry_uuid): query = model_query(models.Registry) query = self._add_project_filters(context, query) @@ -1457,6 +1468,11 @@ class Connection(object): with session.begin(): query = model_query(models.Registry, session=session) query = add_identity_filter(query, registry_uuid) - count = query.delete() - if count != 1: - raise exception.RegistryNotFound(registry=registry_uuid) + try: + count = query.delete() + if count != 1: + raise exception.RegistryNotFound(registry=registry_uuid) + except db_exc.DBReferenceError: + raise exception.Conflict('Failed to delete registry ' + '%(registry)s since it is in use.', + registry=registry_uuid) diff --git a/zun/db/sqlalchemy/models.py b/zun/db/sqlalchemy/models.py index 53f7f3fde..800dedfb5 100644 --- a/zun/db/sqlalchemy/models.py +++ b/zun/db/sqlalchemy/models.py @@ -175,6 +175,9 @@ class Container(Base): privileged = Column(Boolean, default=False) healthcheck = Column(JSONEncodedDict) exposed_ports = Column(JSONEncodedDict) + registry_id = Column(Integer, + ForeignKey('registry.id'), + nullable=True) class VolumeMapping(Base): diff --git a/zun/objects/container.py b/zun/objects/container.py index 9f9295d32..0a825f491 100644 --- a/zun/objects/container.py +++ b/zun/objects/container.py @@ -20,12 +20,13 @@ from zun.objects import base from zun.objects import exec_instance as exec_inst from zun.objects import fields as z_fields from zun.objects import pci_device +from zun.objects import registry LOG = logging.getLogger(__name__) -CONTAINER_OPTIONAL_ATTRS = ["pci_devices", "exec_instances"] +CONTAINER_OPTIONAL_ATTRS = ["pci_devices", "exec_instances", "registry"] @base.ZunObjectRegistry.register @@ -96,7 +97,8 @@ class Container(base.ZunPersistentObject, base.ZunObject): # Version 1.36: Add 'get_count' method # Version 1.37: Add 'exposed_ports' attribute # Version 1.38: Add 'cpuset' attribute - VERSION = '1.37' + # Version 1.39: Add 'register' and 'registry_id' attributes + VERSION = '1.39' fields = { 'id': fields.IntegerField(), @@ -143,13 +145,15 @@ class Container(base.ZunPersistentObject, base.ZunObject): nullable=True), 'privileged': fields.BooleanField(nullable=True), 'healthcheck': z_fields.JsonField(nullable=True), + 'registry_id': fields.IntegerField(nullable=True), + 'registry': fields.ObjectField("Registry", nullable=True), } @staticmethod def _from_db_object(container, db_container): """Converts a database entity to a formal object.""" for field in container.fields: - if field in ['pci_devices', 'exec_instances']: + if field in ['pci_devices', 'exec_instances', 'registry']: continue if field == 'cpuset': container.cpuset = Cpuset._from_dict( @@ -349,6 +353,9 @@ class Container(base.ZunPersistentObject, base.ZunObject): if attrname == 'exec_instances': self._load_exec_instances() + if attrname == 'registry': + self._load_registry() + self.obj_reset_changes([attrname]) def _load_pci_devices(self): @@ -359,6 +366,12 @@ class Container(base.ZunPersistentObject, base.ZunObject): self.exec_instances = exec_inst.ExecInstance.list_by_container_id( self._context, self.id) + def _load_registry(self): + self.registry = None + if self.registry_id: + self.registry = registry.Registry.get_by_id( + self._context, self.registry_id) + @base.remotable_classmethod def get_count(cls, context, project_id, flag): """Get the counts of Container objects in the database. diff --git a/zun/objects/registry.py b/zun/objects/registry.py index c46129599..50b534d38 100644 --- a/zun/objects/registry.py +++ b/zun/objects/registry.py @@ -51,6 +51,18 @@ class Registry(base.ZunPersistentObject, base.ZunObject): return [Registry._from_db_object(cls(context), obj) for obj in db_objects] + @base.remotable_classmethod + def get_by_id(cls, context, id): + """Find a registry based on id and return a :class:`Registry` object. + + :param id: the id of a registry. + :param context: Security context + :returns: a :class:`Registry` object. + """ + db_registry = dbapi.get_registry_by_id(context, id) + registry = Registry._from_db_object(cls(context), db_registry) + return registry + @base.remotable_classmethod def get_by_uuid(cls, context, uuid): """Find a registry based on uuid and return a :class:`Registry` object. diff --git a/zun/tests/unit/api/base.py b/zun/tests/unit/api/base.py index c441a2756..741d8a7a4 100644 --- a/zun/tests/unit/api/base.py +++ b/zun/tests/unit/api/base.py @@ -26,7 +26,7 @@ from zun.tests.unit.db import base PATH_PREFIX = '/v1' -CURRENT_VERSION = "container 1.30" +CURRENT_VERSION = "container 1.31" class FunctionalTest(base.DbTestCase): diff --git a/zun/tests/unit/api/controllers/test_root.py b/zun/tests/unit/api/controllers/test_root.py index 6041c172d..bc3f91396 100644 --- a/zun/tests/unit/api/controllers/test_root.py +++ b/zun/tests/unit/api/controllers/test_root.py @@ -28,7 +28,7 @@ class TestRootController(api_base.FunctionalTest): 'default_version': {'id': 'v1', 'links': [{'href': 'http://localhost/v1/', 'rel': 'self'}], - 'max_version': '1.30', + 'max_version': '1.31', 'min_version': '1.1', 'status': 'CURRENT'}, 'description': 'Zun is an OpenStack project which ' @@ -37,7 +37,7 @@ class TestRootController(api_base.FunctionalTest): 'versions': [{'id': 'v1', 'links': [{'href': 'http://localhost/v1/', 'rel': 'self'}], - 'max_version': '1.30', + 'max_version': '1.31', 'min_version': '1.1', 'status': 'CURRENT'}]} diff --git a/zun/tests/unit/api/controllers/v1/test_registries.py b/zun/tests/unit/api/controllers/v1/test_registries.py index f30f68f2f..98068cf44 100644 --- a/zun/tests/unit/api/controllers/v1/test_registries.py +++ b/zun/tests/unit/api/controllers/v1/test_registries.py @@ -31,7 +31,7 @@ class TestRegistryController(api_base.FunctionalTest): params=params, content_type='application/json') - self.assertEqual(202, response.status_int) + self.assertEqual(201, response.status_int) self.assertEqual(1, len(response.json)) r = response.json['registry'] self.assertIsNotNone(r.get('uuid')) @@ -57,7 +57,7 @@ class TestRegistryController(api_base.FunctionalTest): params=params, content_type='application/json') - self.assertEqual(202, response.status_int) + self.assertEqual(201, response.status_int) self.assertEqual(1, len(response.json)) r = response.json['registry'] self.assertIsNotNone(r.get('uuid')) @@ -82,7 +82,7 @@ class TestRegistryController(api_base.FunctionalTest): response = self.post('/v1/registries/', params=params, content_type='application/json') - self.assertEqual(202, response.status_int) + self.assertEqual(201, response.status_int) response = self.get('/v1/registries/') self.assertEqual(200, response.status_int) self.assertEqual(2, len(response.json)) @@ -175,7 +175,7 @@ class TestRegistryController(api_base.FunctionalTest): response = self.post('/v1/registries/', params=params, content_type='application/json') - self.assertEqual(202, response.status_int) + self.assertEqual(201, response.status_int) # get by uuid registry_uuid = response.json['registry']['uuid'] response = self.get('/v1/registries/%s/' % registry_uuid) @@ -218,7 +218,7 @@ class TestRegistryController(api_base.FunctionalTest): response = self.post('/v1/registries/', params=params, content_type='application/json') - self.assertEqual(202, response.status_int) + self.assertEqual(201, response.status_int) registry_uuid = response.json['registry']['uuid'] params = {'registry': {'name': 'new-name', 'domain': 'new-domain', 'username': 'new-username', 'password': 'new-pass'}} @@ -242,7 +242,7 @@ class TestRegistryController(api_base.FunctionalTest): response = self.post('/v1/registries/', params=params, content_type='application/json') - self.assertEqual(202, response.status_int) + self.assertEqual(201, response.status_int) registry_uuid = response.json['registry']['uuid'] response = self.delete('/v1/registries/%s/' % registry_uuid) self.assertEqual(204, response.status_int) diff --git a/zun/tests/unit/db/utils.py b/zun/tests/unit/db/utils.py index 30c727a10..daee513ed 100644 --- a/zun/tests/unit/db/utils.py +++ b/zun/tests/unit/db/utils.py @@ -120,6 +120,7 @@ def get_test_container(**kwargs): 'exposed_ports': kwargs.get('exposed_ports', {"80/tcp": {}}), 'cpu_policy': kwargs.get('cpu_policy', None), 'cpuset': kwargs.get('cpuset', None), + 'registry_id': kwargs.get('registry_id', None), } diff --git a/zun/tests/unit/objects/test_objects.py b/zun/tests/unit/objects/test_objects.py index 46d7d1e91..499564902 100644 --- a/zun/tests/unit/objects/test_objects.py +++ b/zun/tests/unit/objects/test_objects.py @@ -344,7 +344,7 @@ class TestObject(test_base.TestCase, _TestObject): # For more information on object version testing, read # https://docs.openstack.org/zun/latest/ object_data = { - 'Container': '1.37-193d8cd6635760882a27142760931af9', + 'Container': '1.39-6a7bc5bcd85277c30982c1106f10c336', 'Cpuset': '1.0-06c4e6335683c18b87e2e54080f8c341', 'Volume': '1.0-4ec18c39ea49f898cc354f9ca178dfb7', 'VolumeMapping': '1.5-57febc66526185a75a744637e7a387c7', @@ -368,7 +368,7 @@ object_data = { 'ContainerActionEvent': '1.0-2974d0a6f5d4821fd4e223a88c10181a', 'Network': '1.1-26e8d37a54e5fc905ede657744a221d9', 'ExecInstance': '1.0-59464e7b96db847c0abb1e96d3cec30a', - 'Registry': '1.0-21ed56234497120755c60deba7c9e1dc', + 'Registry': '1.0-36c2053fbc30e0021630e657dd1699c9', }