diff --git a/zun/api/controllers/v1/containers.py b/zun/api/controllers/v1/containers.py index 7fb86f4c5..53acf28b4 100644 --- a/zun/api/controllers/v1/containers.py +++ b/zun/api/controllers/v1/containers.py @@ -355,6 +355,12 @@ class ContainersController(base.Controller): pci_req = self._create_pci_requests_for_sriov_ports(context, requested_networks) + healthcheck = container_dict.pop('healthcheck', {}) + if healthcheck: + api_utils.version_check('healthcheck', '1.22') + healthcheck['test'] = healthcheck.pop('cmd', '') + container_dict['healthcheck'] = healthcheck + mounts = container_dict.pop('mounts', []) if mounts: api_utils.version_check('mounts', '1.11') diff --git a/zun/api/controllers/v1/schemas/containers.py b/zun/api/controllers/v1/schemas/containers.py index ef6e94b63..94f820353 100644 --- a/zun/api/controllers/v1/schemas/containers.py +++ b/zun/api/controllers/v1/schemas/containers.py @@ -38,6 +38,7 @@ _legacy_container_properties = { 'availability_zone': parameter_types.availability_zone, 'auto_heal': parameter_types.boolean, 'privileged': parameter_types.boolean, + 'healthcheck': parameter_types.healthcheck, } legacy_container_create = { diff --git a/zun/api/controllers/v1/schemas/parameter_types.py b/zun/api/controllers/v1/schemas/parameter_types.py index 27b16f30b..8d35c658e 100644 --- a/zun/api/controllers/v1/schemas/parameter_types.py +++ b/zun/api/controllers/v1/schemas/parameter_types.py @@ -172,6 +172,30 @@ availability_zone = { 'maxLength': 255, } +healthcheck = { + 'type': ['object', 'null'], + 'items': { + 'type': 'object', + 'properties': { + 'cmd': { + 'type': ['string'], + 'minLength': 1, + 'maxLength': 255 + }, + 'interval': { + 'type': ['integer', 'null'] + }, + 'retries': { + 'type': ['integer', 'null'] + }, + 'timeout': { + 'type': ['integer', 'null'] + } + }, + 'additionalProperties': False + } +} + mounts = { 'type': ['array', 'null'], 'items': { diff --git a/zun/api/controllers/v1/views/containers_view.py b/zun/api/controllers/v1/views/containers_view.py index 78aa976a2..971b87ccc 100644 --- a/zun/api/controllers/v1/views/containers_view.py +++ b/zun/api/controllers/v1/views/containers_view.py @@ -48,6 +48,7 @@ _basic_keys = ( 'disk', 'auto_heal', 'privileged', + 'healthcheck', ) diff --git a/zun/api/controllers/versions.py b/zun/api/controllers/versions.py index 27a6abacd..e2b691afc 100644 --- a/zun/api/controllers/versions.py +++ b/zun/api/controllers/versions.py @@ -51,13 +51,14 @@ REST_API_VERSION_HISTORY = """REST API Version History: * 1.16 - Modify restart_policy to capsule spec content * 1.17 - Add support for detaching ports * 1.18 - Modify the response of network list - * 1.19 - Intoduce container resize API + * 1.19 - Introduce container resize API * 1.20 - Convert type of 'command' from string to list * 1.21 - Add support privileged + * 1.22 - Add healthcheck to container create """ BASE_VER = '1.1' -CURRENT_MAX_VER = '1.21' +CURRENT_MAX_VER = '1.22' class Version(object): diff --git a/zun/api/rest_api_version_history.rst b/zun/api/rest_api_version_history.rst index 56d081328..69216bdda 100644 --- a/zun/api/rest_api_version_history.rst +++ b/zun/api/rest_api_version_history.rst @@ -177,3 +177,9 @@ user documentation. ---- Support privileged container + +1.22 +---- + + Add healthcheck to container create + diff --git a/zun/container/docker/driver.py b/zun/container/docker/driver.py index f4f2466ab..86ac427d3 100644 --- a/zun/container/docker/driver.py +++ b/zun/container/docker/driver.py @@ -301,6 +301,18 @@ class DockerDriver(driver.ContainerDriver): if container.disk: disk_size = str(container.disk) + 'G' host_config['storage_opt'] = {'size': disk_size} + # The time unit in docker of heath checking is us, and the unit + # of interval and timeout is seconds. + if container.healthcheck: + healthcheck = {} + healthcheck['test'] = container.healthcheck.get('test', '') + interval = container.healthcheck.get('interval', 0) + healthcheck['interval'] = interval * 10 ** 9 + healthcheck['retries'] = int(container.healthcheck. + get('retries', 0)) + timeout = container.healthcheck.get('timeout', 0) + healthcheck['timeout'] = timeout * 10 ** 9 + kwargs['healthcheck'] = healthcheck kwargs['host_config'] = docker.create_host_config(**host_config) if image['tag']: diff --git a/zun/db/sqlalchemy/alembic/versions/2fb377a5a519_add_healthcheck_to_container.py b/zun/db/sqlalchemy/alembic/versions/2fb377a5a519_add_healthcheck_to_container.py new file mode 100644 index 000000000..e70ba5faf --- /dev/null +++ b/zun/db/sqlalchemy/alembic/versions/2fb377a5a519_add_healthcheck_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 healthcheck to containers + +Revision ID: 2fb377a5a519 +Revises: 105626c4f972 +Create Date: 2018-05-03 11:27:00.722445 + +""" + +# revision identifiers, used by Alembic. +revision = '2fb377a5a519' +down_revision = '105626c4f972' +branch_labels = None +depends_on = None + +from alembic import op +import sqlalchemy as sa +import zun + + +def upgrade(): + with op.batch_alter_table('container', schema=None) as batch_op: + batch_op.add_column(sa.Column( + 'healthcheck', zun.db.sqlalchemy.models.JSONEncodedDict(), + nullable=True)) diff --git a/zun/db/sqlalchemy/models.py b/zun/db/sqlalchemy/models.py index bd925fbd6..0040441c6 100644 --- a/zun/db/sqlalchemy/models.py +++ b/zun/db/sqlalchemy/models.py @@ -171,6 +171,7 @@ class Container(Base): ForeignKey('capsule.id', ondelete='CASCADE')) started_at = Column(DateTime) privileged = Column(Boolean, default=False) + healthcheck = Column(JSONEncodedDict) class VolumeMapping(Base): diff --git a/zun/objects/container.py b/zun/objects/container.py index 2838acc9e..be6e8d69e 100644 --- a/zun/objects/container.py +++ b/zun/objects/container.py @@ -64,7 +64,8 @@ class Container(base.ZunPersistentObject, base.ZunObject): # Version 1.32: Add 'exec_instances' attribute # Version 1.33: Change 'command' to List type # Version 1.34: Add privileged to container - VERSION = '1.34' + # Version 1.35: Add 'healthcheck' attribute + VERSION = '1.35' fields = { 'id': fields.IntegerField(), @@ -107,6 +108,7 @@ class Container(base.ZunPersistentObject, base.ZunObject): 'exec_instances': fields.ListOfObjectsField('ExecInstance', nullable=True), 'privileged': fields.BooleanField(nullable=True), + 'healthcheck': z_fields.JsonField(nullable=True), } @staticmethod diff --git a/zun/tests/unit/api/base.py b/zun/tests/unit/api/base.py index 8b63044d3..5c698ec71 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.21" +CURRENT_VERSION = "container 1.22" 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 66f0b14e0..b7c3fecca 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.21', + 'max_version': '1.22', '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.21', + 'max_version': '1.22', 'min_version': '1.1', 'status': 'CURRENT'}]} diff --git a/zun/tests/unit/api/controllers/v1/test_containers.py b/zun/tests/unit/api/controllers/v1/test_containers.py index 0c43cebff..454692592 100644 --- a/zun/tests/unit/api/controllers/v1/test_containers.py +++ b/zun/tests/unit/api/controllers/v1/test_containers.py @@ -121,6 +121,16 @@ class TestContainerController(api_base.FunctionalTest): params=params, content_type='application/json', headers=headers) + def test_run_container_healthcheck_wrong_value(self): + params = ('{"name": "MyDocker", "image": "ubuntu",' + '"command": ["env"], "memory": "512",' + '"environment": {"key1": "val1", "key2": "val2"},' + '"healthcheck": "test"}') + with self.assertRaisesRegex(AppError, + "Invalid input for field"): + self.post('/v1/containers?run=true', + params=params, content_type='application/json') + @patch('zun.network.neutron.NeutronAPI.get_available_network') @patch('zun.compute.api.API.container_create') @patch('zun.compute.api.API.image_search') diff --git a/zun/tests/unit/container/docker/test_docker_driver.py b/zun/tests/unit/container/docker/test_docker_driver.py index 20684e3e4..4498b6ef9 100644 --- a/zun/tests/unit/container/docker/test_docker_driver.py +++ b/zun/tests/unit/container/docker/test_docker_driver.py @@ -117,6 +117,7 @@ class TestDockerDriver(base.DriverTestCase): image = {'path': '', 'image': '', 'repo': 'test', 'tag': 'test'} mock_container = self.mock_default_container mock_container.status = 'Creating' + mock_container.healthcheck = {} networks = [{'network': 'fake-network'}] volumes = [] fake_port = {'mac_address': 'fake_mac'} @@ -183,6 +184,7 @@ class TestDockerDriver(base.DriverTestCase): image = {'path': '', 'image': '', 'repo': 'test', 'tag': 'test'} mock_container = self.mock_default_container mock_container.status = 'Creating' + mock_container.healthcheck = {} networks = [{'network': 'fake-network'}] volumes = [] fake_port = {'mac_address': 'fake_mac'} @@ -251,6 +253,7 @@ class TestDockerDriver(base.DriverTestCase): image = {'path': '', 'image': '', 'repo': 'test', 'tag': 'test'} mock_container = self.mock_default_container mock_container.status = 'Creating' + mock_container.healthcheck = {} mock_container.runtime = None networks = [{'network': 'fake-network'}] volumes = [] diff --git a/zun/tests/unit/db/utils.py b/zun/tests/unit/db/utils.py index da4cb40b3..72c5fce99 100644 --- a/zun/tests/unit/db/utils.py +++ b/zun/tests/unit/db/utils.py @@ -103,6 +103,10 @@ def get_test_container(**kwargs): 'capsule_id': kwargs.get('capsule_id', 42), 'started_at': kwargs.get('started_at'), 'privileged': kwargs.get('privileged', False), + 'healthcheck': kwargs.get('healthcheck', + {"retries": "2", "timeout": 3, + "test": "stat /etc/passwd || exit 1", + "interval": 3}), } diff --git a/zun/tests/unit/objects/test_objects.py b/zun/tests/unit/objects/test_objects.py index 9b97eeee2..e73f8020b 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.34-22c46c6ae571b83295c3dac74fe8772f', + 'Container': '1.35-7cadf071bb6865a6da6b7be581ce76f6', 'VolumeMapping': '1.1-50df6202f7846a136a91444c38eba841', 'Image': '1.1-330e6205c80b99b59717e1cfc6a79935', 'MyObj': '1.0-34c4b1aadefd177b13f9a2f894cc23cd',