Add support for start_period in healthcheck config

Improve merging strategy for healthcheck configs

Signed-off-by: Joffrey F <joffrey@docker.com>
This commit is contained in:
Joffrey F 2017-08-11 15:52:43 -07:00 committed by Joffrey F
parent 22d9a258f4
commit 7210fdb21c
6 changed files with 117 additions and 14 deletions

View File

@ -797,16 +797,12 @@ def process_healthcheck(service_dict, service_name):
elif 'test' in raw: elif 'test' in raw:
hc['test'] = raw['test'] hc['test'] = raw['test']
if 'interval' in raw: for field in ['interval', 'timeout', 'start_period']:
if not isinstance(raw['interval'], six.integer_types): if field in raw:
hc['interval'] = parse_nanoseconds_int(raw['interval']) if not isinstance(raw[field], six.integer_types):
hc[field] = parse_nanoseconds_int(raw[field])
else: # Conversion has been done previously else: # Conversion has been done previously
hc['interval'] = raw['interval'] hc[field] = raw[field]
if 'timeout' in raw:
if not isinstance(raw['timeout'], six.integer_types):
hc['timeout'] = parse_nanoseconds_int(raw['timeout'])
else: # Conversion has been done previously
hc['timeout'] = raw['timeout']
if 'retries' in raw: if 'retries' in raw:
hc['retries'] = raw['retries'] hc['retries'] = raw['retries']
@ -967,6 +963,7 @@ def merge_service_dicts(base, override, version):
md.merge_field('logging', merge_logging, default={}) md.merge_field('logging', merge_logging, default={})
merge_ports(md, base, override) merge_ports(md, base, override)
md.merge_field('blkio_config', merge_blkio_config, default={}) md.merge_field('blkio_config', merge_blkio_config, default={})
md.merge_field('healthcheck', merge_healthchecks, default={})
for field in set(ALLOWED_KEYS) - set(md): for field in set(ALLOWED_KEYS) - set(md):
md.merge_scalar(field) md.merge_scalar(field)
@ -985,6 +982,14 @@ def merge_unique_items_lists(base, override):
return sorted(set().union(base, override)) return sorted(set().union(base, override))
def merge_healthchecks(base, override):
if override.get('disabled') is True:
return override
result = base.copy()
result.update(override)
return result
def merge_ports(md, base, override): def merge_ports(md, base, override):
def parse_sequence_func(seq): def parse_sequence_func(seq):
acc = [] acc = []

View File

@ -309,6 +309,7 @@
"disable": {"type": "boolean"}, "disable": {"type": "boolean"},
"interval": {"type": "string"}, "interval": {"type": "string"},
"retries": {"type": "number"}, "retries": {"type": "number"},
"start_period": {"type": "string"},
"test": { "test": {
"oneOf": [ "oneOf": [
{"type": "string"}, {"type": "string"},

View File

@ -131,6 +131,10 @@ def denormalize_service_dict(service_dict, version, image_digest=None):
service_dict['healthcheck']['timeout'] service_dict['healthcheck']['timeout']
) )
if 'start_period' in service_dict['healthcheck']:
service_dict['healthcheck']['start_period'] = serialize_ns_time_value(
service_dict['healthcheck']['start_period']
)
if 'ports' in service_dict and version < V3_2: if 'ports' in service_dict and version < V3_2:
service_dict['ports'] = [ service_dict['ports'] = [
p.legacy_repr() if isinstance(p, types.ServicePort) else p p.legacy_repr() if isinstance(p, types.ServicePort) else p

View File

@ -14,6 +14,7 @@ from docker.utils import parse_bytes as sdk_parse_bytes
from .config.errors import ConfigurationError from .config.errors import ConfigurationError
from .errors import StreamParseError from .errors import StreamParseError
from .timeparse import MULTIPLIERS
from .timeparse import timeparse from .timeparse import timeparse
@ -112,7 +113,7 @@ def microseconds_from_time_nano(time_nano):
def nanoseconds_from_time_seconds(time_seconds): def nanoseconds_from_time_seconds(time_seconds):
return time_seconds * 1000000000 return int(time_seconds / MULTIPLIERS['nano'])
def parse_seconds_float(value): def parse_seconds_float(value):
@ -123,7 +124,7 @@ def parse_nanoseconds_int(value):
parsed = timeparse(value or '') parsed = timeparse(value or '')
if parsed is None: if parsed is None:
return None return None
return int(parsed * 1000000000) return nanoseconds_from_time_seconds(parsed)
def build_string_dict(source_dict): def build_string_dict(source_dict):

View File

@ -36,6 +36,7 @@ from compose.service import ConvergenceStrategy
from compose.service import NetworkMode from compose.service import NetworkMode
from compose.service import PidMode from compose.service import PidMode
from compose.service import Service from compose.service import Service
from compose.utils import parse_nanoseconds_int
from tests.integration.testcases import is_cluster from tests.integration.testcases import is_cluster
from tests.integration.testcases import no_cluster from tests.integration.testcases import no_cluster
from tests.integration.testcases import v2_1_only from tests.integration.testcases import v2_1_only
@ -270,6 +271,24 @@ class ServiceTest(DockerClientTestCase):
self.assertTrue(path.basename(actual_host_path) == path.basename(host_path), self.assertTrue(path.basename(actual_host_path) == path.basename(host_path),
msg=("Last component differs: %s, %s" % (actual_host_path, host_path))) msg=("Last component differs: %s, %s" % (actual_host_path, host_path)))
def test_create_container_with_healthcheck_config(self):
one_second = parse_nanoseconds_int('1s')
healthcheck = {
'test': ['true'],
'interval': 2 * one_second,
'timeout': 5 * one_second,
'retries': 5,
'start_period': 2 * one_second
}
service = self.create_service('db', healthcheck=healthcheck)
container = service.create_container()
remote_healthcheck = container.get('Config.Healthcheck')
assert remote_healthcheck['Test'] == healthcheck['test']
assert remote_healthcheck['Interval'] == healthcheck['interval']
assert remote_healthcheck['Timeout'] == healthcheck['timeout']
assert remote_healthcheck['Retries'] == healthcheck['retries']
assert remote_healthcheck['StartPeriod'] == healthcheck['start_period']
def test_recreate_preserves_volume_with_trailing_slash(self): def test_recreate_preserves_volume_with_trailing_slash(self):
"""When the Compose file specifies a trailing slash in the container path, make """When the Compose file specifies a trailing slash in the container path, make
sure we copy the volume over when recreating. sure we copy the volume over when recreating.

View File

@ -2197,6 +2197,75 @@ class ConfigTest(unittest.TestCase):
} }
} }
def test_merge_healthcheck_config(self):
base = {
'image': 'bar',
'healthcheck': {
'start_period': 1000,
'interval': 3000,
'test': ['true']
}
}
override = {
'healthcheck': {
'interval': 5000,
'timeout': 10000,
'test': ['echo', 'OK'],
}
}
actual = config.merge_service_dicts(base, override, V2_3)
assert actual['healthcheck'] == {
'start_period': base['healthcheck']['start_period'],
'test': override['healthcheck']['test'],
'interval': override['healthcheck']['interval'],
'timeout': override['healthcheck']['timeout'],
}
def test_merge_healthcheck_override_disables(self):
base = {
'image': 'bar',
'healthcheck': {
'start_period': 1000,
'interval': 3000,
'timeout': 2000,
'retries': 3,
'test': ['true']
}
}
override = {
'healthcheck': {
'disabled': True
}
}
actual = config.merge_service_dicts(base, override, V2_3)
assert actual['healthcheck'] == {'disabled': True}
def test_merge_healthcheck_override_enables(self):
base = {
'image': 'bar',
'healthcheck': {
'disabled': True
}
}
override = {
'healthcheck': {
'disabled': False,
'start_period': 1000,
'interval': 3000,
'timeout': 2000,
'retries': 3,
'test': ['true']
}
}
actual = config.merge_service_dicts(base, override, V2_3)
assert actual['healthcheck'] == override['healthcheck']
def test_external_volume_config(self): def test_external_volume_config(self):
config_details = build_config_details({ config_details = build_config_details({
'version': '2', 'version': '2',
@ -4008,6 +4077,7 @@ class HealthcheckTest(unittest.TestCase):
'interval': '1s', 'interval': '1s',
'timeout': '1m', 'timeout': '1m',
'retries': 3, 'retries': 3,
'start_period': '10s'
}}, }},
'.', '.',
) )
@ -4017,6 +4087,7 @@ class HealthcheckTest(unittest.TestCase):
'interval': nanoseconds_from_time_seconds(1), 'interval': nanoseconds_from_time_seconds(1),
'timeout': nanoseconds_from_time_seconds(60), 'timeout': nanoseconds_from_time_seconds(60),
'retries': 3, 'retries': 3,
'start_period': nanoseconds_from_time_seconds(10)
} }
def test_disable(self): def test_disable(self):
@ -4147,15 +4218,17 @@ class SerializeTest(unittest.TestCase):
'test': 'exit 1', 'test': 'exit 1',
'interval': '1m40s', 'interval': '1m40s',
'timeout': '30s', 'timeout': '30s',
'retries': 5 'retries': 5,
'start_period': '2s90ms'
} }
} }
processed_service = config.process_service(config.ServiceConfig( processed_service = config.process_service(config.ServiceConfig(
'.', 'test', 'test', service_dict '.', 'test', 'test', service_dict
)) ))
denormalized_service = denormalize_service_dict(processed_service, V2_1) denormalized_service = denormalize_service_dict(processed_service, V2_3)
assert denormalized_service['healthcheck']['interval'] == '100s' assert denormalized_service['healthcheck']['interval'] == '100s'
assert denormalized_service['healthcheck']['timeout'] == '30s' assert denormalized_service['healthcheck']['timeout'] == '30s'
assert denormalized_service['healthcheck']['start_period'] == '2090ms'
def test_denormalize_image_has_digest(self): def test_denormalize_image_has_digest(self):
service_dict = { service_dict = {