Files
felhom-controller/controller/internal/backup/appbackup_bridge.go
T
admin 78ff991f1c v0.153.0 — R-47: the DB replay no longer races the app, on BOTH restore paths
Closes R-47. No new agent coupling — MinAgent stays 0.90.0.

The replay needs a running DB container, so both restore paths started the
WHOLE stack first, giving the application a window to rebuild the very schema
objects the dump was about to create. Measured live on 2026-07-19 (H4,
DIAG-immich-restore-round2): immich-server rebuilt clip_index two seconds
before the dump's CREATE INDEX, the replay aborted "already exists" under
ON_ERROR_STOP=1, and immich reported schema drift. The data survived only
because pg_dump emits COPY before CREATE INDEX.

Both paths now open a DB-ONLY window: only the stack's database service(s)
come up, the dump is replayed with the app still down, and the full start
runs only after the replay exits 0. Fail-closed: a dump with no identifiable
DB service refuses BEFORE the first mutation. Every exit from the window
still does a best-effort full start, so a failed restore never leaves a box
with a database and no application.

New: appbackup.DBServiceNames (yaml.v3 services-map parse — never a line
scan; immich's top-level volume keys are the decoy) sharing dbTypeForImage
with DiscoverDatabases; stacks.Manager.StartStackServices (refuses an empty
list — argument-less `up -d` is a full start); RedeployFromEnv split into
PersistUnitRedeployConfig + its unchanged tail. StackDataProvider's
RecreateStackFromUnit becomes RecreateStackDefinitionFromUnit — the hidden
`up -d` inside the old name is what carried the defect on the local path.

19 new tests (ordering plus state-at-replay-time, zero-mutation fail-closed
effects, replay-failure bring-up, parser decoys, empty-list refusal); three
companion red-proofs run and reverted. 23/23 packages green.

Not yet live-validated: STOP-1 supervised reconstitute, golden 0.153.0.
2026-07-20 17:01:52 +02:00

159 lines
5.5 KiB
Go

package backup
// This file bridges the backup package to internal/appbackup, where the
// self-contained app-data backup primitives (DB dump, Docker-volume archive
// discovery, keep-side path helpers) now live. The backup package keeps these
// names available — via type/const aliases and thin function forwarders — so
// the (still present) delete-side code and the both-side consumers (web, api,
// report) compile unchanged. Behaviour is identical: the forwarders call
// straight through to appbackup.
//
// Go has no function aliasing, so the functions are one-line forwarders while
// the types/consts use real aliases.
import (
"context"
"log"
"time"
"gitea.dooplex.hu/admin/felhom-controller/internal/appbackup"
)
// --- type aliases (appdata) ---
type StackDataProvider = appbackup.StackDataProvider
type StackSummary = appbackup.StackSummary
type AppBackupInfo = appbackup.AppBackupInfo
type AppDataPath = appbackup.AppDataPath
type AppDockerVolume = appbackup.AppDockerVolume
type RecoveryInfo = appbackup.RecoveryInfo
type ClassifiedBind = appbackup.ClassifiedBind
// --- type aliases (dbdump) ---
type DBType = appbackup.DBType
type DiscoveredDB = appbackup.DiscoveredDB
type DumpResult = appbackup.DumpResult
type DumpValidation = appbackup.DumpValidation
type DumpFileInfo = appbackup.DumpFileInfo
// --- const aliases ---
const (
DBTypePostgres = appbackup.DBTypePostgres
DBTypeMariaDB = appbackup.DBTypeMariaDB
)
// Backup-classification class constants (Task 3-core) — aliased so the tier engines can switch on
// class without importing appbackup directly.
const (
ClassMandatory = appbackup.ClassMandatory
ClassOptional = appbackup.ClassOptional
ClassExcluded = appbackup.ClassExcluded
)
// FelhomDataDir is the namespace directory on storage drives for all felhom-managed data.
const FelhomDataDir = appbackup.FelhomDataDir
// --- function forwarders (dbdump) ---
func DiscoverDatabases(ctx context.Context, logger *log.Logger, debug bool, knownStacks []string) ([]DiscoveredDB, error) {
return appbackup.DiscoverDatabases(ctx, logger, debug, knownStacks)
}
func DumpAll(ctx context.Context, dbs []DiscoveredDB, dumpDir string, logger *log.Logger, debug bool) []DumpResult {
return appbackup.DumpAll(ctx, dbs, dumpDir, logger, debug)
}
func DumpOne(ctx context.Context, db DiscoveredDB, dumpDir string, logger *log.Logger, debug bool) DumpResult {
return appbackup.DumpOne(ctx, db, dumpDir, logger, debug)
}
// ImportDump replays a captured .sql dump back into a running DB container (F17 restore path).
func ImportDump(ctx context.Context, db DiscoveredDB, dumpPath string, logger *log.Logger, debug bool) error {
return appbackup.ImportDump(ctx, db, dumpPath, logger, debug)
}
func ValidateDump(filePath string, dbType DBType) DumpValidation {
return appbackup.ValidateDump(filePath, dbType)
}
func ListDumpFiles(dumpDir string, cached func(name string, size int64, mod time.Time) (DumpValidation, bool)) ([]DumpFileInfo, error) {
return appbackup.ListDumpFiles(dumpDir, cached)
}
// --- function forwarders (appdata) ---
func DiscoverAppData(provider StackDataProvider, discoveredDBs []DiscoveredDB) []AppBackupInfo {
return appbackup.DiscoverAppData(provider, discoveredDBs)
}
func ParseComposeNamedVolumes(composePath string) []AppDockerVolume {
return appbackup.ParseComposeNamedVolumes(composePath)
}
func ResolveDockerVolumeNames(composePath string) []string {
return appbackup.ResolveDockerVolumeNames(composePath)
}
func ParseComposeImages(composePath string) []string {
return appbackup.ParseComposeImages(composePath)
}
// DBServiceNames forwards to appbackup.DBServiceNames — the compose SERVICE names holding a database,
// i.e. the argument list for the DB-only bring-up both restore paths use before a dump replay (R-47).
func DBServiceNames(composePath string) ([]string, error) {
return appbackup.DBServiceNames(composePath)
}
// humanizeBytes forwards to appbackup.HumanizeBytes; kept unexported so the
// many in-package call sites (backup.go, crossdrive.go, restore code) need no edit.
func humanizeBytes(b int64) string {
return appbackup.HumanizeBytes(b)
}
// --- function forwarders (paths) ---
//
// NOTE: the path helpers below take a felhom-data NAMESPACE ROOT, not a bare drive path. Use
// NamespaceRoot (or Manager.namespaceRoot / Manager.AppNamespaceRoot) to resolve the root first.
func NamespaceRoot(drivePath string, inGuestDrive bool) string {
return appbackup.NamespaceRoot(drivePath, inGuestDrive)
}
func PrimaryBackupPath(nsRoot string) string {
return appbackup.PrimaryBackupPath(nsRoot)
}
func AppDBDumpPath(nsRoot, stackName string) string {
return appbackup.AppDBDumpPath(nsRoot, stackName)
}
func AppVolumeDumpPath(nsRoot, stackName string) string {
return appbackup.AppVolumeDumpPath(nsRoot, stackName)
}
func RecoveryUnitPath(nsRoot, stackName string) string {
return appbackup.RecoveryUnitPath(nsRoot, stackName)
}
func RecoveryUnitComposePath(nsRoot, stackName string) string {
return appbackup.RecoveryUnitComposePath(nsRoot, stackName)
}
func RecoveryUnitManifestPath(nsRoot, stackName string) string {
return appbackup.RecoveryUnitManifestPath(nsRoot, stackName)
}
func AppDataDir(nsRoot, stackName string) string {
return appbackup.AppDataDir(nsRoot, stackName)
}
func AppDataDirNames(hddPath, stackName string, hddMounts []string) []string {
return appbackup.AppDataDirNames(hddPath, stackName, hddMounts)
}
func AppDataBindsPresent(hddPath string, hddMounts []string) bool {
return appbackup.AppDataBindsPresent(hddPath, hddMounts)
}