controller v0.242.0: a removed app is listed with its kept backup; five small ones (R-487 R-491 R-490 R-489 R-476 R-456)
gates / gates (push) Successful in 14s
gates / gates (push) Successful in 14s
R-487: the local backup lists are keyed on the drives, not on what is deployed — a removed app whose unit was kept is listed with the restore that reinstalls it, the picker answers for it, and the restore opens the unit where it sits. R-491: a removal clears the app's update hold. R-490: /api/system/info reaches the API router and reads the default storage path. R-489: volumes_removed is the real before/after difference, [] when none. R-476: a Tier-2 copy is dated by its data, not its manifest. R-456: the boot-orphan rule is pinned. Every fix red-proofed.
This commit is contained in:
@@ -1,9 +1,13 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"gitea.dooplex.hu/admin/felhom-controller/internal/settings"
|
||||
"go/ast"
|
||||
"go/parser"
|
||||
"go/token"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
@@ -60,3 +64,58 @@ func TestR486_RemovalKeepsTheTier2RecordUnlessBackupsGo(t *testing.T) {
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// R-491 — a removal clears the app's UPDATE hold (and only that kind).
|
||||
//
|
||||
// COMPANION RED-PROOF (REPORT.md): remove the ClearUpdateHold call from removeStack — this fails.
|
||||
func TestR491_RemovalClearsTheUpdateHold(t *testing.T) {
|
||||
fset := token.NewFileSet()
|
||||
f, err := parser.ParseFile(fset, "router.go", nil, 0)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
found := false
|
||||
for _, d := range f.Decls {
|
||||
fn, ok := d.(*ast.FuncDecl)
|
||||
if !ok || fn.Name.Name != "removeStack" || fn.Body == nil {
|
||||
continue
|
||||
}
|
||||
ast.Inspect(fn.Body, func(n ast.Node) bool {
|
||||
if s, ok := n.(*ast.SelectorExpr); ok && s.Sel.Name == "ClearUpdateHold" {
|
||||
found = true
|
||||
}
|
||||
return true
|
||||
})
|
||||
}
|
||||
if !found {
|
||||
t.Error("removeStack must clear the app's update hold (R-491), or a reinstall starts held")
|
||||
}
|
||||
}
|
||||
|
||||
// R-490 — with a default storage path registered, system-info reports the drive (the fallback every
|
||||
// other reader already had).
|
||||
func TestR490_SystemInfoFallsBackToTheDefaultStoragePath(t *testing.T) {
|
||||
r, sett, _, _ := newSlice4Router(t)
|
||||
r.sett = sett // the fixture router carries no settings; system-info reads the default path from them
|
||||
if err := sett.AddStoragePath(settings.StoragePath{Path: t.TempDir(), Label: "HDD", Schedulable: true, IsDefault: true}); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
w := httptest.NewRecorder()
|
||||
r.ServeHTTP(w, httptest.NewRequest(http.MethodGet, "/api/system/info", nil))
|
||||
if w.Code != http.StatusOK {
|
||||
t.Fatalf("status %d: %s", w.Code, w.Body.String())
|
||||
}
|
||||
var resp struct {
|
||||
Data struct {
|
||||
System struct {
|
||||
HDDConfigured bool `json:"hdd_configured"`
|
||||
} `json:"system"`
|
||||
} `json:"data"`
|
||||
}
|
||||
if err := json.Unmarshal(w.Body.Bytes(), &resp); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if !resp.Data.System.HDDConfigured {
|
||||
t.Errorf("hdd_configured must be true with a default storage path registered; body %s", w.Body.String())
|
||||
}
|
||||
}
|
||||
|
||||
@@ -905,6 +905,17 @@ func (r *Router) removeStack(w http.ResponseWriter, req *http.Request, name stri
|
||||
r.logger.Printf("[WARN] [api] Failed to forget the backup preferences of %s: %v", name, err)
|
||||
}
|
||||
}
|
||||
// R-491 (v0.242.0): an app held after a failed update that the customer then REMOVES is not held
|
||||
// any more — the hold named a copy for a deploy that no longer exists, and every start gate reads
|
||||
// the store, so a reinstall under the same name would begin refused. Measured on demo-hp
|
||||
// 2026-09-13 (nextcloud). An R-379 restore hold is deliberately NOT cleared here.
|
||||
if r.sett != nil {
|
||||
if cleared, err := r.sett.ClearUpdateHold(name); err != nil {
|
||||
r.logger.Printf("[WARN] [api] Failed to clear the update hold of %s: %v", name, err)
|
||||
} else if cleared {
|
||||
r.logger.Printf("[INFO] [api] remove %s: its update hold is cleared with it (R-491)", name)
|
||||
}
|
||||
}
|
||||
|
||||
writeJSON(w, http.StatusOK, apiResponse{OK: true, Data: resp, Message: "Stack " + name + " removed"})
|
||||
|
||||
@@ -978,7 +989,15 @@ func (r *Router) triggerSync(w http.ResponseWriter, _ *http.Request) {
|
||||
}
|
||||
|
||||
func (r *Router) systemInfo(w http.ResponseWriter, _ *http.Request) {
|
||||
info := system.GetInfo(r.cfg.Paths.HDDPath, r.cpuCollector)
|
||||
// R-490 / R-465: the same default-storage-path fallback every other reader of the always-empty
|
||||
// cfg.Paths.HDDPath has; without it hdd_configured was false on every box.
|
||||
hddPath := r.cfg.Paths.HDDPath
|
||||
if r.sett != nil {
|
||||
if p := r.sett.GetDefaultStoragePath(); p != "" {
|
||||
hddPath = p
|
||||
}
|
||||
}
|
||||
info := system.GetInfo(hddPath, r.cpuCollector)
|
||||
// F1: GetInfo now reports the guest RAM cap (from the Docker daemon) as TotalMemMB, but the guest-wide
|
||||
// "used" is not observable from the container. Report the controller's accurate committed-app memory
|
||||
// (sum of running apps' mem requests) as used — a meaningful "allocated of cap" figure for the UI.
|
||||
@@ -993,7 +1012,10 @@ func (r *Router) systemInfo(w http.ResponseWriter, _ *http.Request) {
|
||||
info.MemPercent = float64(used) / float64(info.TotalMemMB) * 100
|
||||
}
|
||||
}
|
||||
syncStatus := r.syncer.Status()
|
||||
var syncStatus interface{}
|
||||
if r.syncer != nil { // nil in tests; production always wires the syncer
|
||||
syncStatus = r.syncer.Status()
|
||||
}
|
||||
data := map[string]interface{}{
|
||||
"system": info,
|
||||
"sync_status": syncStatus,
|
||||
|
||||
Reference in New Issue
Block a user