From 8c1a6cc465cf1b783e1ae3793c6f02491816036c Mon Sep 17 00:00:00 2001 From: Kevin Veen-Birkenbach Date: Sun, 12 Jul 2026 18:18:57 +0200 Subject: [PATCH 1/2] refactor(restore)!: restore volume files via direct host rsync Drop the alpine-rsync helper image: resolve the volume mountpoint with docker volume inspect and rsync the backup straight into it. Removes the --rsync-image flag and the E2E_RSYNC_IMAGE pre-pull; the e2e test container now mounts /var/lib/docker rw so the direct restore can write. BREAKING CHANGE: the restore 'files' subcommand no longer accepts --rsync-image; rsync must be available on the host running baudolo. Co-Authored-By: Claude Fable 5 --- scripts/test-e2e.sh | 11 ++-------- src/baudolo/restore/__main__.py | 5 ----- src/baudolo/restore/files.py | 36 ++++++++++++++++----------------- 3 files changed, 19 insertions(+), 33 deletions(-) diff --git a/scripts/test-e2e.sh b/scripts/test-e2e.sh index 86cb761..f40e951 100755 --- a/scripts/test-e2e.sh +++ b/scripts/test-e2e.sh @@ -27,7 +27,6 @@ dind() { docker exec "${DIND}" docker "$@"; } dind_stdin() { docker exec -i "${DIND}" docker "$@"; } IMG="${E2E_IMAGE:-baudolo:local}" -RSYNC_IMG="${E2E_RSYNC_IMAGE:-ghcr.io/kevinveenbirkenbach/alpine-rsync}" READY_TIMEOUT_SECONDS="${E2E_READY_TIMEOUT_SECONDS:-120}" ARTIFACTS_DIR="${E2E_ARTIFACTS_DIR:-./artifacts}" @@ -164,10 +163,6 @@ for i in $(seq 1 "${READY_TIMEOUT_SECONDS}"); do fi done -log "Pre-pulling helper images in DinD..." -log " - Pulling: ${RSYNC_IMG}" -dind pull "${RSYNC_IMG}" - log "Ensuring alpine exists in DinD (for debug helpers)" dind pull alpine:3.20 >/dev/null @@ -181,8 +176,7 @@ if [ "${DEBUG_SHELL}" = "1" ]; then docker run --rm -it \ --network "${NET}" \ -e DOCKER_HOST="${DIND_HOST_IN_NET}" \ - -e E2E_RSYNC_IMAGE="${RSYNC_IMG}" \ - -v "${DIND_VOL}:/var/lib/docker:ro" \ + -v "${DIND_VOL}:/var/lib/docker" \ -v "${E2E_TMP_VOL}:/tmp" \ "${IMG}" \ bash -lc ' @@ -200,8 +194,7 @@ else docker run --rm \ --network "${NET}" \ -e DOCKER_HOST="${DIND_HOST_IN_NET}" \ - -e E2E_RSYNC_IMAGE="${RSYNC_IMG}" \ - -v "${DIND_VOL}:/var/lib/docker:ro" \ + -v "${DIND_VOL}:/var/lib/docker" \ -v "${E2E_TMP_VOL}:/tmp" \ "${IMG}" \ bash -lc ' diff --git a/src/baudolo/restore/__main__.py b/src/baudolo/restore/__main__.py index 3ad7629..c68f324 100644 --- a/src/baudolo/restore/__main__.py +++ b/src/baudolo/restore/__main__.py @@ -38,10 +38,6 @@ def main(argv: list[str] | None = None) -> int: # ------------------------------------------------------------------ p_files = sub.add_parser("files", help="Restore files into a docker volume") _add_common_backup_args(p_files) - p_files.add_argument( - "--rsync-image", - default="ghcr.io/kevinveenbirkenbach/alpine-rsync", - ) p_files.add_argument( "--source-volume", default=None, @@ -95,7 +91,6 @@ def main(argv: list[str] | None = None) -> int: return restore_volume_files( args.volume_name, bp_files.files_dir(), - rsync_image=args.rsync_image, ) if args.cmd == "postgres": diff --git a/src/baudolo/restore/files.py b/src/baudolo/restore/files.py index 6450572..e10eed3 100644 --- a/src/baudolo/restore/files.py +++ b/src/baudolo/restore/files.py @@ -3,12 +3,10 @@ from __future__ import annotations import os import sys -from .run import run, docker_volume_exists +from .run import docker_volume_exists, run -def restore_volume_files( - volume_name: str, backup_files_dir: str, *, rsync_image: str -) -> int: +def restore_volume_files(volume_name: str, backup_files_dir: str) -> int: if not os.path.isdir(backup_files_dir): print(f"ERROR: backup files dir not found: {backup_files_dir}", file=sys.stderr) return 2 @@ -19,21 +17,21 @@ def restore_volume_files( else: print(f"Volume {volume_name} already exists.") - # Keep behavior close to the old script: rsync -avv --delete - run( - [ - "docker", - "run", - "--rm", - "-v", - f"{volume_name}:/recover/", - "-v", - f"{backup_files_dir}:/backup/", - rsync_image, - "sh", - "-lc", - "rsync -avv --delete /backup/ /recover/", - ] + cp = run( + ["docker", "volume", "inspect", "--format", "{{ .Mountpoint }}", volume_name], + capture=True, ) + raw = cp.stdout or b"" + mountpoint = (raw.decode() if isinstance(raw, bytes) else raw).strip() + if not mountpoint: + print( + f"ERROR: could not resolve mountpoint for volume {volume_name}", + file=sys.stderr, + ) + return 2 + + src = os.path.join(backup_files_dir, "") + dest = os.path.join(mountpoint, "") + run(["rsync", "-avv", "--delete", src, dest]) print("File restore complete.") return 0 From f9776ac47a2a0736906e4073257ef306001d0598 Mon Sep 17 00:00:00 2001 From: Kevin Veen-Birkenbach Date: Sun, 12 Jul 2026 18:19:20 +0200 Subject: [PATCH 2/2] feat(backup)!: exact --images-* matching and --hard-restart-projects Match --images-no-stop-required and --images-no-backup-required against the container's exact .Config.Image instead of a substring, so callers pass full repo:tag references (registry prefix included) and near-miss image names no longer flip the stop/skip decision. Rename the opt-in --hard-compose-restart flag to --hard-restart-projects. The e2e suite pins a SPOT for the DB images and in-container data dirs (postgres:alpine at /var/lib/postgresql, mariadb:latest at /var/lib/mysql) and passes exact image refs to the --images-* flags. BREAKING CHANGE: --images-* now require exact image references, not substrings; --hard-compose-restart is renamed to --hard-restart-projects. Co-Authored-By: Claude Fable 5 --- src/baudolo/backup/app.py | 10 +++++----- src/baudolo/backup/cli.py | 6 +++--- tests/e2e/helpers.py | 10 +++++++++- .../e2e/test_e2e_dump_only_fallback_to_files.py | 17 +++++++---------- tests/e2e/test_e2e_dump_only_sql_mixed_run.py | 11 +++++------ tests/e2e/test_e2e_files_full.py | 4 +--- tests/e2e/test_e2e_files_no_copy.py | 2 +- ..._e2e_images_no_backup_required_early_skip.py | 10 +++------- .../test_e2e_mariadb_anonymous_preemption.py | 6 ++++-- tests/e2e/test_e2e_mariadb_full.py | 8 +++++--- tests/e2e/test_e2e_mariadb_no_copy.py | 8 +++++--- tests/e2e/test_e2e_postgres_full.py | 8 +++++--- tests/e2e/test_e2e_postgres_no_copy.py | 8 +++++--- ..._seed_star_and_db_entries_backup_postgres.py | 11 +++++------ tests/unit/backup/test_compose.py | 10 +++++----- tests/unit/test_backup.py | 13 ++++++++++--- 16 files changed, 78 insertions(+), 64 deletions(-) diff --git a/src/baudolo/backup/app.py b/src/baudolo/backup/app.py index 857d3ca..2829a52 100644 --- a/src/baudolo/backup/app.py +++ b/src/baudolo/backup/app.py @@ -52,7 +52,7 @@ def is_image_ignored(container: str, images_no_backup_required: list[str]) -> bo if not images_no_backup_required: return False img = get_image_info(container) - return any(pat in img for pat in images_no_backup_required) + return img in images_no_backup_required def volume_is_fully_ignored( @@ -68,15 +68,15 @@ def volume_is_fully_ignored( def requires_stop(containers: list[str], images_no_stop_required: list[str]) -> bool: """ - Stop is required if ANY stoppable container image is NOT in the - whitelist patterns. Swarm task containers never count: baudolo must + Stop is required if ANY stoppable container image is NOT in the exact + image whitelist. Swarm task containers never count: baudolo must not cycle them (see docker.is_swarm_task). """ for c in containers: if is_swarm_task(c): continue img = get_image_info(c) - if not any(pat in img for pat in images_no_stop_required): + if img not in images_no_stop_required: return True return False @@ -247,6 +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.hard_compose_restart) + handle_docker_compose_services(args.compose_dir, args.hard_restart_projects) return 0 diff --git a/src/baudolo/backup/cli.py b/src/baudolo/backup/cli.py index f004979..44278e7 100644 --- a/src/baudolo/backup/cli.py +++ b/src/baudolo/backup/cli.py @@ -17,7 +17,7 @@ def parse_args() -> argparse.Namespace: help="Path to the parent directory containing docker-compose setups", ) p.add_argument( - "--hard-compose-restart", + "--hard-restart-projects", 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)", @@ -49,13 +49,13 @@ def parse_args() -> argparse.Namespace: "--images-no-stop-required", nargs="+", required=True, - help="Image name patterns for which containers should not be stopped during file backup", + help="Exact image references (repo:tag, incl. any registry prefix) whose containers must not be stopped during file backup", ) p.add_argument( "--images-no-backup-required", nargs="+", default=[], - help="Image name patterns for which no backup should be performed", + help="Exact image references (repo:tag, incl. any registry prefix) for which no backup should be performed", ) p.add_argument( diff --git a/tests/e2e/helpers.py b/tests/e2e/helpers.py index 2d4a609..56e6f4b 100644 --- a/tests/e2e/helpers.py +++ b/tests/e2e/helpers.py @@ -7,6 +7,14 @@ import time import uuid from pathlib import Path +# SPOT for the database images and their in-container data dirs the e2e +# suite runs against. postgres:alpine tracks latest (18+), which mounts at +# /var/lib/postgresql (not /var/lib/postgresql/data); bump here only. +POSTGRES_IMAGE = "postgres:alpine" +POSTGRES_DATA_DIR = "/var/lib/postgresql" +MARIADB_IMAGE = "mariadb:latest" +MARIADB_DATA_DIR = "/var/lib/mysql" + def run( cmd: list[str], @@ -172,7 +180,7 @@ def backup_run( "baudolo", "--compose-dir", compose_dir, - "--docker-compose-hard-restart-required", + "--hard-restart-projects", "mailu", "--repo-name", repo_name, diff --git a/tests/e2e/test_e2e_dump_only_fallback_to_files.py b/tests/e2e/test_e2e_dump_only_fallback_to_files.py index fa458c1..9f44915 100644 --- a/tests/e2e/test_e2e_dump_only_fallback_to_files.py +++ b/tests/e2e/test_e2e_dump_only_fallback_to_files.py @@ -2,6 +2,8 @@ import unittest from .helpers import ( + POSTGRES_IMAGE, + POSTGRES_DATA_DIR, backup_path, cleanup_docker, create_minimal_compose_dir, @@ -50,8 +52,8 @@ class TestE2EDumpOnlyFallbackToFiles(unittest.TestCase): "-e", "POSTGRES_USER=postgres", "-v", - f"{cls.pg_volume}:/var/lib/postgresql/data", - "postgres:16", + f"{cls.pg_volume}:{POSTGRES_DATA_DIR}", + POSTGRES_IMAGE, ] ) wait_for_postgres(cls.pg_container, user="postgres", timeout_s=90) @@ -65,7 +67,7 @@ class TestE2EDumpOnlyFallbackToFiles(unittest.TestCase): cls.pg_container, "sh", "-lc", - f"echo '{cls.marker}' > /var/lib/postgresql/data/marker.txt", + f"echo '{cls.marker}' > {POSTGRES_DATA_DIR}/marker.txt", ] ) @@ -79,7 +81,7 @@ class TestE2EDumpOnlyFallbackToFiles(unittest.TestCase): "baudolo", "--compose-dir", cls.compose_dir, - "--docker-compose-hard-restart-required", + "--hard-restart-projects", "mailu", "--repo-name", cls.repo_name, @@ -90,10 +92,7 @@ class TestE2EDumpOnlyFallbackToFiles(unittest.TestCase): "--database-containers", cls.pg_container, "--images-no-stop-required", - "postgres", - "mariadb", - "mysql", - "alpine", + POSTGRES_IMAGE, "--dump-only-sql", ] cp = run(cmd, capture=True, check=True) @@ -116,8 +115,6 @@ class TestE2EDumpOnlyFallbackToFiles(unittest.TestCase): cls.repo_name, "--source-volume", cls.pg_volume, - "--rsync-image", - "ghcr.io/kevinveenbirkenbach/alpine-rsync", ] ) diff --git a/tests/e2e/test_e2e_dump_only_sql_mixed_run.py b/tests/e2e/test_e2e_dump_only_sql_mixed_run.py index ae94df8..fd5be66 100644 --- a/tests/e2e/test_e2e_dump_only_sql_mixed_run.py +++ b/tests/e2e/test_e2e_dump_only_sql_mixed_run.py @@ -1,6 +1,8 @@ import unittest from .helpers import ( + POSTGRES_IMAGE, + POSTGRES_DATA_DIR, backup_path, cleanup_docker, create_minimal_compose_dir, @@ -70,8 +72,8 @@ class TestE2EDumpOnlySqlMixedRun(unittest.TestCase): "-e", f"POSTGRES_PASSWORD={cls.pg_password}", "-v", - f"{cls.db_volume}:/var/lib/postgresql/data", - "postgres:16-alpine", + f"{cls.db_volume}:{POSTGRES_DATA_DIR}", + POSTGRES_IMAGE, ] ) wait_for_postgres(cls.pg_container, user="postgres", timeout_s=90) @@ -122,10 +124,7 @@ class TestE2EDumpOnlySqlMixedRun(unittest.TestCase): "--database-containers", cls.pg_container, "--images-no-stop-required", - "alpine", - "postgres", - "mariadb", - "mysql", + POSTGRES_IMAGE, "--dump-only-sql", "--backups-dir", cls.backups_dir, diff --git a/tests/e2e/test_e2e_files_full.py b/tests/e2e/test_e2e_files_full.py index e40d18b..1223669 100644 --- a/tests/e2e/test_e2e_files_full.py +++ b/tests/e2e/test_e2e_files_full.py @@ -57,7 +57,7 @@ class TestE2EFilesFull(unittest.TestCase): compose_dir=cls.compose_dir, databases_csv=cls.databases_csv, database_containers=["dummy-db"], - images_no_stop_required=["alpine", "postgres", "mariadb", "mysql"], + images_no_stop_required=["alpine:3.20"], ) cls.hash, cls.version = latest_version_dir(cls.backups_dir, cls.repo_name) @@ -94,8 +94,6 @@ class TestE2EFilesFull(unittest.TestCase): self.repo_name, "--source-volume", self.volume_src, - "--rsync-image", - "ghcr.io/kevinveenbirkenbach/alpine-rsync", ] ) diff --git a/tests/e2e/test_e2e_files_no_copy.py b/tests/e2e/test_e2e_files_no_copy.py index 6f6a892..8ed2216 100644 --- a/tests/e2e/test_e2e_files_no_copy.py +++ b/tests/e2e/test_e2e_files_no_copy.py @@ -55,7 +55,7 @@ class TestE2EFilesNoCopy(unittest.TestCase): compose_dir=cls.compose_dir, databases_csv=cls.databases_csv, database_containers=["dummy-db"], - images_no_stop_required=["alpine", "postgres", "mariadb", "mysql"], + images_no_stop_required=["alpine:3.20"], dump_only_sql=True, ) diff --git a/tests/e2e/test_e2e_images_no_backup_required_early_skip.py b/tests/e2e/test_e2e_images_no_backup_required_early_skip.py index 619d38f..76d54b2 100644 --- a/tests/e2e/test_e2e_images_no_backup_required_early_skip.py +++ b/tests/e2e/test_e2e_images_no_backup_required_early_skip.py @@ -76,7 +76,7 @@ class TestE2EImagesNoBackupRequiredEarlySkip(unittest.TestCase): "baudolo", "--compose-dir", cls.compose_dir, - "--docker-compose-hard-restart-required", + "--hard-restart-projects", "mailu", "--repo-name", cls.repo_name, @@ -87,13 +87,9 @@ class TestE2EImagesNoBackupRequiredEarlySkip(unittest.TestCase): "--database-containers", "dummy-db", "--images-no-stop-required", - "alpine", - "redis", - "postgres", - "mariadb", - "mysql", + "redis:alpine", "--images-no-backup-required", - "redis", + "redis:alpine", ] cp = run(cmd, capture=True, check=True) cls.stdout = cp.stdout or "" diff --git a/tests/e2e/test_e2e_mariadb_anonymous_preemption.py b/tests/e2e/test_e2e_mariadb_anonymous_preemption.py index c16f745..4a9171b 100644 --- a/tests/e2e/test_e2e_mariadb_anonymous_preemption.py +++ b/tests/e2e/test_e2e_mariadb_anonymous_preemption.py @@ -30,6 +30,8 @@ import pandas from baudolo.backup import db as db_mod from .helpers import ( + MARIADB_IMAGE, + MARIADB_DATA_DIR, cleanup_docker, require_docker, run, @@ -69,8 +71,8 @@ class TestE2EMariaDBAnonymousPreemption(unittest.TestCase): "-e", f"MARIADB_ROOT_PASSWORD={cls.root_password}", "-v", - f"{cls.db_volume}:/var/lib/mysql", - "mariadb:12.2", + f"{cls.db_volume}:{MARIADB_DATA_DIR}", + MARIADB_IMAGE, ] ) diff --git a/tests/e2e/test_e2e_mariadb_full.py b/tests/e2e/test_e2e_mariadb_full.py index 3f23b9a..7541ed4 100644 --- a/tests/e2e/test_e2e_mariadb_full.py +++ b/tests/e2e/test_e2e_mariadb_full.py @@ -2,6 +2,8 @@ import unittest from .helpers import ( + MARIADB_IMAGE, + MARIADB_DATA_DIR, backup_run, backup_path, cleanup_docker, @@ -56,8 +58,8 @@ class TestE2EMariaDBFull(unittest.TestCase): "-e", f"MARIADB_PASSWORD={cls.db_password}", "-v", - f"{cls.db_volume}:/var/lib/mysql", - "mariadb:11", + f"{cls.db_volume}:{MARIADB_DATA_DIR}", + MARIADB_IMAGE, ] ) @@ -97,7 +99,7 @@ class TestE2EMariaDBFull(unittest.TestCase): compose_dir=cls.compose_dir, databases_csv=cls.databases_csv, database_containers=[cls.db_container], - images_no_stop_required=["mariadb", "mysql", "alpine", "postgres"], + images_no_stop_required=[MARIADB_IMAGE], ) cls.hash, cls.version = latest_version_dir(cls.backups_dir, cls.repo_name) diff --git a/tests/e2e/test_e2e_mariadb_no_copy.py b/tests/e2e/test_e2e_mariadb_no_copy.py index 68a0dca..a0272a8 100644 --- a/tests/e2e/test_e2e_mariadb_no_copy.py +++ b/tests/e2e/test_e2e_mariadb_no_copy.py @@ -2,6 +2,8 @@ import unittest from .helpers import ( + MARIADB_IMAGE, + MARIADB_DATA_DIR, backup_run, backup_path, cleanup_docker, @@ -55,8 +57,8 @@ class TestE2EMariaDBNoCopy(unittest.TestCase): "-e", f"MARIADB_PASSWORD={cls.db_password}", "-v", - f"{cls.db_volume}:/var/lib/mysql", - "mariadb:11", + f"{cls.db_volume}:{MARIADB_DATA_DIR}", + MARIADB_IMAGE, ] ) @@ -94,7 +96,7 @@ class TestE2EMariaDBNoCopy(unittest.TestCase): compose_dir=cls.compose_dir, databases_csv=cls.databases_csv, database_containers=[cls.db_container], - images_no_stop_required=["mariadb", "mysql", "alpine", "postgres"], + images_no_stop_required=[MARIADB_IMAGE], dump_only_sql=True, ) diff --git a/tests/e2e/test_e2e_postgres_full.py b/tests/e2e/test_e2e_postgres_full.py index fde00ad..4124812 100644 --- a/tests/e2e/test_e2e_postgres_full.py +++ b/tests/e2e/test_e2e_postgres_full.py @@ -2,6 +2,8 @@ import unittest from .helpers import ( + POSTGRES_IMAGE, + POSTGRES_DATA_DIR, backup_run, backup_path, cleanup_docker, @@ -47,8 +49,8 @@ class TestE2EPostgresFull(unittest.TestCase): "-e", "POSTGRES_USER=postgres", "-v", - f"{cls.pg_volume}:/var/lib/postgresql/data", - "postgres:16", + f"{cls.pg_volume}:{POSTGRES_DATA_DIR}", + POSTGRES_IMAGE, ] ) wait_for_postgres(cls.pg_container, user="postgres", timeout_s=90) @@ -76,7 +78,7 @@ class TestE2EPostgresFull(unittest.TestCase): compose_dir=cls.compose_dir, databases_csv=cls.databases_csv, database_containers=[cls.pg_container], - images_no_stop_required=["postgres", "mariadb", "mysql", "alpine"], + images_no_stop_required=[POSTGRES_IMAGE], ) cls.hash, cls.version = latest_version_dir(cls.backups_dir, cls.repo_name) diff --git a/tests/e2e/test_e2e_postgres_no_copy.py b/tests/e2e/test_e2e_postgres_no_copy.py index 28570a8..96961c4 100644 --- a/tests/e2e/test_e2e_postgres_no_copy.py +++ b/tests/e2e/test_e2e_postgres_no_copy.py @@ -2,6 +2,8 @@ import unittest from .helpers import ( + POSTGRES_IMAGE, + POSTGRES_DATA_DIR, backup_run, backup_path, cleanup_docker, @@ -46,8 +48,8 @@ class TestE2EPostgresNoCopy(unittest.TestCase): "-e", "POSTGRES_USER=postgres", "-v", - f"{cls.pg_volume}:/var/lib/postgresql/data", - "postgres:16", + f"{cls.pg_volume}:{POSTGRES_DATA_DIR}", + POSTGRES_IMAGE, ] ) wait_for_postgres(cls.pg_container, user="postgres", timeout_s=90) @@ -74,7 +76,7 @@ class TestE2EPostgresNoCopy(unittest.TestCase): compose_dir=cls.compose_dir, databases_csv=cls.databases_csv, database_containers=[cls.pg_container], - images_no_stop_required=["postgres", "mariadb", "mysql", "alpine"], + images_no_stop_required=[POSTGRES_IMAGE], dump_only_sql=True, ) diff --git a/tests/e2e/test_e2e_seed_star_and_db_entries_backup_postgres.py b/tests/e2e/test_e2e_seed_star_and_db_entries_backup_postgres.py index 88a11d7..7a0997f 100644 --- a/tests/e2e/test_e2e_seed_star_and_db_entries_backup_postgres.py +++ b/tests/e2e/test_e2e_seed_star_and_db_entries_backup_postgres.py @@ -1,6 +1,8 @@ import unittest from .helpers import ( + POSTGRES_IMAGE, + POSTGRES_DATA_DIR, backup_path, cleanup_docker, create_minimal_compose_dir, @@ -66,8 +68,8 @@ class TestE2ESeedStarAndDbEntriesBackupPostgres(unittest.TestCase): "-e", f"POSTGRES_PASSWORD={cls.pg_password}", "-v", - f"{cls.db_volume}:/var/lib/postgresql/data", - "postgres:16-alpine", + f"{cls.db_volume}:{POSTGRES_DATA_DIR}", + POSTGRES_IMAGE, ] ) wait_for_postgres(cls.pg_container, user="postgres", timeout_s=90) @@ -165,10 +167,7 @@ class TestE2ESeedStarAndDbEntriesBackupPostgres(unittest.TestCase): "--database-containers", cls.pg_container, "--images-no-stop-required", - "alpine", - "postgres", - "mariadb", - "mysql", + POSTGRES_IMAGE, "--dump-only-sql", "--backups-dir", cls.backups_dir, diff --git a/tests/unit/backup/test_compose.py b/tests/unit/backup/test_compose.py index 0eaca18..8f846e1 100644 --- a/tests/unit/backup/test_compose.py +++ b/tests/unit/backup/test_compose.py @@ -279,15 +279,15 @@ class HardRestartArgTests(unittest.TestCase): def test_default_is_empty(self) -> None: args = self._parse([]) - self.assertEqual(args.hard_compose_restart, []) + self.assertEqual(args.hard_restart_projects, []) def test_empty_flag_stays_empty(self) -> None: - args = self._parse(["--hard-compose-restart"]) - self.assertEqual(args.hard_compose_restart, []) + args = self._parse(["--hard-restart-projects"]) + self.assertEqual(args.hard_restart_projects, []) def test_explicit_names_preserved(self) -> None: - args = self._parse(["--hard-compose-restart", "mailu", "foo"]) - self.assertEqual(args.hard_compose_restart, ["mailu", "foo"]) + args = self._parse(["--hard-restart-projects", "mailu", "foo"]) + self.assertEqual(args.hard_restart_projects, ["mailu", "foo"]) def test_backups_dir_is_required(self) -> None: import sys diff --git a/tests/unit/test_backup.py b/tests/unit/test_backup.py index 9a52ebb..29746e4 100644 --- a/tests/unit/test_backup.py +++ b/tests/unit/test_backup.py @@ -10,13 +10,12 @@ class TestRequiresStop(unittest.TestCase): def test_requires_stop_false_when_all_images_are_whitelisted( self, mock_get_image_info, _mock_is_swarm_task ): - # All containers use images containing allowed substrings mock_get_image_info.side_effect = [ "repo/mastodon:v4", "repo/wordpress:latest", ] containers = ["c1", "c2"] - whitelist = ["mastodon", "wordpress"] + whitelist = ["repo/mastodon:v4", "repo/wordpress:latest"] self.assertFalse(requires_stop(containers, whitelist)) @patch("baudolo.backup.app.get_image_info") @@ -28,9 +27,17 @@ class TestRequiresStop(unittest.TestCase): "repo/nginx:latest", ] containers = ["c1", "c2"] - whitelist = ["mastodon", "wordpress"] + whitelist = ["repo/mastodon:v4", "repo/wordpress:latest"] self.assertTrue(requires_stop(containers, whitelist)) + @patch("baudolo.backup.app.get_image_info") + def test_requires_stop_true_on_substring_only_match( + self, mock_get_image_info, _mock_is_swarm_task + ): + mock_get_image_info.return_value = "reg:5000/repo/mastodon:v4" + self.assertTrue(requires_stop(["c1"], ["mastodon"])) + self.assertTrue(requires_stop(["c1"], ["repo/mastodon:v4"])) + @patch("baudolo.backup.app.get_image_info") def test_requires_stop_true_when_whitelist_empty( self, mock_get_image_info, _mock_is_swarm_task