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 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 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") 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.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 }