package installtests import ( "os" "os/exec" "strings" "testing" "gopkg.in/yaml.v3" ) func TestProviderMSPDeployComposeIsProviderModeAndStripeFree(t *testing.T) { composePath := repoFile("deploy", "provider-msp", "docker-compose.yml") composeBytes, err := os.ReadFile(composePath) if err != nil { t.Fatalf("read provider MSP compose: %v", err) } var compose map[string]any if err := yaml.Unmarshal(composeBytes, &compose); err != nil { t.Fatalf("provider MSP compose must be valid YAML: %v", err) } text := string(composeBytes) assertContainsAny(t, text, "CP_CONTROL_PLANE_MODE=provider_hosted_msp", "CP_CONTROL_PLANE_MODE=${CP_CONTROL_PLANE_MODE:-provider_hosted_msp}", ) assertContainsAll(t, text, "CP_DATA_DIR=${PULSE_PROVIDER_MSP_DATA_DIR:-/data}", "CP_PROVIDER_MSP_LICENSE_FILE=/run/secrets/provider_msp_license", "CP_DOCKER_NETWORK=${PULSE_PROVIDER_MSP_DOCKER_NETWORK:-pulse-provider-msp}", "CP_TRUSTED_PROXY_CIDRS=${CP_TRUSTED_PROXY_CIDRS}", "DOCKER_HOST=tcp://docker-socket-proxy:2375", "CP_STORAGE_DATA_PATH=${PULSE_PROVIDER_MSP_DATA_DIR:-/data}", "CP_STORAGE_ROOT_PATH=/storage-root-spacecheck", "CP_STORAGE_DOCKER_PATH=/storage-docker-spacecheck", "${PULSE_PROVIDER_MSP_DATA_DIR:-/data}:${PULSE_PROVIDER_MSP_DATA_DIR:-/data}", "DOCKER_SOCKET_PROXY_IMAGE", "PING=1", "VERSION=1", "INFO=1", "${PULSE_PROVIDER_MSP_DOCKER_SOCKET:-/var/run/docker.sock}:/var/run/docker.sock:ro", "${PULSE_PROVIDER_MSP_ROOT_SPACECHECK_DIR:-/var/lib/pulse-provider-msp/spacecheck/root}:/storage-root-spacecheck:ro", "${PULSE_PROVIDER_MSP_DOCKER_SPACECHECK_DIR:-/var/lib/docker/.pulse-provider-msp-spacecheck}:/storage-docker-spacecheck:ro", "pulse.provider-msp.role=traefik", "pulse.provider-msp.role=control-plane", "provider_msp_license:", "name: ${PULSE_PROVIDER_MSP_DOCKER_NETWORK:-pulse-provider-msp}", "subnet: ${PULSE_PROVIDER_MSP_DOCKER_SUBNET:-172.30.0.0/24}", ) assertNotContainsAny(t, text, ":/host-root", ":/host-var-lib-docker", "STRIPE_", "CP_TRIAL_SIGNUP_PRICE_ID", "CP_MSP_STARTER_PRICE_ID", "CP_MSP_GROWTH_PRICE_ID", "CP_MSP_SCALE_PRICE_ID", "CP_PUBLIC_CLOUD_SIGNUP_ENABLED", ) } func TestProviderMSPDeployEnvExampleMatchesBootstrapPath(t *testing.T) { envBytes, err := os.ReadFile(repoFile("deploy", "provider-msp", ".env.example")) if err != nil { t.Fatalf("read provider MSP env example: %v", err) } text := string(envBytes) assertContainsAll(t, text, "CP_ENV=production", "PULSE_PROVIDER_MSP_DATA_DIR=/data", "PULSE_PROVIDER_MSP_DOCKER_NETWORK=pulse-provider-msp", "PULSE_PROVIDER_MSP_DOCKER_SUBNET=172.30.0.0/24", "PULSE_PROVIDER_MSP_DOCKER_SOCKET=/var/run/docker.sock", "PULSE_PROVIDER_MSP_ROOT_SPACECHECK_DIR=/var/lib/pulse-provider-msp/spacecheck/root", "PULSE_PROVIDER_MSP_DOCKER_SPACECHECK_DIR=/var/lib/docker/.pulse-provider-msp-spacecheck", "CP_TRUSTED_PROXY_CIDRS=172.30.0.0/24", "CP_PROVIDER_MSP_LICENSE_FILE=./provider-msp-license.jwt", "CP_ENTITLEMENT_SIGNING_PRIVATE_KEY=", "sudo -E ./setup.sh", "docker compose run --rm control-plane provider-msp bootstrap", "docker compose run --rm control-plane provider-msp preflight", "docker compose run --rm control-plane provider-msp status", "docker compose run --rm control-plane provider-msp status --require-backup", "./upgrade.sh --dry-run", "./upgrade.sh", "./upgrade.sh --rollout-tenants", "./run-install-proof.sh", "docker compose run --rm control-plane provider-msp install-proof", "docker compose run --rm control-plane provider-msp recover --all-degraded --dry-run", "docker compose run --rm control-plane provider-msp recover --all-degraded", "docker compose run --rm control-plane provider-msp backup create", "docker compose run --rm control-plane provider-msp backup verify", "docker compose run --rm control-plane provider-msp backup restore", "--target-data-dir", "--dry-run", "docker compose run --rm control-plane provider-msp proof", "--account-name", "--owner-email", "--cleanup", ) assertNotContainsAny(t, text, "STRIPE_", "CP_TRIAL_SIGNUP_PRICE_ID", "CP_MSP_STARTER_PRICE_ID", "CP_MSP_GROWTH_PRICE_ID", "CP_MSP_SCALE_PRICE_ID", "CP_PUBLIC_CLOUD_SIGNUP_ENABLED", ) } func TestProviderMSPSetupScriptMatchesProviderContract(t *testing.T) { scriptPath := repoFile("deploy", "provider-msp", "setup.sh") result := exec.Command("bash", "-n", scriptPath) if output, err := result.CombinedOutput(); err != nil { t.Fatalf("provider MSP setup shell syntax failed: %v\n%s", err, output) } scriptBytes, err := os.ReadFile(scriptPath) if err != nil { t.Fatalf("read provider MSP setup: %v", err) } text := string(scriptBytes) assertContainsAll(t, text, "PULSE_PROVIDER_MSP_INSTALL_DIR", "PULSE_PROVIDER_MSP_DATA_DIR", "PULSE_PROVIDER_MSP_DOCKER_NETWORK", "PULSE_PROVIDER_MSP_BUNDLE_URL", "docker-ce docker-ce-cli containerd.io docker-buildx-plugin docker-compose-plugin", "docker-compose.yml", "traefik.yml", "traefik-dynamic.yml", ".env.example", "run-install-proof.sh", "upgrade.sh", "CP_PROVIDER_MSP_LICENSE_FILE", "CP_ENTITLEMENT_SIGNING_PRIVATE_KEY", "PULSE_PROVIDER_MSP_DATA_DIR", "PULSE_PROVIDER_MSP_DOCKER_NETWORK", "PULSE_PROVIDER_MSP_DOCKER_SUBNET", "PULSE_PROVIDER_MSP_DOCKER_SOCKET", "PULSE_PROVIDER_MSP_ROOT_SPACECHECK_DIR", "PULSE_PROVIDER_MSP_DOCKER_SPACECHECK_DIR", "CP_TRUSTED_PROXY_CIDRS", "DOCKER_SOCKET_PROXY_IMAGE", "must be an absolute path", "must point to a reachable Docker socket", "CP_ADMIN_KEY must be at least 32 characters", "CP_TRUSTED_PROXY_CIDRS must include PULSE_PROVIDER_MSP_DOCKER_SUBNET", "169.254.169.254/32", "will be created by compose with subnet", "must not be configured in provider-hosted MSP mode", "CP_ALLOW_DOCKERLESS_PROVISIONING must be false", "CP_STORAGE_GUARDRAILS_ENABLED must be true", "docker compose config --quiet", "docker compose pull traefik docker-socket-proxy control-plane", "PULSE_PROVIDER_MSP_ACCOUNT_NAME", "PULSE_PROVIDER_MSP_OWNER_EMAIL", "./run-install-proof.sh", ) assertNotContainsAny(t, text, "docker network create --subnet") } func TestProviderMSPUpgradeRunnerMatchesComposeContract(t *testing.T) { scriptPath := repoFile("deploy", "provider-msp", "upgrade.sh") result := exec.Command("bash", "-n", scriptPath) if output, err := result.CombinedOutput(); err != nil { t.Fatalf("provider MSP upgrade runner shell syntax failed: %v\n%s", err, output) } scriptBytes, err := os.ReadFile(scriptPath) if err != nil { t.Fatalf("read provider MSP upgrade runner: %v", err) } text := string(scriptBytes) assertContainsAll(t, text, "PROVIDER_MSP_UPGRADE_DRY_RUN", "PROVIDER_MSP_UPGRADE_ROLLOUT_TENANTS", "PROVIDER_MSP_UPGRADE_PRUNE_PREVIOUS", "PROVIDER_MSP_UPGRADE_BACKUP_OUTPUT", "PROVIDER_MSP_UPGRADE_RESTORE_TARGET", "PROVIDER_MSP_UPGRADE_RUN_ID", "PROVIDER_MSP_UPGRADE_HEALTH_TIMEOUT", "docker compose config --quiet", "docker version >/dev/null", "provider-msp preflight", "provider-msp status", "provider-msp status --require-backup", "provider-msp backup create", "provider-msp backup verify", "provider-msp backup restore", "--target-data-dir", "tenant-runtime rollout --all --image", "tenant-runtime rollout", "--all", "--image", "--run-id", "--health-timeout", "--prune-previous", "docker compose pull traefik docker-socket-proxy control-plane", "docker compose up -d traefik docker-socket-proxy control-plane", "docker compose run --rm --no-deps control-plane", "provider_msp_upgrade_ok=true", "tenant_runtime_rollout_applied=true", "tenant_runtime_rollout_applied=false", ) assertNotContainsAny(t, text, "STRIPE_", "CP_PUBLIC_CLOUD_SIGNUP_ENABLED", ) } func TestProviderMSPInstallProofRunnerMatchesComposeContract(t *testing.T) { scriptPath := repoFile("deploy", "provider-msp", "run-install-proof.sh") result := exec.Command("bash", "-n", scriptPath) if output, err := result.CombinedOutput(); err != nil { t.Fatalf("provider MSP install-proof runner shell syntax failed: %v\n%s", err, output) } scriptBytes, err := os.ReadFile(scriptPath) if err != nil { t.Fatalf("read provider MSP install-proof runner: %v", err) } text := string(scriptBytes) assertContainsAll(t, text, "docker compose config --quiet", "docker version >/dev/null", "docker compose pull traefik docker-socket-proxy control-plane", "docker compose up -d traefik docker-socket-proxy", "provider-msp install-proof", "--account-name", "--owner-email", "--workspace-count", "--install-type", "--target-path", "--skip-image-pull", "${#extra_install_args[@]}", "docker compose run --rm --no-deps control-plane", "docker compose up -d traefik docker-socket-proxy control-plane", "provider-msp status", ) assertNotContainsAny(t, text, "STRIPE_", "CP_PUBLIC_CLOUD_SIGNUP_ENABLED", ) } func TestProviderMSPTraefikUsesProviderNetwork(t *testing.T) { traefikBytes, err := os.ReadFile(repoFile("deploy", "provider-msp", "traefik.yml")) if err != nil { t.Fatalf("read provider MSP Traefik config: %v", err) } var cfg map[string]any if err := yaml.Unmarshal(traefikBytes, &cfg); err != nil { t.Fatalf("provider MSP Traefik config must be valid YAML: %v", err) } assertContainsAll(t, string(traefikBytes), "network: pulse-provider-msp", "certificatesResolvers:", "letsencrypt:", "le:", ) } func TestProviderMSPControlPlaneDockerfileBuildsReleaseLicenseBinary(t *testing.T) { dockerfileBytes, err := os.ReadFile(repoFile("deploy", "provider-msp", "Dockerfile.control-plane")) if err != nil { t.Fatalf("read control-plane Dockerfile: %v", err) } text := string(dockerfileBytes) assertContainsAll(t, text, "# syntax=docker/dockerfile:1.7", "FROM --platform=linux/amd64 node:20-alpine@sha256:fb4cd12c85ee03686f6af5362a0b0d56d50c58a04632e6c0fb8363f609372293 AS frontend-builder", "npm ci", "npm run build", "FROM --platform=$BUILDPLATFORM golang:1.26.5-alpine@sha256:0178a641fbb4858c5f1b48e34bdaabe0350a330a1b1149aabd498d0699ff5fb2 AS builder", "FROM alpine:3.20@sha256:d9e853e87e55526f6b2917df91a2115c36dd7c696a35be12163d44e6e2a4b6bc", "ARG PULSE_LICENSE_PUBLIC_KEY_SHA256", "ARG TARGETOS", "ARG TARGETARCH", "--mount=type=secret,id=pulse_license_public_key,required=false", "PULSE_LICENSE_PUBLIC_KEY_SHA256 is required for control-plane release image builds.", "PULSE_LICENSE_PUBLIC_KEY_SHA256 was provided but no license public key was mounted.", `LICENSE_PUBLIC_KEY="$(tr -d '\r\n' < /run/secrets/pulse_license_public_key)"`, "mounted license public key must decode to 32 bytes.", "mounted license public key does not match PULSE_LICENSE_PUBLIC_KEY_SHA256.", "COPY --from=frontend-builder /app/internal/api/frontend-modern/dist ./internal/api/frontend-modern/dist", `TARGET_GOOS="${TARGETOS:-linux}"`, `TARGET_GOARCH="${TARGETARCH:-$(go env GOARCH)}"`, `./scripts/release_ldflags.sh server --version "${VERSION}" --build-time "${BUILD_TIME}" --git-commit "${GIT_COMMIT}" --license-public-key "${LICENSE_PUBLIC_KEY}"`, `CGO_ENABLED=0 GOOS="${TARGET_GOOS}" GOARCH="${TARGET_GOARCH}" go build \`, "-tags release", "-buildvcs=false", "-trimpath", "-o /pulse-control-plane ./cmd/pulse-control-plane", ) assertNotContainsAny(t, text, "golang:1.25.7-alpine AS builder", "FROM alpine:3.21", "CGO_ENABLED=0 go build -o /pulse-control-plane ./cmd/pulse-control-plane", ) } func assertContainsAll(t *testing.T, text string, required ...string) { t.Helper() for _, needle := range required { if !strings.Contains(text, needle) { t.Fatalf("missing %q in:\n%s", needle, text) } } } func assertContainsAny(t *testing.T, text string, allowed ...string) { t.Helper() for _, needle := range allowed { if strings.Contains(text, needle) { return } } t.Fatalf("missing any of %q in:\n%s", allowed, text) } func assertNotContainsAny(t *testing.T, text string, forbidden ...string) { t.Helper() for _, needle := range forbidden { if strings.Contains(text, needle) { t.Fatalf("forbidden %q found in:\n%s", needle, text) } } }