mirror of
https://github.com/rcourtman/Pulse.git
synced 2026-07-09 16:00:59 +00:00
326 lines
12 KiB
Go
326 lines
12 KiB
Go
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.25.11-alpine@sha256:8d95af53d0d58e1759ddb4028285d9b1239067e4fbf4f544618cad0f60fbc354 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)
|
|
}
|
|
}
|
|
}
|