Merge pull request #4163 from aanand/add-healthcheck

Implement 'healthcheck' option
This commit is contained in:
Joffrey F 2016-12-14 15:35:05 -08:00 committed by GitHub
commit 635a281777
9 changed files with 172 additions and 9 deletions

View File

@ -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)

View File

@ -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
},

View File

@ -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

View File

@ -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())

View File

@ -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'

View File

@ -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)

View File

@ -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

View File

@ -29,7 +29,7 @@ services:
constraints: [node=foo]
healthcheck:
command: cat /etc/passwd
test: cat /etc/passwd
interval: 10s
timeout: 1s
retries: 5

View File

@ -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 = [