From b4f2b5fa70a513a89df08a100cce63a9727b6580 Mon Sep 17 00:00:00 2001 From: Jamie Greeff Date: Thu, 4 Aug 2016 15:17:06 +0100 Subject: [PATCH 1/3] Add support for passing healthcheck to create_container Signed-off-by: Jamie Greeff --- docker/api/container.py | 7 ++-- docker/types/__init__.py | 1 + docker/types/healthcheck.py | 47 +++++++++++++++++++++++++++ docker/utils/utils.py | 9 ++++- tests/integration/healthcheck_test.py | 35 ++++++++++++++++++++ 5 files changed, 95 insertions(+), 4 deletions(-) create mode 100644 docker/types/healthcheck.py create mode 100644 tests/integration/healthcheck_test.py diff --git a/docker/api/container.py b/docker/api/container.py index d71d17ad..338b79fe 100644 --- a/docker/api/container.py +++ b/docker/api/container.py @@ -115,7 +115,8 @@ class ContainerApiMixin(object): cpu_shares=None, working_dir=None, domainname=None, memswap_limit=None, cpuset=None, host_config=None, mac_address=None, labels=None, volume_driver=None, - stop_signal=None, networking_config=None): + stop_signal=None, networking_config=None, + healthcheck=None): if isinstance(volumes, six.string_types): volumes = [volumes, ] @@ -130,7 +131,7 @@ class ContainerApiMixin(object): tty, mem_limit, ports, environment, dns, volumes, volumes_from, network_disabled, entrypoint, cpu_shares, working_dir, domainname, memswap_limit, cpuset, host_config, mac_address, labels, - volume_driver, stop_signal, networking_config, + volume_driver, stop_signal, networking_config, healthcheck, ) return self.create_container_from_config(config, name) @@ -365,7 +366,7 @@ class ContainerApiMixin(object): cap_drop=cap_drop, volumes_from=volumes_from, devices=devices, network_mode=network_mode, restart_policy=restart_policy, extra_hosts=extra_hosts, read_only=read_only, pid_mode=pid_mode, - ipc_mode=ipc_mode, security_opt=security_opt, ulimits=ulimits + ipc_mode=ipc_mode, security_opt=security_opt, ulimits=ulimits, ) start_config = None diff --git a/docker/types/__init__.py b/docker/types/__init__.py index 71c0c974..a7c3a56b 100644 --- a/docker/types/__init__.py +++ b/docker/types/__init__.py @@ -4,4 +4,5 @@ from .services import ( ContainerSpec, DriverConfig, EndpointSpec, Mount, Resources, RestartPolicy, TaskTemplate, UpdateConfig ) +from .healthcheck import Healthcheck from .swarm import SwarmSpec, SwarmExternalCA diff --git a/docker/types/healthcheck.py b/docker/types/healthcheck.py new file mode 100644 index 00000000..8405869e --- /dev/null +++ b/docker/types/healthcheck.py @@ -0,0 +1,47 @@ +from .base import DictType + + +class Healthcheck(DictType): + def __init__(self, **kwargs): + test = kwargs.get('test', kwargs.get('Test')) + interval = kwargs.get('interval', kwargs.get('Interval')) + timeout = kwargs.get('timeout', kwargs.get('Timeout')) + retries = kwargs.get('retries', kwargs.get('Retries')) + super(Healthcheck, self).__init__({ + 'Test': test, + 'Interval': interval, + 'Timeout': timeout, + 'Retries': retries + }) + + @property + def test(self): + return self['Test'] + + @test.setter + def test(self, value): + self['Test'] = value + + @property + def interval(self): + return self['Interval'] + + @interval.setter + def interval(self, value): + self['Interval'] = value + + @property + def timeout(self): + return self['Timeout'] + + @timeout.setter + def timeout(self, value): + self['Timeout'] = value + + @property + def retries(self): + return self['Retries'] + + @retries.setter + def retries(self, value): + self['Retries'] = value diff --git a/docker/utils/utils.py b/docker/utils/utils.py index 97261cdc..ee324db5 100644 --- a/docker/utils/utils.py +++ b/docker/utils/utils.py @@ -1029,6 +1029,7 @@ def create_container_config( entrypoint=None, cpu_shares=None, working_dir=None, domainname=None, memswap_limit=None, cpuset=None, host_config=None, mac_address=None, labels=None, volume_driver=None, stop_signal=None, networking_config=None, + healthcheck=None, ): if isinstance(command, six.string_types): command = split_command(command) @@ -1057,6 +1058,11 @@ def create_container_config( 'stop_signal was only introduced in API version 1.21' ) + if healthcheck is not None and version_lt(version, '1.24'): + raise errors.InvalidVersion( + 'Health options were only introduced in API version 1.24' + ) + if compare_version('1.19', version) < 0: if volume_driver is not None: raise errors.InvalidVersion( @@ -1164,5 +1170,6 @@ def create_container_config( 'MacAddress': mac_address, 'Labels': labels, 'VolumeDriver': volume_driver, - 'StopSignal': stop_signal + 'StopSignal': stop_signal, + 'Healthcheck': healthcheck, } diff --git a/tests/integration/healthcheck_test.py b/tests/integration/healthcheck_test.py new file mode 100644 index 00000000..b1a7e4ac --- /dev/null +++ b/tests/integration/healthcheck_test.py @@ -0,0 +1,35 @@ +import time +import docker + +from ..base import requires_api_version +from .. import helpers + + +class HealthcheckTest(helpers.BaseTestCase): + + @requires_api_version('1.21') + def test_healthcheck(self): + healthcheck = docker.types.Healthcheck( + test=["CMD-SHELL", + "foo.txt || (/bin/usleep 10000 && touch foo.txt)"], + interval=500000, + timeout=1000000000, + retries=1 + ) + container = self.client.create_container(helpers.BUSYBOX, 'cat', + detach=True, stdin_open=True, + healthcheck=healthcheck) + id = container['Id'] + self.client.start(id) + self.tmp_containers.append(id) + res1 = self.client.inspect_container(id) + self.assertIn('State', res1) + self.assertIn('Health', res1['State']) + self.assertIn('Status', res1['State']['Health']) + self.assertEqual(res1['State']['Health']['Status'], "starting") + time.sleep(0.5) + res2 = self.client.inspect_container(id) + self.assertIn('State', res2) + self.assertIn('Health', res2['State']) + self.assertIn('Status', res2['State']['Health']) + self.assertEqual(res2['State']['Health']['Status'], "healthy") From 6bb7844ab395ffa8d9ed9d5206871849d6a6f319 Mon Sep 17 00:00:00 2001 From: Aanand Prasad Date: Fri, 11 Nov 2016 12:59:47 +0000 Subject: [PATCH 2/3] Rework healthcheck integration test Signed-off-by: Aanand Prasad --- tests/helpers.py | 9 ++++ tests/integration/healthcheck_test.py | 75 +++++++++++++++++---------- 2 files changed, 57 insertions(+), 27 deletions(-) diff --git a/tests/helpers.py b/tests/helpers.py index 529b727a..f8b3e613 100644 --- a/tests/helpers.py +++ b/tests/helpers.py @@ -2,6 +2,7 @@ import os import os.path import tarfile import tempfile +import time import docker import pytest @@ -47,3 +48,11 @@ def requires_api_version(version): ), reason="API version is too low (< {0})".format(version) ) + + +def wait_on_condition(condition, delay=0.1, timeout=40): + start_time = time.time() + while not condition(): + if time.time() - start_time > timeout: + raise AssertionError("Timeout: %s" % condition) + time.sleep(delay) diff --git a/tests/integration/healthcheck_test.py b/tests/integration/healthcheck_test.py index b1a7e4ac..9df8cd76 100644 --- a/tests/integration/healthcheck_test.py +++ b/tests/integration/healthcheck_test.py @@ -1,35 +1,56 @@ -import time import docker -from ..base import requires_api_version +from .base import BaseIntegrationTest +from .base import BUSYBOX from .. import helpers +SECOND = 1000000000 -class HealthcheckTest(helpers.BaseTestCase): - @requires_api_version('1.21') - def test_healthcheck(self): +class HealthcheckTest(BaseIntegrationTest): + + @helpers.requires_api_version('1.24') + def test_healthcheck_passes(self): healthcheck = docker.types.Healthcheck( - test=["CMD-SHELL", - "foo.txt || (/bin/usleep 10000 && touch foo.txt)"], - interval=500000, - timeout=1000000000, - retries=1 + test=["CMD-SHELL", "true"], + interval=1*SECOND, + timeout=1*SECOND, + retries=1, ) - container = self.client.create_container(helpers.BUSYBOX, 'cat', - detach=True, stdin_open=True, - healthcheck=healthcheck) - id = container['Id'] - self.client.start(id) - self.tmp_containers.append(id) - res1 = self.client.inspect_container(id) - self.assertIn('State', res1) - self.assertIn('Health', res1['State']) - self.assertIn('Status', res1['State']['Health']) - self.assertEqual(res1['State']['Health']['Status'], "starting") - time.sleep(0.5) - res2 = self.client.inspect_container(id) - self.assertIn('State', res2) - self.assertIn('Health', res2['State']) - self.assertIn('Status', res2['State']['Health']) - self.assertEqual(res2['State']['Health']['Status'], "healthy") + container = self.client.create_container( + BUSYBOX, 'top', healthcheck=healthcheck) + self.tmp_containers.append(container) + + res = self.client.inspect_container(container) + assert res['Config']['Healthcheck'] == { + "Test": ["CMD-SHELL", "true"], + "Interval": 1*SECOND, + "Timeout": 1*SECOND, + "Retries": 1, + } + + def condition(): + res = self.client.inspect_container(container) + return res['State']['Health']['Status'] == "healthy" + + self.client.start(container) + helpers.wait_on_condition(condition) + + @helpers.requires_api_version('1.24') + def test_healthcheck_fails(self): + healthcheck = docker.types.Healthcheck( + test=["CMD-SHELL", "false"], + interval=1*SECOND, + timeout=1*SECOND, + retries=1, + ) + container = self.client.create_container( + BUSYBOX, 'top', healthcheck=healthcheck) + self.tmp_containers.append(container) + + def condition(): + res = self.client.inspect_container(container) + return res['State']['Health']['Status'] == "unhealthy" + + self.client.start(container) + helpers.wait_on_condition(condition) From e4b6d0dca6d667c6defd10a9d507f7b4c11e6eb2 Mon Sep 17 00:00:00 2001 From: Aanand Prasad Date: Fri, 11 Nov 2016 17:32:42 +0000 Subject: [PATCH 3/3] Convert dicts to Healthcheck objects, string commands to CMD-SHELL lists Signed-off-by: Aanand Prasad --- docker/types/healthcheck.py | 6 +++ docker/utils/utils.py | 5 ++- tests/integration/healthcheck_test.py | 63 ++++++++++++--------------- 3 files changed, 39 insertions(+), 35 deletions(-) diff --git a/docker/types/healthcheck.py b/docker/types/healthcheck.py index 8405869e..ba63d21e 100644 --- a/docker/types/healthcheck.py +++ b/docker/types/healthcheck.py @@ -1,12 +1,18 @@ from .base import DictType +import six + class Healthcheck(DictType): def __init__(self, **kwargs): test = kwargs.get('test', kwargs.get('Test')) + if isinstance(test, six.string_types): + test = ["CMD-SHELL", test] + interval = kwargs.get('interval', kwargs.get('Interval')) timeout = kwargs.get('timeout', kwargs.get('Timeout')) retries = kwargs.get('retries', kwargs.get('Retries')) + super(Healthcheck, self).__init__({ 'Test': test, 'Interval': interval, diff --git a/docker/utils/utils.py b/docker/utils/utils.py index ee324db5..f61b5dd5 100644 --- a/docker/utils/utils.py +++ b/docker/utils/utils.py @@ -18,7 +18,7 @@ import six from .. import constants from .. import errors from .. import tls -from ..types import Ulimit, LogConfig +from ..types import Ulimit, LogConfig, Healthcheck if six.PY2: from urllib import splitnport @@ -1119,6 +1119,9 @@ def create_container_config( # Force None, an empty list or dict causes client.start to fail volumes_from = None + if healthcheck and isinstance(healthcheck, dict): + healthcheck = Healthcheck(**healthcheck) + attach_stdin = False attach_stdout = False attach_stderr = False diff --git a/tests/integration/healthcheck_test.py b/tests/integration/healthcheck_test.py index 9df8cd76..9c0f3980 100644 --- a/tests/integration/healthcheck_test.py +++ b/tests/integration/healthcheck_test.py @@ -1,5 +1,3 @@ -import docker - from .base import BaseIntegrationTest from .base import BUSYBOX from .. import helpers @@ -7,50 +5,47 @@ from .. import helpers SECOND = 1000000000 +def wait_on_health_status(client, container, status): + def condition(): + res = client.inspect_container(container) + return res['State']['Health']['Status'] == status + return helpers.wait_on_condition(condition) + + class HealthcheckTest(BaseIntegrationTest): @helpers.requires_api_version('1.24') - def test_healthcheck_passes(self): - healthcheck = docker.types.Healthcheck( - test=["CMD-SHELL", "true"], - interval=1*SECOND, - timeout=1*SECOND, - retries=1, - ) + def test_healthcheck_shell_command(self): container = self.client.create_container( - BUSYBOX, 'top', healthcheck=healthcheck) + BUSYBOX, 'top', healthcheck=dict(test='echo "hello world"')) self.tmp_containers.append(container) res = self.client.inspect_container(container) - assert res['Config']['Healthcheck'] == { - "Test": ["CMD-SHELL", "true"], - "Interval": 1*SECOND, - "Timeout": 1*SECOND, - "Retries": 1, - } - - def condition(): - res = self.client.inspect_container(container) - return res['State']['Health']['Status'] == "healthy" + assert res['Config']['Healthcheck']['Test'] == \ + ['CMD-SHELL', 'echo "hello world"'] + @helpers.requires_api_version('1.24') + def test_healthcheck_passes(self): + container = self.client.create_container( + BUSYBOX, 'top', healthcheck=dict( + test="true", + interval=1*SECOND, + timeout=1*SECOND, + retries=1, + )) + self.tmp_containers.append(container) self.client.start(container) - helpers.wait_on_condition(condition) + wait_on_health_status(self.client, container, "healthy") @helpers.requires_api_version('1.24') def test_healthcheck_fails(self): - healthcheck = docker.types.Healthcheck( - test=["CMD-SHELL", "false"], - interval=1*SECOND, - timeout=1*SECOND, - retries=1, - ) container = self.client.create_container( - BUSYBOX, 'top', healthcheck=healthcheck) + BUSYBOX, 'top', healthcheck=dict( + test="false", + interval=1*SECOND, + timeout=1*SECOND, + retries=1, + )) self.tmp_containers.append(container) - - def condition(): - res = self.client.inspect_container(container) - return res['State']['Health']['Status'] == "unhealthy" - self.client.start(container) - helpers.wait_on_condition(condition) + wait_on_health_status(self.client, container, "unhealthy")