mirror of
https://github.com/kevinveenbirkenbach/docker-volume-backup.git
synced 2026-08-20 21:22:54 +00:00
fix(backup)!: dump the container that holds the database, with its password
Two defects kept dedicated Postgres databases out of the backup. docker exec never forwarded PGPASSWORD. execute_to_file set it on baudolo's own process, but nothing carried it across the container boundary, so an engine whose pg_hba demands a password on TCP loopback refused the dump. forward_env passes a bare `-e NAME`, letting docker copy the value out of this process's environment instead of spelling it into argv, where the host's process list would publish it. get_instance returned the container name unchanged when that name carried no database token, claiming an instance it had never derived. An application container therefore answered the same databases.csv row as its own dedicated engine, and application images often ship the engine's client tools, so the dump command started and wrote a file that looked like a backup and held none of the data. Discourse is the live case: its launcher names the container `discourse`, and the image ships pg_dumpall. The regex stays a normaliser - `<app>-database` from compose and `<app>_database.1.<task>` from swarm still resolve to the same instance. Only the fallthrough changes. BREAKING CHANGE: a database container whose name carries no `database`, `db` or `postgres` token must now be named in --database-containers. Without that declaration its rows no longer match and no dump is written. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -17,13 +17,27 @@ if TYPE_CHECKING:
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def get_instance(container: str, database_containers: list[str]) -> str:
|
||||
"""
|
||||
Derive a stable instance name from the container name.
|
||||
def get_instance(container: str, database_containers: list[str]) -> str | None:
|
||||
"""The databases.csv instance a container serves, or None for no database.
|
||||
|
||||
A declared container is its own instance. Every other name is normalised by
|
||||
stripping a database suffix token, which maps both `<app>-database` from
|
||||
compose and `<app>_database.1.<task>` from swarm onto the same instance.
|
||||
|
||||
Args:
|
||||
container: the running container's name.
|
||||
database_containers: names passed via --database-containers, taken as
|
||||
declared engines whatever they are called.
|
||||
|
||||
Returns:
|
||||
The instance name, or None when the name carries no database token: an
|
||||
application container is not an engine, even when it ships the client
|
||||
tools that would let a dump command start.
|
||||
"""
|
||||
if container in database_containers:
|
||||
return container
|
||||
return re.split(r"(_|-)(database|db|postgres)", container)[0]
|
||||
parts = re.split(r"(_|-)(database|db|postgres)", container)
|
||||
return parts[0] if len(parts) > 1 else None
|
||||
|
||||
|
||||
def fallback_pg_dumpall(
|
||||
@@ -37,6 +51,7 @@ def fallback_pg_dumpall(
|
||||
container,
|
||||
["pg_dumpall", "-U", username, "-h", "localhost"],
|
||||
interactive=True,
|
||||
forward_env=["PGPASSWORD"],
|
||||
),
|
||||
out_file,
|
||||
env={"PGPASSWORD": password},
|
||||
@@ -62,6 +77,9 @@ def backup_database(
|
||||
Returns True if at least one dump was produced.
|
||||
"""
|
||||
instance_name = get_instance(container, database_containers)
|
||||
if instance_name is None:
|
||||
log.debug("Container '%s' carries no database token", container)
|
||||
return False
|
||||
|
||||
entries = databases_df[databases_df["instance"] == instance_name]
|
||||
if entries.empty:
|
||||
@@ -133,6 +151,7 @@ def backup_database(
|
||||
"--no-privileges",
|
||||
],
|
||||
interactive=True,
|
||||
forward_env=["PGPASSWORD"],
|
||||
),
|
||||
dump_file,
|
||||
env={"PGPASSWORD": password},
|
||||
|
||||
@@ -9,10 +9,35 @@ if TYPE_CHECKING:
|
||||
|
||||
|
||||
def docker_exec_argv(
|
||||
container: str, argv: Sequence[str], *, interactive: bool = False
|
||||
container: str,
|
||||
argv: Sequence[str],
|
||||
*,
|
||||
interactive: bool = False,
|
||||
forward_env: Sequence[str] = (),
|
||||
) -> list[str]:
|
||||
"""The argv that runs *argv* inside *container*."""
|
||||
return ["docker", "exec", *(["-i"] if interactive else []), container, *argv]
|
||||
"""The argv that runs *argv* inside *container*.
|
||||
|
||||
Args:
|
||||
container: the container to run in.
|
||||
argv: the command, already split.
|
||||
interactive: keep stdin open, for a command that is fed a dump.
|
||||
forward_env: names of environment variables to hand to the container.
|
||||
Passed as bare ``-e NAME``, so docker copies the value out of this
|
||||
process's own environment; spelling ``-e NAME=value`` instead would
|
||||
publish a secret in the host's process list.
|
||||
|
||||
Returns:
|
||||
The argv list.
|
||||
"""
|
||||
forwarded = [arg for name in forward_env for arg in ("-e", name)]
|
||||
return [
|
||||
"docker",
|
||||
"exec",
|
||||
*(["-i"] if interactive else []),
|
||||
*forwarded,
|
||||
container,
|
||||
*argv,
|
||||
]
|
||||
|
||||
|
||||
def get_image_info(container: str) -> str:
|
||||
|
||||
Reference in New Issue
Block a user