fix(backup): detect the engine by its dump tool, not by the image name

36b2336 matched postgres and mariadb against the image's repository path. That name is not a property of the software: a dedicated Postgres inside an app's own stack is tagged <app>-database or postgis/postgis and carries no engine token at all, so it was never recognised, never dumped, and its data directory was copied as files without a single warning - the fallback notice hangs on found_db, which stayed false.

The container is now asked what it can run. pg_dumpall, mariadb-dump and mysqldump are probed by executing them, not by asking a shell for them, because a distroless image has no shell and would deny every tool it ships. The verdict is cached per image ID rather than per container, so replicas of one image cost a single probe.

Because the probe names the tool it found, the dump uses it instead of the hardcoded /usr/bin/mariadb-dump, which makes an image that ships only mysqldump dumpable rather than silently file-copied.

image_name and has_image are gone with their registry-host and tag stripping; the trap they worked around cannot occur when nothing reads the name. get_image_info stays, since the --images-* lists match exact references.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-08-17 13:51:26 +02:00
parent c03329e4ca
commit f791046c02
9 changed files with 448 additions and 85 deletions

View File

@@ -8,7 +8,45 @@ import pandas
from pandas.errors import EmptyDataError
from .db import backup_database
from .docker import has_image
from .docker import has_tool, image_id
DUMP_TOOLS: tuple[tuple[str, str], ...] = (
("postgres", "pg_dumpall"),
("mariadb", "mariadb-dump"),
("mariadb", "mysqldump"),
)
_ENGINE_BY_IMAGE: dict[str, tuple[str, str] | None] = {}
def container_engine(container: str) -> tuple[str, str] | None:
"""The (engine, dump tool) a container can serve, or None for neither.
Asks the container what it can run instead of reading its image name. A
dedicated Postgres is tagged `<app>-database` or `postgis/postgis` and
carries no engine token at all, while a swarm registry host such as
`svc-db-mariadb-swarm-mgr-01:5000` carries the wrong one.
Args:
container: must be running - `docker exec` is the probe, and a
stopped container would be cached as "no engine" for its whole
image. The only caller feeds it `docker ps` output.
Returns:
The engine and the tool that dumps it, cached per image ID so that
replicas of one image are probed once.
"""
image = image_id(container)
if image not in _ENGINE_BY_IMAGE:
_ENGINE_BY_IMAGE[image] = next(
(
(engine, tool)
for engine, tool in DUMP_TOOLS
if has_tool(container, tool)
),
None,
)
return _ENGINE_BY_IMAGE[image]
def backup_mariadb_or_postgres(
@@ -21,17 +59,19 @@ def backup_mariadb_or_postgres(
"""
Returns (is_db_container, dumped_any)
"""
for img in ["mariadb", "postgres"]:
if has_image(container, img):
dumped = backup_database(
container=container,
volume_dir=volume_dir,
db_type=img,
databases_df=databases_df,
database_containers=database_containers,
)
return True, dumped
return False, False
engine = container_engine(container)
if engine is None:
return False, False
db_type, dump_tool = engine
dumped = backup_database(
container=container,
volume_dir=volume_dir,
db_type=db_type,
dump_tool=dump_tool,
databases_df=databases_df,
database_containers=database_containers,
)
return True, dumped
def _empty_databases_df() -> pandas.DataFrame: