feat: traefik-selectel-dns — A-записи Selectel по роутам Traefik + CI сборка образа
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,135 @@
|
||||
// Команда 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))
|
||||
}
|
||||
Reference in New Issue
Block a user