8fc2b4a1a9
gates / gates (push) Successful in 26s
The guarded update gains a folder copy of the app's named volumes, taken after the pull where the app stops anyway (decision 19, chosen by the 2026-09-23 bake-off). On a failed health check the box undoes: every copy validated by its finished-marker first, volumes refilled, definition and pin from the job's own pre-update copies, the old version checked with the OLD .felhom.yml probe. It holds only if the undo fails, and the hold sentence says so and what state the data is in. Bind-mounted folders are never touched. - R-637 built; R-638/R-640/R-641 do not arise with a folder copy; R-639 (pre-update copies incl. .felhom.yml kept until the undo is over). - journal phases copying/undoing with power-cut recovery. - app.yaml last_update_undone + one line on the app page (hu/en). - R-642: start/restart never answer "completed". - Removal deletes kept undo copies. MinAgent unchanged (0.131.0). Nine red-proofs in REPORT.md. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0159rPz1ZhFKsS53msqPYxtS
85 lines
4.4 KiB
Python
85 lines
4.4 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/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()
|