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:
@@ -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)
|
||||
|
||||
Reference in New Issue
Block a user