Files
traefik-selectel/cmd/traefik-selectel-dns/main.go
T
vasyanskandClaude Sonnet 5 273cd54415
Build / Tests (push) Successful in 13s
Build / Build image (push) Failing after 15s
Build / Notify on failure (push) Failing after 0s
feat: traefik-selectel-dns — A-записи Selectel по роутам Traefik + CI сборка образа
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-21 21:50:07 +07:00

136 lines
3.9 KiB
Go

// Команда 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))
}