// Команда traefik-selectel-dns: создаёт A-записи в Selectel DNS для хостов из роутеров Traefik. package main import ( "context" "flag" "fmt" "log/slog" "os" "os/signal" "syscall" "time" "github.com/realmanual/traefik-selectel/internal/config" "github.com/realmanual/traefik-selectel/internal/dockerevents" "github.com/realmanual/traefik-selectel/internal/health" "github.com/realmanual/traefik-selectel/internal/reconciler" "github.com/realmanual/traefik-selectel/internal/selectel" "github.com/realmanual/traefik-selectel/internal/traefik" ) var version = "dev" const dockerDebounce = 5 * time.Second func main() { os.Exit(run()) } func run() int { configPath := flag.String("config", os.Getenv("TSD_CONFIG"), "путь к YAML-конфигу (или TSD_CONFIG)") healthcheck := flag.Bool("healthcheck", false, "проверить /healthz локального экземпляра и выйти (для HEALTHCHECK)") showVersion := flag.Bool("version", false, "показать версию") flag.Parse() if *showVersion { fmt.Println(version) return 0 } cfg, err := config.Load(*configPath, os.Getenv, config.OSReadFile) if err != nil { fmt.Fprintln(os.Stderr, "ошибка конфигурации:", err) return 2 } if *healthcheck { if err := health.Probe(cfg.Listen, 3*time.Second); err != nil { fmt.Fprintln(os.Stderr, err) return 1 } return 0 } log := newLogger(cfg) log.Info("запуск", "version", version, "zones", cfg.Zones, "ip", cfg.IP, "ttl", cfg.TTL, "interval", cfg.Interval.String(), "deleteEnabled", cfg.Delete.Enabled, "dryRun", cfg.DryRun, "traefikAPI", cfg.Traefik.APIURL, "dockerEvents", cfg.Docker.Enabled) if cfg.DryRun { log.Warn("включён dry-run: изменения в DNS не выполняются") } tokens, err := selectel.NewIAMTokenSource(cfg.Credentials, cfg.Selectel.AuthURL, cfg.Selectel.Timeout) if err != nil { log.Error("инициализация Selectel", "error", err) return 2 } dns := selectel.NewClient(cfg.Selectel.BaseURL, tokens, cfg.Selectel.Timeout) tr, err := traefik.NewClient(cfg.Traefik.APIURL, cfg.Traefik.Timeout) if err != nil { log.Error("инициализация Traefik-клиента", "error", err) return 2 } rec := reconciler.New(reconciler.Config{ Zones: cfg.Zones, IP: cfg.IP, TTL: cfg.TTL, Comment: config.ManagedComment, DryRun: cfg.DryRun, DeleteEnabled: cfg.Delete.Enabled, GracePeriod: cfg.Delete.GracePeriod, }, tr, dns, log) ctx, stop := signal.NotifyContext(context.Background(), syscall.SIGTERM, syscall.SIGINT) defer stop() state := health.NewState(3*cfg.Interval + reconciler.CycleTimeout) healthErr := make(chan error, 1) go func() { healthErr <- health.Serve(ctx, cfg.Listen, state.Handler()) }() trigger := make(chan struct{}, 1) if cfg.Docker.Enabled { w := dockerevents.New(cfg.Docker.Host, log, func() { select { case trigger <- struct{}{}: default: } }) go w.Run(ctx) } loopDone := make(chan struct{}) go func() { defer close(loopDone) rec.Run(ctx, cfg.Interval, dockerDebounce, trigger, func(_ reconciler.Result, err error) { state.Record(err) }) }() select { case <-ctx.Done(): log.Info("получен сигнал завершения, останавливаемся") case err := <-healthErr: if err != nil { log.Error("health-сервер остановился", "error", err) stop() <-loopDone return 1 } } <-loopDone log.Info("остановлено") return 0 } func newLogger(cfg config.Config) *slog.Logger { var lvl slog.Level switch cfg.LogLevel { case "debug": lvl = slog.LevelDebug case "warn": lvl = slog.LevelWarn case "error": lvl = slog.LevelError default: lvl = slog.LevelInfo } opts := &slog.HandlerOptions{Level: lvl} if cfg.LogFormat == "text" { return slog.New(slog.NewTextHandler(os.Stderr, opts)) } return slog.New(slog.NewJSONHandler(os.Stderr, opts)) }