From 3adfa41a092dd7b338814c5f9facfcf5e09a49b5 Mon Sep 17 00:00:00 2001 From: kisfenyo Date: Mon, 15 Jun 2026 17:10:02 +0200 Subject: [PATCH] controller v0.67.2: gate keys present on BoundUnderParent (reboot convergence) The drive-absent gate treats a stable path usable only when bound under the parent (BoundUnderParent), not merely host-mounted. Makes a host reboot converge: apps stay gated until the agent binds the drive under the parent, then are restarted (recreated) on the populated path. Test updated. Co-Authored-By: Claude Opus 4.8 (1M context) --- CHANGELOG.md | 8 ++++++++ controller/internal/web/intermediary.go | 12 ++++++++---- controller/internal/web/intermediary_test.go | 10 +++++++--- 3 files changed, 23 insertions(+), 7 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 3c5f2a9..789b143 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,13 @@ ## Changelog +### v0.67.2 — gate: key "present" on BoundUnderParent (reboot convergence) (2026-06-15) + +The drive-absent gate now treats a stable path as usable only when the agent reports it BOUND UNDER THE +PARENT (`BoundUnderParent`), not merely host-mounted (`State==attached`). This makes a host reboot +converge correctly: at boot the raw drive mounts early but the agent binds it under the parent slightly +later, so until then the apps' stable-path binds are empty — the gate keeps the apps stopped and +restarts (recreates) them once the bind is live. Legacy raw paths still use the host-mount signal. + ### v0.67.1 — gate: only act on external drives under /mnt/felhom-drives/ (2026-06-15) Fix (caught live on the v0.67.0 deploy): `planDriveGates` marked the internal SSD path diff --git a/controller/internal/web/intermediary.go b/controller/internal/web/intermediary.go index b19bce6..95d5b30 100644 --- a/controller/internal/web/intermediary.go +++ b/controller/internal/web/intermediary.go @@ -90,13 +90,17 @@ func planDriveGates(paths []settings.StoragePath, disks []agentapi.DiskInfo) []g present := map[string]bool{} rawByPath := map[string]string{} for _, d := range disks { - live := d.State == "attached" + // A STABLE path is usable only when the drive's felhom-data is actually BOUND UNDER THE PARENT + // (BoundUnderParent) — NOT merely when the raw drive is host-mounted (State==attached). This is + // what makes the gate converge a reboot correctly: at boot the raw drive mounts early but the + // agent binds it under the parent slightly later; until then the apps' stable-path binds are empty, + // so the gate keeps them stopped and restarts (recreates) them once the bind is live. if d.GuestPath != "" { - present[d.GuestPath] = present[d.GuestPath] || live + present[d.GuestPath] = present[d.GuestPath] || d.BoundUnderParent rawByPath[d.GuestPath] = d.MountPath } - if d.MountPath != "" { // legacy: a raw path registered directly - present[d.MountPath] = present[d.MountPath] || live + if d.MountPath != "" { // legacy raw path registered directly — host mount is the usable signal + present[d.MountPath] = present[d.MountPath] || d.State == "attached" rawByPath[d.MountPath] = d.MountPath } } diff --git a/controller/internal/web/intermediary_test.go b/controller/internal/web/intermediary_test.go index 4f06166..8f126a4 100644 --- a/controller/internal/web/intermediary_test.go +++ b/controller/internal/web/intermediary_test.go @@ -41,9 +41,13 @@ func TestPlanDriveGates(t *testing.T) { {Path: "/mnt/sys_drive/felhom-data"}, // INTERNAL SSD (absent from agent) → never gated } disks := []agentapi.DiskInfo{ - {MountPath: "/mnt/usb", GuestPath: "/mnt/felhom-drives/usb", State: "attached"}, - {MountPath: "/mnt/back", GuestPath: "/mnt/felhom-drives/back", State: "attached"}, - // flash + gone + dead report NO present disk + // present = BoundUnderParent (the usable-in-guest signal), not merely State==attached. + {MountPath: "/mnt/usb", GuestPath: "/mnt/felhom-drives/usb", State: "attached", BoundUnderParent: true}, + {MountPath: "/mnt/back", GuestPath: "/mnt/felhom-drives/back", State: "attached", BoundUnderParent: true}, + // flash reports State=attached but NOT bound under parent yet → still treated ABSENT (the + // reboot-ordering case: raw drive mounted, agent hasn't bound it under the parent yet). + {MountPath: "/mnt/flash", GuestPath: "/mnt/felhom-drives/flash", State: "attached", BoundUnderParent: false}, + // gone + dead report NO present disk } actions := map[string]gateAction{} for _, a := range planDriveGates(paths, disks) {