Files
felhom-controller/controller/internal/backup/file_legs.go
T
admin 2f8ff2414c
gates / gates (push) Successful in 17s
v0.244.0: the backup page stops promising what it does not hold (R-537/R-538/R-536)
R-537 — the contents label is now PER TIER. One string computed from the app's
shape was rendered on all three tier rows; a Tier-1 unit has no file-copy step, so
for the four class-A apps it was claiming „Adatok" for files it does not hold.

R-538 — a unit restore REFUSES before anything is touched when the unit cannot
return the app's drive-side files, and names the route that can. It runs before the
stack is stopped because the measured harm included the app's own wastebasket going
unreachable, which still held every byte.

R-536 — „Alkalmazás telepítve" moved from the deploy's acceptance to its completion,
with app_deploy_started and app_deploy_failed as the honest pair.

Each fix red-proofed: seen failing with its own sentence, passing when restored.
Requires hub v0.116.0 for the two new event types. MinAgent unchanged (0.131.0).

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0159rPz1ZhFKsS53msqPYxtS
2026-09-16 16:55:55 +02:00

57 lines
2.5 KiB
Go

package backup
import (
"strings"
"gitea.dooplex.hu/admin/felhom-controller/internal/appbackup"
)
// DeclaredDriveFileLegs answers ONE question, for the label and for the restore guard alike: which
// of this app's own files live on the customer's DATA DRIVE rather than inside a Docker volume?
//
// R-537 / R-538 (measured 2026-09-16 on a fresh box). A Tier-1 recovery unit captures compose +
// app.yaml + the DB dumps and volume tars that already exist beside it — `CaptureRecoveryUnit` has
// no file-copy step at all, and `RecoveryManifest` has no field to record one. The whole-guest tiers
// do not carry them either (`mp8 /mnt/felhom-drives` is a bind mount and vzdump logs
// "excluding bind mount point mp8 … (not a volume)"). So for the four class-A apps the drive-side
// paths are carried by Tier 2 and Tier 3 ONLY — which is the design (07-backup-architecture §6.2),
// and is exactly why a page that says „Adatok" over a Tier-1 unit, or a restore that replays a
// database over files it does not have, is a lie rather than a design choice.
//
// It returns the DECLARED mandatory paths, resolved but deliberately NOT stat-filtered. The filter
// belongs to a capture (a declared path that is missing on disk is a capture gap, and
// `offboxCaptureSet` warns about it there). Here the question is what the app CLAIMS to keep on the
// drive, and an empty folder the customer has not filled yet must still count — otherwise the label
// tells the truth today and starts lying the moment they use the app.
//
// Empty for: no stack provider, a legacy app with no `backup:` block (nothing declares a namespace
// path — all 45 class-B apps), or an app with no resolvable HDD_PATH (undeployed).
func (m *Manager) DeclaredDriveFileLegs(stack string) []string {
if m.stackProvider == nil {
return nil
}
binds, has := m.stackProvider.GetStackClassifiedBinds(stack)
if !has {
return nil
}
hdd := strings.TrimSpace(m.stackProvider.GetStackHDDPath(stack))
if hdd == "" {
return nil
}
cs := appbackup.ComputeCaptureSet(binds, has, appbackup.TierOffsite, m.namespaceRoot(hdd), m.stackProvider.GetImportRoot())
out := make([]string, 0, len(cs.Paths))
for _, p := range cs.Paths {
out = append(out, p.Abs)
}
if len(out) == 0 {
return nil
}
return out
}
// HasDriveFileLegs is DeclaredDriveFileLegs as a predicate, for the surfaces that only need the
// yes/no. Kept beside it so the two can never disagree.
func (m *Manager) HasDriveFileLegs(stack string) bool {
return len(m.DeclaredDriveFileLegs(stack)) > 0
}