Merge pull request #8122 from ojab/add_init_containers

Add init container support
This commit is contained in:
Anca Iordache 2021-04-06 18:25:24 +01:00 committed by GitHub
commit 683fac0dbf
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
6 changed files with 146 additions and 6 deletions

View File

@ -188,7 +188,7 @@
"properties": { "properties": {
"condition": { "condition": {
"type": "string", "type": "string",
"enum": ["service_started", "service_healthy"] "enum": ["service_started", "service_healthy", "service_completed_successfully"]
} }
}, },
"required": ["condition"] "required": ["condition"]

View File

@ -27,3 +27,8 @@ class NoHealthCheckConfigured(HealthCheckException):
service_name service_name
) )
) )
class CompletedUnsuccessfully(Exception):
def __init__(self, container_id, exit_code):
self.msg = 'Container "{}" exited with code {}.'.format(container_id, exit_code)

View File

@ -16,6 +16,7 @@ from compose.cli.colors import green
from compose.cli.colors import red from compose.cli.colors import red
from compose.cli.signals import ShutdownException from compose.cli.signals import ShutdownException
from compose.const import PARALLEL_LIMIT from compose.const import PARALLEL_LIMIT
from compose.errors import CompletedUnsuccessfully
from compose.errors import HealthCheckFailed from compose.errors import HealthCheckFailed
from compose.errors import NoHealthCheckConfigured from compose.errors import NoHealthCheckConfigured
from compose.errors import OperationFailedError from compose.errors import OperationFailedError
@ -61,7 +62,8 @@ def parallel_execute_watch(events, writer, errors, results, msg, get_name, fail_
elif isinstance(exception, APIError): elif isinstance(exception, APIError):
errors[get_name(obj)] = exception.explanation errors[get_name(obj)] = exception.explanation
writer.write(msg, get_name(obj), 'error', red) writer.write(msg, get_name(obj), 'error', red)
elif isinstance(exception, (OperationFailedError, HealthCheckFailed, NoHealthCheckConfigured)): elif isinstance(exception, (OperationFailedError, HealthCheckFailed, NoHealthCheckConfigured,
CompletedUnsuccessfully)):
errors[get_name(obj)] = exception.msg errors[get_name(obj)] = exception.msg
writer.write(msg, get_name(obj), 'error', red) writer.write(msg, get_name(obj), 'error', red)
elif isinstance(exception, UpstreamError): elif isinstance(exception, UpstreamError):
@ -241,6 +243,12 @@ def feed_queue(objects, func, get_deps, results, state, limiter):
'not processing'.format(obj) 'not processing'.format(obj)
) )
results.put((obj, None, e)) results.put((obj, None, e))
except CompletedUnsuccessfully as e:
log.debug(
'Service(s) upstream of {} did not completed successfully - '
'not processing'.format(obj)
)
results.put((obj, None, e))
if state.is_done(): if state.is_done():
results.put(STOP) results.put(STOP)

View File

@ -45,6 +45,7 @@ from .const import LABEL_VERSION
from .const import NANOCPUS_SCALE from .const import NANOCPUS_SCALE
from .const import WINDOWS_LONGPATH_PREFIX from .const import WINDOWS_LONGPATH_PREFIX
from .container import Container from .container import Container
from .errors import CompletedUnsuccessfully
from .errors import HealthCheckFailed from .errors import HealthCheckFailed
from .errors import NoHealthCheckConfigured from .errors import NoHealthCheckConfigured
from .errors import OperationFailedError from .errors import OperationFailedError
@ -112,6 +113,7 @@ HOST_CONFIG_KEYS = [
CONDITION_STARTED = 'service_started' CONDITION_STARTED = 'service_started'
CONDITION_HEALTHY = 'service_healthy' CONDITION_HEALTHY = 'service_healthy'
CONDITION_COMPLETED_SUCCESSFULLY = 'service_completed_successfully'
class BuildError(Exception): class BuildError(Exception):
@ -771,6 +773,8 @@ class Service:
configs[svc] = lambda s: True configs[svc] = lambda s: True
elif config['condition'] == CONDITION_HEALTHY: elif config['condition'] == CONDITION_HEALTHY:
configs[svc] = lambda s: s.is_healthy() configs[svc] = lambda s: s.is_healthy()
elif config['condition'] == CONDITION_COMPLETED_SUCCESSFULLY:
configs[svc] = lambda s: s.is_completed_successfully()
else: else:
# The config schema already prevents this, but it might be # The config schema already prevents this, but it might be
# bypassed if Compose is called programmatically. # bypassed if Compose is called programmatically.
@ -1322,6 +1326,21 @@ class Service:
raise HealthCheckFailed(ctnr.short_id) raise HealthCheckFailed(ctnr.short_id)
return result return result
def is_completed_successfully(self):
""" Check that all containers for this service has completed successfully
Returns false if at least one container does not exited and
raises CompletedUnsuccessfully exception if at least one container
exited with non-zero exit code.
"""
result = True
for ctnr in self.containers(stopped=True):
ctnr.inspect()
if ctnr.get('State.Status') != 'exited':
result = False
elif ctnr.exit_code != 0:
raise CompletedUnsuccessfully(ctnr.short_id, ctnr.exit_code)
return result
def _parse_proxy_config(self): def _parse_proxy_config(self):
client = self.client client = self.client
if 'proxies' not in client._general_configs: if 'proxies' not in client._general_configs:

View File

@ -25,6 +25,7 @@ from compose.const import COMPOSE_SPEC as VERSION
from compose.const import LABEL_PROJECT from compose.const import LABEL_PROJECT
from compose.const import LABEL_SERVICE from compose.const import LABEL_SERVICE
from compose.container import Container from compose.container import Container
from compose.errors import CompletedUnsuccessfully
from compose.errors import HealthCheckFailed from compose.errors import HealthCheckFailed
from compose.errors import NoHealthCheckConfigured from compose.errors import NoHealthCheckConfigured
from compose.project import Project from compose.project import Project
@ -1899,6 +1900,110 @@ class ProjectTest(DockerClientTestCase):
with pytest.raises(NoHealthCheckConfigured): with pytest.raises(NoHealthCheckConfigured):
svc1.is_healthy() svc1.is_healthy()
def test_project_up_completed_successfully_dependency(self):
config_dict = {
'version': '2.1',
'services': {
'svc1': {
'image': BUSYBOX_IMAGE_WITH_TAG,
'command': 'true'
},
'svc2': {
'image': BUSYBOX_IMAGE_WITH_TAG,
'command': 'top',
'depends_on': {
'svc1': {'condition': 'service_completed_successfully'},
}
}
}
}
config_data = load_config(config_dict)
project = Project.from_config(
name='composetest', config_data=config_data, client=self.client
)
project.up()
svc1 = project.get_service('svc1')
svc2 = project.get_service('svc2')
assert 'svc1' in svc2.get_dependency_names()
assert svc2.containers()[0].is_running
assert len(svc1.containers()) == 0
assert svc1.is_completed_successfully()
def test_project_up_completed_unsuccessfully_dependency(self):
config_dict = {
'version': '2.1',
'services': {
'svc1': {
'image': BUSYBOX_IMAGE_WITH_TAG,
'command': 'false'
},
'svc2': {
'image': BUSYBOX_IMAGE_WITH_TAG,
'command': 'top',
'depends_on': {
'svc1': {'condition': 'service_completed_successfully'},
}
}
}
}
config_data = load_config(config_dict)
project = Project.from_config(
name='composetest', config_data=config_data, client=self.client
)
with pytest.raises(ProjectError):
project.up()
containers = project.containers()
assert len(containers) == 0
svc1 = project.get_service('svc1')
svc2 = project.get_service('svc2')
assert 'svc1' in svc2.get_dependency_names()
with pytest.raises(CompletedUnsuccessfully):
svc1.is_completed_successfully()
def test_project_up_completed_differently_dependencies(self):
config_dict = {
'version': '2.1',
'services': {
'svc1': {
'image': BUSYBOX_IMAGE_WITH_TAG,
'command': 'true'
},
'svc2': {
'image': BUSYBOX_IMAGE_WITH_TAG,
'command': 'false'
},
'svc3': {
'image': BUSYBOX_IMAGE_WITH_TAG,
'command': 'top',
'depends_on': {
'svc1': {'condition': 'service_completed_successfully'},
'svc2': {'condition': 'service_completed_successfully'},
}
}
}
}
config_data = load_config(config_dict)
project = Project.from_config(
name='composetest', config_data=config_data, client=self.client
)
with pytest.raises(ProjectError):
project.up()
containers = project.containers()
assert len(containers) == 0
svc1 = project.get_service('svc1')
svc2 = project.get_service('svc2')
svc3 = project.get_service('svc3')
assert ['svc1', 'svc2'] == svc3.get_dependency_names()
assert svc1.is_completed_successfully()
with pytest.raises(CompletedUnsuccessfully):
svc2.is_completed_successfully()
def test_project_up_seccomp_profile(self): def test_project_up_seccomp_profile(self):
seccomp_data = { seccomp_data = {
'defaultAction': 'SCMP_ACT_ALLOW', 'defaultAction': 'SCMP_ACT_ALLOW',

View File

@ -2397,7 +2397,8 @@ web:
'image': 'busybox', 'image': 'busybox',
'depends_on': { 'depends_on': {
'app1': {'condition': 'service_started'}, 'app1': {'condition': 'service_started'},
'app2': {'condition': 'service_healthy'} 'app2': {'condition': 'service_healthy'},
'app3': {'condition': 'service_completed_successfully'}
} }
} }
override = {} override = {}
@ -2409,11 +2410,12 @@ web:
'image': 'busybox', 'image': 'busybox',
'depends_on': { 'depends_on': {
'app1': {'condition': 'service_started'}, 'app1': {'condition': 'service_started'},
'app2': {'condition': 'service_healthy'} 'app2': {'condition': 'service_healthy'},
'app3': {'condition': 'service_completed_successfully'}
} }
} }
override = { override = {
'depends_on': ['app3'] 'depends_on': ['app4']
} }
actual = config.merge_service_dicts(base, override, VERSION) actual = config.merge_service_dicts(base, override, VERSION)
@ -2422,7 +2424,8 @@ web:
'depends_on': { 'depends_on': {
'app1': {'condition': 'service_started'}, 'app1': {'condition': 'service_started'},
'app2': {'condition': 'service_healthy'}, 'app2': {'condition': 'service_healthy'},
'app3': {'condition': 'service_started'} 'app3': {'condition': 'service_completed_successfully'},
'app4': {'condition': 'service_started'},
} }
} }