mirror of
https://github.com/rcourtman/Pulse.git
synced 2026-10-19 10:33:29 +00:00
fix(cloudcp): inject hosted entitlement public key
This commit is contained in:
parent
c053c6c86b
commit
53154ccb8a
5 changed files with 60 additions and 18 deletions
|
|
@ -1,6 +1,8 @@
|
|||
package cloudcp
|
||||
|
||||
import (
|
||||
"crypto/ed25519"
|
||||
"encoding/base64"
|
||||
"fmt"
|
||||
"net/url"
|
||||
"os"
|
||||
|
|
@ -9,6 +11,7 @@ import (
|
|||
"strings"
|
||||
|
||||
"github.com/joho/godotenv"
|
||||
pkglicensing "github.com/rcourtman/pulse-go-rewrite/pkg/licensing"
|
||||
)
|
||||
|
||||
// CPConfig holds all configuration for the control plane.
|
||||
|
|
@ -39,6 +42,7 @@ type CPConfig struct {
|
|||
CloudPowerPriceID string // Cloud Power tier price ID (optional)
|
||||
CloudMaxPriceID string // Cloud Max tier price ID (optional)
|
||||
TrialActivationPrivateKey string
|
||||
TrialActivationPublicKey string
|
||||
RequireEmailProvider bool
|
||||
ResendAPIKey string // Resend API key (optional — if empty, emails are logged)
|
||||
EmailFrom string // Sender email address (e.g. "noreply@pulserelay.pro")
|
||||
|
|
@ -128,6 +132,13 @@ func LoadConfig() (*CPConfig, error) {
|
|||
ResendAPIKey: strings.TrimSpace(os.Getenv("RESEND_API_KEY")),
|
||||
EmailFrom: envOrDefault("PULSE_EMAIL_FROM", "noreply@pulserelay.pro"),
|
||||
}
|
||||
if strings.TrimSpace(cfg.TrialActivationPrivateKey) != "" {
|
||||
publicKey, err := deriveTrialActivationPublicKey(cfg.TrialActivationPrivateKey)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("derive trial activation public key: %w", err)
|
||||
}
|
||||
cfg.TrialActivationPublicKey = publicKey
|
||||
}
|
||||
|
||||
if err := cfg.validate(); err != nil {
|
||||
return nil, fmt.Errorf("validate control plane config: %w", err)
|
||||
|
|
@ -135,6 +146,18 @@ func LoadConfig() (*CPConfig, error) {
|
|||
return cfg, nil
|
||||
}
|
||||
|
||||
func deriveTrialActivationPublicKey(encodedPrivateKey string) (string, error) {
|
||||
privateKey, err := pkglicensing.DecodeEd25519PrivateKey(strings.TrimSpace(encodedPrivateKey))
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
publicKey, ok := privateKey.Public().(ed25519.PublicKey)
|
||||
if !ok || len(publicKey) != ed25519.PublicKeySize {
|
||||
return "", fmt.Errorf("invalid derived trial activation public key")
|
||||
}
|
||||
return base64.StdEncoding.EncodeToString(publicKey), nil
|
||||
}
|
||||
|
||||
func (c *CPConfig) validate() error {
|
||||
var missing []string
|
||||
if c.AdminKey == "" {
|
||||
|
|
|
|||
|
|
@ -16,7 +16,7 @@ func setRequiredCPEnv(t *testing.T) {
|
|||
|
||||
func setTrialSigningEnv(t *testing.T) {
|
||||
t.Helper()
|
||||
t.Setenv("CP_TRIAL_ACTIVATION_PRIVATE_KEY", "test-activation-key")
|
||||
t.Setenv("CP_TRIAL_ACTIVATION_PRIVATE_KEY", "A8medgdNdm12GXfTXWo6+TMZ2BeHPCLg2kd0znn6ZUk=")
|
||||
}
|
||||
|
||||
func TestLoadConfig_MissingRequired(t *testing.T) {
|
||||
|
|
@ -94,6 +94,19 @@ func TestLoadConfig_CustomValues(t *testing.T) {
|
|||
}
|
||||
}
|
||||
|
||||
func TestLoadConfig_DerivesTrialActivationPublicKey(t *testing.T) {
|
||||
setRequiredCPEnv(t)
|
||||
setTrialSigningEnv(t)
|
||||
|
||||
cfg, err := LoadConfig()
|
||||
if err != nil {
|
||||
t.Fatalf("LoadConfig: %v", err)
|
||||
}
|
||||
if strings.TrimSpace(cfg.TrialActivationPublicKey) == "" {
|
||||
t.Fatal("TrialActivationPublicKey = empty, want derived public key")
|
||||
}
|
||||
}
|
||||
|
||||
func TestLoadConfig_TrustedProxyCIDRs(t *testing.T) {
|
||||
setRequiredCPEnv(t)
|
||||
t.Setenv("CP_TRUSTED_PROXY_CIDRS", "172.18.0.0/16, 127.0.0.1/32")
|
||||
|
|
|
|||
|
|
@ -21,13 +21,14 @@ import (
|
|||
|
||||
// ManagerConfig holds Docker manager settings.
|
||||
type ManagerConfig struct {
|
||||
Image string
|
||||
Network string
|
||||
BaseDomain string
|
||||
TrustedProxyCIDRs []string
|
||||
MemoryLimit int64 // bytes
|
||||
CPUShares int64
|
||||
ContainerPort int // port inside the container (default 7655)
|
||||
Image string
|
||||
Network string
|
||||
BaseDomain string
|
||||
TrialActivationPublicKey string
|
||||
TrustedProxyCIDRs []string
|
||||
MemoryLimit int64 // bytes
|
||||
CPUShares int64
|
||||
ContainerPort int // port inside the container (default 7655)
|
||||
}
|
||||
|
||||
// Manager orchestrates Docker containers for tenant lifecycle.
|
||||
|
|
@ -92,7 +93,7 @@ func (m *Manager) CreateAndStart(ctx context.Context, tenantID, tenantDataDir st
|
|||
&container.Config{
|
||||
Image: m.cfg.Image,
|
||||
Labels: labels,
|
||||
Env: tenantEnv(tenantID, m.cfg.BaseDomain, m.tenantTrustedProxyCIDRs(ctx)),
|
||||
Env: tenantEnv(tenantID, m.cfg.BaseDomain, m.cfg.TrialActivationPublicKey, m.tenantTrustedProxyCIDRs(ctx)),
|
||||
},
|
||||
&container.HostConfig{
|
||||
RestartPolicy: container.RestartPolicy{Name: "unless-stopped"},
|
||||
|
|
@ -142,7 +143,7 @@ func tenantRuntimeOwnershipPaths() []string {
|
|||
}
|
||||
}
|
||||
|
||||
func tenantEnv(tenantID, baseDomain string, trustedProxyCIDRs []string) []string {
|
||||
func tenantEnv(tenantID, baseDomain, trialActivationPublicKey string, trustedProxyCIDRs []string) []string {
|
||||
publicURL := ""
|
||||
tenantID = strings.TrimSpace(tenantID)
|
||||
baseDomain = strings.TrimSpace(baseDomain)
|
||||
|
|
@ -162,6 +163,9 @@ func tenantEnv(tenantID, baseDomain string, trustedProxyCIDRs []string) []string
|
|||
if publicURL != "" {
|
||||
env = append(env, "PULSE_PUBLIC_URL="+publicURL)
|
||||
}
|
||||
if strings.TrimSpace(trialActivationPublicKey) != "" {
|
||||
env = append(env, "PULSE_TRIAL_ACTIVATION_PUBLIC_KEY="+strings.TrimSpace(trialActivationPublicKey))
|
||||
}
|
||||
if len(trustedProxyCIDRs) > 0 {
|
||||
env = append(env, "PULSE_TRUSTED_PROXY_CIDRS="+strings.Join(trustedProxyCIDRs, ","))
|
||||
}
|
||||
|
|
|
|||
|
|
@ -70,7 +70,7 @@ func TestTenantMountsKeepImmutableFilesReadOnly(t *testing.T) {
|
|||
func TestTenantEnvIncludesImmutableOwnershipContract(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
env := tenantEnv("t-example", "cloud.pulserelay.pro", []string{"172.18.0.0/16", "127.0.0.1/32"})
|
||||
env := tenantEnv("t-example", "cloud.pulserelay.pro", "pubkey-123", []string{"172.18.0.0/16", "127.0.0.1/32"})
|
||||
want := map[string]bool{
|
||||
"PULSE_DATA_DIR=/etc/pulse": true,
|
||||
"PULSE_HOSTED_MODE=true": true,
|
||||
|
|
@ -79,6 +79,7 @@ func TestTenantEnvIncludesImmutableOwnershipContract(t *testing.T) {
|
|||
"PUID=1000": true,
|
||||
"PGID=1000": true,
|
||||
"PULSE_PUBLIC_URL=https://t-example.cloud.pulserelay.pro": true,
|
||||
"PULSE_TRIAL_ACTIVATION_PUBLIC_KEY=pubkey-123": true,
|
||||
"PULSE_TRUSTED_PROXY_CIDRS=172.18.0.0/16,127.0.0.1/32": true,
|
||||
immutableOwnershipPathsEnv + "=/etc/pulse/secrets/handoff.key:/etc/pulse/.cloud_handoff_key": true,
|
||||
}
|
||||
|
|
@ -95,7 +96,7 @@ func TestTenantEnvIncludesImmutableOwnershipContract(t *testing.T) {
|
|||
func TestTenantEnvOmitsPublicURLWithoutTenantContext(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
env := tenantEnv("", "", nil)
|
||||
env := tenantEnv("", "", "", nil)
|
||||
sawTenantID := false
|
||||
for _, item := range env {
|
||||
if strings.HasPrefix(item, "PULSE_PUBLIC_URL=") {
|
||||
|
|
|
|||
|
|
@ -53,12 +53,13 @@ func Run(ctx context.Context, version string) error {
|
|||
// Initialize Docker manager (best-effort — control plane can run without Docker for dev/testing)
|
||||
var dockerMgr *cpDocker.Manager
|
||||
dockerMgr, err = cpDocker.NewManager(cpDocker.ManagerConfig{
|
||||
Image: cfg.PulseImage,
|
||||
Network: cfg.DockerNetwork,
|
||||
BaseDomain: baseDomainFromURL(cfg.BaseURL),
|
||||
TrustedProxyCIDRs: cfg.TrustedProxyCIDRs,
|
||||
MemoryLimit: cfg.TenantMemoryLimit,
|
||||
CPUShares: cfg.TenantCPUShares,
|
||||
Image: cfg.PulseImage,
|
||||
Network: cfg.DockerNetwork,
|
||||
BaseDomain: baseDomainFromURL(cfg.BaseURL),
|
||||
TrialActivationPublicKey: cfg.TrialActivationPublicKey,
|
||||
TrustedProxyCIDRs: cfg.TrustedProxyCIDRs,
|
||||
MemoryLimit: cfg.TenantMemoryLimit,
|
||||
CPUShares: cfg.TenantCPUShares,
|
||||
})
|
||||
if err != nil {
|
||||
log.Warn().Err(err).Msg("Docker unavailable — container management disabled")
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue