Browse Source

Trigger container update on image id update

NOTE: Adapted to queens due to missing backports in rocky.

If you reuse a container tag for a new image, paunch does not currently
rebuild the container to get the new image.  This change adds logic to
the updated check to also check the image id defined in the container
instance against the image id for the image to see if the container
image has changed.

Closes-Bug: #1895974
Change-Id: I2710497282eb4d6fa8f68dc6d50044c1ce68eb35
(cherry picked from commit 4ae7d886db)
changes/69/752569/2
Alex Schultz 7 months ago
parent
commit
be9f6a3c46
2 changed files with 223 additions and 10 deletions
  1. +29
    -6
      paunch/builder/compose1.py
  2. +194
    -4
      paunch/tests/test_builder_compose1.py

+ 29
- 6
paunch/builder/compose1.py View File

@ -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:
self.log.debug("Deleting container (no_config_data): "
# 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(ex_data_str))
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
# 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):


+ 194
- 4
paunch/tests/test_builder_compose1.py View File

@ -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')

Loading…
Cancel
Save