soteria/internal/config/config.go

163 lines
4.5 KiB
Go

package config
import (
"errors"
"os"
"strconv"
"strings"
)
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"
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
}
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)
if ttl, ok := getenvInt("SOTERIA_JOB_TTL_SECONDS"); ok {
cfg.JobTTLSeconds = int32(ttl)
} else {
cfg.JobTTLSeconds = defaultJobTTLSeconds
}
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")
}
}
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 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
}