mirror of
https://github.com/docker/compose.git
synced 2025-07-20 20:24:30 +02:00
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": {
|
"properties": {
|
||||||
"condition": {
|
"condition": {
|
||||||
"type": "string",
|
"type": "string",
|
||||||
"enum": ["service_started", "service_healthy"]
|
"enum": ["service_started", "service_healthy", "service_completed_successfully"]
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"required": ["condition"]
|
"required": ["condition"]
|
||||||
|
@ -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)
|
||||||
|
@ -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)
|
||||||
|
@ -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:
|
||||||
|
@ -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',
|
||||||
|
@ -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'},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Loading…
x
Reference in New Issue
Block a user