mirror of
https://github.com/kevinveenbirkenbach/docker-volume-backup.git
synced 2026-09-23 21:23:19 +00:00
refactor(backup)!: run argv lists, never a shell
The backup built command strings and handed them to shell=True, so four interpolated values per dump - user, password, container, database - were each a way out of the command. validate_database covered one of them since the previous commit; now there is nothing to cover: every command is an argv list, and a value can only ever be an argument. execute_to_file absorbs the atomic dump write. The shell redirect into <file>.tmp and the separate mv process become a Python file handle and os.replace, and a failing dump deletes its partial file instead of leaving it. PGPASSWORD moves out of the command string into the child's environment, where a process listing does not show it. docker exec is built in one place, docker_exec_argv; db.py's three hand-built copies and the probe use it. The dead docker_volume_exists goes - never called, and the restore side owns the living twin. The rsync quoting in --link-dest falls away: inside an argv it would have become part of the path. The snapshot module's injected runner changes type with it, which the three e2e drivers implement - the first conversion missed them, btrfs ran with no arguments, and the e2e caught it. Marked breaking for that contract: any external runner injected into volume_snapshot must now accept a list. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -1,18 +1,27 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from collections.abc import Sequence
|
||||
|
||||
from .shell import BackupException, execute_shell_command
|
||||
|
||||
|
||||
def docker_exec_argv(
|
||||
container: str, argv: Sequence[str], *, interactive: bool = False
|
||||
) -> list[str]:
|
||||
"""The argv that runs *argv* inside *container*."""
|
||||
return ["docker", "exec", *(["-i"] if interactive else []), container, *argv]
|
||||
|
||||
|
||||
def get_image_info(container: str) -> str:
|
||||
return execute_shell_command(
|
||||
f"docker inspect --format '{{{{.Config.Image}}}}' {container}"
|
||||
["docker", "inspect", "--format", "{{.Config.Image}}", container]
|
||||
)[0]
|
||||
|
||||
|
||||
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}"
|
||||
["docker", "inspect", "--format", "{{.Image}}", container]
|
||||
)[0].strip()
|
||||
|
||||
|
||||
@@ -24,19 +33,26 @@ def has_tool(container: str, tool: str) -> bool:
|
||||
tool it ships.
|
||||
"""
|
||||
try:
|
||||
execute_shell_command(f"docker exec {container} {tool} --version")
|
||||
execute_shell_command(docker_exec_argv(container, [tool, "--version"]))
|
||||
except BackupException:
|
||||
return False
|
||||
return True
|
||||
|
||||
|
||||
def docker_volume_names() -> list[str]:
|
||||
return execute_shell_command("docker volume ls --format '{{.Name}}'")
|
||||
return execute_shell_command(["docker", "volume", "ls", "--format", "{{.Name}}"])
|
||||
|
||||
|
||||
def containers_using_volume(volume_name: str) -> list[str]:
|
||||
return execute_shell_command(
|
||||
f"docker ps --filter volume=\"{volume_name}\" --format '{{{{.Names}}}}'"
|
||||
[
|
||||
"docker",
|
||||
"ps",
|
||||
"--filter",
|
||||
f"volume={volume_name}",
|
||||
"--format",
|
||||
"{{.Names}}",
|
||||
]
|
||||
)
|
||||
|
||||
|
||||
@@ -50,12 +66,25 @@ def is_swarm_task(container: str) -> bool:
|
||||
keeps failing the run loudly instead of silently skipping the stop."""
|
||||
try:
|
||||
out = execute_shell_command(
|
||||
"docker inspect --format "
|
||||
f"'{{{{index .Config.Labels \"com.docker.swarm.task.id\"}}}}' {container}"
|
||||
[
|
||||
"docker",
|
||||
"inspect",
|
||||
"--format",
|
||||
'{{index .Config.Labels "com.docker.swarm.task.id"}}',
|
||||
container,
|
||||
]
|
||||
)
|
||||
except BackupException:
|
||||
still_listed = execute_shell_command(
|
||||
f"docker ps -a --filter name=^{container}$ --format '{{{{.Names}}}}'"
|
||||
[
|
||||
"docker",
|
||||
"ps",
|
||||
"-a",
|
||||
"--filter",
|
||||
f"name=^{container}$",
|
||||
"--format",
|
||||
"{{.Names}}",
|
||||
]
|
||||
)
|
||||
if still_listed and still_listed[0].strip():
|
||||
raise
|
||||
@@ -82,17 +111,5 @@ def change_containers_status(containers: list[str], status: str) -> None:
|
||||
if not containers:
|
||||
print(f"No containers to {status}.", flush=True)
|
||||
return
|
||||
names = " ".join(containers)
|
||||
print(f"{status.capitalize()} containers: {names}...", flush=True)
|
||||
execute_shell_command(f"docker {status} {names}")
|
||||
|
||||
|
||||
def docker_volume_exists(volume: str) -> bool:
|
||||
# Avoid throwing exceptions for exists checks.
|
||||
try:
|
||||
execute_shell_command(
|
||||
f"docker volume inspect {volume} >/dev/null 2>&1 && echo OK"
|
||||
)
|
||||
return True
|
||||
except BackupException:
|
||||
return False
|
||||
print(f"{status.capitalize()} containers: {' '.join(containers)}...", flush=True)
|
||||
execute_shell_command(["docker", status, *containers])
|
||||
|
||||
Reference in New Issue
Block a user