Merge branch 'feat/exact-image-match'

# Conflicts:
#	tests/e2e/helpers.py
#	tests/e2e/test_e2e_dump_only_fallback_to_files.py
#	tests/e2e/test_e2e_images_no_backup_required_early_skip.py
This commit is contained in:
2026-07-12 18:27:05 +02:00
15 changed files with 37 additions and 43 deletions

View File

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

View File

@@ -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(

View File

@@ -180,7 +180,7 @@ def backup_run(
"baudolo",
"--compose-dir",
compose_dir,
"--hard-compose-restart",
"--hard-restart-projects",
"mailu",
"--repo-name",
repo_name,

View File

@@ -81,7 +81,7 @@ class TestE2EDumpOnlyFallbackToFiles(unittest.TestCase):
"baudolo",
"--compose-dir",
cls.compose_dir,
"--hard-compose-restart",
"--hard-restart-projects",
"mailu",
"--repo-name",
cls.repo_name,
@@ -92,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)

View File

@@ -124,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,

View File

@@ -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)

View File

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

View File

@@ -76,7 +76,7 @@ class TestE2EImagesNoBackupRequiredEarlySkip(unittest.TestCase):
"baudolo",
"--compose-dir",
cls.compose_dir,
"--hard-compose-restart",
"--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 ""

View File

@@ -99,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)

View File

@@ -96,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,
)

View File

@@ -78,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)

View File

@@ -76,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,
)

View File

@@ -167,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,

View File

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

View File

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