refactor(restore)!: restore volume files via direct host rsync

Drop the alpine-rsync helper image: resolve the volume mountpoint with
docker volume inspect and rsync the backup straight into it. Removes the
--rsync-image flag and the E2E_RSYNC_IMAGE pre-pull; the e2e test
container now mounts /var/lib/docker rw so the direct restore can write.

BREAKING CHANGE: the restore 'files' subcommand no longer accepts
--rsync-image; rsync must be available on the host running baudolo.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
2026-07-12 18:18:57 +02:00
parent 331931d617
commit 8c1a6cc465
3 changed files with 19 additions and 33 deletions

View File

@@ -27,7 +27,6 @@ dind() { docker exec "${DIND}" docker "$@"; }
dind_stdin() { docker exec -i "${DIND}" docker "$@"; } dind_stdin() { docker exec -i "${DIND}" docker "$@"; }
IMG="${E2E_IMAGE:-baudolo:local}" IMG="${E2E_IMAGE:-baudolo:local}"
RSYNC_IMG="${E2E_RSYNC_IMAGE:-ghcr.io/kevinveenbirkenbach/alpine-rsync}"
READY_TIMEOUT_SECONDS="${E2E_READY_TIMEOUT_SECONDS:-120}" READY_TIMEOUT_SECONDS="${E2E_READY_TIMEOUT_SECONDS:-120}"
ARTIFACTS_DIR="${E2E_ARTIFACTS_DIR:-./artifacts}" ARTIFACTS_DIR="${E2E_ARTIFACTS_DIR:-./artifacts}"
@@ -164,10 +163,6 @@ for i in $(seq 1 "${READY_TIMEOUT_SECONDS}"); do
fi fi
done done
log "Pre-pulling helper images in DinD..."
log " - Pulling: ${RSYNC_IMG}"
dind pull "${RSYNC_IMG}"
log "Ensuring alpine exists in DinD (for debug helpers)" log "Ensuring alpine exists in DinD (for debug helpers)"
dind pull alpine:3.20 >/dev/null dind pull alpine:3.20 >/dev/null
@@ -181,8 +176,7 @@ if [ "${DEBUG_SHELL}" = "1" ]; then
docker run --rm -it \ docker run --rm -it \
--network "${NET}" \ --network "${NET}" \
-e DOCKER_HOST="${DIND_HOST_IN_NET}" \ -e DOCKER_HOST="${DIND_HOST_IN_NET}" \
-e E2E_RSYNC_IMAGE="${RSYNC_IMG}" \ -v "${DIND_VOL}:/var/lib/docker" \
-v "${DIND_VOL}:/var/lib/docker:ro" \
-v "${E2E_TMP_VOL}:/tmp" \ -v "${E2E_TMP_VOL}:/tmp" \
"${IMG}" \ "${IMG}" \
bash -lc ' bash -lc '
@@ -200,8 +194,7 @@ else
docker run --rm \ docker run --rm \
--network "${NET}" \ --network "${NET}" \
-e DOCKER_HOST="${DIND_HOST_IN_NET}" \ -e DOCKER_HOST="${DIND_HOST_IN_NET}" \
-e E2E_RSYNC_IMAGE="${RSYNC_IMG}" \ -v "${DIND_VOL}:/var/lib/docker" \
-v "${DIND_VOL}:/var/lib/docker:ro" \
-v "${E2E_TMP_VOL}:/tmp" \ -v "${E2E_TMP_VOL}:/tmp" \
"${IMG}" \ "${IMG}" \
bash -lc ' bash -lc '

View File

@@ -38,10 +38,6 @@ def main(argv: list[str] | None = None) -> int:
# ------------------------------------------------------------------ # ------------------------------------------------------------------
p_files = sub.add_parser("files", help="Restore files into a docker volume") p_files = sub.add_parser("files", help="Restore files into a docker volume")
_add_common_backup_args(p_files) _add_common_backup_args(p_files)
p_files.add_argument(
"--rsync-image",
default="ghcr.io/kevinveenbirkenbach/alpine-rsync",
)
p_files.add_argument( p_files.add_argument(
"--source-volume", "--source-volume",
default=None, default=None,
@@ -95,7 +91,6 @@ def main(argv: list[str] | None = None) -> int:
return restore_volume_files( return restore_volume_files(
args.volume_name, args.volume_name,
bp_files.files_dir(), bp_files.files_dir(),
rsync_image=args.rsync_image,
) )
if args.cmd == "postgres": if args.cmd == "postgres":

View File

@@ -3,12 +3,10 @@ from __future__ import annotations
import os import os
import sys import sys
from .run import run, docker_volume_exists from .run import docker_volume_exists, run
def restore_volume_files( def restore_volume_files(volume_name: str, backup_files_dir: str) -> int:
volume_name: str, backup_files_dir: str, *, rsync_image: str
) -> int:
if not os.path.isdir(backup_files_dir): if not os.path.isdir(backup_files_dir):
print(f"ERROR: backup files dir not found: {backup_files_dir}", file=sys.stderr) print(f"ERROR: backup files dir not found: {backup_files_dir}", file=sys.stderr)
return 2 return 2
@@ -19,21 +17,21 @@ def restore_volume_files(
else: else:
print(f"Volume {volume_name} already exists.") print(f"Volume {volume_name} already exists.")
# Keep behavior close to the old script: rsync -avv --delete cp = run(
run( ["docker", "volume", "inspect", "--format", "{{ .Mountpoint }}", volume_name],
[ capture=True,
"docker",
"run",
"--rm",
"-v",
f"{volume_name}:/recover/",
"-v",
f"{backup_files_dir}:/backup/",
rsync_image,
"sh",
"-lc",
"rsync -avv --delete /backup/ /recover/",
]
) )
raw = cp.stdout or b""
mountpoint = (raw.decode() if isinstance(raw, bytes) else raw).strip()
if not mountpoint:
print(
f"ERROR: could not resolve mountpoint for volume {volume_name}",
file=sys.stderr,
)
return 2
src = os.path.join(backup_files_dir, "")
dest = os.path.join(mountpoint, "")
run(["rsync", "-avv", "--delete", src, dest])
print("File restore complete.") print("File restore complete.")
return 0 return 0