Files
felhom-controller/controller/internal/backup/volume_atomic_test.go
T

120 lines
4.6 KiB
Go

package backup
import (
"io"
"log"
"os"
"path/filepath"
"testing"
"gitea.dooplex.hu/admin/felhom-controller/internal/config"
)
// atomicTestManager wires a Manager whose GetAppDrivePath resolves to a single drive dir, with the
// F7 tar seam injected. Returns the manager + the volume-dump dir where `.tar`/`.tar.tmp` land.
func atomicTestManager(t *testing.T, stack, volName string, tar func(volName, dumpDir string) ([]byte, error)) (*Manager, string) {
t.Helper()
drive := t.TempDir()
cfg := &config.Config{}
cfg.Paths.SystemDataPath = drive
fake := &volDumpFakeProvider{
stacks: []StackSummary{{Name: stack}},
volumes: map[string][]string{stack: {volName}},
}
m := &Manager{cfg: cfg, logger: log.New(io.Discard, "", 0),
systemDataPath: drive, stackProvider: fake, tarVolume: tar}
dumpDir := AppVolumeDumpPath(m.namespaceRoot(drive), stack)
return m, dumpDir
}
// Happy path: the tar seam writes a good `.tar.tmp`; DumpAppVolumes promotes it to `.tar`, leaves no
// `.tmp`, and the content is exactly what the seam wrote.
func TestDumpAppVolumes_HappyAtomicPromote(t *testing.T) {
const vol = "app_data"
want := []byte("GOOD-TAR-CONTENT")
m, dumpDir := atomicTestManager(t, "app", vol, func(_, dir string) ([]byte, error) {
return nil, os.WriteFile(filepath.Join(dir, vol+".tar.tmp"), want, 0644)
})
if err := m.DumpAppVolumes("app"); err != nil {
t.Fatalf("DumpAppVolumes: %v", err)
}
got, err := os.ReadFile(filepath.Join(dumpDir, vol+".tar"))
if err != nil {
t.Fatalf("final .tar missing: %v", err)
}
if string(got) != string(want) {
t.Errorf("final .tar content = %q, want %q", got, want)
}
if _, err := os.Stat(filepath.Join(dumpDir, vol+".tar.tmp")); !os.IsNotExist(err) {
t.Errorf("a `.tar.tmp` was left behind after a successful dump")
}
}
// THE F7 RED-PROOF: a good `.tar` already exists (the last restore point). The tar seam fails
// mid-write (writes a partial/0-byte `.tar.tmp` then errors, simulating a NFS cut). The ORIGINAL
// `.tar` MUST be byte-identical afterwards, and no 0-byte `.tar` may exist. Companion: revert
// DumpAppVolumes to the in-place `tar cf …/<vol>.tar` write → the marker is truncated → this fails.
func TestDumpAppVolumes_MidWriteFailurePreservesLastGood(t *testing.T) {
const vol = "app_data"
marker := []byte("LAST-GOOD-247-BYTE-DUMP-MARKER") // the campaign's 247-byte last-good dump, in spirit
m, dumpDir := atomicTestManager(t, "app", vol, func(_, dir string) ([]byte, error) {
// Simulate a mid-write cut: partially write the tmp, then fail (as a dead NFS target would).
_ = os.WriteFile(filepath.Join(dir, vol+".tar.tmp"), []byte("PARTIAL-TRUNCATED"), 0644)
return []byte("tar: write error: Input/output error"), errTest
})
// Pre-seed the last good restore point.
if err := os.MkdirAll(dumpDir, 0755); err != nil {
t.Fatal(err)
}
tarPath := filepath.Join(dumpDir, vol+".tar")
if err := os.WriteFile(tarPath, marker, 0644); err != nil {
t.Fatal(err)
}
err := m.DumpAppVolumes("app")
if err == nil {
t.Fatal("DumpAppVolumes must report failure when the tar step fails")
}
// The restore point is byte-identical — F7's whole point.
got, rerr := os.ReadFile(tarPath)
if rerr != nil {
t.Fatalf("the last good `.tar` was DESTROYED by a failed write (F7 regression): %v", rerr)
}
if string(got) != string(marker) {
t.Errorf("last good `.tar` was mutated by a failed write: got %q, want %q (F7 regression)", got, marker)
}
// No stray tmp and no 0-byte tar.
if _, err := os.Stat(filepath.Join(dumpDir, vol+".tar.tmp")); !os.IsNotExist(err) {
t.Errorf("the failed `.tar.tmp` was not cleaned up")
}
if info, _ := os.Stat(tarPath); info != nil && info.Size() == 0 {
t.Errorf("the restore point is now a 0-byte file (the exact F7 failure)")
}
}
// A leftover `.tar.tmp` from a previously-killed run is cleaned up by the next dump and is never a
// restore point (its name ends `.tmp`, invisible to the `.tar` restore-point scan).
func TestDumpAppVolumes_LeftoverTmpCleaned(t *testing.T) {
const vol = "app_data"
m, dumpDir := atomicTestManager(t, "app", vol, func(_, dir string) ([]byte, error) {
return nil, os.WriteFile(filepath.Join(dir, vol+".tar.tmp"), []byte("new"), 0644)
})
if err := os.MkdirAll(dumpDir, 0755); err != nil {
t.Fatal(err)
}
// A stale tmp for a DIFFERENT (removed) volume — must be swept.
stale := filepath.Join(dumpDir, "old_removed_vol.tar.tmp")
if err := os.WriteFile(stale, []byte("stale"), 0644); err != nil {
t.Fatal(err)
}
if err := m.DumpAppVolumes("app"); err != nil {
t.Fatalf("DumpAppVolumes: %v", err)
}
if _, err := os.Stat(stale); !os.IsNotExist(err) {
t.Errorf("a stale `.tar.tmp` was left behind (must be swept)")
}
}