From b9a8b391f09a8235fd8f81bbbed49d5e199bb360 Mon Sep 17 00:00:00 2001 From: Kevin Veen-Birkenbach Date: Sat, 11 Jul 2026 09:28:21 +0200 Subject: [PATCH] fix(backup): re-raise inspect failures for containers that still exist Treating every failed swarm-task inspect as skippable opened a false-green window: a transient inspect failure on a still-running, non-whitelisted container skipped the stop and backed the volume up hot while the run reported success. Re-check whether the container is still listed; only a genuinely vanished container skips, an existing one re-raises so a broken daemon keeps failing the run loudly. Covered by unit tests for both paths. Co-Authored-By: Claude Fable 5 --- src/baudolo/backup/docker.py | 9 ++++++++- tests/unit/backup/test_docker_swarm.py | 14 +++++++++++++- 2 files changed, 21 insertions(+), 2 deletions(-) diff --git a/src/baudolo/backup/docker.py b/src/baudolo/backup/docker.py index ed52065..8aea523 100644 --- a/src/baudolo/backup/docker.py +++ b/src/baudolo/backup/docker.py @@ -29,13 +29,20 @@ def is_swarm_task(container: str) -> bool: manually: the orchestrator replaces the stopped task and a later `docker start` fails on the detached overlay network. A container that vanished between listing and inspect (--rm one-shots, task-history GC) - counts as not stoppable instead of aborting the whole backup run.""" + counts as not stoppable instead of aborting the whole backup run; if the + container still exists the inspect failure re-raises, so a broken daemon + keeps failing the run loudly instead of silently skipping the stop.""" try: out = execute_shell_command( "docker inspect --format " f"'{{{{index .Config.Labels \"com.docker.swarm.task.id\"}}}}' {container}" ) except BackupException: + still_listed = execute_shell_command( + f"docker ps -a --filter name=^{container}$ --format '{{{{.Names}}}}'" + ) + if still_listed and still_listed[0].strip(): + raise return True return bool(out and out[0].strip()) diff --git a/tests/unit/backup/test_docker_swarm.py b/tests/unit/backup/test_docker_swarm.py index fc5869a..f694274 100644 --- a/tests/unit/backup/test_docker_swarm.py +++ b/tests/unit/backup/test_docker_swarm.py @@ -21,7 +21,7 @@ class TestIsSwarmTask(unittest.TestCase): @patch.object( docker_mod, "execute_shell_command", - side_effect=BackupException("gone"), + side_effect=[BackupException("gone"), []], ) def test_vanished_container_counts_as_not_stoppable(self, _mock) -> None: # A container removed between listing and inspect must not abort the @@ -29,6 +29,18 @@ class TestIsSwarmTask(unittest.TestCase): # stop/start and image-inspect path. self.assertTrue(docker_mod.is_swarm_task("gone-container")) + @patch.object( + docker_mod, + "execute_shell_command", + side_effect=[BackupException("daemon hiccup"), ["still-here"]], + ) + def test_inspect_failure_on_existing_container_still_fails(self, _mock) -> None: + # If the container still exists, an inspect failure must keep failing + # the run: silently skipping the stop would back up a hot volume and + # report green without the stop guarantee. + with self.assertRaises(BackupException): + docker_mod.is_swarm_task("still-here") + class TestFilterStoppable(unittest.TestCase): @patch.object(docker_mod, "is_swarm_task", side_effect=[False, True, False])