Add --abort-on-container-failure

Signed-off-by: gtebbutt <5956226+gtebbutt@users.noreply.github.com>
This commit is contained in:
gtebbutt 2025-04-30 22:44:06 +01:00 committed by Povilas Kanapickas
parent d532e09d7d
commit e1d938ffa6
8 changed files with 120 additions and 6 deletions

View File

@ -0,0 +1 @@
- Added --abort-on-container-failure option, to match docker-compose

View File

@ -2978,9 +2978,20 @@ async def compose_up(compose: PodmanCompose, args):
exit_code = 0 exit_code = 0
exiting = False exiting = False
first_failed_task = None
while tasks: while tasks:
done, tasks = await asyncio.wait(tasks, return_when=asyncio.FIRST_COMPLETED) 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 not exiting:
# If 2 containers exit at the exact same time, the cancellation of the other ones # 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 # 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.cancel()
t: Task t: Task
exiting = True exiting = True
for t in done: if first_failed_task:
if t.get_name() == exit_code_from: # Matches docker-compose behaviour, where the exit code of the task that triggered
exit_code = t.result() # 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 return exit_code
@ -3481,7 +3497,7 @@ def compose_up_parse(parser):
"--detach", "--detach",
action="store_true", action="store_true",
help="Detached mode: Run container in the background, print new container name. \ 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("--no-color", action="store_true", help="Produce monochrome output.")
parser.add_argument( parser.add_argument(
@ -3522,7 +3538,14 @@ def compose_up_parse(parser):
parser.add_argument( parser.add_argument(
"--abort-on-container-exit", "--abort-on-container-exit",
action="store_true", 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( parser.add_argument(
"-t", "-t",

View File

View File

@ -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"]

View File

@ -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"]

View File

@ -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"]

View File

@ -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"]

View File

@ -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",
])