Files
felhom-controller/controller/internal/stacks/samba_classify.go
T
admin 1d26a69dd4 feat(samba): backup classification from the shares registry (R-7 slice 1, Part 4)
ClassifiedBinds("samba") resolves from the shares registry instead of catalog
metadata (samba has no .felhom.yml; its binds are absolute share paths). [R4]
Offsite ON -> mandatory (offsite + tier-2); OFF -> optional (tier-2 only);
smb.conf/passdb never classified. Verified through the REAL ComputeCaptureSet
tier filter incl. the negative (optional NOT in offsite). Zero engine edits.

Part-4 Step-1 finding: tier-2 (RunTier2) short-circuits on os.Stat(unitDir)
BEFORE GetStackClassifiedBinds, and the offsite runner enumerates
settings.GetOffboxApps() — both are recovery-unit shaped, which a share-only
infra stack has not. Wiring share data into a live run is therefore more than an
enumeration tweak; reported as a design fork per the STOP clause, not improvised.
2026-07-18 11:35:09 +02:00

67 lines
2.6 KiB
Go

package stacks
import (
"path/filepath"
"strings"
"gitea.dooplex.hu/admin/felhom-controller/internal/appbackup"
)
// Backup classification for the samba stack (R-7 slice 1, [R4]).
//
// samba is CONTROLLER-GENERATED infra: it has no .felhom.yml and its compose binds are absolute
// share paths, so the catalog path (LoadMetadata → ParseComposeClassifiableBinds → ClassifyBinds)
// cannot classify it. Its classification comes from the SHARES REGISTRY instead:
//
// share.Offsite == true → ClassMandatory → TierOffsite (offsite) AND TierSecondary (tier-2)
// share.Offsite == false → ClassOptional → TierSecondary only (never offsite)
//
// The smb.conf / passdb mounts are deliberately NOT emitted — they are config, not customer data.
//
// SCOPE NOTE (the reported design fork, see REPORT.md): making this seam correct does NOT by itself
// put share data into a live tier-2/offsite run. Both engines are structured around a per-app
// RECOVERY UNIT — backup.RunTier2 short-circuits on `os.Stat(unitDir)` before it ever calls
// GetStackClassifiedBinds, and the offsite runner enumerates settings.GetOffboxApps(). A share-only
// infra stack has neither, and teaching them about one is more than an enumeration tweak, so per the
// task's STOP clause it was reported rather than improvised inside the engines.
func (m *Manager) sambaClassifiedBinds() ([]appbackup.ClassifiedBind, bool) {
if m.settings == nil {
return nil, false
}
shares := m.settings.GetSMBShares()
out := make([]appbackup.ClassifiedBind, 0, len(shares))
for _, sh := range shares {
cls := appbackup.ClassOptional
if sh.Offsite {
cls = appbackup.ClassMandatory
}
out = append(out, appbackup.ClassifiedBind{
ComposeBind: appbackup.ComposeBind{
Root: appbackup.RootHDD,
RelPath: m.shareRelPath(sh.Path),
ReadOnly: sh.ReadOnly,
},
Class: cls,
Origin: appbackup.OriginExplicit, // the customer's per-share Felhőmentés toggle IS explicit
})
}
return out, true
}
// shareRelPath expresses a share's absolute path relative to its owning registered storage root, so
// the emitted bind matches the (Root, RelPath) + root-path resolution shape the capture-set helpers
// use. A share under no registered root falls back to the path minus its leading slash (never
// absolute — the structural guards reject absolute RelPaths).
func (m *Manager) shareRelPath(path string) string {
p := filepath.ToSlash(path)
if m.settings != nil {
for _, sp := range m.settings.GetStoragePaths() {
root := filepath.ToSlash(sp.Path)
if strings.HasPrefix(p, root+"/") {
return strings.TrimPrefix(p, root+"/")
}
}
}
return strings.TrimPrefix(p, "/")
}