mirror of https://github.com/docker/compose.git
Add support for returning the exit value of a specific container
Current best practice for using docker-compose as a tool for continuous integration requires fragile shell pipelines to query the exit status of composed containers, e.g.: http://stackoverflow.com/questions/29568352/using-docker-compose-with-ci-how-to-deal-with-exit-codes-and-daemonized-linked http://blog.ministryofprogramming.com/docker-compose-and-exit-codes/ This PR adds a `--forward-exitval <container>` flag that allows `docker-compose up` to return the exit value of a specified container. The container may optionally have a number specified (foo_2) otherwise the first is defaulted to. Signed-off-by: Nathan J. Mehl <n@climate.com>
This commit is contained in:
parent
a953651597
commit
a73190e1cc
|
@ -854,6 +854,8 @@ class TopLevelCommand(object):
|
||||||
running. (default: 10)
|
running. (default: 10)
|
||||||
--remove-orphans Remove containers for services not
|
--remove-orphans Remove containers for services not
|
||||||
defined in the Compose file
|
defined in the Compose file
|
||||||
|
--forward-exitval SERVICE Return the exit value of the selected service container.
|
||||||
|
Requires --abort-on-container-exit.
|
||||||
"""
|
"""
|
||||||
start_deps = not options['--no-deps']
|
start_deps = not options['--no-deps']
|
||||||
cascade_stop = options['--abort-on-container-exit']
|
cascade_stop = options['--abort-on-container-exit']
|
||||||
|
@ -861,10 +863,14 @@ class TopLevelCommand(object):
|
||||||
timeout = timeout_from_opts(options)
|
timeout = timeout_from_opts(options)
|
||||||
remove_orphans = options['--remove-orphans']
|
remove_orphans = options['--remove-orphans']
|
||||||
detached = options.get('-d')
|
detached = options.get('-d')
|
||||||
|
forward_exitval = container_exitval_from_opts(options)
|
||||||
|
|
||||||
if detached and cascade_stop:
|
if detached and cascade_stop:
|
||||||
raise UserError("--abort-on-container-exit and -d cannot be combined.")
|
raise UserError("--abort-on-container-exit and -d cannot be combined.")
|
||||||
|
|
||||||
|
if forward_exitval and not cascade_stop:
|
||||||
|
raise UserError("--forward-exitval requires --abort-on-container-exit.")
|
||||||
|
|
||||||
with up_shutdown_context(self.project, service_names, timeout, detached):
|
with up_shutdown_context(self.project, service_names, timeout, detached):
|
||||||
to_attach = self.project.up(
|
to_attach = self.project.up(
|
||||||
service_names=service_names,
|
service_names=service_names,
|
||||||
|
@ -878,9 +884,11 @@ class TopLevelCommand(object):
|
||||||
if detached:
|
if detached:
|
||||||
return
|
return
|
||||||
|
|
||||||
|
all_containers = filter_containers_to_service_names(to_attach, service_names)
|
||||||
|
|
||||||
log_printer = log_printer_from_project(
|
log_printer = log_printer_from_project(
|
||||||
self.project,
|
self.project,
|
||||||
filter_containers_to_service_names(to_attach, service_names),
|
all_containers,
|
||||||
options['--no-color'],
|
options['--no-color'],
|
||||||
{'follow': True},
|
{'follow': True},
|
||||||
cascade_stop,
|
cascade_stop,
|
||||||
|
@ -891,6 +899,22 @@ class TopLevelCommand(object):
|
||||||
if cascade_stop:
|
if cascade_stop:
|
||||||
print("Aborting on container exit...")
|
print("Aborting on container exit...")
|
||||||
self.project.stop(service_names=service_names, timeout=timeout)
|
self.project.stop(service_names=service_names, timeout=timeout)
|
||||||
|
if forward_exitval:
|
||||||
|
def is_us(container):
|
||||||
|
return container.name_without_project == forward_exitval
|
||||||
|
candidates = filter(is_us, all_containers)
|
||||||
|
if not candidates:
|
||||||
|
log.error('No containers matching the spec "%s" were run.',
|
||||||
|
forward_exitval)
|
||||||
|
sys.exit(2)
|
||||||
|
if len(candidates) > 1:
|
||||||
|
log.error('Multiple (%d) containers matching the spec "%s" '
|
||||||
|
'were found; cannot forward exit code because we '
|
||||||
|
'do not know which one to.', len(candidates),
|
||||||
|
forward_exitval)
|
||||||
|
sys.exit(2)
|
||||||
|
exit_code = candidates[0].inspect()['State']['ExitCode']
|
||||||
|
sys.exit(exit_code)
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def version(cls, options):
|
def version(cls, options):
|
||||||
|
@ -923,6 +947,27 @@ def convergence_strategy_from_opts(options):
|
||||||
return ConvergenceStrategy.changed
|
return ConvergenceStrategy.changed
|
||||||
|
|
||||||
|
|
||||||
|
def container_exitval_from_opts(options):
|
||||||
|
""" Assemble a container name suitable for mapping into the
|
||||||
|
output of filter_containers_to_service_names. If the
|
||||||
|
container name ends in an underscore followed by a
|
||||||
|
positive integer, the user has deliberately specified
|
||||||
|
a container number and we believe her. Otherwise, append
|
||||||
|
`_1` to the name so as to return the exit value of the
|
||||||
|
first such named container.
|
||||||
|
"""
|
||||||
|
container_name = options.get('--forward-exitval')
|
||||||
|
if not container_name:
|
||||||
|
return None
|
||||||
|
segments = container_name.split('_')
|
||||||
|
if segments[-1].isdigit() and int(segments[-1]) > 0:
|
||||||
|
return '_'.join(segments)
|
||||||
|
else:
|
||||||
|
log.warn('"%s" does not specify a container number, '
|
||||||
|
'defaulting to "%s_1"', container_name, container_name)
|
||||||
|
return '_'.join(segments + ['1'])
|
||||||
|
|
||||||
|
|
||||||
def timeout_from_opts(options):
|
def timeout_from_opts(options):
|
||||||
timeout = options.get('--timeout')
|
timeout = options.get('--timeout')
|
||||||
return None if timeout is None else int(timeout)
|
return None if timeout is None else int(timeout)
|
||||||
|
|
|
@ -467,7 +467,7 @@ _docker_compose_up() {
|
||||||
|
|
||||||
case "$cur" in
|
case "$cur" in
|
||||||
-*)
|
-*)
|
||||||
COMPREPLY=( $( compgen -W "--abort-on-container-exit --build -d --force-recreate --help --no-build --no-color --no-deps --no-recreate --timeout -t --remove-orphans" -- "$cur" ) )
|
COMPREPLY=( $( compgen -W "--forward-exitval --abort-on-container-exit --build -d --force-recreate --help --no-build --no-color --no-deps --no-recreate --timeout -t --remove-orphans" -- "$cur" ) )
|
||||||
;;
|
;;
|
||||||
*)
|
*)
|
||||||
__docker_compose_services_all
|
__docker_compose_services_all
|
||||||
|
|
|
@ -1927,3 +1927,13 @@ class CLITestCase(DockerClientTestCase):
|
||||||
self.dispatch(['up', '-d'])
|
self.dispatch(['up', '-d'])
|
||||||
result = self.dispatch(['top'])
|
result = self.dispatch(['top'])
|
||||||
assert result.stdout.count("top") == 4
|
assert result.stdout.count("top") == 4
|
||||||
|
|
||||||
|
def test_forward_exitval(self):
|
||||||
|
self.base_dir = 'tests/fixtures/forward-exitval'
|
||||||
|
proc = start_process(
|
||||||
|
self.base_dir,
|
||||||
|
['up', '--abort-on-container-exit', '--forward-exitval', 'another'])
|
||||||
|
|
||||||
|
result = wait_on_process(proc, returncode=1)
|
||||||
|
|
||||||
|
assert 'forwardexitval_another_1 exited with code 1' in result.stdout
|
||||||
|
|
|
@ -0,0 +1,6 @@
|
||||||
|
simple:
|
||||||
|
image: busybox:latest
|
||||||
|
command: sh -c "echo hello && tail -f /dev/null"
|
||||||
|
another:
|
||||||
|
image: busybox:latest
|
||||||
|
command: /bin/false
|
Loading…
Reference in New Issue