From a73190e1cc2c57ba2ac0361b376b2adc5c4dabeb Mon Sep 17 00:00:00 2001 From: "Nathan J. Mehl" Date: Fri, 27 Jan 2017 08:56:02 -0800 Subject: [PATCH] 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 ` 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 --- compose/cli/main.py | 47 ++++++++++++++++++- contrib/completion/bash/docker-compose | 2 +- tests/acceptance/cli_test.py | 10 ++++ .../forward-exitval/docker-compose.yml | 6 +++ 4 files changed, 63 insertions(+), 2 deletions(-) create mode 100644 tests/fixtures/forward-exitval/docker-compose.yml diff --git a/compose/cli/main.py b/compose/cli/main.py index 51ba36a09..bbd6952a0 100644 --- a/compose/cli/main.py +++ b/compose/cli/main.py @@ -854,6 +854,8 @@ class TopLevelCommand(object): running. (default: 10) --remove-orphans Remove containers for services not 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'] cascade_stop = options['--abort-on-container-exit'] @@ -861,10 +863,14 @@ class TopLevelCommand(object): timeout = timeout_from_opts(options) remove_orphans = options['--remove-orphans'] detached = options.get('-d') + forward_exitval = container_exitval_from_opts(options) if detached and cascade_stop: 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): to_attach = self.project.up( service_names=service_names, @@ -878,9 +884,11 @@ class TopLevelCommand(object): if detached: return + all_containers = filter_containers_to_service_names(to_attach, service_names) + log_printer = log_printer_from_project( self.project, - filter_containers_to_service_names(to_attach, service_names), + all_containers, options['--no-color'], {'follow': True}, cascade_stop, @@ -891,6 +899,22 @@ class TopLevelCommand(object): if cascade_stop: print("Aborting on container exit...") 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 def version(cls, options): @@ -923,6 +947,27 @@ def convergence_strategy_from_opts(options): 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): timeout = options.get('--timeout') return None if timeout is None else int(timeout) diff --git a/contrib/completion/bash/docker-compose b/contrib/completion/bash/docker-compose index 79f0fc313..979942f97 100644 --- a/contrib/completion/bash/docker-compose +++ b/contrib/completion/bash/docker-compose @@ -467,7 +467,7 @@ _docker_compose_up() { 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 diff --git a/tests/acceptance/cli_test.py b/tests/acceptance/cli_test.py index 8366ca75e..6e03c448c 100644 --- a/tests/acceptance/cli_test.py +++ b/tests/acceptance/cli_test.py @@ -1927,3 +1927,13 @@ class CLITestCase(DockerClientTestCase): self.dispatch(['up', '-d']) result = self.dispatch(['top']) 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 diff --git a/tests/fixtures/forward-exitval/docker-compose.yml b/tests/fixtures/forward-exitval/docker-compose.yml new file mode 100644 index 000000000..687e78b97 --- /dev/null +++ b/tests/fixtures/forward-exitval/docker-compose.yml @@ -0,0 +1,6 @@ +simple: + image: busybox:latest + command: sh -c "echo hello && tail -f /dev/null" +another: + image: busybox:latest + command: /bin/false