Add resiliency primitives: atomic writes, reconcile mutex, state backup

- Add storage.WriteFileAtomic (temp + rename) and storage.CopyFile helpers
- Convert all 8 production config writers to atomic writes: config/state.yaml,
  dnsmasq, nftables, domains, wireguard (config + secrets + peers + wg0.conf),
  tunnel/cloudflared
- Add sync.Mutex to Reconciler to serialize concurrent Reconcile() calls
  triggered by domain registration goroutines
- Add state.yaml backup (.bak) before every write; LoadState falls back to
  backup if primary is corrupted
- Reconciler refuses to use empty config on corruption (only on first run
  when no state file exists yet)
This commit is contained in:
2026-07-14 11:37:33 +00:00
parent 3172e56288
commit 88acd437a1
9 changed files with 110 additions and 27 deletions

View File

@@ -11,6 +11,7 @@ import (
"github.com/wild-cloud/wild-central/internal/config"
"github.com/wild-cloud/wild-central/internal/network"
"github.com/wild-cloud/wild-central/internal/storage"
)
// DNSEntry represents a single domain-to-IP DNS mapping for dnsmasq.
@@ -220,7 +221,7 @@ func (g *Manager) UpdateConfig(cfg *config.State, entries []DNSEntry, restart bo
// Write config
slog.Info("writing dnsmasq config", "component", "dnsmasq", "path", g.configPath)
if err := os.WriteFile(g.configPath, []byte(configContent), 0644); err != nil {
if err := storage.WriteFileAtomic(g.configPath, []byte(configContent), 0644); err != nil {
return fmt.Errorf("writing dnsmasq config: %w", err)
}