R-569: stack-lifecycle handlers pick their status by error KIND, not English words

stop/start/restart/update, remove and delete matched "protected", "not found", "not deployed",
"still running", "not orphaned" in err.Error(). New sentinels in internal/stacks
(stack_errors.go) carried by the producers in manager.go/delete.go via util.KindErrorf (message
bytes unchanged); api.stackOpStatusFor maps them. Tests: reworded-message table per family,
wiring check over all three handlers, producers keep kind + words.

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:
2026-10-05 21:36:54 +02:00
parent 9271f33359
commit d15ad105be
7 changed files with 203 additions and 47 deletions
+10 -10
View File
@@ -269,12 +269,12 @@ func (m *Manager) DeleteStack(name string, removeHDDData bool) (*DeleteResponse,
// Safety: never delete protected stacks
if m.cfg.IsProtectedStack(name) {
return nil, fmt.Errorf("stack %q is protected and cannot be deleted", name)
return nil, util.KindErrorf(ErrProtectedStack, "stack %q is protected and cannot be deleted", name)
}
stack, ok := m.GetStack(name)
if !ok {
return nil, fmt.Errorf("stack %q not found", name)
return nil, util.KindErrorf(ErrStackNotFound, "stack %q not found", name)
}
if m.isDebug() {
@@ -284,7 +284,7 @@ func (m *Manager) DeleteStack(name string, removeHDDData bool) (*DeleteResponse,
// Must be orphaned
if !stack.Orphaned {
return nil, fmt.Errorf("stack %q is not orphaned — only orphaned stacks can be deleted", name)
return nil, util.KindErrorf(ErrNotOrphaned, "stack %q is not orphaned — only orphaned stacks can be deleted", name)
}
// Must not be deploying (H2 fix)
@@ -296,7 +296,7 @@ func (m *Manager) DeleteStack(name string, removeHDDData bool) (*DeleteResponse,
// StateDegraded (R-51) counts as running here: a degraded stack still has LIVE containers, and
// deleting its directory out from under them would leave orphans behind.
if stack.State == StateRunning || stack.State == StateStarting || stack.State == StateRestarting || stack.State == StateDegraded {
return nil, fmt.Errorf("stack %q is still running — stop it first before deleting", name)
return nil, util.KindErrorf(ErrStillRunning, "stack %q is still running — stop it first before deleting", name)
}
stackDir := filepath.Dir(stack.ComposePath)
@@ -417,7 +417,7 @@ func (m *Manager) DeleteStack(name string, removeHDDData bool) (*DeleteResponse,
func (m *Manager) GetStackHDDData(name string) (*HDDDataResponse, error) {
stack, ok := m.GetStack(name)
if !ok {
return nil, fmt.Errorf("stack %q not found", name)
return nil, util.KindErrorf(ErrStackNotFound, "stack %q not found", name)
}
// R-442: the app's own recorded HDD_PATH, not the global config (which no box sets).
@@ -577,12 +577,12 @@ func (m *Manager) RemoveStack(name string, removeHDDData bool, backupPathsToRemo
// Safety: never remove protected stacks
if m.cfg.IsProtectedStack(name) {
return nil, fmt.Errorf("stack %q is protected and cannot be removed", name)
return nil, util.KindErrorf(ErrProtectedStack, "stack %q is protected and cannot be removed", name)
}
stack, ok := m.GetStack(name)
if !ok {
return nil, fmt.Errorf("stack %q not found", name)
return nil, util.KindErrorf(ErrStackNotFound, "stack %q not found", name)
}
if m.isDebug() {
@@ -601,7 +601,7 @@ func (m *Manager) RemoveStack(name string, removeHDDData bool, backupPathsToRemo
if !stack.Deployed {
half, why := m.halfStateEvidence(name, stack)
if !half {
return nil, fmt.Errorf("stack %q is not deployed", name)
return nil, util.KindErrorf(ErrNotDeployed, "stack %q is not deployed", name)
}
m.logger.Printf("[WARN] [stacks] RemoveStack %s: deployed=false but %s — removing what exists (R-634)", name, why)
}
@@ -632,7 +632,7 @@ func (m *Manager) RemoveStack(name string, removeHDDData bool, backupPathsToRemo
// StateDegraded (R-51) counts as running here: a degraded stack still has LIVE containers, and
// deleting its directory out from under them would leave orphans behind.
if stack.State == StateRunning || stack.State == StateStarting || stack.State == StateRestarting || stack.State == StateDegraded {
return nil, fmt.Errorf("stack %q is still running — stop it first before removing", name)
return nil, util.KindErrorf(ErrStillRunning, "stack %q is still running — stop it first before removing", name)
}
stackDir := filepath.Dir(stack.ComposePath)
@@ -830,7 +830,7 @@ func (m *Manager) RemoveStack(name string, removeHDDData bool, backupPathsToRemo
func (m *Manager) GetStackBackupData(name string, drivePath string, mirrorDirs []string) (*BackupDataResponse, error) {
_, ok := m.GetStack(name)
if !ok {
return nil, fmt.Errorf("stack %q not found", name)
return nil, util.KindErrorf(ErrStackNotFound, "stack %q not found", name)
}
resp := &BackupDataResponse{