Files
OrchestrAD/backend/internal/services/settings_resolver.go
Alphaeus Mote 5eae8cc102 feat(services): DB-over-env settings precedence resolver
Add SettingsService.ResolveString/Bool/Int resolving runtime config as
app_settings (UI) > environment variable > default. Env seeds bootstrap; any
value set in the UI persists to app_settings and wins. Foundation for OIDC and
TLS UI configuration. Unit-tested for the precedence order.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-09-02 10:56:06 -04:00

64 lines
1.9 KiB
Go

// Package services - settings precedence resolution.
//
// Runtime configuration resolves with a fixed precedence:
//
// app_settings (set via the UI/API) > environment variable > built-in default
//
// This lets environment variables seed a working configuration for bootstrap
// (containers, first run, the MSI), while any value an administrator sets in the
// UI is persisted to app_settings and wins from then on. Bootstrap-only settings
// that must be known before the database opens (data path, secret key, listen
// address/port) are NOT resolved here — they stay in package config.
package services
import (
"os"
"strconv"
"strings"
)
// ResolveString returns the effective string value for key: the app_settings
// value if one is set (non-empty), else the environment variable envVar if set,
// else def. A blank envVar skips the environment lookup.
func (s *SettingsService) ResolveString(key, envVar, def string) string {
if setting, err := s.Get(key); err == nil && setting != nil && setting.Value != "" {
return setting.Value
}
if envVar != "" {
if v := os.Getenv(envVar); v != "" {
return v
}
}
return def
}
// ResolveBool resolves a boolean setting with the same precedence. Recognized
// true values (case-insensitive): 1, t, true, yes, on.
func (s *SettingsService) ResolveBool(key, envVar string, def bool) bool {
raw := s.ResolveString(key, envVar, "")
if raw == "" {
return def
}
switch strings.ToLower(strings.TrimSpace(raw)) {
case "1", "t", "true", "yes", "on":
return true
case "0", "f", "false", "no", "off":
return false
default:
return def
}
}
// ResolveInt resolves an integer setting with the same precedence, falling back
// to def when unset or unparseable.
func (s *SettingsService) ResolveInt(key, envVar string, def int) int {
raw := s.ResolveString(key, envVar, "")
if raw == "" {
return def
}
if i, err := strconv.Atoi(strings.TrimSpace(raw)); err == nil {
return i
}
return def
}