Files
Pulse/internal/cloudcp/config.go
rcourtman 824168941b feat(cloudcp): switch email provider from Postmark to Resend
Resend is already used by the license server and finance workflows, so
using it here avoids a second provider. The admin magic-link endpoint
now accepts send_email to deliver the link directly via Resend.

Env var: POSTMARK_SERVER_TOKEN -> RESEND_API_KEY
2026-02-11 13:03:22 +00:00

109 lines
3.2 KiB
Go

package cloudcp
import (
"fmt"
"os"
"path/filepath"
"strconv"
"strings"
"github.com/joho/godotenv"
)
// CPConfig holds all configuration for the control plane.
type CPConfig struct {
DataDir string
BindAddress string
Port int
AdminKey string
BaseURL string
PulseImage string
DockerNetwork string
TenantMemoryLimit int64 // bytes
TenantCPUShares int64
StripeWebhookSecret string
StripeAPIKey string
ResendAPIKey string // Resend API key (optional — if empty, emails are logged)
EmailFrom string // Sender email address (e.g. "noreply@pulserelay.pro")
}
// TenantsDir returns the directory where per-tenant data is stored.
func (c *CPConfig) TenantsDir() string {
return filepath.Join(c.DataDir, "tenants")
}
// ControlPlaneDir returns the directory for control plane's own data (registry DB, etc).
func (c *CPConfig) ControlPlaneDir() string {
return filepath.Join(c.DataDir, "control-plane")
}
// LoadConfig loads control plane configuration from environment variables.
// A .env file is loaded if present but not required.
func LoadConfig() (*CPConfig, error) {
// Best-effort .env loading (not required)
_ = godotenv.Load()
cfg := &CPConfig{
DataDir: envOrDefault("CP_DATA_DIR", "/data"),
BindAddress: envOrDefault("CP_BIND_ADDRESS", "0.0.0.0"),
Port: envOrDefaultInt("CP_PORT", 8443),
AdminKey: strings.TrimSpace(os.Getenv("CP_ADMIN_KEY")),
BaseURL: strings.TrimSpace(os.Getenv("CP_BASE_URL")),
PulseImage: envOrDefault("CP_PULSE_IMAGE", "ghcr.io/rcourtman/pulse:latest"),
DockerNetwork: envOrDefault("CP_DOCKER_NETWORK", "pulse-cloud"),
TenantMemoryLimit: envOrDefaultInt64("CP_TENANT_MEMORY_LIMIT", 512*1024*1024), // 512 MiB
TenantCPUShares: envOrDefaultInt64("CP_TENANT_CPU_SHARES", 256),
StripeWebhookSecret: strings.TrimSpace(os.Getenv("STRIPE_WEBHOOK_SECRET")),
StripeAPIKey: strings.TrimSpace(os.Getenv("STRIPE_API_KEY")),
ResendAPIKey: strings.TrimSpace(os.Getenv("RESEND_API_KEY")),
EmailFrom: envOrDefault("PULSE_EMAIL_FROM", "noreply@pulserelay.pro"),
}
if err := cfg.validate(); err != nil {
return nil, err
}
return cfg, nil
}
func (c *CPConfig) validate() error {
var missing []string
if c.AdminKey == "" {
missing = append(missing, "CP_ADMIN_KEY")
}
if c.BaseURL == "" {
missing = append(missing, "CP_BASE_URL")
}
if c.StripeWebhookSecret == "" {
missing = append(missing, "STRIPE_WEBHOOK_SECRET")
}
if len(missing) > 0 {
return fmt.Errorf("missing required environment variables: %s", strings.Join(missing, ", "))
}
return nil
}
func envOrDefault(key, fallback string) string {
if v := strings.TrimSpace(os.Getenv(key)); v != "" {
return v
}
return fallback
}
func envOrDefaultInt(key string, fallback int) int {
if v := strings.TrimSpace(os.Getenv(key)); v != "" {
if n, err := strconv.Atoi(v); err == nil {
return n
}
}
return fallback
}
func envOrDefaultInt64(key string, fallback int64) int64 {
if v := strings.TrimSpace(os.Getenv(key)); v != "" {
if n, err := strconv.ParseInt(v, 10, 64); err == nil {
return n
}
}
return fallback
}