From 7b3b091d7ecbce6a5e57a87c7b2c8dc839193b26 Mon Sep 17 00:00:00 2001 From: Wei-Chen Chen Date: Thu, 20 Aug 2026 16:58:15 -0700 Subject: [PATCH] ci: publish multi-arch operator images The published images are linux/amd64 only, which makes the operator uninstallable on an arm64 cluster: config/manager and config/daemon both reference the same ghcr.io/bootc-dev/bootc-operator tag, so neither the controller Deployment nor the daemon DaemonSet has an instance to run. The daemon in particular cannot be architecture independent, since it drives bootc on the host through nsenter resolved from its own filesystem. Nothing in the operator is architecture specific, so cross-compile both binaries via TARGETARCH rather than emulating the Go toolchain, build each architecture as its own CI matrix leg, and assemble a manifest list in the push job. Saving a manifest list through a CI artifact is poorly supported, whereas per-architecture archives round-trip cleanly. Push the list as v2s2 so the per-architecture manifests keep the media type published today, leaving the added list layer as the only change downstream sees. e2e stays on amd64, since bink publishes its node disk images for amd64 alone. Multi-arch node images are also out of scope: they may need platform-aware digest resolution. Signed-off-by: Wei-Chen Chen Closes: #144 --- .github/workflows/ci.yaml | 61 +++++++++++++++++++++++++++++---------- CONTRIBUTING.md | 15 ++++++++++ Containerfile | 11 +++++-- Makefile | 11 +++++-- 4 files changed, 78 insertions(+), 20 deletions(-) diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index f05d601..bface19 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -99,6 +99,14 @@ jobs: runs-on: ubuntu-latest permissions: contents: read + strategy: + fail-fast: false + matrix: + # The Containerfile cross-compiles, so a non-native architecture still + # builds on this amd64 runner without emulation. + arch: [amd64, arm64] + env: + ARCH: ${{ matrix.arch }} steps: - name: Checkout uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 @@ -106,16 +114,16 @@ jobs: persist-credentials: false - name: Build operator image - run: make buildimg + run: make buildimg IMG="bootc-operator:dev-${ARCH}" PLATFORM="linux/${ARCH}" - name: Save operator image - run: podman save -o operator-image.tar bootc-operator:dev + run: podman save -o "operator-image-${ARCH}.tar" "bootc-operator:dev-${ARCH}" - name: Upload operator image uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 with: - name: operator-image - path: operator-image.tar + name: operator-image-${{ matrix.arch }} + path: operator-image-${{ matrix.arch }}.tar e2e: runs-on: ubuntu-latest @@ -180,10 +188,10 @@ jobs: - name: Download operator image uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 with: - name: operator-image + name: operator-image-amd64 - name: Load operator image - run: podman load -i operator-image.tar + run: podman load -i operator-image-amd64.tar - name: Fix registries configuration run: | @@ -195,7 +203,7 @@ jobs: run: make start-bink - name: Deploy to bink cluster - run: make deploy-bink + run: make deploy-bink IMG=bootc-operator:dev-amd64 - name: Gather deploy logs if: failure() @@ -222,13 +230,29 @@ jobs: env: IMAGE: ghcr.io/${{ github.repository }} steps: - - name: Download operator image + - name: Download operator images uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 with: - name: operator-image + pattern: operator-image-* + merge-multiple: true - - name: Load operator image - run: podman load -i operator-image.tar + - name: Assemble manifest list + run: | + shopt -s failglob + refs=() + for tar in operator-image-*.tar; do + arch="${tar#operator-image-}" + arch="${arch%.tar}" + podman load -i "$tar" + refs+=("containers-storage:localhost/bootc-operator:dev-$arch") + done + # Guard against a missing artifact silently publishing a partial + # manifest list, which would leave some nodes unable to pull. + if [[ "${#refs[@]}" -lt 2 ]]; then + echo "expected at least 2 architectures, got ${#refs[@]}" >&2 + exit 1 + fi + podman manifest create bootc-operator:dev "${refs[@]}" - name: Push to GHCR env: @@ -239,13 +263,20 @@ jobs: REF_NAME: ${{ github.ref_name }} run: | podman login -u "${ACTOR}" -p "${GH_TOKEN}" ghcr.io - podman push bootc-operator:dev "${IMAGE}":dev - podman push bootc-operator:dev "${IMAGE}":"${SHA}" + tags=(dev "${SHA}") if [[ "${REF}" == refs/tags/v* ]]; then - podman push bootc-operator:dev "${IMAGE}":"${REF_NAME}" + tags+=("${REF_NAME}") fi if [[ "${REF}" == refs/heads/main ]]; then - podman push bootc-operator:dev "${IMAGE}":latest + tags+=(latest) fi + + # v2s2 keeps the per-architecture manifests in the same format as + # the images published before this became a manifest list, so the + # only change downstream sees is the added list layer. + for tag in "${tags[@]}"; do + podman manifest push --all --format v2s2 \ + bootc-operator:dev "docker://${IMAGE}:${tag}" + done diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 998d9a1..e92bada 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -78,11 +78,26 @@ make deploy-bink BINK_CLUSTER_NAME=dev ```shell make build # Build all binaries (manager + daemon) to ./bin/ make buildimg # Build the container image (default: bootc-operator:dev) +make buildimg-all # Build a multi-arch manifest list (linux/amd64, linux/arm64) ``` The `bootc-operator` container image contains both the controller and daemon binaries. +Released images are manifest lists covering `linux/amd64` and `linux/arm64`. +Both binaries are pure Go, so the Containerfile cross-compiles them for the +target architecture instead of emulating the toolchain; building for a +non-native architecture therefore costs about the same as a native build and +needs no qemu setup. To build a single non-native image, pass `PLATFORM`: + +```shell +make buildimg PLATFORM=linux/arm64 +``` + +Note that the e2e suite remains amd64-only, because the +[bink](https://github.com/bootc-dev/bink) node images it boots are published +for amd64 alone. + After modifying API types in `api/`, regenerate CRDs and code: ```shell diff --git a/Containerfile b/Containerfile index e6a92ad..05bbf64 100644 --- a/Containerfile +++ b/Containerfile @@ -1,4 +1,4 @@ -FROM quay.io/fedora/fedora-minimal:44 AS buildroot +FROM --platform=$BUILDPLATFORM quay.io/fedora/fedora-minimal:44 AS buildroot ARG DNF_FLAGS="-y --setopt=install_weak_deps=False" RUN --mount=type=cache,id=dnf,target=/var/cache/libdnf5 \ dnf install ${DNF_FLAGS} golang @@ -9,10 +9,15 @@ COPY go.mod go.sum ./ RUN --mount=type=cache,id=gomod,target=/root/go/pkg/mod \ go mod download COPY . . +# The buildroot stage always runs on the build host's architecture, so cross +# compile via TARGETARCH rather than emulating the Go toolchain. Both binaries +# are pure Go, hence CGO_ENABLED=0. The Go build cache is content addressed and +# keyed on the target, so sharing it across architectures is safe. +ARG TARGETARCH RUN --mount=type=cache,id=gomod,target=/root/go/pkg/mod \ --mount=type=cache,id=gobuild,target=/root/.cache/go-build \ - go build -o manager ./cmd/controller/ && \ - go build -o daemon ./cmd/daemon/ + CGO_ENABLED=0 GOARCH=${TARGETARCH} go build -o manager ./cmd/controller/ && \ + CGO_ENABLED=0 GOARCH=${TARGETARCH} go build -o daemon ./cmd/daemon/ FROM quay.io/fedora/fedora-minimal:44 COPY --from=builder /workspace/manager /workspace/daemon /usr/local/bin/ diff --git a/Makefile b/Makefile index b3beaad..18a3070 100644 --- a/Makefile +++ b/Makefile @@ -1,6 +1,9 @@ # Image URL to use all building/pushing image targets IMG ?= bootc-operator:dev CONTAINER_TOOL ?= podman +# Architectures published by buildimg-all. Both binaries are pure Go, so the +# Containerfile cross-compiles rather than emulating the toolchain. +PLATFORMS ?= linux/amd64,linux/arm64 # Bink cluster settings. deploy-bink and e2e share the same cluster by default. # To use a separate dev cluster: make deploy-bink BINK_CLUSTER_NAME=dev BINK_CLUSTER_NAME ?= e2e @@ -89,8 +92,12 @@ build-daemon: ## Build daemon binary. go build -o bin/daemon ./cmd/daemon/ .PHONY: buildimg -buildimg: ## Build container image. - $(CONTAINER_TOOL) build -t $(IMG) . +buildimg: ## Build container image. PLATFORM=linux/arm64 cross-builds for another arch. + $(CONTAINER_TOOL) build $(if $(PLATFORM),--platform $(PLATFORM)) -t $(IMG) . + +.PHONY: buildimg-all +buildimg-all: ## Build a multi-arch manifest list image (override PLATFORMS to change). + $(CONTAINER_TOOL) build --platform $(PLATFORMS) --manifest $(IMG) . .PHONY: build-update-image build-update-image: ## Build a derived node image for update testing and push to bink registry.