v0.57.0: stable host-storage list + per-app Tier-2 config panel
Part A of the UI-fixes/storage-spike spec.
A1: enrichHostStorageTargets sorts /api/host-metrics storage_targets
server-side and attaches friendly Hungarian labels + purpose, fixing the
#host-storage-bars reorder-on-poll bug. Display labels only — PVE storage
ids are never renamed.
A2: new GET/POST /stacks/{name}/backup Tier-2 config panel; the "2. mentés"
Beállítás button is repointed there from the dead-end deploy page. Customer
can pin a target drive or disable Tier 2; preference is preserved across the
runner's status writes. Always visible (single-SSD + non-HDD apps included).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -1,6 +1,91 @@
|
||||
package backup
|
||||
|
||||
import "testing"
|
||||
import (
|
||||
"log"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
|
||||
"gitea.dooplex.hu/admin/felhom-controller/internal/config"
|
||||
"gitea.dooplex.hu/admin/felhom-controller/internal/settings"
|
||||
)
|
||||
|
||||
// newTestManager builds a Manager backed by a real (temp-file) settings store and the given
|
||||
// system-data path as the SSD fallback source (no stackProvider → source = systemDataPath).
|
||||
func newTestManager(t *testing.T, systemDataPath string) (*Manager, *settings.Settings) {
|
||||
t.Helper()
|
||||
logger := log.New(os.Stderr, "", 0)
|
||||
sett, err := settings.Load(filepath.Join(t.TempDir(), "settings.json"), logger)
|
||||
if err != nil {
|
||||
t.Fatalf("settings.Load: %v", err)
|
||||
}
|
||||
cfg := &config.Config{}
|
||||
cfg.Paths.SystemDataPath = systemDataPath
|
||||
return NewManager(cfg, sett, logger), sett
|
||||
}
|
||||
|
||||
// A customer-pinned PreferredTarget must win over the auto-pick (which would take the first
|
||||
// off-disk drive), and be reported with the "kézi választás" reason.
|
||||
func TestSelectTier2Target_HonorsPreferred(t *testing.T) {
|
||||
m, sett := newTestManager(t, "/srv/sys")
|
||||
// Two eligible drives; auto-pick would take the alphabetically-first ("/mnt/a").
|
||||
if err := sett.AddStoragePath(settings.StoragePath{Path: "/mnt/a", Label: "A", Schedulable: true}); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := sett.AddStoragePath(settings.StoragePath{Path: "/mnt/b", Label: "B", Schedulable: true}); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := sett.SetTier2Preference("app", false, "/mnt/b"); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
target, err := m.selectTier2Target("app", 1024)
|
||||
if err != nil {
|
||||
t.Fatalf("selectTier2Target: %v", err)
|
||||
}
|
||||
if target.NamespaceRoot != filepath.FromSlash("/mnt/b") {
|
||||
t.Errorf("NamespaceRoot = %q, want /mnt/b (pinned)", target.NamespaceRoot)
|
||||
}
|
||||
if target.Reason != "kézi választás" {
|
||||
t.Errorf("Reason = %q, want 'kézi választás'", target.Reason)
|
||||
}
|
||||
}
|
||||
|
||||
// An invalid pin (path not registered) silently falls through to the auto-pick.
|
||||
func TestSelectTier2Target_InvalidPreferredFallsBack(t *testing.T) {
|
||||
m, sett := newTestManager(t, "/srv/sys")
|
||||
if err := sett.AddStoragePath(settings.StoragePath{Path: "/mnt/a", Label: "A", Schedulable: true}); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := sett.SetTier2Preference("app", false, "/mnt/gone"); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
target, err := m.selectTier2Target("app", 1024)
|
||||
if err != nil {
|
||||
t.Fatalf("selectTier2Target: %v", err)
|
||||
}
|
||||
if target.NamespaceRoot != filepath.FromSlash("/mnt/a") || target.Reason != "másik adatmeghajtó" {
|
||||
t.Errorf("got %q/%q, want /mnt/a auto-pick", target.NamespaceRoot, target.Reason)
|
||||
}
|
||||
}
|
||||
|
||||
// A runner status write must NOT clobber the customer's preference fields.
|
||||
func TestRecordTier2_PreservesPreference(t *testing.T) {
|
||||
m, sett := newTestManager(t, "/srv/sys")
|
||||
if err := sett.SetTier2Preference("app", true, "/mnt/b"); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
m.recordTier2NoTarget("app", "teszt")
|
||||
cd := sett.GetCrossDriveConfig("app")
|
||||
if cd == nil {
|
||||
t.Fatal("config missing after status write")
|
||||
}
|
||||
if !cd.UserDisabled || cd.PreferredTarget != "/mnt/b" {
|
||||
t.Errorf("preference clobbered: UserDisabled=%v PreferredTarget=%q", cd.UserDisabled, cd.PreferredTarget)
|
||||
}
|
||||
if cd.LastStatus != "no_target" {
|
||||
t.Errorf("LastStatus = %q, want no_target", cd.LastStatus)
|
||||
}
|
||||
}
|
||||
|
||||
// TestTier2FitsHeadroom covers the size-aware rootfs-headroom guard that protects the ~8 GB guest
|
||||
// rootfs from being filled by a Tier 2 SSD copy (reserve = max(2 GB, 20% of total)).
|
||||
|
||||
Reference in New Issue
Block a user