// Package config reads the service's settings from the environment. package config import ( "fmt" "os" "strings" "time" ) // Config is the whole configuration surface. Environment only: a control plane is deployed, not // hand-run, and a settings file is one more thing to keep in sync with the deployment. type Config struct { Addr string // DSN is the Postgres connection string. Empty is allowed and means "start unready": the // service answers liveness and reports on /readyz why it cannot serve. // // ⚠ It carries a password. It is read from the environment, never logged, and never echoed // into an error message. DSN string // TrustedOrigins are origins besides our own that may make unsafe requests. TrustedOrigins []string // SessionIdleTTL is how long a session survives without use; SessionMaxAge is the ceiling no // amount of use can extend. SessionIdleTTL time.Duration SessionMaxAge time.Duration // Migrate applies pending migrations at boot. Off by default: a rollout should migrate once, // deliberately, not once per replica. Migrate bool } // Load reads the environment. func Load() (Config, error) { c := Config{ Addr: env("TM_PLATFORM_ADDR", "127.0.0.1:8080"), DSN: os.Getenv("TM_PLATFORM_DSN"), SessionIdleTTL: 14 * 24 * time.Hour, SessionMaxAge: 90 * 24 * time.Hour, Migrate: os.Getenv("TM_PLATFORM_MIGRATE") == "1", } if raw := os.Getenv("TM_PLATFORM_TRUSTED_ORIGINS"); raw != "" { for _, o := range strings.Split(raw, ",") { if o = strings.TrimSpace(o); o != "" { c.TrustedOrigins = append(c.TrustedOrigins, o) } } } var err error if c.SessionIdleTTL, err = duration("TM_PLATFORM_SESSION_IDLE", c.SessionIdleTTL); err != nil { return Config{}, err } if c.SessionMaxAge, err = duration("TM_PLATFORM_SESSION_MAX_AGE", c.SessionMaxAge); err != nil { return Config{}, err } if c.SessionIdleTTL > c.SessionMaxAge { return Config{}, fmt.Errorf("config: session idle TTL %s exceeds max age %s", c.SessionIdleTTL, c.SessionMaxAge) } return c, nil } func env(key, def string) string { if v := os.Getenv(key); v != "" { return v } return def } func duration(key string, def time.Duration) (time.Duration, error) { raw := os.Getenv(key) if raw == "" { return def, nil } d, err := time.ParseDuration(raw) if err != nil { return 0, fmt.Errorf("config: %s: %w", key, err) } if d <= 0 { return 0, fmt.Errorf("config: %s must be positive", key) } return d, nil }