Files
felhom-controller/controller/internal/stacks/skeleton_derive.go
T
admin 4773809334 v0.172.0 fixup: EnsureImportRoot must apply the convention to the parent userdata dir
Found on the demo-felhom deploy leg: ensuring only <sysNS>/userdata/import left
its parent at 755 root:root, because EnsureUserdataDir MkdirAll's intermediates
at plain 0755 and chmods only the leaf. That made the system drive's userdata
root the one on the box outside the 2775/gid-1000 convention.
2026-07-26 08:15:47 +02:00

93 lines
3.8 KiB
Go

package stacks
import (
"os"
"path/filepath"
"sort"
"gitea.dooplex.hu/admin/felhom-controller/internal/appbackup"
)
// Catalog-derived userdata skeleton (R-75).
//
// Before this, the customer-facing tree was a hardcoded Go list, so a new catalog app with a folder
// needed a controller release. The set is now DERIVED from the catalog the controller has already
// synced to disk — using the same authoritative parser the classifier uses — merged with the
// carry-list (appbackup.UserdataSkeletonCarry) so it can only ever ADD.
//
// Scope note: this derives ${USERDATA_PATH} binds only. ${IMPORT_PATH} binds are NOT part of a
// drive's skeleton — the canonical drop-zone lives once, on the system drive, and is ensured by
// EnsureImportRoot instead.
// DeriveUserdataDirs returns the ${USERDATA_PATH}-relative dirs implied by every template in
// stacksDir. ALL catalog apps count, not just deployed ones (Fork-2 ruling): the skeleton is
// storage-path-scoped and idempotent, and the all-apps set is barely larger than the historical
// hardcoded one (SPIKE P0(a): 14 vs 14, differing by one entry each way). Deployed-only filtering
// belongs in the UI, where an empty folder would actually confuse someone.
//
// Unreadable dirs / missing composes are skipped silently — a partially-synced catalog must degrade
// to "fewer derived dirs", never to an error that blocks the skeleton (the carry-list is the floor).
// The result is sorted; BuildUserdataSkeleton sorts again after merging, so both layers are pinned.
func DeriveUserdataDirs(stacksDir string) []string {
entries, err := os.ReadDir(stacksDir)
if err != nil {
return nil
}
set := map[string]bool{}
for _, e := range entries {
if !e.IsDir() {
continue
}
composePath := filepath.Join(stacksDir, e.Name(), "docker-compose.yml")
for _, b := range ParseComposeClassifiableBinds(composePath) {
if b.Root != appbackup.RootUserdata || b.RelPath == "" {
continue
}
set[b.RelPath] = true
}
}
out := make([]string, 0, len(set))
for d := range set {
out = append(out, d)
}
sort.Strings(out)
return out
}
// UserdataSkeletonDirs is the merged, sorted set to create on a storage path: catalog-derived plus
// the carry-list. This is THE function every skeleton caller should use.
func (m *Manager) UserdataSkeletonDirs() []string {
return appbackup.BuildUserdataSkeleton(DeriveUserdataDirs(m.cfg.Paths.StacksDir))
}
// EnsureUserdataSkeleton applies the merged skeleton to a storage path's namespace root. Signature
// kept as func(string) error so it drops straight into fbPathDeps.ensureSkeleton.
func (m *Manager) EnsureUserdataSkeleton(nsRoot string) error {
return appbackup.EnsureUserdataSkeleton(nsRoot, m.UserdataSkeletonDirs())
}
// EnsureImportRoot creates the CANONICAL drop-zone root on the system drive with the userdata
// convention, so it exists (and is browsable + shareable) even before any drop-zone app is deployed.
// Idempotent; a no-op when the import root is unresolvable.
//
// It deliberately does NOT pre-create per-app subfolders: those are the deploy belt's job, so an app
// the customer never installed does not litter the drop-zone with an empty folder.
func (m *Manager) EnsureImportRoot() error {
root := m.GetImportRoot()
if root == "" {
return nil
}
// The PARENT userdata dir must carry the convention too. EnsureUserdataDir MkdirAll's its
// intermediates at plain 0755 and then chmods only the leaf, so ensuring just the import dir
// leaves <sysNS>/userdata at 755 root:root — the one userdata root on the box that would not
// be 2775/gid-1000. Observed live on demo-felhom before this line existed.
sys := m.cfg.Paths.SystemDataPath
if sys == "" {
return nil
}
if err := appbackup.EnsureUserdataDir(appbackup.UserdataDir(appbackup.NamespaceRoot(sys, false))); err != nil {
return err
}
return appbackup.EnsureUserdataDir(root)
}