summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--compose/config/config.py29
-rw-r--r--compose/config/config_schema_v3.0.json5
-rw-r--r--compose/service.py4
-rw-r--r--compose/utils.py16
-rw-r--r--requirements.txt2
-rw-r--r--tests/acceptance/cli_test.py50
-rw-r--r--tests/fixtures/healthcheck/docker-compose.yml24
-rw-r--r--tests/fixtures/v3-full/docker-compose.yml2
-rw-r--r--tests/unit/config/config_test.py49
9 files changed, 172 insertions, 9 deletions
diff --git a/compose/config/config.py b/compose/config/config.py
index 5215b361..ca8de309 100644
--- a/compose/config/config.py
+++ b/compose/config/config.py
@@ -17,6 +17,7 @@ from ..const import COMPOSEFILE_V2_0 as V2_0
from ..const import COMPOSEFILE_V2_1 as V2_1
from ..const import COMPOSEFILE_V3_0 as V3_0
from ..utils import build_string_dict
+from ..utils import parse_nanoseconds_int
from ..utils import splitdrive
from .environment import env_vars_from_file
from .environment import Environment
@@ -65,6 +66,7 @@ DOCKER_CONFIG_KEYS = [
'extra_hosts',
'group_add',
'hostname',
+ 'healthcheck',
'image',
'ipc',
'labels',
@@ -642,6 +644,10 @@ def process_service(service_config):
if 'extra_hosts' in service_dict:
service_dict['extra_hosts'] = parse_extra_hosts(service_dict['extra_hosts'])
+ if 'healthcheck' in service_dict:
+ service_dict['healthcheck'] = process_healthcheck(
+ service_dict['healthcheck'], service_config.name)
+
for field in ['dns', 'dns_search', 'tmpfs']:
if field in service_dict:
service_dict[field] = to_list(service_dict[field])
@@ -649,6 +655,29 @@ def process_service(service_config):
return service_dict
+def process_healthcheck(raw, service_name):
+ hc = {}
+
+ if raw.get('disable'):
+ if len(raw) > 1:
+ raise ConfigurationError(
+ 'Service "{}" defines an invalid healthcheck: '
+ '"disable: true" cannot be combined with other options'
+ .format(service_name))
+ hc['test'] = ['NONE']
+ elif 'test' in raw:
+ hc['test'] = raw['test']
+
+ if 'interval' in raw:
+ hc['interval'] = parse_nanoseconds_int(raw['interval'])
+ if 'timeout' in raw:
+ hc['timeout'] = parse_nanoseconds_int(raw['timeout'])
+ if 'retries' in raw:
+ hc['retries'] = raw['retries']
+
+ return hc
+
+
def finalize_service(service_config, service_names, version, environment):
service_dict = dict(service_config.config)
diff --git a/compose/config/config_schema_v3.0.json b/compose/config/config_schema_v3.0.json
index 9ac31b1f..4edd8dd4 100644
--- a/compose/config/config_schema_v3.0.json
+++ b/compose/config/config_schema_v3.0.json
@@ -205,12 +205,13 @@
"interval": {"type":"string"},
"timeout": {"type":"string"},
"retries": {"type": "number"},
- "command": {
+ "test": {
"oneOf": [
{"type": "string"},
{"type": "array", "items": {"type": "string"}}
]
- }
+ },
+ "disable": {"type": "boolean"}
},
"additionalProperties": false
},
diff --git a/compose/service.py b/compose/service.py
index 39737694..cf52d489 100644
--- a/compose/service.py
+++ b/compose/service.py
@@ -17,7 +17,6 @@ from docker.utils.ports import split_port
from . import __version__
from . import progress_stream
-from . import timeparse
from .config import DOCKER_CONFIG_KEYS
from .config import merge_environment
from .config.types import VolumeSpec
@@ -35,6 +34,7 @@ from .parallel import parallel_start
from .progress_stream import stream_output
from .progress_stream import StreamOutputError
from .utils import json_hash
+from .utils import parse_seconds_float
log = logging.getLogger(__name__)
@@ -450,7 +450,7 @@ class Service(object):
def stop_timeout(self, timeout):
if timeout is not None:
return timeout
- timeout = timeparse.timeparse(self.options.get('stop_grace_period') or '')
+ timeout = parse_seconds_float(self.options.get('stop_grace_period'))
if timeout is not None:
return timeout
return DEFAULT_TIMEOUT
diff --git a/compose/utils.py b/compose/utils.py
index 8f05e308..b8bdf732 100644
--- a/compose/utils.py
+++ b/compose/utils.py
@@ -11,6 +11,7 @@ import ntpath
import six
from .errors import StreamParseError
+from .timeparse import timeparse
json_decoder = json.JSONDecoder()
@@ -107,6 +108,21 @@ def microseconds_from_time_nano(time_nano):
return int(time_nano % 1000000000 / 1000)
+def nanoseconds_from_time_seconds(time_seconds):
+ return time_seconds * 1000000000
+
+
+def parse_seconds_float(value):
+ return timeparse(value or '')
+
+
+def parse_nanoseconds_int(value):
+ parsed = timeparse(value or '')
+ if parsed is None:
+ return None
+ return int(parsed * 1000000000)
+
+
def build_string_dict(source_dict):
return dict((k, str(v if v is not None else '')) for k, v in source_dict.items())
diff --git a/requirements.txt b/requirements.txt
index 933146c7..63469799 100644
--- a/requirements.txt
+++ b/requirements.txt
@@ -1,11 +1,11 @@
PyYAML==3.11
backports.ssl-match-hostname==3.5.0.1; python_version < '3'
cached-property==1.2.0
-docker-py==1.10.6
dockerpty==0.4.1
docopt==0.6.1
enum34==1.0.4; python_version < '3.4'
functools32==3.2.3.post2; python_version < '3.2'
+git+https://github.com/docker/docker-py.git@2ff7371ae7703033f981e1b137a3be0caf7a4f9c#egg=docker-py
ipaddress==1.0.16
jsonschema==2.5.1
pypiwin32==219; sys_platform == 'win32'
diff --git a/tests/acceptance/cli_test.py b/tests/acceptance/cli_test.py
index 0d5d5058..856b8f93 100644
--- a/tests/acceptance/cli_test.py
+++ b/tests/acceptance/cli_test.py
@@ -21,6 +21,7 @@ from .. import mock
from compose.cli.command import get_project
from compose.container import Container
from compose.project import OneOffFilter
+from compose.utils import nanoseconds_from_time_seconds
from tests.integration.testcases import DockerClientTestCase
from tests.integration.testcases import get_links
from tests.integration.testcases import pull_busybox
@@ -331,9 +332,9 @@ class CLITestCase(DockerClientTestCase):
},
'healthcheck': {
- 'command': 'cat /etc/passwd',
- 'interval': '10s',
- 'timeout': '1s',
+ 'test': 'cat /etc/passwd',
+ 'interval': 10000000000,
+ 'timeout': 1000000000,
'retries': 5,
},
@@ -927,6 +928,49 @@ class CLITestCase(DockerClientTestCase):
assert foo_container.get('HostConfig.NetworkMode') == \
'container:{}'.format(bar_container.id)
+ def test_up_with_healthcheck(self):
+ def wait_on_health_status(container, status):
+ def condition():
+ container.inspect()
+ return container.get('State.Health.Status') == status
+
+ return wait_on_condition(condition, delay=0.5)
+
+ self.base_dir = 'tests/fixtures/healthcheck'
+ self.dispatch(['up', '-d'], None)
+
+ passes = self.project.get_service('passes')
+ passes_container = passes.containers()[0]
+
+ assert passes_container.get('Config.Healthcheck') == {
+ "Test": ["CMD-SHELL", "/bin/true"],
+ "Interval": nanoseconds_from_time_seconds(1),
+ "Timeout": nanoseconds_from_time_seconds(30*60),
+ "Retries": 1,
+ }
+
+ wait_on_health_status(passes_container, 'healthy')
+
+ fails = self.project.get_service('fails')
+ fails_container = fails.containers()[0]
+
+ assert fails_container.get('Config.Healthcheck') == {
+ "Test": ["CMD", "/bin/false"],
+ "Interval": nanoseconds_from_time_seconds(2.5),
+ "Retries": 2,
+ }
+
+ wait_on_health_status(fails_container, 'unhealthy')
+
+ disabled = self.project.get_service('disabled')
+ disabled_container = disabled.containers()[0]
+
+ assert disabled_container.get('Config.Healthcheck') == {
+ "Test": ["NONE"],
+ }
+
+ assert 'Health' not in disabled_container.get('State')
+
def test_up_with_no_deps(self):
self.base_dir = 'tests/fixtures/links-composefile'
self.dispatch(['up', '-d', '--no-deps', 'web'], None)
diff --git a/tests/fixtures/healthcheck/docker-compose.yml b/tests/fixtures/healthcheck/docker-compose.yml
new file mode 100644
index 00000000..2c45b8d8
--- /dev/null
+++ b/tests/fixtures/healthcheck/docker-compose.yml
@@ -0,0 +1,24 @@
+version: "3"
+services:
+ passes:
+ image: busybox
+ command: top
+ healthcheck:
+ test: "/bin/true"
+ interval: 1s
+ timeout: 30m
+ retries: 1
+
+ fails:
+ image: busybox
+ command: top
+ healthcheck:
+ test: ["CMD", "/bin/false"]
+ interval: 2.5s
+ retries: 2
+
+ disabled:
+ image: busybox
+ command: top
+ healthcheck:
+ disable: true
diff --git a/tests/fixtures/v3-full/docker-compose.yml b/tests/fixtures/v3-full/docker-compose.yml
index 1187dd7b..b4d1b642 100644
--- a/tests/fixtures/v3-full/docker-compose.yml
+++ b/tests/fixtures/v3-full/docker-compose.yml
@@ -29,7 +29,7 @@ services:
constraints: [node=foo]
healthcheck:
- command: cat /etc/passwd
+ test: cat /etc/passwd
interval: 10s
timeout: 1s
retries: 5
diff --git a/tests/unit/config/config_test.py b/tests/unit/config/config_test.py
index 114145e1..f7df3aee 100644
--- a/tests/unit/config/config_test.py
+++ b/tests/unit/config/config_test.py
@@ -24,6 +24,7 @@ from compose.config.errors import ConfigurationError
from compose.config.errors import VERSION_EXPLANATION
from compose.config.types import VolumeSpec
from compose.const import IS_WINDOWS_PLATFORM
+from compose.utils import nanoseconds_from_time_seconds
from tests import mock
from tests import unittest
@@ -3171,6 +3172,54 @@ class BuildPathTest(unittest.TestCase):
assert 'build path' in exc.exconly()
+class HealthcheckTest(unittest.TestCase):
+ def test_healthcheck(self):
+ service_dict = make_service_dict(
+ 'test',
+ {'healthcheck': {
+ 'test': ['CMD', 'true'],
+ 'interval': '1s',
+ 'timeout': '1m',
+ 'retries': 3,
+ }},
+ '.',
+ )
+
+ assert service_dict['healthcheck'] == {
+ 'test': ['CMD', 'true'],
+ 'interval': nanoseconds_from_time_seconds(1),
+ 'timeout': nanoseconds_from_time_seconds(60),
+ 'retries': 3,
+ }
+
+ def test_disable(self):
+ service_dict = make_service_dict(
+ 'test',
+ {'healthcheck': {
+ 'disable': True,
+ }},
+ '.',
+ )
+
+ assert service_dict['healthcheck'] == {
+ 'test': ['NONE'],
+ }
+
+ def test_disable_with_other_config_is_invalid(self):
+ with pytest.raises(ConfigurationError) as excinfo:
+ make_service_dict(
+ 'invalid-healthcheck',
+ {'healthcheck': {
+ 'disable': True,
+ 'interval': '1s',
+ }},
+ '.',
+ )
+
+ assert 'invalid-healthcheck' in excinfo.exconly()
+ assert 'disable' in excinfo.exconly()
+
+
class GetDefaultConfigFilesTestCase(unittest.TestCase):
files = [