diff --git a/template/capsule/capsule.yaml b/template/capsule/capsule.yaml index 300529f99..484cd0a46 100644 --- a/template/capsule/capsule.yaml +++ b/template/capsule/capsule.yaml @@ -5,8 +5,8 @@ kind: capsule metadata: name: capsule-example labels: - - app: web - - nihao: baibai + app: web + nihao: baibai restart_policy: always spec: containers: diff --git a/zun/api/controllers/experimental/capsules.py b/zun/api/controllers/experimental/capsules.py index 699738a08..d516b0c41 100644 --- a/zun/api/controllers/experimental/capsules.py +++ b/zun/api/controllers/experimental/capsules.py @@ -13,7 +13,9 @@ # under the License. from oslo_log import log as logging +from oslo_utils import strutils import pecan +import six from zun.api.controllers import base from zun.api.controllers.experimental import collection @@ -76,6 +78,70 @@ class CapsuleController(base.Controller): } + @pecan.expose('json') + @exception.wrap_pecan_controller_exception + def get_all(self, **kwargs): + '''Retrieve a list of capsules.''' + context = pecan.request.context + policy.enforce(context, "capsule:get_all", + action="capsule:get_all") + return self._get_capsules_collection(**kwargs) + + def _get_capsules_collection(self, **kwargs): + context = pecan.request.context + all_tenants = kwargs.get('all_tenants') + if all_tenants: + try: + all_tenants = strutils.bool_from_string(all_tenants, True) + except ValueError as err: + raise exception.InvalidInput(six.text_type(err)) + else: + # If no value, it's considered to disable all_tenants + all_tenants = False + if all_tenants: + context.all_tenants = True + compute_api = pecan.request.compute_api + limit = api_utils.validate_limit(kwargs.get('limit')) + 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') + expand = kwargs.get('expand') + filters = None + marker_obj = None + marker = kwargs.get('marker') + if marker: + marker_obj = objects.Capsule.get_by_uuid(context, + marker) + capsules = objects.Capsule.list(context, + limit, + marker_obj, + sort_key, + sort_dir, + filters=filters) + + # Sync status for container inside capsule + for i, capsule in enumerate(capsules): + try: + containers_list = capsule.containers_uuids + if containers_list is not None: + # Capsule is depending on infra container status + uuid = containers_list[0] + container = utils.get_container(uuid) + container = compute_api.container_show(context, container) + capsule.status = container.status + capsule.save(context) + except Exception as e: + LOG.exception(("Error while list capsule %(uuid)s: " + "%(e)s."), + {'uuid': capsule.uuid, 'e': e}) + capsules[i].status = consts.UNKNOWN + + return CapsuleCollection.convert_with_links(capsules, 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 diff --git a/zun/db/sqlalchemy/alembic/versions/fc27c7415d9c_change_the_properties_of_meta_labels.py b/zun/db/sqlalchemy/alembic/versions/fc27c7415d9c_change_the_properties_of_meta_labels.py new file mode 100644 index 000000000..725fa4c40 --- /dev/null +++ b/zun/db/sqlalchemy/alembic/versions/fc27c7415d9c_change_the_properties_of_meta_labels.py @@ -0,0 +1,37 @@ +# Copyright 2017 Arm Limited. +# +# 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. + +"""change the properties of meta_labels + +Revision ID: fc27c7415d9c +Revises: bcd6410d645e +Create Date: 2017-09-07 10:56:07.489031 + +""" + +# revision identifiers, used by Alembic. +revision = 'fc27c7415d9c' +down_revision = 'bcd6410d645e' +branch_labels = None +depends_on = None + +from alembic import op + +import zun + + +def upgrade(): + op.alter_column('capsule', 'meta_labels', + type_=zun.db.sqlalchemy.models.JSONEncodedDict() + ) diff --git a/zun/db/sqlalchemy/models.py b/zun/db/sqlalchemy/models.py index 53d477cb2..a08c9cdc8 100644 --- a/zun/db/sqlalchemy/models.py +++ b/zun/db/sqlalchemy/models.py @@ -340,7 +340,7 @@ class Capsule(Base): status = Column(String(20)) status_reason = Column(Text, nullable=True) - meta_labels = Column(JSONEncodedList) + meta_labels = Column(JSONEncodedDict) meta_name = Column(String(255)) spec = Column(JSONEncodedDict) containers_uuids = Column(JSONEncodedList) diff --git a/zun/objects/capsule.py b/zun/objects/capsule.py index 816a51a6f..896f7fa79 100644 --- a/zun/objects/capsule.py +++ b/zun/objects/capsule.py @@ -23,7 +23,8 @@ from zun.objects import fields as z_fields class Capsule(base.ZunPersistentObject, base.ZunObject): # Version 1.0: Initial version # Version 1.1: Add host to capsule - VERSION = '1.1' + # Version 1.2: Change the properties of meta_labels + VERSION = '1.2' fields = { 'capsule_version': fields.StringField(nullable=True), @@ -48,7 +49,7 @@ class Capsule(base.ZunPersistentObject, base.ZunObject): 'spec': z_fields.JsonField(nullable=True), 'meta_name': fields.StringField(nullable=True), - 'meta_labels': z_fields.JsonField(nullable=True), + 'meta_labels': fields.DictOfStringsField(nullable=True), 'containers': fields.ListOfObjectsField('Container', nullable=True), 'containers_uuids': fields.ListOfStringsField(nullable=True), 'host': fields.StringField(nullable=True), diff --git a/zun/tests/unit/api/controllers/experimental/test_capsules.py b/zun/tests/unit/api/controllers/experimental/test_capsules.py index cb786d5d4..4d287d097 100644 --- a/zun/tests/unit/api/controllers/experimental/test_capsules.py +++ b/zun/tests/unit/api/controllers/experimental/test_capsules.py @@ -12,9 +12,11 @@ # See the License for the specific language governing permissions and # limitations under the License. +import mock from mock import patch from oslo_utils import uuidutils from webtest.app import AppError +from zun.common import consts from zun.common import exception from zun import objects from zun.tests.unit.api import base as api_base @@ -34,20 +36,19 @@ class TestCapsuleController(api_base.FunctionalTest): '"image": "test", "drivers": "cinder", "volumeType": ' '"type1", "driverOptions": "options", ' '"size": "5GB"}]}, ' - '"metadata": {"labels": [{"foo0": "bar0"}, ' - '{"foo1": "bar1"}], ' + '"metadata": {"labels": {"foo0": "bar0", "foo1": "bar1"}, ' '"name": "capsule-example"}}}') response = self.post('/capsules/', params=params, content_type='application/json') return_value = response.json expected_meta_name = "capsule-example" - expected_meta_label = [{"foo0": "bar0"}, {"foo1": "bar1"}] + expected_meta_labels = {"foo0": "bar0", "foo1": "bar1"} expected_container_num = 2 self.assertEqual(len(return_value["containers_uuids"]), expected_container_num) self.assertEqual(return_value["meta_name"], expected_meta_name) - self.assertEqual(return_value["meta_labels"], expected_meta_label) + self.assertEqual(return_value["meta_labels"], expected_meta_labels) self.assertEqual(202, response.status_int) self.assertTrue(mock_capsule_create.called) @@ -63,22 +64,21 @@ class TestCapsuleController(api_base.FunctionalTest): '"image": "test1", "labels": {"app1": "web1"}, ' '"image_driver": "docker", "resources": ' '{"allocation": {"cpu": 1, "memory": 1024}}}]}, ' - '"metadata": {"labels": [{"foo0": "bar0"}, ' - '{"foo1": "bar1"}], ' + '"metadata": {"labels": {"foo0": "bar0", "foo1": "bar1"}, ' '"name": "capsule-example"}}}') response = self.post('/capsules/', params=params, content_type='application/json') return_value = response.json expected_meta_name = "capsule-example" - expected_meta_label = [{"foo0": "bar0"}, {"foo1": "bar1"}] + expected_meta_labels = {"foo0": "bar0", "foo1": "bar1"} expected_container_num = 3 self.assertEqual(len(return_value["containers_uuids"]), expected_container_num) self.assertEqual(return_value["meta_name"], expected_meta_name) self.assertEqual(return_value["meta_labels"], - expected_meta_label) + expected_meta_labels) self.assertEqual(202, response.status_int) self.assertTrue(mock_capsule_create.called) @@ -92,7 +92,7 @@ class TestCapsuleController(api_base.FunctionalTest): '"image": "test1", "labels": {"app0": "web0"}, ' '"image_driver": "docker", "resources": ' '{"allocation": {"cpu": 1, "memory": 1024}}}]}, ' - '"metadata": {"labels": [{"foo0": "bar0"}], ' + '"metadata": {"labels": {"foo0": "bar0"}, ' '"name": "capsule-example"}}}') mock_check_template.side_effect = exception.InvalidCapsuleTemplate( "kind fields need to be set as capsule or Capsule") @@ -106,7 +106,7 @@ class TestCapsuleController(api_base.FunctionalTest): mock_capsule_create): params = ('{"spec": {"kind": "capsule",' '"spec": {container:[]}, ' - '"metadata": {"labels": [{"foo0": "bar0"}], ' + '"metadata": {"labels": {"foo0": "bar0"}, ' '"name": "capsule-example"}}}') mock_check_template.side_effect = exception.InvalidCapsuleTemplate( "Capsule need to have one container at least") @@ -120,7 +120,7 @@ class TestCapsuleController(api_base.FunctionalTest): mock_capsule_create): params = ('{"spec": {"kind": "capsule",' '"spec": {}, ' - '"metadata": {"labels": [{"foo0": "bar0"}], ' + '"metadata": {"labels": {"foo0": "bar0"}, ' '"name": "capsule-example"}}}') mock_check_template.side_effect = exception.InvalidCapsuleTemplate( "Capsule need to have one container at least") @@ -135,7 +135,7 @@ class TestCapsuleController(api_base.FunctionalTest): params = ('{"spec": {"kind": "capsule",' '"spec": {container:[{"environment": ' '{"ROOT_PASSWORD": "foo1"}]}, ' - '"metadata": {"labels": [{"foo0": "bar0"}], ' + '"metadata": {"labels": {"foo0": "bar0"}, ' '"name": "capsule-example"}}}') mock_check_template.side_effect = exception.InvalidCapsuleTemplate( "Container image is needed") @@ -278,3 +278,129 @@ class TestCapsuleController(api_base.FunctionalTest): self.assertEqual(204, response.status_int) context = mock_capsule_save.call_args[0][0] self.assertIs(False, context.all_tenants) + + @patch('zun.compute.api.API.container_show') + @patch('zun.objects.Capsule.list') + @patch('zun.objects.Container.get_by_uuid') + def test_get_all_capsules(self, mock_container_get_by_uuid, + mock_capsule_list, + mock_container_show): + test_container = utils.get_test_container() + test_container_obj = objects.Container(self.context, + **test_container) + mock_container_get_by_uuid.return_value = test_container_obj + + test_capsule = utils.get_test_capsule() + test_capsule_obj = objects.Capsule(self.context, **test_capsule) + mock_capsule_list.return_value = [test_capsule_obj] + mock_container_show.return_value = test_container_obj + + response = self.app.get('/capsules/') + + mock_capsule_list.assert_called_once_with(mock.ANY, + 1000, None, 'id', 'asc', + filters=None) + context = mock_capsule_list.call_args[0][0] + self.assertIs(False, context.all_tenants) + self.assertEqual(200, response.status_int) + actual_capsules = response.json['capsules'] + self.assertEqual(1, len(actual_capsules)) + self.assertEqual(test_capsule['uuid'], + actual_capsules[0].get('uuid')) + + @patch('zun.compute.api.API.container_show') + @patch('zun.objects.Capsule.list') + @patch('zun.objects.Container.get_by_uuid') + def test_get_all_capsules_all_tenants(self, + mock_container_get_by_uuid, + mock_capsule_list, + mock_container_show): + test_container = utils.get_test_container() + test_container_obj = objects.Container(self.context, + **test_container) + mock_container_get_by_uuid.return_value = test_container_obj + + test_capsule = utils.get_test_capsule() + test_capsule_obj = objects.Capsule(self.context, **test_capsule) + mock_capsule_list.return_value = [test_capsule_obj] + mock_container_show.return_value = test_container_obj + + response = self.app.get('/capsules/?all_tenants=1') + + mock_capsule_list.assert_called_once_with(mock.ANY, + 1000, None, 'id', 'asc', + filters=None) + context = mock_capsule_list.call_args[0][0] + self.assertIs(True, context.all_tenants) + self.assertEqual(200, response.status_int) + actual_capsules = response.json['capsules'] + self.assertEqual(1, len(actual_capsules)) + self.assertEqual(test_capsule['uuid'], + actual_capsules[0].get('uuid')) + + @patch('zun.compute.api.API.container_show') + @patch('zun.objects.Capsule.list') + @patch('zun.objects.Container.get_by_uuid') + def test_get_all_capsules_with_exception(self, + mock_container_get_by_uuid, + mock_capsule_list, + mock_container_show): + test_container = utils.get_test_container() + test_container_obj = objects.Container(self.context, + **test_container) + mock_container_get_by_uuid.return_value = test_container_obj + + test_capsule = utils.get_test_capsule() + test_capsule_obj = objects.Capsule(self.context, **test_capsule) + mock_capsule_list.return_value = [test_capsule_obj] + mock_container_show.side_effect = Exception + + response = self.app.get('/capsules/') + + mock_capsule_list.assert_called_once_with(mock.ANY, + 1000, None, 'id', 'asc', + filters=None) + context = mock_capsule_list.call_args[0][0] + self.assertIs(False, context.all_tenants) + self.assertEqual(200, response.status_int) + actual_capsules = response.json['capsules'] + self.assertEqual(1, len(actual_capsules)) + self.assertEqual(test_capsule['uuid'], + actual_capsules[0].get('uuid')) + self.assertEqual(consts.UNKNOWN, + actual_capsules[0].get('status')) + + @patch('zun.compute.api.API.container_show') + @patch('zun.objects.Capsule.list') + @patch('zun.objects.Capsule.save') + @patch('zun.objects.Container.get_by_uuid') + def test_get_all_capsules_with_pagination_marker( + self, + mock_container_get_by_uuid, + mock_capsule_save, + mock_capsule_list, + mock_container_show): + test_container = utils.get_test_container() + test_container_obj = objects.Container(self.context, + **test_container) + mock_container_get_by_uuid.return_value = test_container_obj + capsule_list = [] + for id_ in range(4): + test_capsule = utils.create_test_capsule( + id=id_, uuid=uuidutils.generate_uuid(), + name='capsule' + str(id_), context=self.context) + capsule_list.append(objects.Capsule(self.context, + **test_capsule)) + mock_capsule_list.return_value = capsule_list[-1:] + mock_container_show.return_value = capsule_list[-1] + mock_capsule_save.return_value = True + + response = self.app.get('/capsules/?limit=3&marker=%s' + % capsule_list[2].uuid) + + self.assertEqual(200, response.status_int) + actual_capsules = response.json['capsules'] + + self.assertEqual(1, len(actual_capsules)) + self.assertEqual(actual_capsules[-1].get('uuid'), + actual_capsules[0].get('uuid')) diff --git a/zun/tests/unit/db/utils.py b/zun/tests/unit/db/utils.py index 494c087ee..0db925a07 100644 --- a/zun/tests/unit/db/utils.py +++ b/zun/tests/unit/db/utils.py @@ -379,3 +379,18 @@ def get_test_capsule(**kwargs): '6219e0fb-2935-4db2-a3c7-86a2ac3ac84e']), 'host': kwargs.get('host', 'localhost'), } + + +def create_test_capsule(**kwargs): + """Create test capsule entry in DB and return Capsule DB object. + + Function to be used to create test Capsule objects in the database. + :param kwargs: kwargs with overriding values for capsule's attributes. + :returns: Test Capsule DB object. + """ + capsule = get_test_capsule(**kwargs) + # Let DB generate ID if it isn't specified explicitly + if CONF.db_type == 'sql' and 'id' not in kwargs: + del capsule['id'] + dbapi = db_api._get_dbdriver_instance() + return dbapi.create_capsule(kwargs['context'], capsule) diff --git a/zun/tests/unit/objects/test_objects.py b/zun/tests/unit/objects/test_objects.py index 00997631b..79b6431e6 100644 --- a/zun/tests/unit/objects/test_objects.py +++ b/zun/tests/unit/objects/test_objects.py @@ -354,7 +354,7 @@ object_data = { 'ResourceProvider': '1.0-92b427359d5a4cf9ec6c72cbe630ee24', 'ZunService': '1.1-b1549134bfd5271daec417ca8cabc77e', 'ComputeNode': '1.7-9b700eb146e9978d84e9ccc5849d90e2', - 'Capsule': '1.1-bbf2165650900e7d79d1c12a12464b59', + 'Capsule': '1.2-1f8b3716ef272c9d9cb55390f6a7cdc3', }