controller: F7 atomic volume dumps + F6 no-single-copy + F5 stale-primary sweep (WIP, pre-build)
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_017CDMFpFx84pfviCTVuGGhf
This commit is contained in:
@@ -0,0 +1,107 @@
|
||||
package backup
|
||||
|
||||
import (
|
||||
"io"
|
||||
"log"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
|
||||
"gitea.dooplex.hu/admin/felhom-controller/internal/config"
|
||||
"gitea.dooplex.hu/admin/felhom-controller/internal/settings"
|
||||
)
|
||||
|
||||
// f5Manager wires a Manager with a real settings store holding the given enrolled drives, plus a
|
||||
// sys drive, and a fake provider mapping each deployed app to its CURRENT HDD path.
|
||||
func f5Manager(t *testing.T, sysDrive string, enrolled []string, deployed map[string]string) *Manager {
|
||||
t.Helper()
|
||||
sett, err := settings.Load(filepath.Join(t.TempDir(), "settings.json"), log.New(io.Discard, "", 0))
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
for _, d := range enrolled {
|
||||
if err := sett.AddStoragePath(settings.StoragePath{Path: d, Label: filepath.Base(d), Schedulable: true}); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
var stacks []StackSummary
|
||||
for name := range deployed {
|
||||
stacks = append(stacks, StackSummary{Name: name})
|
||||
}
|
||||
cfg := &config.Config{}
|
||||
cfg.Paths.SystemDataPath = sysDrive
|
||||
fake := &volDumpFakeProvider{stacks: stacks, hdd: deployed}
|
||||
return &Manager{cfg: cfg, settings: sett, logger: log.New(io.Discard, "", 0),
|
||||
systemDataPath: sysDrive, stackProvider: fake}
|
||||
}
|
||||
|
||||
// seedPrimaryDir creates a backups/primary/<app> dir (with a marker file) under a drive's namespace.
|
||||
func seedPrimaryDir(t *testing.T, driveNsRoot, app string) string {
|
||||
t.Helper()
|
||||
dir := RecoveryUnitPath(driveNsRoot, app)
|
||||
if err := os.MkdirAll(filepath.Join(dir, "volume-dumps"), 0755); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := os.WriteFile(filepath.Join(dir, "manifest.json"), []byte("{}"), 0644); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
return dir
|
||||
}
|
||||
|
||||
// F5: an app redeployed from drive A to drive B → its stale primary dir on A is removed, its live
|
||||
// dir on B is kept. Guard red-proof lives in the two "kept" assertions.
|
||||
func TestPruneStalePrimaryDirs_RemovesRedeployedResidue(t *testing.T) {
|
||||
driveA := t.TempDir()
|
||||
driveB := t.TempDir()
|
||||
sys := filepath.Join(t.TempDir(), "sys")
|
||||
// nextcloud currently on B; a stale unit remains on A.
|
||||
m := f5Manager(t, sys, []string{driveA, driveB}, map[string]string{"nextcloud": driveB})
|
||||
|
||||
nsA := NamespaceRoot(driveA, true)
|
||||
nsB := NamespaceRoot(driveB, true)
|
||||
staleA := seedPrimaryDir(t, nsA, "nextcloud")
|
||||
liveB := seedPrimaryDir(t, nsB, "nextcloud")
|
||||
|
||||
m.pruneStalePrimaryDirs()
|
||||
|
||||
if _, err := os.Stat(staleA); !os.IsNotExist(err) {
|
||||
t.Errorf("stale primary dir on the OLD drive was not removed (F5)")
|
||||
}
|
||||
if _, err := os.Stat(liveB); err != nil {
|
||||
t.Errorf("the LIVE primary dir on the current drive was wrongly removed: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
// GUARD: an UNDEPLOYED app's primary dir is a restore point — it must be KEPT (companion: drop the
|
||||
// `!deployed { continue }` guard → this dir is deleted → fail).
|
||||
func TestPruneStalePrimaryDirs_KeepsUndeployedRestorePoint(t *testing.T) {
|
||||
driveA := t.TempDir()
|
||||
sys := filepath.Join(t.TempDir(), "sys")
|
||||
// Only "nextcloud" is deployed (on A); "removed-app" has a leftover unit but is NOT deployed.
|
||||
m := f5Manager(t, sys, []string{driveA}, map[string]string{"nextcloud": driveA})
|
||||
nsA := NamespaceRoot(driveA, true)
|
||||
seedPrimaryDir(t, nsA, "nextcloud")
|
||||
orphan := seedPrimaryDir(t, nsA, "removed-app")
|
||||
|
||||
m.pruneStalePrimaryDirs()
|
||||
|
||||
if _, err := os.Stat(orphan); err != nil {
|
||||
t.Errorf("an UNDEPLOYED app's restore point was wrongly deleted (guard failure): %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
// GUARD: the dir on an app's CURRENT drive is never touched (already covered above, but assert it
|
||||
// directly with a single-drive app so no cross-drive move is involved).
|
||||
func TestPruneStalePrimaryDirs_KeepsCurrentDriveDir(t *testing.T) {
|
||||
driveA := t.TempDir()
|
||||
sys := filepath.Join(t.TempDir(), "sys")
|
||||
m := f5Manager(t, sys, []string{driveA}, map[string]string{"nextcloud": driveA})
|
||||
nsA := NamespaceRoot(driveA, true)
|
||||
live := seedPrimaryDir(t, nsA, "nextcloud")
|
||||
|
||||
m.pruneStalePrimaryDirs()
|
||||
|
||||
if _, err := os.Stat(live); err != nil {
|
||||
t.Errorf("the current-drive primary dir was wrongly removed: %v", err)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user