Skip to content

ci: publish multi-arch operator images - #145

Open
zarcen wants to merge 1 commit into
bootc-dev:mainfrom
zarcen:multiarch-operator-images
Open

ci: publish multi-arch operator images#145
zarcen wants to merge 1 commit into
bootc-dev:mainfrom
zarcen:multiarch-operator-images

Conversation

@zarcen

@zarcen zarcen commented Aug 21, 2026

Copy link
Copy Markdown

Closes #144.

The published images are linux/amd64 only, so the operator is not installable on an arm64 cluster. config/manager/manager.yaml and config/daemon/daemon.yaml 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 there. The daemon cannot be made architecture independent either, since it drives bootc on the host through nsenter resolved from its own filesystem, so its container has to match the node.

Nothing in the operator is architecture specific, and the fedora-minimal base is already published for arm64, so this is build and publish plumbing rather than a code change.

What changed

  • Containerfile pins the buildroot stage to $BUILDPLATFORM and cross-compiles via $TARGETARCH. Both binaries are pure Go, and the final stage has no RUN steps, so no emulation is involved at any point — a non-native build costs about the same as a native one.
  • CI builds each architecture as its own matrix leg and assembles a manifest list in the push job, rather than building a list up front. Saving a manifest list through a CI artifact is poorly supported, whereas per-architecture archives round-trip cleanly. The assemble step fails closed if an artifact is missing, since silently publishing a partial list would leave some nodes unable to pull.
  • make buildimg PLATFORM=linux/arm64 cross-builds a single image and make buildimg-all builds the manifest list. Plain make buildimg is unchanged and still builds for the host.

Downstream compatibility

The tags under ghcr.io/bootc-dev/bootc-operator become manifest lists rather than single manifests, so it is worth being explicit about what existing consumers see.

The list is pushed with --format v2s2, which keeps the per-architecture manifests as application/vnd.docker.distribution.manifest.v2+json, the exact media type published today. The added list layer is therefore the only change. Pushing an OCI index instead would also have changed the child manifest media type, which seemed like unnecessary churn for a change about arm64 — happy to switch if maintainers would rather move that way, it is one flag.

Existing digest pins keep resolving, since nothing is removed from the registry. Tag consumers get platform selection for free on Docker, containerd, CRI-O and podman. Tooling that assumes a tag resolves to a single manifest, such as anything reading an image config straight off a tag, would need to handle a list; I did not find such a consumer in this repo. The bink and e2e path is untouched, because deploy-bink pushes a plain single-architecture image to the local registry rather than a list.

Deliberately not included

config/manager/manager.yaml still carries the commented-out kubebuilder nodeAffinity scaffold, whose text this change makes stale: it points at a docker-buildx target that does not exist here, and once the image is a manifest list kubelet selects the right instance without any arch constraint. I raised it in #144 as a question rather than deciding it here, to keep this PR to one logical change. Say the word and I will drop the block or narrow it to kubernetes.io/os: linux.

arm64 e2e coverage is also out of scope and wants its own issue: bink publishes its node disk images for amd64 alone, and GitHub's hosted arm64 runners do not expose /dev/kvm, so it is a runner-infrastructure question rather than CI plumbing.

Multi-arch node images are out of scope too. internal/registry/resolver.go resolves a tag with remote.Get, which returns the index digest for a manifest list, while internal/daemon/reconciler.go compares that against the digest bootc status reports for the booted image. Every image in play today is a single manifest, so the two cannot disagree, but that may need platform-aware resolution later.

How to validate

CI covers this end to end. Locally, verified with podman 5.6.2 on an arm64 host:

# Cross-build both directions and confirm the result is really the target arch.
make buildimg IMG=bootc-operator:amd64 PLATFORM=linux/amd64
make buildimg IMG=bootc-operator:arm64 PLATFORM=linux/arm64
podman image inspect bootc-operator:amd64 --format '{{.Architecture}}'   # amd64
podman image inspect bootc-operator:arm64 --format '{{.Architecture}}'   # arm64

# Manifest list build.
make buildimg-all IMG=bootc-operator:multi
podman manifest inspect bootc-operator:multi   # two instances

# The CI path: per-arch archive -> load -> manifest list -> push.
podman save -o op-amd64.tar bootc-operator:amd64
podman load -i op-amd64.tar                    # arch survives the round-trip

Pushing the assembled list to a local registry produces an application/vnd.docker.distribution.manifest.list.v2+json with both instances, and podman pull --arch amd64|arm64 against it selects the matching image. Each cross-build took about 1m40s on the same host, confirming no emulation is in the path.


Generated-by: AI
I am knowledgeable in this problem domain and reviewed it carefully.

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 <zarcen@gmail.com>
Closes: bootc-dev#144
@zarcen
zarcen force-pushed the multiarch-operator-images branch from 7d88a1c to 7b3b091 Compare August 21, 2026 01:04
@zarcen
zarcen marked this pull request as ready for review August 21, 2026 01:14
@alicefr

alicefr commented Aug 21, 2026

Copy link
Copy Markdown
Collaborator

@zarcen thanks for the PR, unfortunately, we cannot test images on arm without first the arm support in bink

@zarcen

zarcen commented Aug 21, 2026

Copy link
Copy Markdown
Author

@zarcen thanks for the PR, unfortunately, we cannot test images on arm without first the arm support in bink

@alicefr appreciate the review.
I was looking into that. Do you mind if I try to bring arm support to bink first? Other than CI, it doesn't appear to have existing blockers

@alicefr

alicefr commented Aug 21, 2026

Copy link
Copy Markdown
Collaborator

@zarcen thanks for the PR, unfortunately, we cannot test images on arm without first the arm support in bink

@alicefr appreciate the review. I was looking into that. Do you mind if I try to bring arm support to bink first? Other than CI, it doesn't appear to have existing blockers

Please go ahead, any contribution is welcome!
Bink relies on virt-install which hopefully should translate all the right options for libvirt. Addtionally, we rely on fedora-bootc, you should also double check that they already publish images for arm

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Publish arm64 images so the operator can run on arm64 clusters

2 participants