package config import ( "fmt" "net" "os" "strconv" "strings" ) type Config struct { TrustedProxies []string Mode, Address, DatabaseURL, JWTSecret, AdminUsername, AdminPassword string PVEURL, PVETokenID, PVETokenSecret, PVECAFile string ExclusiveGPUNode string ExclusiveGPUVMIDs []int } func Load() (Config, error) { c := Config{Mode: env("VDI_MODE", "production"), Address: env("VDI_LISTEN", "127.0.0.1:8080"), DatabaseURL: os.Getenv("DATABASE_URL"), JWTSecret: os.Getenv("VDI_JWT_SECRET"), AdminUsername: os.Getenv("VDI_ADMIN_USERNAME"), AdminPassword: os.Getenv("VDI_ADMIN_PASSWORD"), PVEURL: os.Getenv("PVE_URL"), PVETokenID: os.Getenv("PVE_TOKEN_ID"), PVETokenSecret: os.Getenv("PVE_TOKEN_SECRET"), PVECAFile: os.Getenv("PVE_CA_FILE")} c.ExclusiveGPUNode = strings.TrimSpace(os.Getenv("VDI_EXCLUSIVE_GPU_NODE")) ids := strings.TrimSpace(os.Getenv("VDI_EXCLUSIVE_GPU_VMIDS")) if (c.ExclusiveGPUNode == "") != (ids == "") { return c, fmt.Errorf("exclusive GPU requires both node and VMIDs") } seen := map[int]bool{} if ids != "" { if strings.ContainsAny(c.ExclusiveGPUNode, "/\\?# ") { return c, fmt.Errorf("invalid exclusive GPU node") } for _, value := range strings.Split(ids, ",") { id, err := strconv.Atoi(strings.TrimSpace(value)) if err != nil || id < 100 || seen[id] { return c, fmt.Errorf("invalid exclusive GPU VMIDs") } seen[id] = true c.ExclusiveGPUVMIDs = append(c.ExclusiveGPUVMIDs, id) } if len(seen) < 2 || len(seen) > 16 { return c, fmt.Errorf("exclusive GPU requires 2 to 16 VMs") } } if value := os.Getenv("VDI_TRUSTED_PROXIES"); value != "" { for _, proxy := range strings.Split(value, ",") { proxy = strings.TrimSpace(proxy) if _, _, err := net.ParseCIDR(proxy); err != nil && net.ParseIP(proxy) == nil { return c, fmt.Errorf("invalid VDI_TRUSTED_PROXIES entry") } c.TrustedProxies = append(c.TrustedProxies, proxy) } } if c.Mode != "demo" && c.Mode != "production" { return c, fmt.Errorf("VDI_MODE must be demo or production") } if c.DatabaseURL == "" { return c, fmt.Errorf("DATABASE_URL is required") } if len(c.JWTSecret) < 32 { return c, fmt.Errorf("VDI_JWT_SECRET must contain at least 32 characters") } if c.Mode == "production" && strings.TrimSpace(c.AdminUsername) == "" { return c, fmt.Errorf("production requires VDI_ADMIN_USERNAME") } if c.PVEURL != "" && (c.PVETokenID == "" || c.PVETokenSecret == "") { return c, fmt.Errorf("PVE_TOKEN_ID and PVE_TOKEN_SECRET are required with PVE_URL") } return c, nil } func env(key, fallback string) string { if v := os.Getenv(key); v != "" { return v } return fallback }