mirror of
https://github.com/kevinveenbirkenbach/docker-volume-backup.git
synced 2026-08-20 21:22:54 +00:00
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:
@@ -69,12 +69,17 @@ def backup_database(
|
||||
container: str,
|
||||
volume_dir: str,
|
||||
db_type: str,
|
||||
dump_tool: str,
|
||||
databases_df: pandas.DataFrame,
|
||||
database_containers: list[str],
|
||||
) -> bool:
|
||||
"""
|
||||
Backup databases for a given DB container.
|
||||
|
||||
Args:
|
||||
dump_tool: the MariaDB client found in the container, so an image
|
||||
that ships only mysqldump is dumped with the tool it has.
|
||||
|
||||
Returns True if at least one dump was produced.
|
||||
"""
|
||||
instance_name = get_instance(container, database_containers)
|
||||
@@ -114,7 +119,7 @@ def backup_database(
|
||||
if db_type == "mariadb":
|
||||
# Force TCP so auth matches '<user>'@'%' instead of socket -> 'localhost'.
|
||||
cmd = (
|
||||
f"docker exec {container} /usr/bin/mariadb-dump "
|
||||
f"docker exec {container} {dump_tool} "
|
||||
f"-h 127.0.0.1 --protocol=tcp "
|
||||
f"-u {user} -p{password} {db_name}"
|
||||
)
|
||||
|
||||
@@ -9,29 +9,25 @@ def get_image_info(container: str) -> str:
|
||||
)[0]
|
||||
|
||||
|
||||
def image_name(container: str) -> str:
|
||||
"""The image's repository path, without registry host, tag or digest.
|
||||
def image_id(container: str) -> str:
|
||||
"""The container's image ID, identical for every replica of one image."""
|
||||
return execute_shell_command(
|
||||
f"docker inspect --format '{{{{.Image}}}}' {container}"
|
||||
)[0].strip()
|
||||
|
||||
A swarm node that hosts the local registry puts its own hostname in front
|
||||
of every pull, so the raw reference of a Postgres container can read
|
||||
`svc-db-mariadb-swarm-mgr-01:5000/postgres_custom:17-3.5`. Matching the
|
||||
whole reference finds "mariadb" there and dumps the database with
|
||||
mariadb-dump, which the Postgres image does not ship (exit 127). Tags bite
|
||||
the same way: `xwiki_custom:lts-postgres-tomcat`.
|
||||
|
||||
def has_tool(container: str, tool: str) -> bool:
|
||||
"""Whether *tool* runs inside the container.
|
||||
|
||||
Executes the binary rather than asking a shell for it: a distroless image
|
||||
has no shell, and `sh -c 'command -v'` would answer "absent" for every
|
||||
tool it ships.
|
||||
"""
|
||||
reference = get_image_info(container).strip().split("@", 1)[0]
|
||||
head, _, tail = reference.rpartition("/")
|
||||
tail = tail.split(":", 1)[0]
|
||||
if head:
|
||||
registry = head.split("/", 1)[0]
|
||||
if "." in registry or ":" in registry or registry == "localhost":
|
||||
head = head.partition("/")[2]
|
||||
return f"{head}/{tail}" if head else tail
|
||||
|
||||
|
||||
def has_image(container: str, pattern: str) -> bool:
|
||||
"""Return True if the container's image name contains the pattern."""
|
||||
return pattern in image_name(container)
|
||||
try:
|
||||
execute_shell_command(f"docker exec {container} {tool} --version")
|
||||
except BackupException:
|
||||
return False
|
||||
return True
|
||||
|
||||
|
||||
def docker_volume_names() -> list[str]:
|
||||
|
||||
@@ -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:
|
||||
|
||||
Reference in New Issue
Block a user