fix(restore): refuse to write files into an unmounted backing store

Writing into a volume's mountpoint only restores it when the mountpoint is the storage. A volume with driver options - NFS, a bind device, tmpfs - keeps the same /var/lib/docker/volumes/<name>/_data path, but docker mounts the real backing store over it on demand and unmounts it again when the last consumer stops. Restoring while nothing holds it lands in the empty directory underneath, is hidden by the next mount, and rsync reports success.

The declaration decides, not the mount table: the driver and its options are true at every moment, where the mount table is only true while a container happens to hold the volume.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-08-17 05:25:54 +02:00
parent bb647c66ec
commit e80f11d5e4
3 changed files with 211 additions and 4 deletions

View File

@@ -1,9 +1,23 @@
"""Restore a volume's file tree by writing into its mountpoint.
That shortcut only holds for a plain local volume, where the mountpoint *is*
the storage. A volume with driver options - NFS, a bind device, tmpfs - keeps
the same ``/var/lib/docker/volumes/<name>/_data`` path, but docker mounts the
real backing store over it on demand and unmounts it again when the last
consumer stops. Writing there while nothing has it mounted lands in the empty
directory underneath, is hidden by the next mount, and rsync reports success.
"""
from __future__ import annotations
import os
import sys
from .run import docker_volume_exists, run
from .run import docker_volume_exists, run, stdout_of
INSPECT_FORMAT = (
"{{ .Mountpoint }}|{{ .Driver }}|{{ if .Options }}opts{{ else }}plain{{ end }}"
)
def restore_volume_files(volume_name: str, backup_files_dir: str) -> int:
@@ -18,11 +32,11 @@ def restore_volume_files(volume_name: str, backup_files_dir: str) -> int:
print(f"Volume {volume_name} already exists.")
cp = run(
["docker", "volume", "inspect", "--format", "{{ .Mountpoint }}", volume_name],
["docker", "volume", "inspect", "--format", INSPECT_FORMAT, volume_name],
capture=True,
)
raw = cp.stdout or b""
mountpoint = (raw.decode() if isinstance(raw, bytes) else raw).strip()
fields = stdout_of(cp).split("|")
mountpoint = fields[0] if fields else ""
if not mountpoint:
print(
f"ERROR: could not resolve mountpoint for volume {volume_name}",
@@ -30,6 +44,17 @@ def restore_volume_files(volume_name: str, backup_files_dir: str) -> int:
)
return 2
driver, options = (fields + ["local", "plain"])[1:3]
if (driver != "local" or options == "opts") and not os.path.ismount(mountpoint):
print(
f"ERROR: volume {volume_name} has a backing store of its own "
f"(driver {driver}) but nothing has it mounted; writing to "
f"{mountpoint} now would land under the mount and be lost. "
"Start a container that mounts the volume, then restore again.",
file=sys.stderr,
)
return 2
src = os.path.join(backup_files_dir, "")
dest = os.path.join(mountpoint, "")
run(["rsync", "-avv", "--delete", src, dest])