Files
traefik-dns-watcher/reconcile.go

104 lines
2.7 KiB
Go

package main
import (
"fmt"
"log/slog"
"sync"
)
// reconcileMu ensures only one reconcile runs at a time.
// TryLock is used so that concurrent triggers simply skip rather than queue.
var reconcileMu sync.Mutex
// Reconcile is the core reconciliation loop:
// 1. Fetches the active router list from Traefik (source of truth).
// 2. Pulls the DNS repository to ensure it is up to date.
// 3. Writes desired zone files for each managed zone.
// 4. Commits and pushes if anything changed.
//
// If Traefik is unreachable or git pull fails, the reconcile is aborted
// without modifying any DNS files (safe-fail behaviour).
func Reconcile(cfg *Config) {
if !reconcileMu.TryLock() {
slog.Info("reconcile already in progress, skipping")
return
}
defer reconcileMu.Unlock()
slog.Info("reconcile started")
routers, err := FetchRouters(cfg)
if err != nil {
slog.Error("failed to fetch Traefik routers — skipping reconcile to avoid stale DNS removal",
"error", err)
return
}
slog.Info("fetched routers from Traefik", "count", len(routers))
desired := FilterDomains(routers, cfg)
if err := GitPull(cfg); err != nil {
slog.Error("git pull failed — skipping reconcile", "error", err)
return
}
var total RecordStats
for zone, subdomains := range desired {
path := ZoneFilePath(cfg, zone)
current, err := ReadZoneFile(path)
if err != nil {
slog.Error("failed to read zone file", "zone", zone, "error", err)
return
}
newZF := BuildZoneFile(subdomains, cfg)
stats := DiffZoneFile(current, newZF)
total.Added += stats.Added
total.Removed += stats.Removed
total.Changed += stats.Changed
if err := WriteZoneFile(path, newZF); err != nil {
slog.Error("failed to write zone file", "zone", zone, "error", err)
return
}
if stats.Added+stats.Removed+stats.Changed > 0 {
slog.Info("zone updated",
"zone", zone,
"records", len(subdomains),
"added", stats.Added,
"removed", stats.Removed,
"changed", stats.Changed,
)
} else {
slog.Info("zone unchanged", "zone", zone, "records", len(subdomains))
}
}
changed, err := GitStatusChanged(cfg)
if err != nil {
slog.Error("git status check failed", "error", err)
return
}
if !changed {
slog.Info("reconcile complete — no dynamic DNS changes to commit")
return
}
msg := fmt.Sprintf(
"chore(dns): reconcile dynamic records from traefik\n\nadded: %d, removed: %d, updated: %d",
total.Added, total.Removed, total.Changed,
)
if err := GitCommitAndPush(cfg, msg); err != nil {
slog.Error("failed to commit/push DNS changes", "error", err)
return
}
slog.Info("reconcile complete — changes pushed",
"added", total.Added,
"removed", total.Removed,
"updated", total.Changed,
)
}