# -*- coding: utf-8 -*- """docker-run volume-path gate (v0.125.0, scenario D) — the class behind the v0.124.0 HIGH finding: a `docker … -v ` mount whose host side is a CONTROLLER-LOCAL path (os.MkdirTemp etc.) resolves against the GUEST filesystem when the controller runs containerized, silently stranding data. Every `"-v"` argument in non-test Go code must be on the explicit allowlist below; anything new fails the gate until it is reviewed and either rewritten (docker cp streaming — appexport's pattern) or proven host-visible and allowlisted WITH ITS WHY. Run from controller/: python scripts/docker_run_volume_path_gate.py """ import io, os, re, sys ROOTS = ["internal", "cmd"] # (file suffix, substring that must appear on the "-v" line, why it is safe) ALLOWLIST = [ ("internal/appbackup/dbdump.go", '"psql", "-v"', "psql's own -v flag (ON_ERROR_STOP) — not a docker mount at all"), ("internal/appexport/estimate.go", '"-v", volumeName+":/vol:ro"', "realVolumeSize's container-view `du`: named-volume source (no host path) mounted read-only " "into a throwaway alpine — docker resolves the volume name daemon-side, exactly the class of " "the internal/backup/backup.go entry below. Reviewed 2026-08-02 (R-29 leg a)"), ("internal/appexport/export.go", '"create", "-v", volName+":/vol"', "named-volume mount (no host path): docker resolves volume names daemon-side; the tar " "itself streams via docker cp (v0.125.0)"), ("internal/backup/backup.go", '"-v", volName+":/vol:ro"', "Tier-1 volume dump, named-volume source — daemon-side, no host path"), ("internal/backup/backup.go", '"-v", dumpDir+":/out"', "Tier-1 volume dump target: dumpDir is ALWAYS a registered-drive namespace path " "(/mnt/** or /opt/docker/** — the golden deployment bind-mounts these into the " "controller container at IDENTICAL paths, so the daemon resolves them correctly; " "verified by container-inspect 2026-07-13)"), ("internal/backup/restore.go", '"-v", volName+":/vol"', "Tier-1 volume restore, named-volume dest — daemon-side"), ("internal/backup/restore.go", '"-v", dumpDir+":/in:ro"', "Tier-1 volume restore source: same registered-drive namespace argument as the dump " "target above — host-visible by the identical binds"), ("internal/web/handlers.go", '"compose", "down", "-v"', "docker compose's own --volumes flag (DR reset wipes the stack's volumes) — not a mount"), ] VLINE = re.compile(r'"-v"') def allowed(path, line): p = path.replace("\\", "/") for suffix, marker, _why in ALLOWLIST: if p.endswith(suffix) and marker in line: return True return False def main(): hits = 0 for root in ROOTS: for dirpath, _dirs, files in os.walk(root): for fn in files: if not fn.endswith(".go") or fn.endswith("_test.go"): continue path = os.path.join(dirpath, fn) for lineno, line in enumerate(io.open(path, encoding="utf-8"), 1): if VLINE.search(line) and not allowed(path, line): hits += 1 print("%s:%d %s" % (path, lineno, line.strip()[:100].encode("ascii", "backslashreplace").decode())) if hits: print("DOCKER -v GATE FAILED: %d unreviewed '-v' argument(s) — rewrite as docker cp " "streaming or allowlist with a WHY" % hits) sys.exit(1) print("docker -v gate OK — every volume mount is named-volume or proven host-visible") if __name__ == "__main__": main()