From 8ae917813858a2b8f539a3e889398ed6542f41e8 Mon Sep 17 00:00:00 2001 From: kisfenyo Date: Sun, 18 Oct 2026 22:42:52 +0200 Subject: [PATCH] v0.331.0: a removal deletes the volumes still labelled with the app's project that its definition no longer declares (R-1117) compose down --volumes removes only the current definition's volumes; a volume a template dropped (or an older restore re-created) survived 'remove with data' on 9202 (nextcloud_nextcloud_redis_data). Red-proofed. --- CHANGELOG.md | 8 +- CONTEXT.md | 3 +- REPORT.md | 3 + controller/README.md | 2 +- controller/internal/stacks/delete.go | 11 +++ .../stacks/r1117_leftover_volume_test.go | 78 +++++++++++++++++++ 6 files changed, 102 insertions(+), 3 deletions(-) create mode 100644 controller/internal/stacks/r1117_leftover_volume_test.go diff --git a/CHANGELOG.md b/CHANGELOG.md index 4332ce1..5f34f07 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,4 +1,4 @@ -## v0.331.0 — OnlyOffice opens and SAVES from FileBrowser and Nextcloud; FileBrowser is back at once after a restart (R-1018) (2026-10-18) +## v0.331.0 — OnlyOffice opens and SAVES from FileBrowser and Nextcloud; FileBrowser is back at once after a restart (R-1018); a removal deletes a volume the definition dropped (R-1117) (2026-10-18) **MinAgent: 0.158.1** — the floor's, carried (not above the vouched agent 0.159.2). Nothing here calls a new agent route. No golden (`09` §3 decision 250). Task: the evening brief of 2026-10-18 (felhom.eu `REPORT-evening-2026-10-18.md`). @@ -21,6 +21,12 @@ box's public names; felhom.eu `audits/onlyoffice-2026-10-18/`). FileBrowser, and a re-created FileBrowser starts off traefik-public — R-1126 — until the next base-stack round; measured 4 min 8 s and 3 min 38 s). `RestartStack("filebrowser")` now runs the gate at once, in the background, like the mounts sync. `TestRestartStack_R1018_FileBrowserPublishedAgainAtOnce`. +- **A removal left a volume the definition no longer declares** (R-1117, found while measuring the Redis template + change on 9202): `compose down --volumes` removes only the CURRENT definition's volumes, so a volume a template dropped + — or one a restore re-created from an older unit — kept the app's project label and survived „remove with data" + (`nextcloud_nextcloud_redis_data`, measured). `RemoveStack` now removes, by name, every volume still labelled with the + app's project after the down. `TestR1117_RemoveDeletesALabelledVolumeTheDefinitionNoLongerDeclares` (another + project's volume is never touched); red-proof felhom.eu `audits/redis-volumes-2026-10-18/redproof-leftover-volume.txt`. - Red-proofs: each fix removed → its test fails with the wrong value (felhom.eu `audits/onlyoffice-2026-10-18/redproof-fix{1,2,3}.txt`). - No page changed; every fixture byte-identical. diff --git a/CONTEXT.md b/CONTEXT.md index 06fe580..720ee43 100644 --- a/CONTEXT.md +++ b/CONTEXT.md @@ -2,7 +2,8 @@ > **2026-10-18 (evening) — v0.331.0: OnlyOffice opens and saves from FileBrowser (`server.internalUrl`) and Nextcloud -> (`DocumentServerInternalUrl` with its slash); a FileBrowser restart runs the R-1126 gate at once (R-1018).** MinAgent 0.158.1. +> (`DocumentServerInternalUrl` with its slash); a FileBrowser restart runs the R-1126 gate at once (R-1018); a removal deletes volumes still labelled with the +> app's project that the definition no longer declares (R-1117).** MinAgent 0.158.1. > **2026-10-18 (day) — v0.330.0: „anyway" asks the agent's disk list first and starts again the apps it stopped when the > agent refuses (R-1144).** MinAgent 0.158.1. > **2026-10-18 (night) — v0.329.0: each app reaches only its own `appdata/` (R-1142, compiled-in folder map: diff --git a/REPORT.md b/REPORT.md index cf38ebc..377fb00 100644 --- a/REPORT.md +++ b/REPORT.md @@ -18,4 +18,7 @@ Chrome through the box's public names mapped to 9202): 2. Nextcloud: `DocumentServerInternalUrl = http://onlyoffice:80/` — `TestNextcloudApply_R1018_InternalAddressesTakeAPath`. 3. `RestartStack("filebrowser")` runs the R-1126 gate — `TestRestartStack_R1018_FileBrowserPublishedAgainAtOnce`. +4. `RemoveStack` removes the volumes still labelled with the app's project after `compose down --volumes` (R-1117: a + volume a template dropped survived „remove with data" on 9202) — `TestR1117_RemoveDeletesALabelledVolumeTheDefinitionNoLongerDeclares`. + Gates: `go build/vet/test ./...` green; `controller_gates.py --fast` green. Fixtures: none changed. diff --git a/controller/README.md b/controller/README.md index 4618c01..5689f4e 100644 --- a/controller/README.md +++ b/controller/README.md @@ -1055,7 +1055,7 @@ The `/apps/{slug}` page renders hero section, screenshots, setup guide, and opti | Stop | `docker compose stop` (blocked for protected stacks) | | Restart | `docker compose restart` | | Update | `docker compose pull` + `docker compose up -d` | -| Remove | `docker compose down --volumes` + remove `app.yaml` + optional HDD/backup cleanup; template preserved for redeploy | +| Remove | `docker compose down --volumes`, then every volume still labelled with the app's compose project (one a template dropped or an older restore re-created — v0.331.0, R-1117) + remove `app.yaml` + optional HDD/backup cleanup; template preserved for redeploy | | Delete | `docker compose down --rmi local --volumes` + optional HDD data cleanup (orphaned stacks only) | **Remove vs Delete**: "Eltávolítás" (Remove) is for deployed catalog stacks — it reverts the stack to "Nincs telepítve" state while keeping the template for easy redeployment. "Törlés" (Delete) is for orphaned stacks — it removes the entire stack directory including templates. Both require stopping the stack first. diff --git a/controller/internal/stacks/delete.go b/controller/internal/stacks/delete.go index 8d6df83..39281e7 100644 --- a/controller/internal/stacks/delete.go +++ b/controller/internal/stacks/delete.go @@ -756,6 +756,17 @@ func (m *Manager) RemoveStack(name string, removeHDDData bool, backupPathsToRemo if n := m.RemoveUndoCopies(name); n > 0 { m.logger.Printf("[INFO] [stacks] RemoveStack %s: removed %d undo copy volume(s)", name, n) } + // R-1117: `down --volumes` removes only what the CURRENT definition declares. A volume a template dropped (or a + // restore re-created from an older unit) still carries the app's project label and outlived the removal (measured + // on 9202, 2026-10-18: nextcloud_nextcloud_redis_data). The removal removes volumes, so those go too — by name, and + // only the ones labelled with THIS project. Pinned by TestR1117_RemoveDeletesALabelledVolumeTheDefinitionNoLongerDeclares. + for _, v := range m.projectVolumes(name) { + if out, err := m.execCommand("docker", "volume", "rm", v); err != nil { + m.logger.Printf("[WARN] [stacks] RemoveStack %s: leftover volume %s not removed: %v (%s)", name, v, err, strings.TrimSpace(out)) + } else { + m.logger.Printf("[INFO] [stacks] RemoveStack %s: removed leftover volume %s (the app's project label; not in its definition — R-1117)", name, v) + } + } // Step 3: the volumes that are gone now — `[]` when none, never null (R-489). resp.VolumesRemoved = removedVolumes(volsBefore, m.appVolumeSet(name, stackDir)) diff --git a/controller/internal/stacks/r1117_leftover_volume_test.go b/controller/internal/stacks/r1117_leftover_volume_test.go new file mode 100644 index 0000000..c2c7638 --- /dev/null +++ b/controller/internal/stacks/r1117_leftover_volume_test.go @@ -0,0 +1,78 @@ +package stacks + +import ( + "sort" + "strings" + "sync" + "testing" +) + +// R-1117: a template that drops a volume leaves the old one on an installed box, still carrying the app's compose project +// label (and a restore re-creates it with that label). `compose down --volumes` removes only the volumes the CURRENT +// definition declares, so before this fix the removal listed it and kept it — measured on scratch 9202, 2026-10-18: +// Nextcloud removed „with data", `nextcloud_nextcloud_redis_data` still there. The consequence pinned: after the +// removal no volume with the app's project label is left, and a volume of another project is never touched. +// RED-PROOF: delete the leftover loop in RemoveStack → "left behind: [app_app_redis_data]". +func TestR1117_RemoveDeletesALabelledVolumeTheDefinitionNoLongerDeclares(t *testing.T) { + drive := t.TempDir() + m, _, _ := newR442Manager(t, "app", ssdCompose, driveAppYAML(drive), drive) + var mu sync.Mutex + // project label per volume: the app's own (declared or not), and another app's. + vols := map[string]string{"app_app_redis_data": "app", "other_app_data": "other"} + var removedByRm []string + m.execFn = func(name string, args ...string) (string, error) { + mu.Lock() + defer mu.Unlock() + a := strings.Join(args, " ") + switch { + case strings.HasPrefix(a, "volume ls --filter label=com.docker.compose.project="): + p := strings.TrimPrefix(strings.Fields(a)[3], "label=com.docker.compose.project=") + var out []string + for v, proj := range vols { + if proj == p { + out = append(out, v) + } + } + sort.Strings(out) + return strings.Join(out, "\n"), nil + case a == "volume ls -q": + var out []string + for v := range vols { + out = append(out, v) + } + sort.Strings(out) + return strings.Join(out, "\n"), nil + case strings.HasPrefix(a, "volume rm "): + v := strings.Fields(a)[len(strings.Fields(a))-1] + delete(vols, v) + removedByRm = append(removedByRm, v) + return v, nil + } + return "", nil + } + resp, err := m.RemoveStack("app", false, nil) + if err != nil { + t.Fatalf("RemoveStack: %v", err) + } + mu.Lock() + defer mu.Unlock() + var left []string + for v, proj := range vols { + if proj == "app" { + left = append(left, v) + } + } + if len(left) > 0 { + t.Fatalf("left behind: %v — a volume with the app's project label outlived its removal", left) + } + if _, ok := vols["other_app_data"]; !ok { + t.Fatalf("another app's volume was removed (rm calls: %v)", removedByRm) + } + found := false + for _, v := range resp.VolumesRemoved { + found = found || v == "app_app_redis_data" + } + if !found { + t.Errorf("volumes_removed = %v, want it to name app_app_redis_data", resp.VolumesRemoved) + } +}