Files
traefik-dns-watcher/config.go
2026-03-15 21:48:31 +03:00

136 lines
3.6 KiB
Go

package main
import (
"fmt"
"os"
"strconv"
"strings"
"time"
)
// Config holds all runtime configuration loaded from environment variables.
type Config struct {
TraefikURL string
TraefikUsername string
TraefikPassword string
GitAuthUsername string
GitAuthToken string
Zones []string
PublicIP string
PublicIPv6 string // empty = no AAAA records
RepoPath string
RepoBranch string
RepoRemote string
DynamicDir string
AuthorName string
AuthorEmail string
ReconcileInterval time.Duration
DebounceDelay time.Duration
LogLevel string
RecordTTL int
CloudflareAutoTTL bool
ExcludeRouters map[string]struct{}
}
// LoadConfig reads configuration from environment variables and validates required fields.
func LoadConfig() (*Config, error) {
cfg := &Config{}
cfg.TraefikURL = envOrDefault("TRAEFIK_URL", "http://localhost:8080")
cfg.TraefikUsername = os.Getenv("TRAEFIK_USERNAME")
cfg.TraefikPassword = os.Getenv("TRAEFIK_PASSWORD")
// Optional HTTPS git authentication credentials.
// If GIT_AUTH_TOKEN is set, git commands will run non-interactively via GIT_ASKPASS.
cfg.GitAuthUsername = envOrDefault("GIT_AUTH_USERNAME", "x-access-token")
cfg.GitAuthToken = os.Getenv("GIT_AUTH_TOKEN")
zonesStr := os.Getenv("DNS_ZONES")
if zonesStr == "" {
return nil, fmt.Errorf("DNS_ZONES is required (comma-separated list of zones, e.g. example.com,example.net)")
}
for _, z := range strings.Split(zonesStr, ",") {
if z = strings.TrimSpace(z); z != "" {
cfg.Zones = append(cfg.Zones, z)
}
}
cfg.PublicIP = os.Getenv("PUBLIC_IP")
if cfg.PublicIP == "" {
return nil, fmt.Errorf("PUBLIC_IP is required")
}
cfg.PublicIPv6 = os.Getenv("PUBLIC_IPV6")
cfg.RepoPath = os.Getenv("DNS_REPO_PATH")
if cfg.RepoPath == "" {
return nil, fmt.Errorf("DNS_REPO_PATH is required")
}
cfg.RepoBranch = envOrDefault("DNS_REPO_BRANCH", "main")
cfg.RepoRemote = envOrDefault("DNS_REPO_REMOTE", "origin")
cfg.DynamicDir = envOrDefault("DNS_REPO_DYNAMIC_DIR", "zones-dynamic")
cfg.AuthorName = envOrDefault("DNS_REPO_AUTHOR_NAME", "traefik-dns-watcher")
cfg.AuthorEmail = envOrDefault("DNS_REPO_AUTHOR_EMAIL", "dns-bot@localhost")
var err error
cfg.ReconcileInterval, err = parseDurationEnv("RECONCILE_INTERVAL", "60s")
if err != nil {
return nil, err
}
cfg.DebounceDelay, err = parseDurationEnv("DEBOUNCE_DELAY", "5s")
if err != nil {
return nil, err
}
ttlStr := envOrDefault("RECORD_TTL", "300")
cfg.RecordTTL, err = strconv.Atoi(ttlStr)
if err != nil {
return nil, fmt.Errorf("RECORD_TTL: invalid integer %q: %w", ttlStr, err)
}
autoTTLStr := envOrDefault("CF_AUTO_TTL", "true")
cfg.CloudflareAutoTTL, err = strconv.ParseBool(autoTTLStr)
if err != nil {
return nil, fmt.Errorf("CF_AUTO_TTL: invalid boolean %q: %w", autoTTLStr, err)
}
cfg.LogLevel = strings.ToLower(envOrDefault("LOG_LEVEL", "info"))
switch cfg.LogLevel {
case "debug", "info", "warn", "error":
default:
return nil, fmt.Errorf("LOG_LEVEL: invalid value %q (allowed: debug, info, warn, error)", cfg.LogLevel)
}
cfg.ExcludeRouters = make(map[string]struct{})
if v := os.Getenv("EXCLUDE_ROUTERS"); v != "" {
for _, r := range strings.Split(v, ",") {
if r = strings.TrimSpace(r); r != "" {
cfg.ExcludeRouters[r] = struct{}{}
}
}
}
return cfg, nil
}
func envOrDefault(key, def string) string {
if v := os.Getenv(key); v != "" {
return v
}
return def
}
func parseDurationEnv(key, def string) (time.Duration, error) {
s := envOrDefault(key, def)
d, err := time.ParseDuration(s)
if err != nil {
return 0, fmt.Errorf("%s: invalid duration %q: %w", key, s, err)
}
return d, nil
}