Browse Source

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 <[email protected]>
Nathan J. Mehl 8 years ago
parent
commit
a73190e1cc

+ 46 - 1
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)

+ 1 - 1
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

+ 10 - 0
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

+ 6 - 0
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