Add Support of healthcheck in docker_container module (#46772)

* Add Support of healthcheck in docker_container module

Fixes #33622
Now container can be started with healthcheck enabled

Signed-off-by: Akshay Gaikwad <akgaikwad001@gmail.com>

* Extend docker_container healthcheck (#1)

* Allowing to disable healthcheck.

* Added test for healthcheck.

* Make sure correct types are used.

* Healthcheck needs to be explicitly disabled with test: ['NONE'].

* pep8 fixes

Signed-off-by: Akshay Gaikwad <akgaikwad001@gmail.com>

* Fix bug if healthcheck interval is 1 day or more

`timedelta` object has days too and seconds are up to one day.
Therefore use `total_seconds()` to convert time into seconds.

Signed-off-by: Akshay Gaikwad <akgaikwad001@gmail.com>

* Add test for healthcheck when healthcheck is not specified

This is to avoid the situation when healthcheck is not specified and
treat this as healthcheck is changed or removed.

Signed-off-by: Akshay Gaikwad <akgaikwad001@gmail.com>

* Convert string syntax for healthcheck test to CMD-SHELL

Also add another test case to check idempotency when healthcheck test
is specified as string

Signed-off-by: Akshay Gaikwad <akgaikwad001@gmail.com>

* Playbook fails if minimun docker version is not satisfy for healthcheck

This is to make more consistent with other non-supported options.

Signed-off-by: Akshay Gaikwad <akgaikwad001@gmail.com>
pull/47557/head
Akshay Gaikwad 6 years ago committed by John R Barker
parent 01bee0c2d9
commit 20b95adf2b

@ -0,0 +1,2 @@
minor_changes:
- "docker_container - Added support for healthcheck."

@ -147,6 +147,21 @@ options:
groups:
description:
- List of additional group names and/or IDs that the container process will run as.
healthcheck:
version_added: "2.8"
type: dict
description:
- 'Configure a check that is run to determine whether or not containers for this service are "healthy".
See the docs for the L(HEALTHCHECK Dockerfile instruction,https://docs.docker.com/engine/reference/builder/#healthcheck)
for details on how healthchecks work.'
- 'I(test) - Command to run to check health. C(test) must be either a string or a list. If it is a list, the first item must
be one of C(NONE), C(CMD) or C(CMD-SHELL).'
- 'I(interval) - Time between running the check. (default: 30s)'
- 'I(timeout) - Maximum time to allow one check to run. (default: 30s)'
- 'I(retries) - Consecutive failures needed to report unhealthy. It accept integer value. (default: 3)'
- 'I(start_period) - Start period for the container to initialize before starting health-retries countdown. (default: 0s)'
- 'C(interval), C(timeout) and C(start_period) are specified as durations. They accept duration as a string in a format
that look like: C(5h34m56s), C(1m30s) etc. The supported units are C(us), C(ms), C(s), C(m) and C(h)'
hostname:
description:
- Container hostname.
@ -659,6 +674,29 @@ EXAMPLES = '''
comparisons:
'*': ignore # by default, ignore *all* options (including image)
env: strict # except for environment variables; there, we want to be strict
- name: Start container with healthstatus
docker_container:
name: nginx-proxy
image: nginx:1.13
state: started
healthcheck:
# Check if nginx server is healthy by curl'ing the server.
# If this fails or timeouts, the healthcheck fails.
test: ["CMD", "curl", "--fail", "http://nginx.host.com"]
interval: 1m30s
timeout: 10s
retries: 3
start_period: 30s
- name: Remove healthcheck from container
docker_container:
name: nginx-proxy
image: nginx:1.13
state: started
healthcheck:
# The "NONE" check needs to be specified
test: ["NONE"]
'''
RETURN = '''
@ -708,6 +746,7 @@ docker_container:
import os
import re
import shlex
from datetime import timedelta
from distutils.version import LooseVersion
from ansible.module_utils.basic import human_to_bytes
@ -824,6 +863,7 @@ class TaskParameters(DockerBaseClass):
self.exposed_ports = None
self.force_kill = None
self.groups = None
self.healthcheck = None
self.hostname = None
self.ignore_image = None
self.image = None
@ -917,6 +957,7 @@ class TaskParameters(DockerBaseClass):
self.ulimits = self._parse_ulimits()
self.sysctls = self._parse_sysctls()
self.log_config = self._parse_log_config()
self.healthcheck, self.disable_healthcheck = self._parse_healthcheck()
self.exp_links = None
self.volume_binds = self._get_volume_binds(self.volumes)
@ -1012,6 +1053,9 @@ class TaskParameters(DockerBaseClass):
if self.client.HAS_STOP_TIMEOUT_OPT:
create_params['stop_timeout'] = 'stop_timeout'
if self.client.HAS_HEALTHCHECK_OPT:
create_params['healthcheck'] = 'healthcheck'
result = dict(
host_config=self._host_config(),
volumes=self._get_mounts(),
@ -1330,6 +1374,77 @@ class TaskParameters(DockerBaseClass):
except ValueError as exc:
self.fail('Error parsing logging options - %s' % (exc))
def _parse_healthcheck(self):
'''
Return dictionary of healthcheck parameters
'''
if (not self.healthcheck) or (not self.healthcheck.get('test')):
return None, None
result = dict()
# all the supported healthecheck parameters
options = dict(
test='test',
interval='interval',
timeout='timeout',
start_period='start_period',
retries='retries'
)
duration_options = ['interval', 'timeout', 'start_period']
for (key, value) in options.items():
if value in self.healthcheck:
if value in duration_options:
time = self._convert_duration_to_nanosecond(self.healthcheck.get(value))
if time:
result[key] = time
elif self.healthcheck.get(value):
result[key] = self.healthcheck.get(value)
if key == 'test':
if isinstance(result[key], (tuple, list)):
result[key] = [str(e) for e in result[key]]
else:
result[key] = ["CMD-SHELL", str(result[key])]
elif key == 'retries':
try:
result[key] = int(result[key])
except Exception as e:
self.fail('Cannot parse number of retries for healthcheck. '
'Expected an integer, got "{0}".'.format(result[key]))
if result['test'] == ['NONE']:
# If the user explicitly disables the healthcheck, return None
# as the healthcheck object, and set disable_healthcheck to True
return None, True
return result, False
def _convert_duration_to_nanosecond(self, time_str):
'''
Return time duration in nanosecond
'''
if not isinstance(time_str, str):
self.fail("Missing unit in duration - %s" % time_str)
regex = re.compile(r'^(((?P<hours>\d+)h)?((?P<minutes>\d+)m(?!s))?((?P<seconds>\d+)s)?((?P<milliseconds>\d+)ms)?((?P<microseconds>\d+)us)?)$')
parts = regex.match(time_str)
if not parts:
self.fail("Invalid time duration - %s" % time_str)
parts = parts.groupdict()
time_params = {}
for (name, value) in parts.items():
if value:
time_params[name] = int(value)
time = timedelta(**time_params)
time_in_nanoseconds = int(time.total_seconds() * 1000000000)
return time_in_nanoseconds
def _parse_tmpfs(self):
'''
Turn tmpfs into a hash of Tmpfs objects
@ -1406,6 +1521,7 @@ class Container(DockerBaseClass):
self.parameters_map['expected_binds'] = 'volumes'
self.parameters_map['expected_cmd'] = 'command'
self.parameters_map['expected_devices'] = 'devices'
self.parameters_map['expected_healthcheck'] = 'healthcheck'
def fail(self, msg):
self.parameters.client.module.fail_json(msg=msg)
@ -1512,6 +1628,7 @@ class Container(DockerBaseClass):
self.parameters.expected_env = self._get_expected_env(image)
self.parameters.expected_cmd = self._get_expected_cmd()
self.parameters.expected_devices = self._get_expected_devices()
self.parameters.expected_healthcheck = self._get_expected_healthcheck()
if not self.container.get('HostConfig'):
self.fail("has_config_diff: Error parsing container properties. HostConfig missing.")
@ -1584,6 +1701,8 @@ class Container(DockerBaseClass):
volumes_from=host_config.get('VolumesFrom'),
working_dir=config.get('WorkingDir'),
publish_all_ports=host_config.get('PublishAllPorts'),
expected_healthcheck=config.get('Healthcheck'),
disable_healthcheck=(not config.get('Healthcheck') or config.get('Healthcheck').get('Test') == ['NONE']),
)
if self.parameters.restart_policy:
config_mapping['restart_retries'] = restart_policy.get('MaximumRetryCount')
@ -1966,6 +2085,16 @@ class Container(DockerBaseClass):
return port + '/tcp'
return port
def _get_expected_healthcheck(self):
self.log('_get_expected_healthcheck')
expected_healthcheck = dict()
if self.parameters.healthcheck:
expected_healthcheck.update([(k.title().replace("_", ""), v)
for k, v in self.parameters.healthcheck.items()])
return expected_healthcheck
class ContainerManager(DockerBaseClass):
'''
@ -1978,6 +2107,8 @@ class ContainerManager(DockerBaseClass):
if client.module.params.get('log_options') and not client.module.params.get('log_driver'):
client.module.warn('log_options is ignored when log_driver is not specified')
if client.module.params.get('healthcheck') and not client.module.params.get('healthcheck').get('test'):
client.module.warn('healthcheck is ignored when test is not specified')
if client.module.params.get('restart_retries') and not client.module.params.get('restart_policy'):
client.module.warn('restart_retries is ignored when restart_policy is not specified')
@ -2385,6 +2516,9 @@ class AnsibleDockerClientContainer(AnsibleDockerClient):
# Add implicit options
comparisons['publish_all_ports'] = dict(type='value', comparison='strict', name='published_ports')
comparisons['expected_ports'] = dict(type='dict', comparison=comparisons['published_ports']['comparison'], name='expected_ports')
comparisons['disable_healthcheck'] = dict(type='value',
comparison='ignore' if comparisons['healthcheck']['comparison'] == 'ignore' else 'strict',
name='disable_healthcheck')
# Check legacy values
if self.module.params['ignore_image'] and comparisons['image']['comparison'] != 'ignore':
self.module.warn('The ignore_image option has been overridden by the comparisons option!')
@ -2448,11 +2582,16 @@ class AnsibleDockerClientContainer(AnsibleDockerClient):
if self.module.params.get("runtime") and not runtime_supported:
self.fail('docker API version is %s. Minimum version required is 1.12 to set runtime option.' % (docker_api_version,))
healthcheck_supported = LooseVersion(docker_version) >= LooseVersion('2.0')
if self.module.params.get("healthcheck") and not healthcheck_supported:
self.fail("docker or docker-py version is %s. Minimum version required is 2.0 to set healthcheck option." % (docker_version,))
self.HAS_INIT_OPT = init_supported
self.HAS_UTS_MODE_OPT = uts_mode_supported
self.HAS_BLKIO_WEIGHT_OPT = blkio_weight_supported
self.HAS_CPUSET_MEMS_OPT = cpuset_mems_supported
self.HAS_STOP_TIMEOUT_OPT = stop_timeout_supported
self.HAS_HEALTHCHECK_OPT = healthcheck_supported
self.HAS_AUTO_REMOVE_OPT = HAS_DOCKER_PY_2 or HAS_DOCKER_PY_3
self.HAS_RUNTIME_OPT = runtime_supported
@ -2490,6 +2629,7 @@ def main():
exposed_ports=dict(type='list', aliases=['exposed', 'expose']),
force_kill=dict(type='bool', default=False, aliases=['forcekill']),
groups=dict(type='list'),
healthcheck=dict(type='dict'),
hostname=dict(type='str'),
ignore_image=dict(type='bool', default=False),
image=dict(type='str'),

@ -1182,6 +1182,133 @@
- groups_3 is not changed
- groups_4 is changed
####################################################################
## healthcheck #####################################################
####################################################################
- name: healthcheck
docker_container:
image: alpine:3.8
command: '/bin/sh -c "sleep 10m"'
name: "{{ cname }}"
state: started
healthcheck:
test:
- CMD
- sleep
- 1
timeout: 2s
interval: 0h0m2s3ms4us
retries: 2
stop_timeout: 1
register: healthcheck_1
- name: healthcheck (idempotency)
docker_container:
image: alpine:3.8
command: '/bin/sh -c "sleep 10m"'
name: "{{ cname }}"
state: started
healthcheck:
test:
- CMD
- sleep
- 1
timeout: 2s
interval: 0h0m2s3ms4us
retries: 2
stop_timeout: 1
register: healthcheck_2
- name: healthcheck (changed)
docker_container:
image: alpine:3.8
command: '/bin/sh -c "sleep 10m"'
name: "{{ cname }}"
state: started
healthcheck:
test:
- CMD
- sleep
- 1
timeout: 3s
interval: 0h1m2s3ms4us
retries: 3
stop_timeout: 1
register: healthcheck_3
- name: healthcheck (no change)
docker_container:
image: alpine:3.8
command: '/bin/sh -c "sleep 10m"'
name: "{{ cname }}"
state: started
stop_timeout: 1
register: healthcheck_4
- name: healthcheck (disabled)
docker_container:
image: alpine:3.8
command: '/bin/sh -c "sleep 10m"'
name: "{{ cname }}"
state: started
healthcheck:
test:
- NONE
stop_timeout: 1
register: healthcheck_5
- name: healthcheck (disabled, idempotency)
docker_container:
image: alpine:3.8
command: '/bin/sh -c "sleep 10m"'
name: "{{ cname }}"
state: started
healthcheck:
test:
- NONE
stop_timeout: 1
register: healthcheck_6
- name: healthcheck (string in healthcheck test, changed)
docker_container:
image: alpine:3.8
command: '/bin/sh -c "sleep 10m"'
name: "{{ cname }}"
state: started
healthcheck:
test: "sleep 1"
stop_timeout: 1
register: healthcheck_7
- name: healthcheck (string in healthcheck test, idempotency)
docker_container:
image: alpine:3.8
command: '/bin/sh -c "sleep 10m"'
name: "{{ cname }}"
state: started
healthcheck:
test: "sleep 1"
stop_timeout: 1
register: healthcheck_8
- name: cleanup
docker_container:
name: "{{ cname }}"
state: absent
stop_timeout: 1
- assert:
that:
- healthcheck_1 is changed
- healthcheck_2 is not changed
- healthcheck_3 is changed
- healthcheck_4 is not changed
- healthcheck_5 is changed
- healthcheck_6 is not changed
- healthcheck_7 is changed
- healthcheck_8 is not changed
####################################################################
## hostname ########################################################
####################################################################

Loading…
Cancel
Save