Persist rewrote overrides.yaml with only this process's runtime values, so saving one key (for example websockets:generate-vapid-keys --update) dropped every key persisted earlier. It now starts from the saved file and lets runtime values win.
120 lines
3.1 KiB
Go
120 lines
3.1 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. Keys already in
|
|
// the file are kept; a runtime value replaces the saved value of its key.
|
|
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)
|
|
}
|
|
// Start from the values persisted earlier, so saving one runtime value
|
|
// never drops another key from the file; runtime values win.
|
|
merged := koanf.New(".")
|
|
if err := loadOverrides(merged, dest); err != nil {
|
|
return err
|
|
}
|
|
if c.runtime != nil {
|
|
if err := merged.Merge(c.runtime); err != nil {
|
|
return fmt.Errorf("compass: merge overrides: %w", err)
|
|
}
|
|
}
|
|
raw := []byte("{}\n")
|
|
if len(merged.Keys()) > 0 {
|
|
body, err := merged.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)))
|
|
}
|