diff --git a/etc/zun/policy.json b/etc/zun/policy.json index 8297a3e5e..dd00dffa9 100644 --- a/etc/zun/policy.json +++ b/etc/zun/policy.json @@ -18,6 +18,7 @@ "container:logs": "rule:admin_or_user", "container:execute": "rule:admin_or_user", "container:kill": "rule:admin_or_user", + "container:run": "rule:default", "image:create": "rule:default", "image:get_all": "rule:default", diff --git a/zun/api/controllers/v1/containers.py b/zun/api/controllers/v1/containers.py index cfe9fa297..feb869a48 100644 --- a/zun/api/controllers/v1/containers.py +++ b/zun/api/controllers/v1/containers.py @@ -217,7 +217,8 @@ class ContainersController(rest.RestController): 'unpause': ['POST'], 'logs': ['GET'], 'execute': ['POST'], - 'kill': ['POST'] + 'kill': ['POST'], + 'run': ['POST'] } @pecan.expose('json') @@ -314,6 +315,35 @@ class ContainersController(rest.RestController): pecan.response.status = 202 return Container.convert_with_links(new_container) + @pecan.expose('json') + @api_utils.enforce_content_types(['application/json']) + @exception.wrap_pecan_controller_exception + def run(self, **container_dict): + """Create and starts a new container. + + :param container: a container within the request body. + """ + context = pecan.request.context + policy.enforce(context, "container:run", + action="container:run") + container_dict = Container(**container_dict).as_dict() + container_dict['project_id'] = context.project_id + container_dict['user_id'] = context.user_id + name = container_dict.get('name') or \ + self._generate_name_for_container() + container_dict['name'] = name + container_dict['status'] = fields.ContainerStatus.CREATING + new_container = objects.Container(context, **container_dict) + new_container.create(context) + container = pecan.request.rpcapi.container_run(context, + new_container) + + # Set the HTTP Location Header + pecan.response.location = link.build_url('containers', + container.uuid) + pecan.response.status = 200 + return Container.convert_with_links(container) + @pecan.expose('json') @exception.wrap_pecan_controller_exception def patch(self, container_id, **kwargs): diff --git a/zun/compute/api.py b/zun/compute/api.py index 58d269ca6..cedfa3d81 100644 --- a/zun/compute/api.py +++ b/zun/compute/api.py @@ -37,6 +37,9 @@ class API(rpc_service.API): def container_create(self, context, container): return self._cast('container_create', container=container) + def container_run(self, context, container): + return self._call('container_run', container=container) + def container_delete(self, context, container, force): return self._call('container_delete', container=container, force=force) diff --git a/zun/compute/manager.py b/zun/compute/manager.py index 2b6cf9961..52f286659 100644 --- a/zun/compute/manager.py +++ b/zun/compute/manager.py @@ -15,6 +15,7 @@ import six from oslo_log import log as logging +from oslo_utils import excutils from oslo_utils import strutils from zun.common import exception @@ -62,7 +63,17 @@ class Manager(object): def container_create(self, context, container): utils.spawn_n(self._do_container_create, context, container) - def _do_container_create(self, context, container): + @translate_exception + def container_run(self, context, container): + return self._do_container_run(context, container) + + def _do_container_run(self, context, container): + created_container = self._do_container_create(context, + container, + reraise=True) + return self._do_container_start(context, created_container) + + def _do_container_create(self, context, container, reraise=False): LOG.debug('Creating container...', context=context, container=container) @@ -75,33 +86,64 @@ class Manager(object): image = image_driver.pull_image(context, repo, tag, image_pull_policy) except exception.ImageNotFound as e: - LOG.error(six.text_type(e)) - self._fail_container(container, six.text_type(e)) + with excutils.save_and_reraise_exception(reraise=reraise): + LOG.error(six.text_type(e)) + self._fail_container(container, six.text_type(e)) return except exception.DockerError as e: - LOG.error(_LE("Error occured while calling docker image API: %s"), - six.text_type(e)) - self._fail_container(container, six.text_type(e)) + with excutils.save_and_reraise_exception(reraise=reraise): + LOG.error(_LE( + "Error occured while calling docker image API: %s"), + six.text_type(e)) + self._fail_container(container, six.text_type(e)) return except Exception as e: - LOG.exception(_LE("Unexpected exception: %s"), six.text_type(e)) - self._fail_container(container, six.text_type(e)) + with excutils.save_and_reraise_exception(reraise=reraise): + LOG.exception(_LE("Unexpected exception: %s"), + six.text_type(e)) + self._fail_container(container, six.text_type(e)) return container.task_state = fields.TaskState.CONTAINER_CREATING container.save() try: container = self.driver.create(container, image) - except exception.DockerError as e: - LOG.error(_LE("Error occured while calling docker create API: %s"), - six.text_type(e)) - self._fail_container(container, six.text_type(e)) - except Exception as e: - LOG.exception(_LE("Unexpected exception: %s"), six.text_type(e)) - self._fail_container(container, six.text_type(e)) - finally: container.task_state = None container.save() + return container + except exception.DockerError as e: + with excutils.save_and_reraise_exception(reraise=reraise): + LOG.error(_LE( + "Error occured while calling docker create API: %s"), + six.text_type(e)) + self._fail_container(container, six.text_type(e)) + return + except Exception as e: + with excutils.save_and_reraise_exception(reraise=reraise): + LOG.exception(_LE("Unexpected exception: %s"), + six.text_type(e)) + self._fail_container(container, six.text_type(e)) + return + + def _do_container_start(self, context, container): + LOG.debug('Starting container...', context=context, + container=container.uuid) + try: + # Although we dont need this validation, but i still + # keep it for extra surity + self._validate_container_state(container, 'start') + container = self.driver.start(container) + container.save() + return container + except exception.DockerError as e: + LOG.error(_LE("Error occured while calling docker start API: %s"), + six.text_type(e)) + self._fail_container(container, six.text_type(e)) + raise + except Exception as e: + LOG.exception(_LE("Unexpected exception: %s"), str(e)) + self._fail_container(container, six.text_type(e)) + raise @translate_exception def container_delete(self, context, container, force): @@ -186,20 +228,7 @@ class Manager(object): @translate_exception def container_start(self, context, container): - LOG.debug('Starting container...', context=context, - container=container.uuid) - try: - self._validate_container_state(container, 'start') - container = self.driver.start(container) - container.save() - return container - except exception.DockerError as e: - LOG.error(_LE("Error occured while calling docker start API: %s"), - six.text_type(e)) - raise - except Exception as e: - LOG.exception(_LE("Unexpected exception: %s"), str(e)) - raise e + return self._do_container_start(context, container) @translate_exception def container_pause(self, context, container): diff --git a/zun/tests/unit/api/controllers/v1/test_containers.py b/zun/tests/unit/api/controllers/v1/test_containers.py index 8c8e3bc47..5976c0607 100644 --- a/zun/tests/unit/api/controllers/v1/test_containers.py +++ b/zun/tests/unit/api/controllers/v1/test_containers.py @@ -23,6 +23,20 @@ from zun.tests.unit.objects import utils as obj_utils class TestContainerController(api_base.FunctionalTest): + @patch('zun.compute.api.API.container_run') + def test_run_container(self, mock_container_run): + mock_container_run.side_effect = lambda x, y: y + + params = ('{"name": "MyDocker", "image": "ubuntu",' + '"command": "env", "memory": "512m",' + '"environment": {"key1": "val1", "key2": "val2"}}') + response = self.app.post('/v1/containers/run', + params=params, + content_type='application/json') + + self.assertEqual(200, response.status_int) + self.assertTrue(mock_container_run.called) + @patch('zun.compute.api.API.container_create') def test_create_container(self, mock_container_create): mock_container_create.side_effect = lambda x, y: y diff --git a/zun/tests/unit/compute/test_compute_manager.py b/zun/tests/unit/compute/test_compute_manager.py index ac744ce1b..8bc9b8500 100644 --- a/zun/tests/unit/compute/test_compute_manager.py +++ b/zun/tests/unit/compute/test_compute_manager.py @@ -115,6 +115,101 @@ class TestManager(base.TestCase): self.compute_manager._do_container_create(self.context, container) mock_fail.assert_called_once_with(container, "Creation Failed") + @mock.patch.object(Container, 'save') + @mock.patch('zun.image.driver.pull_image') + @mock.patch.object(fake_driver, 'create') + @mock.patch.object(fake_driver, 'start') + def test_container_run(self, mock_start, + mock_create, mock_pull, mock_save): + container = Container(self.context, **utils.get_test_container()) + mock_pull.return_value = 'fake_path' + mock_create.return_value = container + container.status = 'Stopped' + self.compute_manager.container_run(self.context, container) + mock_save.assert_called_with() + mock_pull.assert_called_once_with(self.context, + container.image, + 'latest', 'always') + mock_create.assert_called_once_with(container, 'fake_path') + mock_start.assert_called_once_with(container) + + @mock.patch.object(Container, 'save') + @mock.patch('zun.image.driver.pull_image') + @mock.patch.object(manager.Manager, '_fail_container') + def test_container_run_image_not_found(self, mock_fail, + mock_pull, mock_save): + container = Container(self.context, **utils.get_test_container()) + mock_pull.side_effect = exception.ImageNotFound( + message="Image Not Found") + with self.assertRaisesRegexp(exception.ImageNotFound, + 'Image Not Found'): + self.compute_manager._do_container_run(self.context, + container) + mock_save.assert_called_with() + mock_fail.assert_called_with(container, 'Image Not Found') + mock_pull.assert_called_once_with(self.context, + container.image, + 'latest', 'always') + + @mock.patch.object(Container, 'save') + @mock.patch('zun.image.driver.pull_image') + @mock.patch.object(manager.Manager, '_fail_container') + def test_container_run_image_pull_exception_raised(self, mock_fail, + mock_pull, mock_save): + container = Container(self.context, **utils.get_test_container()) + mock_pull.side_effect = exception.ZunException( + message="Image Not Found") + with self.assertRaisesRegexp(exception.ZunException, + 'Image Not Found'): + self.compute_manager._do_container_run(self.context, + container) + mock_save.assert_called_with() + mock_fail.assert_called_with(container, 'Image Not Found') + mock_pull.assert_called_once_with(self.context, + container.image, + 'latest', 'always') + + @mock.patch.object(Container, 'save') + @mock.patch('zun.image.driver.pull_image') + @mock.patch.object(manager.Manager, '_fail_container') + def test_container_run_image_pull_docker_error(self, mock_fail, + mock_pull, mock_save): + container = Container(self.context, **utils.get_test_container()) + mock_pull.side_effect = exception.DockerError( + message="Docker Error occurred") + with self.assertRaisesRegexp(exception.DockerError, + 'Docker Error occurred'): + self.compute_manager._do_container_run(self.context, + container) + mock_save.assert_called_with() + mock_fail.assert_called_with(container, 'Docker Error occurred') + mock_pull.assert_called_once_with(self.context, + container.image, + 'latest', 'always') + + @mock.patch.object(Container, 'save') + @mock.patch('zun.image.driver.pull_image') + @mock.patch.object(manager.Manager, '_fail_container') + @mock.patch.object(fake_driver, 'create') + def test_container_run_create_raises_docker_error(self, mock_create, + mock_fail, + mock_pull, mock_save): + container = Container(self.context, **utils.get_test_container()) + mock_pull.return_value = {'name': 'nginx', 'path': None} + mock_create.side_effect = exception.DockerError( + message="Docker Error occurred") + with self.assertRaisesRegexp(exception.DockerError, + 'Docker Error occurred'): + self.compute_manager._do_container_run(self.context, + container) + mock_save.assert_called_with() + mock_fail.assert_called_with(container, 'Docker Error occurred') + mock_pull.assert_called_once_with(self.context, + container.image, + 'latest', 'always') + mock_create.assert_called_once_with(container, + {'name': 'nginx', 'path': None}) + @mock.patch.object(manager.Manager, '_validate_container_state') @mock.patch.object(fake_driver, 'delete') def test_container_delete(self, mock_delete, mock_validate): @@ -221,21 +316,26 @@ class TestManager(base.TestCase): mock_start.assert_called_once_with(container) @mock.patch.object(manager.Manager, '_validate_container_state') - def test_container_start_invalid_state(self, mock_validate): + @mock.patch.object(manager.Manager, '_fail_container') + def test_container_start_invalid_state(self, mock_fail, mock_validate): container = Container(self.context, **utils.get_test_container()) mock_validate.side_effect = exception.InvalidStateException self.assertRaises(exception.InvalidStateException, self.compute_manager.container_start, self.context, container) + mock_fail.assert_called_once() @mock.patch.object(manager.Manager, '_validate_container_state') + @mock.patch.object(manager.Manager, '_fail_container') @mock.patch.object(fake_driver, 'start') - def test_container_start_failed(self, mock_start, mock_validate): + def test_container_start_failed(self, mock_start, + mock_fail, mock_validate): container = Container(self.context, **utils.get_test_container()) mock_start.side_effect = exception.DockerError self.assertRaises(exception.DockerError, self.compute_manager.container_start, self.context, container) + mock_fail.assert_called_once() @mock.patch.object(manager.Manager, '_validate_container_state') @mock.patch.object(fake_driver, 'pause')