mirror of
https://github.com/tiennm99/traefik-cloudflare-dns.git
synced 2026-10-11 03:13:52 +00:00
Create a record for each Host(...) in running containers' Traefik router labels, tagged with a configurable comment and optional tags, and delete owned records once their host has been down for DELETE_AFTER.
124 lines
3.1 KiB
Go
124 lines
3.1 KiB
Go
// Command traefik-cloudflare-dns creates Cloudflare DNS records for the
|
|
// hostnames in Traefik router labels of running Docker containers, and
|
|
// deletes the records it created once their host has been down long enough.
|
|
package main
|
|
|
|
import (
|
|
"context"
|
|
"log/slog"
|
|
"os"
|
|
"os/signal"
|
|
"syscall"
|
|
"time"
|
|
)
|
|
|
|
const (
|
|
debounce = 2 * time.Second
|
|
retryAfter = 10 * time.Second
|
|
)
|
|
|
|
func main() {
|
|
cfg, err := LoadConfig(osGetenv)
|
|
if err != nil {
|
|
slog.Error("invalid configuration", "err", err)
|
|
os.Exit(2)
|
|
}
|
|
log := slog.New(slog.NewTextHandler(os.Stdout, &slog.HandlerOptions{Level: parseLevel(cfg.LogLevel)}))
|
|
|
|
docker, err := NewDockerClient(cfg.DockerHost)
|
|
if err != nil {
|
|
log.Error("invalid configuration", "err", err)
|
|
os.Exit(2)
|
|
}
|
|
rec := NewReconciler(cfg, NewCloudflareClient(cfg.APIToken, cfg.ZoneID), docker, log)
|
|
|
|
ctx, stop := signal.NotifyContext(context.Background(), syscall.SIGINT, syscall.SIGTERM)
|
|
defer stop()
|
|
|
|
log.Info("starting",
|
|
"domain", cfg.Domain, "target", cfg.Target, "type", cfg.RecordType,
|
|
"proxied", cfg.Proxied, "comment", cfg.Comment, "tags", cfg.Tags,
|
|
"delete_after", cfg.DeleteAfter, "resync_interval", cfg.ResyncInterval,
|
|
"dry_run", cfg.DryRun, "docker_host", cfg.DockerHost)
|
|
|
|
trigger := make(chan struct{}, 1)
|
|
go watchEvents(ctx, docker, trigger, log)
|
|
run(ctx, rec, trigger, cfg.ResyncInterval, log)
|
|
log.Info("stopped")
|
|
}
|
|
|
|
// run reconciles once at start, then after each burst of container events,
|
|
// on every resync tick, and shortly after a failed pass, until ctx is done.
|
|
func run(ctx context.Context, rec *Reconciler, trigger <-chan struct{}, every time.Duration, log *slog.Logger) {
|
|
var retry <-chan time.Time
|
|
pass := func() {
|
|
retry = nil
|
|
if err := rec.Reconcile(ctx); err != nil && ctx.Err() == nil {
|
|
log.Error("reconcile failed; retrying", "err", err, "in", retryAfter)
|
|
retry = time.After(retryAfter)
|
|
}
|
|
}
|
|
pass()
|
|
ticker := time.NewTicker(every)
|
|
defer ticker.Stop()
|
|
for {
|
|
select {
|
|
case <-ctx.Done():
|
|
return
|
|
case <-ticker.C:
|
|
pass()
|
|
case <-retry:
|
|
pass()
|
|
case <-trigger:
|
|
select {
|
|
case <-ctx.Done():
|
|
return
|
|
case <-time.After(debounce):
|
|
}
|
|
pass()
|
|
}
|
|
}
|
|
}
|
|
|
|
// watchEvents keeps a Docker event stream open and signals trigger on every
|
|
// container start, die or destroy, reconnecting with backoff when it drops.
|
|
func watchEvents(ctx context.Context, docker *DockerClient, trigger chan<- struct{}, log *slog.Logger) {
|
|
backoff := time.Second
|
|
for ctx.Err() == nil {
|
|
started := time.Now()
|
|
err := docker.WatchContainerEvents(ctx, func(action string) {
|
|
log.Debug("container event", "action", action)
|
|
select {
|
|
case trigger <- struct{}{}:
|
|
default:
|
|
}
|
|
})
|
|
if ctx.Err() != nil {
|
|
return
|
|
}
|
|
if time.Since(started) > time.Minute {
|
|
backoff = time.Second
|
|
}
|
|
log.Warn("docker event stream ended; reconnecting", "err", err, "in", backoff)
|
|
select {
|
|
case <-ctx.Done():
|
|
return
|
|
case <-time.After(backoff):
|
|
}
|
|
backoff = min(backoff*2, time.Minute)
|
|
}
|
|
}
|
|
|
|
func parseLevel(s string) slog.Level {
|
|
switch s {
|
|
case "debug":
|
|
return slog.LevelDebug
|
|
case "warn":
|
|
return slog.LevelWarn
|
|
case "error":
|
|
return slog.LevelError
|
|
default:
|
|
return slog.LevelInfo
|
|
}
|
|
}
|