// Package config loads the master's configuration from environment variables. package config import ( "fmt" "os" "strings" "time" ) // Config holds the master's runtime configuration. type Config struct { DatabaseURL string PublicURL string // where people reach the UI, without trailing slash AgentURL string // what agents dial, shown in the add-agent snippet AgentImage string OIDCIssuer string OIDCClientID string OIDCClientSecret string AllowedGroup string Port string AgentPort string LogLevel string // UpdaterAPIKey is the bearer token for POST /update (UPDATER_API_KEY). // Empty disables the webhook. UpdaterAPIKey string // UpdateTimeout bounds how long one webhook request waits for its auto // jobs (UPDATE_TIMEOUT, default 5m). UpdateTimeout time.Duration // PollInterval is the time between registry polling cycles // (POLL_INTERVAL, default 6h). PollInterval time.Duration } // defaultUpdateTimeout is used when UPDATE_TIMEOUT is unset. const defaultUpdateTimeout = 5 * time.Minute // defaultPollInterval is used when POLL_INTERVAL is unset. const defaultPollInterval = 6 * time.Hour // Load reads and validates the master configuration, failing fast on // anything missing so a half-configured master never starts. func Load() (*Config, error) { cfg := &Config{ DatabaseURL: os.Getenv("DATABASE_URL"), PublicURL: strings.TrimSuffix(os.Getenv("PUBLIC_URL"), "/"), AgentURL: os.Getenv("AGENT_URL"), AgentImage: getenvDefault("AGENT_IMAGE", "gitea.shcizo.se/shcizo/package-updater:latest"), OIDCIssuer: os.Getenv("OIDC_ISSUER"), OIDCClientID: os.Getenv("OIDC_CLIENT_ID"), OIDCClientSecret: os.Getenv("OIDC_CLIENT_SECRET"), AllowedGroup: os.Getenv("ALLOWED_GROUP"), Port: getenvDefault("PORT", "8080"), AgentPort: getenvDefault("AGENT_PORT", "8090"), LogLevel: getenvDefault("LOG_LEVEL", "info"), UpdaterAPIKey: os.Getenv("UPDATER_API_KEY"), UpdateTimeout: defaultUpdateTimeout, PollInterval: defaultPollInterval, } for _, r := range []struct{ name, value string }{ {"DATABASE_URL", cfg.DatabaseURL}, {"PUBLIC_URL", cfg.PublicURL}, {"AGENT_URL", cfg.AgentURL}, {"OIDC_ISSUER", cfg.OIDCIssuer}, {"OIDC_CLIENT_ID", cfg.OIDCClientID}, {"OIDC_CLIENT_SECRET", cfg.OIDCClientSecret}, {"ALLOWED_GROUP", cfg.AllowedGroup}, } { if r.value == "" { return nil, fmt.Errorf("%s is required", r.name) } } if !strings.HasPrefix(cfg.PublicURL, "http://") && !strings.HasPrefix(cfg.PublicURL, "https://") { return nil, fmt.Errorf("PUBLIC_URL must start with http:// or https://") } if !strings.HasPrefix(cfg.AgentURL, "wss://") { return nil, fmt.Errorf("AGENT_URL must start with wss://") } if v := os.Getenv("UPDATE_TIMEOUT"); v != "" { d, err := time.ParseDuration(v) if err != nil || d <= 0 { return nil, fmt.Errorf("UPDATE_TIMEOUT must be a positive duration such as 5m, got %q", v) } cfg.UpdateTimeout = d } if v := os.Getenv("POLL_INTERVAL"); v != "" { d, err := time.ParseDuration(v) if err != nil || d <= 0 { return nil, fmt.Errorf("POLL_INTERVAL must be a positive duration such as 6h, got %q", v) } cfg.PollInterval = d } return cfg, nil } func getenvDefault(key, fallback string) string { if v := os.Getenv(key); v != "" { return v } return fallback }