Files
felhom-controller/controller/scripts/docker_run_volume_path_gate.py
T
admin c432f701dd gate: allowlist appexport/estimate.go named-volume -v mount (R-29 leg a)
realVolumeSize mounts the NAMED VOLUME read-only into a throwaway alpine to du it
from a container view. No host path is involved — docker resolves the volume name
daemon-side — so it is the same safe class as the internal/backup/backup.go entry.
The gate was right to demand review; this diff is that review, on its own, because
burying an allowlist widening inside a feature commit is how an allowlist stops
meaning anything.

Tooling only: no Go change, no build, no deploy, no version bump.
2026-08-02 15:11:08 +02:00

75 lines
3.6 KiB
Python

# -*- 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 <path>` 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()