Pulse/deploy/provider-msp/setup.sh
rcourtman a17882458a Rename CP_TRIAL_ACTIVATION_PRIVATE_KEY to CP_ENTITLEMENT_SIGNING_PRIVATE_KEY in the provider MSP bundle
The variable signs hosted entitlement leases; the trial-activation name is
left over from the retired trial era and reads as trial machinery to an
operator generating their licensing root key. The provider MSP bundle has
no installed base yet, so the canonical rename is free today and frozen
the moment the first design partner installs.

- Control plane reads CP_ENTITLEMENT_SIGNING_PRIVATE_KEY first and falls
  back to CP_TRIAL_ACTIVATION_PRIVATE_KEY, so existing Pulse-hosted cloud
  deployments (deploy/cloud, hibernated snapshot) keep working unchanged.
- deploy/provider-msp (.env.example, compose, setup.sh), MSP.md, and the
  install-test pins use the canonical name; error messages name it too.
- deploy/cloud intentionally keeps the legacy name: that stack historically
  signed hosted trial activations, and its snapshot predates the rename.
2026-06-10 14:08:52 +01:00

601 lines
21 KiB
Bash
Executable file

#!/usr/bin/env bash
# Pulse Provider MSP - first-time host setup
# Run on a fresh Ubuntu 24.04 host as root.
set -euo pipefail
IFS=$'\n\t'
PULSE_PROVIDER_MSP_INSTALL_DIR="${PULSE_PROVIDER_MSP_INSTALL_DIR:-/opt/pulse-provider-msp}"
PULSE_PROVIDER_MSP_DATA_DIR="${PULSE_PROVIDER_MSP_DATA_DIR:-/data}"
PULSE_PROVIDER_MSP_DOCKER_NETWORK="${PULSE_PROVIDER_MSP_DOCKER_NETWORK:-pulse-provider-msp}"
PULSE_PROVIDER_MSP_DOCKER_SUBNET="${PULSE_PROVIDER_MSP_DOCKER_SUBNET:-172.30.0.0/24}"
PULSE_PROVIDER_MSP_DOCKER_SOCKET="${PULSE_PROVIDER_MSP_DOCKER_SOCKET:-/var/run/docker.sock}"
PULSE_PROVIDER_MSP_ROOT_SPACECHECK_DIR="${PULSE_PROVIDER_MSP_ROOT_SPACECHECK_DIR:-/var/lib/pulse-provider-msp/spacecheck/root}"
PULSE_PROVIDER_MSP_DOCKER_SPACECHECK_DIR="${PULSE_PROVIDER_MSP_DOCKER_SPACECHECK_DIR:-/var/lib/docker/.pulse-provider-msp-spacecheck}"
PULSE_PROVIDER_MSP_BUNDLE_URL="${PULSE_PROVIDER_MSP_BUNDLE_URL:-}"
PULSE_PROVIDER_MSP_EXPECT_ENV="${PULSE_PROVIDER_MSP_EXPECT_ENV:-production}"
PULSE_PROVIDER_MSP_SKIP_PULL="${PULSE_PROVIDER_MSP_SKIP_PULL:-0}"
PULSE_PROVIDER_MSP_RUN_INSTALL_PROOF="${PULSE_PROVIDER_MSP_RUN_INSTALL_PROOF:-auto}"
PULSE_PROVIDER_MSP_ACCOUNT_NAME="${PULSE_PROVIDER_MSP_ACCOUNT_NAME:-}"
PULSE_PROVIDER_MSP_OWNER_EMAIL="${PULSE_PROVIDER_MSP_OWNER_EMAIL:-}"
log() {
echo "[$(date -u +'%Y-%m-%dT%H:%M:%SZ')] $*"
}
die() {
echo "error: $*" >&2
exit 1
}
have() { command -v "$1" >/dev/null 2>&1; }
need_root() {
if [[ "${EUID:-$(id -u)}" -ne 0 ]]; then
die "run as root (or: sudo -E bash setup.sh)"
fi
}
apt_install() {
export DEBIAN_FRONTEND=noninteractive
apt-get update -y
apt-get install -y --no-install-recommends "$@"
}
install_docker_ce() {
if have docker && docker --version >/dev/null 2>&1 && docker compose version >/dev/null 2>&1; then
log "docker and compose already installed"
return 0
fi
log "installing Docker CE and compose plugin"
apt_install ca-certificates curl gnupg lsb-release
install -m 0755 -d /etc/apt/keyrings
if [[ ! -f /etc/apt/keyrings/docker.gpg ]]; then
curl -fsSL https://download.docker.com/linux/ubuntu/gpg | gpg --dearmor -o /etc/apt/keyrings/docker.gpg
chmod a+r /etc/apt/keyrings/docker.gpg
fi
local arch codename
arch="$(dpkg --print-architecture)"
codename="$(. /etc/os-release && echo "${VERSION_CODENAME}")"
cat >/etc/apt/sources.list.d/docker.list <<EOF
deb [arch=${arch} signed-by=/etc/apt/keyrings/docker.gpg] https://download.docker.com/linux/ubuntu ${codename} stable
EOF
apt-get update -y
apt-get install -y --no-install-recommends \
docker-ce docker-ce-cli containerd.io docker-buildx-plugin docker-compose-plugin
systemctl enable --now docker
}
install_ops_tools() {
log "installing ops tools"
apt_install jq openssl rsync sqlite3 rclone s3cmd
}
create_data_dirs() {
local data_dir
data_dir="$(provider_data_dir)"
log "creating provider MSP data directories under ${data_dir}"
install -d -m 0700 "${data_dir}"
install -d -m 0700 "${data_dir}/tenants"
install -d -m 0700 "${data_dir}/control-plane"
install -d -m 0700 "${data_dir}/backups"
install -d -m 0700 "${data_dir}/backups/provider-msp"
local root_spacecheck docker_spacecheck
root_spacecheck="$(provider_root_spacecheck_dir)"
docker_spacecheck="$(provider_docker_spacecheck_dir)"
log "creating storage space-check marker directories"
install -d -m 0700 "${root_spacecheck}"
install -d -m 0700 "${docker_spacecheck}"
}
ensure_docker_network() {
local network subnet existing_subnets
network="$(provider_docker_network)"
subnet="$(provider_docker_subnet)"
log "checking Docker network ${network}"
if ! docker network inspect "${network}" >/dev/null 2>&1; then
log "Docker network ${network} will be created by compose with subnet ${subnet}"
return 0
fi
existing_subnets="$(docker network inspect -f '{{range .IPAM.Config}}{{println .Subnet}}{{end}}' "${network}" 2>/dev/null | tr '\n' ',' | sed 's/,$//' || true)"
if [[ ",${existing_subnets}," != *",${subnet},"* ]]; then
die "Docker network ${network} exists with subnet(s) ${existing_subnets:-<none>}; expected ${subnet} so CP_TRUSTED_PROXY_CIDRS can trust Traefik without trusting every peer"
fi
}
block_container_metadata_service() {
if ! have iptables; then
log "iptables not found; skipping container metadata-service block"
return 0
fi
log "ensuring containers cannot reach cloud metadata service"
iptables -N DOCKER-USER 2>/dev/null || true
if ! iptables -C DOCKER-USER -d 169.254.169.254/32 -j REJECT >/dev/null 2>&1; then
iptables -I DOCKER-USER -d 169.254.169.254/32 -j REJECT
fi
}
script_dir_best_effort() {
if [[ -n "${BASH_SOURCE[0]:-}" && -e "${BASH_SOURCE[0]}" ]]; then
(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd -P)
return 0
fi
return 1
}
install_deploy_bundle() {
log "installing provider MSP deploy bundle to ${PULSE_PROVIDER_MSP_INSTALL_DIR}"
install -d -m 0755 "${PULSE_PROVIDER_MSP_INSTALL_DIR}"
local src_dir=""
if src_dir="$(script_dir_best_effort)"; then
:
else
src_dir=""
fi
local required=(
"docker-compose.yml"
"traefik.yml"
"traefik-dynamic.yml"
".env.example"
"run-install-proof.sh"
"upgrade.sh"
)
if [[ -n "${src_dir}" ]]; then
local f
for f in "${required[@]}"; do
[[ -f "${src_dir}/${f}" ]] || src_dir=""
done
fi
if [[ -z "${src_dir}" && -n "${PULSE_PROVIDER_MSP_BUNDLE_URL}" ]]; then
log "deploy bundle not found locally; downloading PULSE_PROVIDER_MSP_BUNDLE_URL"
local tmp
tmp="$(mktemp -d)"
# shellcheck disable=SC2064
trap "rm -rf \"${tmp}\"" EXIT
curl -fsSL "${PULSE_PROVIDER_MSP_BUNDLE_URL}" -o "${tmp}/bundle.tgz"
tar -xzf "${tmp}/bundle.tgz" -C "${tmp}"
local cand ok f
while IFS= read -r cand; do
[[ -n "${cand}" ]] || continue
local d
d="$(dirname "${cand}")"
ok="1"
for f in "${required[@]}"; do
[[ -f "${d}/${f}" ]] || ok="0"
done
if [[ "${ok}" == "1" ]]; then
src_dir="${d}"
break
fi
done < <(find "${tmp}" -type f -name docker-compose.yml -print 2>/dev/null || true)
fi
if [[ -z "${src_dir}" ]]; then
cat >&2 <<'EOF'
error: missing deploy bundle files next to setup.sh.
This script needs these files present on disk:
- docker-compose.yml
- traefik.yml
- traefik-dynamic.yml
- .env.example
- run-install-proof.sh
- upgrade.sh
Run it from deploy/provider-msp/, or set PULSE_PROVIDER_MSP_BUNDLE_URL to a
tar.gz containing those files.
EOF
exit 1
fi
install -m 0644 "${src_dir}/docker-compose.yml" "${PULSE_PROVIDER_MSP_INSTALL_DIR}/docker-compose.yml"
install -m 0644 "${src_dir}/traefik.yml" "${PULSE_PROVIDER_MSP_INSTALL_DIR}/traefik.yml"
install -m 0644 "${src_dir}/traefik-dynamic.yml" "${PULSE_PROVIDER_MSP_INSTALL_DIR}/traefik-dynamic.yml"
install -m 0644 "${src_dir}/.env.example" "${PULSE_PROVIDER_MSP_INSTALL_DIR}/.env.example"
install -m 0755 "${src_dir}/run-install-proof.sh" "${PULSE_PROVIDER_MSP_INSTALL_DIR}/run-install-proof.sh"
install -m 0755 "${src_dir}/upgrade.sh" "${PULSE_PROVIDER_MSP_INSTALL_DIR}/upgrade.sh"
}
env_value() {
local key="$1"
local env_path="${2:-${PULSE_PROVIDER_MSP_INSTALL_DIR}/.env}"
local value
value="$(grep -E "^${key}=" "${env_path}" | tail -n 1 | cut -d= -f2- || true)"
value="${value%\"}"; value="${value#\"}"
value="${value%\'}"; value="${value#\'}"
echo "${value}" | sed -e 's/^[[:space:]]*//' -e 's/[[:space:]]*$//'
}
provider_data_dir() {
local env_path="${PULSE_PROVIDER_MSP_INSTALL_DIR}/.env"
local configured=""
if [[ -f "${env_path}" ]]; then
configured="$(env_value PULSE_PROVIDER_MSP_DATA_DIR "${env_path}")"
fi
echo "${configured:-${PULSE_PROVIDER_MSP_DATA_DIR}}"
}
provider_docker_network() {
local env_path="${PULSE_PROVIDER_MSP_INSTALL_DIR}/.env"
local configured=""
if [[ -f "${env_path}" ]]; then
configured="$(env_value PULSE_PROVIDER_MSP_DOCKER_NETWORK "${env_path}")"
fi
echo "${configured:-${PULSE_PROVIDER_MSP_DOCKER_NETWORK}}"
}
provider_docker_subnet() {
local env_path="${PULSE_PROVIDER_MSP_INSTALL_DIR}/.env"
local configured=""
if [[ -f "${env_path}" ]]; then
configured="$(env_value PULSE_PROVIDER_MSP_DOCKER_SUBNET "${env_path}")"
fi
echo "${configured:-${PULSE_PROVIDER_MSP_DOCKER_SUBNET}}"
}
provider_root_spacecheck_dir() {
local env_path="${PULSE_PROVIDER_MSP_INSTALL_DIR}/.env"
local configured=""
if [[ -f "${env_path}" ]]; then
configured="$(env_value PULSE_PROVIDER_MSP_ROOT_SPACECHECK_DIR "${env_path}")"
fi
echo "${configured:-${PULSE_PROVIDER_MSP_ROOT_SPACECHECK_DIR}}"
}
provider_docker_spacecheck_dir() {
local env_path="${PULSE_PROVIDER_MSP_INSTALL_DIR}/.env"
local configured=""
if [[ -f "${env_path}" ]]; then
configured="$(env_value PULSE_PROVIDER_MSP_DOCKER_SPACECHECK_DIR "${env_path}")"
fi
echo "${configured:-${PULSE_PROVIDER_MSP_DOCKER_SPACECHECK_DIR}}"
}
set_env_value() {
local key="$1"
local value="$2"
local env_path="${3:-${PULSE_PROVIDER_MSP_INSTALL_DIR}/.env}"
local tmp
tmp="$(mktemp)"
if grep -q -E "^${key}=" "${env_path}"; then
awk -v key="${key}" -v value="${value}" 'BEGIN{done=0} $0 ~ "^" key "=" && done==0 { print key "=" value; done=1; next } { print }' "${env_path}" >"${tmp}"
else
cat "${env_path}" >"${tmp}"
printf '%s=%s\n' "${key}" "${value}" >>"${tmp}"
fi
cat "${tmp}" >"${env_path}"
rm -f "${tmp}"
}
ensure_generated_secrets() {
local env_path="${PULSE_PROVIDER_MSP_INSTALL_DIR}/.env"
[[ -f "${env_path}" ]] || die "missing ${env_path}"
have openssl || die "openssl is required to generate provider MSP secrets"
if [[ -z "$(env_value CP_ADMIN_KEY "${env_path}")" ]]; then
log "generating CP_ADMIN_KEY"
set_env_value CP_ADMIN_KEY "$(openssl rand -hex 32)" "${env_path}"
fi
if [[ -z "$(env_value CP_ENTITLEMENT_SIGNING_PRIVATE_KEY "${env_path}")" ]]; then
log "generating CP_ENTITLEMENT_SIGNING_PRIVATE_KEY"
set_env_value CP_ENTITLEMENT_SIGNING_PRIVATE_KEY "$(openssl rand -base64 32 | tr -d '\n')" "${env_path}"
fi
chmod 0600 "${env_path}"
}
# derive_lease_signing_public_key prints the base64 Ed25519 public key for
# CP_ENTITLEMENT_SIGNING_PRIVATE_KEY. The provider MSP license must bind this
# exact key (entitlement_signing_public_key) or the control plane will refuse
# to start; include it when requesting your license. The private key never
# leaves this host.
derive_lease_signing_public_key() {
local env_path="${PULSE_PROVIDER_MSP_INSTALL_DIR}/.env"
[[ -f "${env_path}" ]] || die "missing ${env_path}"
have openssl || die "openssl is required to derive the lease signing public key"
local key_b64 key_len tmp_der
key_b64="$(env_value CP_ENTITLEMENT_SIGNING_PRIVATE_KEY "${env_path}")"
[[ -n "${key_b64}" ]] || die "CP_ENTITLEMENT_SIGNING_PRIVATE_KEY is not set; run setup.sh first"
key_len="$(printf '%s' "${key_b64}" | base64 -d 2>/dev/null | wc -c | tr -d ' ')"
case "${key_len}" in
64)
# 64-byte Ed25519 private key: the public key is the trailing 32 bytes.
printf '%s' "${key_b64}" | base64 -d | tail -c 32 | base64 | tr -d '\n'
;;
32)
# 32-byte seed: wrap in a PKCS#8 DER envelope and let openssl derive
# the public key (raw key = trailing 32 bytes of the SPKI DER).
tmp_der="$(mktemp)"
{
printf '\x30\x2e\x02\x01\x00\x30\x05\x06\x03\x2b\x65\x70\x04\x22\x04\x20'
printf '%s' "${key_b64}" | base64 -d
} >"${tmp_der}"
openssl pkey -inform DER -in "${tmp_der}" -pubout -outform DER 2>/dev/null | tail -c 32 | base64 | tr -d '\n'
rm -f "${tmp_der}"
;;
*)
die "CP_ENTITLEMENT_SIGNING_PRIVATE_KEY must decode to a 32-byte seed or 64-byte Ed25519 key (got ${key_len} bytes)"
;;
esac
}
truthy() {
case "$(echo "$1" | tr '[:upper:]' '[:lower:]')" in
true|1|yes|on) return 0 ;;
*) return 1 ;;
esac
}
falsy() {
case "$(echo "$1" | tr '[:upper:]' '[:lower:]')" in
false|0|no|off) return 0 ;;
*) return 1 ;;
esac
}
ensure_env_file() {
local env_path="${PULSE_PROVIDER_MSP_INSTALL_DIR}/.env"
if [[ -f "${env_path}" ]]; then
chmod 0600 "${env_path}" || true
return 0
fi
log "no ${env_path}; creating from .env.example"
cp -n "${PULSE_PROVIDER_MSP_INSTALL_DIR}/.env.example" "${env_path}"
chmod 0600 "${env_path}"
cat <<EOF
Created ${env_path} from .env.example.
Edit it now and set required values:
- DOMAIN
- ACME_EMAIL
- CF_DNS_API_TOKEN
- TRAEFIK_IMAGE (digest pinned)
- DOCKER_SOCKET_PROXY_IMAGE (digest pinned)
- CONTROL_PLANE_IMAGE (digest pinned)
- CP_PULSE_IMAGE (digest pinned)
- 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
- CP_PROVIDER_MSP_LICENSE_FILE
setup.sh will generate CP_ADMIN_KEY and CP_ENTITLEMENT_SIGNING_PRIVATE_KEY if they
are still blank.
EOF
if [[ -t 0 ]]; then
read -r -p "Press Enter to continue after editing ${env_path}..." _
else
die "non-interactive run: edit ${env_path} then re-run setup.sh"
fi
}
validate_env_file() {
local env_path="${PULSE_PROVIDER_MSP_INSTALL_DIR}/.env"
[[ -f "${env_path}" ]] || die "missing ${env_path}"
local expected_env cp_env
expected_env="$(echo "${PULSE_PROVIDER_MSP_EXPECT_ENV}" | tr '[:upper:]' '[:lower:]' | sed -e 's/^[[:space:]]*//' -e 's/[[:space:]]*$//')"
case "${expected_env}" in
production|staging) ;;
*) die "PULSE_PROVIDER_MSP_EXPECT_ENV must be production or staging (got '${PULSE_PROVIDER_MSP_EXPECT_ENV}')" ;;
esac
local missing=()
local k v
for k in DOMAIN ACME_EMAIL CF_DNS_API_TOKEN CP_ENV TRAEFIK_IMAGE DOCKER_SOCKET_PROXY_IMAGE CONTROL_PLANE_IMAGE CP_ADMIN_KEY CP_PULSE_IMAGE 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 CP_PROVIDER_MSP_LICENSE_FILE CP_ENTITLEMENT_SIGNING_PRIVATE_KEY CP_TENANT_MEMORY_LIMIT CP_ALLOW_DOCKERLESS_PROVISIONING CP_STORAGE_GUARDRAILS_ENABLED CP_STORAGE_MIN_ROOT_AVAILABLE CP_STORAGE_MIN_DATA_AVAILABLE CP_STORAGE_MIN_DOCKER_AVAILABLE CP_STORAGE_MAX_DOCKER_BUILD_CACHE CP_PROOF_TENANT_MAX_AGE CP_PROOF_TENANT_MATCHERS CP_REQUIRE_EMAIL_PROVIDER PULSE_EMAIL_FROM PULSE_EMAIL_REPLY_TO; do
v="$(env_value "${k}" "${env_path}")"
if [[ -z "${v}" ]]; then
missing+=("${k}")
fi
done
if [[ "${#missing[@]}" -ne 0 ]]; then
die "missing required values in ${env_path}: ${missing[*]}"
fi
cp_env="$(env_value CP_ENV "${env_path}" | tr '[:upper:]' '[:lower:]')"
if [[ "${cp_env}" != "${expected_env}" ]]; then
die "CP_ENV must be '${expected_env}' for this setup run (got '${cp_env}')"
fi
local path_var path_value
for path_var in PULSE_PROVIDER_MSP_DATA_DIR PULSE_PROVIDER_MSP_DOCKER_SOCKET PULSE_PROVIDER_MSP_ROOT_SPACECHECK_DIR PULSE_PROVIDER_MSP_DOCKER_SPACECHECK_DIR; do
path_value="$(env_value "${path_var}" "${env_path}")"
if [[ "${path_value}" != /* ]]; then
die "${path_var} must be an absolute path"
fi
done
if [[ ! -S "$(env_value PULSE_PROVIDER_MSP_DOCKER_SOCKET "${env_path}")" ]]; then
die "PULSE_PROVIDER_MSP_DOCKER_SOCKET must point to a reachable Docker socket"
fi
local image_ref
for k in TRAEFIK_IMAGE DOCKER_SOCKET_PROXY_IMAGE CONTROL_PLANE_IMAGE CP_PULSE_IMAGE; do
image_ref="$(env_value "${k}" "${env_path}")"
if [[ "${image_ref}" != *@sha256:* || "${image_ref}" == *"<pin>"* ]]; then
die "${k} must be an immutable digest ref (expected ...@sha256:...)"
fi
done
local forbidden forbidden_value
for forbidden in STRIPE_API_KEY STRIPE_WEBHOOK_SECRET CP_TRIAL_SIGNUP_PRICE_ID CP_PUBLIC_CLOUD_SIGNUP_ENABLED CP_MSP_STARTER_PRICE_ID CP_MSP_GROWTH_PRICE_ID CP_MSP_SCALE_PRICE_ID; do
forbidden_value="$(env_value "${forbidden}" "${env_path}")"
if [[ -n "${forbidden_value}" ]]; then
die "${forbidden} must not be configured in provider-hosted MSP mode"
fi
done
if ! falsy "$(env_value CP_ALLOW_DOCKERLESS_PROVISIONING "${env_path}")"; then
die "CP_ALLOW_DOCKERLESS_PROVISIONING must be false for provider-hosted MSP deploys"
fi
if ! truthy "$(env_value CP_STORAGE_GUARDRAILS_ENABLED "${env_path}")"; then
die "CP_STORAGE_GUARDRAILS_ENABLED must be true for provider-hosted MSP deploys"
fi
local admin_key trial_key trusted_cidrs docker_subnet
admin_key="$(env_value CP_ADMIN_KEY "${env_path}")"
if [[ "${#admin_key}" -lt 32 ]]; then
die "CP_ADMIN_KEY must be at least 32 characters"
fi
trial_key="$(env_value CP_ENTITLEMENT_SIGNING_PRIVATE_KEY "${env_path}")"
if ! printf '%s' "${trial_key}" | base64 -d >/dev/null 2>&1; then
die "CP_ENTITLEMENT_SIGNING_PRIVATE_KEY must be valid base64"
fi
docker_subnet="$(env_value PULSE_PROVIDER_MSP_DOCKER_SUBNET "${env_path}")"
trusted_cidrs="$(env_value CP_TRUSTED_PROXY_CIDRS "${env_path}" | tr -d '[:space:]')"
if [[ ",${trusted_cidrs}," != *",${docker_subnet},"* ]]; then
die "CP_TRUSTED_PROXY_CIDRS must include PULSE_PROVIDER_MSP_DOCKER_SUBNET (${docker_subnet})"
fi
local proof_matchers required_matcher
proof_matchers="$(env_value CP_PROOF_TENANT_MATCHERS "${env_path}" | tr '[:upper:]' '[:lower:]' | tr -d '[:space:]')"
for required_matcher in proof canary rehearsal msp_prod ownerseed owner_seed; do
if [[ ",${proof_matchers}," != *",${required_matcher},"* ]]; then
die "CP_PROOF_TENANT_MATCHERS must include '${required_matcher}'"
fi
done
local require_email
require_email="$(env_value CP_REQUIRE_EMAIL_PROVIDER "${env_path}")"
if ! truthy "${require_email}" && ! falsy "${require_email}"; then
die "CP_REQUIRE_EMAIL_PROVIDER must be an explicit boolean value"
fi
if truthy "${require_email}" && [[ -z "$(env_value RESEND_API_KEY "${env_path}")" ]]; then
die "RESEND_API_KEY is required when CP_REQUIRE_EMAIL_PROVIDER=true"
fi
local license_file
license_file="$(env_value CP_PROVIDER_MSP_LICENSE_FILE "${env_path}")"
if [[ "${license_file}" != /* ]]; then
license_file="${PULSE_PROVIDER_MSP_INSTALL_DIR}/${license_file}"
fi
if [[ ! -f "${license_file}" ]]; then
die "CP_PROVIDER_MSP_LICENSE_FILE does not exist: ${license_file}
Request your provider MSP license with this lease signing public key
(./setup.sh --print-lease-signing-public-key):
$(derive_lease_signing_public_key)
The license must bind this key or the control plane will refuse to start."
fi
}
validate_compose_config() {
log "validating compose config"
(cd "${PULSE_PROVIDER_MSP_INSTALL_DIR}" && docker compose config --quiet)
}
pull_provider_images() {
if truthy "${PULSE_PROVIDER_MSP_SKIP_PULL}"; then
log "skipping image pull because PULSE_PROVIDER_MSP_SKIP_PULL=${PULSE_PROVIDER_MSP_SKIP_PULL}"
return 0
fi
log "pulling provider MSP images"
(cd "${PULSE_PROVIDER_MSP_INSTALL_DIR}" && docker compose pull traefik docker-socket-proxy control-plane)
}
run_install_proof_if_requested() {
local mode
mode="$(echo "${PULSE_PROVIDER_MSP_RUN_INSTALL_PROOF}" | tr '[:upper:]' '[:lower:]')"
case "${mode}" in
auto|true|1|yes|on|false|0|no|off) ;;
*) die "PULSE_PROVIDER_MSP_RUN_INSTALL_PROOF must be auto, true, or false" ;;
esac
if falsy "${mode}"; then
return 0
fi
if [[ -z "${PULSE_PROVIDER_MSP_ACCOUNT_NAME}" || -z "${PULSE_PROVIDER_MSP_OWNER_EMAIL}" ]]; then
if truthy "${mode}"; then
die "PULSE_PROVIDER_MSP_ACCOUNT_NAME and PULSE_PROVIDER_MSP_OWNER_EMAIL are required when PULSE_PROVIDER_MSP_RUN_INSTALL_PROOF=true"
fi
return 0
fi
log "running provider MSP install proof"
(
cd "${PULSE_PROVIDER_MSP_INSTALL_DIR}"
PROVIDER_MSP_ACCOUNT_NAME="${PULSE_PROVIDER_MSP_ACCOUNT_NAME}" \
PROVIDER_MSP_OWNER_EMAIL="${PULSE_PROVIDER_MSP_OWNER_EMAIL}" \
./run-install-proof.sh
)
}
print_summary() {
local env_path="${PULSE_PROVIDER_MSP_INSTALL_DIR}/.env"
local domain data_dir network
domain="$(env_value DOMAIN "${env_path}")"
data_dir="$(provider_data_dir)"
network="$(provider_docker_network)"
cat <<EOF
Pulse Provider MSP setup prepared.
Paths:
- Deploy dir: ${PULSE_PROVIDER_MSP_INSTALL_DIR}
- Data dir: ${data_dir}
- Network: ${network}
Proof:
cd ${PULSE_PROVIDER_MSP_INSTALL_DIR}
./run-install-proof.sh --account-name "Example MSP" --owner-email owner@example.com
Portal:
https://${domain}/
Lease signing public key (your provider MSP license must bind this key;
re-print any time with ./setup.sh --print-lease-signing-public-key):
$(derive_lease_signing_public_key)
EOF
}
main() {
need_root
if [[ "${1:-}" == "--print-lease-signing-public-key" ]]; then
ensure_env_file
ensure_generated_secrets
derive_lease_signing_public_key
printf '\n'
exit 0
fi
log "starting provider MSP first-time setup"
apt_install apt-transport-https
install_docker_ce
install_ops_tools
install_deploy_bundle
ensure_env_file
ensure_generated_secrets
validate_env_file
create_data_dirs
ensure_docker_network
block_container_metadata_service
validate_compose_config
pull_provider_images
run_install_proof_if_requested
print_summary
}
main "$@"