Skip to content

Commit

Permalink
Allow to wait for a container to become healthy.
Browse files Browse the repository at this point in the history
  • Loading branch information
felixfontein committed Jul 7, 2024
1 parent 7ec56d3 commit f2b86f1
Show file tree
Hide file tree
Showing 4 changed files with 115 additions and 13 deletions.
4 changes: 4 additions & 0 deletions changelogs/fragments/921-docker_container-healthy.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
minor_changes:
- "docker_container - the new ``state=healthy`` allows to wait for a container to become healthy on startup.
The ``healthy_wait_timeout`` option allows to configure the maximum time to wait for this to happen
(https://github.com/ansible-collections/community.docker/issues/890, https://github.com/ansible-collections/community.docker/pull/921)."
46 changes: 33 additions & 13 deletions plugins/module_utils/module_container/module.py
Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,9 @@ def __init__(self, module, engine_driver, client, active_options):
self.param_pull_check_mode_behavior = self.module.params['pull_check_mode_behavior']
self.param_recreate = self.module.params['recreate']
self.param_removal_wait_timeout = self.module.params['removal_wait_timeout']
self.param_healthy_wait_timeout = self.module.params['healthy_wait_timeout']
if self.param_healthy_wait_timeout <= 0:
self.param_healthy_wait_timeout = None
self.param_restart = self.module.params['restart']
self.param_state = self.module.params['state']
self._parse_comparisons()
Expand Down Expand Up @@ -212,7 +215,7 @@ def fail(self, *args, **kwargs):
self.client.fail(*args, **kwargs)

def run(self):
if self.param_state in ('stopped', 'started', 'present'):
if self.param_state in ('stopped', 'started', 'present', 'healthy'):
self.present(self.param_state)
elif self.param_state == 'absent':
self.absent()
Expand All @@ -227,29 +230,32 @@ def run(self):
if self.facts:
self.results['container'] = self.facts

def wait_for_state(self, container_id, complete_states=None, wait_states=None, accept_removal=False, max_wait=None):
def wait_for_state(self, container_id, complete_states=None, wait_states=None, accept_removal=False, max_wait=None, health_state=False):
delay = 1.0
total_wait = 0
while True:
# Inspect container
result = self.engine_driver.inspect_container_by_id(self.client, container_id)
if result is None:
if accept_removal:
return
return result
msg = 'Encontered vanished container while waiting for container "{0}"'
self.fail(msg.format(container_id))
# Check container state
state = result.get('State', {}).get('Status')
state_info = result.get('State') or {}
if health_state:
state_info = state_info.get('Health') or {}
state = state_info.get('Status')
if complete_states is not None and state in complete_states:
return
return result
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))
self.fail(msg.format(container_id, state), container=result)
# Wait
if max_wait is not None:
if total_wait > max_wait:
if total_wait > max_wait or delay < 1E-4:
msg = 'Timeout of {1} seconds exceeded while waiting for container "{0}"'
self.fail(msg.format(container_id, max_wait))
self.fail(msg.format(container_id, max_wait), container=result)
if total_wait + delay > max_wait:
delay = max_wait - total_wait
sleep(delay)
Expand Down Expand Up @@ -368,10 +374,10 @@ def present(self, state):
container = self.update_limits(container, container_image, comparison_image, host_info)
container = self.update_networks(container, container_created)

if state == 'started' and not container.running:
if state in ('started', 'healthy') and not container.running:
self.diff_tracker.add('running', parameter=True, active=was_running)
container = self.container_start(container.id)
elif state == 'started' and self.param_restart:
elif state in ('started', 'healthy') and self.param_restart:
self.diff_tracker.add('running', parameter=True, active=was_running)
self.diff_tracker.add('restarted', parameter=True, active=False)
container = self.container_restart(container.id)
Expand All @@ -380,7 +386,7 @@ def present(self, state):
self.container_stop(container.id)
container = self._get_container(container.id)

if state == 'started' and self.param_paused is not None and container.paused != self.param_paused:
if state in ('started', 'healthy') and self.param_paused is not None and container.paused != self.param_paused:
self.diff_tracker.add('paused', parameter=self.param_paused, active=was_paused)
if not self.check_mode:
try:
Expand All @@ -398,6 +404,19 @@ def present(self, state):

self.facts = container.raw

if state == 'healthy' and not self.check_mode:
# `None` means that no health check enabled; simply treat this as 'healthy'
inspect_result = self.wait_for_state(
container.id,
wait_states=['starting', 'unhealthy'],
complete_states=['healthy', None],
max_wait=self.param_healthy_wait_timeout,
health_state=True,
)
if inspect_result:
# Return the latest inspection results retrieved
self.facts = inspect_result

def absent(self):
container = self._get_container(self.param_name)
if container.exists:
Expand Down Expand Up @@ -878,10 +897,11 @@ def run_module(engine_driver):
recreate=dict(type='bool', default=False),
removal_wait_timeout=dict(type='float'),
restart=dict(type='bool', default=False),
state=dict(type='str', default='started', choices=['absent', 'present', 'started', 'stopped']),
state=dict(type='str', default='started', choices=['absent', 'present', 'healthy', 'started', 'stopped']),
healthy_wait_timeout=dict(type='float', default=300),
),
required_if=[
('state', 'present', ['image'])
('state', 'present', ['image']),
],
)

Expand Down
14 changes: 14 additions & 0 deletions plugins/modules/docker_container.py
Original file line number Diff line number Diff line change
Expand Up @@ -381,6 +381,7 @@
- "O(healthcheck.interval), O(healthcheck.timeout), O(healthcheck.start_period), and O(healthcheck.start_interval) are specified as durations.
They accept duration as a string in a format that look like: V(5h34m56s), V(1m30s), and so on.
The supported units are V(us), V(ms), V(s), V(m) and V(h)."
- See also O(state=healthy).
type: dict
suboptions:
test:
Expand Down Expand Up @@ -919,6 +920,8 @@
with the requested config.'
- 'V(started) - Asserts that the container is first V(present), and then if the container is not running moves it to a running
state. Use O(restart) to force a matching container to be stopped and restarted.'
- V(healthy) - Asserts that the container is V(present) and V(started), and is actually healthy as well.
The time waited can be controlled with O(healthy_wait_timeout). This state has been added in community.docker 3.11.0.
- 'V(stopped) - Asserts that the container is first V(present), and then if the container is running moves it to a stopped
state.'
- "To control what will be taken into account when comparing configuration, see the O(comparisons) option. To avoid that the
Expand All @@ -932,12 +935,23 @@
choices:
- absent
- present
- healthy
- stopped
- started
stop_signal:
description:
- Override default signal used to stop the container.
type: str
healthy_wait_timeout:
description:
- When waiting for the container to become healthy if O(state=healthy), this option controls for how long
the module waits until the container's state becomes healthy.
- The timeout is specified in seconds. The default, V(300), is 5 minutes.
- Set this to 0 or a negative value to wait however long it takes.
Note that depending on the container this can result in the module not terminating.
default: 300
type: float
version_added: 3.11.0
stop_timeout:
description:
- Number of seconds to wait for the container to stop before sending C(SIGKILL).
Expand Down
64 changes: 64 additions & 0 deletions tests/integration/targets/docker_container/tasks/tests/healthy.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
---
# Copyright (c) Ansible Project
# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt)
# SPDX-License-Identifier: GPL-3.0-or-later

- name: Registering container name
set_fact:
cname: "{{ cname_prefix ~ '-hi' }}"
- name: Registering container name
set_fact:
cnames: "{{ cnames + [cname] }}"

- name: Prepare container
docker_container:
name: "{{ cname }}"
image: "{{ docker_test_image_healthcheck }}"
command: '10m'
state: stopped
register: healthy_1

- debug: var=healthy_1.container.State

- name: Start container (not healthy in time)
docker_container:
name: "{{ cname }}"
state: healthy
healthy_wait_timeout: 1
register: healthy_2
ignore_errors: true

- debug: var=healthy_2.container.State

- name: Prepare container
docker_container:
name: "{{ cname }}"
image: "{{ docker_test_image_healthcheck }}"
command: '10m 5s'
state: stopped
force_kill: true
register: healthy_3

- debug: var=healthy_3.container.State

- name: Start container (healthy in time)
docker_container:
name: "{{ cname }}"
state: healthy
healthy_wait_timeout: 10
register: healthy_4

- debug: var=healthy_4.container.State

- name: Cleanup
docker_container:
name: "{{ cname }}"
state: absent
force_kill: true
- assert:
that:
- healthy_2 is failed
- healthy_2.container.State.Health.Status == "starting"
- healthy_2.msg.startswith("Timeout of 1.0 seconds exceeded while waiting for container ")
- healthy_4 is changed
- healthy_4.container.State.Health.Status == "healthy"

0 comments on commit f2b86f1

Please sign in to comment.