package config import ( "fmt" "log/slog" "os" "strconv" "time" ) // Config represents the complete application configuration. // All configuration values are read from environment variables with CERTCTL_ prefix. type Config struct { Server ServerConfig Database DatabaseConfig Scheduler SchedulerConfig Log LogConfig Auth AuthConfig } // ServerConfig contains HTTP server configuration. type ServerConfig struct { Host string Port int } // DatabaseConfig contains database connection configuration. type DatabaseConfig struct { URL string MaxConnections int MigrationsPath string } // SchedulerConfig contains scheduler timing configuration. type SchedulerConfig struct { RenewalCheckInterval time.Duration JobProcessorInterval time.Duration AgentHealthCheckInterval time.Duration NotificationProcessInterval time.Duration } // LogConfig contains logging configuration. type LogConfig struct { Level string // "debug", "info", "warn", "error" Format string // "json" or "text" } // AuthConfig contains authentication configuration. type AuthConfig struct { Type string // "api-key", "jwt", "none" Secret string // Secret key for signing (if applicable) } // Load reads configuration from environment variables and returns a Config. // Environment variables must have the CERTCTL_ prefix. // Example: CERTCTL_SERVER_HOST, CERTCTL_DATABASE_URL, etc. func Load() (*Config, error) { cfg := &Config{ Server: ServerConfig{ Host: getEnv("CERTCTL_SERVER_HOST", "127.0.0.1"), Port: getEnvInt("CERTCTL_SERVER_PORT", 8080), }, Database: DatabaseConfig{ URL: getEnv("CERTCTL_DATABASE_URL", "postgres://localhost/certctl"), MaxConnections: getEnvInt("CERTCTL_DATABASE_MAX_CONNS", 25), MigrationsPath: getEnv("CERTCTL_DATABASE_MIGRATIONS_PATH", "./migrations"), }, Scheduler: SchedulerConfig{ RenewalCheckInterval: getEnvDuration("CERTCTL_SCHEDULER_RENEWAL_CHECK_INTERVAL", 1*time.Hour), JobProcessorInterval: getEnvDuration("CERTCTL_SCHEDULER_JOB_PROCESSOR_INTERVAL", 30*time.Second), AgentHealthCheckInterval: getEnvDuration("CERTCTL_SCHEDULER_AGENT_HEALTH_CHECK_INTERVAL", 2*time.Minute), NotificationProcessInterval: getEnvDuration("CERTCTL_SCHEDULER_NOTIFICATION_PROCESS_INTERVAL", 1*time.Minute), }, Log: LogConfig{ Level: getEnv("CERTCTL_LOG_LEVEL", "info"), Format: getEnv("CERTCTL_LOG_FORMAT", "json"), }, Auth: AuthConfig{ Type: getEnv("CERTCTL_AUTH_TYPE", "api-key"), Secret: getEnv("CERTCTL_AUTH_SECRET", ""), }, } if err := cfg.Validate(); err != nil { return nil, err } return cfg, nil } // Validate checks that the configuration is valid. func (c *Config) Validate() error { // Validate server configuration if c.Server.Port < 1 || c.Server.Port > 65535 { return fmt.Errorf("invalid server port: %d", c.Server.Port) } // Validate database configuration if c.Database.URL == "" { return fmt.Errorf("database URL is required") } if c.Database.MaxConnections < 1 { return fmt.Errorf("database max_connections must be at least 1") } // Validate log level validLogLevels := map[string]bool{ "debug": true, "info": true, "warn": true, "error": true, } if !validLogLevels[c.Log.Level] { return fmt.Errorf("invalid log level: %s", c.Log.Level) } // Validate log format validFormats := map[string]bool{ "json": true, "text": true, } if !validFormats[c.Log.Format] { return fmt.Errorf("invalid log format: %s", c.Log.Format) } // Validate auth type validAuthTypes := map[string]bool{ "api-key": true, "jwt": true, "none": true, } if !validAuthTypes[c.Auth.Type] { return fmt.Errorf("invalid auth type: %s", c.Auth.Type) } // If using JWT or API-key, secret is required if (c.Auth.Type == "jwt" || c.Auth.Type == "api-key") && c.Auth.Secret == "" { return fmt.Errorf("auth secret is required for auth type %s", c.Auth.Type) } // Validate scheduler intervals if c.Scheduler.RenewalCheckInterval < 1*time.Minute { return fmt.Errorf("renewal check interval must be at least 1 minute") } if c.Scheduler.JobProcessorInterval < 1*time.Second { return fmt.Errorf("job processor interval must be at least 1 second") } if c.Scheduler.AgentHealthCheckInterval < 1*time.Second { return fmt.Errorf("agent health check interval must be at least 1 second") } if c.Scheduler.NotificationProcessInterval < 1*time.Second { return fmt.Errorf("notification process interval must be at least 1 second") } return nil } // getEnv reads a string environment variable with the given key and default value. func getEnv(key, defaultValue string) string { if value := os.Getenv(key); value != "" { return value } return defaultValue } // getEnvInt reads an integer environment variable with the given key and default value. func getEnvInt(key string, defaultValue int) int { if value := os.Getenv(key); value != "" { intVal, err := strconv.Atoi(value) if err != nil { return defaultValue } return intVal } return defaultValue } // getEnvDuration reads a time.Duration environment variable. // The value should be a valid Go duration string (e.g., "1h", "30s", "5m"). func getEnvDuration(key string, defaultValue time.Duration) time.Duration { if value := os.Getenv(key); value != "" { duration, err := time.ParseDuration(value) if err != nil { return defaultValue } return duration } return defaultValue } // GetLogLevel returns the appropriate slog.Level from the configured log level. func (c *Config) GetLogLevel() slog.Level { switch c.Log.Level { case "debug": return slog.LevelDebug case "info": return slog.LevelInfo case "warn": return slog.LevelWarn case "error": return slog.LevelError default: return slog.LevelInfo } }