v0.331.0: a removal deletes the volumes still labelled with the app's project that its definition no longer declares (R-1117)
gates / gates (push) Successful in 42s

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.
This commit is contained in:
2026-10-18 22:42:52 +02:00
parent 772ef36f5f
commit 8ae9178138
6 changed files with 102 additions and 3 deletions
+7 -1
View File
@@ -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.
+2 -1
View File
@@ -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/<folder>` (R-1142, compiled-in folder map:
+3
View File
@@ -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.
+1 -1
View File
@@ -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.
+11
View File
@@ -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))
@@ -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)
}
}