From e1d938ffa6a9f697d6290a1d8d9f13b0d4ca63a3 Mon Sep 17 00:00:00 2001 From: gtebbutt <5956226+gtebbutt@users.noreply.github.com> Date: Wed, 30 Apr 2025 22:44:06 +0100 Subject: [PATCH] Add --abort-on-container-failure Signed-off-by: gtebbutt <5956226+gtebbutt@users.noreply.github.com> --- .../abort-on-container-failure.feature | 1 + podman_compose.py | 35 +++++++++++--- tests/integration/abort/__init__.py | 0 .../abort/docker-compose-fail-first.yaml | 11 +++++ .../abort/docker-compose-fail-none.yaml | 11 +++++ .../abort/docker-compose-fail-second.yaml | 11 +++++ .../docker-compose-fail-simultaneous.yaml | 11 +++++ .../abort/test_podman_compose_abort.py | 46 +++++++++++++++++++ 8 files changed, 120 insertions(+), 6 deletions(-) create mode 100644 newsfragments/abort-on-container-failure.feature create mode 100644 tests/integration/abort/__init__.py create mode 100644 tests/integration/abort/docker-compose-fail-first.yaml create mode 100644 tests/integration/abort/docker-compose-fail-none.yaml create mode 100644 tests/integration/abort/docker-compose-fail-second.yaml create mode 100644 tests/integration/abort/docker-compose-fail-simultaneous.yaml create mode 100644 tests/integration/abort/test_podman_compose_abort.py diff --git a/newsfragments/abort-on-container-failure.feature b/newsfragments/abort-on-container-failure.feature new file mode 100644 index 0000000..ae3bd1e --- /dev/null +++ b/newsfragments/abort-on-container-failure.feature @@ -0,0 +1 @@ +- Added --abort-on-container-failure option, to match docker-compose diff --git a/podman_compose.py b/podman_compose.py index 4808dd9..161bb3e 100755 --- a/podman_compose.py +++ b/podman_compose.py @@ -2978,9 +2978,20 @@ async def compose_up(compose: PodmanCompose, args): exit_code = 0 exiting = False + first_failed_task = None + while tasks: done, tasks = await asyncio.wait(tasks, return_when=asyncio.FIRST_COMPLETED) - if args.abort_on_container_exit: + + if args.abort_on_container_failure and first_failed_task is None: + # Generally a single returned item when using asyncio.FIRST_COMPLETED, but that's not + # guaranteed. If multiple tasks finish at the exact same time the choice of which + # finished "first" is arbitrary + for t in done: + if t.result() != 0: + first_failed_task = t + + if args.abort_on_container_exit or first_failed_task: if not exiting: # If 2 containers exit at the exact same time, the cancellation of the other ones # cause the status to overwrite. Sleeping for 1 seems to fix this and make it match @@ -2991,9 +3002,14 @@ async def compose_up(compose: PodmanCompose, args): t.cancel() t: Task exiting = True - for t in done: - if t.get_name() == exit_code_from: - exit_code = t.result() + if first_failed_task: + # Matches docker-compose behaviour, where the exit code of the task that triggered + # the cancellation is always propagated when aborting on failure + exit_code = first_failed_task.result() + else: + for t in done: + if t.get_name() == exit_code_from: + exit_code = t.result() return exit_code @@ -3481,7 +3497,7 @@ def compose_up_parse(parser): "--detach", action="store_true", help="Detached mode: Run container in the background, print new container name. \ - Incompatible with --abort-on-container-exit.", + Incompatible with --abort-on-container-exit and --abort-on-container-failure.", ) parser.add_argument("--no-color", action="store_true", help="Produce monochrome output.") parser.add_argument( @@ -3522,7 +3538,14 @@ def compose_up_parse(parser): parser.add_argument( "--abort-on-container-exit", action="store_true", - help="Stops all containers if any container was stopped. Incompatible with -d.", + help="Stops all containers if any container was stopped. Incompatible with -d and " + "--abort-on-container-failure.", + ) + parser.add_argument( + "--abort-on-container-failure", + action="store_true", + help="Stops all containers if any container stops with a non-zero exit code. Incompatible " + "with -d and --abort-on-container-exit.", ) parser.add_argument( "-t", diff --git a/tests/integration/abort/__init__.py b/tests/integration/abort/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/tests/integration/abort/docker-compose-fail-first.yaml b/tests/integration/abort/docker-compose-fail-first.yaml new file mode 100644 index 0000000..374fccf --- /dev/null +++ b/tests/integration/abort/docker-compose-fail-first.yaml @@ -0,0 +1,11 @@ +version: "3" +services: + sh1: + image: nopush/podman-compose-test + command: ["dumb-init", "/bin/busybox", "sh", "-c", "sleep 1; exit 1"] + sh2: + image: nopush/podman-compose-test + command: ["dumb-init", "/bin/busybox", "sh", "-c", "sleep 2; exit 0"] + sh3: + image: nopush/podman-compose-test + command: ["dumb-init", "/bin/busybox", "sh", "-c", "sleep 3; exit 0"] diff --git a/tests/integration/abort/docker-compose-fail-none.yaml b/tests/integration/abort/docker-compose-fail-none.yaml new file mode 100644 index 0000000..3156893 --- /dev/null +++ b/tests/integration/abort/docker-compose-fail-none.yaml @@ -0,0 +1,11 @@ +version: "3" +services: + sh1: + image: nopush/podman-compose-test + command: ["dumb-init", "/bin/busybox", "sh", "-c", "sleep 1; exit 0"] + sh2: + image: nopush/podman-compose-test + command: ["dumb-init", "/bin/busybox", "sh", "-c", "sleep 2; exit 0"] + sh3: + image: nopush/podman-compose-test + command: ["dumb-init", "/bin/busybox", "sh", "-c", "sleep 3; exit 0"] diff --git a/tests/integration/abort/docker-compose-fail-second.yaml b/tests/integration/abort/docker-compose-fail-second.yaml new file mode 100644 index 0000000..457bfa3 --- /dev/null +++ b/tests/integration/abort/docker-compose-fail-second.yaml @@ -0,0 +1,11 @@ +version: "3" +services: + sh1: + image: nopush/podman-compose-test + command: ["dumb-init", "/bin/busybox", "sh", "-c", "sleep 1; exit 0"] + sh2: + image: nopush/podman-compose-test + command: ["dumb-init", "/bin/busybox", "sh", "-c", "sleep 2; exit 1"] + sh3: + image: nopush/podman-compose-test + command: ["dumb-init", "/bin/busybox", "sh", "-c", "sleep 3; exit 0"] diff --git a/tests/integration/abort/docker-compose-fail-simultaneous.yaml b/tests/integration/abort/docker-compose-fail-simultaneous.yaml new file mode 100644 index 0000000..d78ec07 --- /dev/null +++ b/tests/integration/abort/docker-compose-fail-simultaneous.yaml @@ -0,0 +1,11 @@ +version: "3" +services: + sh1: + image: nopush/podman-compose-test + command: ["dumb-init", "/bin/busybox", "sh", "-c", "sleep 1; exit 1"] + sh2: + image: nopush/podman-compose-test + command: ["dumb-init", "/bin/busybox", "sh", "-c", "sleep 1; exit 0"] + sh3: + image: nopush/podman-compose-test + command: ["dumb-init", "/bin/busybox", "sh", "-c", "sleep 2; exit 0"] diff --git a/tests/integration/abort/test_podman_compose_abort.py b/tests/integration/abort/test_podman_compose_abort.py new file mode 100644 index 0000000..905b339 --- /dev/null +++ b/tests/integration/abort/test_podman_compose_abort.py @@ -0,0 +1,46 @@ +# SPDX-License-Identifier: GPL-2.0 + +import os +import unittest + +from parameterized import parameterized + +from tests.integration.test_utils import RunSubprocessMixin +from tests.integration.test_utils import podman_compose_path +from tests.integration.test_utils import test_path + + +def compose_yaml_path(failure_order): + return os.path.join(test_path(), "abort", f"docker-compose-fail-{failure_order}.yaml") + + +class TestComposeAbort(unittest.TestCase, RunSubprocessMixin): + @parameterized.expand([ + ("exit", "first", 0), + ("failure", "first", 1), + ("exit", "second", 0), + ("failure", "second", 1), + ("exit", "simultaneous", 0), + ("failure", "simultaneous", 1), + ("exit", "none", 0), + ("failure", "none", 0), + ]) + def test_abort(self, abort_type, failure_order, expected_exit_code): + try: + self.run_subprocess_assert_returncode( + [ + podman_compose_path(), + "-f", + compose_yaml_path(failure_order), + "up", + f"--abort-on-container-{abort_type}", + ], + expected_exit_code, + ) + finally: + self.run_subprocess_assert_returncode([ + podman_compose_path(), + "-f", + compose_yaml_path(failure_order), + "down", + ])