v0.66.0: userdata layout + shared-storage ownership convention
appbackup/userdata.go: EnsureUserdataDir (MkdirAll + explicit setgid Chmod 2775 +
chown gid 1000), UserdataSkeleton, EnsureUserdataSkeleton; linux chown/StatGID +
non-linux stubs. stackEnv injects USERDATA_PATH=<HDD_PATH>/userdata. Skeleton
pre-created on register + FileBrowser sync; deploy belt (composeExecCustomEnv on
'up') pre-creates every ${USERDATA_PATH} bind source. FileBrowser mounts userdata
(was appdata) — uid 1000 can now write into 2775 setgid. #8: migrate merge walk +
copyFile preserve source setgid+group so the convention survives MigrateAll.
Non-hollow tests incl. Linux setgid assertions + migration-preserve companion.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -13,6 +13,7 @@ import (
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"gitea.dooplex.hu/admin/felhom-controller/internal/appbackup"
|
||||
"gitea.dooplex.hu/admin/felhom-controller/internal/config"
|
||||
"gitea.dooplex.hu/admin/felhom-controller/internal/crypto"
|
||||
"gitea.dooplex.hu/admin/felhom-controller/internal/settings"
|
||||
@@ -122,6 +123,33 @@ func NewManager(cfg *config.Config, logger *log.Logger) (*Manager, error) {
|
||||
}, nil
|
||||
}
|
||||
|
||||
// ensureUserdataMounts is the deploy belt: pre-create every ${USERDATA_PATH}/... bind source the
|
||||
// stack declares with the userdata convention, so Docker never auto-creates one as guest-root.
|
||||
func (m *Manager) ensureUserdataMounts(stackDir string, env []string) {
|
||||
userdataPath := envLookup(env, "USERDATA_PATH")
|
||||
if userdataPath == "" {
|
||||
return
|
||||
}
|
||||
composePath := filepath.Join(stackDir, "docker-compose.yml")
|
||||
for _, src := range ParseComposeUserdataMounts(composePath, userdataPath) {
|
||||
if err := appbackup.EnsureUserdataDir(src); err != nil {
|
||||
m.logger.Printf("[WARN] [stacks] userdata belt: ensure %s: %v", src, err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// envLookup returns the value of key in a "K=V" env slice (last occurrence wins, "" if absent).
|
||||
func envLookup(env []string, key string) string {
|
||||
prefix := key + "="
|
||||
val := ""
|
||||
for _, e := range env {
|
||||
if strings.HasPrefix(e, prefix) {
|
||||
val = e[len(prefix):]
|
||||
}
|
||||
}
|
||||
return val
|
||||
}
|
||||
|
||||
// SetEncryptionKey sets the AES-256 key used to encrypt/decrypt sensitive values in app.yaml.
|
||||
func (m *Manager) SetEncryptionKey(key []byte) {
|
||||
m.mu.Lock()
|
||||
@@ -829,6 +857,12 @@ func (m *Manager) stackEnv(stackDir string) []string {
|
||||
for k, v := range appCfg.Env {
|
||||
env = append(env, fmt.Sprintf("%s=%s", k, v))
|
||||
}
|
||||
// Inject USERDATA_PATH = <namespace root>/userdata alongside HDD_PATH (v0.66.0). HDD_PATH IS
|
||||
// the namespace root (the chosen StoragePath: a Model-A user drive's mount, or the SSD's
|
||||
// felhom-data dir), so the catalog's ${USERDATA_PATH}/... mounts resolve under userdata/.
|
||||
if hdd := appCfg.Env["HDD_PATH"]; hdd != "" {
|
||||
env = append(env, fmt.Sprintf("USERDATA_PATH=%s", appbackup.UserdataDir(hdd)))
|
||||
}
|
||||
}
|
||||
|
||||
return env
|
||||
@@ -857,6 +891,13 @@ func (m *Manager) composeExecCustomEnv(dir string, env []string, args ...string)
|
||||
cmd.Env = env
|
||||
}
|
||||
|
||||
// Deploy belt (v0.66.0): before `up`, pre-create every ${USERDATA_PATH}/... bind source with the
|
||||
// userdata convention (2775 setgid, gid 1000) so the Docker daemon never auto-creates one as
|
||||
// guest-root — covers apps not in the skeleton too. Best-effort: a perms hiccup never blocks deploy.
|
||||
if len(args) > 0 && args[0] == "up" {
|
||||
m.ensureUserdataMounts(dir, env)
|
||||
}
|
||||
|
||||
// Log env var keys at debug level
|
||||
if m.isDebug() {
|
||||
var appKeys []string
|
||||
|
||||
Reference in New Issue
Block a user