8ae9178138
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.
79 lines
2.6 KiB
Go
79 lines
2.6 KiB
Go
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)
|
|
}
|
|
}
|