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 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, ) }