feat(archive): BAK-03 Restore-Verfahren fuer Datenbank und Objekt-Storage
internal/restore: Atomarer Restore ueber Temp-Verzeichnis + Rename, nicht-leeres Ziel ohne -force bricht VOR jeder Beruehrung ab, JSONL- Protokoll jedes Laufs. Drei reale Defekte beim Bau gefunden und behoben: pg_combinebackup braucht Plain- statt Tar-Format (Extraktionsschritt ergaenzt), pg_wal.tar.gz wurde nie verifiziert/wiederhergestellt (BAK-01s Verify jetzt erweitert), Go-exec haengt bei pg_ctl start wegen vererbter Pipes (Testfix: echte Logdatei statt CombinedOutput). Beide Restore-Pfade real auf 131 ueber restore-cli nachgewiesen, inkl. echtem Postgres-Start aus wiederhergestelltem Verzeichnis.
This commit is contained in:
@@ -0,0 +1,102 @@
|
||||
package backup
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
|
||||
// TestRestore_CombinesIntoStartablePostgresInstance ist Pruefung 1 fuer
|
||||
// BAK-03 (Datenbank-Teil): Restore auf leerem System vollstaendig und
|
||||
// erfolgreich durchgefuehrt — real bewiesen, indem das wiederhergestellte
|
||||
// Verzeichnis tatsaechlich als eigenstaendige Postgres-Instanz gestartet
|
||||
// und per echter Verbindung abgefragt wird, nicht nur auf Dateiexistenz
|
||||
// geprueft.
|
||||
func TestRestore_CombinesIntoStartablePostgresInstance(t *testing.T) {
|
||||
cfg := requireTestConfig(t)
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 60*time.Second)
|
||||
defer cancel()
|
||||
|
||||
if _, err := exec.LookPath("pg_combinebackup"); err != nil {
|
||||
t.Skip("pg_combinebackup nicht installiert, restore-integrationstest uebersprungen")
|
||||
}
|
||||
pgCtl, err := exec.LookPath("pg_ctl")
|
||||
if err != nil {
|
||||
t.Skip("pg_ctl nicht installiert, restore-integrationstest uebersprungen")
|
||||
}
|
||||
|
||||
genID := NewGenerationID(time.Now())
|
||||
if _, err := FullBackup(ctx, cfg, genID); err != nil {
|
||||
t.Fatalf("fullbackup: %v", err)
|
||||
}
|
||||
|
||||
restoreOut := filepath.Join(t.TempDir(), "restored-pgdata")
|
||||
if err := os.MkdirAll(restoreOut, 0o700); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := Restore(ctx, cfg, genID, restoreOut); err != nil {
|
||||
t.Fatalf("restore: %v", err)
|
||||
}
|
||||
if _, err := os.Stat(filepath.Join(restoreOut, "PG_VERSION")); err != nil {
|
||||
t.Fatalf("wiederhergestelltes verzeichnis ist kein gueltiges PGDATA (PG_VERSION fehlt): %v", err)
|
||||
}
|
||||
|
||||
// Auf diesem Debian-Postgres liegt postgresql.conf NICHT in PGDATA
|
||||
// (sondern in /etc/postgresql/17/main/) - pg_basebackup sichert daher
|
||||
// nur PGDATA-Inhalte, die Konfigurationsdatei fehlt im Restore
|
||||
// GENAUSO wie im echten Betriebs-Restore-Verfahren. Fuer den
|
||||
// End-zu-End-Nachweis (echte Daten wiederherstellbar) hier eine
|
||||
// minimale, ausschliesslich fuer den Testlauf gueltige Konfiguration
|
||||
// nachgereicht - dokumentiert als operativer Hinweis fuer ein echtes
|
||||
// Restore-Runbook, nicht Teil des Produktcodes.
|
||||
minimalConf := "listen_addresses = ''\n"
|
||||
if err := os.WriteFile(filepath.Join(restoreOut, "postgresql.conf"), []byte(minimalConf), 0o600); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
// dito pg_hba.conf - liegt auf Debian ebenfalls in /etc/postgresql,
|
||||
// nicht in PGDATA. Nur Unix-Socket-Verbindungen des lokalen Testlaufs
|
||||
// erlaubt (kein TCP, dieselbe Isolation wie listen_addresses='').
|
||||
hba := "local all all trust\n"
|
||||
if err := os.WriteFile(filepath.Join(restoreOut, "pg_hba.conf"), []byte(hba), 0o600); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
// Konfigurationsdateien aus der Quellinstanz brachte pg_basebackup mit
|
||||
// (sie enthalten ggf. den alten Port/Unix-Socket) - fuer diesen Test
|
||||
// bewusst neuer Port und eigenes Socket-Verzeichnis, damit die
|
||||
// wiederhergestellte Instanz nicht mit der laufenden Test-Instanz
|
||||
// kollidiert.
|
||||
socketDir := t.TempDir()
|
||||
testPort := "55432"
|
||||
// WICHTIG: pg_ctl start foerdert postgres als Hintergrundprozess, der
|
||||
// die geerbten stdout/stderr-Pipes NICHT schliesst - CombinedOutput()
|
||||
// (das auf ein Pipe-EOF wartet) haengt sich daher auf, obwohl pg_ctl
|
||||
// selbst laengst zurueckgekehrt ist. Deshalb echte Logdatei statt Pipe
|
||||
// (Standard-pg_ctl-Muster), kein exec.Cmd.Stdout/-Stderr-Pipe-Zugriff.
|
||||
logFile := filepath.Join(t.TempDir(), "postgres.log")
|
||||
startCmd := exec.CommandContext(ctx, pgCtl, "start", "-D", restoreOut, "-w", "-t", "30", "-l", logFile,
|
||||
"-o", fmt.Sprintf("-p %s -k %s -c listen_addresses=''", testPort, socketDir))
|
||||
if err := startCmd.Run(); err != nil {
|
||||
logContent, _ := os.ReadFile(logFile)
|
||||
t.Fatalf("pg_ctl start (wiederhergestellte instanz): %v (log: %s)", err, string(logContent))
|
||||
}
|
||||
defer func() {
|
||||
stopCmd := exec.Command(pgCtl, "stop", "-D", restoreOut, "-m", "fast")
|
||||
_ = stopCmd.Run()
|
||||
}()
|
||||
|
||||
psqlOut, err := exec.CommandContext(ctx, "psql",
|
||||
"-h", socketDir, "-p", testPort, "-U", cfg.User, "-d", "postgres",
|
||||
"-tAc", "SELECT 1").CombinedOutput()
|
||||
if err != nil {
|
||||
t.Fatalf("echte verbindung zur wiederhergestellten instanz fehlgeschlagen: %v (ausgabe: %s)", err, string(psqlOut))
|
||||
}
|
||||
if strings.TrimSpace(string(psqlOut)) != "1" {
|
||||
t.Fatalf("unerwartete antwort von der wiederhergestellten instanz: %q", string(psqlOut))
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user