From 467e4de71545504a911886e4c99ce499d1b602f8 Mon Sep 17 00:00:00 2001 From: Josh Siviour Date: Mon, 8 Jun 2026 09:48:59 +1000 Subject: [PATCH] feat(eks): publish eks-node AMI to R2 + spx catalogue entry (#313) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * feat(eks): publish eks-node AMI to R2 + spx catalogue entry Add a spinifex-eks-node catalogue entry so operators install the EKS node image with a single command: spx admin images import --name spinifex-eks-node The entry pulls the Alpine+K3s image from iso.mulgadc.com, verifies its sha256, and stamps spinifex:managed-by=eks — the tag the control-plane and node-group launchers resolve the boot AMI by, so no extra config is needed. Add scripts/publish-system-image.sh (+ make publish-eks-node-image) to convert the built raw to a compressed qcow2, emit a checksum sidecar, and upload both to the R2 bucket backing iso.mulgadc.com/system-ami via the S3 API. * ci(eks): add manual workflow to build + publish eks-node AMI to R2 Build the eks-node AMI on the runner (libguestfs) and publish via publish-system-image.sh using the existing CF_R2_* secrets. Manual workflow_dispatch with a dry-run input. Fix the publish script's default bucket to spinifex-iso (the bucket iso.mulgadc.com is served from). --- .github/workflows/publish-eks-ami.yml | 92 +++++++++++ Makefile | 5 +- .../spinifex-admin-cli/README.md | 19 +++ scripts/publish-system-image.sh | 147 ++++++++++++++++++ spinifex/utils/images.go | 21 +++ 5 files changed, 283 insertions(+), 1 deletion(-) create mode 100644 .github/workflows/publish-eks-ami.yml create mode 100755 scripts/publish-system-image.sh diff --git a/.github/workflows/publish-eks-ami.yml b/.github/workflows/publish-eks-ami.yml new file mode 100644 index 000000000..89b0d6483 --- /dev/null +++ b/.github/workflows/publish-eks-ami.yml @@ -0,0 +1,92 @@ +name: Publish EKS node AMI + +# Builds the eks-node system AMI (libguestfs, on the runner) and publishes the +# compressed qcow2 + .sha256 sidecar to the Cloudflare R2 bucket that backs +# iso.mulgadc.com/system-ami/. Operators then install it on any cluster with: +# spx admin images import --name spinifex-eks-node +# +# Manual-only: publishing overwrites the mirror artifact, so it is a deliberate +# action, not something every push should trigger. + +on: + workflow_dispatch: + inputs: + dry_run: + description: "Build + convert only; skip the R2 upload" + type: boolean + default: false + +permissions: + contents: read + +concurrency: + group: publish-eks-ami + cancel-in-progress: false + +jobs: + publish: + name: Build + publish eks-node AMI + runs-on: ubuntu-24.04 + + steps: + - name: Checkout Spinifex + uses: actions/checkout@v6 + with: + path: spinifex + + - name: Checkout Viperblock + uses: actions/checkout@v6 + with: + repository: mulgadc/viperblock + ref: dev + path: viperblock + + - name: Checkout Predastore + uses: actions/checkout@v6 + with: + repository: mulgadc/predastore + ref: dev + path: predastore + + - name: Setup Go + uses: actions/setup-go@v6 + with: + cache-dependency-path: "spinifex/go.sum" + go-version-file: "spinifex/go.mod" + + - name: Setup Go Workspace + run: go work init ./spinifex ./viperblock ./predastore + + - name: Install build + upload tooling + run: | + set -euo pipefail + sudo apt-get update -qq + sudo apt-get install -y --no-install-recommends libguestfs-tools qemu-utils + if ! command -v aws &>/dev/null; then + curl -fsSL "https://awscli.amazonaws.com/awscli-exe-linux-x86_64.zip" -o /tmp/awscliv2.zip + unzip -q /tmp/awscliv2.zip -d /tmp/aws-install + sudo /tmp/aws-install/aws/install + rm -rf /tmp/awscliv2.zip /tmp/aws-install + fi + + - name: Build + publish to R2 + working-directory: spinifex + env: + # Map the repo's R2 secrets onto the names publish-system-image.sh reads. + AWS_ACCESS_KEY_ID: ${{ secrets.CF_R2_ACCESS_KEY_ID }} + AWS_SECRET_ACCESS_KEY: ${{ secrets.CF_R2_SECRET_ACCESS_KEY }} + R2_ENDPOINT: ${{ secrets.CF_R2_ENDPOINT_URL }} + R2_BUCKET: spinifex-iso + DRY_RUN: ${{ inputs.dry_run }} + run: | + set -euo pipefail + # libguestfs needs to read a host kernel; Ubuntu ships it mode 0600. + # direct backend = no libvirt on the runner. + export LIBGUESTFS_BACKEND=direct + sudo chmod 0644 /boot/vmlinuz-* || true + FLAGS=(--build) + if [[ "${DRY_RUN}" == "true" ]]; then + FLAGS+=(--dry-run) + fi + ./scripts/publish-system-image.sh \ + scripts/images/eks-node/manifest.conf "${FLAGS[@]}" diff --git a/Makefile b/Makefile index 4bf597b2f..889b39853 100644 --- a/Makefile +++ b/Makefile @@ -92,6 +92,9 @@ build-eks-node-image: ## Build the unified eks-node AMI (K3s server+agent; role import-eks-node-image: ## Build + register the eks-node AMI (requires a running cluster) $(MAKE) build-system-image IMAGE=eks-node IMPORT=1 +publish-eks-node-image: ## Build + publish the eks-node AMI to Cloudflare R2 (needs R2_ENDPOINT + AWS_* env) + ./scripts/publish-system-image.sh scripts/images/eks-node/manifest.conf --build + MICROVM_OUT_DIR := build/microvm MICROVM_ARTIFACTS := $(MICROVM_OUT_DIR)/vmlinuz $(MICROVM_OUT_DIR)/initramfs.cpio.gz MICROVM_INPUTS := scripts/build-microvm-image.sh $(MICROVM_OUT_DIR)/init.sh $(MICROVM_OUT_DIR)/inittab bin/lb-agent @@ -350,7 +353,7 @@ ansible-cluster-bootstrap: $(if $(POOL),-e cluster_external_pool=$(POOL),) \ $(_ANSIBLE_EXTRA) -.PHONY: build build-ui build-installer build-lb-agent build-system-image build-eks-node-image import-eks-node-image build-microvm-image install-microvm go_build go_run preflight test test-cover test-race diff-coverage bench run test-actions test-harness manifest-check diff-coverage bench run \ +.PHONY: build build-ui build-installer build-lb-agent build-system-image build-eks-node-image import-eks-node-image publish-eks-node-image build-microvm-image install-microvm go_build go_run preflight test test-cover test-race diff-coverage bench run test-actions test-harness manifest-check diff-coverage bench run \ deploy reinstall clean \ install-system install-go install-aws quickinstall \ lint fix govulncheck \ diff --git a/docs/administration/spinifex-admin-cli/README.md b/docs/administration/spinifex-admin-cli/README.md index 70944a37e..0a503a12a 100644 --- a/docs/administration/spinifex-admin-cli/README.md +++ b/docs/administration/spinifex-admin-cli/README.md @@ -82,6 +82,25 @@ spx admin images import --name debian-13-arm64 Catalog imports verify the image against the catalog-declared SHA-256/SHA-512 digest before extraction. Use `--file` to import operator-supplied media (verification skipped — operator is responsible for integrity), or `--force` to re-download after a checksum mismatch. +### EKS node image + +To run EKS, import the prebuilt node image from the catalog: + +```bash +spx admin images import --name spinifex-eks-node +``` + +This pulls the Alpine + K3s node AMI from `iso.mulgadc.com`, verifies its +checksum, and registers it tagged `spinifex:managed-by=eks`. `eks create-cluster` +and `eks create-nodegroup` resolve the boot AMI by that tag, so no further +configuration is needed. + +Operators publishing a freshly built node image to the mirror run the +**Publish EKS node AMI** GitHub Actions workflow (manual dispatch; uses the +`CF_R2_*` repo secrets) or, locally, `make publish-eks-node-image` (see +`scripts/publish-system-image.sh`; requires `R2_ENDPOINT` + R2-scoped +`AWS_ACCESS_KEY_ID`/`AWS_SECRET_ACCESS_KEY`). + ## Cluster Shutdown diff --git a/scripts/publish-system-image.sh b/scripts/publish-system-image.sh new file mode 100755 index 000000000..3a5c05df1 --- /dev/null +++ b/scripts/publish-system-image.sh @@ -0,0 +1,147 @@ +#!/bin/bash +set -euo pipefail + +# publish-system-image.sh — Publish a built system AMI to Cloudflare R2 +# +# Converts the raw image produced by build-system-image.sh into a compressed +# qcow2, writes a .sha256 sidecar (coreutils format, matched by the spx catalog +# checksum verifier), and uploads both to the R2 bucket that backs +# iso.mulgadc.com/system-ami/. The matching catalog entry in +# spinifex/spinifex/utils/images.go then lets operators run: +# +# spx admin images import --name +# +# Object name is -.qcow2 — this MUST match the +# basename of the catalog entry's URL. +# +# Requirements: qemu-img, sha256sum, aws (CLI; R2 speaks the S3 API) +# Usage: ./scripts/publish-system-image.sh [--build] [--dry-run] +# --build Run build-system-image.sh first (otherwise reuse the cached raw) +# --dry-run Print the convert/checksum/upload steps without uploading +# +# Env (required unless --dry-run): +# R2_ENDPOINT https://.r2.cloudflarestorage.com +# R2_BUCKET defaults to "spinifex-iso" (backs iso.mulgadc.com) +# AWS_ACCESS_KEY_ID R2 token access key +# AWS_SECRET_ACCESS_KEY R2 token secret + +SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)" + +usage() { + echo "Usage: $0 [--build] [--dry-run]" + exit 1 +} + +if [[ $# -lt 1 || "$1" == "-h" || "$1" == "--help" ]]; then + usage +fi + +MANIFEST="$1" +shift + +if [[ ! -f "$MANIFEST" ]]; then + echo "ERROR: Manifest not found: $MANIFEST" + exit 1 +fi + +DO_BUILD=false +DRY_RUN=false +for arg in "$@"; do + case "$arg" in + --build) DO_BUILD=true ;; + --dry-run) DRY_RUN=true ;; + *) echo "ERROR: unknown flag: $arg"; usage ;; + esac +done + +# shellcheck source=/dev/null +source "$MANIFEST" + +DISTRO="${DISTRO:-alpine}" +ARCH="x86_64" # build-system-image.sh builds x86_64 only today +R2_BUCKET="${R2_BUCKET:-spinifex-iso}" # iso.mulgadc.com is served from this bucket + +if [[ -z "${IMAGE_NAME:-}" ]]; then + echo "ERROR: Manifest missing required field: IMAGE_NAME" + exit 1 +fi + +# Mirror build-system-image.sh's derived paths so we reuse its cached raw. +BUILD_DIR="/tmp/${IMAGE_NAME}-image-build" +RAW_IMAGE="${BUILD_DIR}/${IMAGE_NAME}-${DISTRO}.raw" + +OBJECT_BASE="${AMI_NAME:-$IMAGE_NAME}-${ARCH}" +QCOW2_OUT="${BUILD_DIR}/${OBJECT_BASE}.qcow2" +SHA_OUT="${QCOW2_OUT}.sha256" +OBJECT_QCOW2="${OBJECT_BASE}.qcow2" +OBJECT_SHA="${OBJECT_BASE}.qcow2.sha256" + +for tool in qemu-img sha256sum; do + if ! command -v "$tool" &>/dev/null; then + echo "ERROR: $tool not found" + exit 1 + fi +done +if [[ "$DRY_RUN" == false ]] && ! command -v aws &>/dev/null; then + echo "ERROR: aws CLI not found (required for R2 upload; use --dry-run to skip)" + exit 1 +fi + +if [[ "$DO_BUILD" == true ]]; then + echo "=== Building ${IMAGE_NAME} ===" + "${SCRIPT_DIR}/build-system-image.sh" "$MANIFEST" +fi + +if [[ ! -f "$RAW_IMAGE" ]]; then + echo "ERROR: Raw image not found: $RAW_IMAGE" + echo " Run with --build, or 'make build-system-image IMAGE=...' first." + exit 1 +fi + +if [[ "$DRY_RUN" == false ]]; then + for var in R2_ENDPOINT AWS_ACCESS_KEY_ID AWS_SECRET_ACCESS_KEY; do + if [[ -z "${!var:-}" ]]; then + echo "ERROR: $var must be set (or use --dry-run)" + exit 1 + fi + done +fi + +echo "=== Publish System Image ===" +echo " Manifest: ${MANIFEST}" +echo " Raw: ${RAW_IMAGE}" +echo " Object: s3://${R2_BUCKET}/system-ami/${OBJECT_QCOW2}" +echo "" + +run() { + echo "+ $*" + if [[ "$DRY_RUN" == false ]]; then + "$@" + fi +} + +# Compressed qcow2 keeps the transfer small (consistent with the GPU images). +run qemu-img convert -c -f raw -O qcow2 "$RAW_IMAGE" "$QCOW2_OUT" + +# Sidecar in coreutils format: " ". The spx catalog verifier +# matches the entry by the downloaded file's basename, so write the basename +# the catalog URL resolves to, not the local build path. +if [[ "$DRY_RUN" == false ]]; then + ( cd "$(dirname "$QCOW2_OUT")" && sha256sum "$(basename "$QCOW2_OUT")" > "$(basename "$SHA_OUT")" ) + echo " sha256: $(cut -d' ' -f1 "$SHA_OUT")" +else + echo "+ sha256sum ${OBJECT_QCOW2} > ${OBJECT_SHA}" +fi + +S3_PREFIX="s3://${R2_BUCKET}/system-ami" +run aws s3 cp --endpoint-url "${R2_ENDPOINT:-}" "$QCOW2_OUT" "${S3_PREFIX}/${OBJECT_QCOW2}" +run aws s3 cp --endpoint-url "${R2_ENDPOINT:-}" "$SHA_OUT" "${S3_PREFIX}/${OBJECT_SHA}" + +echo "" +if [[ "$DRY_RUN" == true ]]; then + echo "=== Dry run complete (no upload) ===" +else + echo "=== Published ===" + echo " https://iso.mulgadc.com/system-ami/${OBJECT_QCOW2}" + echo " https://iso.mulgadc.com/system-ami/${OBJECT_SHA}" +fi diff --git a/spinifex/utils/images.go b/spinifex/utils/images.go index 14486a384..e103f7db5 100644 --- a/spinifex/utils/images.go +++ b/spinifex/utils/images.go @@ -462,6 +462,27 @@ var AvailableImages = map[string]Images{ BootMode: "uefi", Tags: map[string]string{"gpu-vendor": "amd"}, }, + + // EKS node system AMI. The control-plane + node-group launchers resolve the + // boot AMI by the spinifex:managed-by=eks tag (not by name), so importing + // this catalog entry — which copies the tag onto the AMI — is enough for + // CreateCluster/CreateNodegroup to find it. Built from + // scripts/images/eks-node/manifest.conf (Alpine bios base); published to R2 + // with scripts/publish-system-image.sh. + "spinifex-eks-node": { + Name: "spinifex-eks-node", + Description: "Mulga EKS node image — Alpine 3.21.7 + K3s v1.32.5 + eks-token-webhook (server|agent role selected at first boot)", + Distro: "alpine", + Version: "3.21.7", + Arch: "x86_64", + Platform: "Linux/UNIX", + CreatedAt: time.Date(2026, 6, 8, 0, 0, 0, 0, time.UTC), + URL: "https://iso.mulgadc.com/system-ami/spinifex-eks-node-x86_64.qcow2", + Checksum: "https://iso.mulgadc.com/system-ami/spinifex-eks-node-x86_64.qcow2.sha256", + ChecksumType: "sha256", + BootMode: "bios", + Tags: map[string]string{"spinifex:managed-by": "eks"}, + }, } // AMI / image extraction utils