diff --git a/CHANGELOG.md b/CHANGELOG.md index 058f9a9a..974040b3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -40,6 +40,11 @@ night report (`audits/night-2026-10-28/`). and said once; no agent (9202) = Docker's flag only, as before. The inspect now reads `{{.Id}}` too. Tests `TestR528_*` (stacks, cmd/controller), red before the fix (the host-counted kill was not seen) or red-proved by mutation (the back-off, the wiring). +- **Found walking the capability map's B2 on 9202 (small, fixed in the session):** a Stop/Start of a name that is no + app on the box (the bootstrap-managed `felhom-controller`) answered 500 „…az alkalmazás beállításai nem menthetők." — + the desired-state write's „not found" read as a save failure. `stacks.SetDesiredState` returns the + `ErrStackNotFound` kind with the household's sentence (`err.stacks.alkalmazas_nem_talalhato`); `actionStack` answers + 404 before recording any intent. Nothing was ever stopped. Test `TestAnActionOnANameThatIsNoAppIsA404` (red first). - **H8 — `internal/backup/restore_copy_sweep.go`:** an unused restore copy goes after 14 days. Every 6 hours (`restore-copy-sweep`) a copy is removed only when the app has no restore hold of any kind, the app-stop marker is readable and does not name the app, no restore runs, and the copy is older than 14 days. Tests `TestR1256_H8_*` diff --git a/controller/internal/api/router.go b/controller/internal/api/router.go index 72f24d79..2d2c4d7b 100644 --- a/controller/internal/api/router.go +++ b/controller/internal/api/router.go @@ -771,6 +771,12 @@ func (r *Router) actionStack(w http.ResponseWriter, req *http.Request, action, n // above, so nothing is recorded for an action that was never going to happen. if desired, ok := desiredStateForAction(action); ok { if derr := r.stackMgr.SetDesiredState(name, desired); derr != nil { + if errors.Is(derr, stacks.ErrStackNotFound) { + // no app by that name on this box (e.g. the bootstrap-managed controller): a 404, not a save failure + r.logger.Printf("[WARN] [api] %s for %s refused: no such app", action, name) + writeJSON(w, stackOpStatusFor(derr), apiResponse{OK: false, Error: r.errText(req, derr)}) + return + } r.logger.Printf("[ERROR] [api] %s for %s refused: could not record desired state: %v", action, name, derr) writeJSON(w, http.StatusInternalServerError, apiResponse{ OK: false, diff --git a/controller/internal/api/unknown_stack_action_test.go b/controller/internal/api/unknown_stack_action_test.go new file mode 100644 index 00000000..386038d8 --- /dev/null +++ b/controller/internal/api/unknown_stack_action_test.go @@ -0,0 +1,29 @@ +package api + +import ( + "encoding/json" + "net/http" + "net/http/httptest" + "strings" + "testing" +) + +// Found walking the capability map's B2 („App lifecycle") on 9202, 2026-10-28 night: a Stop of a name that is no app +// on this box (`felhom-controller` — bootstrap-managed, not a stack) answered 500 „…az alkalmazás beállításai nem +// menthetők." — the desired-state write's „not found" read as a save failure. It is a 404 now, before any intent is +// recorded. RED-PROOF: drop the ErrStackNotFound branch in actionStack → 500 → FAILS. +func TestAnActionOnANameThatIsNoAppIsA404(t *testing.T) { + r, _, _, _ := newSlice4Router(t) + for _, action := range []string{"stop", "start"} { + w := httptest.NewRecorder() + r.actionStack(w, httptest.NewRequest("POST", "/api/stacks/nosuchapp/"+action, nil), action, "nosuchapp") + var resp apiResponse + _ = json.Unmarshal(w.Body.Bytes(), &resp) + if w.Code != http.StatusNotFound { + t.Fatalf("%s of a name that is no app: %d %q — want 404", action, w.Code, resp.Error) + } + if strings.Contains(strings.ToLower(resp.Error), "mentheto") || strings.Contains(resp.Error, "menthetők") { + t.Fatalf("%s: the answer still blames a save: %q", action, resp.Error) + } + } +} diff --git a/controller/internal/stacks/desiredstate.go b/controller/internal/stacks/desiredstate.go index a65bc318..32caa22c 100644 --- a/controller/internal/stacks/desiredstate.go +++ b/controller/internal/stacks/desiredstate.go @@ -3,6 +3,8 @@ package stacks import ( "fmt" "path/filepath" + + "gitea.dooplex.hu/admin/felhom-controller/internal/util" ) // Desired-state values for AppConfig.DesiredState (R-166, decision D-b). @@ -57,7 +59,8 @@ func (m *Manager) SetDesiredState(name, desired string) error { stack, ok := m.GetStack(name) if !ok { - return fmt.Errorf("stack %q not found", name) + // a KIND (404 at the API) and the household's sentence — not a save failure + return util.MsgErrorf(ErrStackNotFound, "err.stacks.alkalmazas_nem_talalhato", name) } stackDir := filepath.Dir(stack.ComposePath)