homelabby/internal/config/config.go
Mikkel Georgsen 6595e345a2 feat(01-foundation-01): viper config loader and SPA fallback fix
- Add internal/config/config.go with viper-backed Config struct
- Explicit BindEnv calls for reliable env var -> config mapping (mapstructure v2 compat)
- Config loads from config.json + .env, env vars take precedence
- Add config.json with non-secret defaults (port, timeouts, URLs)
- Fix SPA fallback: spaHandler serves index.html for unknown paths (client-side routing)
- All 5 tests pass: TestHealth, TestLoadDefaults, TestLoadEnvOverride, TestLoadNetBoxURL
- Add Makefile with build/dev/test/clean targets
2026-04-10 01:27:54 +00:00

77 lines
2.5 KiB
Go

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
}