mirror of
https://github.com/rcourtman/Pulse.git
synced 2026-09-10 02:25:56 +00:00
Let providers evaluate MSP without asking permission first
Two mandatory round-trips stood between an interested MSP and their first screen, and neither was technical. setup.sh required four image digests shipped as literal <pin> placeholders, so the only way to get them was to ask. All four images are publicly readable, so there was never anything to hand out. setup.sh now resolves each blank pin to an immutable digest from its published tag via buildx imagetools and writes it back to .env; hand-set values are left alone. setup.sh then died outright without a licence file, so nobody could start the stack, create a workspace, or see the portal until a human minted a licence for them. The control plane already ran unlicensed via ProviderMSPPlanSourceEnvFallback; only the installer refused. A licence path that is set but missing is still a hard failure, since that is a misconfiguration rather than a choice. Unlicensed now means evaluation rather than the cheapest paid tier. The env fallback defaulted to msp_starter, handing every unlicensed deployment the full 5-client Starter allowance and leaving no boundary between evaluating and buying. Adds msp_eval at 2 workspaces: same capabilities, smaller cap, not purchasable, not on the public ladder. An isolation guarantee is the one claim a provider cannot evaluate from a screenshot, and both MSP leads this year went quiet at exactly this step. Contracts: cloud-paid records the unlicensed plan rule and the strictly-below-paid invariant; deployment-installability records credential-free, correspondence-free installability. Verification: TestMSPEvalCapStaysBelowCheapestPaidTier, TestCanonicalizePlanVersion_MSPEval, TestProviderMSPSetupScriptSupportsUnlicensedEvaluation. The last was negative-tested by reintroducing a <pin> placeholder and confirming it fails. ensure_image_pins exercised against the live registries. licensing, cloudcp, control-plane and installtests all green.
This commit is contained in:
@@ -8,11 +8,16 @@ ACME_EMAIL=admin@example.com
|
||||
# Cloudflare DNS-01 wildcard TLS
|
||||
CF_DNS_API_TOKEN=
|
||||
|
||||
# Image pins; use immutable digest refs in production
|
||||
TRAEFIK_IMAGE=traefik@sha256:<pin>
|
||||
DOCKER_SOCKET_PROXY_IMAGE=tecnativa/docker-socket-proxy@sha256:<pin>
|
||||
CONTROL_PLANE_IMAGE=ghcr.io/rcourtman/pulse-control-plane@sha256:<pin>
|
||||
CP_PULSE_IMAGE=ghcr.io/rcourtman/pulse@sha256:<pin>
|
||||
# Image pins. Leave blank and setup.sh resolves each one to an immutable
|
||||
# digest from its published tag, then writes the digest back here. All four
|
||||
# images are public, so this needs no registry credentials.
|
||||
#
|
||||
# Pin them by hand instead if you want to hold a specific build; setup.sh
|
||||
# leaves any value you set alone.
|
||||
TRAEFIK_IMAGE=
|
||||
DOCKER_SOCKET_PROXY_IMAGE=
|
||||
CONTROL_PLANE_IMAGE=
|
||||
CP_PULSE_IMAGE=
|
||||
|
||||
# Control plane
|
||||
CP_ENV=production
|
||||
@@ -25,7 +30,14 @@ 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
|
||||
|
||||
# Licence file. Leave BLANK to evaluate: the control plane runs on msp_eval,
|
||||
# which is the full product capped at 2 client workspaces, so you can stand
|
||||
# this up and onboard two real clients without talking to anyone first.
|
||||
#
|
||||
# Set it to your licence path once you buy. Paid client caps come from the
|
||||
# licence, never from this file.
|
||||
CP_PROVIDER_MSP_LICENSE_FILE=
|
||||
# Entitlement lease signing key. setup.sh generates this; the private key
|
||||
# never leaves this host. Your provider MSP license must bind the derived
|
||||
# PUBLIC key (entitlement_signing_public_key); print it with
|
||||
|
||||
@@ -281,6 +281,59 @@ set_env_value() {
|
||||
rm -f "${tmp}"
|
||||
}
|
||||
|
||||
# default_image_ref maps each image variable to the tag its digest is resolved
|
||||
# from when the operator has not pinned one by hand.
|
||||
default_image_ref() {
|
||||
case "$1" in
|
||||
TRAEFIK_IMAGE) echo "traefik:v3" ;;
|
||||
DOCKER_SOCKET_PROXY_IMAGE) echo "tecnativa/docker-socket-proxy:latest" ;;
|
||||
CONTROL_PLANE_IMAGE) echo "ghcr.io/rcourtman/pulse-control-plane:latest" ;;
|
||||
CP_PULSE_IMAGE) echo "ghcr.io/rcourtman/pulse:latest" ;;
|
||||
*) return 1 ;;
|
||||
esac
|
||||
}
|
||||
|
||||
# resolve_image_digest turns a tag into an immutable digest ref. Uses buildx
|
||||
# imagetools, which the Docker install above provides, and which reads the
|
||||
# registry without pulling the image.
|
||||
resolve_image_digest() {
|
||||
local ref="$1" digest
|
||||
digest="$(docker buildx imagetools inspect "${ref}" --format '{{.Manifest.Digest}}' 2>/dev/null || true)"
|
||||
[[ "${digest}" == sha256:* ]] || return 1
|
||||
printf '%s@%s\n' "${ref%:*}" "${digest}"
|
||||
}
|
||||
|
||||
# ensure_image_pins fills in any image variable the operator left blank or on
|
||||
# the shipped <pin> placeholder.
|
||||
#
|
||||
# The bundle used to ship four unfillable "@sha256:<pin>" placeholders that
|
||||
# setup.sh then refused to run without, so the only way to obtain them was to
|
||||
# ask us. All four images are publicly readable, so there was never anything
|
||||
# to hand out; it just meant nobody could start without a conversation first.
|
||||
#
|
||||
# Still resolved to an immutable digest, not left on a floating tag, so a
|
||||
# rebuild of the tag cannot silently change what a provider is running.
|
||||
ensure_image_pins() {
|
||||
local env_path="${PULSE_PROVIDER_MSP_INSTALL_DIR}/.env"
|
||||
[[ -f "${env_path}" ]] || die "missing ${env_path}"
|
||||
|
||||
local key current ref resolved
|
||||
for key in TRAEFIK_IMAGE DOCKER_SOCKET_PROXY_IMAGE CONTROL_PLANE_IMAGE CP_PULSE_IMAGE; do
|
||||
current="$(env_value "${key}" "${env_path}")"
|
||||
if [[ -n "${current}" && "${current}" != *"<pin>"* ]]; then
|
||||
continue
|
||||
fi
|
||||
ref="$(default_image_ref "${key}")" || die "no default image ref for ${key}"
|
||||
log "resolving ${key} digest from ${ref}"
|
||||
if ! resolved="$(resolve_image_digest "${ref}")"; then
|
||||
die "could not resolve a digest for ${ref}
|
||||
Set ${key} in ${env_path} by hand, or check this host can reach the registry."
|
||||
fi
|
||||
log " ${resolved}"
|
||||
set_env_value "${key}" "${resolved}" "${env_path}"
|
||||
done
|
||||
}
|
||||
|
||||
ensure_generated_secrets() {
|
||||
local env_path="${PULSE_PROVIDER_MSP_INSTALL_DIR}/.env"
|
||||
[[ -f "${env_path}" ]] || die "missing ${env_path}"
|
||||
@@ -377,7 +430,6 @@ Edit it now and set required values:
|
||||
- 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.
|
||||
@@ -404,7 +456,7 @@ validate_env_file() {
|
||||
|
||||
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
|
||||
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_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}")
|
||||
@@ -485,14 +537,31 @@ validate_env_file() {
|
||||
die "RESEND_API_KEY is required when CP_REQUIRE_EMAIL_PROVIDER=true"
|
||||
fi
|
||||
|
||||
# An empty CP_PROVIDER_MSP_LICENSE_FILE is evaluation mode, not a mistake.
|
||||
#
|
||||
# This used to be mandatory, which meant nobody could start the stack, create
|
||||
# a client workspace, or see the portal until they had emailed for a licence
|
||||
# and waited for a human to mint one. That put a round-trip with us in front
|
||||
# of the first screen, and an isolation guarantee is the one claim a provider
|
||||
# cannot evaluate from a screenshot.
|
||||
#
|
||||
# Unlicensed runs on msp_eval (2 client workspaces). Set the licence file
|
||||
# when you buy; the paid caps come from the licence, never from here.
|
||||
local license_file
|
||||
license_file="$(env_value CP_PROVIDER_MSP_LICENSE_FILE "${env_path}")"
|
||||
if [[ -z "${license_file}" ]]; then
|
||||
log "no CP_PROVIDER_MSP_LICENSE_FILE set: evaluation mode, 2 client workspaces"
|
||||
log "to buy, request a licence bound to this lease signing public key:"
|
||||
log " $(derive_lease_signing_public_key)"
|
||||
return 0
|
||||
fi
|
||||
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
|
||||
die "CP_PROVIDER_MSP_LICENSE_FILE is set but does not exist: ${license_file}
|
||||
Leave it blank to run in evaluation mode (2 client workspaces), or 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."
|
||||
@@ -598,6 +667,9 @@ main() {
|
||||
install_deploy_bundle
|
||||
ensure_env_file
|
||||
ensure_generated_secrets
|
||||
# After install_docker_ce, which provides the buildx used to read the
|
||||
# registry, and before validation, which requires the pins to be set.
|
||||
ensure_image_pins
|
||||
validate_env_file
|
||||
create_data_dirs
|
||||
ensure_docker_network
|
||||
|
||||
Reference in New Issue
Block a user