// Package config loads mediamark runtime configuration from the environment. package config import ( "fmt" "os" "path/filepath" "strings" ) // Config is the fully-resolved mediamark configuration. type Config struct { // Listen is the HTTP listen address, e.g. ":8080". Listen string // MediaRoot holds the fafflix/ (source) and cheeztv/ (kids) library trees. MediaRoot string // KeysDir holds per-app *arr api keys as files named "sonarr"/"radarr", // projected by the Vault Secrets Operator and re-read on every use. KeysDir string // SonarrURL and RadarrURL include the *arr instance's UrlBase; the API // lives at /api/v3/... SonarrURL string RadarrURL string // GroupsHeader is the oauth2-proxy header carrying Authentik group names. GroupsHeader string // AllowedGroups gates every page load and API call. Never empty. AllowedGroups []string } // Load resolves configuration from the environment, failing closed on an empty // allow-list (an empty list would authorize nobody or, worse, be read as // "anyone" by a future refactor). func Load() (*Config, error) { c := &Config{ Listen: envOr("MEDIAMARK_LISTEN", ":8080"), MediaRoot: envOr("MEDIAMARK_MEDIA_ROOT", "/media"), KeysDir: envOr("MEDIAMARK_KEYS_DIR", "/etc/mediamark/keys"), SonarrURL: envOr("MEDIAMARK_SONARR_URL", "http://sonarr.arrstack.svc.cluster.local:8989/3aa168/sonarr"), RadarrURL: envOr("MEDIAMARK_RADARR_URL", "http://radarr.arrstack.svc.cluster.local:7878/3aa168/radarr"), GroupsHeader: envOr("MEDIAMARK_GROUPS_HEADER", "X-Forwarded-Groups"), AllowedGroups: ParseGroups(envOr("MEDIAMARK_ALLOWED_GROUPS", "akP-mediamark-user")), } if len(c.AllowedGroups) == 0 { return nil, fmt.Errorf("MEDIAMARK_ALLOWED_GROUPS must name at least one group") } if strings.TrimSpace(c.GroupsHeader) == "" { return nil, fmt.Errorf("MEDIAMARK_GROUPS_HEADER must not be empty") } if !filepath.IsAbs(c.MediaRoot) { return nil, fmt.Errorf("MEDIAMARK_MEDIA_ROOT %q must be an absolute path", c.MediaRoot) } c.MediaRoot = filepath.Clean(c.MediaRoot) for _, u := range []struct{ name, val string }{{"MEDIAMARK_SONARR_URL", c.SonarrURL}, {"MEDIAMARK_RADARR_URL", c.RadarrURL}} { if !strings.HasPrefix(u.val, "http://") && !strings.HasPrefix(u.val, "https://") { return nil, fmt.Errorf("%s %q must be an http(s) URL", u.name, u.val) } } return c, nil } // ParseGroups splits a group list tolerating both comma and whitespace // separation, dropping empties. oauth2-proxy emits comma-separated groups but // deployments hand-write the allow-list. func ParseGroups(s string) []string { fields := strings.FieldsFunc(s, func(r rune) bool { return r == ',' || r == ' ' || r == '\t' || r == '\n' || r == '\r' || r == ';' }) out := make([]string, 0, len(fields)) for _, f := range fields { if f = strings.TrimSpace(f); f != "" { out = append(out, f) } } return out } func envOr(key, def string) string { if v := os.Getenv(key); v != "" { return v } return def }