F17: per-app restore now replays the captured .sql DB dump (.sql wins)
The restore paths (RestoreFromRecoveryUnit + the RestoreApp fallback) repopulated Docker volume tars but NEVER replayed the captured <stack>-<dbtype>.sql dump, so DB-resident data (e.g. rows in a DB whose data dir is a bind mount) did not come back — the romm marker round-trip in the audit lost the row. New appbackup.ImportDump (read-side counterpart to DumpOne) replays a .sql/.sql.gz into the running DB using the live container's OWN discovered credentials (no env threading; reuses DiscoveredDB + getMariaDBPassword). backup.reimportDBDumps orchestrates it AFTER volume restore + stack bring-up, so the logical dump WINS over any volume-tar copy of the DB (operator-chosen precedence). pg_dump --clean --if-exists and mariadb-dump (default --add-drop-table) make replay idempotent; psql ON_ERROR_STOP=1 surfaces real import errors. Also: volume-restore per-volume failures and DB-import failures now SURFACE (the restore returns an error) instead of a swallowed WARN, so a failed data restore cannot read as success. Tests (restore_db_test.go, injectable discover/import seams): imports when dump+DB present, failure surfaces, no-dump skips discovery, dump-but-no-matching-DB is a non-fatal skip. Live DB round-trip to be validated post-deploy.
This commit is contained in:
@@ -0,0 +1,125 @@
|
||||
package backup
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"io"
|
||||
"log"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func newReimportTestManager() *Manager {
|
||||
return &Manager{logger: log.New(io.Discard, "", 0)}
|
||||
}
|
||||
|
||||
func writeDump(t *testing.T, nsRoot, stack string, dbType DBType) string {
|
||||
t.Helper()
|
||||
dir := AppDBDumpPath(nsRoot, stack)
|
||||
if err := os.MkdirAll(dir, 0o755); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
p := filepath.Join(dir, fmt.Sprintf("%s-%s.sql", stack, dbType))
|
||||
if err := os.WriteFile(p, []byte("-- dump\nDROP TABLE IF EXISTS t;\n"), 0o644); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
return p
|
||||
}
|
||||
|
||||
// TestReimportDBDumps_ImportsWhenDumpAndDBPresent asserts F17: when a captured .sql dump exists and a
|
||||
// matching running DB is discovered, reimportDBDumps replays it. The PRE-FIX restore never called any
|
||||
// import — this orchestration is the fix.
|
||||
func TestReimportDBDumps_ImportsWhenDumpAndDBPresent(t *testing.T) {
|
||||
nsRoot := t.TempDir()
|
||||
wantPath := writeDump(t, nsRoot, "app", DBTypeMariaDB)
|
||||
|
||||
m := newReimportTestManager()
|
||||
m.discoverDBs = func(ctx context.Context) ([]DiscoveredDB, error) {
|
||||
return []DiscoveredDB{
|
||||
{StackName: "other", DBType: DBTypePostgres, ContainerName: "other-db"},
|
||||
{StackName: "app", DBType: DBTypeMariaDB, ContainerName: "app-db", ContainerID: "cid"},
|
||||
}, nil
|
||||
}
|
||||
var gotPath string
|
||||
var gotDB DiscoveredDB
|
||||
m.importDBDump = func(ctx context.Context, db DiscoveredDB, dumpPath string) error {
|
||||
gotPath, gotDB = dumpPath, db
|
||||
return nil
|
||||
}
|
||||
|
||||
n, err := m.reimportDBDumps(context.Background(), "app", nsRoot)
|
||||
if err != nil {
|
||||
t.Fatalf("reimportDBDumps: %v", err)
|
||||
}
|
||||
if n != 1 {
|
||||
t.Fatalf("imported = %d, want 1", n)
|
||||
}
|
||||
if gotPath != wantPath {
|
||||
t.Fatalf("imported path = %q, want %q", gotPath, wantPath)
|
||||
}
|
||||
if gotDB.ContainerName != "app-db" {
|
||||
t.Fatalf("imported into %q, want app-db (must match the stack's own DB)", gotDB.ContainerName)
|
||||
}
|
||||
}
|
||||
|
||||
// TestReimportDBDumps_FailureSurfaces asserts an import failure is RETURNED, not swallowed (a failed
|
||||
// data restore must not read as success).
|
||||
func TestReimportDBDumps_FailureSurfaces(t *testing.T) {
|
||||
nsRoot := t.TempDir()
|
||||
writeDump(t, nsRoot, "app", DBTypeMariaDB)
|
||||
|
||||
m := newReimportTestManager()
|
||||
m.discoverDBs = func(ctx context.Context) ([]DiscoveredDB, error) {
|
||||
return []DiscoveredDB{{StackName: "app", DBType: DBTypeMariaDB, ContainerName: "app-db"}}, nil
|
||||
}
|
||||
m.importDBDump = func(ctx context.Context, db DiscoveredDB, dumpPath string) error {
|
||||
return fmt.Errorf("boom")
|
||||
}
|
||||
|
||||
if _, err := m.reimportDBDumps(context.Background(), "app", nsRoot); err == nil || !strings.Contains(err.Error(), "boom") {
|
||||
t.Fatalf("expected the import failure to surface, got %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
// TestReimportDBDumps_NoDumpNoImport asserts apps with no .sql dump never trigger discovery/import.
|
||||
func TestReimportDBDumps_NoDumpNoImport(t *testing.T) {
|
||||
nsRoot := t.TempDir()
|
||||
m := newReimportTestManager()
|
||||
discoverCalled := false
|
||||
m.discoverDBs = func(ctx context.Context) ([]DiscoveredDB, error) {
|
||||
discoverCalled = true
|
||||
return nil, nil
|
||||
}
|
||||
m.importDBDump = func(ctx context.Context, db DiscoveredDB, dumpPath string) error {
|
||||
t.Fatal("importDBDump must not be called when there is no dump")
|
||||
return nil
|
||||
}
|
||||
n, err := m.reimportDBDumps(context.Background(), "app", nsRoot)
|
||||
if err != nil || n != 0 {
|
||||
t.Fatalf("reimportDBDumps with no dump = (%d, %v), want (0, nil)", n, err)
|
||||
}
|
||||
if discoverCalled {
|
||||
t.Fatalf("discovery should be skipped when there is no .sql dump")
|
||||
}
|
||||
}
|
||||
|
||||
// TestReimportDBDumps_DumpButNoMatchingDB asserts that a dump with no matching running DB container is a
|
||||
// non-fatal skip (logged), returning 0 imported and no error — the app is up, just no DB matched.
|
||||
func TestReimportDBDumps_DumpButNoMatchingDB(t *testing.T) {
|
||||
nsRoot := t.TempDir()
|
||||
writeDump(t, nsRoot, "app", DBTypeMariaDB)
|
||||
m := newReimportTestManager()
|
||||
m.discoverDBs = func(ctx context.Context) ([]DiscoveredDB, error) {
|
||||
return []DiscoveredDB{{StackName: "different", DBType: DBTypeMariaDB}}, nil
|
||||
}
|
||||
m.importDBDump = func(ctx context.Context, db DiscoveredDB, dumpPath string) error {
|
||||
t.Fatal("must not import when no DB matches the stack")
|
||||
return nil
|
||||
}
|
||||
n, err := m.reimportDBDumps(context.Background(), "app", nsRoot)
|
||||
if err != nil || n != 0 {
|
||||
t.Fatalf("= (%d, %v), want (0, nil)", n, err)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user