fix(ci): mirror base images as whole multi-arch indexes, with assertions
mirror-base-image.yaml pulled the upstream index digest on an amd64 runner and re-pushed it, publishing only the amd64 manifest; the python/golang mirrors had no arm64 variant. Replace pull/tag/push with a registry-to-registry `imagetools create` of the digest-pinned index via .gitea/scripts/ mirror-index-copy.sh: copy to <tag>-staging, assert linux/amd64+linux/arm64 are listed and the index digest equals the Docker Hub digest, then move the real tag and re-assert. A failed assertion never touches the real tag. Adds a local-registry test (.gitea/scripts/test-mirror-index-copy.sh). Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0185ukXjXyosSkunDsdWz3mb
This commit is contained in:
1 parent
50c58c4e9e
commit
905b2894b6
3 files changed
+139
-28
No files matched your search
Executable
+64
@@ -0,0 +1,64 @@
|
||||
#!/bin/bash
|
||||
## mirror-index-copy.sh SRC DST — CI only (not copied into images).
|
||||
## SRC registry/path@sha256:<index digest> (digest-pinned upstream reference)
|
||||
## DST registry/path:<tag> (the mirror tag consumers FROM)
|
||||
##
|
||||
## Registry-to-registry copy of the WHOLE upstream image index (every
|
||||
## platform), then assertions. No local `docker pull`, so nothing is flattened
|
||||
## to the runner's platform (the old pull/tag/push step re-published only the
|
||||
## amd64 manifest, leaving the mirror without an arm64 variant).
|
||||
##
|
||||
## Ordering (protects a previously multi-arch DST tag):
|
||||
## 1. copy SRC -> DST_REPO:<tag>-staging (real tag untouched)
|
||||
## 2. assert staging: index lists every REQUIRED platform and its digest
|
||||
## equals the digest in SRC
|
||||
## 3. only then point DST at that digest (registry-side, no data transfer)
|
||||
## 4. re-assert on DST
|
||||
## A failure at 1/2 exits non-zero before the real tag is touched. A failure
|
||||
## at 4 cannot be rolled back by this script (the tag already moved) but it is
|
||||
## loud and the same digest was already proven at step 2.
|
||||
##
|
||||
## Env: REQUIRED_PLATFORMS (default "linux/amd64 linux/arm64").
|
||||
## IMAGETOOLS (default "docker buildx imagetools"), for tests/skopeo-free CI.
|
||||
## (buildx talks plain HTTP to a localhost registry, so the test needs no flags.)
|
||||
set -euo pipefail
|
||||
|
||||
SRC="${1:?usage: mirror-index-copy.sh SRC@sha256:DIGEST DST_REPO:TAG}"
|
||||
DST="${2:?usage: mirror-index-copy.sh SRC@sha256:DIGEST DST_REPO:TAG}"
|
||||
REQUIRED_PLATFORMS="${REQUIRED_PLATFORMS:-linux/amd64 linux/arm64}"
|
||||
read -r -a IMGTOOLS <<<"${IMAGETOOLS:-docker buildx imagetools}"
|
||||
|
||||
fail() { echo "::error::mirror-index-copy: $*" >&2; exit 1; }
|
||||
|
||||
# Validate shapes (registry host may carry :port, e.g. localhost:5055).
|
||||
[[ "$SRC" =~ ^[a-z0-9][a-z0-9._:/-]*@sha256:[0-9a-f]{64}$ ]] || fail "bad source reference '${SRC}'"
|
||||
[[ "$DST" =~ ^[a-z0-9][a-z0-9._:/-]*:[A-Za-z0-9_][A-Za-z0-9_.-]{0,127}$ ]] || fail "bad destination '${DST}'"
|
||||
src_digest="${SRC#*@}"
|
||||
dst_repo="${DST%:*}"; dst_tag="${DST##*:}"
|
||||
STAGING="${dst_repo}:${dst_tag}-staging"
|
||||
|
||||
# assert_ref REF: REF must be an index listing every required platform, and
|
||||
# its manifest digest must equal the upstream index digest.
|
||||
assert_ref() {
|
||||
local ref="$1" raw have want got
|
||||
raw=$("${IMGTOOLS[@]}" inspect "$ref" --raw) || fail "cannot inspect ${ref}"
|
||||
have=$(jq -r '[.manifests[]? | select(.platform.os != "unknown") | "\(.platform.os)/\(.platform.architecture)"] | unique | join(" ")' <<<"$raw")
|
||||
[ -n "$have" ] || fail "${ref} is not a multi-platform index (single-platform manifest?); platforms: none"
|
||||
for want in $REQUIRED_PLATFORMS; do
|
||||
case " $have " in *" $want "*) ;; *) fail "${ref} is missing ${want} (has: ${have})" ;; esac
|
||||
done
|
||||
got=$("${IMGTOOLS[@]}" inspect "$ref" --format '{{.Manifest.Digest}}') || fail "cannot read digest of ${ref}"
|
||||
[ "$got" = "$src_digest" ] || fail "${ref} digest ${got} != upstream ${src_digest}"
|
||||
echo "ok: ${ref} platforms=[${have}] digest=${got}"
|
||||
}
|
||||
|
||||
echo "::group::copy index ${SRC} -> ${STAGING}"
|
||||
"${IMGTOOLS[@]}" create -t "$STAGING" "$SRC"
|
||||
echo "::endgroup::"
|
||||
assert_ref "$STAGING"
|
||||
|
||||
echo "::group::promote staging -> ${DST}"
|
||||
"${IMGTOOLS[@]}" create -t "$DST" "${dst_repo}@${src_digest}"
|
||||
echo "::endgroup::"
|
||||
assert_ref "$DST"
|
||||
echo "mirror ok: ${DST} == ${src_digest}"
|
||||
Executable
+56
@@ -0,0 +1,56 @@
|
||||
#!/bin/bash
|
||||
## Test for mirror-index-copy.sh. CI/dev only; needs docker + buildx + jq and a
|
||||
## THROWAWAY registry on REG (default localhost:5055):
|
||||
## docker run -d --name mirror-test-reg -p 127.0.0.1:5055:5000 registry:2
|
||||
## (remove it afterwards). Never point REG at a real registry. Builds only
|
||||
## FROM scratch images, so it needs no network beyond the registry.
|
||||
## Set MIRROR_TEST_UPSTREAM=docker.io/library/python@sha256:... to also run the
|
||||
## copy against a real multi-arch upstream digest (anonymous read-only pull).
|
||||
set -uo pipefail
|
||||
here="$(cd "$(dirname "$0")" && pwd)"
|
||||
REG="${REG:-localhost:5055}"
|
||||
case "$REG" in localhost:*|127.0.0.1:*) ;; *) echo "REG must be a localhost throwaway registry"; exit 2;; esac
|
||||
curl -fsS -m5 "http://${REG}/v2/" >/dev/null || { echo "no registry on ${REG}"; exit 2; }
|
||||
run="t$$"; work=$(mktemp -d); trap 'rm -rf "$work"' EXIT
|
||||
pass=0; failn=0
|
||||
ok() { echo "PASS: $1"; pass=$((pass+1)); }
|
||||
bad() { echo "FAIL: $1"; failn=$((failn+1)); }
|
||||
digest() { docker buildx imagetools inspect "$1" --format '{{.Manifest.Digest}}'; }
|
||||
|
||||
mkimg() { # mkimg REPO:TAG PLATFORM -> tiny FROM-scratch image
|
||||
printf 'FROM scratch\nLABEL t=%s\n' "$2-$run" >"$work/Dockerfile"
|
||||
docker buildx build -q --platform "$2" --provenance=false --sbom=false \
|
||||
--output "type=image,name=${1},push=true" "$work" >/dev/null || { echo "build $1 failed"; exit 2; }
|
||||
}
|
||||
mkimg "${REG}/${run}/amd64:v" linux/amd64
|
||||
mkimg "${REG}/${run}/arm64:v" linux/arm64
|
||||
A=$(digest "${REG}/${run}/amd64:v"); B=$(digest "${REG}/${run}/arm64:v")
|
||||
# synthetic two-platform upstream index, and a single-platform one
|
||||
docker buildx imagetools create -t "${REG}/${run}/multi:v" "${REG}/${run}/amd64@${A}" "${REG}/${run}/arm64@${B}" >/dev/null 2>&1 || exit 2
|
||||
MULTI=$(digest "${REG}/${run}/multi:v"); SINGLE="$A"
|
||||
|
||||
# 1. two-platform source: pass, digest preserved, tag lands on source digest
|
||||
if "$here/mirror-index-copy.sh" "${REG}/${run}/multi@${MULTI}" "${REG}/${run}/dst:1" >"$work/o1" 2>&1 \
|
||||
&& [ "$(digest "${REG}/${run}/dst:1")" = "$MULTI" ]; then ok "multi-arch copy passes and digest round-trips ($MULTI)"; else bad "multi-arch copy"; cat "$work/o1"; fi
|
||||
|
||||
# 2. single-platform source: must FAIL loudly and must not create the real tag
|
||||
if "$here/mirror-index-copy.sh" "${REG}/${run}/amd64@${SINGLE}" "${REG}/${run}/dst2:1" >"$work/o2" 2>&1; then bad "single-platform source was accepted"; else
|
||||
grep -q 'is not a multi-platform index\|missing linux/arm64' "$work/o2" && ok "single-platform source rejected: $(grep -m1 '::error::' "$work/o2")" || { bad "failed for the wrong reason"; cat "$work/o2"; }
|
||||
if docker buildx imagetools inspect "${REG}/${run}/dst2:1" >/dev/null 2>&1; then bad "real tag created despite failure"; else ok "real tag not created on failure"; fi
|
||||
fi
|
||||
|
||||
# 3. existing multi-arch tag must survive a failing (single-platform) refresh
|
||||
before=$(digest "${REG}/${run}/dst:1")
|
||||
if "$here/mirror-index-copy.sh" "${REG}/${run}/amd64@${SINGLE}" "${REG}/${run}/dst:1" >"$work/o3" 2>&1; then bad "downgrade accepted"; else
|
||||
[ "$(digest "${REG}/${run}/dst:1")" = "$before" ] && ok "previously multi-arch :tag untouched after failed refresh" || bad ":tag moved to single-platform"; fi
|
||||
|
||||
# 4. malformed references are refused before any registry traffic
|
||||
"$here/mirror-index-copy.sh" "${REG}/x:tag" "${REG}/${run}/d:1" >/dev/null 2>&1 && bad "tag-only source accepted" || ok "non-digest source refused"
|
||||
"$here/mirror-index-copy.sh" "${REG}/x@sha256:$(printf 'a%.0s' {1..64})" 'bad dst' >/dev/null 2>&1 && bad "bad dst accepted" || ok "bad destination refused"
|
||||
|
||||
# 5. optional: real upstream digest
|
||||
if [ -n "${MIRROR_TEST_UPSTREAM:-}" ]; then
|
||||
u="${MIRROR_TEST_UPSTREAM}"
|
||||
if "$here/mirror-index-copy.sh" "$u" "${REG}/${run}/up:1" >"$work/o5" 2>&1 && [ "$(digest "${REG}/${run}/up:1")" = "${u#*@}" ]; then ok "upstream index copied with identical digest ${u#*@}"; grep '^ok:' "$work/o5"; else bad "upstream copy"; tail -5 "$work/o5"; fi
|
||||
fi
|
||||
echo "passed=$pass failed=$failn"; [ "$failn" -eq 0 ]
|
||||
Reference in new issue
Block a user