Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions .github/workflows/build.yml
Original file line number Diff line number Diff line change
Expand Up @@ -115,6 +115,9 @@ jobs:
done
cosign sign --yes "${images[@]}"

- name: Verify multi-arch images can run
run: make test-multiarch PLATFORMS=linux/amd64,linux/arm64

- name: Create kind cluster
uses: helm/kind-action@92086f6be054225fa813e0a4b13787fc9088faab # v1.13.0

Expand Down
24 changes: 16 additions & 8 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -159,23 +159,31 @@ docker-save: ## Save docker image to archive.
# - have enable BuildKit, More info: https://docs.docker.com/develop/develop-images/build_enhancements/
# - be able to push the image for your registry (i.e. if you do not inform a valid value via IMG=<myregistry/image:<tag>> then the export will fail)
# To properly provided solutions that supports more than one platform you should use this option.
#
# NOTE: This project builds the manager binary with CGO enabled (it links against the
# Bitwarden SDK's prebuilt, architecture-specific static libraries via cgo). Because of that,
# the builder stage of the Dockerfile CANNOT be cross-compiled using the standard
# `--platform=$BUILDPLATFORM` trick: doing so would run the C compiler/assembler (musl-gcc)
# for the *build* platform against Go objects targeting a *different* platform (e.g. building
# on amd64 while targeting arm64), which fails or produces a broken binary and results in
# `exec format error` at runtime (see https://github.com/bitwarden/sm-kubernetes/issues/131).
# Instead, each target platform's builder stage must run natively (or under QEMU emulation),
# so we build directly against the existing Dockerfile without rewriting the FROM lines.
PLATFORMS ?= linux/arm64,linux/amd64#,linux/s390x,linux/ppc64le
.PHONY: docker-buildx
docker-buildx: test ## Build and push docker image for the manager for cross-platform support
# copy existing Dockerfile and insert --platform=${BUILDPLATFORM} into Dockerfile.cross, and preserve the original Dockerfile
sed -e '1 s/\(^FROM\)/FROM --platform=\$$\{BUILDPLATFORM\}/; t' -e ' 1,// s//FROM --platform=\$$\{BUILDPLATFORM\}/' Dockerfile > Dockerfile.cross
- $(CONTAINER_TOOL) buildx create --name project-v3-builder
$(CONTAINER_TOOL) buildx use project-v3-builder
- $(CONTAINER_TOOL) buildx build --push --platform=$(PLATFORMS) --tag ${IMG} -f Dockerfile.cross .
- $(CONTAINER_TOOL) buildx build --push --platform=$(PLATFORMS) --tag ${IMG} -f Dockerfile .
- $(CONTAINER_TOOL) buildx rm project-v3-builder
rm Dockerfile.cross

.PHONY: podman-buildx
podman-buildx: test ## Build and push docker image for the manager for cross-platform support
# copy existing Dockerfile and insert --platform=${BUILDPLATFORM} into Dockerfile.cross, and preserve the original Dockerfile
sed -e '1 s/\(^FROM\)/FROM --platform=\$$\{BUILDPLATFORM\}/; t' -e ' 1,// s//FROM --platform=\$$\{BUILDPLATFORM\}/' Dockerfile > Dockerfile.cross
- $(CONTAINER_TOOL) build --platform=$(PLATFORMS) --tag ${IMG} -f Dockerfile.cross .
rm Dockerfile.cross
- $(CONTAINER_TOOL) build --platform=$(PLATFORMS) --tag ${IMG} -f Dockerfile .

.PHONY: test-multiarch
test-multiarch: ## Build and run the manager image for each platform in PLATFORMS to verify it executes (regression test for issue #131).
PLATFORMS="$(PLATFORMS)" CONTAINER_TOOL="$(CONTAINER_TOOL)" ./hack/test-multiarch.sh

##@ Deployment

Expand Down
118 changes: 118 additions & 0 deletions hack/test-multiarch.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,118 @@
#!/usr/bin/env bash
#
# Integration test that verifies the manager image can actually be executed on
# every platform it is published for (e.g. linux/amd64 and linux/arm64).
#
# This is a regression test for https://github.com/bitwarden/sm-kubernetes/issues/131,
# where the published image for ARM platforms failed to start with:
# exec /manager: exec format error
#
# The test builds the manager image for each requested platform using
# `docker buildx` (loading a single-platform image at a time, since
# multi-platform builds cannot be loaded into the local image store), then runs
# the resulting image under that platform (using QEMU emulation when the
# platform doesn't match the host) to confirm the binary starts and responds
# normally instead of failing with an architecture/format error.
#
# Usage:
# ./hack/test-multiarch.sh
# PLATFORMS=linux/arm64 ./hack/test-multiarch.sh
#
# Can be run locally (requires Docker with buildx and, for non-native
# platforms, QEMU user-mode emulation registered, e.g. via
# `docker run --privileged --rm tonistiigi/binfmt --install all`) and is also
# wired into the GitHub Actions build workflow.
set -euo pipefail

REPO_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
cd "$REPO_ROOT"

CONTAINER_TOOL=${CONTAINER_TOOL:-docker}
PLATFORMS=${PLATFORMS:-linux/amd64,linux/arm64}
IMAGE_TAG_BASE=${IMAGE_TAG_BASE:-localhost/sm-operator-multiarch-test}
DOCKERFILE=${DOCKERFILE:-Dockerfile}

# GitHub Actions log grouping/annotations are only useful (and only rendered)
# when running inside a GitHub Actions job. When run locally, plain output is
# less noisy.
in_github_actions() {
[[ "${GITHUB_ACTIONS:-}" == "true" ]]
}

group_start() {
if in_github_actions; then
echo "::group::$1"
else
echo "=== $1 ==="
fi
}

group_end() {
if in_github_actions; then
echo "::endgroup::"
fi
}

log_error() {
if in_github_actions; then
echo "::error::$1"
else
echo "ERROR: $1" >&2
fi
}

IFS=',' read -r -a platform_array <<< "$PLATFORMS"

failures=()

for platform in "${platform_array[@]}"; do
safe_platform="${platform//\//-}"
tag="${IMAGE_TAG_BASE}:${safe_platform}"

group_start "Building image for platform ${platform}"
if ! "$CONTAINER_TOOL" buildx build \
--platform "$platform" \
--file "$DOCKERFILE" \
--tag "$tag" \
--load \
"$REPO_ROOT"; then
log_error "Failed to build image for platform ${platform}"
failures+=("$platform (build failed)")
group_end
continue
fi
group_end

group_start "Running image for platform ${platform}"
# `--help` is used as a lightweight smoke invocation: the manager binary
# parses flags and prints usage before starting the controller, so this
# exercises the entrypoint without requiring a Kubernetes API server.
output=""
status=0
output=$("$CONTAINER_TOOL" run --rm --platform "$platform" "$tag" --help 2>&1) || status=$?

echo "$output"

if [[ "$output" == *"exec format error"* ]]; then
log_error "Image for platform ${platform} failed with exec format error"
failures+=("$platform (exec format error)")
elif [[ $status -ne 0 ]]; then
log_error "Image for platform ${platform} exited with status ${status}"
failures+=("$platform (exit status ${status})")
elif [[ "$output" != *"Usage of /manager"* ]]; then
log_error "Image for platform ${platform} did not produce the expected usage output"
failures+=("$platform (unexpected output)")
else
echo "Platform ${platform}: OK"
fi
group_end

"$CONTAINER_TOOL" image rm "$tag" >/dev/null 2>&1 || true
done

if [[ ${#failures[@]} -gt 0 ]]; then
log_error "Multi-arch integration test failed for: ${failures[*]}"
exit 1
fi

echo "Multi-arch integration test passed for platforms: ${PLATFORMS}"