diff --git a/CHANGELOG.md b/CHANGELOG.md index 7126de6..aac2e0c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,27 @@ ## Changelog +### v0.52.0 — Phase 1 GATE: deploy-side double-nest fix + path-agreement lock (2026-06-13) + +Completes the Model-A double-nest reconciliation deferred in v0.48.0. v0.51.0 fixed the **backup +helper** side (`NamespaceRoot` provenance); the **deploy/compose** side still wrote one segment too +deep. On a Model-A in-guest drive the guest mount `/mnt/` already IS the host's +`/felhom-data` namespace, so the catalog templates' `${HDD_PATH}/felhom-data/appdata/` +double-nested to `.../felhom-data/felhom-data/...` on disk — diverging from where the backup helpers +look (`AppDataDir(NamespaceRoot(HDD_PATH,true))`, single-nested). + +- **Fix lives in the app catalog** (`app-catalog-felhom.eu`): all four HDD app templates + (`romm`, `nextcloud`, `immich`, `paperless-ngx`) changed `${HDD_PATH}/felhom-data/appdata/` → + `${HDD_PATH}/appdata/`. The controller passes `HDD_PATH` through verbatim and never appended + the segment, so no controller runtime change was needed. Catalog change lands via git-sync / + "Sablonok frissítése". +- **Agreement test (new):** `internal/stacks/hddpath_agreement_test.go` resolves a compose's + `${HDD_PATH}` bind mounts via the real deploy-side `ParseComposeHDDMounts` and asserts they are + byte-identical to the backup-side `AppDataDir(NamespaceRoot(HDD_PATH,true))` — no doubled + `felhom-data`, deploy and backup locked together so they cannot drift again. +- **Live migration:** existing drive-resident apps whose data sat at the doubled + `…/felhom-data/felhom-data/appdata/` are migrated (stop → move → verify → redeploy) to the + single-nested path (RomM confirmed on the demo guest). + ### v0.51.0 — offsite-backup UI (felhom-pbs DR) + Model-A double-nest fix (2026-06-12) Pairs with felhom-agent v0.28.0 (whole-guest backup re-targeted to the offsite PBS tier). diff --git a/controller/internal/stacks/hddpath_agreement_test.go b/controller/internal/stacks/hddpath_agreement_test.go new file mode 100644 index 0000000..bd98a09 --- /dev/null +++ b/controller/internal/stacks/hddpath_agreement_test.go @@ -0,0 +1,69 @@ +package stacks_test + +import ( + "os" + "path/filepath" + "strings" + "testing" + + "gitea.dooplex.hu/admin/felhom-controller/internal/appbackup" + "gitea.dooplex.hu/admin/felhom-controller/internal/stacks" +) + +// TestDeployPathAgreesWithBackupHelpers is the Phase-1 GATE invariant: the host path an app actually +// writes to — resolved from its compose ${HDD_PATH} bind mounts, exactly as deploy does — must be +// byte-identical to where the app-data backup helpers look. Single-nested, no doubled felhom-data. +// +// Before the fix, catalog templates used ${HDD_PATH}/felhom-data/appdata/. On a Model-A in-guest +// drive the guest mount /mnt/ ALREADY is the host's /felhom-data namespace, so that extra +// segment produced .../felhom-data/felhom-data/... on disk, while NamespaceRoot(drive, true) resolved +// the single-nested .../appdata/. Deploy and backup disagreed. The catalog templates now use +// ${HDD_PATH}/appdata/; this test locks the two sides together so they can't drift again. +func TestDeployPathAgreesWithBackupHelpers(t *testing.T) { + const hddPath = "/mnt/felhom-usb" // an enrolled in-guest drive mount (basename is NOT felhom-data) + const app = "romm" + + // A compose volumes block in the FIXED form (no felhom-data segment), mirroring the catalog template. + compose := "services:\n" + + " romm:\n" + + " volumes:\n" + + " - ${HDD_PATH}/appdata/romm/library:/romm/library\n" + + " - ${HDD_PATH}/appdata/romm/resources:/romm/resources\n" + + " - romm_config:/romm/config\n" + + "volumes:\n" + + " romm_config:\n" + + dir := t.TempDir() + composePath := filepath.Join(dir, "docker-compose.yml") + if err := os.WriteFile(composePath, []byte(compose), 0600); err != nil { + t.Fatalf("writing temp compose: %v", err) + } + + // Deploy side: where the app's bytes actually land (named volume romm_config is correctly excluded). + mounts := stacks.ParseComposeHDDMounts(composePath, hddPath) + if len(mounts) != 2 { + t.Fatalf("expected 2 HDD bind mounts, got %d: %v", len(mounts), mounts) + } + + // Backup side: the app-data dir the helpers resolve for the same in-guest drive. + ns := appbackup.NamespaceRoot(hddPath, true) // in-guest Model-A drive → mount as-is, no felhom-data + wantAppDir := filepath.ToSlash(appbackup.AppDataDir(ns, app)) + if wantAppDir != "/mnt/felhom-usb/appdata/romm" { + t.Fatalf("backup app-data dir unexpected: %q", wantAppDir) + } + + for _, m := range mounts { + s := filepath.ToSlash(m) + // (1) No doubled felhom-data, and in guest-path space no felhom-data segment at all. + if strings.Contains(s, appbackup.FelhomDataDir+"/"+appbackup.FelhomDataDir) { + t.Errorf("deploy mount double-nests felhom-data: %q", s) + } + if n := strings.Count(s, appbackup.FelhomDataDir); n > 0 { + t.Errorf("deploy mount unexpectedly carries %d felhom-data segment(s): %q", n, s) + } + // (2) Every app bind mount lives under the backup helpers' app-data dir — deploy and backup agree. + if !strings.HasPrefix(s, wantAppDir+"/") { + t.Errorf("deploy mount %q is not under backup app-data dir %q — the two sides diverge", s, wantAppDir) + } + } +}