diff --git a/.github/workflows/e2e.yml b/.github/workflows/e2e.yml index ea168f0bd..42b749a37 100644 --- a/.github/workflows/e2e.yml +++ b/.github/workflows/e2e.yml @@ -52,7 +52,10 @@ jobs: # inputs.suites. if: github.event_name != 'workflow_dispatch' || contains(github.event.inputs.suites, 'e2e-single') || contains(github.event.inputs.suites, 'e2e-iam') || contains(github.event.inputs.suites, 'e2e-cert') || contains(github.event.inputs.suites, 'e2e-eks') || contains(github.event.inputs.suites, 'e2e-ecs') || contains(github.event.inputs.suites, 'e2e-storagegrowth') || contains(github.event.inputs.suites, 'e2e-partialblock') || contains(github.event.inputs.suites, 'e2e-rds') runs-on: [self-hosted, ci-single] - timeout-minutes: 50 + # Sized against the rds suite, which is the long pole and now carries both + # engines: ~6 minutes of provision, bootstrap and two image builds, plus a + # suite whose own per-test timeout is 45m. + timeout-minutes: 70 env: TOFU_LOG: /tmp/e2e-tofu-single-${{ github.run_id }}.log ARTIFACT_DIR: /tmp/spinifex-e2e-artifacts-${{ github.run_id }} @@ -250,53 +253,58 @@ jobs: EOF ok "kubectl installed on VM" - # CreateDBInstance boots the DB VM from the rds-postgres AMI, so that image + # CreateDBInstance boots the DB VM from an RDS AMI, so both engines' images # must be in the VM's image store before the suite runs — same split as the # eks-node pair above: build on the runner (needs source + network), import - # on the VM (needs predastore/NATS). The suite's psql client needs no step - # here: it runs inside a client VM that installs postgresql-client from its - # own user-data, because a private-only endpoint is unreachable from the - # runner and from the cluster host alike. - - name: Build rds-postgres AMI (libguestfs, runner) + # on the VM (needs predastore/NATS). The suite's psql and mariadb clients + # need no step here: they run inside a client VM that installs them from + # its own user-data, because a private-only endpoint is unreachable from + # the runner and from the cluster host alike. + - name: Build RDS AMIs (libguestfs, runner) if: contains(env.SUITES, 'e2e-rds') working-directory: spinifex run: | set -euo pipefail source "${GITHUB_WORKSPACE}/spinifex/.github/scripts/ci-fmt.sh" - banner "build rds-postgres AMI (libguestfs — no host devices)" sudo apt-get update -qq sudo apt-get install -y --no-install-recommends libguestfs-tools qemu-utils export LIBGUESTFS_BACKEND=direct sudo chmod 0644 /boot/vmlinuz-* || true - group_open "make build-rds-postgres-image" - make build-rds-postgres-image - group_close - ok "raw: $(du -h /tmp/rds-postgres-image-build/rds-postgres-alpine.raw | cut -f1)" + for IMAGE in rds-postgres rds-mariadb; do + banner "build ${IMAGE} AMI (libguestfs — no host devices)" + group_open "make build-${IMAGE}-image" + make "build-${IMAGE}-image" + group_close + ok "raw: $(du -h "/tmp/${IMAGE}-image-build/${IMAGE}-alpine.raw" | cut -f1)" + done - - name: Import rds-postgres AMI on VM + - name: Import RDS AMIs on VM if: contains(env.SUITES, 'e2e-rds') run: | set -euo pipefail source "${GITHUB_WORKSPACE}/spinifex/.github/scripts/ci-fmt.sh" - eval "$(grep -E '^(ALPINE_VERSION|AMI_NAME|SYSTEM_TAG)=' \ - spinifex/scripts/images/rds-postgres/manifest.conf)" - # SYSTEM_TAG carries three tags, not one: managed-by marks it a system - # image and engine/engine-version are what an EngineVersion request - # resolves against. --tag is repeatable, so expand rather than pass the - # whole string as a single value. - TAG_ARGS="" - for tag in ${SYSTEM_TAG}; do TAG_ARGS="${TAG_ARGS} --tag ${tag}"; done SSH_OPTS=(-o StrictHostKeyChecking=no -i "${{ steps.vm.outputs.ssh_key }}") VM=tf-user@${{ steps.vm.outputs.wan_ip }} - banner "import rds-postgres AMI to VM image store" - scp "${SSH_OPTS[@]}" \ - /tmp/rds-postgres-image-build/rds-postgres-alpine.raw \ - "$VM":/tmp/rds-postgres-alpine.raw - ssh "${SSH_OPTS[@]}" "$VM" \ - "spx admin images import --file /tmp/rds-postgres-alpine.raw \ - --distro alpine --version ${ALPINE_VERSION} --arch x86_64 \ - --boot-mode bios --ami-name ${AMI_NAME}${TAG_ARGS}" >/dev/null 2>&1 - ok "rds-postgres AMI imported" + for IMAGE in rds-postgres rds-mariadb; do + eval "$(grep -E '^(ALPINE_VERSION|AMI_NAME|SYSTEM_TAG)=' \ + "spinifex/scripts/images/${IMAGE}/manifest.conf")" + # SYSTEM_TAG carries four tags, not one: managed-by marks it a system + # image, engine/engine-version are what an EngineVersion request + # resolves against, and the contract tag gates the data-volume format + # grant. --tag is repeatable, so expand rather than pass the whole + # string as a single value. + TAG_ARGS="" + for tag in ${SYSTEM_TAG}; do TAG_ARGS="${TAG_ARGS} --tag ${tag}"; done + banner "import ${IMAGE} AMI to VM image store" + scp "${SSH_OPTS[@]}" \ + "/tmp/${IMAGE}-image-build/${IMAGE}-alpine.raw" \ + "$VM":"/tmp/${IMAGE}-alpine.raw" + ssh "${SSH_OPTS[@]}" "$VM" \ + "spx admin images import --file /tmp/${IMAGE}-alpine.raw \ + --distro alpine --version ${ALPINE_VERSION} --arch x86_64 \ + --boot-mode bios --ami-name ${AMI_NAME}${TAG_ARGS}" >/dev/null 2>&1 + ok "${IMAGE} AMI imported" + done - name: Build E2E test binaries on runner working-directory: spinifex/tests/e2e @@ -383,9 +391,10 @@ jobs: echo \"::group::suite \$SUITE\"; \ printf '\n%s%s━━ suite %s ━━%s\n' \"\$B\" \"\$C\" \"\$SUITE\" \"\$R\"; \ date -u +%Y-%m-%dT%H:%M:%SZ > '${ARTIFACT_DIR}/test-'\$SUITE'.start'; \ + case \$SUITE in rds) TMO=45m ;; *) TMO=30m ;; esac; \ GITHUB_ACTIONS=true FORCE_COLOR=1 SPINIFEX_E2E=1 SPINIFEX_MODE=single \ ARTIFACT_DIR='${ARTIFACT_DIR}' \ - ./\$SUITE.test -test.v -test.timeout=30m ${TEST_RUN_FLAG} \ + ./\$SUITE.test -test.v -test.timeout=\$TMO ${TEST_RUN_FLAG} \ 2>&1 | tee '${ARTIFACT_DIR}/test-'\$SUITE'.log' \ | sed -uE 's/^[[:space:]]+([a-zA-Z0-9_]+_test\.go:[0-9]+: )?//'; \ rc=\${PIPESTATUS[0]}; \ diff --git a/.github/workflows/publish-system-ami.yml b/.github/workflows/publish-system-ami.yml index c93b098a0..ae676aa6b 100644 --- a/.github/workflows/publish-system-ami.yml +++ b/.github/workflows/publish-system-ami.yml @@ -2,18 +2,20 @@ name: Publish System AMIs # Builds the system AMIs (libguestfs, on the runner) and publishes each # compressed qcow2 + .sha256 sidecar to the Cloudflare R2 bucket that backs -# iso.mulgadc.com/system-ami/. Five images ship: +# iso.mulgadc.com/system-ami/. Six images ship: # spinifex-eks-node Alpine EKS K3s node (server|agent, role at boot) # spinifex-ecs-node Alpine ECS container instance # spinifex-eks-node-gpu Ubuntu EKS K3s GPU worker (NVIDIA driver + toolkit) # spinifex-ecs-node-gpu Ubuntu ECS GPU container instance # spinifex-rds-postgres Alpine RDS PostgreSQL instance +# spinifex-rds-mariadb Alpine RDS MariaDB instance # Operators then install them on any cluster with: # spx admin images import --name spinifex-eks-node # spx admin images import --name spinifex-ecs-node # spx admin images import --name spinifex-eks-node-gpu # spx admin images import --name spinifex-ecs-node-gpu # spx admin images import --name spinifex-rds-postgres +# spx admin images import --name spinifex-rds-mariadb # # Manual-only: publishing overwrites the mirror artifact, so it is a deliberate # action, not something every push should trigger. The `variant` input scopes @@ -59,7 +61,7 @@ jobs: set -euo pipefail cpu='{"name":"spinifex-eks-node","manifest":"scripts/images/eks-node/manifest.conf"},{"name":"spinifex-ecs-node","manifest":"scripts/images/ecs-agent/manifest.conf"}' gpu='{"name":"spinifex-eks-node-gpu","manifest":"scripts/images/eks-node-gpu/manifest.conf"},{"name":"spinifex-ecs-node-gpu","manifest":"scripts/images/ecs-agent-gpu/manifest.conf"}' - rds='{"name":"spinifex-rds-postgres","manifest":"scripts/images/rds-postgres/manifest.conf"}' + rds='{"name":"spinifex-rds-postgres","manifest":"scripts/images/rds-postgres/manifest.conf"},{"name":"spinifex-rds-mariadb","manifest":"scripts/images/rds-mariadb/manifest.conf"}' case "${VARIANT}" in cpu) items="${cpu}" ;; gpu) items="${gpu}" ;; diff --git a/Makefile b/Makefile index 7417df363..0b2d189db 100644 --- a/Makefile +++ b/Makefile @@ -111,6 +111,12 @@ build-rds-postgres-image: ## Build the spinifex-rds-postgres AMI (Alpine + Postg import-rds-postgres-image: ## Build + register the rds-postgres AMI (requires a running cluster) $(MAKE) build-system-image IMAGE=rds-postgres IMPORT=1 +build-rds-mariadb-image: ## Build the spinifex-rds-mariadb AMI (Alpine + MariaDB 11.8 + rds-init; IMPORT=1 to register) + $(MAKE) build-system-image IMAGE=rds-mariadb + +import-rds-mariadb-image: ## Build + register the rds-mariadb AMI (requires a running cluster) + $(MAKE) build-system-image IMAGE=rds-mariadb IMPORT=1 + 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 @@ -388,7 +394,7 @@ distro-arm64: distro-clean: rm -rf dist/ -.PHONY: build build-ui build-installer build-lb-agent build-ecs-agent build-system-image build-eks-node-image import-eks-node-image publish-eks-node-image build-ecs-node-image import-ecs-node-image build-rds-postgres-image import-rds-postgres-image build-microvm-image install-microvm go_build preflight test test-cover test-race diff-coverage bench test-actions test-images test-harness test-integration generate-aws-model-coverage aws-model-coverage test-segscan-oracle manifest-check manifest-lint manifest-lint-update \ +.PHONY: build build-ui build-installer build-lb-agent build-ecs-agent build-system-image build-eks-node-image import-eks-node-image publish-eks-node-image build-ecs-node-image import-ecs-node-image build-rds-postgres-image import-rds-postgres-image build-rds-mariadb-image import-rds-mariadb-image build-microvm-image install-microvm go_build preflight test test-cover test-race diff-coverage bench test-actions test-images test-harness test-integration generate-aws-model-coverage aws-model-coverage test-segscan-oracle manifest-check manifest-lint manifest-lint-update \ deploy reinstall clean \ install-system install-go install-aws quickinstall \ lint fix govulncheck nilaway \ diff --git a/scripts/README.md b/scripts/README.md index 84011fe74..443a531b0 100644 --- a/scripts/README.md +++ b/scripts/README.md @@ -55,6 +55,7 @@ Each subdirectory holds a `manifest.conf`, init scripts, and a `setup.sh` that ` | `eks-node/` | The unified Alpine EKS node AMI (K3s server *or* agent, selected at first boot). `setup.sh` installs the pinned, SHA-verified K3s binary and the role init scripts. | | `eks-node-gpu/` | The Ubuntu EKS GPU worker AMI. Agent role only — GPU nodes never run the control plane — plus the NVIDIA GPU stack. | | `rds-postgres/` | The `spinifex-rds-postgres` AMI. `setup.sh` sets exec bits on the OpenRC services and creates the root-only directory cloud-init drops the agent env file and gateway CA into. | +| `rds-mariadb/` | The `spinifex-rds-mariadb` AMI. Same layout as `rds-postgres/`, plus the `/etc/my.cnf.d` include drop-in and the `conf.d/mariadb` dependency that keeps the packaged service off an unbootstrapped datadir. | | `ubuntu-gpu-{amd,nvidia}-setup.sh` | Chroot setup for GPU-capable Ubuntu guest images: ROCm CLI tools and AMD firmware, or the DKMS-built headless NVIDIA driver. Guest-side only — host VFIO setup is `spx admin gpu setup`. | ### `images/eks-node/` — in-guest helpers diff --git a/spinifex/utils/images.go b/spinifex/utils/images.go index dc0e69c10..1501bb4ad 100644 --- a/spinifex/utils/images.go +++ b/spinifex/utils/images.go @@ -534,6 +534,26 @@ var AvailableImages = map[string]Images{ "rds-data-volume-contract": "format-auth-v1", }, }, + + // Same resolution path as rds-postgres — only the engine tags differ, which + // is what keeps a MariaDB request off the PostgreSQL image and vice versa. + "spinifex-rds-mariadb": { + Name: "spinifex-rds-mariadb", + Description: "Mulga RDS MariaDB image — Alpine 3.24.1 + MariaDB 11.8 + rds-init (install-db, master password, parameters and TLS applied at first boot)", + Distro: "alpine", + Version: "3.24.1", + Arch: "x86_64", + Platform: "Linux/UNIX", + CreatedAt: time.Date(2026, 8, 11, 0, 0, 0, 0, time.UTC), + URL: "https://iso.mulgadc.com/system-ami/spinifex-rds-mariadb-x86_64.qcow2", + Checksum: "https://iso.mulgadc.com/system-ami/spinifex-rds-mariadb-x86_64.qcow2.sha256", + ChecksumType: "sha256", + BootMode: "bios", + Tags: map[string]string{ + "spinifex:managed-by": "rds", "engine": "mariadb", "engine-version": "11.8", + "rds-data-volume-contract": "format-auth-v1", + }, + }, } func ExtractDiskImageFromFile(imagepath string, tmpdir string) (diskimage string, err error) { diff --git a/tests/e2e/rds/mariadb_image_test.go b/tests/e2e/rds/mariadb_image_test.go new file mode 100644 index 000000000..ba79f657a --- /dev/null +++ b/tests/e2e/rds/mariadb_image_test.go @@ -0,0 +1,53 @@ +//go:build e2e + +package rds + +import ( + "testing" + + "github.com/aws/aws-sdk-go/aws" + "github.com/aws/aws-sdk-go/service/ec2" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +// What the control plane resolves a MariaDB create against. The AMI name is not +// parsed for the engine — these tags are the whole contract. +const ( + mariaAMIName = "spinifex-rds-mariadb" + mariaEngine = "mariadb" + mariaVersion = "11.8" +) + +// TestMariaDBSystemImage asserts the CI job's build-and-import step produced an +// image the engine can actually be resolved from. It creates nothing, so it +// takes no DB-VM budget, and it is the cheapest possible failure for a missing +// or mis-tagged AMI: without it the first MariaDB create fails deep in launch +// with an error that names no cause. +func TestMariaDBSystemImage(t *testing.T) { + t.Parallel() + f := requireRDSFixture(t) + + out, err := f.AWS.EC2.DescribeImages(&ec2.DescribeImagesInput{ + Filters: []*ec2.Filter{{ + Name: aws.String("name"), + Values: []*string{aws.String(mariaAMIName)}, + }}, + }) + require.NoError(t, err, "DescribeImages") + require.Len(t, out.Images, 1, "expected exactly one %s image in the store", mariaAMIName) + + image := out.Images[0] + assert.Equal(t, "available", aws.StringValue(image.State)) + + tags := make(map[string]string, len(image.Tags)) + for _, tag := range image.Tags { + tags[aws.StringValue(tag.Key)] = aws.StringValue(tag.Value) + } + assert.Equal(t, "rds", tags["spinifex:managed-by"]) + assert.Equal(t, mariaEngine, tags["engine"]) + assert.Equal(t, mariaVersion, tags["engine-version"]) + // Without this the control plane withholds the data-volume format grant and + // every create wedges waiting for a datadir the guest may not format. + assert.Equal(t, "format-auth-v1", tags["rds-data-volume-contract"]) +}