Add healthchecks option to kolla_docker

blueprint container-health-check

Implements healthchecks option in kolla_docker Ansible module

Change-Id: I9323d4e75378d06f52b869f31009fd656bf270d2
This commit is contained in:
Michal Nasiadka 2020-07-30 13:55:17 +02:00
parent 6c1399d078
commit d6f69174ac
2 changed files with 357 additions and 4 deletions

View File

@ -207,6 +207,12 @@ options:
required: False required: False
default: 120 default: 120
type: int type: int
healthcheck:
description:
- Container healthcheck configuration
required: False
default: dict()
type: dict
author: Sam Yaple author: Sam Yaple
''' '''
@ -341,7 +347,8 @@ class DockerWorker(object):
self.compare_environment(container_info) or self.compare_environment(container_info) or
self.compare_container_state(container_info) or self.compare_container_state(container_info) or
self.compare_dimensions(container_info) or self.compare_dimensions(container_info) or
self.compare_command(container_info) self.compare_command(container_info) or
self.compare_healthcheck(container_info)
) )
def compare_ipc_mode(self, container_info): def compare_ipc_mode(self, container_info):
@ -535,6 +542,30 @@ class DockerWorker(object):
new_args != container_info['Args']): new_args != container_info['Args']):
return True return True
def compare_healthcheck(self, container_info):
new_healthcheck = self.parse_healthcheck(
self.params.get('healthcheck'))
current_healthcheck = container_info['Config'].get('Healthcheck')
healthcheck_map = {
'test': 'Test',
'retries': 'Retries',
'interval': 'Interval',
'start_period': 'StartPeriod',
'timeout': 'Timeout'}
if new_healthcheck:
new_healthcheck = new_healthcheck['healthcheck']
if current_healthcheck:
new_healthcheck = dict((healthcheck_map.get(k, k), v)
for (k, v) in new_healthcheck.items())
return new_healthcheck != current_healthcheck
else:
return True
else:
if current_healthcheck:
return True
def parse_image(self): def parse_image(self):
full_image = self.params.get('image') full_image = self.params.get('image')
@ -719,7 +750,8 @@ class DockerWorker(object):
def build_container_options(self): def build_container_options(self):
volumes, binds = self.generate_volumes() volumes, binds = self.generate_volumes()
return {
options = {
'command': self.params.get('command'), 'command': self.params.get('command'),
'detach': self.params.get('detach'), 'detach': self.params.get('detach'),
'environment': self._format_env_vars(), 'environment': self._format_env_vars(),
@ -731,6 +763,12 @@ class DockerWorker(object):
'tty': self.params.get('tty'), 'tty': self.params.get('tty'),
} }
healthcheck = self.parse_healthcheck(self.params.get('healthcheck'))
if healthcheck:
options.update(healthcheck)
return options
def create_container(self): def create_container(self):
self.changed = True self.changed = True
options = self.build_container_options() options = self.build_container_options()
@ -826,6 +864,71 @@ class DockerWorker(object):
else: else:
self.module.exit_json(**info['State']) self.module.exit_json(**info['State'])
def parse_healthcheck(self, healthcheck):
if not healthcheck:
return None
result = dict(healthcheck={})
# All supported healthcheck parameters
supported = set(['test', 'interval', 'timeout', 'start_period',
'retries'])
unsupported = set(healthcheck) - supported
missing = supported - set(healthcheck)
duration_options = set(['interval', 'timeout', 'start_period'])
if unsupported:
self.module.exit_json(failed=True,
msg=repr("Unsupported healthcheck options"),
unsupported_healthcheck=unsupported)
if missing:
self.module.exit_json(failed=True,
msg=repr("Missing healthcheck option"),
missing_healthcheck=missing)
for key in healthcheck:
value = healthcheck.get(key)
if key in duration_options:
try:
result['healthcheck'][key] = int(value) * 1000000000
except TypeError:
raise TypeError(
'Cannot parse healthcheck "{0}". '
'Expected an integer, got "{1}".'
.format(value, type(value).__name__)
)
except ValueError:
raise ValueError(
'Cannot parse healthcheck "{0}". '
'Expected an integer, got "{1}".'
.format(value, type(value).__name__)
)
else:
if key == 'test':
# If the user explicitly disables the healthcheck,
# return None as the healthcheck object
if value in (['NONE'], 'NONE'):
return None
else:
if isinstance(value, (tuple, list)):
result['healthcheck'][key] = \
[str(e) for e in value]
else:
result['healthcheck'][key] = \
['CMD-SHELL', str(value)]
elif key == 'retries':
try:
result['healthcheck'][key] = int(value)
except ValueError:
raise ValueError(
'Cannot parse healthcheck number of retries.'
'Expected an integer, got "{0}".'
.format(type(value))
)
return result
def stop_container(self): def stop_container(self):
name = self.params.get('name') name = self.params.get('name')
graceful_timeout = self.params.get('graceful_timeout') graceful_timeout = self.params.get('graceful_timeout')
@ -935,6 +1038,7 @@ def generate_module():
labels=dict(required=False, type='dict', default=dict()), labels=dict(required=False, type='dict', default=dict()),
name=dict(required=False, type='str'), name=dict(required=False, type='str'),
environment=dict(required=False, type='dict'), environment=dict(required=False, type='dict'),
healthcheck=dict(required=False, type='dict'),
image=dict(required=False, type='str'), image=dict(required=False, type='str'),
ipc_mode=dict(required=False, type='str', choices=['', ipc_mode=dict(required=False, type='str', choices=['',
'host', 'host',

View File

@ -93,6 +93,7 @@ class ModuleArgsTest(base.BaseTestCase):
dimensions=dict(required=False, type='dict', default=dict()), dimensions=dict(required=False, type='dict', default=dict()),
tty=dict(required=False, type='bool', default=False), tty=dict(required=False, type='bool', default=False),
client_timeout=dict(required=False, type='int', default=120), client_timeout=dict(required=False, type='int', default=120),
healthcheck=dict(required=False, type='dict'),
) )
required_if = [ required_if = [
['action', 'pull_image', ['image']], ['action', 'pull_image', ['image']],
@ -259,8 +260,9 @@ class TestContainer(base.BaseTestCase):
self.assertTrue(self.dw.changed) self.assertTrue(self.dw.changed)
self.fake_data['params'].pop('dimensions') self.fake_data['params'].pop('dimensions')
self.fake_data['params']['host_config']['blkio_weight'] = '10' self.fake_data['params']['host_config']['blkio_weight'] = '10'
expected_args = {'command', 'detach', 'environment', 'host_config', expected_args = {'command', 'detach', 'environment',
'image', 'labels', 'name', 'tty', 'volumes'} 'host_config', 'image', 'labels', 'name', 'tty',
'volumes'}
self.dw.dc.create_container.assert_called_once_with( self.dw.dc.create_container.assert_called_once_with(
**{k: self.fake_data['params'][k] for k in expected_args}) **{k: self.fake_data['params'][k] for k in expected_args})
self.dw.dc.create_host_config.assert_called_with( self.dw.dc.create_host_config.assert_called_with(
@ -278,6 +280,20 @@ class TestContainer(base.BaseTestCase):
failed=True, msg=repr("Unsupported dimensions"), failed=True, msg=repr("Unsupported dimensions"),
unsupported_dimensions=set(['random'])) unsupported_dimensions=set(['random']))
def test_create_container_with_healthcheck(self):
self.fake_data['params']['healthcheck'] = \
{'test': ['CMD-SHELL', '/bin/check.sh']}
self.dw = get_DockerWorker(self.fake_data['params'])
self.dw.dc.create_host_config = mock.MagicMock(
return_value=self.fake_data['params']['host_config'])
self.dw.create_container()
self.assertTrue(self.dw.changed)
expected_args = {'command', 'detach', 'environment', 'host_config',
'healthcheck', 'image', 'labels', 'name', 'tty',
'volumes'}
self.dw.dc.create_container.assert_called_once_with(
**{k: self.fake_data['params'][k] for k in expected_args})
def test_start_container_without_pull(self): def test_start_container_without_pull(self):
self.fake_data['params'].update({'auth_username': 'fake_user', self.fake_data['params'].update({'auth_username': 'fake_user',
'auth_password': 'fake_psw', 'auth_password': 'fake_psw',
@ -1102,3 +1118,236 @@ class TestAttrComp(base.BaseTestCase):
'Ulimits': [ulimits_nofile]} 'Ulimits': [ulimits_nofile]}
self.dw = get_DockerWorker(self.fake_data['params']) self.dw = get_DockerWorker(self.fake_data['params'])
self.assertFalse(self.dw.compare_dimensions(container_info)) self.assertFalse(self.dw.compare_dimensions(container_info))
def test_compare_empty_new_healthcheck(self):
container_info = dict()
container_info['Config'] = {
'Healthcheck': {
'Test': [
"CMD-SHELL",
"/bin/check.sh"],
"Interval": 30000000000,
"Timeout": 30000000000,
"StartPeriod": 5000000000,
"Retries": 3}}
self.dw = get_DockerWorker(self.fake_data['params'])
self.assertTrue(self.dw.compare_healthcheck(container_info))
def test_compare_empty_current_healthcheck(self):
self.fake_data['params']['healthcheck'] = {
'test': ['CMD-SHELL', '/bin/check.sh'],
'interval': 30,
'timeout': 30,
'start_period': 5,
'retries': 3}
container_info = dict()
container_info['Config'] = {}
self.dw = get_DockerWorker(self.fake_data['params'])
self.assertTrue(self.dw.compare_healthcheck(container_info))
def test_compare_healthcheck_no_test(self):
self.fake_data['params']['healthcheck'] = {
'interval': 30,
'timeout': 30,
'start_period': 5,
'retries': 3}
container_info = dict()
container_info['Config'] = {
'Healthcheck': {
'Test': [
"CMD-SHELL",
"/bin/check.sh"],
"Interval": 30000000000,
"Timeout": 30000000000,
"StartPeriod": 5000000000,
"Retries": 3}}
self.dw = get_DockerWorker(self.fake_data['params'])
self.dw.compare_healthcheck(container_info)
self.dw.module.exit_json.assert_called_once_with(
failed=True, msg=repr("Missing healthcheck option"),
missing_healthcheck=set(['test']))
def test_compare_healthcheck_pos(self):
self.fake_data['params']['healthcheck'] = \
{'test': ['CMD', '/bin/check']}
container_info = dict()
container_info['Config'] = {
'Healthcheck': {
'Test': [
"CMD-SHELL",
"/bin/check.sh"],
"Interval": 30000000000,
"Timeout": 30000000000,
"StartPeriod": 5000000000,
"Retries": 3}}
self.dw = get_DockerWorker(self.fake_data['params'])
self.assertTrue(self.dw.compare_healthcheck(container_info))
def test_compare_healthcheck_neg(self):
self.fake_data['params']['healthcheck'] = \
{'test': ['CMD-SHELL', '/bin/check.sh'],
'interval': 30,
'timeout': 30,
'start_period': 5,
'retries': 3}
container_info = dict()
container_info['Config'] = {
"Healthcheck": {
"Test": [
"CMD-SHELL",
"/bin/check.sh"],
"Interval": 30000000000,
"Timeout": 30000000000,
"StartPeriod": 5000000000,
"Retries": 3}}
self.dw = get_DockerWorker(self.fake_data['params'])
self.assertFalse(self.dw.compare_healthcheck(container_info))
def test_compare_healthcheck_time_zero(self):
self.fake_data['params']['healthcheck'] = \
{'test': ['CMD-SHELL', '/bin/check.sh'],
'interval': 0,
'timeout': 30,
'start_period': 5,
'retries': 3}
container_info = dict()
container_info['Config'] = {
"Healthcheck": {
"Test": [
"CMD-SHELL",
"/bin/check.sh"],
"Interval": 30000000000,
"Timeout": 30000000000,
"StartPeriod": 5000000000,
"Retries": 3}}
self.dw = get_DockerWorker(self.fake_data['params'])
self.assertTrue(self.dw.compare_healthcheck(container_info))
def test_compare_healthcheck_time_wrong_type(self):
self.fake_data['params']['healthcheck'] = \
{'test': ['CMD-SHELL', '/bin/check.sh'],
'timeout': 30,
'start_period': 5,
'retries': 3}
self.fake_data['params']['healthcheck']['interval'] = \
{"broken": {"interval": "True"}}
container_info = dict()
container_info['Config'] = {
"Healthcheck": {
"Test": [
"CMD-SHELL",
"/bin/check.sh"],
"Interval": 30000000000,
"Timeout": 30000000000,
"StartPeriod": 5000000000,
"Retries": 3}}
self.dw = get_DockerWorker(self.fake_data['params'])
self.assertRaises(TypeError,
lambda: self.dw.compare_healthcheck(container_info))
def test_compare_healthcheck_time_wrong_value(self):
self.fake_data['params']['healthcheck'] = \
{'test': ['CMD-SHELL', '/bin/check.sh'],
'timeout': 30,
'start_period': 5,
'retries': 3}
self.fake_data['params']['healthcheck']['interval'] = "dog"
container_info = dict()
container_info['Config'] = {
"Healthcheck": {
"Test": [
"CMD-SHELL",
"/bin/check.sh"],
"Interval": 30000000000,
"Timeout": 30000000000,
"StartPeriod": 5000000000,
"Retries": 3}}
self.dw = get_DockerWorker(self.fake_data['params'])
self.assertRaises(ValueError,
lambda: self.dw.compare_healthcheck(container_info))
def test_compare_healthcheck_opt_missing(self):
self.fake_data['params']['healthcheck'] = \
{'test': ['CMD-SHELL', '/bin/check.sh'],
'interval': 30,
'timeout': 30,
'retries': 3}
container_info = dict()
container_info['Config'] = {
"Healthcheck": {
"Test": [
"CMD-SHELL",
"/bin/check.sh"],
"Interval": 30000000000,
"Timeout": 30000000000,
"StartPeriod": 5000000000,
"Retries": 3}}
self.dw = get_DockerWorker(self.fake_data['params'])
self.dw.compare_healthcheck(container_info)
self.dw.module.exit_json.assert_called_once_with(
failed=True, msg=repr("Missing healthcheck option"),
missing_healthcheck=set(['start_period']))
def test_compare_healthcheck_opt_extra(self):
self.fake_data['params']['healthcheck'] = \
{'test': ['CMD-SHELL', '/bin/check.sh'],
'interval': 30,
'start_period': 5,
'extra_option': 1,
'timeout': 30,
'retries': 3}
container_info = dict()
container_info['Config'] = {
"Healthcheck": {
"Test": [
"CMD-SHELL",
"/bin/check.sh"],
"Interval": 30000000000,
"Timeout": 30000000000,
"StartPeriod": 5000000000,
"Retries": 3}}
self.dw = get_DockerWorker(self.fake_data['params'])
self.dw.compare_healthcheck(container_info)
self.dw.module.exit_json.assert_called_once_with(
failed=True, msg=repr("Unsupported healthcheck options"),
unsupported_healthcheck=set(['extra_option']))
def test_compare_healthcheck_value_false(self):
self.fake_data['params']['healthcheck'] = \
{'test': ['CMD-SHELL', '/bin/check.sh'],
'interval': 30,
'start_period': 5,
'extra_option': 1,
'timeout': 30,
'retries': False}
container_info = dict()
container_info['Config'] = {
"Healthcheck": {
"Test": [
"CMD-SHELL",
"/bin/check.sh"],
"Interval": 30000000000,
"Timeout": 30000000000,
"StartPeriod": 5000000000,
"Retries": 3}}
self.dw = get_DockerWorker(self.fake_data['params'])
self.assertTrue(self.dw.compare_healthcheck(container_info))
def test_parse_healthcheck_empty(self):
self.dw = get_DockerWorker(self.fake_data['params'])
self.assertIsNone(self.dw.parse_healthcheck(
self.fake_data.get('params', {}).get('healthcheck')))
def test_parse_healthcheck_test_none(self):
self.fake_data['params']['healthcheck'] = \
{'test': 'NONE'}
self.dw = get_DockerWorker(self.fake_data['params'])
self.assertIsNone(self.dw.parse_healthcheck(
self.fake_data['params']['healthcheck']))
def test_parse_healthcheck_test_none_brackets(self):
self.fake_data['params']['healthcheck'] = \
{'test': ['NONE']}
self.dw = get_DockerWorker(self.fake_data['params'])
self.assertIsNone(self.dw.parse_healthcheck(
self.fake_data['params']['healthcheck']))