mirror of
https://github.com/hanzoai/ci.git
synced 2026-10-27 09:19:34 +00:00
v8 is arm64's default variant: containerd elides it, several registries store it, and docker.io/library/alpine:3.20 answers linux/arm64/v8 for the image every arm64 node runs. Comparing the strings failed an image that was correct. No other variant is elided -- arm/v6 and arm/v7 are different machines. Co-authored-by: Hanzo Dev <[email protected]>
112 lines
5.0 KiB
Bash
Executable File
112 lines
5.0 KiB
Bash
Executable File
#!/usr/bin/env bash
|
|
# imgplat — the architectures a published image actually serves. One rule,
|
|
# every build lane.
|
|
#
|
|
# imgplat <image-ref> # print them, one per line
|
|
# imgplat <image-ref> <expected> # print them, and refuse a mismatch
|
|
#
|
|
# imgplat ghcr.io/hanzoai/cloud:1.2.3 linux/amd64,linux/arm64
|
|
#
|
|
# WHY A BUILD READS BACK WHAT IT JUST PUSHED
|
|
#
|
|
# A push exiting 0 is the writer's account of a write. The manifest the registry
|
|
# answers with is the fact, and the two disagree in a way nothing downstream can
|
|
# see: an index carrying ONE manifest and an index carrying two are both indexes,
|
|
# both resolve, both pull, and only one of them runs on an arm64 node.
|
|
#
|
|
# Measured on ghcr.io/hanzoai/cloud:sha-ac39bda — an index with a single
|
|
# linux/amd64 manifest, published under the arch-neutral tag shape that
|
|
# bin/imgtags reserves for multi-arch. The tag says "any architecture", the bytes
|
|
# say amd64, and a fleet pinning it onto an arm64 node gets `exec format error`
|
|
# at container start — long after the run went green.
|
|
#
|
|
# So the platform set is asserted where the digest is already asserted, by the
|
|
# path a kubelet takes: read the manifest from the registry.
|
|
#
|
|
# WHY THIS IS A SCRIPT. Two lanes publish images. `buildx` pushes from the runner
|
|
# and holds a docker login; `delegate` posts to the build door and holds a crane
|
|
# login. Written twice, the rule is right twice and then wrong once, and the
|
|
# wrong once is silent -- the same argument bin/imgtags makes about tag SHAPE and
|
|
# bin/imgver about the NUMBER. This is the argument about ARCHITECTURE.
|
|
set -euo pipefail
|
|
|
|
ref="${1:?usage: imgplat <image-ref> [expected-platforms]}"
|
|
want="${2:-}"
|
|
|
|
# `unknown/unknown` is an attestation manifest, not a runnable image: buildx
|
|
# attaches provenance and SBOM as index members carrying that platform. Counting
|
|
# them would report a single-arch image as serving two.
|
|
plats() {
|
|
jq -r '
|
|
if .manifests then
|
|
[.manifests[] | select(.platform.architecture != "unknown")
|
|
| .platform.os + "/" + .platform.architecture
|
|
+ (if .platform.variant then "/" + .platform.variant else "" end)]
|
|
else
|
|
[]
|
|
end | .[]'
|
|
}
|
|
|
|
# crane where the delegate lane has it, buildx imagetools where the buildx lane
|
|
# does. Both print the raw manifest, so the parse below is the same either way.
|
|
#
|
|
# EVERY READER IS TRIED, not the first one present. A private repository answers
|
|
# a reader that holds no credential with the same "not found" it gives a name
|
|
# that does not exist, and the two lanes log in with different tools -- so
|
|
# choosing by `command -v` picks the installed one rather than the authorized
|
|
# one, and a build fails claiming its own image is missing.
|
|
raw() {
|
|
local out
|
|
if command -v crane >/dev/null 2>&1; then
|
|
out=$(crane manifest "$ref" 2>/dev/null) && [ -n "$out" ] && { printf '%s' "$out"; return 0; }
|
|
fi
|
|
if docker buildx version >/dev/null 2>&1; then
|
|
out=$(docker buildx imagetools inspect "$ref" --raw 2>/dev/null) && [ -n "$out" ] && { printf '%s' "$out"; return 0; }
|
|
fi
|
|
echo "imgplat: nothing here could read $ref back. crane and docker buildx are the two readers, and one of them has to hold the credential the registry wants -- the lane that publishes an image is the lane that must prove what it published." >&2
|
|
return 1
|
|
}
|
|
|
|
m=$(raw)
|
|
got=$(printf '%s' "$m" | plats | sort -u | paste -sd, -)
|
|
|
|
# No .manifests: a bare manifest. Its architecture lives in the config blob, and
|
|
# crane says it in one call.
|
|
if [ -z "$got" ]; then
|
|
got=$(crane config "$ref" 2>/dev/null | jq -r '.os + "/" + .architecture' 2>/dev/null || true)
|
|
[ -n "$got" ] && [ "$got" != "null/null" ] \
|
|
|| got=$(printf '%s' "$m" | jq -r '(.platform.os // "linux") + "/" + (.platform.architecture // "unknown")')
|
|
fi
|
|
|
|
printf '%s\n' "$got" | tr ',' '\n'
|
|
|
|
[ -n "$want" ] || exit 0
|
|
|
|
# Compare as SETS, and in containerd's spelling. Two normalizations, both
|
|
# measured against real registry replies:
|
|
#
|
|
# ORDER is not information -- buildx is handed the list in hanzo.yml's order
|
|
# and a registry answers in the order it stored.
|
|
#
|
|
# `linux/arm64` and `linux/arm64/v8` ARE THE SAME PLATFORM. v8 is arm64's
|
|
# default variant, which containerd elides and several registries store;
|
|
# docker.io/library/alpine:3.20 answers linux/arm64/v8 for the image every
|
|
# arm64 node runs. Comparing the strings fails an image that is correct.
|
|
# No other variant is elided: linux/arm/v6 and linux/arm/v7 are different
|
|
# machines and stay different here.
|
|
norm() {
|
|
printf '%s' "$1" | tr ', ' '\n\n' | sed '/^$/d; s|^linux/arm64/v8$|linux/arm64|' | sort -u | paste -sd, -
|
|
}
|
|
w=$(norm "$want"); g=$(norm "$got")
|
|
[ "$w" = "$g" ] && exit 0
|
|
|
|
cat >&2 <<MSG
|
|
imgplat: $ref serves $g, and hanzo.yml declares $w.
|
|
|
|
A tag that names architectures it does not carry is the failure this check
|
|
exists to stop: it resolves, it pulls, and it dies at container start on the
|
|
node that needed the missing one. Either the build did not produce every
|
|
platform, or the tag was written onto the wrong bytes.
|
|
MSG
|
|
exit 1
|