persistence sweep: 53 templates measured; gramps-web + wishlist fixed; runtime gate added
Campaign 10's R-156 found papra writing its database into the container's writable layer while the volume the template preserves stayed empty — a backup that completes, verifies, and contains nothing. papra was never the point: nothing anywhere checked that the folder a template preserves is the folder the app writes to. All 53 templates have now been measured live. 43 CLEAN / 3 BROKEN / 7 UNDETERMINED. UNDETERMINED is counted separately, each with its reason, and never folded into CLEAN. FIXED (neither app is deployed anywhere, so nothing was stranded): - gramps-web mounted /app/data, /app/media, /tmp — and /app/data is a path the application never writes. Its accounts database and ITS FAMILY TREE both landed in the writable layer while gramps_data was tarred nightly as an empty directory. Now persists the eight paths the image's own environment names, matching upstream's reference compose. Proven: users.sqlite and the family-tree files survive a redeploy byte-identical, same inode. - wishlist mounted wishlist_data:/data, another path the app never writes; prod.db landed in the ANONYMOUS volume from the image's VOLUME directive — absent from ResolveDockerVolumeNames, so never backed up, and orphaned by a redeploy. Now mounts /usr/src/app/data + /usr/src/app/uploads. Proven: prod.db byte-identical, same inode, across a redeploy. Every corrected path confirmed by two independent sources — the shipped image's own environment/Config.Volumes and upstream's reference compose — never inferred from a directory name. papra is NOT fixed. It is live on one box, and changing the mount target makes the next compose up recreate the container and destroy the writable layer its documents live in. The fix is prepared and proven in the scratch guest (current: db.sqlite differs after a redeploy, so a real account created via the API is lost; fixed: byte-identical, it survives). Referred to the operator with the two options; no migration written. NEW GATE scripts/check-volume-persistence.py — the third catalog gate and the only RUNTIME one. This class is invisible to static analysis, measured not assumed: a static audit of all 53 composes reports the catalog clean AND reports papra clean. Exit 0 clean / 1 REFUSED / 2 undecided. It refuses to report at all unless it has just re-proven itself in both directions against two canary templates that differ only in which path the volume mounts at, so every run carries a live demonstration of R-156 and of its fix. No docker exec anywhere (Campaign 7 §1.1). 44 fixture tests driving check(), the function __main__ calls; every rule red-proofed. Enforcement is convention, not CI — this repo has no CI. Stated plainly in the report; raising it is proposed as R-160. Report, per-app evidence, proofs and proposed register entries (R-158..R-161, NOT filed — felhom.eu is fenced this session): audits/persistence-sweep-2026-08-02/
This commit is contained in:
@@ -0,0 +1,90 @@
|
||||
self-testing the prober (both directions)…
|
||||
prober flags the R-156 signature and clears a correct template — trustworthy
|
||||
actualbudget CLEAN
|
||||
adventurelog CLEAN
|
||||
audiobookshelf CLEAN
|
||||
bentopdf UNDETERMINED nothing was written to any mount and nothing data-classified in any writable layer — the app produced no data to locate. Health is not data: this is U
|
||||
bookstack CLEAN
|
||||
calcom CLEAN
|
||||
calibre-web CLEAN calibre-web: /app/calibre-web-automated/empty_library — database file(s) touched but byte-identical to the image; a chown sweep, not a write; calibre-
|
||||
claper UNDETERMINED claper: EVERY declared mount is empty, yet the container created entries in 2 director(y/ies) outside all of them (e.g. ['/app/priv', '/app/priv/stati
|
||||
code-server CLEAN
|
||||
crafty-controller CLEAN crafty-controller: declared volume /crafty/servers is EMPTY; crafty-controller: declared volume /crafty/import is EMPTY; crafty-controller: declared v
|
||||
docmost UNDETERMINED docmost: EVERY declared mount is empty, yet the container created entries in 1 director(y/ies) outside all of them (e.g. ['/app/apps/client/dist']) —
|
||||
emby CLEAN
|
||||
ghost CLEAN
|
||||
gitea CLEAN
|
||||
glance UNDETERMINED glance: not running (status=restarting exit=1 restarts=None)
|
||||
gokapi CLEAN
|
||||
grafana CLEAN
|
||||
gramps-web BROKEN gramps-web: DATA in the writable layer at /app/users (db_signature=True, e.g. ['users.sqlite']); gramps-web: declared volume /app/data is EMPTY; gramp
|
||||
home-assistant CLEAN
|
||||
homebox CLEAN
|
||||
homepage CLEAN
|
||||
immich UNDETERMINED immich-machine-learning: EVERY declared mount is empty, yet the container created entries in 3 director(y/ies) outside all of them (e.g. ['/', '/root'
|
||||
jellyfin CLEAN
|
||||
kimai CLEAN
|
||||
komga CLEAN
|
||||
mealie CLEAN
|
||||
n8n CLEAN
|
||||
navidrome CLEAN
|
||||
nextcloud CLEAN
|
||||
onlyoffice CLEAN onlyoffice: writable-layer writes at /var/www/onlyoffice/documentserver/sdkjs-plugins/{07FD8DFA-DFE0-4089-AL24-0730933CC80A}/resources/store (path sug
|
||||
opengist CLEAN
|
||||
outline CLEAN outline: declared volume /var/lib/outline/data is EMPTY; outline-redis: declared volume /data is EMPTY
|
||||
paperless-ngx CLEAN
|
||||
papra BROKEN papra: mount /app/data is NOT writable by the app's own uid=999 gid=999; papra: DATA in the writable layer at /app/app-data/db (db_signature=True, e.g
|
||||
plant-it UNDETERMINED no containers created (compose up rc=1: msdeluise/plant-it:0.10.0 Pulling
|
||||
Image msdeluise/plant-it:0.10.0 Error pull access denied for msdeluise/pla
|
||||
plex CLEAN plex: declared volume /transcode is EMPTY
|
||||
privatebin BROKEN privatebin: 14 file(s) in an ANONYMOUS volume at /run — not in the compose `volumes:` block, so it is never backed up and a redeploy orphans it; priva
|
||||
radarr CLEAN radarr: writable-layer writes at /media (path suggests state, no database signature — judgement needed): ['movies']
|
||||
rallly CLEAN
|
||||
recipe-importer UNDETERMINED recipe-importer: EVERY declared mount is empty, yet the container created entries in 1 director(y/ies) outside all of them (e.g. ['/']) — nothing this
|
||||
romm CLEAN
|
||||
seerr CLEAN
|
||||
sonarr CLEAN sonarr: writable-layer writes at /media (path suggests state, no database signature — judgement needed): ['tv']
|
||||
sparkyfitness UNDETERMINED sparkyfitness-server: EVERY declared mount is empty, yet the container created entries in 1 director(y/ies) outside all of them (e.g. ['/app/SparkyFit
|
||||
tandoor CLEAN tandoor: declared volume /opt/recipes/mediafiles is EMPTY
|
||||
termix CLEAN
|
||||
uptime-kuma UNDETERMINED nothing was written to any mount and nothing data-classified in any writable layer — the app produced no data to locate. Health is not data: this is U
|
||||
vaultwarden CLEAN
|
||||
vikunja CLEAN vikunja: declared volume /app/vikunja/files is EMPTY
|
||||
wanderer UNDETERMINED wanderer: EVERY declared mount is empty, yet the container created entries in 1 director(y/ies) outside all of them (e.g. ['/root']) — nothing this ap
|
||||
wger CLEAN wger: declared volume /home/wger/media is EMPTY
|
||||
wishlist BROKEN wishlist: 1 file(s) in an ANONYMOUS volume at /usr/src/app/data — not in the compose `volumes:` block, so it is never backed up and a redeploy orphans
|
||||
zipline UNDETERMINED zipline: EVERY declared mount is empty, yet the container created entries in 1 director(y/ies) outside all of them (e.g. ['/zipline']) — nothing this
|
||||
|
||||
BROKEN — the app's data does NOT land where the template preserves it:
|
||||
gramps-web
|
||||
gramps-web: DATA in the writable layer at /app/users (db_signature=True, e.g. ['users.sqlite'])
|
||||
gramps-web: declared volume /app/data is EMPTY
|
||||
gramps-web: declared volume /app/media is EMPTY
|
||||
gramps-web: declared volume /tmp is EMPTY
|
||||
papra
|
||||
papra: mount /app/data is NOT writable by the app's own uid=999 gid=999
|
||||
papra: DATA in the writable layer at /app/app-data/db (db_signature=True, e.g. ['db.sqlite'])
|
||||
papra: declared volume /app/data is EMPTY
|
||||
privatebin
|
||||
privatebin: 14 file(s) in an ANONYMOUS volume at /run — not in the compose `volumes:` block, so it is never backed up and a redeploy orphans it
|
||||
privatebin: declared volume /srv/data is EMPTY
|
||||
wishlist
|
||||
wishlist: 1 file(s) in an ANONYMOUS volume at /usr/src/app/data — not in the compose `volumes:` block, so it is never backed up and a redeploy orphans it
|
||||
wishlist: declared volume /data is EMPTY
|
||||
|
||||
UNDETERMINED — NOT a pass. The question was not answered for these:
|
||||
bentopdf: nothing was written to any mount and nothing data-classified in any writable layer — the app produced no data to locate. Health is not data: this is UNDETERMINED, not CLEAN
|
||||
claper: claper: EVERY declared mount is empty, yet the container created entries in 2 director(y/ies) outside all of them (e.g. ['/app/priv', '/app/priv/static']) — nothing this app wrote landed anywhere the
|
||||
docmost: docmost: EVERY declared mount is empty, yet the container created entries in 1 director(y/ies) outside all of them (e.g. ['/app/apps/client/dist']) — nothing this app wrote landed anywhere the templat
|
||||
glance: glance: not running (status=restarting exit=1 restarts=None)
|
||||
immich: immich-machine-learning: EVERY declared mount is empty, yet the container created entries in 3 director(y/ies) outside all of them (e.g. ['/', '/root', '/root/.config']) — nothing this app wrote lande
|
||||
plant-it: no containers created (compose up rc=1: msdeluise/plant-it:0.10.0 Pulling
|
||||
Image msdeluise/plant-it:0.10.0 Error pull access denied for msdeluise/plant-it, repository does not exist or may require 'd
|
||||
recipe-importer: recipe-importer: EVERY declared mount is empty, yet the container created entries in 1 director(y/ies) outside all of them (e.g. ['/']) — nothing this app wrote landed anywhere the template preserves.
|
||||
sparkyfitness: sparkyfitness-server: EVERY declared mount is empty, yet the container created entries in 1 director(y/ies) outside all of them (e.g. ['/app/SparkyFitnessServer']) — nothing this app wrote landed anyw
|
||||
uptime-kuma: nothing was written to any mount and nothing data-classified in any writable layer — the app produced no data to locate. Health is not data: this is UNDETERMINED, not CLEAN; uptime-kuma: declared volu
|
||||
wanderer: wanderer: EVERY declared mount is empty, yet the container created entries in 1 director(y/ies) outside all of them (e.g. ['/root']) — nothing this app wrote landed anywhere the template preserves. Ne
|
||||
zipline: zipline: EVERY declared mount is empty, yet the container created entries in 1 director(y/ies) outside all of them (e.g. ['/zipline']) — nothing this app wrote landed anywhere the template preserves.
|
||||
|
||||
38 clean · 4 broken · 11 undetermined (of 53 in scope)
|
||||
volume-persistence gate REFUSED
|
||||
@@ -0,0 +1,66 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Render the sweep matrix from the per-app probe.json evidence. Run on DooPlex after pulling
|
||||
the evidence back from the scratch guest."""
|
||||
import json, sys
|
||||
from pathlib import Path
|
||||
|
||||
EV = Path(sys.argv[1] if len(sys.argv) > 1 else "audits/persistence-sweep-2026-08-02/evidence")
|
||||
|
||||
MOUNT_SHORT = {"named-declared": "vol", "named-external": "vol-ext", "anonymous": "ANON",
|
||||
"bind": "bind", "tmpfs": "tmpfs"}
|
||||
|
||||
|
||||
def summarise(p):
|
||||
v = p["verdict"]
|
||||
probe = p.get("probe") or {}
|
||||
ctrs = probe.get("containers") or []
|
||||
mounts, wrote, unwritable = [], [], []
|
||||
for c in ctrs:
|
||||
for m in c.get("mounts") or []:
|
||||
if m["class"] == "tmpfs":
|
||||
continue
|
||||
mounts.append(f"{m['target']}[{MOUNT_SHORT.get(m['class'], m['class'])}"
|
||||
f"{'' if m['files'] > 0 else ',EMPTY'}]")
|
||||
if m["files"] > 0:
|
||||
wrote.append(f"{m['target']}({m['files']})")
|
||||
if m.get("writable_by_app") == "NO":
|
||||
unwritable.append(m["target"])
|
||||
for d in c.get("diff_data_dirs") or []:
|
||||
wrote.append(f"**{d['dir']} (writable layer)**")
|
||||
return {
|
||||
"verdict": v,
|
||||
"mounted": ", ".join(mounts) or "—",
|
||||
"wrote": ", ".join(wrote) or "nothing",
|
||||
"unwritable": ", ".join(unwritable) or "—",
|
||||
"reason": "; ".join(p.get("reasons") or []),
|
||||
"containers": len(ctrs),
|
||||
}
|
||||
|
||||
|
||||
rows = {}
|
||||
for d in sorted(EV.iterdir()):
|
||||
f = d / "probe.json"
|
||||
if f.is_file():
|
||||
rows[d.name] = summarise(json.loads(f.read_text()))
|
||||
|
||||
order = {"BROKEN": 0, "UNDETERMINED": 1, "CLEAN": 2}
|
||||
print("| app | verdict | what the template mounts | where the app actually wrote | mount not writable by app uid |")
|
||||
print("|---|---|---|---|---|")
|
||||
for app in sorted(rows, key=lambda a: (order.get(rows[a]["verdict"], 3), a)):
|
||||
r = rows[app]
|
||||
print(f"| `{app}` | **{r['verdict']}** | {r['mounted']} | {r['wrote']} | {r['unwritable']} |")
|
||||
|
||||
print()
|
||||
for v in ("BROKEN", "UNDETERMINED", "CLEAN"):
|
||||
apps = [a for a in sorted(rows) if rows[a]["verdict"] == v]
|
||||
print(f"{v}: {len(apps)} — {', '.join(apps) if apps else '(none)'}")
|
||||
print(f"TOTAL: {len(rows)}")
|
||||
|
||||
print("\n--- UNDETERMINED reasons ---")
|
||||
for a in sorted(rows):
|
||||
if rows[a]["verdict"] == "UNDETERMINED":
|
||||
print(f"| `{a}` | {rows[a]['reason'][:400]} |")
|
||||
print("\n--- BROKEN reasons ---")
|
||||
for a in sorted(rows):
|
||||
if rows[a]["verdict"] == "BROKEN":
|
||||
print(f"| `{a}` | {rows[a]['reason'][:600]} |")
|
||||
@@ -0,0 +1,165 @@
|
||||
#!/usr/bin/env python3
|
||||
"""R-156 fix proof for papra — the BEFORE/AFTER pair, on real user data.
|
||||
|
||||
Not "the volume exists afterwards" — that is exactly the confusion R-156 is made of. The claim
|
||||
under test is: a document a real user uploaded is still there after `docker compose down` + `up -d`.
|
||||
|
||||
Leg A CURRENT template (`papra_data:/app/data`) — expected: the data is DESTROYED
|
||||
Leg B FIXED template (`papra_data:/app/app-data`) — expected: the data SURVIVES, byte-identical
|
||||
|
||||
The account and the document are created through papra's own HTTP API, never by writing into a
|
||||
volume by hand: R-156's own evidence shows a root-written canary making an empty volume read as
|
||||
populated. Data is read back with `docker cp`, which works identically whether it sits in a volume
|
||||
or in the writable layer, so both legs are measured the same way.
|
||||
|
||||
No password or session token is ever printed or written to the evidence file.
|
||||
"""
|
||||
import hashlib, importlib.util, json, os, re, secrets, shutil, sys, tempfile, time
|
||||
from pathlib import Path
|
||||
|
||||
_spec = importlib.util.spec_from_file_location("cvp", "/opt/sweep/scripts/check-volume-persistence.py")
|
||||
cvp = importlib.util.module_from_spec(_spec)
|
||||
_spec.loader.exec_module(cvp)
|
||||
|
||||
TPL = Path("/opt/sweep/templates/papra")
|
||||
EV = Path("/opt/sweep/evidence/_papra-fix-proof")
|
||||
DOC = b"Felhom R-156 fix proof - ennek a dokumentumnak tul kell elnie az ujratelepitest.\n"
|
||||
DOC_SHA = hashlib.sha256(DOC).hexdigest()
|
||||
|
||||
|
||||
def curl(url, *args):
|
||||
return cvp._sh(["curl", "-sS", "--max-time", "25", *args, url], timeout=45)
|
||||
|
||||
|
||||
def wait_healthy(base, tries=60):
|
||||
for _ in range(tries):
|
||||
cid = (cvp._sh(base + ["ps", "-q"], timeout=60).stdout or "").split()
|
||||
if cid:
|
||||
st = (cvp._inspect(cid[0]) or {}).get("State") or {}
|
||||
if (st.get("Health") or {}).get("Status") == "healthy":
|
||||
return cid[0]
|
||||
time.sleep(5)
|
||||
return (cvp._sh(base + ["ps", "-q"], timeout=60).stdout or "").split()[0]
|
||||
|
||||
|
||||
def seed(ip, jar, log):
|
||||
"""Create an account and upload a document through papra's own API."""
|
||||
email = f"proof-{secrets.token_hex(4)}@felhom.invalid"
|
||||
pw = "Proof-" + secrets.token_hex(10) # never recorded
|
||||
r = curl(f"http://{ip}:1221/api/auth/sign-up/email", "-X", "POST",
|
||||
"-H", "Content-Type: application/json", "-c", jar, "-i",
|
||||
"-d", json.dumps({"email": email, "password": pw, "name": "Proof"}))
|
||||
head = (r.stdout or "").splitlines()[0].strip() if r.stdout else "?"
|
||||
log.append(f"sign-up -> {head}")
|
||||
orgs = curl(f"http://{ip}:1221/api/organizations", "-b", jar).stdout or ""
|
||||
m = re.search(r'"id"\s*:\s*"([^"]+)"', orgs)
|
||||
if not m:
|
||||
log.append(f"organizations -> no id in {orgs[:160]}")
|
||||
return {"account": head, "document_uploaded": False}
|
||||
p = f"/tmp/proofdoc-{secrets.token_hex(3)}.txt"
|
||||
open(p, "wb").write(DOC)
|
||||
u = curl(f"http://{ip}:1221/api/organizations/{m.group(1)}/documents", "-X", "POST",
|
||||
"-b", jar, "-F", f"file=@{p}", "-i")
|
||||
os.remove(p)
|
||||
ok = bool(re.search(r"HTTP/1\.[01] 2\d\d", u.stdout or ""))
|
||||
log.append(f"upload -> {(u.stdout or '?').splitlines()[0].strip()}")
|
||||
return {"account": head, "document_uploaded": ok}
|
||||
|
||||
|
||||
def snapshot(cid, log, tag):
|
||||
"""Read the app's data OUT of the container, wherever it lives."""
|
||||
d = tempfile.mkdtemp(prefix="snap-")
|
||||
out = {}
|
||||
try:
|
||||
for path in ("/app/app-data/db/db.sqlite", "/app/data"):
|
||||
dst = os.path.join(d, path.strip("/").replace("/", "_"))
|
||||
if cvp._sh(["docker", "cp", f"{cid}:{path}", dst], timeout=300).returncode != 0:
|
||||
out[path] = {"present": False}
|
||||
continue
|
||||
if os.path.isfile(dst):
|
||||
b = open(dst, "rb").read()
|
||||
out[path] = {"present": True, "bytes": len(b),
|
||||
"sha256": hashlib.sha256(b).hexdigest()}
|
||||
else:
|
||||
out[path] = {"present": True, "dir": True,
|
||||
"files": sorted(os.path.relpath(os.path.join(r, f), dst)
|
||||
for r, _, fs in os.walk(dst) for f in fs)}
|
||||
docs = []
|
||||
if cvp._sh(["docker", "cp", f"{cid}:/app/app-data/documents",
|
||||
os.path.join(d, "documents")], timeout=300).returncode == 0:
|
||||
for r, _, fs in os.walk(os.path.join(d, "documents")):
|
||||
for f in fs:
|
||||
b = open(os.path.join(r, f), "rb").read()
|
||||
docs.append({"size": len(b), "sha256": hashlib.sha256(b).hexdigest(),
|
||||
"is_the_proof_document": hashlib.sha256(b).hexdigest() == DOC_SHA})
|
||||
out["documents"] = docs
|
||||
finally:
|
||||
shutil.rmtree(d, ignore_errors=True)
|
||||
log.append(f"[{tag}] " + json.dumps(out)[:260])
|
||||
return out
|
||||
|
||||
|
||||
def leg(tag, compose_body):
|
||||
log = []
|
||||
work = Path(tempfile.mkdtemp(prefix=f"papra-{tag}-"))
|
||||
(work / "docker-compose.yml").write_text(compose_body)
|
||||
env = cvp.build_env("papra", (TPL / ".felhom.yml").read_text(), compose_body)
|
||||
(work / ".env").write_text("".join(f"{k}={v}\n" for k, v in sorted(env.items())))
|
||||
project = "pp" + tag
|
||||
base = ["docker", "compose", "-p", project, "--project-directory", str(work),
|
||||
"-f", str(work / "docker-compose.yml")]
|
||||
cvp._sh(["docker", "network", "create", "traefik-public"], timeout=60)
|
||||
jar = f"/tmp/pj-{tag}"
|
||||
try:
|
||||
cvp._sh(base + ["up", "-d"], timeout=1200)
|
||||
cid = wait_healthy(base)
|
||||
ip = [n["IPAddress"] for n in
|
||||
((cvp._inspect(cid) or {}).get("NetworkSettings") or {}).get("Networks", {}).values()
|
||||
if n.get("IPAddress")][0]
|
||||
time.sleep(15)
|
||||
seeded = seed(ip, jar, log)
|
||||
time.sleep(20)
|
||||
before = snapshot(cid, log, "before")
|
||||
|
||||
cvp._sh(base + ["down"], timeout=900) # a REDEPLOY: no -v
|
||||
cvp._sh(base + ["up", "-d"], timeout=1200)
|
||||
cid2 = wait_healthy(base)
|
||||
time.sleep(25)
|
||||
after = snapshot(cid2, log, "after")
|
||||
|
||||
db = "/app/app-data/db/db.sqlite"
|
||||
db_same = bool(before[db].get("sha256")
|
||||
and before[db].get("sha256") == after[db].get("sha256"))
|
||||
doc_b = any(x["is_the_proof_document"] for x in before["documents"])
|
||||
doc_a = any(x["is_the_proof_document"] for x in after["documents"])
|
||||
return {"leg": tag, "seeded": seeded, "before": before, "after": after,
|
||||
"db_identical_after_redeploy": db_same,
|
||||
"uploaded_document_present_before": doc_b,
|
||||
"uploaded_document_present_after": doc_a,
|
||||
"verdict": ("USER DATA SURVIVES" if (db_same and doc_b and doc_a)
|
||||
else "USER DATA LOST" if doc_b and not doc_a
|
||||
else "INCONCLUSIVE - the document was never uploaded"
|
||||
if not doc_b else "DATA LOST"),
|
||||
"log": log}
|
||||
finally:
|
||||
cvp._sh(base + ["down", "-v", "--remove-orphans"], timeout=900)
|
||||
shutil.rmtree(work, ignore_errors=True)
|
||||
if os.path.exists(jar):
|
||||
os.remove(jar)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
EV.mkdir(parents=True, exist_ok=True)
|
||||
cur = (TPL / "docker-compose.yml").read_text()
|
||||
fixed = cur.replace("- papra_data:/app/data", "- papra_data:/app/app-data")
|
||||
assert fixed != cur, "the mount line did not match — refusing to 'prove' an unchanged template"
|
||||
(EV / "template.diff").write_text(
|
||||
"--- current\n+++ fixed\n- - papra_data:/app/data\n+ - papra_data:/app/app-data\n")
|
||||
out = {}
|
||||
for tag, body in (("current", cur), ("fixed", fixed)):
|
||||
out[tag] = leg(tag, body)
|
||||
r = out[tag]
|
||||
print(f"{tag:<8} {r['verdict']:<34} db_identical={r['db_identical_after_redeploy']} "
|
||||
f"doc_before={r['uploaded_document_present_before']} "
|
||||
f"doc_after={r['uploaded_document_present_after']}", flush=True)
|
||||
(EV / "papra-fix-proof.json").write_text(json.dumps(out, indent=2, sort_keys=True))
|
||||
@@ -0,0 +1,42 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Re-derive every verdict from the captured probes using the committed `classify()`.
|
||||
|
||||
The structural "did anything land in a mount at all?" rule was added to `classify()` after the
|
||||
sweep had started. It reads only fields the probe already captures (mount occupancy and the
|
||||
directories created outside every mount), and the Docker-touching half of the gate — the part that
|
||||
produces `probe` — was not changed by it. So the matrix is recomputed rather than re-measured, and
|
||||
the verdict every app carries is the one the COMMITTED gate produces for its captured evidence.
|
||||
|
||||
Anything the rule could not have seen would require a fresh capture; nothing here does.
|
||||
|
||||
Usage: reclassify.py <evidence-dir>
|
||||
"""
|
||||
import importlib.util, json, sys
|
||||
from pathlib import Path
|
||||
|
||||
_spec = importlib.util.spec_from_file_location(
|
||||
"cvp", Path(__file__).resolve().parent / "check-volume-persistence.py")
|
||||
cvp = importlib.util.module_from_spec(_spec)
|
||||
_spec.loader.exec_module(cvp)
|
||||
|
||||
EV = Path(sys.argv[1])
|
||||
changed = []
|
||||
for d in sorted(EV.iterdir()):
|
||||
f = d / "probe.json"
|
||||
if not f.is_file():
|
||||
continue
|
||||
rec = json.loads(f.read_text())
|
||||
old = rec.get("verdict")
|
||||
status, why = cvp.classify(rec.get("probe") or {})
|
||||
if status != old:
|
||||
changed.append((d.name, old, status))
|
||||
rec["verdict"] = status
|
||||
rec["reasons"] = why
|
||||
rec["verdict_recomputed_by"] = "check-volume-persistence.py classify() (see reclassify.py)"
|
||||
f.write_text(json.dumps(rec, indent=2, sort_keys=True))
|
||||
|
||||
print(f"re-classified {sum(1 for d in EV.iterdir() if (d / 'probe.json').is_file())} apps")
|
||||
for a, o, n in changed:
|
||||
print(f" CHANGED {a}: {o} -> {n}")
|
||||
if not changed:
|
||||
print(" no verdict changed")
|
||||
@@ -0,0 +1,146 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Survives-a-redeploy prover — the second half of the proof the gate cannot give.
|
||||
|
||||
`check-volume-persistence.py` answers *where did the data land*. This answers *is it still there
|
||||
after a redeploy*, which is the claim the customer actually cares about. Presence is not success:
|
||||
that a volume exists afterwards says nothing about whether the data is in it.
|
||||
|
||||
Method, per app:
|
||||
1. deploy from the template, exercise, settle
|
||||
2. FINGERPRINT every file the app created inside every declared mount — (relpath, size, sha256,
|
||||
inode), read host-side from the volume's own directory
|
||||
3. `docker compose down` — NO `-v`. That is a redeploy; `down --volumes` is the UNINSTALL path
|
||||
and destroys volumes deliberately (Campaign 7 §2)
|
||||
4. `docker compose up -d`, wait, settle
|
||||
5. RE-FINGERPRINT. A file SURVIVED only if sha256 AND inode both match — the same bytes in the
|
||||
same file object, not a fresh file the app recreated under the same name.
|
||||
|
||||
Nothing is ever seeded into a volume by hand. R-156's own evidence shows a root-written canary
|
||||
making an empty volume read as populated, which is the confusion this work exists to remove.
|
||||
|
||||
Usage: python3 survive2.py <app> [<app> …] (templates under /opt/sweep/templates)
|
||||
"""
|
||||
import hashlib, importlib.util, json, os, shutil, sys, tempfile, time
|
||||
from pathlib import Path
|
||||
|
||||
_spec = importlib.util.spec_from_file_location("cvp", "/opt/sweep/check-volume-persistence.py")
|
||||
cvp = importlib.util.module_from_spec(_spec)
|
||||
_spec.loader.exec_module(cvp)
|
||||
|
||||
TEMPLATES = Path("/opt/sweep/templates")
|
||||
EVIDENCE = Path("/opt/sweep/evidence")
|
||||
|
||||
|
||||
def fingerprint(mounts):
|
||||
fp = {}
|
||||
for m in mounts:
|
||||
src = m["source"]
|
||||
if not src or not os.path.isdir(src):
|
||||
continue
|
||||
for dp, _, fns in os.walk(src, onerror=lambda e: None):
|
||||
for fn in fns:
|
||||
p = os.path.join(dp, fn)
|
||||
if not os.path.isfile(p):
|
||||
continue
|
||||
try:
|
||||
st = os.lstat(p)
|
||||
h = hashlib.sha256(open(p, "rb").read()).hexdigest()
|
||||
except OSError:
|
||||
continue
|
||||
fp[f"{m['target']}::{os.path.relpath(p, src)}"] = {
|
||||
"size": st.st_size, "sha256": h, "inode": st.st_ino}
|
||||
return fp
|
||||
|
||||
|
||||
def bring_up(base, declared, project, ports, settle=45, wait=300):
|
||||
rc = cvp._sh(base + ["up", "-d"], timeout=1800).returncode
|
||||
cids = [c for c in cvp._sh(base + ["ps", "-aq"], timeout=120).stdout.split() if c]
|
||||
deadline = time.time() + wait
|
||||
while time.time() < deadline:
|
||||
pend = False
|
||||
for cid in cids:
|
||||
st = (cvp._inspect(cid) or {}).get("State") or {}
|
||||
if st.get("Status") in ("created", "restarting") or \
|
||||
(st.get("Health") or {}).get("Status") == "starting":
|
||||
pend = True
|
||||
if not pend:
|
||||
break
|
||||
time.sleep(10)
|
||||
running = [c for c in cids
|
||||
if ((cvp._inspect(c) or {}).get("State") or {}).get("Status") == "running"]
|
||||
if running and ports:
|
||||
cvp._exercise(running, ports)
|
||||
time.sleep(settle)
|
||||
mounts = []
|
||||
for cid in cids:
|
||||
info = cvp._inspect(cid)
|
||||
if not info:
|
||||
continue
|
||||
nm = (info.get("Name") or cid).lstrip("/")
|
||||
for m in (info.get("Mounts") or []):
|
||||
cls = cvp._classify_mount(m, project, declared)
|
||||
if cls == "tmpfs":
|
||||
continue
|
||||
mounts.append({"ctr": nm, "target": m.get("Destination"), "class": cls,
|
||||
"source": m.get("Source"), "name": m.get("Name")})
|
||||
return rc, cids, mounts
|
||||
|
||||
|
||||
def prove(app, tdir=None, label=None):
|
||||
tdir = Path(tdir) if tdir else TEMPLATES / app
|
||||
label = label or app
|
||||
edir = EVIDENCE / app
|
||||
edir.mkdir(parents=True, exist_ok=True)
|
||||
work = Path(tempfile.mkdtemp(prefix=f"surv-{label}-"))
|
||||
compose_text = (tdir / "docker-compose.yml").read_text()
|
||||
felhom_text = (tdir / ".felhom.yml").read_text() if (tdir / ".felhom.yml").is_file() else ""
|
||||
shutil.copy(tdir / "docker-compose.yml", work / "docker-compose.yml")
|
||||
env = cvp.build_env(app, felhom_text, compose_text)
|
||||
(work / ".env").write_text("".join(f"{k}={v}\n" for k, v in sorted(env.items())))
|
||||
project = "sv" + "".join(ch for ch in label.lower() if ch.isalnum())
|
||||
base = ["docker", "compose", "-p", project, "--project-directory", str(work),
|
||||
"-f", str(work / "docker-compose.yml")]
|
||||
os.makedirs(cvp.SCRATCH_HDD + "/userdata", exist_ok=True)
|
||||
os.makedirs(cvp.SCRATCH_IMPORT, exist_ok=True)
|
||||
cvp._sh(["docker", "network", "create", "traefik-public"], timeout=60)
|
||||
try:
|
||||
cfg = json.loads(cvp._sh(base + ["config", "--format", "json"], timeout=180).stdout)
|
||||
declared = set((cfg.get("volumes") or {}).keys())
|
||||
ports = sorted({int(m.group(1))
|
||||
for svc in (cfg.get("services") or {}).values()
|
||||
for lbl in ((svc.get("labels") or {}).values()
|
||||
if isinstance(svc.get("labels"), dict)
|
||||
else (svc.get("labels") or []))
|
||||
for m in [cvp.PORT_RE.search(str(lbl))] if m})
|
||||
|
||||
rc1, _, mounts1 = bring_up(base, declared, project, ports)
|
||||
before = fingerprint(mounts1)
|
||||
|
||||
cvp._sh(base + ["down"], timeout=900) # a REDEPLOY — no -v
|
||||
rc2, _, mounts2 = bring_up(base, declared, project, ports)
|
||||
after = fingerprint(mounts2)
|
||||
|
||||
survived = sorted(k for k in before if k in after
|
||||
and after[k]["sha256"] == before[k]["sha256"]
|
||||
and after[k]["inode"] == before[k]["inode"])
|
||||
changed = sorted(k for k in before if k in after and k not in survived)
|
||||
lost = sorted(k for k in before if k not in after)
|
||||
res = {"app": app, "label": label, "up1_rc": rc1, "up2_rc": rc2,
|
||||
"files_before": len(before), "files_after": len(after),
|
||||
"survived": survived, "changed_in_place": changed, "lost": lost,
|
||||
"mounts": mounts1,
|
||||
"verdict": ("SURVIVES" if (before and not lost and survived)
|
||||
else "NO-DATA-TO-LOSE" if not before else "DATA-LOST")}
|
||||
(edir / f"survive-{label}.json").write_text(json.dumps(res, indent=2, sort_keys=True))
|
||||
return res
|
||||
finally:
|
||||
cvp._sh(base + ["down", "-v", "--remove-orphans"], timeout=900)
|
||||
shutil.rmtree(work, ignore_errors=True)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
for a in sys.argv[1:]:
|
||||
r = prove(a)
|
||||
print("%-16s %-16s before=%d after=%d survived=%d changed=%d lost=%d %s"
|
||||
% (a, r["verdict"], r["files_before"], r["files_after"], len(r["survived"]),
|
||||
len(r["changed_in_place"]), len(r["lost"]), r["lost"][:3]), flush=True)
|
||||
@@ -0,0 +1,69 @@
|
||||
{
|
||||
"probe": {
|
||||
"app": "bentopdf",
|
||||
"containers": [
|
||||
{
|
||||
"diff_added": 6,
|
||||
"diff_benign_db_touches": [],
|
||||
"diff_data_dirs": [],
|
||||
"diff_other_dirs": [
|
||||
{
|
||||
"added": [],
|
||||
"changed_count": 2,
|
||||
"db_signature": false,
|
||||
"dir": "/",
|
||||
"files": [
|
||||
"etc",
|
||||
"var"
|
||||
]
|
||||
},
|
||||
{
|
||||
"added": [],
|
||||
"changed_count": 1,
|
||||
"db_signature": false,
|
||||
"dir": "/etc",
|
||||
"files": [
|
||||
"nginx"
|
||||
]
|
||||
},
|
||||
{
|
||||
"added": [],
|
||||
"changed_count": 1,
|
||||
"db_signature": false,
|
||||
"dir": "/var",
|
||||
"files": [
|
||||
"cache"
|
||||
]
|
||||
}
|
||||
],
|
||||
"diff_token_dirs": [],
|
||||
"diff_total": 14,
|
||||
"diff_unresolved": [],
|
||||
"exit": 0,
|
||||
"gid": 101,
|
||||
"health": "healthy",
|
||||
"image": "ghcr.io/alam00000/bentopdf:v2.8.6",
|
||||
"mounts": [],
|
||||
"name": "bentopdf",
|
||||
"restarts": null,
|
||||
"status": "running",
|
||||
"uid": 101
|
||||
}
|
||||
],
|
||||
"declared_volumes": [],
|
||||
"env_keys": [
|
||||
"DOMAIN",
|
||||
"HDD_PATH",
|
||||
"IMPORT_PATH",
|
||||
"SUBDOMAIN",
|
||||
"TZ",
|
||||
"USERDATA_PATH"
|
||||
],
|
||||
"exercise": [],
|
||||
"ports": []
|
||||
},
|
||||
"reasons": [
|
||||
"nothing was written to any mount and nothing data-classified in any writable layer \u2014 the app produced no data to locate. Health is not data: this is UNDETERMINED, not CLEAN"
|
||||
],
|
||||
"verdict": "UNDETERMINED"
|
||||
}
|
||||
@@ -0,0 +1,176 @@
|
||||
{
|
||||
"probe": {
|
||||
"app": "claper",
|
||||
"containers": [
|
||||
{
|
||||
"diff_added": 2,
|
||||
"diff_benign_db_touches": [],
|
||||
"diff_data_dirs": [],
|
||||
"diff_other_dirs": [
|
||||
{
|
||||
"added": [],
|
||||
"changed_count": 1,
|
||||
"db_signature": false,
|
||||
"dir": "/",
|
||||
"files": [
|
||||
"app"
|
||||
]
|
||||
},
|
||||
{
|
||||
"added": [],
|
||||
"changed_count": 1,
|
||||
"db_signature": false,
|
||||
"dir": "/app",
|
||||
"files": [
|
||||
"priv"
|
||||
]
|
||||
},
|
||||
{
|
||||
"added": [
|
||||
"static"
|
||||
],
|
||||
"changed_count": 0,
|
||||
"db_signature": false,
|
||||
"dir": "/app/priv",
|
||||
"files": [
|
||||
"static"
|
||||
]
|
||||
},
|
||||
{
|
||||
"added": [
|
||||
"uploads"
|
||||
],
|
||||
"changed_count": 0,
|
||||
"db_signature": false,
|
||||
"dir": "/app/priv/static",
|
||||
"files": [
|
||||
"uploads"
|
||||
]
|
||||
}
|
||||
],
|
||||
"diff_token_dirs": [],
|
||||
"diff_total": 4,
|
||||
"diff_unresolved": [],
|
||||
"exit": 0,
|
||||
"gid": 0,
|
||||
"health": "healthy",
|
||||
"image": "ghcr.io/claperco/claper:2.5",
|
||||
"mounts": [
|
||||
{
|
||||
"class": "named-declared",
|
||||
"files": 0,
|
||||
"name": "volgate-claper_claper_uploads",
|
||||
"sample": [],
|
||||
"source": "/var/lib/docker/volumes/volgate-claper_claper_uploads/_data",
|
||||
"target": "/app/priv/static/uploads",
|
||||
"writable_by_app": "yes"
|
||||
}
|
||||
],
|
||||
"name": "claper",
|
||||
"restarts": null,
|
||||
"status": "running",
|
||||
"uid": 0
|
||||
},
|
||||
{
|
||||
"diff_added": 2,
|
||||
"diff_benign_db_touches": [],
|
||||
"diff_data_dirs": [],
|
||||
"diff_other_dirs": [
|
||||
{
|
||||
"added": [],
|
||||
"changed_count": 1,
|
||||
"db_signature": false,
|
||||
"dir": "/",
|
||||
"files": [
|
||||
"run"
|
||||
]
|
||||
}
|
||||
],
|
||||
"diff_token_dirs": [],
|
||||
"diff_total": 4,
|
||||
"diff_unresolved": [],
|
||||
"exit": 0,
|
||||
"gid": 70,
|
||||
"health": "healthy",
|
||||
"image": "postgres:16-alpine",
|
||||
"mounts": [
|
||||
{
|
||||
"class": "named-declared",
|
||||
"files": 1469,
|
||||
"name": "volgate-claper_claper_postgres_data",
|
||||
"sample": [
|
||||
"postmaster.opts",
|
||||
"postgresql.conf",
|
||||
"postgresql.auto.conf",
|
||||
"pg_ident.conf",
|
||||
"pg_hba.conf",
|
||||
"postmaster.pid",
|
||||
"PG_VERSION",
|
||||
"base/1/826",
|
||||
"base/1/2996",
|
||||
"base/1/3764",
|
||||
"base/1/2610_fsm",
|
||||
"base/1/3603_fsm",
|
||||
"base/1/1249_vm",
|
||||
"base/1/3600_fsm",
|
||||
"base/1/13512",
|
||||
"base/1/3602",
|
||||
"base/1/6228",
|
||||
"base/1/4174",
|
||||
"base/1/4157",
|
||||
"base/1/4146",
|
||||
"base/1/2611",
|
||||
"base/1/2657",
|
||||
"base/1/3079_vm",
|
||||
"base/1/1247_vm",
|
||||
"base/1/1259",
|
||||
"base/1/3601_fsm",
|
||||
"base/1/1417",
|
||||
"base/1/4155",
|
||||
"base/1/2668",
|
||||
"base/1/2831",
|
||||
"base/1/2841",
|
||||
"base/1/3607",
|
||||
"base/1/2836",
|
||||
"base/1/1255_fsm",
|
||||
"base/1/2615_vm",
|
||||
"base/1/3575",
|
||||
"base/1/2605_vm",
|
||||
"base/1/2670",
|
||||
"base/1/6106",
|
||||
"base/1/4163"
|
||||
],
|
||||
"source": "/var/lib/docker/volumes/volgate-claper_claper_postgres_data/_data",
|
||||
"target": "/var/lib/postgresql/data",
|
||||
"writable_by_app": "yes"
|
||||
}
|
||||
],
|
||||
"name": "claper-postgres",
|
||||
"restarts": null,
|
||||
"status": "running",
|
||||
"uid": 70
|
||||
}
|
||||
],
|
||||
"declared_volumes": [
|
||||
"claper_postgres_data",
|
||||
"claper_uploads"
|
||||
],
|
||||
"env_keys": [
|
||||
"DB_PASSWORD",
|
||||
"DOMAIN",
|
||||
"HDD_PATH",
|
||||
"IMPORT_PATH",
|
||||
"SECRET_KEY_BASE",
|
||||
"SUBDOMAIN",
|
||||
"TZ",
|
||||
"USERDATA_PATH"
|
||||
],
|
||||
"exercise": [],
|
||||
"ports": []
|
||||
},
|
||||
"reasons": [
|
||||
"claper: this container's mounts are all empty while it created entries in ['/app/priv', '/app/priv/static'] \u2014 benign when a sibling container holds the state, worth a look when none does",
|
||||
"claper: declared volume /app/priv/static/uploads is EMPTY"
|
||||
],
|
||||
"verdict": "CLEAN"
|
||||
}
|
||||
@@ -0,0 +1,216 @@
|
||||
{
|
||||
"probe": {
|
||||
"app": "docmost",
|
||||
"containers": [
|
||||
{
|
||||
"diff_added": 1,
|
||||
"diff_benign_db_touches": [],
|
||||
"diff_data_dirs": [],
|
||||
"diff_other_dirs": [
|
||||
{
|
||||
"added": [],
|
||||
"changed_count": 1,
|
||||
"db_signature": false,
|
||||
"dir": "/",
|
||||
"files": [
|
||||
"app"
|
||||
]
|
||||
},
|
||||
{
|
||||
"added": [],
|
||||
"changed_count": 1,
|
||||
"db_signature": false,
|
||||
"dir": "/app",
|
||||
"files": [
|
||||
"apps"
|
||||
]
|
||||
},
|
||||
{
|
||||
"added": [],
|
||||
"changed_count": 1,
|
||||
"db_signature": false,
|
||||
"dir": "/app/apps",
|
||||
"files": [
|
||||
"client"
|
||||
]
|
||||
},
|
||||
{
|
||||
"added": [],
|
||||
"changed_count": 1,
|
||||
"db_signature": false,
|
||||
"dir": "/app/apps/client",
|
||||
"files": [
|
||||
"dist"
|
||||
]
|
||||
},
|
||||
{
|
||||
"added": [
|
||||
"index-template.html"
|
||||
],
|
||||
"changed_count": 1,
|
||||
"db_signature": false,
|
||||
"dir": "/app/apps/client/dist",
|
||||
"files": [
|
||||
"index-template.html"
|
||||
]
|
||||
}
|
||||
],
|
||||
"diff_token_dirs": [],
|
||||
"diff_total": 6,
|
||||
"diff_unresolved": [],
|
||||
"exit": 0,
|
||||
"gid": 1000,
|
||||
"health": "healthy",
|
||||
"image": "docmost/docmost:0.95.0",
|
||||
"mounts": [
|
||||
{
|
||||
"class": "named-declared",
|
||||
"files": 0,
|
||||
"name": "volgate-docmost_docmost_storage",
|
||||
"sample": [],
|
||||
"source": "/var/lib/docker/volumes/volgate-docmost_docmost_storage/_data",
|
||||
"target": "/app/data/storage",
|
||||
"writable_by_app": "yes"
|
||||
}
|
||||
],
|
||||
"name": "docmost",
|
||||
"restarts": null,
|
||||
"status": "running",
|
||||
"uid": 1000
|
||||
},
|
||||
{
|
||||
"diff_added": 2,
|
||||
"diff_benign_db_touches": [],
|
||||
"diff_data_dirs": [],
|
||||
"diff_other_dirs": [
|
||||
{
|
||||
"added": [],
|
||||
"changed_count": 1,
|
||||
"db_signature": false,
|
||||
"dir": "/",
|
||||
"files": [
|
||||
"run"
|
||||
]
|
||||
}
|
||||
],
|
||||
"diff_token_dirs": [],
|
||||
"diff_total": 4,
|
||||
"diff_unresolved": [],
|
||||
"exit": 0,
|
||||
"gid": 70,
|
||||
"health": "healthy",
|
||||
"image": "postgres:16-alpine",
|
||||
"mounts": [
|
||||
{
|
||||
"class": "named-declared",
|
||||
"files": 1540,
|
||||
"name": "volgate-docmost_docmost_postgres_data",
|
||||
"sample": [
|
||||
"postmaster.opts",
|
||||
"postgresql.conf",
|
||||
"postgresql.auto.conf",
|
||||
"pg_ident.conf",
|
||||
"pg_hba.conf",
|
||||
"postmaster.pid",
|
||||
"PG_VERSION",
|
||||
"base/1/826",
|
||||
"base/1/2996",
|
||||
"base/1/3764",
|
||||
"base/1/2610_fsm",
|
||||
"base/1/3603_fsm",
|
||||
"base/1/1249_vm",
|
||||
"base/1/3600_fsm",
|
||||
"base/1/13512",
|
||||
"base/1/3602",
|
||||
"base/1/6228",
|
||||
"base/1/4174",
|
||||
"base/1/4157",
|
||||
"base/1/4146",
|
||||
"base/1/2611",
|
||||
"base/1/2657",
|
||||
"base/1/3079_vm",
|
||||
"base/1/1247_vm",
|
||||
"base/1/1259",
|
||||
"base/1/3601_fsm",
|
||||
"base/1/1417",
|
||||
"base/1/4155",
|
||||
"base/1/2668",
|
||||
"base/1/2831",
|
||||
"base/1/2841",
|
||||
"base/1/3607",
|
||||
"base/1/2836",
|
||||
"base/1/1255_fsm",
|
||||
"base/1/2615_vm",
|
||||
"base/1/3575",
|
||||
"base/1/2605_vm",
|
||||
"base/1/2670",
|
||||
"base/1/6106",
|
||||
"base/1/4163"
|
||||
],
|
||||
"source": "/var/lib/docker/volumes/volgate-docmost_docmost_postgres_data/_data",
|
||||
"target": "/var/lib/postgresql/data",
|
||||
"writable_by_app": "yes"
|
||||
}
|
||||
],
|
||||
"name": "docmost-postgres",
|
||||
"restarts": null,
|
||||
"status": "running",
|
||||
"uid": 70
|
||||
},
|
||||
{
|
||||
"diff_added": 0,
|
||||
"diff_benign_db_touches": [],
|
||||
"diff_data_dirs": [],
|
||||
"diff_other_dirs": [],
|
||||
"diff_token_dirs": [],
|
||||
"diff_total": 0,
|
||||
"diff_unresolved": [],
|
||||
"exit": 0,
|
||||
"gid": 1000,
|
||||
"health": "healthy",
|
||||
"image": "redis:7-alpine",
|
||||
"mounts": [
|
||||
{
|
||||
"class": "named-declared",
|
||||
"files": 3,
|
||||
"name": "volgate-docmost_docmost_redis_data",
|
||||
"sample": [
|
||||
"appendonlydir/appendonly.aof.1.base.rdb",
|
||||
"appendonlydir/appendonly.aof.1.incr.aof",
|
||||
"appendonlydir/appendonly.aof.manifest"
|
||||
],
|
||||
"source": "/var/lib/docker/volumes/volgate-docmost_docmost_redis_data/_data",
|
||||
"target": "/data",
|
||||
"writable_by_app": "yes"
|
||||
}
|
||||
],
|
||||
"name": "docmost-redis",
|
||||
"restarts": null,
|
||||
"status": "running",
|
||||
"uid": 999
|
||||
}
|
||||
],
|
||||
"declared_volumes": [
|
||||
"docmost_postgres_data",
|
||||
"docmost_redis_data",
|
||||
"docmost_storage"
|
||||
],
|
||||
"env_keys": [
|
||||
"APP_SECRET",
|
||||
"DB_PASSWORD",
|
||||
"DOMAIN",
|
||||
"HDD_PATH",
|
||||
"IMPORT_PATH",
|
||||
"SUBDOMAIN",
|
||||
"TZ",
|
||||
"USERDATA_PATH"
|
||||
],
|
||||
"exercise": [],
|
||||
"ports": []
|
||||
},
|
||||
"reasons": [
|
||||
"docmost: this container's mounts are all empty while it created entries in ['/app/apps/client/dist'] \u2014 benign when a sibling container holds the state, worth a look when none does",
|
||||
"docmost: declared volume /app/data/storage is EMPTY"
|
||||
],
|
||||
"verdict": "CLEAN"
|
||||
}
|
||||
@@ -0,0 +1,73 @@
|
||||
{
|
||||
"probe": {
|
||||
"app": "glance",
|
||||
"containers": [
|
||||
{
|
||||
"diff_added": 1,
|
||||
"diff_benign_db_touches": [],
|
||||
"diff_data_dirs": [],
|
||||
"diff_other_dirs": [
|
||||
{
|
||||
"added": [],
|
||||
"changed_count": 1,
|
||||
"db_signature": false,
|
||||
"dir": "/",
|
||||
"files": [
|
||||
"app"
|
||||
]
|
||||
},
|
||||
{
|
||||
"added": [
|
||||
"config"
|
||||
],
|
||||
"changed_count": 0,
|
||||
"db_signature": false,
|
||||
"dir": "/app",
|
||||
"files": [
|
||||
"config"
|
||||
]
|
||||
}
|
||||
],
|
||||
"diff_token_dirs": [],
|
||||
"diff_total": 2,
|
||||
"diff_unresolved": [],
|
||||
"exit": 1,
|
||||
"gid": null,
|
||||
"health": "unhealthy",
|
||||
"image": "glanceapp/glance:v0.8.5",
|
||||
"mounts": [
|
||||
{
|
||||
"class": "named-declared",
|
||||
"files": 0,
|
||||
"name": "volgate-glance_glance_config",
|
||||
"sample": [],
|
||||
"source": "/var/lib/docker/volumes/volgate-glance_glance_config/_data",
|
||||
"target": "/app/config",
|
||||
"writable_by_app": "unknown-uid"
|
||||
}
|
||||
],
|
||||
"name": "glance",
|
||||
"restarts": null,
|
||||
"status": "restarting",
|
||||
"uid": null
|
||||
}
|
||||
],
|
||||
"declared_volumes": [
|
||||
"glance_config"
|
||||
],
|
||||
"env_keys": [
|
||||
"DOMAIN",
|
||||
"HDD_PATH",
|
||||
"IMPORT_PATH",
|
||||
"SUBDOMAIN",
|
||||
"TZ",
|
||||
"USERDATA_PATH"
|
||||
],
|
||||
"exercise": [],
|
||||
"ports": []
|
||||
},
|
||||
"reasons": [
|
||||
"glance: not running (status=restarting exit=1 restarts=None)"
|
||||
],
|
||||
"verdict": "UNDETERMINED"
|
||||
}
|
||||
@@ -0,0 +1,265 @@
|
||||
{
|
||||
"probe": {
|
||||
"app": "gramps-web",
|
||||
"containers": [
|
||||
{
|
||||
"diff_added": 127,
|
||||
"diff_benign_db_touches": [],
|
||||
"diff_data_dirs": [
|
||||
{
|
||||
"added": [
|
||||
"users.sqlite"
|
||||
],
|
||||
"changed_count": 0,
|
||||
"db_signature": true,
|
||||
"dir": "/app/users",
|
||||
"files": [
|
||||
"users.sqlite"
|
||||
]
|
||||
}
|
||||
],
|
||||
"diff_other_dirs": [
|
||||
{
|
||||
"added": [],
|
||||
"changed_count": 3,
|
||||
"db_signature": false,
|
||||
"dir": "/",
|
||||
"files": [
|
||||
"app",
|
||||
"root",
|
||||
"usr"
|
||||
]
|
||||
},
|
||||
{
|
||||
"added": [
|
||||
"data"
|
||||
],
|
||||
"changed_count": 4,
|
||||
"db_signature": false,
|
||||
"dir": "/app",
|
||||
"files": [
|
||||
"data"
|
||||
]
|
||||
},
|
||||
{
|
||||
"added": [],
|
||||
"changed_count": 1,
|
||||
"db_signature": false,
|
||||
"dir": "/app/cache",
|
||||
"files": [
|
||||
"request_cache"
|
||||
]
|
||||
},
|
||||
{
|
||||
"added": [
|
||||
"2029240f6d1128be89ddc32729463129"
|
||||
],
|
||||
"changed_count": 0,
|
||||
"db_signature": false,
|
||||
"dir": "/app/cache/request_cache",
|
||||
"files": [
|
||||
"2029240f6d1128be89ddc32729463129"
|
||||
]
|
||||
},
|
||||
{
|
||||
"added": [],
|
||||
"changed_count": 2,
|
||||
"db_signature": false,
|
||||
"dir": "/app/src",
|
||||
"files": [
|
||||
"alembic_users",
|
||||
"gramps_webapi"
|
||||
]
|
||||
},
|
||||
{
|
||||
"added": [],
|
||||
"changed_count": 1,
|
||||
"db_signature": false,
|
||||
"dir": "/app/src/alembic_users",
|
||||
"files": [
|
||||
"versions"
|
||||
]
|
||||
},
|
||||
{
|
||||
"added": [],
|
||||
"changed_count": 3,
|
||||
"db_signature": false,
|
||||
"dir": "/app/src/gramps_webapi",
|
||||
"files": [
|
||||
"api",
|
||||
"auth",
|
||||
"util"
|
||||
]
|
||||
},
|
||||
{
|
||||
"added": [],
|
||||
"changed_count": 2,
|
||||
"db_signature": false,
|
||||
"dir": "/app/src/gramps_webapi/api",
|
||||
"files": [
|
||||
"resources",
|
||||
"search"
|
||||
]
|
||||
},
|
||||
{
|
||||
"added": [
|
||||
"2029240f6d1128be89ddc32729463129"
|
||||
],
|
||||
"changed_count": 0,
|
||||
"db_signature": false,
|
||||
"dir": "/app/thumbnail_cache",
|
||||
"files": [
|
||||
"2029240f6d1128be89ddc32729463129"
|
||||
]
|
||||
},
|
||||
{
|
||||
"added": [
|
||||
".gramps"
|
||||
],
|
||||
"changed_count": 1,
|
||||
"db_signature": false,
|
||||
"dir": "/root",
|
||||
"files": [
|
||||
".gramps"
|
||||
]
|
||||
},
|
||||
{
|
||||
"added": [
|
||||
"grampsdb"
|
||||
],
|
||||
"changed_count": 0,
|
||||
"db_signature": false,
|
||||
"dir": "/root/.gramps",
|
||||
"files": [
|
||||
"grampsdb"
|
||||
]
|
||||
},
|
||||
{
|
||||
"added": [
|
||||
"edaa44da-5ceb-4855-966e-8462f6b067f7"
|
||||
],
|
||||
"changed_count": 0,
|
||||
"db_signature": false,
|
||||
"dir": "/root/.gramps/grampsdb",
|
||||
"files": [
|
||||
"edaa44da-5ceb-4855-966e-8462f6b067f7"
|
||||
]
|
||||
},
|
||||
{
|
||||
"added": [
|
||||
"name.txt",
|
||||
"database.txt"
|
||||
],
|
||||
"changed_count": 0,
|
||||
"db_signature": false,
|
||||
"dir": "/root/.gramps/grampsdb/edaa44da-5ceb-4855-966e-8462f6b067f7",
|
||||
"files": [
|
||||
"name.txt",
|
||||
"database.txt"
|
||||
]
|
||||
},
|
||||
{
|
||||
"added": [],
|
||||
"changed_count": 1,
|
||||
"db_signature": false,
|
||||
"dir": "/root/gramps",
|
||||
"files": [
|
||||
"gramps60"
|
||||
]
|
||||
},
|
||||
{
|
||||
"added": [],
|
||||
"changed_count": 1,
|
||||
"db_signature": false,
|
||||
"dir": "/root/gramps/gramps60",
|
||||
"files": [
|
||||
"plugins"
|
||||
]
|
||||
},
|
||||
{
|
||||
"added": [],
|
||||
"changed_count": 1,
|
||||
"db_signature": false,
|
||||
"dir": "/root/gramps/gramps60/plugins",
|
||||
"files": [
|
||||
"FilterRules"
|
||||
]
|
||||
},
|
||||
{
|
||||
"added": [],
|
||||
"changed_count": 1,
|
||||
"db_signature": false,
|
||||
"dir": "/usr",
|
||||
"files": [
|
||||
"lib"
|
||||
]
|
||||
}
|
||||
],
|
||||
"diff_token_dirs": [],
|
||||
"diff_total": 153,
|
||||
"diff_unresolved": [],
|
||||
"exit": 0,
|
||||
"gid": 0,
|
||||
"health": "healthy",
|
||||
"image": "ghcr.io/gramps-project/grampsweb:v25.6.0",
|
||||
"mounts": [
|
||||
{
|
||||
"class": "named-declared",
|
||||
"files": 0,
|
||||
"name": "volgate-grampsweb_gramps_data",
|
||||
"sample": [],
|
||||
"source": "/var/lib/docker/volumes/volgate-grampsweb_gramps_data/_data",
|
||||
"target": "/app/data",
|
||||
"writable_by_app": "yes"
|
||||
},
|
||||
{
|
||||
"class": "named-declared",
|
||||
"files": 0,
|
||||
"name": "volgate-grampsweb_gramps_media",
|
||||
"sample": [],
|
||||
"source": "/var/lib/docker/volumes/volgate-grampsweb_gramps_media/_data",
|
||||
"target": "/app/media",
|
||||
"writable_by_app": "yes"
|
||||
},
|
||||
{
|
||||
"class": "named-declared",
|
||||
"files": 0,
|
||||
"name": "volgate-grampsweb_gramps_tmp",
|
||||
"sample": [],
|
||||
"source": "/var/lib/docker/volumes/volgate-grampsweb_gramps_tmp/_data",
|
||||
"target": "/tmp",
|
||||
"writable_by_app": "yes"
|
||||
}
|
||||
],
|
||||
"name": "gramps-web",
|
||||
"restarts": null,
|
||||
"status": "running",
|
||||
"uid": 0
|
||||
}
|
||||
],
|
||||
"declared_volumes": [
|
||||
"gramps_data",
|
||||
"gramps_media",
|
||||
"gramps_tmp"
|
||||
],
|
||||
"env_keys": [
|
||||
"DOMAIN",
|
||||
"GRAMPSWEB_SECRET_KEY",
|
||||
"HDD_PATH",
|
||||
"IMPORT_PATH",
|
||||
"SUBDOMAIN",
|
||||
"TZ",
|
||||
"USERDATA_PATH"
|
||||
],
|
||||
"exercise": [],
|
||||
"ports": []
|
||||
},
|
||||
"reasons": [
|
||||
"gramps-web: DATA in the writable layer at /app/users (db_signature=True, e.g. ['users.sqlite'])",
|
||||
"NOTHING this app wrote landed in ANY folder the template preserves: all 2 mount(s) across 1 container(s) are empty, yet entries were created in ['/app', '/app/cache/request_cache', '/app/thumbnail_cache', '/root']. Needs adjudication.",
|
||||
"gramps-web: this container's mounts are all empty while it created entries in ['/app', '/app/cache/request_cache', '/app/thumbnail_cache'] \u2014 benign when a sibling container holds the state, worth a look when none does",
|
||||
"gramps-web: declared volume /app/data is EMPTY",
|
||||
"gramps-web: declared volume /app/media is EMPTY"
|
||||
],
|
||||
"verdict": "BROKEN"
|
||||
}
|
||||
@@ -0,0 +1,329 @@
|
||||
{
|
||||
"probe": {
|
||||
"app": "immich",
|
||||
"containers": [
|
||||
{
|
||||
"diff_added": 6,
|
||||
"diff_benign_db_touches": [],
|
||||
"diff_data_dirs": [],
|
||||
"diff_other_dirs": [
|
||||
{
|
||||
"added": [
|
||||
"cache"
|
||||
],
|
||||
"changed_count": 1,
|
||||
"db_signature": false,
|
||||
"dir": "/",
|
||||
"files": [
|
||||
"cache"
|
||||
]
|
||||
},
|
||||
{
|
||||
"added": [
|
||||
".config"
|
||||
],
|
||||
"changed_count": 0,
|
||||
"db_signature": false,
|
||||
"dir": "/root",
|
||||
"files": [
|
||||
".config"
|
||||
]
|
||||
},
|
||||
{
|
||||
"added": [
|
||||
"matplotlib"
|
||||
],
|
||||
"changed_count": 0,
|
||||
"db_signature": false,
|
||||
"dir": "/root/.config",
|
||||
"files": [
|
||||
"matplotlib"
|
||||
]
|
||||
}
|
||||
],
|
||||
"diff_token_dirs": [],
|
||||
"diff_total": 7,
|
||||
"diff_unresolved": [],
|
||||
"exit": 0,
|
||||
"gid": 0,
|
||||
"health": "healthy",
|
||||
"image": "ghcr.io/immich-app/immich-machine-learning:v3.0.3",
|
||||
"mounts": [
|
||||
{
|
||||
"class": "named-declared",
|
||||
"files": 0,
|
||||
"name": "volgate-immich_immich_ml_cache",
|
||||
"sample": [],
|
||||
"source": "/var/lib/docker/volumes/volgate-immich_immich_ml_cache/_data",
|
||||
"target": "/cache",
|
||||
"writable_by_app": "yes"
|
||||
}
|
||||
],
|
||||
"name": "immich-machine-learning",
|
||||
"restarts": null,
|
||||
"status": "running",
|
||||
"uid": 0
|
||||
},
|
||||
{
|
||||
"diff_added": 3,
|
||||
"diff_benign_db_touches": [],
|
||||
"diff_data_dirs": [],
|
||||
"diff_other_dirs": [
|
||||
{
|
||||
"added": [],
|
||||
"changed_count": 2,
|
||||
"db_signature": false,
|
||||
"dir": "/",
|
||||
"files": [
|
||||
"etc",
|
||||
"run"
|
||||
]
|
||||
},
|
||||
{
|
||||
"added": [],
|
||||
"changed_count": 1,
|
||||
"db_signature": false,
|
||||
"dir": "/etc",
|
||||
"files": [
|
||||
"postgresql"
|
||||
]
|
||||
},
|
||||
{
|
||||
"added": [
|
||||
"postgresql.conf"
|
||||
],
|
||||
"changed_count": 0,
|
||||
"db_signature": false,
|
||||
"dir": "/etc/postgresql",
|
||||
"files": [
|
||||
"postgresql.conf"
|
||||
]
|
||||
}
|
||||
],
|
||||
"diff_token_dirs": [],
|
||||
"diff_total": 7,
|
||||
"diff_unresolved": [],
|
||||
"exit": 0,
|
||||
"gid": 999,
|
||||
"health": "healthy",
|
||||
"image": "ghcr.io/immich-app/postgres:16-vectorchord0.4.3-pgvectors0.2.0",
|
||||
"mounts": [
|
||||
{
|
||||
"class": "named-declared",
|
||||
"files": 1832,
|
||||
"name": "volgate-immich_immich_postgres_data",
|
||||
"sample": [
|
||||
"postmaster.opts",
|
||||
"postgresql.conf",
|
||||
"postgresql.auto.conf",
|
||||
"pg_ident.conf",
|
||||
"pg_hba.conf",
|
||||
"postmaster.pid",
|
||||
"PG_VERSION",
|
||||
"base/1/826",
|
||||
"base/1/2996",
|
||||
"base/1/3764",
|
||||
"base/1/2610_fsm",
|
||||
"base/1/3603_fsm",
|
||||
"base/1/1249_vm",
|
||||
"base/1/3600_fsm",
|
||||
"base/1/3602",
|
||||
"base/1/6228",
|
||||
"base/1/4174",
|
||||
"base/1/4157",
|
||||
"base/1/4146",
|
||||
"base/1/2611",
|
||||
"base/1/2657",
|
||||
"base/1/3079_vm",
|
||||
"base/1/1247_vm",
|
||||
"base/1/13406",
|
||||
"base/1/1259",
|
||||
"base/1/3601_fsm",
|
||||
"base/1/1417",
|
||||
"base/1/4155",
|
||||
"base/1/2668",
|
||||
"base/1/2831",
|
||||
"base/1/2841",
|
||||
"base/1/13414",
|
||||
"base/1/3607",
|
||||
"base/1/2836",
|
||||
"base/1/1255_fsm",
|
||||
"base/1/2615_vm",
|
||||
"base/1/3575",
|
||||
"base/1/2605_vm",
|
||||
"base/1/2670",
|
||||
"base/1/6106"
|
||||
],
|
||||
"source": "/var/lib/docker/volumes/volgate-immich_immich_postgres_data/_data",
|
||||
"target": "/var/lib/postgresql/data",
|
||||
"writable_by_app": "yes"
|
||||
}
|
||||
],
|
||||
"name": "immich-postgres",
|
||||
"restarts": null,
|
||||
"status": "running",
|
||||
"uid": 999
|
||||
},
|
||||
{
|
||||
"diff_added": 0,
|
||||
"diff_benign_db_touches": [],
|
||||
"diff_data_dirs": [],
|
||||
"diff_other_dirs": [],
|
||||
"diff_token_dirs": [],
|
||||
"diff_total": 0,
|
||||
"diff_unresolved": [],
|
||||
"exit": 0,
|
||||
"gid": 1000,
|
||||
"health": "healthy",
|
||||
"image": "redis:7-alpine",
|
||||
"mounts": [
|
||||
{
|
||||
"class": "named-declared",
|
||||
"files": 3,
|
||||
"name": "volgate-immich_immich_redis_data",
|
||||
"sample": [
|
||||
"appendonlydir/appendonly.aof.1.base.rdb",
|
||||
"appendonlydir/appendonly.aof.1.incr.aof",
|
||||
"appendonlydir/appendonly.aof.manifest"
|
||||
],
|
||||
"source": "/var/lib/docker/volumes/volgate-immich_immich_redis_data/_data",
|
||||
"target": "/data",
|
||||
"writable_by_app": "yes"
|
||||
}
|
||||
],
|
||||
"name": "immich-redis",
|
||||
"restarts": null,
|
||||
"status": "running",
|
||||
"uid": 999
|
||||
},
|
||||
{
|
||||
"diff_added": 4,
|
||||
"diff_benign_db_touches": [],
|
||||
"diff_data_dirs": [],
|
||||
"diff_other_dirs": [
|
||||
{
|
||||
"added": [
|
||||
"external",
|
||||
"data"
|
||||
],
|
||||
"changed_count": 1,
|
||||
"db_signature": false,
|
||||
"dir": "/",
|
||||
"files": [
|
||||
"external",
|
||||
"data"
|
||||
]
|
||||
},
|
||||
{
|
||||
"added": [
|
||||
"photos"
|
||||
],
|
||||
"changed_count": 0,
|
||||
"db_signature": false,
|
||||
"dir": "/external",
|
||||
"files": [
|
||||
"photos"
|
||||
]
|
||||
},
|
||||
{
|
||||
"added": [],
|
||||
"changed_count": 1,
|
||||
"db_signature": false,
|
||||
"dir": "/usr",
|
||||
"files": [
|
||||
"src"
|
||||
]
|
||||
},
|
||||
{
|
||||
"added": [],
|
||||
"changed_count": 1,
|
||||
"db_signature": false,
|
||||
"dir": "/usr/src",
|
||||
"files": [
|
||||
"app"
|
||||
]
|
||||
},
|
||||
{
|
||||
"added": [
|
||||
"upload"
|
||||
],
|
||||
"changed_count": 0,
|
||||
"db_signature": false,
|
||||
"dir": "/usr/src/app",
|
||||
"files": [
|
||||
"upload"
|
||||
]
|
||||
}
|
||||
],
|
||||
"diff_token_dirs": [],
|
||||
"diff_total": 7,
|
||||
"diff_unresolved": [],
|
||||
"exit": 0,
|
||||
"gid": 0,
|
||||
"health": "healthy",
|
||||
"image": "ghcr.io/immich-app/immich-server:v3.0.3",
|
||||
"mounts": [
|
||||
{
|
||||
"class": "bind",
|
||||
"files": 6,
|
||||
"name": null,
|
||||
"sample": [
|
||||
"library/.immich",
|
||||
"upload/.immich",
|
||||
"profile/.immich",
|
||||
"encoded-video/.immich",
|
||||
"thumbs/.immich",
|
||||
"backups/.immich"
|
||||
],
|
||||
"source": "/srv/felhom-gate/hdd/appdata/immich",
|
||||
"target": "/usr/src/app/upload",
|
||||
"writable_by_app": "yes"
|
||||
},
|
||||
{
|
||||
"class": "bind",
|
||||
"files": 0,
|
||||
"name": null,
|
||||
"sample": [],
|
||||
"source": "/srv/felhom-gate/hdd/userdata/media/photos",
|
||||
"target": "/external/photos",
|
||||
"writable_by_app": "yes"
|
||||
},
|
||||
{
|
||||
"class": "anonymous",
|
||||
"files": 0,
|
||||
"name": "51538436a985b60deb1473d5837733528b3440627b2f7761b235f8bd92010975",
|
||||
"sample": [],
|
||||
"source": "/var/lib/docker/volumes/51538436a985b60deb1473d5837733528b3440627b2f7761b235f8bd92010975/_data",
|
||||
"target": "/data",
|
||||
"writable_by_app": "yes"
|
||||
}
|
||||
],
|
||||
"name": "immich-server",
|
||||
"restarts": null,
|
||||
"status": "running",
|
||||
"uid": 0
|
||||
}
|
||||
],
|
||||
"declared_volumes": [
|
||||
"immich_ml_cache",
|
||||
"immich_postgres_data",
|
||||
"immich_redis_data"
|
||||
],
|
||||
"env_keys": [
|
||||
"DB_PASSWORD",
|
||||
"DOMAIN",
|
||||
"HDD_PATH",
|
||||
"IMPORT_PATH",
|
||||
"SUBDOMAIN",
|
||||
"TZ",
|
||||
"USERDATA_PATH"
|
||||
],
|
||||
"exercise": [],
|
||||
"ports": []
|
||||
},
|
||||
"reasons": [
|
||||
"immich-machine-learning: this container's mounts are all empty while it created entries in ['/', '/root', '/root/.config'] \u2014 benign when a sibling container holds the state, worth a look when none does",
|
||||
"immich-machine-learning: declared volume /cache is EMPTY"
|
||||
],
|
||||
"verdict": "CLEAN"
|
||||
}
|
||||
@@ -0,0 +1,99 @@
|
||||
{
|
||||
"probe": {
|
||||
"app": "papra",
|
||||
"containers": [
|
||||
{
|
||||
"diff_added": 173,
|
||||
"diff_benign_db_touches": [],
|
||||
"diff_data_dirs": [
|
||||
{
|
||||
"added": [
|
||||
"db.sqlite"
|
||||
],
|
||||
"changed_count": 0,
|
||||
"db_signature": true,
|
||||
"dir": "/app/app-data/db",
|
||||
"files": [
|
||||
"db.sqlite"
|
||||
]
|
||||
}
|
||||
],
|
||||
"diff_other_dirs": [
|
||||
{
|
||||
"added": [],
|
||||
"changed_count": 1,
|
||||
"db_signature": false,
|
||||
"dir": "/",
|
||||
"files": [
|
||||
"app"
|
||||
]
|
||||
},
|
||||
{
|
||||
"added": [
|
||||
"data"
|
||||
],
|
||||
"changed_count": 1,
|
||||
"db_signature": false,
|
||||
"dir": "/app",
|
||||
"files": [
|
||||
"data"
|
||||
]
|
||||
},
|
||||
{
|
||||
"added": [],
|
||||
"changed_count": 1,
|
||||
"db_signature": false,
|
||||
"dir": "/app/app-data",
|
||||
"files": [
|
||||
"db"
|
||||
]
|
||||
}
|
||||
],
|
||||
"diff_token_dirs": [],
|
||||
"diff_total": 177,
|
||||
"diff_unresolved": [],
|
||||
"exit": 0,
|
||||
"gid": 999,
|
||||
"health": "healthy",
|
||||
"image": "ghcr.io/papra-hq/papra:26.6.1-rootless",
|
||||
"mounts": [
|
||||
{
|
||||
"class": "named-declared",
|
||||
"files": 0,
|
||||
"name": "volgate-papra_papra_data",
|
||||
"sample": [],
|
||||
"source": "/var/lib/docker/volumes/volgate-papra_papra_data/_data",
|
||||
"target": "/app/data",
|
||||
"writable_by_app": "NO"
|
||||
}
|
||||
],
|
||||
"name": "papra",
|
||||
"restarts": null,
|
||||
"status": "running",
|
||||
"uid": 999
|
||||
}
|
||||
],
|
||||
"declared_volumes": [
|
||||
"papra_data"
|
||||
],
|
||||
"env_keys": [
|
||||
"AUTH_SECRET",
|
||||
"DOMAIN",
|
||||
"HDD_PATH",
|
||||
"IMPORT_PATH",
|
||||
"SUBDOMAIN",
|
||||
"TZ",
|
||||
"USERDATA_PATH"
|
||||
],
|
||||
"exercise": [],
|
||||
"ports": []
|
||||
},
|
||||
"reasons": [
|
||||
"papra: mount /app/data is NOT writable by the app's own uid=999 gid=999",
|
||||
"papra: DATA in the writable layer at /app/app-data/db (db_signature=True, e.g. ['db.sqlite'])",
|
||||
"NOTHING this app wrote landed in ANY folder the template preserves: all 1 mount(s) across 1 container(s) are empty, yet entries were created in ['/app']. Needs adjudication.",
|
||||
"papra: this container's mounts are all empty while it created entries in ['/app'] \u2014 benign when a sibling container holds the state, worth a look when none does",
|
||||
"papra: declared volume /app/data is EMPTY"
|
||||
],
|
||||
"verdict": "BROKEN"
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
{
|
||||
"probe": {
|
||||
"app": "plant-it",
|
||||
"error": "no containers created (compose up rc=1: msdeluise/plant-it:0.10.0 Pulling \n Image msdeluise/plant-it:0.10.0 Error pull access denied for msdeluise/plant-it, repository does not exist or may require 'docker login'\nError response from daemon: pull access denied for msdeluise/plant-it, repository does not exist or may require 'docker login'\n)"
|
||||
},
|
||||
"reasons": [
|
||||
"no containers created (compose up rc=1: msdeluise/plant-it:0.10.0 Pulling \n Image msdeluise/plant-it:0.10.0 Error pull access denied for msdeluise/plant-it, repository does not exist or may require 'docker login'\nError response from daemon: pull access denied for msdeluise/plant-it, repository does not exist or may require 'docker login'\n)"
|
||||
],
|
||||
"verdict": "UNDETERMINED"
|
||||
}
|
||||
@@ -0,0 +1,95 @@
|
||||
{
|
||||
"probe": {
|
||||
"app": "privatebin",
|
||||
"containers": [
|
||||
{
|
||||
"diff_added": 0,
|
||||
"diff_benign_db_touches": [],
|
||||
"diff_data_dirs": [],
|
||||
"diff_other_dirs": [],
|
||||
"diff_token_dirs": [],
|
||||
"diff_total": 0,
|
||||
"diff_unresolved": [],
|
||||
"exit": 0,
|
||||
"gid": 82,
|
||||
"health": "healthy",
|
||||
"image": "privatebin/pdo:2.0.5",
|
||||
"mounts": [
|
||||
{
|
||||
"class": "anonymous",
|
||||
"files": 0,
|
||||
"name": "81f6e29600a6e6cbaa58dda4e144909abf098adc3c6116ca85166068c6a02e34",
|
||||
"sample": [],
|
||||
"source": "/var/lib/docker/volumes/81f6e29600a6e6cbaa58dda4e144909abf098adc3c6116ca85166068c6a02e34/_data",
|
||||
"target": "/var/lib/nginx/tmp",
|
||||
"writable_by_app": "yes"
|
||||
},
|
||||
{
|
||||
"class": "named-declared",
|
||||
"files": 0,
|
||||
"name": "volgate-privatebin_privatebin_data",
|
||||
"sample": [],
|
||||
"source": "/var/lib/docker/volumes/volgate-privatebin_privatebin_data/_data",
|
||||
"target": "/srv/data",
|
||||
"writable_by_app": "yes"
|
||||
},
|
||||
{
|
||||
"class": "anonymous",
|
||||
"files": 14,
|
||||
"name": "6c40818b0f3ef0f6687a2394c61219df6a1d806829e2c6d45c137c9e62427a14",
|
||||
"sample": [
|
||||
"nginx.pid",
|
||||
"php-fpm.sock",
|
||||
"services/.s6-svscan/control",
|
||||
"services/.s6-svscan/lock",
|
||||
"services/php-fpm85/run",
|
||||
"services/php-fpm85/supervise/control",
|
||||
"services/php-fpm85/supervise/death_tally",
|
||||
"services/php-fpm85/supervise/status",
|
||||
"services/php-fpm85/supervise/lock",
|
||||
"services/nginx/run",
|
||||
"services/nginx/supervise/control",
|
||||
"services/nginx/supervise/death_tally",
|
||||
"services/nginx/supervise/status",
|
||||
"services/nginx/supervise/lock"
|
||||
],
|
||||
"source": "/var/lib/docker/volumes/6c40818b0f3ef0f6687a2394c61219df6a1d806829e2c6d45c137c9e62427a14/_data",
|
||||
"target": "/run",
|
||||
"writable_by_app": "yes"
|
||||
},
|
||||
{
|
||||
"class": "anonymous",
|
||||
"files": 0,
|
||||
"name": "660faab3050406b4dac39dedfc189331d712fb02b3990aed2e81033a08917cf3",
|
||||
"sample": [],
|
||||
"source": "/var/lib/docker/volumes/660faab3050406b4dac39dedfc189331d712fb02b3990aed2e81033a08917cf3/_data",
|
||||
"target": "/tmp",
|
||||
"writable_by_app": "yes"
|
||||
}
|
||||
],
|
||||
"name": "privatebin",
|
||||
"restarts": null,
|
||||
"status": "running",
|
||||
"uid": 65534
|
||||
}
|
||||
],
|
||||
"declared_volumes": [
|
||||
"privatebin_data"
|
||||
],
|
||||
"env_keys": [
|
||||
"DOMAIN",
|
||||
"HDD_PATH",
|
||||
"IMPORT_PATH",
|
||||
"SUBDOMAIN",
|
||||
"TZ",
|
||||
"USERDATA_PATH"
|
||||
],
|
||||
"exercise": [],
|
||||
"ports": []
|
||||
},
|
||||
"reasons": [
|
||||
"nothing was written to any mount and nothing data-classified in any writable layer \u2014 the app produced no data to locate. Health is not data: this is UNDETERMINED, not CLEAN",
|
||||
"privatebin: declared volume /srv/data is EMPTY"
|
||||
],
|
||||
"verdict": "UNDETERMINED"
|
||||
}
|
||||
@@ -0,0 +1,94 @@
|
||||
{
|
||||
"probe": {
|
||||
"app": "recipe-importer",
|
||||
"containers": [
|
||||
{
|
||||
"diff_added": 13,
|
||||
"diff_benign_db_touches": [],
|
||||
"diff_data_dirs": [],
|
||||
"diff_other_dirs": [
|
||||
{
|
||||
"added": [
|
||||
"data"
|
||||
],
|
||||
"changed_count": 2,
|
||||
"db_signature": false,
|
||||
"dir": "/",
|
||||
"files": [
|
||||
"data"
|
||||
]
|
||||
},
|
||||
{
|
||||
"added": [],
|
||||
"changed_count": 1,
|
||||
"db_signature": false,
|
||||
"dir": "/app",
|
||||
"files": [
|
||||
"app"
|
||||
]
|
||||
},
|
||||
{
|
||||
"added": [],
|
||||
"changed_count": 1,
|
||||
"db_signature": false,
|
||||
"dir": "/usr",
|
||||
"files": [
|
||||
"local"
|
||||
]
|
||||
},
|
||||
{
|
||||
"added": [],
|
||||
"changed_count": 1,
|
||||
"db_signature": false,
|
||||
"dir": "/usr/local",
|
||||
"files": [
|
||||
"lib"
|
||||
]
|
||||
}
|
||||
],
|
||||
"diff_token_dirs": [],
|
||||
"diff_total": 22,
|
||||
"diff_unresolved": [],
|
||||
"exit": 0,
|
||||
"gid": 0,
|
||||
"health": "healthy",
|
||||
"image": "gitea.dooplex.hu/admin/recipe-importer:v0.9.11",
|
||||
"mounts": [
|
||||
{
|
||||
"class": "named-declared",
|
||||
"files": 0,
|
||||
"name": "volgate-recipeimporter_recipe_data",
|
||||
"sample": [],
|
||||
"source": "/var/lib/docker/volumes/volgate-recipeimporter_recipe_data/_data",
|
||||
"target": "/data",
|
||||
"writable_by_app": "yes"
|
||||
}
|
||||
],
|
||||
"name": "recipe-importer",
|
||||
"restarts": null,
|
||||
"status": "running",
|
||||
"uid": 0
|
||||
}
|
||||
],
|
||||
"declared_volumes": [
|
||||
"recipe_data"
|
||||
],
|
||||
"env_keys": [
|
||||
"DOMAIN",
|
||||
"HDD_PATH",
|
||||
"IMPORT_PATH",
|
||||
"SECRET_KEY",
|
||||
"SUBDOMAIN",
|
||||
"TZ",
|
||||
"USERDATA_PATH"
|
||||
],
|
||||
"exercise": [],
|
||||
"ports": []
|
||||
},
|
||||
"reasons": [
|
||||
"NOTHING this app wrote landed in ANY folder the template preserves: all 1 mount(s) across 1 container(s) are empty, yet entries were created in ['/']. Needs adjudication.",
|
||||
"recipe-importer: this container's mounts are all empty while it created entries in ['/'] \u2014 benign when a sibling container holds the state, worth a look when none does",
|
||||
"recipe-importer: declared volume /data is EMPTY"
|
||||
],
|
||||
"verdict": "UNDETERMINED"
|
||||
}
|
||||
@@ -0,0 +1,230 @@
|
||||
{
|
||||
"probe": {
|
||||
"app": "sparkyfitness",
|
||||
"containers": [
|
||||
{
|
||||
"diff_added": 11,
|
||||
"diff_benign_db_touches": [],
|
||||
"diff_data_dirs": [],
|
||||
"diff_other_dirs": [
|
||||
{
|
||||
"added": [],
|
||||
"changed_count": 3,
|
||||
"db_signature": false,
|
||||
"dir": "/",
|
||||
"files": [
|
||||
"etc",
|
||||
"var",
|
||||
"run"
|
||||
]
|
||||
},
|
||||
{
|
||||
"added": [],
|
||||
"changed_count": 1,
|
||||
"db_signature": false,
|
||||
"dir": "/etc",
|
||||
"files": [
|
||||
"nginx"
|
||||
]
|
||||
},
|
||||
{
|
||||
"added": [],
|
||||
"changed_count": 1,
|
||||
"db_signature": false,
|
||||
"dir": "/var",
|
||||
"files": [
|
||||
"cache"
|
||||
]
|
||||
}
|
||||
],
|
||||
"diff_token_dirs": [],
|
||||
"diff_total": 19,
|
||||
"diff_unresolved": [],
|
||||
"exit": 0,
|
||||
"gid": 0,
|
||||
"health": "healthy",
|
||||
"image": "codewithcj/sparkyfitness:v0.17.3",
|
||||
"mounts": [],
|
||||
"name": "sparkyfitness",
|
||||
"restarts": null,
|
||||
"status": "running",
|
||||
"uid": 0
|
||||
},
|
||||
{
|
||||
"diff_added": 2,
|
||||
"diff_benign_db_touches": [],
|
||||
"diff_data_dirs": [],
|
||||
"diff_other_dirs": [
|
||||
{
|
||||
"added": [],
|
||||
"changed_count": 1,
|
||||
"db_signature": false,
|
||||
"dir": "/",
|
||||
"files": [
|
||||
"run"
|
||||
]
|
||||
}
|
||||
],
|
||||
"diff_token_dirs": [],
|
||||
"diff_total": 4,
|
||||
"diff_unresolved": [],
|
||||
"exit": 0,
|
||||
"gid": 70,
|
||||
"health": "healthy",
|
||||
"image": "postgres:15-alpine",
|
||||
"mounts": [
|
||||
{
|
||||
"class": "named-declared",
|
||||
"files": 1814,
|
||||
"name": "volgate-sparkyfitness_sparkyfitness_db_data",
|
||||
"sample": [
|
||||
"pgdata/postmaster.opts",
|
||||
"pgdata/postgresql.conf",
|
||||
"pgdata/postgresql.auto.conf",
|
||||
"pgdata/pg_ident.conf",
|
||||
"pgdata/pg_hba.conf",
|
||||
"pgdata/postmaster.pid",
|
||||
"pgdata/PG_VERSION",
|
||||
"pgdata/base/1/826",
|
||||
"pgdata/base/1/2996",
|
||||
"pgdata/base/1/3764",
|
||||
"pgdata/base/1/2610_fsm",
|
||||
"pgdata/base/1/3603_fsm",
|
||||
"pgdata/base/1/1249_vm",
|
||||
"pgdata/base/1/3600_fsm",
|
||||
"pgdata/base/1/13506_vm",
|
||||
"pgdata/base/1/3602",
|
||||
"pgdata/base/1/6228",
|
||||
"pgdata/base/1/4174",
|
||||
"pgdata/base/1/4157",
|
||||
"pgdata/base/1/4146",
|
||||
"pgdata/base/1/2611",
|
||||
"pgdata/base/1/2657",
|
||||
"pgdata/base/1/3079_vm",
|
||||
"pgdata/base/1/1247_vm",
|
||||
"pgdata/base/1/13501_fsm",
|
||||
"pgdata/base/1/1259",
|
||||
"pgdata/base/1/3601_fsm",
|
||||
"pgdata/base/1/1417",
|
||||
"pgdata/base/1/4155",
|
||||
"pgdata/base/1/2668",
|
||||
"pgdata/base/1/13491_fsm",
|
||||
"pgdata/base/1/2831",
|
||||
"pgdata/base/1/2841",
|
||||
"pgdata/base/1/3607",
|
||||
"pgdata/base/1/2836",
|
||||
"pgdata/base/1/1255_fsm",
|
||||
"pgdata/base/1/2615_vm",
|
||||
"pgdata/base/1/3575",
|
||||
"pgdata/base/1/2605_vm",
|
||||
"pgdata/base/1/2670"
|
||||
],
|
||||
"source": "/var/lib/docker/volumes/volgate-sparkyfitness_sparkyfitness_db_data/_data",
|
||||
"target": "/var/lib/postgresql/data",
|
||||
"writable_by_app": "yes"
|
||||
}
|
||||
],
|
||||
"name": "sparkyfitness-db",
|
||||
"restarts": null,
|
||||
"status": "running",
|
||||
"uid": 70
|
||||
},
|
||||
{
|
||||
"diff_added": 392,
|
||||
"diff_benign_db_touches": [],
|
||||
"diff_data_dirs": [],
|
||||
"diff_other_dirs": [
|
||||
{
|
||||
"added": [],
|
||||
"changed_count": 1,
|
||||
"db_signature": false,
|
||||
"dir": "/",
|
||||
"files": [
|
||||
"app"
|
||||
]
|
||||
},
|
||||
{
|
||||
"added": [],
|
||||
"changed_count": 1,
|
||||
"db_signature": false,
|
||||
"dir": "/app",
|
||||
"files": [
|
||||
"SparkyFitnessServer"
|
||||
]
|
||||
},
|
||||
{
|
||||
"added": [
|
||||
"uploads",
|
||||
"backup",
|
||||
"temp_uploads"
|
||||
],
|
||||
"changed_count": 0,
|
||||
"db_signature": false,
|
||||
"dir": "/app/SparkyFitnessServer",
|
||||
"files": [
|
||||
"uploads",
|
||||
"backup",
|
||||
"temp_uploads"
|
||||
]
|
||||
}
|
||||
],
|
||||
"diff_token_dirs": [],
|
||||
"diff_total": 395,
|
||||
"diff_unresolved": [],
|
||||
"exit": 0,
|
||||
"gid": 0,
|
||||
"health": "healthy",
|
||||
"image": "codewithcj/sparkyfitness_server:v0.17.3",
|
||||
"mounts": [
|
||||
{
|
||||
"class": "named-declared",
|
||||
"files": 0,
|
||||
"name": "volgate-sparkyfitness_sparkyfitness_uploads",
|
||||
"sample": [],
|
||||
"source": "/var/lib/docker/volumes/volgate-sparkyfitness_sparkyfitness_uploads/_data",
|
||||
"target": "/app/SparkyFitnessServer/uploads",
|
||||
"writable_by_app": "yes"
|
||||
},
|
||||
{
|
||||
"class": "named-declared",
|
||||
"files": 0,
|
||||
"name": "volgate-sparkyfitness_sparkyfitness_backup",
|
||||
"sample": [],
|
||||
"source": "/var/lib/docker/volumes/volgate-sparkyfitness_sparkyfitness_backup/_data",
|
||||
"target": "/app/SparkyFitnessServer/backup",
|
||||
"writable_by_app": "yes"
|
||||
}
|
||||
],
|
||||
"name": "sparkyfitness-server",
|
||||
"restarts": null,
|
||||
"status": "running",
|
||||
"uid": 0
|
||||
}
|
||||
],
|
||||
"declared_volumes": [
|
||||
"sparkyfitness_backup",
|
||||
"sparkyfitness_db_data",
|
||||
"sparkyfitness_uploads"
|
||||
],
|
||||
"env_keys": [
|
||||
"API_ENCRYPTION_KEY",
|
||||
"APP_DB_PASSWORD",
|
||||
"BETTER_AUTH_SECRET",
|
||||
"DB_PASSWORD",
|
||||
"DOMAIN",
|
||||
"HDD_PATH",
|
||||
"IMPORT_PATH",
|
||||
"SUBDOMAIN",
|
||||
"TZ",
|
||||
"USERDATA_PATH"
|
||||
],
|
||||
"exercise": [],
|
||||
"ports": []
|
||||
},
|
||||
"reasons": [
|
||||
"sparkyfitness-server: this container's mounts are all empty while it created entries in ['/app/SparkyFitnessServer'] \u2014 benign when a sibling container holds the state, worth a look when none does",
|
||||
"sparkyfitness-server: declared volume /app/SparkyFitnessServer/uploads is EMPTY",
|
||||
"sparkyfitness-server: declared volume /app/SparkyFitnessServer/backup is EMPTY"
|
||||
],
|
||||
"verdict": "CLEAN"
|
||||
}
|
||||
@@ -0,0 +1,53 @@
|
||||
{
|
||||
"probe": {
|
||||
"app": "uptime-kuma",
|
||||
"containers": [
|
||||
{
|
||||
"diff_added": 0,
|
||||
"diff_benign_db_touches": [],
|
||||
"diff_data_dirs": [],
|
||||
"diff_other_dirs": [],
|
||||
"diff_token_dirs": [],
|
||||
"diff_total": 0,
|
||||
"diff_unresolved": [],
|
||||
"exit": 0,
|
||||
"gid": 0,
|
||||
"health": "healthy",
|
||||
"image": "louislam/uptime-kuma:2.4.0",
|
||||
"mounts": [
|
||||
{
|
||||
"class": "named-declared",
|
||||
"files": 0,
|
||||
"name": "volgate-uptimekuma_uptime_kuma_data",
|
||||
"sample": [],
|
||||
"source": "/var/lib/docker/volumes/volgate-uptimekuma_uptime_kuma_data/_data",
|
||||
"target": "/app/data",
|
||||
"writable_by_app": "yes"
|
||||
}
|
||||
],
|
||||
"name": "uptime-kuma",
|
||||
"restarts": null,
|
||||
"status": "running",
|
||||
"uid": 0
|
||||
}
|
||||
],
|
||||
"declared_volumes": [
|
||||
"uptime_kuma_data"
|
||||
],
|
||||
"env_keys": [
|
||||
"DOMAIN",
|
||||
"HDD_PATH",
|
||||
"IMPORT_PATH",
|
||||
"SUBDOMAIN",
|
||||
"TZ",
|
||||
"USERDATA_PATH"
|
||||
],
|
||||
"exercise": [],
|
||||
"ports": []
|
||||
},
|
||||
"reasons": [
|
||||
"nothing was written to any mount and nothing data-classified in any writable layer \u2014 the app produced no data to locate. Health is not data: this is UNDETERMINED, not CLEAN",
|
||||
"uptime-kuma: declared volume /app/data is EMPTY"
|
||||
],
|
||||
"verdict": "UNDETERMINED"
|
||||
}
|
||||
@@ -0,0 +1,193 @@
|
||||
{
|
||||
"probe": {
|
||||
"app": "wanderer",
|
||||
"containers": [
|
||||
{
|
||||
"diff_added": 6,
|
||||
"diff_benign_db_touches": [],
|
||||
"diff_data_dirs": [],
|
||||
"diff_other_dirs": [
|
||||
{
|
||||
"added": [],
|
||||
"changed_count": 1,
|
||||
"db_signature": false,
|
||||
"dir": "/",
|
||||
"files": [
|
||||
"root"
|
||||
]
|
||||
},
|
||||
{
|
||||
"added": [
|
||||
".npm"
|
||||
],
|
||||
"changed_count": 0,
|
||||
"db_signature": false,
|
||||
"dir": "/root",
|
||||
"files": [
|
||||
".npm"
|
||||
]
|
||||
}
|
||||
],
|
||||
"diff_token_dirs": [],
|
||||
"diff_total": 8,
|
||||
"diff_unresolved": [],
|
||||
"exit": 0,
|
||||
"gid": 0,
|
||||
"health": "unhealthy",
|
||||
"image": "flomp/wanderer-web:v0.20.0",
|
||||
"mounts": [
|
||||
{
|
||||
"class": "named-declared",
|
||||
"files": 0,
|
||||
"name": "volgate-wanderer_wanderer_uploads",
|
||||
"sample": [],
|
||||
"source": "/var/lib/docker/volumes/volgate-wanderer_wanderer_uploads/_data",
|
||||
"target": "/app/uploads",
|
||||
"writable_by_app": "yes"
|
||||
}
|
||||
],
|
||||
"name": "wanderer",
|
||||
"restarts": null,
|
||||
"status": "running",
|
||||
"uid": 0
|
||||
},
|
||||
{
|
||||
"diff_added": 3,
|
||||
"diff_benign_db_touches": [],
|
||||
"diff_data_dirs": [],
|
||||
"diff_other_dirs": [
|
||||
{
|
||||
"added": [
|
||||
"data",
|
||||
"pb_data"
|
||||
],
|
||||
"changed_count": 0,
|
||||
"db_signature": false,
|
||||
"dir": "/",
|
||||
"files": [
|
||||
"data",
|
||||
"pb_data"
|
||||
]
|
||||
}
|
||||
],
|
||||
"diff_token_dirs": [
|
||||
{
|
||||
"added": [
|
||||
"plugins"
|
||||
],
|
||||
"changed_count": 0,
|
||||
"db_signature": false,
|
||||
"dir": "/data",
|
||||
"files": [
|
||||
"plugins"
|
||||
]
|
||||
}
|
||||
],
|
||||
"diff_total": 3,
|
||||
"diff_unresolved": [],
|
||||
"exit": 0,
|
||||
"gid": 0,
|
||||
"health": "healthy",
|
||||
"image": "flomp/wanderer-db:v0.20.0",
|
||||
"mounts": [
|
||||
{
|
||||
"class": "named-declared",
|
||||
"files": 6,
|
||||
"name": "volgate-wanderer_wanderer_data",
|
||||
"sample": [
|
||||
"auxiliary.db-wal",
|
||||
"data.db",
|
||||
"data.db-wal",
|
||||
"auxiliary.db",
|
||||
"auxiliary.db-shm",
|
||||
"data.db-shm"
|
||||
],
|
||||
"source": "/var/lib/docker/volumes/volgate-wanderer_wanderer_data/_data",
|
||||
"target": "/pb_data",
|
||||
"writable_by_app": "yes"
|
||||
},
|
||||
{
|
||||
"class": "named-declared",
|
||||
"files": 0,
|
||||
"name": "volgate-wanderer_wanderer_plugins",
|
||||
"sample": [],
|
||||
"source": "/var/lib/docker/volumes/volgate-wanderer_wanderer_plugins/_data",
|
||||
"target": "/data/plugins",
|
||||
"writable_by_app": "yes"
|
||||
}
|
||||
],
|
||||
"name": "wanderer-db",
|
||||
"restarts": null,
|
||||
"status": "running",
|
||||
"uid": 0
|
||||
},
|
||||
{
|
||||
"diff_added": 0,
|
||||
"diff_benign_db_touches": [],
|
||||
"diff_data_dirs": [],
|
||||
"diff_other_dirs": [],
|
||||
"diff_token_dirs": [],
|
||||
"diff_total": 0,
|
||||
"diff_unresolved": [],
|
||||
"exit": 0,
|
||||
"gid": 0,
|
||||
"health": "healthy",
|
||||
"image": "getmeili/meilisearch:v1.36.0",
|
||||
"mounts": [
|
||||
{
|
||||
"class": "named-declared",
|
||||
"files": 11,
|
||||
"name": "volgate-wanderer_wanderer_meili_data",
|
||||
"sample": [
|
||||
"data.ms/VERSION",
|
||||
"data.ms/indexes/7ee7d19b-c036-412d-be25-65ae8f9e4801/data.mdb",
|
||||
"data.ms/indexes/7ee7d19b-c036-412d-be25-65ae8f9e4801/lock.mdb",
|
||||
"data.ms/indexes/82fdecc6-7425-4e45-989b-bdb644ecd606/data.mdb",
|
||||
"data.ms/indexes/82fdecc6-7425-4e45-989b-bdb644ecd606/lock.mdb",
|
||||
"data.ms/indexes/c48904e5-2b2a-41c0-a34c-c9575e31be45/data.mdb",
|
||||
"data.ms/indexes/c48904e5-2b2a-41c0-a34c-c9575e31be45/lock.mdb",
|
||||
"data.ms/auth/data.mdb",
|
||||
"data.ms/auth/lock.mdb",
|
||||
"data.ms/tasks/data.mdb",
|
||||
"data.ms/tasks/lock.mdb"
|
||||
],
|
||||
"source": "/var/lib/docker/volumes/volgate-wanderer_wanderer_meili_data/_data",
|
||||
"target": "/meili_data",
|
||||
"writable_by_app": "yes"
|
||||
}
|
||||
],
|
||||
"name": "wanderer-search",
|
||||
"restarts": null,
|
||||
"status": "running",
|
||||
"uid": 0
|
||||
}
|
||||
],
|
||||
"declared_volumes": [
|
||||
"wanderer_data",
|
||||
"wanderer_meili_data",
|
||||
"wanderer_plugins",
|
||||
"wanderer_uploads"
|
||||
],
|
||||
"env_keys": [
|
||||
"DOMAIN",
|
||||
"HDD_PATH",
|
||||
"IMPORT_PATH",
|
||||
"MEILI_MASTER_KEY",
|
||||
"POCKETBASE_ENCRYPTION_KEY",
|
||||
"SUBDOMAIN",
|
||||
"SUBDOMAIN_DB",
|
||||
"TZ",
|
||||
"USERDATA_PATH"
|
||||
],
|
||||
"exercise": [],
|
||||
"ports": []
|
||||
},
|
||||
"reasons": [
|
||||
"wanderer: unhealthy",
|
||||
"wanderer: this container's mounts are all empty while it created entries in ['/root'] \u2014 benign when a sibling container holds the state, worth a look when none does",
|
||||
"wanderer: declared volume /app/uploads is EMPTY",
|
||||
"wanderer-db: declared volume /data/plugins is EMPTY",
|
||||
"wanderer-db: writable-layer writes at /data (path suggests state, no database signature \u2014 judgement needed): ['plugins']"
|
||||
],
|
||||
"verdict": "UNDETERMINED"
|
||||
}
|
||||
@@ -0,0 +1,162 @@
|
||||
{
|
||||
"probe": {
|
||||
"app": "wishlist",
|
||||
"containers": [
|
||||
{
|
||||
"diff_added": 35,
|
||||
"diff_benign_db_touches": [],
|
||||
"diff_data_dirs": [],
|
||||
"diff_other_dirs": [
|
||||
{
|
||||
"added": [
|
||||
"data"
|
||||
],
|
||||
"changed_count": 2,
|
||||
"db_signature": false,
|
||||
"dir": "/",
|
||||
"files": [
|
||||
"data"
|
||||
]
|
||||
},
|
||||
{
|
||||
"added": [
|
||||
".config",
|
||||
".local"
|
||||
],
|
||||
"changed_count": 0,
|
||||
"db_signature": false,
|
||||
"dir": "/root",
|
||||
"files": [
|
||||
".config",
|
||||
".local"
|
||||
]
|
||||
},
|
||||
{
|
||||
"added": [
|
||||
"caddy"
|
||||
],
|
||||
"changed_count": 0,
|
||||
"db_signature": false,
|
||||
"dir": "/root/.config",
|
||||
"files": [
|
||||
"caddy"
|
||||
]
|
||||
},
|
||||
{
|
||||
"added": [
|
||||
"autosave.json"
|
||||
],
|
||||
"changed_count": 0,
|
||||
"db_signature": false,
|
||||
"dir": "/root/.config/caddy",
|
||||
"files": [
|
||||
"autosave.json"
|
||||
]
|
||||
},
|
||||
{
|
||||
"added": [
|
||||
"share"
|
||||
],
|
||||
"changed_count": 0,
|
||||
"db_signature": false,
|
||||
"dir": "/root/.local",
|
||||
"files": [
|
||||
"share"
|
||||
]
|
||||
},
|
||||
{
|
||||
"added": [],
|
||||
"changed_count": 1,
|
||||
"db_signature": false,
|
||||
"dir": "/usr",
|
||||
"files": [
|
||||
"src"
|
||||
]
|
||||
},
|
||||
{
|
||||
"added": [],
|
||||
"changed_count": 1,
|
||||
"db_signature": false,
|
||||
"dir": "/usr/src",
|
||||
"files": [
|
||||
"app"
|
||||
]
|
||||
},
|
||||
{
|
||||
"added": [
|
||||
"data",
|
||||
"uploads"
|
||||
],
|
||||
"changed_count": 0,
|
||||
"db_signature": false,
|
||||
"dir": "/usr/src/app",
|
||||
"files": [
|
||||
"data",
|
||||
"uploads"
|
||||
]
|
||||
}
|
||||
],
|
||||
"diff_token_dirs": [],
|
||||
"diff_total": 45,
|
||||
"diff_unresolved": [],
|
||||
"exit": 0,
|
||||
"gid": 0,
|
||||
"health": "healthy",
|
||||
"image": "ghcr.io/cmintey/wishlist:v0.66.0",
|
||||
"mounts": [
|
||||
{
|
||||
"class": "named-declared",
|
||||
"files": 0,
|
||||
"name": "volgate-wishlist_wishlist_data",
|
||||
"sample": [],
|
||||
"source": "/var/lib/docker/volumes/volgate-wishlist_wishlist_data/_data",
|
||||
"target": "/data",
|
||||
"writable_by_app": "yes"
|
||||
},
|
||||
{
|
||||
"class": "anonymous",
|
||||
"files": 1,
|
||||
"name": "c5b4a3bf4591884b54c90465299d2c299b60a9692efc85699222663c7cc8efcb",
|
||||
"sample": [
|
||||
"prod.db"
|
||||
],
|
||||
"source": "/var/lib/docker/volumes/c5b4a3bf4591884b54c90465299d2c299b60a9692efc85699222663c7cc8efcb/_data",
|
||||
"target": "/usr/src/app/data",
|
||||
"writable_by_app": "yes"
|
||||
},
|
||||
{
|
||||
"class": "anonymous",
|
||||
"files": 0,
|
||||
"name": "0a719c708e8a1351e6e7661358e5f7376e1f81ad0bc6d883660aa79d29999fd4",
|
||||
"sample": [],
|
||||
"source": "/var/lib/docker/volumes/0a719c708e8a1351e6e7661358e5f7376e1f81ad0bc6d883660aa79d29999fd4/_data",
|
||||
"target": "/usr/src/app/uploads",
|
||||
"writable_by_app": "yes"
|
||||
}
|
||||
],
|
||||
"name": "wishlist",
|
||||
"restarts": null,
|
||||
"status": "running",
|
||||
"uid": 0
|
||||
}
|
||||
],
|
||||
"declared_volumes": [
|
||||
"wishlist_data"
|
||||
],
|
||||
"env_keys": [
|
||||
"DOMAIN",
|
||||
"HDD_PATH",
|
||||
"IMPORT_PATH",
|
||||
"SUBDOMAIN",
|
||||
"TZ",
|
||||
"USERDATA_PATH"
|
||||
],
|
||||
"exercise": [],
|
||||
"ports": []
|
||||
},
|
||||
"reasons": [
|
||||
"wishlist: 1 file(s) in an ANONYMOUS volume at /usr/src/app/data \u2014 not in the compose `volumes:` block, so it is never backed up and a redeploy orphans it",
|
||||
"wishlist: declared volume /data is EMPTY"
|
||||
],
|
||||
"verdict": "BROKEN"
|
||||
}
|
||||
@@ -0,0 +1,169 @@
|
||||
{
|
||||
"probe": {
|
||||
"app": "zipline",
|
||||
"containers": [
|
||||
{
|
||||
"diff_added": 3,
|
||||
"diff_benign_db_touches": [],
|
||||
"diff_data_dirs": [],
|
||||
"diff_other_dirs": [
|
||||
{
|
||||
"added": [],
|
||||
"changed_count": 1,
|
||||
"db_signature": false,
|
||||
"dir": "/",
|
||||
"files": [
|
||||
"zipline"
|
||||
]
|
||||
},
|
||||
{
|
||||
"added": [
|
||||
"uploads",
|
||||
"public"
|
||||
],
|
||||
"changed_count": 0,
|
||||
"db_signature": false,
|
||||
"dir": "/zipline",
|
||||
"files": [
|
||||
"uploads",
|
||||
"public"
|
||||
]
|
||||
}
|
||||
],
|
||||
"diff_token_dirs": [],
|
||||
"diff_total": 5,
|
||||
"diff_unresolved": [],
|
||||
"exit": 0,
|
||||
"gid": 0,
|
||||
"health": "healthy",
|
||||
"image": "ghcr.io/diced/zipline:4.6.1",
|
||||
"mounts": [
|
||||
{
|
||||
"class": "named-declared",
|
||||
"files": 0,
|
||||
"name": "volgate-zipline_zipline_public",
|
||||
"sample": [],
|
||||
"source": "/var/lib/docker/volumes/volgate-zipline_zipline_public/_data",
|
||||
"target": "/zipline/public",
|
||||
"writable_by_app": "yes"
|
||||
},
|
||||
{
|
||||
"class": "named-declared",
|
||||
"files": 0,
|
||||
"name": "volgate-zipline_zipline_uploads",
|
||||
"sample": [],
|
||||
"source": "/var/lib/docker/volumes/volgate-zipline_zipline_uploads/_data",
|
||||
"target": "/zipline/uploads",
|
||||
"writable_by_app": "yes"
|
||||
}
|
||||
],
|
||||
"name": "zipline",
|
||||
"restarts": null,
|
||||
"status": "running",
|
||||
"uid": 0
|
||||
},
|
||||
{
|
||||
"diff_added": 2,
|
||||
"diff_benign_db_touches": [],
|
||||
"diff_data_dirs": [],
|
||||
"diff_other_dirs": [
|
||||
{
|
||||
"added": [],
|
||||
"changed_count": 1,
|
||||
"db_signature": false,
|
||||
"dir": "/",
|
||||
"files": [
|
||||
"run"
|
||||
]
|
||||
}
|
||||
],
|
||||
"diff_token_dirs": [],
|
||||
"diff_total": 4,
|
||||
"diff_unresolved": [],
|
||||
"exit": 0,
|
||||
"gid": 70,
|
||||
"health": "healthy",
|
||||
"image": "postgres:16-alpine",
|
||||
"mounts": [
|
||||
{
|
||||
"class": "named-declared",
|
||||
"files": 1356,
|
||||
"name": "volgate-zipline_zipline_postgres_data",
|
||||
"sample": [
|
||||
"postmaster.opts",
|
||||
"postgresql.conf",
|
||||
"postgresql.auto.conf",
|
||||
"pg_ident.conf",
|
||||
"pg_hba.conf",
|
||||
"postmaster.pid",
|
||||
"PG_VERSION",
|
||||
"base/1/826",
|
||||
"base/1/2996",
|
||||
"base/1/3764",
|
||||
"base/1/2610_fsm",
|
||||
"base/1/3603_fsm",
|
||||
"base/1/1249_vm",
|
||||
"base/1/3600_fsm",
|
||||
"base/1/13512",
|
||||
"base/1/3602",
|
||||
"base/1/6228",
|
||||
"base/1/4174",
|
||||
"base/1/4157",
|
||||
"base/1/4146",
|
||||
"base/1/2611",
|
||||
"base/1/2657",
|
||||
"base/1/3079_vm",
|
||||
"base/1/1247_vm",
|
||||
"base/1/1259",
|
||||
"base/1/3601_fsm",
|
||||
"base/1/1417",
|
||||
"base/1/4155",
|
||||
"base/1/2668",
|
||||
"base/1/2831",
|
||||
"base/1/2841",
|
||||
"base/1/3607",
|
||||
"base/1/2836",
|
||||
"base/1/1255_fsm",
|
||||
"base/1/2615_vm",
|
||||
"base/1/3575",
|
||||
"base/1/2605_vm",
|
||||
"base/1/2670",
|
||||
"base/1/6106",
|
||||
"base/1/4163"
|
||||
],
|
||||
"source": "/var/lib/docker/volumes/volgate-zipline_zipline_postgres_data/_data",
|
||||
"target": "/var/lib/postgresql/data",
|
||||
"writable_by_app": "yes"
|
||||
}
|
||||
],
|
||||
"name": "zipline-postgres",
|
||||
"restarts": null,
|
||||
"status": "running",
|
||||
"uid": 70
|
||||
}
|
||||
],
|
||||
"declared_volumes": [
|
||||
"zipline_postgres_data",
|
||||
"zipline_public",
|
||||
"zipline_uploads"
|
||||
],
|
||||
"env_keys": [
|
||||
"CORE_SECRET",
|
||||
"DB_PASSWORD",
|
||||
"DOMAIN",
|
||||
"HDD_PATH",
|
||||
"IMPORT_PATH",
|
||||
"SUBDOMAIN",
|
||||
"TZ",
|
||||
"USERDATA_PATH"
|
||||
],
|
||||
"exercise": [],
|
||||
"ports": []
|
||||
},
|
||||
"reasons": [
|
||||
"zipline: this container's mounts are all empty while it created entries in ['/zipline'] \u2014 benign when a sibling container holds the state, worth a look when none does",
|
||||
"zipline: declared volume /zipline/public is EMPTY",
|
||||
"zipline: declared volume /zipline/uploads is EMPTY"
|
||||
],
|
||||
"verdict": "CLEAN"
|
||||
}
|
||||
@@ -0,0 +1,48 @@
|
||||
self-testing the prober (both directions)…
|
||||
prober flags the R-156 signature and clears a correct template — trustworthy
|
||||
bentopdf UNDETERMINED nothing was written to any mount and nothing data-classified in any writable layer — the app produced no data to locate. Health is not data: this is U
|
||||
claper CLEAN claper: this container's mounts are all empty while it created entries in ['/app/priv', '/app/priv/static'] — benign when a sibling container holds th
|
||||
docmost CLEAN docmost: this container's mounts are all empty while it created entries in ['/app/apps/client/dist'] — benign when a sibling container holds the state
|
||||
glance UNDETERMINED glance: not running (status=restarting exit=1 restarts=None)
|
||||
gramps-web BROKEN gramps-web: DATA in the writable layer at /app/users (db_signature=True, e.g. ['users.sqlite']); NOTHING this app wrote landed in ANY folder the templ
|
||||
immich CLEAN immich-machine-learning: this container's mounts are all empty while it created entries in ['/', '/root', '/root/.config'] — benign when a sibling con
|
||||
papra BROKEN papra: mount /app/data is NOT writable by the app's own uid=999 gid=999; papra: DATA in the writable layer at /app/app-data/db (db_signature=True, e.g
|
||||
plant-it UNDETERMINED no containers created (compose up rc=1: msdeluise/plant-it:0.10.0 Pulling
|
||||
Image msdeluise/plant-it:0.10.0 Error pull access denied for msdeluise/pla
|
||||
privatebin UNDETERMINED nothing was written to any mount and nothing data-classified in any writable layer — the app produced no data to locate. Health is not data: this is U
|
||||
recipe-importer UNDETERMINED NOTHING this app wrote landed in ANY folder the template preserves: all 1 mount(s) across 1 container(s) are empty, yet entries were created in ['/'].
|
||||
sparkyfitness CLEAN sparkyfitness-server: this container's mounts are all empty while it created entries in ['/app/SparkyFitnessServer'] — benign when a sibling container
|
||||
uptime-kuma UNDETERMINED nothing was written to any mount and nothing data-classified in any writable layer — the app produced no data to locate. Health is not data: this is U
|
||||
wanderer UNDETERMINED wanderer: unhealthy; wanderer: this container's mounts are all empty while it created entries in ['/root'] — benign when a sibling container holds the
|
||||
wishlist BROKEN wishlist: 1 file(s) in an ANONYMOUS volume at /usr/src/app/data — not in the compose `volumes:` block, so it is never backed up and a redeploy orphans
|
||||
zipline CLEAN zipline: this container's mounts are all empty while it created entries in ['/zipline'] — benign when a sibling container holds the state, worth a loo
|
||||
|
||||
BROKEN — the app's data does NOT land where the template preserves it:
|
||||
gramps-web
|
||||
gramps-web: DATA in the writable layer at /app/users (db_signature=True, e.g. ['users.sqlite'])
|
||||
NOTHING this app wrote landed in ANY folder the template preserves: all 2 mount(s) across 1 container(s) are empty, yet entries were created in ['/app', '/app/cache/request_cache', '/app/thumbnail_cache', '/root']. Needs adjudication.
|
||||
gramps-web: this container's mounts are all empty while it created entries in ['/app', '/app/cache/request_cache', '/app/thumbnail_cache'] — benign when a sibling container holds the state, worth a look when none does
|
||||
gramps-web: declared volume /app/data is EMPTY
|
||||
gramps-web: declared volume /app/media is EMPTY
|
||||
papra
|
||||
papra: mount /app/data is NOT writable by the app's own uid=999 gid=999
|
||||
papra: DATA in the writable layer at /app/app-data/db (db_signature=True, e.g. ['db.sqlite'])
|
||||
NOTHING this app wrote landed in ANY folder the template preserves: all 1 mount(s) across 1 container(s) are empty, yet entries were created in ['/app']. Needs adjudication.
|
||||
papra: this container's mounts are all empty while it created entries in ['/app'] — benign when a sibling container holds the state, worth a look when none does
|
||||
papra: declared volume /app/data is EMPTY
|
||||
wishlist
|
||||
wishlist: 1 file(s) in an ANONYMOUS volume at /usr/src/app/data — not in the compose `volumes:` block, so it is never backed up and a redeploy orphans it
|
||||
wishlist: declared volume /data is EMPTY
|
||||
|
||||
UNDETERMINED — NOT a pass. The question was not answered for these:
|
||||
bentopdf: nothing was written to any mount and nothing data-classified in any writable layer — the app produced no data to locate. Health is not data: this is UNDETERMINED, not CLEAN
|
||||
glance: glance: not running (status=restarting exit=1 restarts=None)
|
||||
plant-it: no containers created (compose up rc=1: msdeluise/plant-it:0.10.0 Pulling
|
||||
Image msdeluise/plant-it:0.10.0 Error pull access denied for msdeluise/plant-it, repository does not exist or may require 'd
|
||||
privatebin: nothing was written to any mount and nothing data-classified in any writable layer — the app produced no data to locate. Health is not data: this is UNDETERMINED, not CLEAN; privatebin: declared volum
|
||||
recipe-importer: NOTHING this app wrote landed in ANY folder the template preserves: all 1 mount(s) across 1 container(s) are empty, yet entries were created in ['/']. Needs adjudication.; recipe-importer: this contai
|
||||
uptime-kuma: nothing was written to any mount and nothing data-classified in any writable layer — the app produced no data to locate. Health is not data: this is UNDETERMINED, not CLEAN; uptime-kuma: declared volu
|
||||
wanderer: wanderer: unhealthy; wanderer: this container's mounts are all empty while it created entries in ['/root'] — benign when a sibling container holds the state, worth a look when none does; wanderer: dec
|
||||
|
||||
5 clean · 3 broken · 7 undetermined (of 15 in scope)
|
||||
volume-persistence gate REFUSED
|
||||
@@ -0,0 +1,125 @@
|
||||
#!/usr/bin/env python3
|
||||
"""wishlist survives-a-redeploy proof, on REAL user data.
|
||||
|
||||
`survive2.py` reported DATA-LOST because `prod.db`'s bytes changed across the redeploy — but that
|
||||
rule cannot tell "the app modified its own database on boot" (fine; vaultwarden's WAL does the
|
||||
same) from "the app deleted and recreated it" (data gone). Two things settle it:
|
||||
|
||||
1. the file's INODE — same object, or a new one?
|
||||
2. a row the USER created — still queryable after the redeploy, or not?
|
||||
|
||||
(2) is the claim that actually matters, so it is the verdict. It is created through wishlist's own
|
||||
HTTP API, never by writing into the volume by hand.
|
||||
"""
|
||||
import hashlib, importlib.util, json, os, re, secrets, shutil, tempfile, time
|
||||
from pathlib import Path
|
||||
|
||||
_spec = importlib.util.spec_from_file_location(
|
||||
"cvp", "/opt/sweep/scripts/check-volume-persistence.py")
|
||||
cvp = importlib.util.module_from_spec(_spec); _spec.loader.exec_module(cvp)
|
||||
|
||||
TPL = Path("/opt/sweep/templates/wishlist")
|
||||
EV = Path("/opt/sweep/evidence/wishlist")
|
||||
|
||||
|
||||
def curl(url, *a):
|
||||
return cvp._sh(["curl", "-sS", "--max-time", "25", *a, url], timeout=45)
|
||||
|
||||
|
||||
def stat_db(mounts):
|
||||
"""(inode, sha256, size) of prod.db, read host-side from the volume itself."""
|
||||
for m in mounts:
|
||||
if m["target"] == "/usr/src/app/data" and m["source"]:
|
||||
f = os.path.join(m["source"], "prod.db")
|
||||
if os.path.isfile(f):
|
||||
st = os.lstat(f)
|
||||
return {"inode": st.st_ino, "size": st.st_size,
|
||||
"sha256": hashlib.sha256(open(f, "rb").read()).hexdigest()}
|
||||
return None
|
||||
|
||||
|
||||
def sqlite_rows(mounts, table):
|
||||
"""Count rows in `table` by reading the volume's DB with a throwaway sqlite container —
|
||||
a READ of what the app wrote, never a write into the volume."""
|
||||
for m in mounts:
|
||||
if m["target"] == "/usr/src/app/data" and m["source"]:
|
||||
r = cvp._sh(["docker", "run", "--rm", "-v", f"{m['name']}:/v:ro",
|
||||
"keinos/sqlite3:latest", "sqlite3", "/v/prod.db",
|
||||
f"SELECT COUNT(*) FROM {table};"], timeout=180)
|
||||
return (r.stdout or r.stderr or "").strip()
|
||||
return "no-db"
|
||||
|
||||
|
||||
def up(base, wait=300, settle=40):
|
||||
cvp._sh(base + ["up", "-d"], timeout=1200)
|
||||
cids = [c for c in cvp._sh(base + ["ps", "-aq"], timeout=120).stdout.split() if c]
|
||||
dl = time.time() + wait
|
||||
while time.time() < dl:
|
||||
if not any(((cvp._inspect(c) or {}).get("State") or {}).get("Status") in ("created", "restarting")
|
||||
or ((((cvp._inspect(c) or {}).get("State") or {}).get("Health") or {}).get("Status")
|
||||
== "starting") for c in cids):
|
||||
break
|
||||
time.sleep(10)
|
||||
ip = None
|
||||
for c in cids:
|
||||
for n in ((cvp._inspect(c) or {}).get("NetworkSettings") or {}).get("Networks", {}).values():
|
||||
if n.get("IPAddress"):
|
||||
ip = n["IPAddress"]
|
||||
mounts = []
|
||||
for c in cids:
|
||||
info = cvp._inspect(c) or {}
|
||||
for m in (info.get("Mounts") or []):
|
||||
mounts.append({"target": m.get("Destination"), "source": m.get("Source"),
|
||||
"name": m.get("Name")})
|
||||
time.sleep(settle)
|
||||
return cids, ip, mounts
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
EV.mkdir(parents=True, exist_ok=True)
|
||||
work = Path(tempfile.mkdtemp(prefix="wl-"))
|
||||
shutil.copy(TPL / "docker-compose.yml", work / "docker-compose.yml")
|
||||
env = cvp.build_env("wishlist", (TPL / ".felhom.yml").read_text(),
|
||||
(TPL / "docker-compose.yml").read_text())
|
||||
(work / ".env").write_text("".join(f"{k}={v}\n" for k, v in sorted(env.items())))
|
||||
base = ["docker", "compose", "-p", "wlproof", "--project-directory", str(work),
|
||||
"-f", str(work / "docker-compose.yml")]
|
||||
cvp._sh(["docker", "network", "create", "traefik-public"], timeout=60)
|
||||
log = []
|
||||
try:
|
||||
cids, ip, mounts = up(base)
|
||||
# --- seed REAL user data through wishlist's own API
|
||||
email = f"proof-{secrets.token_hex(4)}@felhom.invalid"
|
||||
pw = "Proof-" + secrets.token_hex(10) # never recorded
|
||||
r = curl(f"http://{ip}:3000/api/auth/signup", "-X", "POST", "-i",
|
||||
"-H", "Content-Type: application/json",
|
||||
"-d", json.dumps({"email": email, "password": pw, "name": "Proof",
|
||||
"username": "proofuser"}))
|
||||
log.append("signup -> " + ((r.stdout or "?").splitlines() or ["?"])[0].strip())
|
||||
time.sleep(20)
|
||||
|
||||
before_db = stat_db(mounts)
|
||||
before_users = sqlite_rows(mounts, "user")
|
||||
log.append(f"before: db={before_db} users={before_users}")
|
||||
|
||||
cvp._sh(base + ["down"], timeout=900) # a REDEPLOY — no -v
|
||||
cids, ip, mounts = up(base)
|
||||
after_db = stat_db(mounts)
|
||||
after_users = sqlite_rows(mounts, "user")
|
||||
log.append(f"after : db={after_db} users={after_users}")
|
||||
|
||||
same_inode = bool(before_db and after_db and before_db["inode"] == after_db["inode"])
|
||||
rows_kept = (before_users.isdigit() and after_users.isdigit()
|
||||
and int(after_users) >= int(before_users) and int(before_users) > 0)
|
||||
res = {"app": "wishlist", "before": before_db, "after": after_db,
|
||||
"user_rows_before": before_users, "user_rows_after": after_users,
|
||||
"same_inode": same_inode, "user_rows_preserved": rows_kept,
|
||||
"verdict": ("SURVIVES" if same_inode and rows_kept
|
||||
else "SURVIVES (inode only, no seeded rows)" if same_inode
|
||||
else "DATA LOST"),
|
||||
"log": log}
|
||||
(EV / "survive-wishlist-content.json").write_text(json.dumps(res, indent=2, sort_keys=True))
|
||||
print(json.dumps({k: v for k, v in res.items() if k != "log"}, indent=2))
|
||||
finally:
|
||||
cvp._sh(base + ["down", "-v", "--remove-orphans"], timeout=900)
|
||||
shutil.rmtree(work, ignore_errors=True)
|
||||
Reference in New Issue
Block a user