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

@@ -9,6 +9,7 @@ import (
"os"
"path/filepath"
"strings"
"sync"
"time"
"github.com/wild-cloud/wild-central/internal/authelia"
@@ -69,6 +70,7 @@ type GenerateAutheliaConfigFn func(state *config.State) error
// Reconciler orchestrates config regeneration when domains change.
type Reconciler struct {
mu sync.Mutex // serializes concurrent Reconcile() calls
Domains DomainManager
HAProxy HAProxyManager
DNS DNSManager
@@ -81,8 +83,12 @@ type Reconciler struct {
}
// Reconcile reads all registered domains and regenerates dnsmasq DNS entries
// and HAProxy routes to match.
// and HAProxy routes to match. Serialized by mutex — concurrent calls wait
// rather than racing on config files.
func (r *Reconciler) Reconcile() {
r.mu.Lock()
defer r.mu.Unlock()
doms, err := r.Domains.List()
if err != nil {
slog.Error("failed to list domains", "component", "reconcile", "error", err)
@@ -91,8 +97,12 @@ func (r *Reconciler) Reconcile() {
globalCfg, err := config.LoadState(r.StatePath)
if err != nil {
slog.Warn("failed to load state, using empty", "component", "reconcile", "error", err)
globalCfg = &config.State{}
if os.IsNotExist(err) {
globalCfg = &config.State{} // first run, no state yet
} else {
slog.Error("failed to load state (refusing to reconcile with empty config)", "component", "reconcile", "error", err)
return
}
}
l4Routes, httpRoutes := buildRoutes(doms)