Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
3.5 KiB
REPORT — controller v0.75.0: gate userdata MkdirAll on a live mountpoint
Baseline → target
felhom-controller main v0.74.0 → v0.75.0. Controller-only; agent/hub untouched.
Problem
Two MkdirAll-into-<drive>/userdata sites fired without checking the drive was mounted (campaign-#2
findings #2/#3): the deploy belt (ensureUserdataMounts) and the FileBrowser sync. During a
drive-absent window they logged mkdir …/userdata: permission denied + caused transient Created
container flapping — and worse, writing into an unmounted mountpoint lands app data on the guest
rootfs, shadowed when the drive returns (data-integrity + rootfs-fill hazard).
Change (commit 0b2605c)
internal/stacks/manager.go—ensureUserdataMountsskips when theHDD_PATHdrive root is an external path (!= sysDataPath) that is not a live mountpoint; the app is held byplanDriveGates. New injectableManager.isMountPointseam (defaults tosystem.IsMountPoint). The system/local path is never gated.internal/web/handlers.go— the FileBrowser sync loop skips (and doesn't mount) a path underStableParentDirthat isn't a live mountpoint, via the new pureskipFileBrowserPathhelper.EnsureUserdataDir/EnsureUserdataSkeleton/planDriveGatesunchanged (gated the callers).
Tests (green; red-proofed)
go build ./... && go vet ./... && go test ./... all green.
- T1
TestEnsureUserdataMounts_SkipsAbsentExternalDrive— belt skips when external drive absent. Red-proof: removing the gate → dir created → FAIL (shown, reverted). - T2
TestEnsureUserdataMounts_EnsuresWhenMounted— belt ensures when drive mounted. - T3
TestEnsureUserdataMounts_SystemPathNeverSkipped— system/local path never gated (must-not-over-gate). - T4
TestSkipFileBrowserPath— only an absent external path is skipped; mounted + system paths kept. Red-proof: gate→false → absentusbnot skipped → FAIL (shown, reverted).
Deploy
Built+pushed gitea.dooplex.hu/admin/felhom-controller:0.75.0 on 192.168.0.180 (digest
sha256:f735676e…; build-server git pull done first — the v0.74.0 stale-source gotcha),
deployed to guest 9201 via the bootstrap path. docker inspect: image=:0.75.0 running healthy.
LIVE acceptance — campaign-#2 C6 re-run, now clean
Disconnected felhom-flash (8 apps) and observed the drive-absent window:
| Check | campaign #2 (pre-fix) | v0.75.0 (fixed) |
|---|---|---|
mkdir …/userdata: permission denied |
many | 0 |
| "could not ensure userdata skeleton" | present | 0 |
| rootfs shadow dir at unmounted mountpoint | created | none — /mnt/felhom-drives/felhom-flash/userdata does not exist (bare mountpoint) |
| apps during absent window | transient Created flapping |
held cleanly (down) |
| reconnect | recovered | recovered: flash remounted /dev/sdc1, 8 apps restarted → 25 running / 0 unhealthy |
The integrity hazard (app data shadowed onto the rootfs) is closed: the now-unmounted mountpoint is
empty, no userdata/... was written to the rootfs.
Not touched / design note (see CONTEXT.md)
The boot-time mkdir … permission denied (daemon boot-restore) is a different cause — docker
auto-starts drive-backed containers (restart: unless-stopped) before the agent mounts the drives, so
docker (not the belt) tries to create the bind source; planDriveGates recovers them after mount
convergence. This task ships only the runtime gate; the boot-ordering decision is documented, not
implemented.