Files
summercms/compass/persist.go
Jakub Zych cc44a1fa49 feat(01-02): implement layered compass config
- Merge plugin defaults, sorted YAML, SUMMER_ env, overrides and Set
- Persist atomically to env overrides; Reload clears runtime Set
- Discover HasConfig before Register and print layered hello values
2026-09-16 13:17:13 +02:00

108 lines
2.7 KiB
Go

package compass
import (
"fmt"
"os"
"path/filepath"
"strings"
"github.com/knadh/koanf/parsers/yaml"
"github.com/knadh/koanf/v2"
)
// Set stores an in-memory override that wins over every file and env layer.
func (c *Config) Set(path string, value any) error {
if c == nil {
return fmt.Errorf("compass: config is nil")
}
if path == "" {
return fmt.Errorf("compass: empty config path")
}
c.mu.Lock()
defer c.mu.Unlock()
if c.runtime == nil {
c.runtime = koanf.New(".")
}
return c.runtime.Set(path, value)
}
// Persist writes runtime overrides to config/env/<env>/overrides.yaml
// using an atomic replace and restrictive file permissions.
func (c *Config) Persist() error {
if c == nil {
return fmt.Errorf("compass: config is nil")
}
c.mu.Lock()
defer c.mu.Unlock()
if c.dir == "" {
return fmt.Errorf("compass: config directory is empty")
}
env, err := sanitizeEnv(c.env)
if err != nil {
return err
}
destDir, dest, err := overridesPath(c.dir, env)
if err != nil {
return err
}
if err := os.MkdirAll(destDir, 0o700); err != nil {
return fmt.Errorf("compass: create overrides dir: %w", err)
}
raw := []byte("{}\n")
if c.runtime != nil {
body, err := c.runtime.Marshal(yaml.Parser())
if err != nil {
return fmt.Errorf("compass: marshal overrides: %w", err)
}
raw = body
}
tmp := dest + ".tmp"
if err := os.WriteFile(tmp, raw, 0o600); err != nil {
return fmt.Errorf("compass: write overrides: %w", err)
}
if err := os.Rename(tmp, dest); err != nil {
_ = os.Remove(tmp)
return fmt.Errorf("compass: persist overrides: %w", err)
}
return nil
}
// Reload rebuilds the tree from disk and plugin sources and clears runtime Set.
func (c *Config) Reload() error {
if c == nil {
return fmt.Errorf("compass: config is nil")
}
c.mu.Lock()
defer c.mu.Unlock()
c.runtime = koanf.New(".")
if err := c.refreshSources(); err != nil {
return err
}
return c.rebuild()
}
func overridesPath(dir, env string) (destDir, dest string, err error) {
root, err := filepath.Abs(dir)
if err != nil {
return "", "", fmt.Errorf("compass: resolve config dir: %w", err)
}
root = filepath.Clean(root)
destDir = filepath.Clean(filepath.Join(root, "env", env))
if !within(root, destDir) {
return "", "", fmt.Errorf("compass: persist path escapes config directory")
}
dest = filepath.Clean(filepath.Join(destDir, "overrides.yaml"))
if !within(destDir, dest) {
return "", "", fmt.Errorf("compass: persist path escapes config directory")
}
return destDir, dest, nil
}
func within(root, path string) bool {
rel, err := filepath.Rel(root, path)
if err != nil {
return false
}
return rel == "." || (rel != ".." && !strings.HasPrefix(rel, ".."+string(os.PathSeparator)))
}