mirror of
https://github.com/ansible-collections/community.general.git
synced 2024-09-14 20:13:21 +02:00
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>
This commit is contained in:
parent
01bee0c2d9
commit
20b95adf2b
3 changed files with 269 additions and 0 deletions
|
@ -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…
Reference in a new issue