104 lines
2.7 KiB
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,
|
|
)
|
|
}
|