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:
@@ -623,6 +623,36 @@ func desiredStateForAction(action string) (string, bool) {
|
||||
}
|
||||
}
|
||||
|
||||
// stackOpStatusFor maps a stack-lifecycle refusal (stop/start/restart/update, remove, delete) to its
|
||||
// HTTP status by the refusal's KIND (R-569) — the R-553 rule one handler family over.
|
||||
//
|
||||
// Until R-569 these three handlers matched "protected", "not found", "not deployed", "still running"
|
||||
// and "not orphaned" in err.Error(). Those strings are internal English, so localisation never moved
|
||||
// them; a reworded internal message would have, silently turning a 404/403/409 into a 500. The kinds
|
||||
// come from internal/stacks (stack_errors.go); the messages are unchanged.
|
||||
//
|
||||
// One function serves the three families because their kinds are disjoint: an action never returns
|
||||
// ErrNotOrphaned, a remove never returns an UpdateRefusal. Pinned by
|
||||
// TestR569_StackOpStatus_SurvivesRewording and TestR569_HandlersUseTheKind.
|
||||
func stackOpStatusFor(err error) int {
|
||||
var ref *stacks.UpdateRefusal
|
||||
switch {
|
||||
case errors.Is(err, stacks.ErrStackNotFound):
|
||||
return http.StatusNotFound
|
||||
case errors.As(err, &ref):
|
||||
if ref.Reason == "not_found" {
|
||||
return http.StatusNotFound
|
||||
}
|
||||
return http.StatusConflict
|
||||
case errors.Is(err, stacks.ErrProtectedStack):
|
||||
return http.StatusForbidden
|
||||
case errors.Is(err, stacks.ErrNotDeployed), errors.Is(err, stacks.ErrStillRunning),
|
||||
errors.Is(err, stacks.ErrNotOrphaned):
|
||||
return http.StatusConflict
|
||||
}
|
||||
return http.StatusInternalServerError
|
||||
}
|
||||
|
||||
func (r *Router) actionStack(w http.ResponseWriter, req *http.Request, action, name string) {
|
||||
r.logger.Printf("[INFO] [api] %s requested for stack: %s", action, name)
|
||||
r.dbg("actionStack: action=%s name=%s", action, name)
|
||||
@@ -760,17 +790,7 @@ func (r *Router) actionStack(w http.ResponseWriter, req *http.Request, action, n
|
||||
|
||||
if err != nil {
|
||||
r.logger.Printf("[ERROR] [api] %s failed for %s: %v", action, name, err)
|
||||
status := http.StatusInternalServerError
|
||||
var ref *stacks.UpdateRefusal
|
||||
if errors.As(err, &ref) {
|
||||
status = http.StatusConflict
|
||||
}
|
||||
if strings.Contains(err.Error(), "protected") {
|
||||
status = http.StatusForbidden
|
||||
}
|
||||
if strings.Contains(err.Error(), "not found") {
|
||||
status = http.StatusNotFound
|
||||
}
|
||||
status := stackOpStatusFor(err) // R-569: by kind, never by the error's words
|
||||
writeJSON(w, status, apiResponse{OK: false, Error: r.errText(req, err)})
|
||||
return
|
||||
}
|
||||
@@ -1052,16 +1072,7 @@ func (r *Router) removeStack(w http.ResponseWriter, req *http.Request, name stri
|
||||
writeJSON(w, http.StatusConflict, apiResponse{OK: false, Error: r.errText(req, err)})
|
||||
return
|
||||
}
|
||||
status := http.StatusInternalServerError
|
||||
if strings.Contains(err.Error(), "protected") {
|
||||
status = http.StatusForbidden
|
||||
}
|
||||
if strings.Contains(err.Error(), "not found") {
|
||||
status = http.StatusNotFound
|
||||
}
|
||||
if strings.Contains(err.Error(), "not deployed") || strings.Contains(err.Error(), "still running") {
|
||||
status = http.StatusConflict
|
||||
}
|
||||
status := stackOpStatusFor(err) // R-569: by kind, never by the error's words
|
||||
writeJSON(w, status, apiResponse{OK: false, Error: r.errText(req, err)})
|
||||
return
|
||||
}
|
||||
@@ -1137,16 +1148,7 @@ func (r *Router) deleteStack(w http.ResponseWriter, req *http.Request, name stri
|
||||
writeJSON(w, http.StatusConflict, apiResponse{OK: false, Error: refused.Message})
|
||||
return
|
||||
}
|
||||
status := http.StatusInternalServerError
|
||||
if strings.Contains(err.Error(), "protected") {
|
||||
status = http.StatusForbidden
|
||||
}
|
||||
if strings.Contains(err.Error(), "not found") {
|
||||
status = http.StatusNotFound
|
||||
}
|
||||
if strings.Contains(err.Error(), "not orphaned") || strings.Contains(err.Error(), "still running") {
|
||||
status = http.StatusConflict
|
||||
}
|
||||
status := stackOpStatusFor(err) // R-569: by kind, never by the error's words
|
||||
writeJSON(w, status, apiResponse{OK: false, Error: r.errText(req, err)})
|
||||
return
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user