Files
dockwatch/internal/config/config.go
T
jbergner 45ca18b74e
release-tag / release-image (push) Failing after 1m20s
init
2026-08-31 17:09:21 +02:00

149 lines
5.0 KiB
Go

package config
import (
"crypto/sha256"
"encoding/base64"
"errors"
"fmt"
"net/url"
"os"
"strconv"
"strings"
"time"
)
type Mode string
const (
ModeStandalone Mode = "standalone"
ModeMaster Mode = "master"
ModeAgent Mode = "agent"
)
type Config struct {
Mode Mode
ListenAddr, BaseURL, DataDir, StacksDir, AppSecret string
SecureCookies, AuthDisabled bool
OIDCIssuer, OIDCClientID, OIDCClientSecret, OIDCRedirectURL, OIDCAdminGroup, OIDCOperatorGroup string
AgentToken string
CheckConcurrency, RetentionDays, AuditRetentionDays int
HTTPTimeout time.Duration
}
func Load() (Config, error) {
checkConcurrency, err := envIntStrict("CHECK_CONCURRENCY", 8)
if err != nil {
return Config{}, err
}
retentionDays, err := envIntStrict("CHECK_RETENTION_DAYS", 30)
if err != nil {
return Config{}, err
}
auditRetentionDays, err := envIntStrict("AUDIT_RETENTION_DAYS", 180)
if err != nil {
return Config{}, err
}
httpTimeoutSeconds, err := envIntStrict("HTTP_TIMEOUT_SECONDS", 10)
if err != nil {
return Config{}, err
}
authDisabled, err := envBoolStrict("AUTH_DISABLED", false)
if err != nil {
return Config{}, err
}
c := Config{
Mode: Mode(env("APP_MODE", "standalone")),
ListenAddr: env("LISTEN_ADDR", ":8080"),
BaseURL: strings.TrimRight(env("BASE_URL", "http://localhost:8080"), "/"),
DataDir: env("DATA_DIR", "/data"),
StacksDir: env("STACKS_DIR", "/stacks"),
AppSecret: os.Getenv("APP_SECRET"),
AuthDisabled: authDisabled,
OIDCIssuer: strings.TrimRight(os.Getenv("OIDC_ISSUER"), "/"),
OIDCClientID: os.Getenv("OIDC_CLIENT_ID"),
OIDCClientSecret: os.Getenv("OIDC_CLIENT_SECRET"),
OIDCRedirectURL: os.Getenv("OIDC_REDIRECT_URL"),
OIDCAdminGroup: env("OIDC_ADMIN_GROUP", "dockwatch-admins"),
OIDCOperatorGroup: env("OIDC_OPERATOR_GROUP", "dockwatch-operators"),
AgentToken: os.Getenv("AGENT_TOKEN"),
CheckConcurrency: checkConcurrency,
RetentionDays: retentionDays,
AuditRetentionDays: auditRetentionDays,
HTTPTimeout: time.Duration(httpTimeoutSeconds) * time.Second,
}
c.SecureCookies = strings.HasPrefix(c.BaseURL, "https://")
if c.OIDCRedirectURL == "" {
c.OIDCRedirectURL = c.BaseURL + "/auth/callback"
}
switch c.Mode {
case ModeStandalone, ModeMaster, ModeAgent:
default:
return c, fmt.Errorf("APP_MODE must be standalone, master, or agent")
}
if c.CheckConcurrency < 1 || c.CheckConcurrency > 128 {
return c, fmt.Errorf("CHECK_CONCURRENCY must be between 1 and 128")
}
if c.RetentionDays < 0 || c.RetentionDays > 3650 {
return c, fmt.Errorf("CHECK_RETENTION_DAYS must be between 0 and 3650")
}
if c.AuditRetentionDays < 0 || c.AuditRetentionDays > 3650 {
return c, fmt.Errorf("AUDIT_RETENTION_DAYS must be between 0 and 3650")
}
if c.HTTPTimeout < time.Second || c.HTTPTimeout > 5*time.Minute {
return c, fmt.Errorf("HTTP_TIMEOUT_SECONDS must be between 1 and 300")
}
if c.Mode != ModeAgent {
u, err := url.Parse(c.BaseURL)
if err != nil || (u.Scheme != "http" && u.Scheme != "https") || u.Host == "" || u.User != nil || u.RawQuery != "" || u.Fragment != "" {
return c, errors.New("BASE_URL must be an absolute http(s) URL without credentials, query or fragment")
}
}
if c.Mode == ModeAgent {
if len(c.AgentToken) < 24 {
return c, errors.New("AGENT_TOKEN must be at least 24 characters in agent mode")
}
return c, nil
}
if len(c.AppSecret) < 32 {
return c, errors.New("APP_SECRET must be at least 32 characters")
}
if !c.AuthDisabled && (c.OIDCIssuer == "" || c.OIDCClientID == "" || c.OIDCClientSecret == "") {
return c, errors.New("OIDC_ISSUER, OIDC_CLIENT_ID and OIDC_CLIENT_SECRET are required unless AUTH_DISABLED=true")
}
return c, nil
}
func (c Config) DBPath() string { return c.DataDir + "/dockwatch.db" }
func (c Config) EncryptionKey() []byte { s := sha256.Sum256([]byte(c.AppSecret)); return s[:] }
func (c Config) SecretFingerprint() string {
h := sha256.Sum256([]byte(c.AppSecret))
return base64.RawURLEncoding.EncodeToString(h[:6])
}
func env(k, f string) string {
if v := os.Getenv(k); v != "" {
return v
}
return f
}
func envIntStrict(k string, f int) (int, error) {
v := strings.TrimSpace(os.Getenv(k))
if v == "" {
return f, nil
}
n, err := strconv.Atoi(v)
if err != nil {
return 0, fmt.Errorf("%s must be an integer: %w", k, err)
}
return n, nil
}
func envBoolStrict(k string, f bool) (bool, error) {
v := strings.TrimSpace(os.Getenv(k))
if v == "" {
return f, nil
}
b, err := strconv.ParseBool(v)
if err != nil {
return false, fmt.Errorf("%s must be a boolean: %w", k, err)
}
return b, nil
}