2caae38a71
gates / gates (push) Successful in 25s
A step whose ladder entry carries engine_conversion {service, engine, from, to}
converts the database: the old engine alone, the check (owners, roles,
extensions, per-table row counts), pg_dumpall validated by its completion line,
the volume emptied only after the undo copy's marker is validated again, the new
engine alone, the load with ON_ERROR_STOP, the check again + PG_VERSION. Any
failure goes to the existing undo; a restart during converting is undone.
A PostgreSQL major move without the mark is refused before anything moves.
The old datadir's copy is kept until a backup is proven after the conversion.
17 tests, 9 red-proofs (audits/night-2026-09-26/B/).
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0159rPz1ZhFKsS53msqPYxtS
92 lines
5.0 KiB
Python
92 lines
5.0 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/stacks/undo.go", '"-v", vol+":/v:ro"',
|
|
"the undo's size check (v0.263.0): `du` on a NAMED volume mounted read-only — no host path, "
|
|
"daemon-side, the estimate.go class"),
|
|
("internal/stacks/undo.go", '"-v", src+":/from:ro", "-v", dst+":/to"',
|
|
"the undo's copy (v0.263.0): named volume -> named volume, both resolved daemon-side; no host "
|
|
"path is involved, so the containerized-controller stranding class cannot occur"),
|
|
("internal/stacks/undo.go", '"-v", copyVol+":/c:ro"',
|
|
"the undo's finished-marker check (v0.263.0): a named copy volume, read-only, daemon-side"),
|
|
("internal/stacks/undo.go", '"-v", copyVol+":/from:ro", "-v", vol+":/to"',
|
|
"the undo's restore (v0.263.0): named copy volume -> the app's named volume, daemon-side"),
|
|
("internal/stacks/pgconvert.go", '"psql", "-h", "127.0.0.1", "-U", user, "-d", db, "-v"',
|
|
"psql's own -v flag (ON_ERROR_STOP) in the conversion's check queries — not a docker mount (v0.273.0)"),
|
|
("internal/stacks/pgconvert.go", '"-v", copyVol+":/from:ro", "-v", vol+":/to"',
|
|
"the conversion's Empty (v0.273.0): two NAMED volumes (the kept copy read-only, the database volume) "
|
|
"into a throwaway alpine — daemon-side, no host path; the undo Restore's exact shape"),
|
|
("internal/stacks/pgconvert.go", '"-d", "postgres", "-v", "ON_ERROR_STOP=1"',
|
|
"psql's own -v flag (ON_ERROR_STOP) in the conversion's load — not a docker mount (v0.273.0)"),
|
|
("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()
|