diff --git a/paunch/builder/compose1.py b/paunch/builder/compose1.py index fd65460..4faa900 100644 --- a/paunch/builder/compose1.py +++ b/paunch/builder/compose1.py @@ -120,25 +120,48 @@ class ComposeV1Builder(object): list(itertools.chain.from_iterable(container_names))): return False - ex_data_str = self.runner.inspect( - container, '{{index .Config.Labels "config_data"}}') - if not ex_data_str: + # fetch container inspect info + inspect_info = self.runner.inspect(container) + if not inspect_info: + # we shouldn't get here but you never know + self.log.debug("Deleting container (no inspect data): " + "%s" % container) + self.runner.remove_container(container) + return True + container_config = inspect_info.get('Config', {}) + config_data = container_config.get('Labels', {}).get('config_data') + try: + ex_data = yaml.safe_load(str(config_data)) + except Exception: + ex_data = None + + if not ex_data: self.log.debug("Deleting container (no_config_data): " "%s" % container) self.runner.remove_container(container) return True - try: - ex_data = yaml.safe_load(str(ex_data_str)) - except Exception: - ex_data = None - + # check if config_data has changed new_data = self.config[container] if new_data != ex_data: self.log.debug("Deleting container (changed config_data): %s" % container) self.runner.remove_container(container) return True + + # check if the container image has changed (but tag as not) + # e.g. if you use :latest, the name doesn't change but the ID does + container_image = container_config.get('Image') + + if container_image: + image_id_str = self.runner.inspect( + container_image, "{{index .Id}}", type='image') + if str(image_id_str).strip() != inspect_info.get('Image'): + self.log.debug("Deleting container (image updated): " + "%s" % container) + self.runner.remove_container(container) + return True + return False def label_arguments(self, cmd, container): diff --git a/paunch/tests/test_builder_compose1.py b/paunch/tests/test_builder_compose1.py index 6a31884..cd6203a 100644 --- a/paunch/tests/test_builder_compose1.py +++ b/paunch/tests/test_builder_compose1.py @@ -243,10 +243,14 @@ three-12345678 three''', '', 0), ('', '', 0), ('', '', 0), # ps for rename one # inspect one - ('{"start_order": 0, "image": "centos:7"}', '', 0), + ('[{"Config": {"Labels": {"config_data": ' + '"{\\\"start_order\\\": 0, \\\"image\\\": \\\"centos:7\\\"}"' + '}}}]', '', 0), ('Created two-12345678', '', 0), # inspect three - ('{"start_order": 42, "image": "centos:7"}', '', 0), + ('[{"Config": {"Labels": {"config_data": ' + '"{\\\"start_order\\\": 42, \\\"image\\\": \\\"centos:7\\\"}"' + '}}}]', '', 0), # stop three, changed config data ('', '', 0), # rm three, changed config data @@ -284,7 +288,6 @@ three-12345678 three''', '', 0), mock.ANY), # check the renamed one, config hasn't changed mock.call(['docker', 'inspect', '--type', 'container', - '--format', '{{index .Config.Labels "config_data"}}', 'one'], mock.ANY), # don't run one, its already running # run two @@ -299,7 +302,6 @@ three-12345678 three''', '', 0), ), # rm three, changed config mock.call(['docker', 'inspect', '--type', 'container', - '--format', '{{index .Config.Labels "config_data"}}', 'three'], mock.ANY), mock.call(['docker', 'stop', 'three'], mock.ANY), mock.call(['docker', 'rm', 'three'], mock.ANY), @@ -636,3 +638,191 @@ three-12345678 three''', '', 0), '--volume=/bar:/bar:ro', '--cpuset-cpus=0,1,2,3', 'foo'], cmd ) + + @mock.patch('paunch.runner.DockerRunner', autospec=True) + def test_delete_updated_no_change(self, runner): + mock_inspect = mock.MagicMock() + mock_inspect.side_effect = [ + { + "Id": ("d038dccebdb0996ed36ab4ff06e7c424b3816d67664aa11e00642" + "be5e00cec55"), + "Config": { + "Labels": { + "config_data": """{ + \"start_order\": 0, + \"image": \"centos:7\" + }""" + }, + "Image": "127.0.0.1:8787/centos:7" + }, + "Image": "sha256:1" + }, + "sha256:1" + ] + runner.inspect = mock_inspect + mock_remove = mock.MagicMock() + runner.remove_container = mock_remove + + config = { + 'one': { + 'start_order': 0, + 'image': 'centos:7', + } + } + + self.builder = compose1.ComposeV1Builder( + 'one', config, runner.return_value) + + self.builder.runner = runner + self.assertFalse(self.builder.delete_updated('one', [['one']])) + calls = [ + mock.call('one'), + mock.call('127.0.0.1:8787/centos:7', + '{{index .Id}}', + type='image') + ] + mock_inspect.has_calls(calls) + mock_remove.assert_not_called() + + @mock.patch('paunch.runner.DockerRunner', autospec=True) + def test_delete_updated_inspect_empty(self, runner): + mock_inspect = mock.MagicMock() + mock_inspect.return_value = None + runner.inspect = mock_inspect + mock_remove = mock.MagicMock() + runner.remove_container = mock_remove + + config = { + 'one': { + 'start_order': 0, + 'image': 'centos:7', + } + } + + self.builder = compose1.ComposeV1Builder( + 'one', config, runner.return_value) + + self.builder.runner = runner + self.assertTrue(self.builder.delete_updated('one', [['one']])) + mock_inspect.assert_called_once_with('one') + mock_remove.assert_called_once_with('one') + + @mock.patch('paunch.runner.DockerRunner', autospec=True) + def test_delete_updated_no_config_data(self, runner): + mock_inspect = mock.MagicMock() + mock_inspect.side_effect = [ + { + "Id": ("d038dccebdb0996ed36ab4ff06e7c424b3816d67664aa11e00642" + "be5e00cec55"), + "Config": { + "Labels": {}, + "Image": "127.0.0.1:8787/centos:7" + }, + "Image": "sha256:1" + }, + "sha256:1" + ] + runner.inspect = mock_inspect + mock_remove = mock.MagicMock() + runner.remove_container = mock_remove + + config = { + 'one': { + 'start_order': 0, + 'image': 'centos:7', + } + } + + self.builder = compose1.ComposeV1Builder( + 'one', config, runner.return_value) + + self.builder.runner = runner + self.assertTrue(self.builder.delete_updated('one', [['one']])) + mock_inspect.assert_called_once_with('one') + mock_remove.assert_called_once_with('one') + + @mock.patch('paunch.runner.DockerRunner', autospec=True) + def test_delete_updated_update_config(self, runner): + mock_inspect = mock.MagicMock() + mock_inspect.side_effect = [ + { + "Id": ("d038dccebdb0996ed36ab4ff06e7c424b3816d67664aa11e00642" + "be5e00cec55"), + "Config": { + "Labels": { + "config_data": """{ + \"start_order\": 1, + \"image": \"centos:7\" + }""" + }, + "Image": "127.0.0.1:8787/centos:7" + }, + "Image": "sha256:1" + }, + "sha256:1" + ] + + runner.inspect = mock_inspect + mock_remove = mock.MagicMock() + runner.remove_container = mock_remove + + config = { + 'one': { + 'start_order': 0, + 'image': 'centos:7', + } + } + + self.builder = compose1.ComposeV1Builder( + 'one', config, runner.return_value) + + self.builder.runner = runner + self.assertTrue(self.builder.delete_updated('one', [['one']])) + mock_inspect.assert_called_once_with('one') + mock_remove.assert_called_once_with('one') + + @mock.patch('paunch.runner.DockerRunner', autospec=True) + def test_delete_updated_update_image(self, runner): + mock_inspect = mock.MagicMock() + mock_inspect.side_effect = [ + { + "Id": ("d038dccebdb0996ed36ab4ff06e7c424b3816d67664aa11e00642" + "be5e00cec55"), + "Config": { + "Labels": { + "config_data": """{ + \"start_order\": 0, + \"image": \"centos:7\" + }""" + }, + "Image": "127.0.0.1:8787/centos:7" + }, + "Image": "sha256:1" + }, + "sha256:2" + ] + + runner.inspect = mock_inspect + mock_remove = mock.MagicMock() + runner.remove_container = mock_remove + + config = { + 'one': { + 'start_order': 0, + 'image': 'centos:7', + } + } + + self.builder = compose1.ComposeV1Builder( + 'one', config, runner.return_value) + + self.builder.runner = runner + self.assertTrue(self.builder.delete_updated('one', [['one']])) + calls = [ + mock.call('one'), + mock.call('127.0.0.1:8787/centos:7', + '{{index .Id}}', + type='image') + ] + mock_inspect.has_calls(calls) + mock_remove.assert_called_once_with('one')