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:
@@ -101,6 +101,50 @@ func WithLock(lockPath string, fn func() error) error {
|
||||
return fn()
|
||||
}
|
||||
|
||||
// WriteFileAtomic writes content to a file atomically via temp + rename.
|
||||
// If the process crashes mid-write, the original file is untouched.
|
||||
func WriteFileAtomic(path string, content []byte, perm os.FileMode) error {
|
||||
dir := filepath.Dir(path)
|
||||
if err := EnsureDir(dir, 0755); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
tmp, err := os.CreateTemp(dir, filepath.Base(path)+".tmp.*")
|
||||
if err != nil {
|
||||
return fmt.Errorf("creating temp file for %s: %w", path, err)
|
||||
}
|
||||
tmpPath := tmp.Name()
|
||||
|
||||
if _, err := tmp.Write(content); err != nil {
|
||||
tmp.Close()
|
||||
os.Remove(tmpPath)
|
||||
return fmt.Errorf("writing temp file %s: %w", tmpPath, err)
|
||||
}
|
||||
if err := tmp.Chmod(perm); err != nil {
|
||||
tmp.Close()
|
||||
os.Remove(tmpPath)
|
||||
return fmt.Errorf("setting permissions on %s: %w", tmpPath, err)
|
||||
}
|
||||
if err := tmp.Close(); err != nil {
|
||||
os.Remove(tmpPath)
|
||||
return fmt.Errorf("closing temp file %s: %w", tmpPath, err)
|
||||
}
|
||||
if err := os.Rename(tmpPath, path); err != nil {
|
||||
os.Remove(tmpPath)
|
||||
return fmt.Errorf("installing %s → %s: %w", tmpPath, path, err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// CopyFile copies a file atomically. Used for creating .bak backups.
|
||||
func CopyFile(src, dst string) error {
|
||||
data, err := os.ReadFile(src)
|
||||
if err != nil {
|
||||
return fmt.Errorf("reading %s: %w", src, err)
|
||||
}
|
||||
return WriteFileAtomic(dst, data, 0644)
|
||||
}
|
||||
|
||||
// EnsureFilePermissions ensures a file has the correct permissions
|
||||
func EnsureFilePermissions(path string, perm os.FileMode) error {
|
||||
if err := os.Chmod(path, perm); err != nil {
|
||||
|
||||
Reference in New Issue
Block a user