From 331931d6178ea591ae40f26714cdd42bd53c9343 Mon Sep 17 00:00:00 2001 From: Kevin Veen-Birkenbach Date: Sun, 12 Jul 2026 14:53:13 +0200 Subject: [PATCH] feat(backup)!: opt-in hard restart and mandatory backups-dir Rename --docker-compose-hard-restart-required to --hard-compose-restart and change its default from ["mailu"] to [] (nargs="*"): the compose down/up is now opt-in, so compose hosts pass "mailu" while swarm hosts, where the dir is a stack whose overlay network collides with compose up, pass nothing. Make --backups-dir mandatory (no /var/lib/backup/ default) so a run can never silently target the wrong backup root. BREAKING CHANGE: the old flag name is removed, the implicit mailu default is gone, and --backups-dir must be passed explicitly. Co-Authored-By: Claude Fable 5 --- src/baudolo/backup/app.py | 4 +-- src/baudolo/backup/cli.py | 12 +++---- tests/unit/backup/test_compose.py | 56 +++++++++++++++++++++++++++++++ 3 files changed, 63 insertions(+), 9 deletions(-) diff --git a/src/baudolo/backup/app.py b/src/baudolo/backup/app.py index 64c125d..857d3ca 100644 --- a/src/baudolo/backup/app.py +++ b/src/baudolo/backup/app.py @@ -247,8 +247,6 @@ def main() -> int: print("Finished volume backups.", flush=True) print("Handling Docker Compose services...", flush=True) - handle_docker_compose_services( - args.compose_dir, args.docker_compose_hard_restart_required - ) + handle_docker_compose_services(args.compose_dir, args.hard_compose_restart) return 0 diff --git a/src/baudolo/backup/cli.py b/src/baudolo/backup/cli.py index aff13c0..f004979 100644 --- a/src/baudolo/backup/cli.py +++ b/src/baudolo/backup/cli.py @@ -17,10 +17,10 @@ def parse_args() -> argparse.Namespace: help="Path to the parent directory containing docker-compose setups", ) p.add_argument( - "--docker-compose-hard-restart-required", - nargs="+", - default=["mailu"], - help="Compose dir names that require 'docker-compose down && up -d' (default: mailu)", + "--hard-compose-restart", + nargs="*", + default=[], + help="Compose dir names that require 'docker-compose down && up -d' (default: none; pass e.g. 'mailu' under compose where the DB cannot be backed up hot)", ) p.add_argument( @@ -35,8 +35,8 @@ def parse_args() -> argparse.Namespace: ) p.add_argument( "--backups-dir", - default="/var/lib/backup/", - help="Backup root directory (default: /var/lib/backup/)", + required=True, + help="Backup root directory (e.g. /var/lib/backup/)", ) p.add_argument( diff --git a/tests/unit/backup/test_compose.py b/tests/unit/backup/test_compose.py index bf6aa67..0eaca18 100644 --- a/tests/unit/backup/test_compose.py +++ b/tests/unit/backup/test_compose.py @@ -251,5 +251,61 @@ class TestCompose(unittest.TestCase): ) +class HardRestartArgTests(unittest.TestCase): + """The hard-restart list defaults to empty (no compose down/up); callers + opt in per dir, e.g. compose hosts pass 'mailu' while swarm hosts, where + the dir is a stack whose overlay network collides with compose up, pass + nothing.""" + + def _parse(self, extra: List[str]): + import sys + + from baudolo.backup import cli + + argv = [ + "baudolo", + "--compose-dir", + "/tmp", + "--backups-dir", + "/tmp/backup", + "--database-containers", + "postgres", + "--images-no-stop-required", + "redis", + *extra, + ] + with patch.object(sys, "argv", argv): + return cli.parse_args() + + def test_default_is_empty(self) -> None: + args = self._parse([]) + self.assertEqual(args.hard_compose_restart, []) + + def test_empty_flag_stays_empty(self) -> None: + args = self._parse(["--hard-compose-restart"]) + self.assertEqual(args.hard_compose_restart, []) + + def test_explicit_names_preserved(self) -> None: + args = self._parse(["--hard-compose-restart", "mailu", "foo"]) + self.assertEqual(args.hard_compose_restart, ["mailu", "foo"]) + + def test_backups_dir_is_required(self) -> None: + import sys + + from baudolo.backup import cli + + argv = [ + "baudolo", + "--compose-dir", + "/tmp", + "--database-containers", + "postgres", + "--images-no-stop-required", + "redis", + ] + with patch.object(sys, "argv", argv), self.assertRaises(SystemExit): + cli.parse_args() + + if __name__ == "__main__": unittest.main(verbosity=2)