Merge "Add registry_id to container"

This commit is contained in:
Zuul 2019-02-12 03:54:29 +00:00 committed by Gerrit Code Review
commit 54d86884bd
20 changed files with 160 additions and 39 deletions

View File

@ -75,7 +75,7 @@ class ContainerCollection(collection.Collection):
context = pecan.request.context context = pecan.request.context
collection = ContainerCollection() collection = ContainerCollection()
collection.containers = \ collection.containers = \
[view.format_container(context, url, p.as_dict()) [view.format_container(context, url, p)
for p in rpc_containers] for p in rpc_containers]
collection.next = collection.get_next(limit, url=url, **kwargs) collection.next = collection.get_next(limit, url=url, **kwargs)
return collection return collection
@ -267,7 +267,7 @@ class ContainersController(base.Controller):
raise exception.ServerNotUsable raise exception.ServerNotUsable
return view.format_container(context, pecan.request.host_url, return view.format_container(context, pecan.request.host_url,
container.as_dict()) container)
def _generate_name_for_container(self): def _generate_name_for_container(self):
"""Generate a random name like: zeta-22-container.""" """Generate a random name like: zeta-22-container."""
@ -413,6 +413,12 @@ class ContainersController(base.Controller):
exposed_ports = utils.build_exposed_ports(exposed_ports) exposed_ports = utils.build_exposed_ports(exposed_ports)
container_dict['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 container_dict['status'] = consts.CREATING
extra_spec = {} extra_spec = {}
extra_spec['hints'] = container_dict.get('hints', None) extra_spec['hints'] = container_dict.get('hints', None)
@ -435,7 +441,7 @@ class ContainersController(base.Controller):
new_container.uuid) new_container.uuid)
pecan.response.status = 202 pecan.response.status = 202
return view.format_container(context, pecan.request.host_url, return view.format_container(context, pecan.request.host_url,
new_container.as_dict()) new_container)
def _check_container_quotas(self, context, container_delta_dict, def _check_container_quotas(self, context, container_delta_dict,
update_container=False): update_container=False):
@ -706,7 +712,7 @@ class ContainersController(base.Controller):
compute_api = pecan.request.compute_api compute_api = pecan.request.compute_api
container = compute_api.container_update(context, container, patch) container = compute_api.container_update(context, container, patch)
return view.format_container(context, pecan.request.host_url, return view.format_container(context, pecan.request.host_url,
container.as_dict()) container)
@base.Controller.api_version("1.1", "1.13") @base.Controller.api_version("1.1", "1.13")
@pecan.expose('json') @pecan.expose('json')
@ -727,7 +733,7 @@ class ContainersController(base.Controller):
context = pecan.request.context context = pecan.request.context
container.save(context) container.save(context)
return view.format_container(context, pecan.request.host_url, return view.format_container(context, pecan.request.host_url,
container.as_dict()) container)
@base.Controller.api_version("1.19") @base.Controller.api_version("1.19")
@pecan.expose('json') @pecan.expose('json')
@ -752,7 +758,7 @@ class ContainersController(base.Controller):
compute_api.resize_container(context, container, kwargs) compute_api.resize_container(context, container, kwargs)
pecan.response.status = 202 pecan.response.status = 202
return view.format_container(context, pecan.request.host_url, return view.format_container(context, pecan.request.host_url,
container.as_dict()) container)
@pecan.expose('json') @pecan.expose('json')
@exception.wrap_pecan_controller_exception @exception.wrap_pecan_controller_exception

View File

@ -39,14 +39,6 @@ RESOURCE_NAME = 'registry'
COLLECTION_NAME = 'registries' 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): def check_policy_on_registry(registry, action):
context = pecan.request.context context = pecan.request.context
policy.enforce(context, action, registry, action=action) policy.enforce(context, action, registry, action=action)
@ -160,7 +152,7 @@ class RegistryController(base.Controller):
context = pecan.request.context context = pecan.request.context
if context.is_admin: if context.is_admin:
context.all_projects = True context.all_projects = True
registry = _get_registry(registry_ident) registry = utils.get_registry(registry_ident)
policy_action = policies.REGISTRY % 'get_one' policy_action = policies.REGISTRY % 'get_one'
check_policy_on_registry(registry.as_dict(), policy_action) check_policy_on_registry(registry.as_dict(), policy_action)
return RegistryItem.render_response(registry) return RegistryItem.render_response(registry)
@ -181,7 +173,7 @@ class RegistryController(base.Controller):
# Set the HTTP Location Header # Set the HTTP Location Header
pecan.response.location = link.build_url(COLLECTION_NAME, pecan.response.location = link.build_url(COLLECTION_NAME,
new_registry.uuid) new_registry.uuid)
pecan.response.status = 202 pecan.response.status = 201
return RegistryItem.render_response(new_registry) return RegistryItem.render_response(new_registry)
@pecan.expose('json') @pecan.expose('json')
@ -193,7 +185,7 @@ class RegistryController(base.Controller):
:param registry_ident: UUID or name of a registry. :param registry_ident: UUID or name of a registry.
:param registry_dict: a json document to apply to this 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 context = pecan.request.context
policy_action = policies.REGISTRY % 'update' policy_action = policies.REGISTRY % 'update'
check_policy_on_registry(registry.as_dict(), policy_action) check_policy_on_registry(registry.as_dict(), policy_action)
@ -220,7 +212,7 @@ class RegistryController(base.Controller):
context = pecan.request.context context = pecan.request.context
if context.is_admin: if context.is_admin:
context.all_projects = True context.all_projects = True
registry = _get_registry(registry_ident) registry = utils.get_registry(registry_ident)
policy_action = policies.REGISTRY % 'delete' policy_action = policies.REGISTRY % 'delete'
check_policy_on_registry(registry.as_dict(), policy_action) check_policy_on_registry(registry.as_dict(), policy_action)
registry.destroy(context) registry.destroy(context)

View File

@ -41,6 +41,7 @@ _legacy_container_properties = {
'privileged': parameter_types.boolean, 'privileged': parameter_types.boolean,
'healthcheck': parameter_types.healthcheck, 'healthcheck': parameter_types.healthcheck,
'exposed_ports': parameter_types.exposed_ports, 'exposed_ports': parameter_types.exposed_ports,
'registry': parameter_types.container_registry,
} }
legacy_container_create = { legacy_container_create = {

View File

@ -572,3 +572,10 @@ registry_password = {
'type': ['string'], 'type': ['string'],
'minLength': 1, 'minLength': 1,
} }
container_registry = {
'type': ['string'],
'minLength': 2,
'maxLength': 255,
'pattern': '^[a-zA-Z0-9][a-zA-Z0-9_.-]+$'
}

View File

@ -55,7 +55,7 @@ def format_capsule(url, capsule, context):
bookmark=True)]) bookmark=True)])
elif key == 'containers': elif key == 'containers':
containers = [] containers = []
for c in value: for c in capsule.containers:
container = containers_view.format_container( container = containers_view.format_container(
context, None, c) context, None, c)
containers.append(container) containers.append(container)

View File

@ -49,6 +49,7 @@ _basic_keys = (
'privileged', 'privileged',
'healthcheck', 'healthcheck',
'cpu_policy', 'cpu_policy',
'registry_id',
) )
@ -69,8 +70,14 @@ def format_container(context, url, container):
'bookmark', url, 'bookmark', url,
'containers', value, 'containers', value,
bookmark=True)]) 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: else:
yield (key, value) yield (key, value)
return dict(itertools.chain.from_iterable( 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()))

View File

@ -63,10 +63,11 @@ REST_API_VERSION_HISTORY = """REST API Version History:
* 1.28 - Add support cpuset * 1.28 - Add support cpuset
* 1.29 - Add enable_cpu_pinning to compute_node * 1.29 - Add enable_cpu_pinning to compute_node
* 1.30 - Introduce API resource for representing private registry * 1.30 - Introduce API resource for representing private registry
* 1.31 - Add 'registry_id' to containers
""" """
BASE_VER = '1.1' BASE_VER = '1.1'
CURRENT_MAX_VER = '1.30' CURRENT_MAX_VER = '1.31'
class Version(object): class Version(object):

View File

@ -54,7 +54,7 @@ user documentation.
1.5 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. Users can use this attribute to choose runtime for their containers.
The specified runtime should be configured by admin to run with Zun. The specified runtime should be configured by admin to run with Zun.
The default runtime for Zun is runc. The default runtime for Zun is runc.
@ -235,3 +235,9 @@ user documentation.
---- ----
Introduce API endpoint for create/read/update/delete private registry. 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.

View File

@ -488,6 +488,14 @@ def get_image(image_id):
return image 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): def check_for_restart_policy(container_dict):
"""Check for restart policy input """Check for restart policy input

View File

@ -1179,6 +1179,18 @@ def create_registry(context, values):
return _get_dbdriver_instance().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") @profiler.trace("db")
def get_registry_by_uuid(context, registry_uuid): def get_registry_by_uuid(context, registry_uuid):
"""Return a registry. """Return a registry.

View File

@ -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'])

View File

@ -1426,6 +1426,17 @@ class Connection(object):
ref.update(values) ref.update(values)
return ref 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): def get_registry_by_uuid(self, context, registry_uuid):
query = model_query(models.Registry) query = model_query(models.Registry)
query = self._add_project_filters(context, query) query = self._add_project_filters(context, query)
@ -1457,6 +1468,11 @@ class Connection(object):
with session.begin(): with session.begin():
query = model_query(models.Registry, session=session) query = model_query(models.Registry, session=session)
query = add_identity_filter(query, registry_uuid) query = add_identity_filter(query, registry_uuid)
count = query.delete() try:
if count != 1: count = query.delete()
raise exception.RegistryNotFound(registry=registry_uuid) 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)

View File

@ -175,6 +175,9 @@ class Container(Base):
privileged = Column(Boolean, default=False) privileged = Column(Boolean, default=False)
healthcheck = Column(JSONEncodedDict) healthcheck = Column(JSONEncodedDict)
exposed_ports = Column(JSONEncodedDict) exposed_ports = Column(JSONEncodedDict)
registry_id = Column(Integer,
ForeignKey('registry.id'),
nullable=True)
class VolumeMapping(Base): class VolumeMapping(Base):

View File

@ -20,12 +20,13 @@ from zun.objects import base
from zun.objects import exec_instance as exec_inst from zun.objects import exec_instance as exec_inst
from zun.objects import fields as z_fields from zun.objects import fields as z_fields
from zun.objects import pci_device from zun.objects import pci_device
from zun.objects import registry
LOG = logging.getLogger(__name__) LOG = logging.getLogger(__name__)
CONTAINER_OPTIONAL_ATTRS = ["pci_devices", "exec_instances"] CONTAINER_OPTIONAL_ATTRS = ["pci_devices", "exec_instances", "registry"]
@base.ZunObjectRegistry.register @base.ZunObjectRegistry.register
@ -96,7 +97,8 @@ class Container(base.ZunPersistentObject, base.ZunObject):
# Version 1.36: Add 'get_count' method # Version 1.36: Add 'get_count' method
# Version 1.37: Add 'exposed_ports' attribute # Version 1.37: Add 'exposed_ports' attribute
# Version 1.38: Add 'cpuset' attribute # Version 1.38: Add 'cpuset' attribute
VERSION = '1.37' # Version 1.39: Add 'register' and 'registry_id' attributes
VERSION = '1.39'
fields = { fields = {
'id': fields.IntegerField(), 'id': fields.IntegerField(),
@ -143,13 +145,15 @@ class Container(base.ZunPersistentObject, base.ZunObject):
nullable=True), nullable=True),
'privileged': fields.BooleanField(nullable=True), 'privileged': fields.BooleanField(nullable=True),
'healthcheck': z_fields.JsonField(nullable=True), 'healthcheck': z_fields.JsonField(nullable=True),
'registry_id': fields.IntegerField(nullable=True),
'registry': fields.ObjectField("Registry", nullable=True),
} }
@staticmethod @staticmethod
def _from_db_object(container, db_container): def _from_db_object(container, db_container):
"""Converts a database entity to a formal object.""" """Converts a database entity to a formal object."""
for field in container.fields: for field in container.fields:
if field in ['pci_devices', 'exec_instances']: if field in ['pci_devices', 'exec_instances', 'registry']:
continue continue
if field == 'cpuset': if field == 'cpuset':
container.cpuset = Cpuset._from_dict( container.cpuset = Cpuset._from_dict(
@ -349,6 +353,9 @@ class Container(base.ZunPersistentObject, base.ZunObject):
if attrname == 'exec_instances': if attrname == 'exec_instances':
self._load_exec_instances() self._load_exec_instances()
if attrname == 'registry':
self._load_registry()
self.obj_reset_changes([attrname]) self.obj_reset_changes([attrname])
def _load_pci_devices(self): 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.exec_instances = exec_inst.ExecInstance.list_by_container_id(
self._context, self.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 @base.remotable_classmethod
def get_count(cls, context, project_id, flag): def get_count(cls, context, project_id, flag):
"""Get the counts of Container objects in the database. """Get the counts of Container objects in the database.

View File

@ -51,6 +51,18 @@ class Registry(base.ZunPersistentObject, base.ZunObject):
return [Registry._from_db_object(cls(context), obj) return [Registry._from_db_object(cls(context), obj)
for obj in db_objects] 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 @base.remotable_classmethod
def get_by_uuid(cls, context, uuid): def get_by_uuid(cls, context, uuid):
"""Find a registry based on uuid and return a :class:`Registry` object. """Find a registry based on uuid and return a :class:`Registry` object.

View File

@ -26,7 +26,7 @@ from zun.tests.unit.db import base
PATH_PREFIX = '/v1' PATH_PREFIX = '/v1'
CURRENT_VERSION = "container 1.30" CURRENT_VERSION = "container 1.31"
class FunctionalTest(base.DbTestCase): class FunctionalTest(base.DbTestCase):

View File

@ -28,7 +28,7 @@ class TestRootController(api_base.FunctionalTest):
'default_version': 'default_version':
{'id': 'v1', {'id': 'v1',
'links': [{'href': 'http://localhost/v1/', 'rel': 'self'}], 'links': [{'href': 'http://localhost/v1/', 'rel': 'self'}],
'max_version': '1.30', 'max_version': '1.31',
'min_version': '1.1', 'min_version': '1.1',
'status': 'CURRENT'}, 'status': 'CURRENT'},
'description': 'Zun is an OpenStack project which ' 'description': 'Zun is an OpenStack project which '
@ -37,7 +37,7 @@ class TestRootController(api_base.FunctionalTest):
'versions': [{'id': 'v1', 'versions': [{'id': 'v1',
'links': [{'href': 'http://localhost/v1/', 'links': [{'href': 'http://localhost/v1/',
'rel': 'self'}], 'rel': 'self'}],
'max_version': '1.30', 'max_version': '1.31',
'min_version': '1.1', 'min_version': '1.1',
'status': 'CURRENT'}]} 'status': 'CURRENT'}]}

View File

@ -31,7 +31,7 @@ class TestRegistryController(api_base.FunctionalTest):
params=params, params=params,
content_type='application/json') content_type='application/json')
self.assertEqual(202, response.status_int) self.assertEqual(201, response.status_int)
self.assertEqual(1, len(response.json)) self.assertEqual(1, len(response.json))
r = response.json['registry'] r = response.json['registry']
self.assertIsNotNone(r.get('uuid')) self.assertIsNotNone(r.get('uuid'))
@ -57,7 +57,7 @@ class TestRegistryController(api_base.FunctionalTest):
params=params, params=params,
content_type='application/json') content_type='application/json')
self.assertEqual(202, response.status_int) self.assertEqual(201, response.status_int)
self.assertEqual(1, len(response.json)) self.assertEqual(1, len(response.json))
r = response.json['registry'] r = response.json['registry']
self.assertIsNotNone(r.get('uuid')) self.assertIsNotNone(r.get('uuid'))
@ -82,7 +82,7 @@ class TestRegistryController(api_base.FunctionalTest):
response = self.post('/v1/registries/', response = self.post('/v1/registries/',
params=params, params=params,
content_type='application/json') content_type='application/json')
self.assertEqual(202, response.status_int) self.assertEqual(201, response.status_int)
response = self.get('/v1/registries/') response = self.get('/v1/registries/')
self.assertEqual(200, response.status_int) self.assertEqual(200, response.status_int)
self.assertEqual(2, len(response.json)) self.assertEqual(2, len(response.json))
@ -175,7 +175,7 @@ class TestRegistryController(api_base.FunctionalTest):
response = self.post('/v1/registries/', response = self.post('/v1/registries/',
params=params, params=params,
content_type='application/json') content_type='application/json')
self.assertEqual(202, response.status_int) self.assertEqual(201, response.status_int)
# get by uuid # get by uuid
registry_uuid = response.json['registry']['uuid'] registry_uuid = response.json['registry']['uuid']
response = self.get('/v1/registries/%s/' % registry_uuid) response = self.get('/v1/registries/%s/' % registry_uuid)
@ -218,7 +218,7 @@ class TestRegistryController(api_base.FunctionalTest):
response = self.post('/v1/registries/', response = self.post('/v1/registries/',
params=params, params=params,
content_type='application/json') content_type='application/json')
self.assertEqual(202, response.status_int) self.assertEqual(201, response.status_int)
registry_uuid = response.json['registry']['uuid'] registry_uuid = response.json['registry']['uuid']
params = {'registry': {'name': 'new-name', 'domain': 'new-domain', params = {'registry': {'name': 'new-name', 'domain': 'new-domain',
'username': 'new-username', 'password': 'new-pass'}} 'username': 'new-username', 'password': 'new-pass'}}
@ -242,7 +242,7 @@ class TestRegistryController(api_base.FunctionalTest):
response = self.post('/v1/registries/', response = self.post('/v1/registries/',
params=params, params=params,
content_type='application/json') content_type='application/json')
self.assertEqual(202, response.status_int) self.assertEqual(201, response.status_int)
registry_uuid = response.json['registry']['uuid'] registry_uuid = response.json['registry']['uuid']
response = self.delete('/v1/registries/%s/' % registry_uuid) response = self.delete('/v1/registries/%s/' % registry_uuid)
self.assertEqual(204, response.status_int) self.assertEqual(204, response.status_int)

View File

@ -122,6 +122,7 @@ def get_test_container(**kwargs):
'exposed_ports': kwargs.get('exposed_ports', {"80/tcp": {}}), 'exposed_ports': kwargs.get('exposed_ports', {"80/tcp": {}}),
'cpu_policy': kwargs.get('cpu_policy', None), 'cpu_policy': kwargs.get('cpu_policy', None),
'cpuset': kwargs.get('cpuset', None), 'cpuset': kwargs.get('cpuset', None),
'registry_id': kwargs.get('registry_id', None),
} }

View File

@ -344,7 +344,7 @@ class TestObject(test_base.TestCase, _TestObject):
# For more information on object version testing, read # For more information on object version testing, read
# https://docs.openstack.org/zun/latest/ # https://docs.openstack.org/zun/latest/
object_data = { object_data = {
'Container': '1.37-193d8cd6635760882a27142760931af9', 'Container': '1.39-6a7bc5bcd85277c30982c1106f10c336',
'Cpuset': '1.0-06c4e6335683c18b87e2e54080f8c341', 'Cpuset': '1.0-06c4e6335683c18b87e2e54080f8c341',
'Volume': '1.0-4ec18c39ea49f898cc354f9ca178dfb7', 'Volume': '1.0-4ec18c39ea49f898cc354f9ca178dfb7',
'VolumeMapping': '1.5-57febc66526185a75a744637e7a387c7', 'VolumeMapping': '1.5-57febc66526185a75a744637e7a387c7',
@ -368,7 +368,7 @@ object_data = {
'ContainerActionEvent': '1.0-2974d0a6f5d4821fd4e223a88c10181a', 'ContainerActionEvent': '1.0-2974d0a6f5d4821fd4e223a88c10181a',
'Network': '1.1-26e8d37a54e5fc905ede657744a221d9', 'Network': '1.1-26e8d37a54e5fc905ede657744a221d9',
'ExecInstance': '1.0-59464e7b96db847c0abb1e96d3cec30a', 'ExecInstance': '1.0-59464e7b96db847c0abb1e96d3cec30a',
'Registry': '1.0-21ed56234497120755c60deba7c9e1dc', 'Registry': '1.0-36c2053fbc30e0021630e657dd1699c9',
} }