mirror of
https://github.com/mulgadc/spinifex.git
synced 2026-10-22 04:07:04 +00:00
feat(eks): publish eks-node AMI to R2 + spx catalogue entry (#313)
* 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).
This commit is contained in:
@@ -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[@]}"
|
||||
@@ -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 \
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
Executable
+147
@@ -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 <catalog-key>
|
||||
#
|
||||
# Object name is <AMI_NAME or IMAGE_NAME>-<arch>.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 <manifest.conf> [--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://<account>.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 <manifest.conf> [--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: "<hex> <basename>". 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:-<R2_ENDPOINT>}" "$QCOW2_OUT" "${S3_PREFIX}/${OBJECT_QCOW2}"
|
||||
run aws s3 cp --endpoint-url "${R2_ENDPOINT:-<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
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user