soteria/internal/config/config.go

228 lines
6.3 KiB
Go

package config
import (
"errors"
"os"
"strconv"
"strings"
"time"
)
const (
defaultBackupDriver = "longhorn"
defaultResticImage = "restic/restic:0.16.4"
defaultJobTTLSeconds = 86400
defaultListenAddr = ":8080"
defaultResticSecret = "soteria-restic"
defaultLonghornURL = "http://longhorn-backend.longhorn-system.svc:9500"
defaultLonghornMode = "incremental"
defaultAllowedGroups = "admin,maintenance"
defaultMetricsRefresh = 300 * time.Second
defaultBackupMaxAge = 24 * time.Hour
serviceNamespacePath = "/var/run/secrets/kubernetes.io/serviceaccount/namespace"
)
type Config struct {
Namespace string
SecretNamespace string
BackupDriver string
ResticImage string
ResticRepository string
ResticSecretName string
ResticBackupArgs []string
ResticForgetArgs []string
S3Endpoint string
S3Region string
JobTTLSeconds int32
JobNodeSelector map[string]string
WorkerServiceAccount string
ListenAddr string
LonghornURL string
LonghornBackupMode string
AuthRequired bool
AllowedGroups []string
AuthBearerTokens []string
MetricsRefreshInterval time.Duration
BackupMaxAge time.Duration
}
func Load() (*Config, error) {
cfg := &Config{}
cfg.Namespace = getenv("SOTERIA_NAMESPACE")
if cfg.Namespace == "" {
ns, _ := os.ReadFile(serviceNamespacePath)
cfg.Namespace = strings.TrimSpace(string(ns))
}
if cfg.Namespace == "" {
cfg.Namespace = "soteria"
}
cfg.SecretNamespace = getenv("SOTERIA_SECRET_NAMESPACE")
if cfg.SecretNamespace == "" {
cfg.SecretNamespace = cfg.Namespace
}
cfg.BackupDriver = getenvDefault("SOTERIA_BACKUP_DRIVER", defaultBackupDriver)
cfg.ResticImage = getenvDefault("SOTERIA_RESTIC_IMAGE", defaultResticImage)
cfg.ResticRepository = getenv("SOTERIA_RESTIC_REPOSITORY")
cfg.ResticSecretName = getenvDefault("SOTERIA_RESTIC_SECRET_NAME", defaultResticSecret)
cfg.ResticBackupArgs = strings.Fields(getenv("SOTERIA_RESTIC_BACKUP_ARGS"))
cfg.ResticForgetArgs = strings.Fields(getenv("SOTERIA_RESTIC_FORGET_ARGS"))
cfg.S3Endpoint = getenv("SOTERIA_S3_ENDPOINT")
cfg.S3Region = getenv("SOTERIA_S3_REGION")
cfg.WorkerServiceAccount = getenv("SOTERIA_JOB_SERVICE_ACCOUNT")
cfg.ListenAddr = getenvDefault("SOTERIA_LISTEN_ADDR", defaultListenAddr)
cfg.JobNodeSelector = parseNodeSelector(getenv("SOTERIA_JOB_NODE_SELECTOR"))
cfg.LonghornURL = getenvDefault("SOTERIA_LONGHORN_URL", defaultLonghornURL)
cfg.LonghornBackupMode = getenvDefault("SOTERIA_LONGHORN_BACKUP_MODE", defaultLonghornMode)
cfg.AuthRequired = getenvBool("SOTERIA_AUTH_REQUIRED")
cfg.AllowedGroups = parseCSV(getenvDefault("SOTERIA_ALLOWED_GROUPS", defaultAllowedGroups))
cfg.AuthBearerTokens = parseCSV(getenv("SOTERIA_AUTH_BEARER_TOKENS"))
cfg.MetricsRefreshInterval = defaultMetricsRefresh
cfg.BackupMaxAge = defaultBackupMaxAge
if ttl, ok := getenvInt("SOTERIA_JOB_TTL_SECONDS"); ok {
cfg.JobTTLSeconds = int32(ttl)
} else {
cfg.JobTTLSeconds = defaultJobTTLSeconds
}
if seconds, ok := getenvInt("SOTERIA_METRICS_REFRESH_SECONDS"); ok {
cfg.MetricsRefreshInterval = time.Duration(seconds) * time.Second
}
if hours, ok := getenvFloat("SOTERIA_BACKUP_MAX_AGE_HOURS"); ok {
cfg.BackupMaxAge = time.Duration(hours * float64(time.Hour))
}
if cfg.ResticRepository == "" {
if cfg.BackupDriver == "restic" {
return nil, errors.New("SOTERIA_RESTIC_REPOSITORY is required for restic driver")
}
}
if cfg.ResticSecretName == "" {
if cfg.BackupDriver == "restic" {
return nil, errors.New("SOTERIA_RESTIC_SECRET_NAME is required for restic driver")
}
}
if strings.Contains(cfg.ResticRepository, "..") {
if cfg.BackupDriver == "restic" {
return nil, errors.New("SOTERIA_RESTIC_REPOSITORY contains invalid path segments")
}
}
if cfg.JobNodeSelector == nil {
return nil, errors.New("SOTERIA_JOB_NODE_SELECTOR is invalid; expected key=value pairs")
}
if cfg.BackupDriver != "longhorn" && cfg.BackupDriver != "restic" {
return nil, errors.New("SOTERIA_BACKUP_DRIVER must be longhorn or restic")
}
if cfg.BackupDriver == "longhorn" && cfg.LonghornURL == "" {
return nil, errors.New("SOTERIA_LONGHORN_URL is required for longhorn driver")
}
if cfg.BackupDriver == "longhorn" && cfg.LonghornBackupMode != "" {
switch cfg.LonghornBackupMode {
case "incremental", "full":
default:
return nil, errors.New("SOTERIA_LONGHORN_BACKUP_MODE must be incremental or full")
}
}
if cfg.MetricsRefreshInterval <= 0 {
return nil, errors.New("SOTERIA_METRICS_REFRESH_SECONDS must be greater than zero")
}
if cfg.BackupMaxAge <= 0 {
return nil, errors.New("SOTERIA_BACKUP_MAX_AGE_HOURS must be greater than zero")
}
return cfg, nil
}
func getenv(key string) string {
return strings.TrimSpace(os.Getenv(key))
}
func getenvDefault(key, value string) string {
if v := getenv(key); v != "" {
return v
}
return value
}
func getenvInt(key string) (int, bool) {
val := getenv(key)
if val == "" {
return 0, false
}
parsed, err := strconv.Atoi(val)
if err != nil {
return 0, false
}
return parsed, true
}
func getenvFloat(key string) (float64, bool) {
val := getenv(key)
if val == "" {
return 0, false
}
parsed, err := strconv.ParseFloat(val, 64)
if err != nil {
return 0, false
}
return parsed, true
}
func getenvBool(key string) bool {
val := strings.ToLower(getenv(key))
switch val {
case "1", "true", "yes", "on":
return true
default:
return false
}
}
func parseNodeSelector(raw string) map[string]string {
raw = strings.TrimSpace(raw)
if raw == "" {
return map[string]string{}
}
selector := map[string]string{}
parts := strings.Split(raw, ",")
for _, part := range parts {
part = strings.TrimSpace(part)
if part == "" {
continue
}
key, value, found := strings.Cut(part, "=")
if !found {
return nil
}
key = strings.TrimSpace(key)
value = strings.TrimSpace(value)
if key == "" || value == "" {
return nil
}
selector[key] = value
}
return selector
}
func parseCSV(raw string) []string {
if strings.TrimSpace(raw) == "" {
return nil
}
parts := strings.Split(raw, ",")
values := make([]string, 0, len(parts))
for _, part := range parts {
part = strings.TrimSpace(part)
if part == "" {
continue
}
values = append(values, part)
}
return values
}