controller v0.268.0: the undo finds volumes by definition; a held app names only a whole copy; one press = one tested step (R-658, R-659, R-660, R-651; 09 §6.4 part 5)
gates / gates (push) Successful in 27s
gates / gates (push) Successful in 27s
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0159rPz1ZhFKsS53msqPYxtS
This commit is contained in:
@@ -8,6 +8,7 @@ import (
|
||||
"os"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
"sort"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
@@ -601,7 +602,7 @@ func (m *Manager) RemoveStack(name string, removeHDDData bool, backupPathsToRemo
|
||||
// R-489 (v0.242.0): the volumes are listed BEFORE and AFTER; the difference is what was removed.
|
||||
// Parsing compose's progress output reported `null` over volumes it did remove — measured five
|
||||
// times on demo-hp 2026-09-13 — because compose prints that progress to a TTY it does not have here.
|
||||
volsBefore := m.projectVolumes(name)
|
||||
volsBefore := m.appVolumeSet(name, stackDir)
|
||||
output, err := m.composeExecCustomEnv(stackDir, env, "down", "--volumes")
|
||||
if m.isDebug() {
|
||||
m.logger.Printf("[DEBUG] [stacks] RemoveStack %s: compose down output: %s", name, truncateStr(output, 500))
|
||||
@@ -629,7 +630,7 @@ func (m *Manager) RemoveStack(name string, removeHDDData bool, backupPathsToRemo
|
||||
}
|
||||
|
||||
// Step 3: the volumes that are gone now — `[]` when none, never null (R-489).
|
||||
resp.VolumesRemoved = removedVolumes(volsBefore, m.projectVolumes(name))
|
||||
resp.VolumesRemoved = removedVolumes(volsBefore, m.appVolumeSet(name, stackDir))
|
||||
if len(resp.VolumesRemoved) > 0 {
|
||||
m.logger.Printf("[INFO] [stacks] RemoveStack %s: removed volume(s) %v", name, resp.VolumesRemoved)
|
||||
}
|
||||
@@ -719,6 +720,16 @@ func (m *Manager) RemoveStack(name string, removeHDDData bool, backupPathsToRemo
|
||||
m.logger.Printf("[ERROR] Failed to remove %s: %v", appYAMLPath, err)
|
||||
return resp, fmt.Errorf("failed to remove app.yaml: %w", err)
|
||||
}
|
||||
// R-651 (v0.268.0): the pinned definition and the pinned version's .felhom.yml go with the app.
|
||||
// MEASURED 2026-09-23 night on 9202: every remove left `applied-compose.yml` and `applied-meta/`
|
||||
// behind, so a reinstall of the same name started beside the removed install's record — which the
|
||||
// undo reads as "the version to go back to". The rest of the directory is the catalog mirror and
|
||||
// stays. Pinned by TestR651_RemoveDeletesTheAppliedRecord.
|
||||
for _, p := range []string{AppliedComposePath(stackDir), filepath.Join(stackDir, appliedMetaDir)} {
|
||||
if err := os.RemoveAll(p); err != nil {
|
||||
m.logger.Printf("[WARN] [stacks] RemoveStack %s: could not remove %s: %v", name, p, err)
|
||||
}
|
||||
}
|
||||
|
||||
m.logger.Printf("[INFO] Stack %s removed successfully (took %.1fs)", name, time.Since(start).Seconds())
|
||||
|
||||
@@ -1004,6 +1015,37 @@ func (m *Manager) projectVolumes(project string) []string {
|
||||
return vols
|
||||
}
|
||||
|
||||
// appVolumeSet is every volume the removal accounts for: the ones carrying the project label AND the
|
||||
// ones the app's definition declares that Docker holds by name (R-658, v0.268.0). A restore before
|
||||
// v0.268.0 recreated volumes without the label, and the remove then answered `volumes_removed: []`
|
||||
// over volumes compose's `down --volumes` did remove (measured at the 2026-09-23 night's teardown).
|
||||
// A listing failure reads as nothing, so a removal never fails on bookkeeping.
|
||||
func (m *Manager) appVolumeSet(project, stackDir string) []string {
|
||||
set := map[string]bool{}
|
||||
for _, v := range m.projectVolumes(project) {
|
||||
set[v] = true
|
||||
}
|
||||
if declared, _, err := DeclaredVolumeNames(ComposePathIn(stackDir)); err == nil && len(declared) > 0 {
|
||||
if out, lerr := m.execCommand("docker", "volume", "ls", "-q"); lerr == nil {
|
||||
have := map[string]bool{}
|
||||
for _, l := range strings.Split(out, "\n") {
|
||||
have[strings.TrimSpace(l)] = true
|
||||
}
|
||||
for _, d := range declared {
|
||||
if have[d] {
|
||||
set[d] = true
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
out := make([]string, 0, len(set))
|
||||
for v := range set {
|
||||
out = append(out, v)
|
||||
}
|
||||
sort.Strings(out)
|
||||
return out
|
||||
}
|
||||
|
||||
// removedVolumes is before minus after, as a non-nil slice (the JSON must read `[]`, not `null`).
|
||||
func removedVolumes(before, after []string) []string {
|
||||
still := map[string]bool{}
|
||||
|
||||
Reference in New Issue
Block a user