Add image endpoint
This patch adds image endpoint for zun-api. As of now supports image_show() and image_create(). Change-Id: I2ef1865e21b99f3bed3a5b7c53816cfe808a2fc2 Partial-Implements: blueprint add-image-endpoint
This commit is contained in:
parent
605818302b
commit
ea963601a5
@ -90,6 +90,26 @@ class NameType(String):
|
||||
raise exception.InvalidValue(message)
|
||||
|
||||
|
||||
class ImageNameType(String):
|
||||
type_name = 'ImageNameType'
|
||||
# ImageNameType allows to be Non-None or a string matches pattern
|
||||
# `[a-zA-Z0-9][a-zA-Z0-9_.-].` with minimum length is 2 and maximum length
|
||||
# 255 string type.
|
||||
|
||||
@classmethod
|
||||
def validate(cls, value):
|
||||
if value is None:
|
||||
message = _('Repo/Image is mandatory. Cannot be left blank.')
|
||||
raise exception.InvalidValue(message)
|
||||
super(ImageNameType, cls).validate(value, min_length=2, max_length=255)
|
||||
match = name_pattern.match(value)
|
||||
if match:
|
||||
return value
|
||||
else:
|
||||
message = _('%s does not match [a-zA-Z0-9][a-zA-Z0-9_.-].') % value
|
||||
raise exception.InvalidValue(message)
|
||||
|
||||
|
||||
class Integer(object):
|
||||
type_name = 'Integer'
|
||||
|
||||
@ -251,8 +271,8 @@ class DateTime(object):
|
||||
type=cls.type_name)
|
||||
|
||||
|
||||
class ContainerMemory(object):
|
||||
type_name = 'ContainerMemory'
|
||||
class MemoryType(object):
|
||||
type_name = 'MemoryType'
|
||||
|
||||
@classmethod
|
||||
def validate(cls, value, default=None):
|
||||
@ -271,3 +291,25 @@ class ContainerMemory(object):
|
||||
in both cases"""
|
||||
raise exception.InvalidValue(message=message, value=value,
|
||||
type=cls.type_name)
|
||||
|
||||
|
||||
class ImageSize(object):
|
||||
type_name = 'ImageSize'
|
||||
|
||||
@classmethod
|
||||
def validate(cls, value, default=None):
|
||||
if value is None:
|
||||
return
|
||||
elif value.isdigit():
|
||||
return value
|
||||
elif (value.isalnum() and
|
||||
value[:-1].isdigit() and value[-1] in VALID_UNITS.keys()):
|
||||
return int(value[:-1]) * VALID_UNITS[value[-1]]
|
||||
else:
|
||||
LOG.exception(_LE('Failed to validate image size'))
|
||||
message = _("""
|
||||
size must be either integer or string of below format.
|
||||
<integer><memory_unit> memory_unit must be 'k','b','m','g'
|
||||
in both cases""")
|
||||
raise exception.InvalidValue(message=message, value=value,
|
||||
type=cls.type_name)
|
||||
|
@ -26,6 +26,7 @@ from zun.api.controllers import base as controllers_base
|
||||
from zun.api.controllers import link
|
||||
from zun.api.controllers import types
|
||||
from zun.api.controllers.v1 import containers as container_controller
|
||||
from zun.api.controllers.v1 import images as image_controller
|
||||
from zun.api.controllers.v1 import zun_services
|
||||
|
||||
LOG = logging.getLogger(__name__)
|
||||
@ -63,6 +64,9 @@ class V1(controllers_base.APIBase):
|
||||
'containers': {
|
||||
'validate': types.List(types.Custom(link.Link)).validate
|
||||
},
|
||||
'images': {
|
||||
'validate': types.List(types.Custom(link.Link)).validate
|
||||
},
|
||||
}
|
||||
|
||||
@staticmethod
|
||||
@ -90,6 +94,12 @@ class V1(controllers_base.APIBase):
|
||||
pecan.request.host_url,
|
||||
'containers', '',
|
||||
bookmark=True)]
|
||||
v1.images = [link.Link.make_link('self', pecan.request.host_url,
|
||||
'images', ''),
|
||||
link.Link.make_link('bookmark',
|
||||
pecan.request.host_url,
|
||||
'images', '',
|
||||
bookmark=True)]
|
||||
return v1
|
||||
|
||||
|
||||
@ -98,6 +108,7 @@ class Controller(rest.RestController):
|
||||
|
||||
services = zun_services.ZunServiceController()
|
||||
containers = container_controller.ContainersController()
|
||||
images = image_controller.ImagesController()
|
||||
|
||||
@pecan.expose('json')
|
||||
def get(self):
|
||||
|
@ -94,7 +94,7 @@ class Container(base.APIBase):
|
||||
'validate': types.Float.validate,
|
||||
},
|
||||
'memory': {
|
||||
'validate': types.ContainerMemory.validate,
|
||||
'validate': types.MemoryType.validate,
|
||||
},
|
||||
'environment': {
|
||||
'validate': types.Dict(types.String, types.String).validate,
|
||||
|
188
zun/api/controllers/v1/images.py
Normal file
188
zun/api/controllers/v1/images.py
Normal file
@ -0,0 +1,188 @@
|
||||
# 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.
|
||||
|
||||
from oslo_log import log as logging
|
||||
from oslo_utils import timeutils
|
||||
import pecan
|
||||
from pecan import rest
|
||||
|
||||
from zun.api.controllers import base
|
||||
from zun.api.controllers import link
|
||||
from zun.api.controllers import types
|
||||
from zun.api.controllers.v1 import collection
|
||||
from zun.api import utils as api_utils
|
||||
from zun.common import exception
|
||||
from zun.common.i18n import _LE
|
||||
from zun.common import policy
|
||||
from zun import objects
|
||||
|
||||
LOG = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class Image(base.APIBase):
|
||||
"""API representation of an image.
|
||||
|
||||
This class enforces type checking and value constraints, and converts
|
||||
between the internal object model and the API representation of
|
||||
an image.
|
||||
"""
|
||||
|
||||
fields = {
|
||||
'uuid': {
|
||||
'validate': types.Uuid.validate,
|
||||
},
|
||||
'image_id': {
|
||||
'validate': types.NameType.validate,
|
||||
},
|
||||
'repo': {
|
||||
'validate': types.ImageNameType.validate,
|
||||
},
|
||||
'tag': {
|
||||
'validate': types.NameType.validate,
|
||||
},
|
||||
'size': {
|
||||
'validate': types.ImageSize.validate,
|
||||
},
|
||||
}
|
||||
|
||||
def __init__(self, **kwargs):
|
||||
super(Image, self).__init__(**kwargs)
|
||||
|
||||
@staticmethod
|
||||
def _convert_with_links(image, url, expand=True):
|
||||
if not expand:
|
||||
image.unset_fields_except([
|
||||
'uuid', 'image_id', 'repo', 'tag', 'size'])
|
||||
|
||||
image.links = [link.Link.make_link(
|
||||
'self', url,
|
||||
'images', image.uuid),
|
||||
link.Link.make_link(
|
||||
'bookmark', url,
|
||||
'images', image.uuid,
|
||||
bookmark=True)]
|
||||
return image
|
||||
|
||||
@classmethod
|
||||
def convert_with_links(cls, rpc_image, expand=True):
|
||||
image = Image(**rpc_image)
|
||||
return cls._convert_with_links(image, pecan.request.host_url,
|
||||
expand)
|
||||
|
||||
@classmethod
|
||||
def sample(cls, expand=True):
|
||||
sample = cls(uuid='27e3153e-d5bf-4b7e-b517-fb518e17f35c',
|
||||
repo='ubuntu',
|
||||
tag='latest',
|
||||
size='700m',
|
||||
created_at=timeutils.utcnow(),
|
||||
updated_at=timeutils.utcnow())
|
||||
return cls._convert_with_links(sample, 'http://localhost:9517', expand)
|
||||
|
||||
|
||||
class ImageCollection(collection.Collection):
|
||||
"""API representation of a collection of images."""
|
||||
|
||||
fields = {
|
||||
'images': {
|
||||
'validate': types.List(types.Custom(Image)).validate,
|
||||
},
|
||||
}
|
||||
|
||||
"""A list containing images objects"""
|
||||
|
||||
def __init__(self, **kwargs):
|
||||
self._type = 'images'
|
||||
|
||||
@staticmethod
|
||||
def convert_with_links(rpc_images, limit, url=None,
|
||||
expand=False, **kwargs):
|
||||
collection = ImageCollection()
|
||||
collection.images = [Image.convert_with_links(p, expand)
|
||||
for p in rpc_images]
|
||||
collection.next = collection.get_next(limit, url=url, **kwargs)
|
||||
return collection
|
||||
|
||||
@classmethod
|
||||
def sample(cls):
|
||||
sample = cls()
|
||||
sample.images = [Image.sample(expand=False)]
|
||||
return sample
|
||||
|
||||
|
||||
class ImagesController(rest.RestController):
|
||||
'''Controller for Images'''
|
||||
|
||||
@pecan.expose('json')
|
||||
@exception.wrap_pecan_controller_exception
|
||||
def get_all(self, **kwargs):
|
||||
'''Retrieve a list of images.'''
|
||||
context = pecan.request.context
|
||||
policy.enforce(context, "image:get_all",
|
||||
action="image:get_all")
|
||||
return self._get_images_collection(**kwargs)
|
||||
|
||||
def _get_images_collection(self, **kwargs):
|
||||
context = pecan.request.context
|
||||
limit = api_utils.validate_limit(kwargs.get('limit', None))
|
||||
sort_dir = api_utils.validate_sort_dir(kwargs.get('sort_dir', 'asc'))
|
||||
sort_key = kwargs.get('sort_key', 'id')
|
||||
resource_url = kwargs.get('resource_url', None)
|
||||
expand = kwargs.get('expand', None)
|
||||
filters = None
|
||||
marker_obj = None
|
||||
marker = kwargs.get('marker', None)
|
||||
if marker:
|
||||
marker_obj = objects.Image.get_by_uuid(context, marker)
|
||||
images = objects.Image.list(context,
|
||||
limit,
|
||||
marker_obj,
|
||||
sort_key,
|
||||
sort_dir,
|
||||
filters=filters)
|
||||
for i, c in enumerate(images):
|
||||
try:
|
||||
images[i] = pecan.request.rpcapi.image_show(context, c)
|
||||
except Exception as e:
|
||||
LOG.exception(_LE("Error while list image %(uuid)s: "
|
||||
"%(e)s."), {'uuid': c.uuid, 'e': e})
|
||||
return ImageCollection.convert_with_links(images, limit,
|
||||
url=resource_url,
|
||||
expand=expand,
|
||||
sort_key=sort_key,
|
||||
sort_dir=sort_dir)
|
||||
|
||||
@pecan.expose('json')
|
||||
@api_utils.enforce_content_types(['application/json'])
|
||||
@exception.wrap_pecan_controller_exception
|
||||
def post(self, **image_dict):
|
||||
"""Create a new image.
|
||||
|
||||
:param image: an image within the request body.
|
||||
"""
|
||||
context = pecan.request.context
|
||||
policy.enforce(context, "image:create",
|
||||
action="image:create")
|
||||
image_dict = Image(**image_dict).as_dict()
|
||||
image_dict['project_id'] = context.project_id
|
||||
image_dict['user_id'] = context.user_id
|
||||
repo_tag = image_dict.get('repo')
|
||||
image_dict['repo'], image_dict['tag'] = api_utils.parse_image_tag(
|
||||
repo_tag)
|
||||
new_image = objects.Image(context, **image_dict)
|
||||
new_image.create()
|
||||
pecan.request.rpcapi.image_create(context, new_image)
|
||||
# Set the HTTP Location Header
|
||||
pecan.response.location = link.build_url('images',
|
||||
new_image.uuid)
|
||||
pecan.response.status = 202
|
||||
return Image.convert_with_links(new_image)
|
@ -68,6 +68,18 @@ def validate_docker_memory(mem_str):
|
||||
% DOCKER_MINIMUM_MEMORY)
|
||||
|
||||
|
||||
def parse_image_tag(image):
|
||||
image_parts = image.split(':', 1)
|
||||
|
||||
image_repo = image_parts[0]
|
||||
image_tag = 'latest'
|
||||
|
||||
if len(image_parts) > 1:
|
||||
image_tag = image_parts[1]
|
||||
|
||||
return image_repo, image_tag
|
||||
|
||||
|
||||
def apply_jsonpatch(doc, patch):
|
||||
for p in patch:
|
||||
if p['op'] == 'add' and p['path'].count('/') == 1:
|
||||
|
@ -337,10 +337,18 @@ class ContainerNotFound(HTTPNotFound):
|
||||
message = _("Container %(container)s could not be found.")
|
||||
|
||||
|
||||
class ImageNotFound(HTTPNotFound):
|
||||
message = _("Image %(image)s could not be found.")
|
||||
|
||||
|
||||
class ContainerAlreadyExists(ResourceExists):
|
||||
message = _("A container with UUID %(uuid)s already exists.")
|
||||
|
||||
|
||||
class ImageAlreadyExists(ResourceExists):
|
||||
message = _("An image with this tag and repo already exists.")
|
||||
|
||||
|
||||
class InvalidStateException(ZunException):
|
||||
message = _("Cannot %(action)s container %(id)s in %(actual_state)s state")
|
||||
code = 409
|
||||
|
@ -23,6 +23,7 @@ class API(rpc_service.API):
|
||||
API version history:
|
||||
|
||||
* 1.0 - Initial version.
|
||||
* 1.1 - Add image endpoints.
|
||||
'''
|
||||
|
||||
def __init__(self, transport=None, context=None, topic=None):
|
||||
@ -67,3 +68,9 @@ class API(rpc_service.API):
|
||||
def container_kill(self, context, container, signal):
|
||||
return self._call('container_kill', container=container,
|
||||
signal=signal)
|
||||
|
||||
def image_show(self, context, image):
|
||||
return self._call('image_show', image=image)
|
||||
|
||||
def image_create(self, context, image):
|
||||
return self._cast('image_create', image=image)
|
||||
|
@ -267,6 +267,35 @@ class Manager(object):
|
||||
LOG.error(_LE("Error occured while calling docker API: %s"),
|
||||
six.text_type(e))
|
||||
raise
|
||||
|
||||
def image_create(self, context, image):
|
||||
utils.spawn_n(self._do_image_create, context, image)
|
||||
|
||||
def _do_image_create(self, context, image):
|
||||
LOG.debug('Creating image...', context=context,
|
||||
image=image)
|
||||
try:
|
||||
repo_tag = image.repo + ":" + image.tag
|
||||
self.driver.pull_image(repo_tag)
|
||||
image_dict = self.driver.inspect_image(repo_tag)
|
||||
image.image_id = image_dict['Id']
|
||||
image.size = image_dict['Size']
|
||||
image.save()
|
||||
except exception.DockerError as e:
|
||||
LOG.error(_LE("Error occured while calling docker API: %s"),
|
||||
six.text_type(e))
|
||||
raise e
|
||||
except Exception as e:
|
||||
LOG.exception(_LE("Unexpected exception: %s"),
|
||||
six.text_type(e))
|
||||
raise e
|
||||
|
||||
@translate_exception
|
||||
def image_show(self, context, image):
|
||||
LOG.debug('Listing image...', context=context)
|
||||
try:
|
||||
self.image.list()
|
||||
return image
|
||||
except Exception as e:
|
||||
LOG.exception(_LE("Unexpected exception: %s"), str(e))
|
||||
raise e
|
||||
|
@ -39,6 +39,17 @@ class DockerDriver(driver.ContainerDriver):
|
||||
image_repo, image_tag = docker_utils.parse_docker_image(image)
|
||||
docker.pull(image_repo, tag=image_tag)
|
||||
|
||||
def inspect_image(self, image):
|
||||
with docker_utils.docker_client() as docker:
|
||||
LOG.debug('Inspecting image %s' % image)
|
||||
image_dict = docker.inspect_image(image)
|
||||
return image_dict
|
||||
|
||||
def images(self, repo, quiet=False):
|
||||
with docker_utils.docker_client() as docker:
|
||||
response = docker.images(repo, quiet)
|
||||
return response
|
||||
|
||||
def create(self, container):
|
||||
with docker_utils.docker_client() as docker:
|
||||
name = container.name
|
||||
|
@ -197,5 +197,79 @@ class Connection(object):
|
||||
:returns: A list of tuples of the specified columns.
|
||||
"""
|
||||
dbdriver = get_instance()
|
||||
return dbdriver.get_zun_service_list(
|
||||
context, disabled, limit, marker, sort_key, sort_dir)
|
||||
return dbdriver.get_zun_service_list(context, disabled, limit,
|
||||
marker, sort_key, sort_dir)
|
||||
|
||||
@classmethod
|
||||
def create_image(cls, values):
|
||||
"""Create a new image.
|
||||
|
||||
:param values: A dict containing several items used to identify
|
||||
and track the image, and several dicts which are
|
||||
passed
|
||||
into the Drivers when managing this image. For
|
||||
example:
|
||||
::
|
||||
{
|
||||
'uuid': uuidutils.generate_uuid(),
|
||||
'repo': 'hello-world',
|
||||
'tag': 'latest'
|
||||
}
|
||||
:returns: An image.
|
||||
"""
|
||||
dbdriver = get_instance()
|
||||
return dbdriver.create_image(values)
|
||||
|
||||
@classmethod
|
||||
def update_image(self, image_id, values):
|
||||
"""Update properties of an image.
|
||||
|
||||
:param container_id: The id or uuid of an image.
|
||||
:returns: An Image.
|
||||
:raises: ImageNotFound
|
||||
"""
|
||||
dbdriver = get_instance()
|
||||
return dbdriver.update_image(image_id, values)
|
||||
|
||||
@classmethod
|
||||
def list_image(cls, context, filters=None,
|
||||
limit=None, marker=None,
|
||||
sort_key=None, sort_dir=None):
|
||||
"""Get matching images.
|
||||
|
||||
Return a list of the specified columns for all images that
|
||||
match the specified filters.
|
||||
:param context: The security context
|
||||
:param filters: Filters to apply. Defaults to None.
|
||||
:param limit: Maximum number of images to return.
|
||||
:param marker: the last item of the previous page; we
|
||||
return the next
|
||||
:param sort_key: Attribute by which results should be sorted.
|
||||
(asc, desc)
|
||||
:returns: A list of tuples of the specified columns.
|
||||
"""
|
||||
dbdriver = get_instance()
|
||||
return dbdriver.list_image(context, filters, limit, marker,
|
||||
sort_key, sort_dir)
|
||||
|
||||
@classmethod
|
||||
def get_image_by_id(cls, context, image_id):
|
||||
"""Return an image.
|
||||
|
||||
:param context: The security context
|
||||
:param image_id: The id of an image.
|
||||
:returns: An image.
|
||||
"""
|
||||
dbdriver = get_instance()
|
||||
return dbdriver.get_image_by_id(context, image_id)
|
||||
|
||||
@classmethod
|
||||
def get_image_by_uuid(cls, context, image_uuid):
|
||||
"""Return an image.
|
||||
|
||||
:param context: The security context
|
||||
:param image_uuid: The uuid of an image.
|
||||
:returns: An image.
|
||||
"""
|
||||
dbdriver = get_instance()
|
||||
return dbdriver.get_image_by_uuid(context, image_uuid)
|
||||
|
@ -0,0 +1,50 @@
|
||||
# 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.
|
||||
|
||||
"""create_table_image
|
||||
|
||||
Revision ID: 72c6947c6636
|
||||
Revises: 1192ba19a6e9
|
||||
Create Date: 2016-09-23 20:30:02.425937
|
||||
|
||||
"""
|
||||
|
||||
# revision identifiers, used by Alembic.
|
||||
revision = '72c6947c6636'
|
||||
down_revision = '1192ba19a6e9'
|
||||
branch_labels = None
|
||||
depends_on = None
|
||||
|
||||
from alembic import op
|
||||
import sqlalchemy as sa
|
||||
|
||||
|
||||
def upgrade():
|
||||
# commands auto generated by Alembic - please adjust! #
|
||||
op.create_table(
|
||||
'image',
|
||||
sa.Column('created_at', sa.DateTime(), nullable=True),
|
||||
sa.Column('updated_at', sa.DateTime(), nullable=True),
|
||||
sa.Column('id', sa.Integer(), nullable=False),
|
||||
sa.Column('project_id', sa.String(length=255), nullable=True),
|
||||
sa.Column('user_id', sa.String(length=255), nullable=True),
|
||||
sa.Column('uuid', sa.String(length=36), nullable=True),
|
||||
sa.Column('image_id', sa.String(length=255), nullable=True),
|
||||
sa.Column('repo', sa.String(length=255), nullable=True),
|
||||
sa.Column('tag', sa.String(length=255), nullable=True),
|
||||
sa.Column('size', sa.String(length=255), nullable=True),
|
||||
sa.PrimaryKeyConstraint('id'),
|
||||
sa.UniqueConstraint('repo', 'tag', name='uniq_image0repotag'),
|
||||
mysql_charset='utf8',
|
||||
mysql_engine='InnoDB'
|
||||
)
|
||||
# end Alembic commands #
|
@ -31,7 +31,6 @@ from zun.db.sqlalchemy import models
|
||||
|
||||
CONF = cfg.CONF
|
||||
|
||||
|
||||
_FACADE = None
|
||||
|
||||
|
||||
@ -270,3 +269,72 @@ class Connection(api.Connection):
|
||||
|
||||
return _paginate_query(models.ZunService, limit, marker,
|
||||
sort_key, sort_dir, query)
|
||||
|
||||
def create_image(self, values):
|
||||
# ensure defaults are present for new containers
|
||||
if not values.get('uuid'):
|
||||
values['uuid'] = uuidutils.generate_uuid()
|
||||
image = models.Image()
|
||||
image.update(values)
|
||||
try:
|
||||
image.save()
|
||||
except db_exc.DBDuplicateEntry:
|
||||
raise exception.ImageAlreadyExists()
|
||||
return image
|
||||
|
||||
def update_image(self, image_id, values):
|
||||
# NOTE(dtantsur): this can lead to very strange errors
|
||||
if 'uuid' in values:
|
||||
msg = _("Cannot overwrite UUID for an existing Image.")
|
||||
raise exception.InvalidParameterValue(err=msg)
|
||||
return self._do_update_image(image_id, values)
|
||||
|
||||
def _do_update_image(self, image_id, values):
|
||||
session = get_session()
|
||||
with session.begin():
|
||||
query = model_query(models.Image, session=session)
|
||||
query = add_identity_filter(query, image_id)
|
||||
try:
|
||||
ref = query.with_lockmode('update').one()
|
||||
except NoResultFound:
|
||||
raise exception.ImageNotFound(image=image_id)
|
||||
|
||||
ref.update(values)
|
||||
return ref
|
||||
|
||||
def _add_image_filters(self, query, filters):
|
||||
if filters is None:
|
||||
filters = {}
|
||||
|
||||
filter_names = ['repo', 'project_id', 'user_id', 'size']
|
||||
for name in filter_names:
|
||||
if name in filters:
|
||||
query = query.filter_by(**{name: filters[name]})
|
||||
|
||||
return query
|
||||
|
||||
def list_image(self, context, filters=None, limit=None, marker=None,
|
||||
sort_key=None, sort_dir=None):
|
||||
query = model_query(models.Image)
|
||||
query = self._add_tenant_filters(context, query)
|
||||
query = self._add_image_filters(query, filters)
|
||||
return _paginate_query(models.Image, limit, marker, sort_key,
|
||||
sort_dir, query)
|
||||
|
||||
def get_image_by_id(self, context, image_id):
|
||||
query = model_query(models.Image)
|
||||
query = self._add_tenant_filters(context, query)
|
||||
query = query.filter_by(id=image_id)
|
||||
try:
|
||||
return query.one()
|
||||
except NoResultFound:
|
||||
raise exception.ImageNotFound(image=image_id)
|
||||
|
||||
def get_image_by_uuid(self, context, image_uuid):
|
||||
query = model_query(models.Image)
|
||||
query = self._add_tenant_filters(context, query)
|
||||
query = query.filter_by(uuid=image_uuid)
|
||||
try:
|
||||
return query.one()
|
||||
except NoResultFound:
|
||||
raise exception.ImageNotFound(image=image_uuid)
|
||||
|
@ -139,3 +139,21 @@ class Container(Base):
|
||||
ports = Column(JSONEncodedList)
|
||||
hostname = Column(String(255))
|
||||
labels = Column(JSONEncodedDict)
|
||||
|
||||
|
||||
class Image(Base):
|
||||
"""Represents an image. """
|
||||
|
||||
__tablename__ = 'image'
|
||||
__table_args__ = (
|
||||
schema.UniqueConstraint('repo', 'tag', name='uniq_image0repotag'),
|
||||
table_args()
|
||||
)
|
||||
id = Column(Integer, primary_key=True)
|
||||
project_id = Column(String(255))
|
||||
user_id = Column(String(255))
|
||||
uuid = Column(String(36))
|
||||
image_id = Column(String(255))
|
||||
repo = Column(String(255))
|
||||
tag = Column(String(255))
|
||||
size = Column(String(255))
|
||||
|
@ -12,11 +12,13 @@
|
||||
|
||||
|
||||
from zun.objects import container
|
||||
from zun.objects import image
|
||||
from zun.objects import zun_service
|
||||
|
||||
|
||||
Container = container.Container
|
||||
ZunService = zun_service.ZunService
|
||||
Image = image.Image
|
||||
|
||||
__all__ = (Container,
|
||||
ZunService)
|
||||
ZunService,
|
||||
Image)
|
||||
|
129
zun/objects/image.py
Normal file
129
zun/objects/image.py
Normal file
@ -0,0 +1,129 @@
|
||||
# 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.
|
||||
|
||||
from oslo_versionedobjects import fields
|
||||
|
||||
from zun.db import api as dbapi
|
||||
from zun.objects import base
|
||||
|
||||
|
||||
@base.ZunObjectRegistry.register
|
||||
class Image(base.ZunPersistentObject, base.ZunObject,
|
||||
base.ZunObjectDictCompat):
|
||||
# Version 1.0: Initial version
|
||||
VERSION = '1.0'
|
||||
|
||||
fields = {
|
||||
'id': fields.IntegerField(),
|
||||
'uuid': fields.StringField(nullable=True),
|
||||
'image_id': fields.StringField(nullable=True),
|
||||
'project_id': fields.StringField(nullable=True),
|
||||
'user_id': fields.StringField(nullable=True),
|
||||
'repo': fields.StringField(nullable=True),
|
||||
'tag': fields.StringField(nullable=True),
|
||||
'size': fields.StringField(nullable=True),
|
||||
}
|
||||
|
||||
@staticmethod
|
||||
def _from_db_object(image, db_image):
|
||||
"""Converts a database entity to a formal object."""
|
||||
for field in image.fields:
|
||||
image[field] = db_image[field]
|
||||
|
||||
image.obj_reset_changes()
|
||||
return image
|
||||
|
||||
@staticmethod
|
||||
def _from_db_object_list(db_objects, cls, context):
|
||||
"""Converts a list of database entities to a list of formal objects."""
|
||||
return [Image._from_db_object(cls(context), obj)
|
||||
for obj in db_objects]
|
||||
|
||||
@base.remotable_classmethod
|
||||
def get_by_id(cls, context, image_id):
|
||||
"""Find an image based on its integer id and return a Image object.
|
||||
|
||||
:param image_id: the id of an image.
|
||||
:param context: Security context
|
||||
:returns: a :class:`Image` object.
|
||||
"""
|
||||
db_image = dbapi.Connection.get_image_by_id(context, image_id)
|
||||
image = Image._from_db_object(cls(context), db_image)
|
||||
return image
|
||||
|
||||
@base.remotable_classmethod
|
||||
def get_by_uuid(cls, context, uuid):
|
||||
"""Find an image based on uuid and return a :class:`Image` object.
|
||||
|
||||
:param uuid: the uuid of an image.
|
||||
:param context: Security context
|
||||
:returns: a :class:`Image` object.
|
||||
"""
|
||||
db_image = dbapi.Connection.get_image_by_uuid(context, uuid)
|
||||
image = Image._from_db_object(cls(context), db_image)
|
||||
return image
|
||||
|
||||
@base.remotable_classmethod
|
||||
def list(cls, context=None, limit=None, marker=None,
|
||||
sort_key=None, sort_dir=None, filters=None):
|
||||
"""Return a list of Image objects.
|
||||
|
||||
:param context: Security context.
|
||||
:param limit: maximum number of resources to return in a single result.
|
||||
:param marker: pagination marker for large data sets.
|
||||
:param sort_key: column to sort results by.
|
||||
:param sort_dir: direction to sort. "asc" or "desc".
|
||||
:param filters: filters when list images, the filter name could be
|
||||
'repo', 'image_id', 'project_id', 'user_id', 'size'
|
||||
:returns: a list of :class:`Image` object.
|
||||
|
||||
"""
|
||||
db_images = dbapi.Connection.list_image(context, limit=limit,
|
||||
marker=marker,
|
||||
sort_key=sort_key,
|
||||
sort_dir=sort_dir,
|
||||
filters=filters)
|
||||
return Image._from_db_object_list(db_images, cls, context)
|
||||
|
||||
@base.remotable
|
||||
def create(self, context=None):
|
||||
"""Create an image record in the DB.
|
||||
|
||||
:param context: Security context. NOTE: This should only
|
||||
be used internally by the indirection_api.
|
||||
Unfortunately, RPC requires context as the first
|
||||
argument, even though we don't use it.
|
||||
A context should be set when instantiating the
|
||||
object, e.g.: Image(context)
|
||||
|
||||
"""
|
||||
values = self.obj_get_changes()
|
||||
db_image = dbapi.Connection.create_image(values)
|
||||
self._from_db_object(self, db_image)
|
||||
|
||||
@base.remotable
|
||||
def save(self, context=None):
|
||||
"""Save updates to this Image.
|
||||
|
||||
Updates will be made column by column based on the result
|
||||
of self.what_changed().
|
||||
|
||||
:param context: Security context. NOTE: This should only
|
||||
be used internally by the indirection_api.
|
||||
Unfortunately, RPC requires context as the first
|
||||
argument, even though we don't use it.
|
||||
A context should be set when instantiating the
|
||||
object, e.g.: Image(context)
|
||||
"""
|
||||
updates = self.obj_get_changes()
|
||||
dbapi.Connection.update_image(self.uuid, updates)
|
||||
self.obj_reset_changes()
|
@ -50,6 +50,11 @@ class TestRootController(api_base.FunctionalTest):
|
||||
u'containers': [{u'href': u'http://localhost/v1/containers/',
|
||||
u'rel': u'self'},
|
||||
{u'href': u'http://localhost/containers/',
|
||||
u'rel': u'bookmark'}],
|
||||
u'id': u'v1',
|
||||
u'images': [{u'href': u'http://localhost/v1/images/',
|
||||
u'rel': u'self'},
|
||||
{u'href': u'http://localhost/images/',
|
||||
u'rel': u'bookmark'}]}
|
||||
|
||||
def make_app(self, paste_file):
|
||||
|
@ -181,27 +181,54 @@ class TestTypes(test_base.BaseTestCase):
|
||||
self.assertRaises(exception.InvalidValue,
|
||||
types.NameType.validate, test_value)
|
||||
|
||||
def test_image_name_type(self):
|
||||
test_value = ""
|
||||
self.assertRaises(exception.InvalidValue,
|
||||
types.ImageNameType.validate, test_value)
|
||||
|
||||
test_value = '*' * 256
|
||||
self.assertRaises(exception.InvalidValue,
|
||||
types.ImageNameType.validate, test_value)
|
||||
|
||||
def test_container_memory_type(self):
|
||||
test_value = '4m'
|
||||
value = types.ContainerMemory.validate(test_value)
|
||||
value = types.MemoryType.validate(test_value)
|
||||
self.assertEqual(value, test_value)
|
||||
|
||||
test_value = '4'
|
||||
self.assertRaises(exception.InvalidValue,
|
||||
types.ContainerMemory.validate, test_value)
|
||||
types.MemoryType.validate, test_value)
|
||||
|
||||
test_value = '10000A'
|
||||
self.assertRaises(exception.InvalidValue,
|
||||
types.ContainerMemory.validate, test_value)
|
||||
types.MemoryType.validate, test_value)
|
||||
|
||||
test_value = '4K'
|
||||
self.assertRaises(exception.InvalidValue,
|
||||
types.ContainerMemory.validate, test_value)
|
||||
types.MemoryType.validate, test_value)
|
||||
|
||||
test_value = '4194304'
|
||||
value = types.ContainerMemory.validate(test_value)
|
||||
value = types.MemoryType.validate(test_value)
|
||||
self.assertEqual(value, test_value)
|
||||
|
||||
test_value = '4194304.0'
|
||||
self.assertRaises(exception.InvalidValue,
|
||||
types.ContainerMemory.validate, test_value)
|
||||
types.MemoryType.validate, test_value)
|
||||
|
||||
def test_image_size(self):
|
||||
test_value = '400'
|
||||
value = types.ImageSize.validate(test_value)
|
||||
self.assertEqual(value, test_value)
|
||||
|
||||
test_value = '4194304.0'
|
||||
self.assertRaises(exception.InvalidValue,
|
||||
types.ImageSize.validate, test_value)
|
||||
|
||||
test_value = '10000A'
|
||||
self.assertRaises(exception.InvalidValue,
|
||||
types.ImageSize.validate, test_value)
|
||||
|
||||
test_value = '4K'
|
||||
expected_value = 4096
|
||||
value = types.ImageSize.validate(test_value)
|
||||
self.assertEqual(value, expected_value)
|
||||
|
119
zun/tests/unit/api/controllers/v1/test_images.py
Normal file
119
zun/tests/unit/api/controllers/v1/test_images.py
Normal file
@ -0,0 +1,119 @@
|
||||
# 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 mock
|
||||
from mock import patch
|
||||
|
||||
from zun.common import utils as comm_utils
|
||||
from zun import objects
|
||||
from zun.tests.unit.api import base as api_base
|
||||
from zun.tests.unit.db import utils
|
||||
|
||||
|
||||
class TestImageController(api_base.FunctionalTest):
|
||||
@patch('zun.compute.api.API.image_create')
|
||||
def test_image_create(self, mock_image_create):
|
||||
mock_image_create.side_effect = lambda x, y: y
|
||||
|
||||
params = ('{"repo": "hello-world"}')
|
||||
response = self.app.post('/v1/images/',
|
||||
params=params,
|
||||
content_type='application/json')
|
||||
|
||||
self.assertEqual(202, response.status_int)
|
||||
self.assertTrue(mock_image_create.called)
|
||||
|
||||
@patch('zun.compute.api.API.image_create')
|
||||
def test_create_image_set_project_id_and_user_id(
|
||||
self, mock_image_create):
|
||||
def _create_side_effect(cnxt, image):
|
||||
self.assertEqual(self.context.project_id, image.project_id)
|
||||
self.assertEqual(self.context.user_id, image.user_id)
|
||||
return image
|
||||
mock_image_create.side_effect = _create_side_effect
|
||||
|
||||
params = ('{"repo": "hello-world"}')
|
||||
self.app.post('/v1/images/',
|
||||
params=params,
|
||||
content_type='application/json')
|
||||
|
||||
@patch('zun.compute.api.API.image_create')
|
||||
def test_image_create_with_tag(self, mock_image_create):
|
||||
mock_image_create.side_effect = lambda x, y: y
|
||||
|
||||
params = ('{"repo": "hello-world:latest"}')
|
||||
response = self.app.post('/v1/images/',
|
||||
params=params,
|
||||
content_type='application/json')
|
||||
|
||||
self.assertEqual(202, response.status_int)
|
||||
self.assertTrue(mock_image_create.called)
|
||||
|
||||
@patch('zun.compute.api.API.image_show')
|
||||
@patch('zun.objects.Image.list')
|
||||
def test_get_all_images(self, mock_image_list, mock_image_show):
|
||||
test_image = utils.get_test_image()
|
||||
images = [objects.Image(self.context, **test_image)]
|
||||
mock_image_list.return_value = images
|
||||
mock_image_show.return_value = images[0]
|
||||
|
||||
response = self.app.get('/v1/images/')
|
||||
|
||||
mock_image_list.assert_called_once_with(mock.ANY,
|
||||
1000, None, 'id', 'asc',
|
||||
filters=None)
|
||||
self.assertEqual(200, response.status_int)
|
||||
actual_images = response.json['images']
|
||||
self.assertEqual(1, len(actual_images))
|
||||
self.assertEqual(test_image['uuid'],
|
||||
actual_images[0].get('uuid'))
|
||||
|
||||
@patch('zun.compute.api.API.image_show')
|
||||
@patch('zun.objects.Image.list')
|
||||
def test_get_all_images_with_pagination_marker(self, mock_image_list,
|
||||
mock_image_show):
|
||||
image_list = []
|
||||
for id_ in range(4):
|
||||
test_image = utils.create_test_image(
|
||||
id=id_,
|
||||
uuid=comm_utils.generate_uuid())
|
||||
image_list.append(objects.Image(self.context, **test_image))
|
||||
mock_image_list.return_value = image_list[-1:]
|
||||
mock_image_show.return_value = image_list[-1]
|
||||
response = self.app.get('/v1/images/?limit=3&marker=%s'
|
||||
% image_list[2].uuid)
|
||||
|
||||
self.assertEqual(200, response.status_int)
|
||||
actual_images = response.json['images']
|
||||
self.assertEqual(1, len(actual_images))
|
||||
self.assertEqual(image_list[-1].uuid,
|
||||
actual_images[0].get('uuid'))
|
||||
|
||||
@patch('zun.compute.api.API.image_show')
|
||||
@patch('zun.objects.Image.list')
|
||||
def test_get_all_images_with_exception(self, mock_image_list,
|
||||
mock_image_show):
|
||||
test_image = utils.get_test_image()
|
||||
images = [objects.Image(self.context, **test_image)]
|
||||
mock_image_list.return_value = images
|
||||
mock_image_show.side_effect = Exception
|
||||
|
||||
response = self.app.get('/v1/images/')
|
||||
|
||||
mock_image_list.assert_called_once_with(mock.ANY, 1000,
|
||||
None, 'id', 'asc',
|
||||
filters=None)
|
||||
self.assertEqual(200, response.status_int)
|
||||
actual_images = response.json['images']
|
||||
self.assertEqual(1, len(actual_images))
|
||||
self.assertEqual(test_image['uuid'],
|
||||
actual_images[0].get('uuid'))
|
132
zun/tests/unit/db/test_image.py
Normal file
132
zun/tests/unit/db/test_image.py
Normal file
@ -0,0 +1,132 @@
|
||||
# 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.
|
||||
|
||||
"""Tests for manipulating Images via the DB API"""
|
||||
from oslo_utils import uuidutils
|
||||
import six
|
||||
|
||||
from zun.common import exception
|
||||
from zun.tests.unit.db import base
|
||||
from zun.tests.unit.db import utils
|
||||
|
||||
|
||||
class DbImageTestCase(base.DbTestCase):
|
||||
|
||||
def test_create_image(self):
|
||||
utils.create_test_image(repo="ubuntu:latest")
|
||||
|
||||
def test_create_image_duplicate_repo(self):
|
||||
utils.create_test_image(repo="ubuntu:latest")
|
||||
utils.create_test_image(repo="ubuntu:14.04")
|
||||
|
||||
def test_create_image_duplicate_tag(self):
|
||||
utils.create_test_image(repo="ubuntu:latest")
|
||||
utils.create_test_image(repo="centos:latest")
|
||||
|
||||
def test_create_image_already_exists(self):
|
||||
utils.create_test_image(repo="ubuntu:latest")
|
||||
self.assertRaises(exception.ResourceExists,
|
||||
utils.create_test_image,
|
||||
repo="ubuntu:latest")
|
||||
|
||||
def test_get_image_by_id(self):
|
||||
image = utils.create_test_image()
|
||||
res = self.dbapi.get_image_by_id(self.context, image.id)
|
||||
self.assertEqual(image.id, res.id)
|
||||
self.assertEqual(image.uuid, res.uuid)
|
||||
|
||||
def test_get_image_by_uuid(self):
|
||||
image = utils.create_test_image()
|
||||
res = self.dbapi.get_image_by_uuid(self.context, image.uuid)
|
||||
self.assertEqual(image.id, res.id)
|
||||
self.assertEqual(image.uuid, res.uuid)
|
||||
|
||||
def test_get_image_that_does_not_exist(self):
|
||||
self.assertRaises(exception.ImageNotFound,
|
||||
self.dbapi.get_image_by_id, self.context, 99)
|
||||
self.assertRaises(exception.ImageNotFound,
|
||||
self.dbapi.get_image_by_uuid,
|
||||
self.context,
|
||||
uuidutils.generate_uuid())
|
||||
|
||||
def test_list_images(self):
|
||||
uuids = []
|
||||
for i in range(1, 6):
|
||||
image = utils.create_test_image(
|
||||
repo="testrepo" + str(i))
|
||||
uuids.append(six.text_type(image['uuid']))
|
||||
res = self.dbapi.list_image(self.context)
|
||||
res_uuids = [r.uuid for r in res]
|
||||
self.assertEqual(sorted(uuids), sorted(res_uuids))
|
||||
|
||||
def test_list_image_sorted(self):
|
||||
uuids = []
|
||||
for _ in range(5):
|
||||
image = utils.create_test_image(
|
||||
uuid=uuidutils.generate_uuid())
|
||||
uuids.append(six.text_type(image.uuid))
|
||||
res = self.dbapi.list_image(self.context, sort_key='uuid')
|
||||
res_uuids = [r.uuid for r in res]
|
||||
self.assertEqual(sorted(uuids), res_uuids)
|
||||
|
||||
self.assertRaises(exception.InvalidParameterValue,
|
||||
self.dbapi.list_image,
|
||||
self.context,
|
||||
sort_key='foo')
|
||||
|
||||
def test_list_image_with_filters(self):
|
||||
image1 = utils.create_test_image(
|
||||
repo='image-one',
|
||||
uuid=uuidutils.generate_uuid())
|
||||
image2 = utils.create_test_image(
|
||||
repo='image-two',
|
||||
uuid=uuidutils.generate_uuid())
|
||||
|
||||
res = self.dbapi.list_image(self.context,
|
||||
filters={'repo': 'image-one'})
|
||||
self.assertEqual([image1.id], [r.id for r in res])
|
||||
|
||||
res = self.dbapi.list_image(self.context,
|
||||
filters={'repo': 'image-two'})
|
||||
self.assertEqual([image2.id], [r.id for r in res])
|
||||
|
||||
res = self.dbapi.list_image(self.context,
|
||||
filters={'repo': 'bad-image'})
|
||||
self.assertEqual([], [r.id for r in res])
|
||||
|
||||
res = self.dbapi.list_image(
|
||||
self.context,
|
||||
filters={'repo': image1.repo})
|
||||
self.assertEqual([image1.id], [r.id for r in res])
|
||||
|
||||
def test_update_image(self):
|
||||
image = utils.create_test_image()
|
||||
old_size = image.size
|
||||
new_size = '2000'
|
||||
self.assertNotEqual(old_size, new_size)
|
||||
|
||||
res = self.dbapi.update_image(image.id,
|
||||
{'size': new_size})
|
||||
self.assertEqual(new_size, res.size)
|
||||
|
||||
def test_update_image_not_found(self):
|
||||
image_uuid = uuidutils.generate_uuid()
|
||||
new_size = '2000'
|
||||
self.assertRaises(exception.ImageNotFound,
|
||||
self.dbapi.update_image,
|
||||
image_uuid, {'size': new_size})
|
||||
|
||||
def test_update_image_uuid(self):
|
||||
image = utils.create_test_image()
|
||||
self.assertRaises(exception.InvalidParameterValue,
|
||||
self.dbapi.update_image, image.id,
|
||||
{'uuid': ''})
|
@ -12,6 +12,7 @@
|
||||
"""Zun test utilities."""
|
||||
|
||||
|
||||
from zun.common import name_generator
|
||||
from zun.db import api as db_api
|
||||
|
||||
|
||||
@ -52,3 +53,42 @@ def create_test_container(**kw):
|
||||
del container['id']
|
||||
dbapi = db_api.get_instance()
|
||||
return dbapi.create_container(container)
|
||||
|
||||
|
||||
def get_test_image(**kw):
|
||||
return {
|
||||
'id': kw.get('id', 42),
|
||||
'uuid': kw.get('uuid', 'ea8e2a25-2901-438d-8157-de7ffd68d051'),
|
||||
'repo': kw.get('repo', 'image1'),
|
||||
'tag': kw.get('tag', 'latest'),
|
||||
'image_id': kw.get('image_id', 'sha256:c54a2cc56cbb2f0400'),
|
||||
'size': kw.get('size', '1848'),
|
||||
'project_id': kw.get('project_id', 'fake_project'),
|
||||
'user_id': kw.get('user_id', 'fake_user'),
|
||||
'created_at': kw.get('created_at'),
|
||||
'updated_at': kw.get('updated_at'),
|
||||
}
|
||||
|
||||
|
||||
def create_test_image(**kw):
|
||||
"""Create test image entry in DB and return Image DB object.
|
||||
|
||||
Function to be used to create test Image objects in the database.
|
||||
:param kw: kwargs with overriding values for image's attributes.
|
||||
:returns: Test Image DB object.
|
||||
"""
|
||||
image = get_test_image(**kw)
|
||||
# Let DB generate ID if it isn't specified explicitly
|
||||
if 'id' not in kw:
|
||||
del image['id']
|
||||
if 'repo' not in kw:
|
||||
image['repo'] = _generate_repo_for_image()
|
||||
dbapi = db_api.get_instance()
|
||||
return dbapi.create_image(image)
|
||||
|
||||
|
||||
def _generate_repo_for_image():
|
||||
'''Generate a random name like: zeta-22-image.'''
|
||||
name_gen = name_generator.NameGenerator()
|
||||
name = name_gen.generate()
|
||||
return name + '-image'
|
||||
|
101
zun/tests/unit/objects/test_image.py
Normal file
101
zun/tests/unit/objects/test_image.py
Normal file
@ -0,0 +1,101 @@
|
||||
# Copyright 2015 OpenStack Foundation
|
||||
# All Rights Reserved.
|
||||
#
|
||||
# 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 mock
|
||||
from testtools.matchers import HasLength
|
||||
|
||||
from zun import objects
|
||||
from zun.tests.unit.db import base
|
||||
from zun.tests.unit.db import utils
|
||||
|
||||
|
||||
class TestImageObject(base.DbTestCase):
|
||||
|
||||
def setUp(self):
|
||||
super(TestImageObject, self).setUp()
|
||||
self.fake_image = utils.get_test_image()
|
||||
|
||||
def test_get_by_id(self):
|
||||
image_id = self.fake_image['id']
|
||||
with mock.patch.object(self.dbapi, 'get_image_by_id',
|
||||
autospec=True) as mock_get_image:
|
||||
mock_get_image.return_value = self.fake_image
|
||||
image = objects.Image.get_by_id(self.context,
|
||||
image_id)
|
||||
mock_get_image.assert_called_once_with(self.context,
|
||||
image_id)
|
||||
self.assertEqual(self.context, image._context)
|
||||
|
||||
def test_get_by_uuid(self):
|
||||
uuid = self.fake_image['uuid']
|
||||
with mock.patch.object(self.dbapi, 'get_image_by_uuid',
|
||||
autospec=True) as mock_get_image:
|
||||
mock_get_image.return_value = self.fake_image
|
||||
image = objects.Image.get_by_uuid(self.context, uuid)
|
||||
mock_get_image.assert_called_once_with(self.context, uuid)
|
||||
self.assertEqual(self.context, image._context)
|
||||
|
||||
def test_list(self):
|
||||
with mock.patch.object(self.dbapi, 'list_image',
|
||||
autospec=True) as mock_get_list:
|
||||
mock_get_list.return_value = [self.fake_image]
|
||||
images = objects.Image.list(self.context)
|
||||
self.assertEqual(1, mock_get_list.call_count)
|
||||
self.assertThat(images, HasLength(1))
|
||||
self.assertIsInstance(images[0], objects.Image)
|
||||
self.assertEqual(self.context, images[0]._context)
|
||||
|
||||
def test_list_with_filters(self):
|
||||
with mock.patch.object(self.dbapi, 'list_image',
|
||||
autospec=True) as mock_get_list:
|
||||
mock_get_list.return_value = [self.fake_image]
|
||||
filt = {'id': '1'}
|
||||
images = objects.Image.list(self.context, filters=filt)
|
||||
self.assertEqual(1, mock_get_list.call_count)
|
||||
self.assertThat(images, HasLength(1))
|
||||
self.assertIsInstance(images[0], objects.Image)
|
||||
self.assertEqual(self.context, images[0]._context)
|
||||
mock_get_list.assert_called_once_with(self.context,
|
||||
filters=filt,
|
||||
limit=None, marker=None,
|
||||
sort_key=None, sort_dir=None)
|
||||
|
||||
def test_create(self):
|
||||
with mock.patch.object(self.dbapi, 'create_image',
|
||||
autospec=True) as mock_create_image:
|
||||
mock_create_image.return_value = self.fake_image
|
||||
image = objects.Image(self.context, **self.fake_image)
|
||||
image.create()
|
||||
mock_create_image.assert_called_once_with(self.fake_image)
|
||||
self.assertEqual(self.context, image._context)
|
||||
|
||||
def test_save(self):
|
||||
uuid = self.fake_image['uuid']
|
||||
with mock.patch.object(self.dbapi, 'get_image_by_uuid',
|
||||
autospec=True) as mock_get_image:
|
||||
mock_get_image.return_value = self.fake_image
|
||||
with mock.patch.object(self.dbapi, 'update_image',
|
||||
autospec=True) as mock_update_image:
|
||||
image = objects.Image.get_by_uuid(self.context, uuid)
|
||||
image.repo = 'image-test'
|
||||
image.tag = '512'
|
||||
image.save()
|
||||
|
||||
mock_get_image.assert_called_once_with(self.context, uuid)
|
||||
mock_update_image.assert_called_once_with(uuid,
|
||||
{'repo':
|
||||
'image-test',
|
||||
'tag': '512'})
|
||||
self.assertEqual(self.context, image._context)
|
Loading…
Reference in New Issue
Block a user