package config import ( "fmt" "github.com/joho/godotenv" "github.com/spf13/viper" ) type Config struct { Host string `mapstructure:"host"` Port int `mapstructure:"port"` LogLevel string `mapstructure:"log_level"` NetBoxURL string `mapstructure:"netbox_url"` NetBoxToken string `mapstructure:"netbox_token"` NetBoxTimeoutSeconds int `mapstructure:"netbox_timeout_seconds"` DragonflyURL string `mapstructure:"dragonfly_url"` WAQRetryIntervalSeconds int `mapstructure:"waq_retry_interval_seconds"` WAQMaxAttempts int `mapstructure:"waq_max_attempts"` QualityGateConfidenceThreshold float64 `mapstructure:"quality_gate_confidence_threshold"` } func Load() (*Config, error) { // Load .env file if present (ignore error — .env is optional in production) _ = godotenv.Load() v := viper.New() // Set defaults v.SetDefault("host", "0.0.0.0") v.SetDefault("port", 8080) v.SetDefault("log_level", "info") v.SetDefault("netbox_timeout_seconds", 10) v.SetDefault("waq_retry_interval_seconds", 30) v.SetDefault("waq_max_attempts", 5) v.SetDefault("quality_gate_confidence_threshold", 0.75) // Config file v.SetConfigName("config") v.SetConfigType("json") v.AddConfigPath(".") v.AddConfigPath("/etc/hwlab") // Environment variables: HWLAB_PORT -> port, HWLAB_NETBOX_URL -> netbox_url, etc. v.SetEnvPrefix("HWLAB") v.AutomaticEnv() // Explicit bindings ensure AutomaticEnv works correctly with mapstructure v2 unmarshalling. // Format: key name -> env var (without prefix, viper adds HWLAB_ automatically). _ = v.BindEnv("host", "HWLAB_HOST") _ = v.BindEnv("port", "HWLAB_PORT") _ = v.BindEnv("log_level", "HWLAB_LOG_LEVEL") _ = v.BindEnv("netbox_url", "HWLAB_NETBOX_URL") _ = v.BindEnv("netbox_token", "HWLAB_NETBOX_TOKEN") _ = v.BindEnv("netbox_timeout_seconds", "HWLAB_NETBOX_TIMEOUT_SECONDS") _ = v.BindEnv("dragonfly_url", "HWLAB_DRAGONFLY_URL") _ = v.BindEnv("waq_retry_interval_seconds", "HWLAB_WAQ_RETRY_INTERVAL_SECONDS") _ = v.BindEnv("waq_max_attempts", "HWLAB_WAQ_MAX_ATTEMPTS") _ = v.BindEnv("quality_gate_confidence_threshold", "HWLAB_QUALITY_GATE_CONFIDENCE_THRESHOLD") // Read config file (non-fatal if missing) if err := v.ReadInConfig(); err != nil { if _, ok := err.(viper.ConfigFileNotFoundError); !ok { return nil, fmt.Errorf("config file: %w", err) } } var cfg Config if err := v.Unmarshal(&cfg); err != nil { return nil, fmt.Errorf("unmarshal config: %w", err) } return &cfg, nil }