fix(11-04): keep earlier overrides when compass Persist saves

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.
This commit is contained in:
Jakub Zych
2026-09-30 13:45:13 +02:00
parent a9af0d77c7
commit 5fb22c28d0
3 changed files with 57 additions and 4 deletions

View File

@@ -19,7 +19,7 @@ Layered YAML configuration with per-environment directories, `SUMMER_` environme
6. In-memory values stored with `compass.Config.Set`.
- Typed getters with zero-value defaults: `compass.Config.String`, `compass.Config.Int`, `compass.Config.Bool`, plus `compass.Config.Lookup` and `compass.Config.Has` to tell a missing key from a zero value.
- `compass.Config.LoadSection` unmarshals a whole subtree into a struct using `koanf` struct tags.
- Runtime overrides: `compass.Config.Set` changes a value in memory, `compass.Config.Persist` saves all runtime values atomically to the environment's `overrides.yaml` (directory mode 0700, file mode 0600, refusing any path outside the config directory), and `compass.Config.Reload` rereads every source and discards unsaved runtime values.
- Runtime overrides: `compass.Config.Set` changes a value in memory, `compass.Config.Persist` saves all runtime values atomically to the environment's `overrides.yaml`, keeping the keys already saved there and replacing the saved value of any key set at runtime (directory mode 0700, file mode 0600, refusing any path outside the config directory), and `compass.Config.Reload` rereads every source and discards unsaved runtime values.
- `compass.Config.Environment` reports the active environment name, which must consist of letters, digits, `-` and `_`.
- Safe for concurrent reads and writes.

View File

@@ -27,7 +27,8 @@ func (c *Config) Set(path string, value any) error {
}
// Persist writes runtime overrides to config/env/<env>/overrides.yaml
// using an atomic replace and restrictive file permissions.
// 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")
@@ -48,9 +49,20 @@ func (c *Config) Persist() error {
if err := os.MkdirAll(destDir, 0o700); err != nil {
return fmt.Errorf("compass: create overrides dir: %w", err)
}
raw := []byte("{}\n")
// 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 {
body, err := c.runtime.Marshal(yaml.Parser())
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)
}

View File

@@ -115,3 +115,44 @@ func TestPersistRejectsInvalidEnvName(t *testing.T) {
t.Fatalf("error should not echo unsanitized env path: %v", err)
}
}
func TestPersistKeepsEarlierOverrides(t *testing.T) {
dir := writeConfigTree(t, map[string]string{
"app.yaml": "name: base\n",
"env/development/overrides.yaml": "app:\n name: persisted\n",
})
cfg, err := Open(Options{Dir: dir, Env: "development", Environ: []string{}})
if err != nil {
t.Fatalf("Open: %v", err)
}
if err := cfg.Set("push.public_key", "saved-key"); err != nil {
t.Fatalf("Set: %v", err)
}
if err := cfg.Persist(); err != nil {
t.Fatalf("Persist: %v", err)
}
again, err := Open(Options{Dir: dir, Env: "development", Environ: []string{}})
if err != nil {
t.Fatalf("reopen: %v", err)
}
if got := again.String("push.public_key"); got != "saved-key" {
t.Fatalf("push.public_key = %q, want saved-key", got)
}
if got := again.String("app.name"); got != "persisted" {
t.Fatalf("app.name = %q, want the earlier persisted value", got)
}
// A runtime value replaces the persisted value of the same key.
if err := again.Set("app.name", "runtime"); err != nil {
t.Fatal(err)
}
if err := again.Persist(); err != nil {
t.Fatal(err)
}
third, err := Open(Options{Dir: dir, Env: "development", Environ: []string{}})
if err != nil {
t.Fatal(err)
}
if third.String("app.name") != "runtime" || third.String("push.public_key") != "saved-key" {
t.Fatalf("after second Persist: app.name=%q push.public_key=%q", third.String("app.name"), third.String("push.public_key"))
}
}