From dbc76b4dc98634cf82cdf5dc43ee7bd73a72d556 Mon Sep 17 00:00:00 2001 From: Felix Fontein Date: Sun, 29 Dec 2019 23:16:32 +0100 Subject: [PATCH 1/2] docker_container: wait for removal if removal is in process (#65854) * Allow to inspect containers directly. * Wait for containers to be removed before recreating them. * Also wait for containers to be removed before creating them. * Add changelog. (cherry picked from commit 4df5bdb11eaed6e9d23d1de0ff7f420409cc1324) --- ...5854-docker_container-wait-for-removal.yml | 2 + lib/ansible/module_utils/docker/common.py | 24 ++++++----- .../modules/cloud/docker/docker_container.py | 43 ++++++++++++++++++- 3 files changed, 57 insertions(+), 12 deletions(-) create mode 100644 changelogs/fragments/65854-docker_container-wait-for-removal.yml diff --git a/changelogs/fragments/65854-docker_container-wait-for-removal.yml b/changelogs/fragments/65854-docker_container-wait-for-removal.yml new file mode 100644 index 00000000000000..5c992a5edd32cc --- /dev/null +++ b/changelogs/fragments/65854-docker_container-wait-for-removal.yml @@ -0,0 +1,2 @@ +bugfixes: +- "docker_container - wait for removal of container if docker API returns early (https://github.com/ansible/ansible/issues/65811)." diff --git a/lib/ansible/module_utils/docker/common.py b/lib/ansible/module_utils/docker/common.py index 18d8d746db7ce2..17ee074ac63abf 100644 --- a/lib/ansible/module_utils/docker/common.py +++ b/lib/ansible/module_utils/docker/common.py @@ -513,6 +513,17 @@ def _get_minimal_versions(self, option_minimal_versions, ignore_params=None): msg = 'Cannot %s with your configuration.' % (usg, ) self.fail(msg) + def get_container_by_id(self, container_id): + try: + self.log("Inspecting container Id %s" % container_id) + result = self.inspect_container(container=container_id) + self.log("Completed container inspection") + return result + except NotFound as dummy: + return None + except Exception as exc: + self.fail("Error inspecting container: %s" % exc) + def get_container(self, name=None): ''' Lookup a container and return the inspection results. @@ -542,17 +553,10 @@ def get_container(self, name=None): except Exception as exc: self.fail("Error retrieving container list: %s" % exc) - if result is not None: - try: - self.log("Inspecting container Id %s" % result['Id']) - result = self.inspect_container(container=result['Id']) - self.log("Completed container inspection") - except NotFound as dummy: - return None - except Exception as exc: - self.fail("Error inspecting container: %s" % exc) + if result is None: + return None - return result + return self.get_container_by_id(result['Id']) def get_network(self, name=None, id=None): ''' diff --git a/lib/ansible/modules/cloud/docker/docker_container.py b/lib/ansible/modules/cloud/docker/docker_container.py index 34731e22e881de..651c92daddb694 100644 --- a/lib/ansible/modules/cloud/docker/docker_container.py +++ b/lib/ansible/modules/cloud/docker/docker_container.py @@ -960,6 +960,7 @@ import shlex import traceback from distutils.version import LooseVersion +from time import sleep from ansible.module_utils.common.text.formatters import human_to_bytes from ansible.module_utils.docker.common import ( @@ -1765,6 +1766,12 @@ def fail(self, msg): def exists(self): return True if self.container else False + @property + def removing(self): + if self.container and self.container.get('State'): + return self.container['State'].get('Status') == 'removing' + return False + @property def running(self): if self.container and self.container.get('State'): @@ -2332,6 +2339,31 @@ def __init__(self, client): self.results['ansible_facts'] = {'docker_container': self.facts} self.results['container'] = self.facts + def wait_for_state(self, container_id, complete_states=None, wait_states=None, accept_removal=False): + delay = 1.0 + while True: + # Inspect container + result = self.client.get_container_by_id(container_id) + if result is None: + if accept_removal: + return + msg = 'Encontered vanished container while waiting for container {0}' + self.fail(msg.format(container_id)) + # Check container state + state = result.get('State', {}).get('Status') + if complete_states is not None and state in complete_states: + return + if wait_states is not None and state not in wait_states: + msg = 'Encontered unexpected state "{1}" while waiting for container {0}' + self.fail(msg.format(container_id, state)) + # Wait + sleep(delay) + # Exponential backoff, but never wait longer than 10 seconds + # (1.1**24 < 10, 1.1**25 > 10, so it will take 25 iterations + # until the maximal 10 seconds delay is reached. By then, the + # code will have slept for ~1.5 minutes.) + delay = min(delay * 1.1, 10) + def present(self, state): container = self._get_container(self.parameters.name) was_running = container.running @@ -2345,12 +2377,18 @@ def present(self, state): # image ID. image = self._get_image() self.log(image, pretty_print=True) - if not container.exists: + if not container.exists or container.removing: # New container - self.log('No container found') + if container.removing: + self.log('Found container in removal phase') + else: + self.log('No container found') if not self.parameters.image: self.fail('Cannot create container when image is not specified!') self.diff_tracker.add('exists', parameter=True, active=False) + if container.removing: + # Wait for container to be removed before trying to create it + self.wait_for_state(container.Id, wait_states=['removing'], accept_removal=True) new_container = self.container_create(self.parameters.image, self.parameters.create_parameters) if new_container: container = new_container @@ -2376,6 +2414,7 @@ def present(self, state): if container.running: self.container_stop(container.Id) self.container_remove(container.Id) + self.wait_for_state(container.Id, wait_states=['removing'], accept_removal=True) new_container = self.container_create(image_to_use, self.parameters.create_parameters) if new_container: container = new_container From 0e2e9b74dd8dd1f05a72d71db7cc13fe0272ff3c Mon Sep 17 00:00:00 2001 From: Felix Fontein Date: Thu, 2 Jan 2020 17:51:52 +0100 Subject: [PATCH 2/2] Don't wait for removal during check mode. (#66145) (cherry picked from commit 14e32c85b47673a5fcade08c7bd84a3aac032e12) --- lib/ansible/modules/cloud/docker/docker_container.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/lib/ansible/modules/cloud/docker/docker_container.py b/lib/ansible/modules/cloud/docker/docker_container.py index 651c92daddb694..35aa926afd2f1d 100644 --- a/lib/ansible/modules/cloud/docker/docker_container.py +++ b/lib/ansible/modules/cloud/docker/docker_container.py @@ -2386,7 +2386,7 @@ def present(self, state): if not self.parameters.image: self.fail('Cannot create container when image is not specified!') self.diff_tracker.add('exists', parameter=True, active=False) - if container.removing: + if container.removing and not self.check_mode: # Wait for container to be removed before trying to create it self.wait_for_state(container.Id, wait_states=['removing'], accept_removal=True) new_container = self.container_create(self.parameters.image, self.parameters.create_parameters) @@ -2414,7 +2414,8 @@ def present(self, state): if container.running: self.container_stop(container.Id) self.container_remove(container.Id) - self.wait_for_state(container.Id, wait_states=['removing'], accept_removal=True) + if not self.check_mode: + self.wait_for_state(container.Id, wait_states=['removing'], accept_removal=True) new_container = self.container_create(image_to_use, self.parameters.create_parameters) if new_container: container = new_container