R-575: the overcommit warning follows the reader's language
memoryVerdict returns the warning's bundle KEY (KeyMemoryOvercommitWarning) and DeployStack hands it on; the API renders it per request (deployWarningData). msgHU, whose only use this was, is gone. 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:
@@ -0,0 +1,32 @@
|
|||||||
|
package api
|
||||||
|
|
||||||
|
import (
|
||||||
|
"net/http/httptest"
|
||||||
|
"strings"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"gitea.dooplex.hu/admin/felhom-controller/internal/stacks"
|
||||||
|
)
|
||||||
|
|
||||||
|
// R-575: the deploy answer renders the overcommit warning in the reader's language — English for an
|
||||||
|
// English request (it used to be Hungarian on every page), Hungarian by default.
|
||||||
|
func TestR575_OvercommitWarningFollowsTheLanguage(t *testing.T) {
|
||||||
|
r := &Router{}
|
||||||
|
en := r.deployWarningData(httptest.NewRequest("POST", "/api/stacks/x/deploy?lang=en", nil), stacks.KeyMemoryOvercommitWarning)
|
||||||
|
hu := r.deployWarningData(httptest.NewRequest("POST", "/api/stacks/x/deploy?lang=hu", nil), stacks.KeyMemoryOvercommitWarning)
|
||||||
|
if en == nil || hu == nil {
|
||||||
|
t.Fatal("a warning key must produce a warning")
|
||||||
|
}
|
||||||
|
if !strings.Contains(en["warning"], "memory") || strings.Contains(en["warning"], "memóri") {
|
||||||
|
t.Errorf("English page got %q", en["warning"])
|
||||||
|
}
|
||||||
|
if !strings.Contains(hu["warning"], "memóri") {
|
||||||
|
t.Errorf("Hungarian page got %q", hu["warning"])
|
||||||
|
}
|
||||||
|
if strings.Contains(en["warning"], "err.stacks.") || strings.Contains(hu["warning"], "err.stacks.") {
|
||||||
|
t.Error("a raw key reached the page")
|
||||||
|
}
|
||||||
|
if d := r.deployWarningData(httptest.NewRequest("POST", "/", nil), ""); d != nil {
|
||||||
|
t.Errorf("no warning -> no data, got %v", d)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -544,8 +544,8 @@ func (r *Router) deployStack(w http.ResponseWriter, req *http.Request, name stri
|
|||||||
// was true — misleading for API/script consumers. Report that the deploy STARTED, not that it
|
// was true — misleading for API/script consumers. Report that the deploy STARTED, not that it
|
||||||
// finished. 202 Accepted reflects "accepted, processing"; ok:true is preserved for the UI.
|
// finished. 202 Accepted reflects "accepted, processing"; ok:true is preserved for the UI.
|
||||||
resp := apiResponse{OK: true, Message: r.msg(req, "api.deploy.started")}
|
resp := apiResponse{OK: true, Message: r.msg(req, "api.deploy.started")}
|
||||||
if warning != "" {
|
if d := r.deployWarningData(req, warning); d != nil {
|
||||||
resp.Data = map[string]string{"warning": warning}
|
resp.Data = d
|
||||||
}
|
}
|
||||||
writeJSON(w, http.StatusAccepted, resp)
|
writeJSON(w, http.StatusAccepted, resp)
|
||||||
|
|
||||||
@@ -1651,3 +1651,13 @@ func startAnswer(name, action, state string) apiResponse {
|
|||||||
Message: "Stack " + name + " " + action + " requested — state now: " + state,
|
Message: "Stack " + name + " " + action + " requested — state now: " + state,
|
||||||
Data: map[string]interface{}{"state": state}}
|
Data: map[string]interface{}{"state": state}}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// deployWarningData renders the deploy's soft warning for the answer. R-575: DeployStack hands back the
|
||||||
|
// warning's bundle KEY, never a sentence, so it is rendered here in the reader's language. nil = none.
|
||||||
|
// Pinned by TestR575_OvercommitWarningFollowsTheLanguage.
|
||||||
|
func (r *Router) deployWarningData(req *http.Request, warningKey string) map[string]string {
|
||||||
|
if warningKey == "" {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return map[string]string{"warning": r.msg(req, warningKey)}
|
||||||
|
}
|
||||||
|
|||||||
@@ -248,6 +248,9 @@ const (
|
|||||||
// 6. Save app.yaml
|
// 6. Save app.yaml
|
||||||
// 7. Run docker compose up -d with env vars
|
// 7. Run docker compose up -d with env vars
|
||||||
// 8. Update in-memory stack state
|
// 8. Update in-memory stack state
|
||||||
|
//
|
||||||
|
// The string it returns is a bundle KEY for a soft warning ("" = none), never a sentence (R-575): the
|
||||||
|
// API renders it in the request's language.
|
||||||
func (m *Manager) DeployStack(req DeployRequest) (string, error) {
|
func (m *Manager) DeployStack(req DeployRequest) (string, error) {
|
||||||
// Atomically check and set the Deploying flag to prevent concurrent deploys (H1 fix).
|
// Atomically check and set the Deploying flag to prevent concurrent deploys (H1 fix).
|
||||||
m.mu.Lock()
|
m.mu.Lock()
|
||||||
@@ -1472,14 +1475,15 @@ func randomAlphanumeric(length int) (string, error) {
|
|||||||
// update replaces the app's own current request, so counting both would refuse an update that fits.
|
// update replaces the app's own current request, so counting both would refuse an update that fits.
|
||||||
// A deploy releases nothing and passes 0, 0.
|
// A deploy releases nothing and passes 0, 0.
|
||||||
//
|
//
|
||||||
// Returns the refusal (nil = admitted) and the soft overcommit warning.
|
// Returns the refusal (nil = admitted) and the soft overcommit warning as a bundle KEY (R-575): no
|
||||||
|
// language is known here, so the caller that answers a request renders it in that request's language.
|
||||||
//
|
//
|
||||||
// v0.253.0 (R-557): the refusal is an ERROR rather than a sentence, and it carries BOTH its kind
|
// v0.253.0 (R-557): the refusal is an ERROR rather than a sentence, and it carries BOTH its kind
|
||||||
// (ErrNotEnoughMemory, so api.deployStatusFor still answers 409) and its message key (so the
|
// (ErrNotEnoughMemory, so api.deployStatusFor still answers 409) and its message key (so the
|
||||||
// household reads it in its own language). One value where there used to be a sentence plus a
|
// household reads it in its own language). One value where there used to be a sentence plus a
|
||||||
// wrapper at each call site. An unreadable memory reading admits with a WARN, exactly as the deploy
|
// wrapper at each call site. An unreadable memory reading admits with a WARN, exactly as the deploy
|
||||||
// always has.
|
// always has.
|
||||||
func (m *Manager) memoryVerdict(newReqMB, newLimitMB, releasedReqMB, releasedLimitMB int) (refusal error, warning string) {
|
func (m *Manager) memoryVerdict(newReqMB, newLimitMB, releasedReqMB, releasedLimitMB int) (refusal error, warningKey string) {
|
||||||
reservedMB := m.cfg.System.ReservedMemoryMB
|
reservedMB := m.cfg.System.ReservedMemoryMB
|
||||||
totalMB, usedMB, memErr := system.GetMemoryMB()
|
totalMB, usedMB, memErr := system.GetMemoryMB()
|
||||||
// F1: the controller container cannot read the guest's RAM cap from /proc (no lxcfs) or its own
|
// F1: the controller container cannot read the guest's RAM cap from /proc (no lxcfs) or its own
|
||||||
@@ -1522,9 +1526,9 @@ func (m *Manager) memoryVerdict(newReqMB, newLimitMB, releasedReqMB, releasedLim
|
|||||||
_, currentLimitMB := m.CommittedMemory()
|
_, currentLimitMB := m.CommittedMemory()
|
||||||
currentLimitMB -= releasedLimitMB
|
currentLimitMB -= releasedLimitMB
|
||||||
if newLimitMB > 0 && currentLimitMB+newLimitMB > totalMB {
|
if newLimitMB > 0 && currentLimitMB+newLimitMB > totalMB {
|
||||||
warning = msgHU("err.stacks.memory_overcommit_warning")
|
warningKey = KeyMemoryOvercommitWarning
|
||||||
}
|
}
|
||||||
return nil, warning
|
return nil, warningKey
|
||||||
}
|
}
|
||||||
|
|
||||||
// loginAppliedEveryStart is the register of `type: password` fields whose app APPLIES the env value at
|
// loginAppliedEveryStart is the register of `type: password` fields whose app APPLIES the env value at
|
||||||
|
|||||||
@@ -2,8 +2,6 @@ package stacks
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"errors"
|
"errors"
|
||||||
|
|
||||||
"gitea.dooplex.hu/admin/felhom-controller/internal/i18n"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
// R-553 — the deploy path's refusals carry a KIND, so the API can pick its status code without
|
// R-553 — the deploy path's refusals carry a KIND, so the API can pick its status code without
|
||||||
@@ -32,21 +30,8 @@ var (
|
|||||||
ErrStackDeploying = errors.New("stack is being deployed")
|
ErrStackDeploying = errors.New("stack is being deployed")
|
||||||
)
|
)
|
||||||
|
|
||||||
// msgHU renders a bundle message in Hungarian.
|
// KeyMemoryOvercommitWarning is the soft overcommit warning's bundle key. It travels as a KEY from
|
||||||
//
|
// memoryVerdict through DeployStack to the API, which renders it in the request's language (R-575 —
|
||||||
// It exists for the few strings this package returns as TEXT rather than as an error — the soft
|
// it used to be rendered here in Hungarian, so an English page showed a Hungarian line). Pinned by
|
||||||
// overcommit warning is the only one today. Those are display strings with no error to carry a key,
|
// TestR575_OvercommitWarningFollowsTheLanguage.
|
||||||
// so the copy lives in the bundle (a translator sees it, and scripts/i18n_go_parity.py pins it) while
|
const KeyMemoryOvercommitWarning = "err.stacks.memory_overcommit_warning"
|
||||||
// the value handed back is still Hungarian. **The consequence, stated rather than hidden: such a
|
|
||||||
// string renders Hungarian on an English page.** Filed as R-575; the fix is for the caller to carry
|
|
||||||
// the key the way Alert and UpdateRefusal do, not for this helper to guess a language it cannot know.
|
|
||||||
func msgHU(key string, args ...interface{}) string {
|
|
||||||
b, err := i18n.Shared()
|
|
||||||
if err != nil {
|
|
||||||
return key
|
|
||||||
}
|
|
||||||
if len(args) == 0 {
|
|
||||||
return b.Msg(i18n.Default, key)
|
|
||||||
}
|
|
||||||
return b.Msgf(i18n.Default, key, args...)
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -0,0 +1,24 @@
|
|||||||
|
package stacks
|
||||||
|
|
||||||
|
import (
|
||||||
|
"io"
|
||||||
|
"log"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"gitea.dooplex.hu/admin/felhom-controller/internal/config"
|
||||||
|
)
|
||||||
|
|
||||||
|
// R-575: the soft overcommit warning leaves memoryVerdict as a bundle KEY, never as Hungarian text, so
|
||||||
|
// the API can render it in the reader's language. PATH is emptied so the memory reading can never fall
|
||||||
|
// through to `docker info` on the host running the test.
|
||||||
|
func TestR575_OvercommitWarningIsAKey(t *testing.T) {
|
||||||
|
t.Setenv("PATH", "")
|
||||||
|
m := &Manager{cfg: &config.Config{}, logger: log.New(io.Discard, "", 0), stacks: map[string]*Stack{}}
|
||||||
|
refusal, warning := m.memoryVerdict(0, 1<<30, 0, 0) // a limit no machine has: overcommit, no hard block
|
||||||
|
if refusal != nil {
|
||||||
|
t.Fatalf("no request -> no refusal, got %v", refusal)
|
||||||
|
}
|
||||||
|
if warning != KeyMemoryOvercommitWarning {
|
||||||
|
t.Fatalf("warning = %q, want the bundle key %q", warning, KeyMemoryOvercommitWarning)
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user