mirror of
https://github.com/rcourtman/Pulse.git
synced 2026-10-19 10:33:29 +00:00
Publish provider MSP control-plane image
This commit is contained in:
parent
25febfbf82
commit
42fb8eed3f
6 changed files with 149 additions and 1 deletions
47
.github/workflows/publish-docker.yml
vendored
47
.github/workflows/publish-docker.yml
vendored
|
|
@ -183,6 +183,46 @@ jobs:
|
|||
push-to-registry: true
|
||||
create-storage-record: false
|
||||
|
||||
- name: Build and push Pulse control-plane image (multi-arch)
|
||||
id: build_control_plane_image
|
||||
uses: docker/build-push-action@10e90e3645eae34f1e60eeb005ba3a3d33f178e8 # v6
|
||||
with:
|
||||
context: .
|
||||
file: deploy/provider-msp/Dockerfile.control-plane
|
||||
platforms: linux/amd64,linux/arm64
|
||||
push: true
|
||||
provenance: mode=max
|
||||
sbom: true
|
||||
cache-from: type=registry,ref=ghcr.io/${{ github.repository_owner }}/pulse-control-plane:buildcache
|
||||
build-args: |
|
||||
VERSION=${{ steps.version.outputs.tag }}
|
||||
PULSE_LICENSE_PUBLIC_KEY_SHA256=${{ steps.license_key_cache.outputs.sha256 }}
|
||||
secrets: |
|
||||
pulse_license_public_key=${{ secrets.PULSE_LICENSE_PUBLIC_KEY }}
|
||||
tags: |
|
||||
rcourtman/pulse-control-plane:${{ steps.version.outputs.tag }}
|
||||
rcourtman/pulse-control-plane:${{ steps.version.outputs.version }}
|
||||
${{ steps.version.outputs.is_prerelease != 'true' && 'rcourtman/pulse-control-plane:latest' || '' }}
|
||||
ghcr.io/${{ github.repository_owner }}/pulse-control-plane:${{ steps.version.outputs.tag }}
|
||||
ghcr.io/${{ github.repository_owner }}/pulse-control-plane:${{ steps.version.outputs.version }}
|
||||
${{ steps.version.outputs.is_prerelease != 'true' && format('ghcr.io/{0}/pulse-control-plane:latest', github.repository_owner) || '' }}
|
||||
|
||||
- name: Attest Pulse control-plane image on Docker Hub
|
||||
uses: actions/attest@59d89421af93a897026c735860bf21b6eb4f7b26 # v4
|
||||
with:
|
||||
subject-name: docker.io/rcourtman/pulse-control-plane
|
||||
subject-digest: ${{ steps.build_control_plane_image.outputs.digest }}
|
||||
push-to-registry: true
|
||||
create-storage-record: false
|
||||
|
||||
- name: Attest Pulse control-plane image on GHCR
|
||||
uses: actions/attest@59d89421af93a897026c735860bf21b6eb4f7b26 # v4
|
||||
with:
|
||||
subject-name: ghcr.io/${{ github.repository_owner }}/pulse-control-plane
|
||||
subject-digest: ${{ steps.build_control_plane_image.outputs.digest }}
|
||||
push-to-registry: true
|
||||
create-storage-record: false
|
||||
|
||||
- name: Output image information
|
||||
run: |
|
||||
IS_PRERELEASE="${{ steps.version.outputs.is_prerelease }}"
|
||||
|
|
@ -195,6 +235,13 @@ jobs:
|
|||
echo " - rcourtman/pulse:latest"
|
||||
fi
|
||||
echo ""
|
||||
echo "Control-plane images (linux/amd64, linux/arm64):"
|
||||
echo " - rcourtman/pulse-control-plane:${{ steps.version.outputs.tag }}"
|
||||
echo " - rcourtman/pulse-control-plane:${{ steps.version.outputs.version }}"
|
||||
if [ "$IS_PRERELEASE" != "true" ]; then
|
||||
echo " - rcourtman/pulse-control-plane:latest"
|
||||
fi
|
||||
echo ""
|
||||
echo "Pulse Agent binaries ship as release assets, not as a Docker image."
|
||||
echo "See the GitHub release page for pulse-agent-{darwin,freebsd,linux,windows}-{amd64,arm64,...}."
|
||||
echo ""
|
||||
|
|
|
|||
44
deploy/provider-msp/Dockerfile.control-plane
Normal file
44
deploy/provider-msp/Dockerfile.control-plane
Normal file
|
|
@ -0,0 +1,44 @@
|
|||
# syntax=docker/dockerfile:1.7
|
||||
|
||||
FROM --platform=$BUILDPLATFORM golang:1.25.9-alpine@sha256:5caaf1cca9dc351e13deafbc3879fd4754801acba8653fa9540cea125d01a71f AS builder
|
||||
ARG VERSION=dev
|
||||
ARG BUILD_TIME=unknown
|
||||
ARG GIT_COMMIT=unknown
|
||||
ARG TARGETOS
|
||||
ARG TARGETARCH
|
||||
ARG PULSE_LICENSE_PUBLIC_KEY_SHA256
|
||||
WORKDIR /src
|
||||
COPY go.mod go.sum ./
|
||||
RUN --mount=type=cache,id=pulse-control-plane-go-mod,target=/go/pkg/mod \
|
||||
--mount=type=cache,id=pulse-control-plane-go-build,target=/root/.cache/go-build \
|
||||
go mod download
|
||||
COPY cmd/ ./cmd/
|
||||
COPY internal/ ./internal/
|
||||
COPY pkg/ ./pkg/
|
||||
COPY scripts/release_ldflags.sh ./scripts/release_ldflags.sh
|
||||
RUN --mount=type=cache,id=pulse-control-plane-go-mod,target=/go/pkg/mod \
|
||||
--mount=type=cache,id=pulse-control-plane-go-build,target=/root/.cache/go-build \
|
||||
--mount=type=secret,id=pulse_license_public_key,required=false \
|
||||
set -eu; \
|
||||
EXPECTED_LICENSE_PUBLIC_KEY_SHA256="${PULSE_LICENSE_PUBLIC_KEY_SHA256#SHA256:}"; \
|
||||
if [ -z "${EXPECTED_LICENSE_PUBLIC_KEY_SHA256}" ]; then echo "Error: PULSE_LICENSE_PUBLIC_KEY_SHA256 is required for control-plane release image builds." >&2; exit 1; fi; \
|
||||
if [ ! -f /run/secrets/pulse_license_public_key ]; then echo "Error: PULSE_LICENSE_PUBLIC_KEY_SHA256 was provided but no license public key was mounted." >&2; exit 1; fi; \
|
||||
LICENSE_PUBLIC_KEY="$(tr -d '\r\n' < /run/secrets/pulse_license_public_key)"; \
|
||||
LICENSE_PUBLIC_KEY_BYTES="$(printf '%s' "${LICENSE_PUBLIC_KEY}" | base64 -d | wc -c | tr -d ' ')"; \
|
||||
if [ "${LICENSE_PUBLIC_KEY_BYTES}" != "32" ]; then echo "Error: mounted license public key must decode to 32 bytes." >&2; exit 1; fi; \
|
||||
ACTUAL_LICENSE_PUBLIC_KEY_SHA256="$(printf '%s' "${LICENSE_PUBLIC_KEY}" | base64 -d | sha256sum | awk '{print $1}')"; \
|
||||
if [ "${ACTUAL_LICENSE_PUBLIC_KEY_SHA256}" != "${EXPECTED_LICENSE_PUBLIC_KEY_SHA256}" ]; then echo "Error: mounted license public key does not match PULSE_LICENSE_PUBLIC_KEY_SHA256." >&2; exit 1; fi; \
|
||||
TARGET_GOOS="${TARGETOS:-linux}"; \
|
||||
TARGET_GOARCH="${TARGETARCH:-$(go env GOARCH)}"; \
|
||||
SERVER_LDFLAGS="$(./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 \
|
||||
-ldflags="${SERVER_LDFLAGS}" \
|
||||
-buildvcs=false \
|
||||
-trimpath \
|
||||
-o /pulse-control-plane ./cmd/pulse-control-plane
|
||||
|
||||
FROM alpine:3.20@sha256:d9e853e87e55526f6b2917df91a2115c36dd7c696a35be12163d44e6e2a4b6bc
|
||||
RUN apk add --no-cache ca-certificates docker-cli
|
||||
COPY --from=builder /pulse-control-plane /usr/local/bin/pulse-control-plane
|
||||
ENTRYPOINT ["pulse-control-plane"]
|
||||
|
|
@ -132,7 +132,9 @@ install_args=(
|
|||
if [[ "$skip_runtime_image_pull" == "1" ]]; then
|
||||
install_args+=(--skip-image-pull)
|
||||
fi
|
||||
install_args+=("${extra_install_args[@]}")
|
||||
if ((${#extra_install_args[@]} > 0)); then
|
||||
install_args+=("${extra_install_args[@]}")
|
||||
fi
|
||||
|
||||
docker compose run --rm --no-deps control-plane "${install_args[@]}"
|
||||
|
||||
|
|
|
|||
|
|
@ -696,6 +696,19 @@ be reused for release-grade hosted or self-hosted runtime images. The matching
|
|||
installability proof lives in `scripts/installtests/build_release_assets_test.go`
|
||||
and `scripts/release_control/release_promotion_policy_test.py`, and both must
|
||||
assert the secret mount and non-secret fingerprint argument together.
|
||||
The standalone hosted control-plane image is part of the same release-license
|
||||
boundary. `deploy/provider-msp/Dockerfile.control-plane` must build
|
||||
`cmd/pulse-control-plane` with `-tags release`, canonical
|
||||
`scripts/release_ldflags.sh server` metadata, an embedded license public key
|
||||
from the BuildKit `pulse_license_public_key` secret, and the same
|
||||
`PULSE_LICENSE_PUBLIC_KEY_SHA256` fingerprint gate. Provider-hosted MSP uses
|
||||
that control-plane image for signed MSP-license enforcement, so it must not be
|
||||
possible to publish a provider MSP control-plane image that accepts
|
||||
`PULSE_LICENSE_DEV_MODE` or `PULSE_LICENSE_PUBLIC_KEY` runtime overrides.
|
||||
`.github/workflows/publish-docker.yml` must publish and attest
|
||||
`rcourtman/pulse-control-plane` and
|
||||
`ghcr.io/<owner>/pulse-control-plane` from that Dockerfile, with the same
|
||||
version tags and prerelease/latest tag policy as the main Pulse runtime image.
|
||||
That same supply-chain boundary also owns the checked-in build roots
|
||||
themselves. `Dockerfile` must pin its Node, Go, and Alpine bases by immutable
|
||||
manifest-list digest so multi-arch release builds do not silently drift onto a
|
||||
|
|
|
|||
|
|
@ -563,12 +563,18 @@ func TestReleaseWorkflowsUseSecretSafeAttestedImageBuilds(t *testing.T) {
|
|||
`sbom: true`,
|
||||
`secrets: |`,
|
||||
`id: license_key_cache`,
|
||||
`id: build_control_plane_image`,
|
||||
`file: deploy/provider-msp/Dockerfile.control-plane`,
|
||||
`PULSE_LICENSE_PUBLIC_KEY_SHA256=${{ steps.license_key_cache.outputs.sha256 }}`,
|
||||
`PULSE_UPDATE_SIGNING_PUBLIC_KEY=${{ vars.PULSE_UPDATE_SIGNING_PUBLIC_KEY }}`,
|
||||
`pulse_license_public_key=${{ secrets.PULSE_LICENSE_PUBLIC_KEY }}`,
|
||||
`pulse_update_signing_key=${{ secrets.PULSE_UPDATE_SIGNING_KEY }}`,
|
||||
`subject-name: docker.io/rcourtman/pulse`,
|
||||
`subject-name: ghcr.io/${{ github.repository_owner }}/pulse`,
|
||||
`subject-name: docker.io/rcourtman/pulse-control-plane`,
|
||||
`subject-name: ghcr.io/${{ github.repository_owner }}/pulse-control-plane`,
|
||||
`rcourtman/pulse-control-plane:${{ steps.version.outputs.tag }}`,
|
||||
`ghcr.io/${{ github.repository_owner }}/pulse-control-plane:${{ steps.version.outputs.tag }}`,
|
||||
// pulse-agent ships as release-asset binaries, not as a Docker
|
||||
// image (see commit dropping the agent image publish steps).
|
||||
// The agent attestation subject-names intentionally do not
|
||||
|
|
|
|||
|
|
@ -149,6 +149,7 @@ func TestProviderMSPInstallProofRunnerMatchesComposeContract(t *testing.T) {
|
|||
"--install-type",
|
||||
"--target-path",
|
||||
"--skip-image-pull",
|
||||
"${#extra_install_args[@]}",
|
||||
"docker compose run --rm --no-deps control-plane",
|
||||
"docker compose up -d traefik control-plane",
|
||||
"provider-msp status",
|
||||
|
|
@ -176,6 +177,41 @@ func TestProviderMSPTraefikUsesProviderNetwork(t *testing.T) {
|
|||
)
|
||||
}
|
||||
|
||||
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=$BUILDPLATFORM golang:1.25.9-alpine@sha256:5caaf1cca9dc351e13deafbc3879fd4754801acba8653fa9540cea125d01a71f 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.",
|
||||
`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 {
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue