package main import ( "errors" "fmt" "net" "net/url" "os" "path/filepath" "strconv" "strings" "time" ) type Config struct { ListenAddress string DatabaseURL string CookieSecure bool TrustProxy bool SessionTTL time.Duration CredentialsDir string AdminUsername string AdminPasswordFile string BootstrapServerName string BootstrapEndpoint string BootstrapCredential string MetricsRetention time.Duration LogsRetention time.Duration IntegrationTokenFile string } func LoadConfig() (Config, error) { config := Config{ ListenAddress: envOr("LISTEN_ADDRESS", ":8080"), DatabaseURL: os.Getenv("DATABASE_URL"), CookieSecure: envBool("COOKIE_SECURE", true), TrustProxy: envBool("TRUST_PROXY", false), SessionTTL: envDuration("SESSION_TTL", 12*time.Hour), CredentialsDir: envOr("AGENT_CREDENTIALS_DIR", "/run/secrets"), AdminUsername: envOr("INITIAL_ADMIN_USERNAME", "admin"), AdminPasswordFile: os.Getenv("INITIAL_ADMIN_PASSWORD_FILE"), BootstrapServerName: os.Getenv("BOOTSTRAP_SERVER_NAME"), BootstrapEndpoint: os.Getenv("BOOTSTRAP_SERVER_ENDPOINT"), BootstrapCredential: os.Getenv("BOOTSTRAP_AGENT_CREDENTIAL_REF"), MetricsRetention: envDuration("METRICS_RETENTION", 30*24*time.Hour), LogsRetention: envDuration("LOGS_RETENTION", 7*24*time.Hour), IntegrationTokenFile: os.Getenv("INTEGRATION_TOKEN_FILE"), } if config.DatabaseURL == "" { config.DatabaseURL = buildDatabaseURL() } if config.DatabaseURL == "" { return Config{}, errors.New("DATABASE_URL or DB_HOST/DB_PASSWORD_FILE is required") } if config.SessionTTL < 5*time.Minute || config.SessionTTL > 7*24*time.Hour { return Config{}, errors.New("SESSION_TTL must be between 5m and 168h") } if config.MetricsRetention < 24*time.Hour { return Config{}, errors.New("METRICS_RETENTION must be at least 24h") } if config.LogsRetention < time.Hour || config.LogsRetention > 90*24*time.Hour { return Config{}, errors.New("LOGS_RETENTION must be between 1h and 2160h") } if config.BootstrapEndpoint != "" { if err := validateAgentEndpoint(config.BootstrapEndpoint); err != nil { return Config{}, fmt.Errorf("bootstrap endpoint: %w", err) } if !validCredentialRef(config.BootstrapCredential) { return Config{}, errors.New("invalid BOOTSTRAP_AGENT_CREDENTIAL_REF") } } return config, nil } func buildDatabaseURL() string { host := os.Getenv("DB_HOST") passwordFile := os.Getenv("DB_PASSWORD_FILE") if host == "" || passwordFile == "" { return "" } password, err := os.ReadFile(filepath.Clean(passwordFile)) if err != nil { return "" } user := envOr("DB_USER", "vps_control") database := envOr("DB_NAME", "vps_control") values := url.Values{"sslmode": {envOr("DB_SSLMODE", "disable")}} return (&url.URL{ Scheme: "postgres", User: url.UserPassword(user, strings.TrimSpace(string(password))), Host: net.JoinHostPort(host, envOr("DB_PORT", "5432")), Path: database, RawQuery: values.Encode(), }).String() } func envOr(name, fallback string) string { if value := os.Getenv(name); value != "" { return value } return fallback } func envBool(name string, fallback bool) bool { value := os.Getenv(name) if value == "" { return fallback } parsed, err := strconv.ParseBool(value) if err != nil { return fallback } return parsed } func envDuration(name string, fallback time.Duration) time.Duration { value := os.Getenv(name) if value == "" { return fallback } parsed, err := time.ParseDuration(value) if err != nil { return fallback } return parsed }