mirror of https://github.com/docker/compose.git
Merge pull request #8122 from ojab/add_init_containers
Add init container support
This commit is contained in:
commit
683fac0dbf
|
@ -188,7 +188,7 @@
|
|||
"properties": {
|
||||
"condition": {
|
||||
"type": "string",
|
||||
"enum": ["service_started", "service_healthy"]
|
||||
"enum": ["service_started", "service_healthy", "service_completed_successfully"]
|
||||
}
|
||||
},
|
||||
"required": ["condition"]
|
||||
|
|
|
@ -27,3 +27,8 @@ class NoHealthCheckConfigured(HealthCheckException):
|
|||
service_name
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
class CompletedUnsuccessfully(Exception):
|
||||
def __init__(self, container_id, exit_code):
|
||||
self.msg = 'Container "{}" exited with code {}.'.format(container_id, exit_code)
|
||||
|
|
|
@ -16,6 +16,7 @@ from compose.cli.colors import green
|
|||
from compose.cli.colors import red
|
||||
from compose.cli.signals import ShutdownException
|
||||
from compose.const import PARALLEL_LIMIT
|
||||
from compose.errors import CompletedUnsuccessfully
|
||||
from compose.errors import HealthCheckFailed
|
||||
from compose.errors import NoHealthCheckConfigured
|
||||
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):
|
||||
errors[get_name(obj)] = exception.explanation
|
||||
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
|
||||
writer.write(msg, get_name(obj), 'error', red)
|
||||
elif isinstance(exception, UpstreamError):
|
||||
|
@ -241,6 +243,12 @@ def feed_queue(objects, func, get_deps, results, state, limiter):
|
|||
'not processing'.format(obj)
|
||||
)
|
||||
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():
|
||||
results.put(STOP)
|
||||
|
|
|
@ -45,6 +45,7 @@ from .const import LABEL_VERSION
|
|||
from .const import NANOCPUS_SCALE
|
||||
from .const import WINDOWS_LONGPATH_PREFIX
|
||||
from .container import Container
|
||||
from .errors import CompletedUnsuccessfully
|
||||
from .errors import HealthCheckFailed
|
||||
from .errors import NoHealthCheckConfigured
|
||||
from .errors import OperationFailedError
|
||||
|
@ -112,6 +113,7 @@ HOST_CONFIG_KEYS = [
|
|||
|
||||
CONDITION_STARTED = 'service_started'
|
||||
CONDITION_HEALTHY = 'service_healthy'
|
||||
CONDITION_COMPLETED_SUCCESSFULLY = 'service_completed_successfully'
|
||||
|
||||
|
||||
class BuildError(Exception):
|
||||
|
@ -771,6 +773,8 @@ class Service:
|
|||
configs[svc] = lambda s: True
|
||||
elif config['condition'] == CONDITION_HEALTHY:
|
||||
configs[svc] = lambda s: s.is_healthy()
|
||||
elif config['condition'] == CONDITION_COMPLETED_SUCCESSFULLY:
|
||||
configs[svc] = lambda s: s.is_completed_successfully()
|
||||
else:
|
||||
# The config schema already prevents this, but it might be
|
||||
# bypassed if Compose is called programmatically.
|
||||
|
@ -1322,6 +1326,21 @@ class Service:
|
|||
raise HealthCheckFailed(ctnr.short_id)
|
||||
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):
|
||||
client = self.client
|
||||
if 'proxies' not in client._general_configs:
|
||||
|
|
|
@ -25,6 +25,7 @@ from compose.const import COMPOSE_SPEC as VERSION
|
|||
from compose.const import LABEL_PROJECT
|
||||
from compose.const import LABEL_SERVICE
|
||||
from compose.container import Container
|
||||
from compose.errors import CompletedUnsuccessfully
|
||||
from compose.errors import HealthCheckFailed
|
||||
from compose.errors import NoHealthCheckConfigured
|
||||
from compose.project import Project
|
||||
|
@ -1899,6 +1900,110 @@ class ProjectTest(DockerClientTestCase):
|
|||
with pytest.raises(NoHealthCheckConfigured):
|
||||
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):
|
||||
seccomp_data = {
|
||||
'defaultAction': 'SCMP_ACT_ALLOW',
|
||||
|
|
|
@ -2397,7 +2397,8 @@ web:
|
|||
'image': 'busybox',
|
||||
'depends_on': {
|
||||
'app1': {'condition': 'service_started'},
|
||||
'app2': {'condition': 'service_healthy'}
|
||||
'app2': {'condition': 'service_healthy'},
|
||||
'app3': {'condition': 'service_completed_successfully'}
|
||||
}
|
||||
}
|
||||
override = {}
|
||||
|
@ -2409,11 +2410,12 @@ web:
|
|||
'image': 'busybox',
|
||||
'depends_on': {
|
||||
'app1': {'condition': 'service_started'},
|
||||
'app2': {'condition': 'service_healthy'}
|
||||
'app2': {'condition': 'service_healthy'},
|
||||
'app3': {'condition': 'service_completed_successfully'}
|
||||
}
|
||||
}
|
||||
override = {
|
||||
'depends_on': ['app3']
|
||||
'depends_on': ['app4']
|
||||
}
|
||||
|
||||
actual = config.merge_service_dicts(base, override, VERSION)
|
||||
|
@ -2422,7 +2424,8 @@ web:
|
|||
'depends_on': {
|
||||
'app1': {'condition': 'service_started'},
|
||||
'app2': {'condition': 'service_healthy'},
|
||||
'app3': {'condition': 'service_started'}
|
||||
'app3': {'condition': 'service_completed_successfully'},
|
||||
'app4': {'condition': 'service_started'},
|
||||
}
|
||||
}
|
||||
|
||||
|
|
Loading…
Reference in New Issue