8b4b9011dd
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
56 lines
3.5 KiB
Markdown
56 lines
3.5 KiB
Markdown
# 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` — `ensureUserdataMounts` skips when the `HDD_PATH` drive root is an
|
|
**external** path (`!= sysDataPath`) that is **not a live mountpoint**; the app is held by
|
|
`planDriveGates`. New injectable `Manager.isMountPoint` seam (defaults to `system.IsMountPoint`).
|
|
The system/local path is never gated.
|
|
- `internal/web/handlers.go` — the FileBrowser sync loop skips (and doesn't mount) a path under
|
|
`StableParentDir` that isn't a live mountpoint, via the new pure `skipFileBrowserPath` helper.
|
|
- `EnsureUserdataDir`/`EnsureUserdataSkeleton`/`planDriveGates` unchanged (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 → absent `usb` not 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.
|