v0.53.0: Phase 2 capture side — per-app secret-free recovery unit
Each app's on-drive backup becomes a self-contained, recreatable recovery unit: compose/ (docker-compose.yml + .felhom.yml + secret-stripped app.yaml) alongside the existing db-dumps/ + volume-dumps/, plus a secret-free manifest.json (image pins, secret env-var NAMES, data_key names, checksums). The unit stores no secret value, no data-key, and not the image — secrets are recovered at restore from the guest's own app.yaml (live/PBS), never regenerated. - appbackup: RecoveryUnit* path helpers, RecoveryInfo + GetStackRecoveryInfo, ParseComposeImages; AppDBDump/Volume refactored onto RecoveryUnitPath. - backup: recovery_unit.go (manifest + CaptureRecoveryUnit), wired into RunDBDumps; capture test proves secret-free. - stacks: DeployField.DataKey + Metadata.DataKeyEnvVars(); main.go stackAdapter implements GetStackRecoveryInfo (excludes secret-named + encrypted values). - Restore-from-unit recreate + fail-closed gate + live AdventureLog validation: next. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -1,6 +1,7 @@
|
||||
package appbackup
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"context"
|
||||
"fmt"
|
||||
"log"
|
||||
@@ -23,6 +24,55 @@ type StackDataProvider interface {
|
||||
StopStack(name string) error
|
||||
StartStack(name string) error
|
||||
RefreshAndIsRunning(name string) bool
|
||||
// GetStackRecoveryInfo returns the data needed to capture a SECRET-FREE recovery unit
|
||||
// (Phase 2): the stack dir, pinned image tags, the non-secret env, and the NAMES of the
|
||||
// secret/data-key env vars (values are NEVER returned — they are recovered at restore time
|
||||
// from the guest's own app.yaml, live or via the PBS whole-guest snapshot). ok=false if the
|
||||
// stack is unknown.
|
||||
GetStackRecoveryInfo(name string) (RecoveryInfo, bool)
|
||||
}
|
||||
|
||||
// RecoveryInfo carries everything needed to write a secret-free recovery unit for a stack.
|
||||
// It deliberately holds NO secret values — only the names of secret/data-key env vars, so the
|
||||
// manifest can record what must be recovered from elsewhere (guest app.yaml / PBS) without the
|
||||
// unit ever storing a secret or a data-encrypting key.
|
||||
type RecoveryInfo struct {
|
||||
StackDir string // dir holding docker-compose.yml + .felhom.yml + app.yaml
|
||||
DisplayName string // app display name
|
||||
ImagePins []string // pinned image tags from compose `image:` lines (re-pulled on restore)
|
||||
NonSecretEnv map[string]string // env with all secret/password/data-key values removed (plaintext only)
|
||||
SecretEnvVars []string // NAMES of stripped secret/password fields (recovered from guest/PBS)
|
||||
DataKeyEnvVars []string // NAMES of data-encrypting-key fields (fail-closed gate on restore)
|
||||
}
|
||||
|
||||
// ParseComposeImages extracts the pinned image references (`image: repo:tag`) from a
|
||||
// docker-compose.yml, in file order, de-duplicated. The image bytes are never stored in the
|
||||
// recovery unit — only these pins, so restore re-pulls from the registry.
|
||||
func ParseComposeImages(composePath string) []string {
|
||||
data, err := os.ReadFile(composePath)
|
||||
if err != nil {
|
||||
return nil
|
||||
}
|
||||
var images []string
|
||||
seen := make(map[string]bool)
|
||||
scanner := bufio.NewScanner(strings.NewReader(string(data)))
|
||||
for scanner.Scan() {
|
||||
line := strings.TrimSpace(scanner.Text())
|
||||
if !strings.HasPrefix(line, "image:") {
|
||||
continue
|
||||
}
|
||||
img := strings.TrimSpace(strings.TrimPrefix(line, "image:"))
|
||||
img = strings.Trim(img, "\"'")
|
||||
// Skip variable-only images we can't pin (e.g. image: ${SOME_IMAGE})
|
||||
if img == "" || strings.HasPrefix(img, "${") {
|
||||
continue
|
||||
}
|
||||
if !seen[img] {
|
||||
seen[img] = true
|
||||
images = append(images, img)
|
||||
}
|
||||
}
|
||||
return images
|
||||
}
|
||||
|
||||
// StackSummary holds minimal stack info needed for app data discovery.
|
||||
|
||||
Reference in New Issue
Block a user