ci: multi-arch-safe php.net guard and emergency workflow (no platform change)
- scripts/image-label-amd64.sh: index-safe read of the amd64 image's labels (OCI index with attestations, Docker manifest list, single manifest). - scripts/php-emergency-guard.sh: used by both build-push.yaml guard sites; same decisions as the old inline step on single-platform images, but FAILS CLOSED when the labels cannot be read (only "tag not found" passes). - lsphp-source-emergency.yaml: push by digest + assemble (copy of HMB assemble-multiarch-index.sh); optional arm64; refuses to replace a multi-arch production tag with amd64-only unless allow_amd64_only. - Tests wired into Shell-Checks; main build matrix platforms unchanged. 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
4b35869477
commit
8a6a6d7b44
8 files changed
+565
-24
No files matched your search
Executable
+106
@@ -0,0 +1,106 @@
|
||||
#!/usr/bin/env bash
|
||||
## image-label-amd64.test.sh — fixtures for scripts/image-label-amd64.sh and
|
||||
## scripts/php-emergency-guard.sh, driven through a stub IMAGETOOLS.
|
||||
## Proves: index-safe (two-platform OCI index WITH attestation children, Docker
|
||||
## manifest list, single-platform manifest), fail-closed on malformed/ambiguous
|
||||
## input, "not found" = proceed, and that for single-platform images the guard
|
||||
## makes the SAME decision as the old inline step on every (src,cur,new) case.
|
||||
set -euo pipefail
|
||||
HERE=$(cd "$(dirname "$0")/.." && pwd)
|
||||
T=$(mktemp -d); trap 'rm -rf "$T"' EXIT
|
||||
mkdir -p "$T/raw" "$T/cfg" "$T/err"
|
||||
fail=0
|
||||
chk() { if [ "$2" = "$3" ]; then echo "ok $1"; else echo "FAIL $1: got '$2' want '$3'"; fail=1; fi; }
|
||||
|
||||
# ---- stub: inspect --raw REF / inspect REF --format ... ; fixtures keyed by ref
|
||||
key() { printf '%s' "$1" | tr '/:@' '___'; }
|
||||
cat >"$T/it" <<'STUB'
|
||||
#!/usr/bin/env bash
|
||||
# args: inspect --raw REF | inspect REF --format F
|
||||
d=$FIXDIR
|
||||
[ "$1" = inspect ] || exit 2
|
||||
if [ "$2" = --raw ]; then ref=$3; kind=raw; else ref=$2; kind=cfg; fi
|
||||
k=$(printf '%s' "$ref" | tr '/:@' '___')
|
||||
if [ -f "$d/err/$k" ]; then cat "$d/err/$k" >&2; exit 1; fi
|
||||
[ -f "$d/$kind/$k" ] || { echo "ERROR: $ref: not found" >&2; exit 1; }
|
||||
cat "$d/$kind/$k"
|
||||
STUB
|
||||
chmod +x "$T/it"
|
||||
export FIXDIR=$T IMAGETOOLS="$T/it"
|
||||
|
||||
A=sha256:$(printf 'a%.0s' {1..64}); B=sha256:$(printf 'b%.0s' {1..64})
|
||||
C=sha256:$(printf 'c%.0s' {1..64}); D=sha256:$(printf 'd%.0s' {1..64})
|
||||
REPO=reg.example/org/img
|
||||
put() { printf '%s' "$3" >"$T/$1/$(key "$2")"; }
|
||||
cfg() { # arch src php -> image config json
|
||||
printf '{"architecture":"%s","os":"linux","config":{"Labels":{"org.whp.lsphp-source":"%s","org.whp.php-version":"%s"}}}' "$1" "$2" "$3"; }
|
||||
|
||||
# 1. OCI index: amd64 + arm64 + two attestation children (unknown/unknown)
|
||||
put raw $REPO:php85 '{"schemaVersion":2,"mediaType":"application/vnd.oci.image.index.v1+json","manifests":[
|
||||
{"mediaType":"application/vnd.oci.image.manifest.v1+json","digest":"'$A'","platform":{"architecture":"amd64","os":"linux"}},
|
||||
{"mediaType":"application/vnd.oci.image.manifest.v1+json","digest":"'$C'","platform":{"architecture":"unknown","os":"unknown"},"annotations":{"vnd.docker.reference.type":"attestation-manifest","vnd.docker.reference.digest":"'$A'"}},
|
||||
{"mediaType":"application/vnd.oci.image.manifest.v1+json","digest":"'$B'","platform":{"architecture":"arm64","os":"linux"}},
|
||||
{"mediaType":"application/vnd.oci.image.manifest.v1+json","digest":"'$D'","platform":{"architecture":"unknown","os":"unknown"},"annotations":{"vnd.docker.reference.type":"attestation-manifest","vnd.docker.reference.digest":"'$B'"}}]}'
|
||||
put cfg $REPO@$A "$(cfg amd64 php.net 8.5.12)"
|
||||
put cfg $REPO@$B "$(cfg arm64 php.net 8.5.99)" # must never be read
|
||||
printf '%s\n%s\n' php.net 8.5.12 >"$T/want"
|
||||
chk "index+attestations: amd64 labels" "$("$HERE/image-label-amd64.sh" $REPO:php85 org.whp.lsphp-source org.whp.php-version)" "$(cat "$T/want")"
|
||||
|
||||
# 2. Docker manifest list (no attestations)
|
||||
put raw $REPO:ml '{"schemaVersion":2,"mediaType":"application/vnd.docker.distribution.manifest.list.v2+json","manifests":[
|
||||
{"mediaType":"application/vnd.docker.distribution.manifest.v2+json","digest":"'$B'","platform":{"architecture":"arm64","os":"linux","variant":"v8"}},
|
||||
{"mediaType":"application/vnd.docker.distribution.manifest.v2+json","digest":"'$A'","platform":{"architecture":"amd64","os":"linux"}}]}'
|
||||
chk "docker manifest list" "$("$HERE/image-label-amd64.sh" $REPO:ml org.whp.php-version)" "8.5.12"
|
||||
|
||||
# 3. Single-platform manifest (today's amd64-only images); ref with digest too
|
||||
put raw $REPO:single '{"schemaVersion":2,"mediaType":"application/vnd.oci.image.manifest.v1+json","config":{"digest":"'$A'"},"layers":[]}'
|
||||
put cfg $REPO:single "$(cfg amd64 litespeed 8.5.1)"
|
||||
chk "single-platform manifest" "$("$HERE/image-label-amd64.sh" $REPO:single org.whp.lsphp-source org.whp.php-version | tr '\n' ,)" "litespeed,8.5.1,"
|
||||
chk "absent label prints empty" "$("$HERE/image-label-amd64.sh" $REPO:single org.whp.nope)" ""
|
||||
put raw $REPO:nolabels '{"mediaType":"x","config":{},"layers":[]}'
|
||||
put cfg $REPO:nolabels '{"architecture":"amd64","config":{}}'
|
||||
chk "config without Labels" "$("$HERE/image-label-amd64.sh" $REPO:nolabels org.whp.php-version)" ""
|
||||
|
||||
# 4. failures: each must exit non-zero (1) with nothing on stdout
|
||||
bad() { # name ref
|
||||
local out rc=0; out=$("$HERE/image-label-amd64.sh" "$2" org.whp.php-version 2>/dev/null) || rc=$?
|
||||
chk "$1 -> rc $3" "$rc" "$3"; chk "$1 -> no stdout" "$out" ""
|
||||
}
|
||||
bad "tag not found" $REPO:nosuch 3
|
||||
put raw $REPO:garbage 'this is not json'; bad "malformed JSON" $REPO:garbage 1
|
||||
put raw $REPO:arr '[1,2]'; bad "JSON not an object" $REPO:arr 1
|
||||
put raw $REPO:unk '{"foo":"bar"}'; bad "neither index nor manifest" $REPO:unk 1
|
||||
put raw $REPO:armonly '{"manifests":[{"digest":"'$B'","platform":{"architecture":"arm64","os":"linux"}}]}'; bad "index without amd64" $REPO:armonly 1
|
||||
put raw $REPO:two '{"manifests":[{"digest":"'$A'","platform":{"architecture":"amd64","os":"linux"}},{"digest":"'$D'","platform":{"architecture":"amd64","os":"linux"}}]}'; bad "ambiguous amd64" $REPO:two 1
|
||||
put raw $REPO:baddig '{"manifests":[{"digest":"sha256:short","platform":{"architecture":"amd64","os":"linux"}}]}'; bad "malformed child digest" $REPO:baddig 1
|
||||
put raw $REPO:badcfg '{"manifests":[{"digest":"'$D'","platform":{"architecture":"amd64","os":"linux"}}]}'
|
||||
put cfg $REPO@$D 'not json'; bad "malformed config" $REPO:badcfg 1
|
||||
put raw $REPO:wrongarch '{"manifests":[{"digest":"'$C'","platform":{"architecture":"amd64","os":"linux"}}]}'
|
||||
put cfg $REPO@$C "$(cfg arm64 php.net 8.5.12)"; bad "amd64 slot with arm64 config" $REPO:wrongarch 1
|
||||
printf 'ERROR: denied: requested access to the resource is denied\n' >"$T/err/$(key $REPO:denied)"; bad "auth/network error is NOT 'not found'" $REPO:denied 1
|
||||
put raw $REPO:orphan '{"manifests":[{"digest":"'$B'","platform":{"architecture":"amd64","os":"linux"}}]}'; bad "child listed but missing" $REPO:orphan 1
|
||||
|
||||
# 5. Guard == old inline logic on single-platform fixtures, all combos
|
||||
old() { # src cur new -> proceed|refuse (verbatim predicate of the old step)
|
||||
local src=$1 cur=$2 new=$3
|
||||
if [ "$src" = "php.net" ] && [ "$cur" != "$new" ] && [ "$(printf '%s\n%s\n' "$cur" "$new" | sort -V | tail -n1)" = "$cur" ]; then echo refuse; else echo proceed; fi; }
|
||||
n=0
|
||||
for src in php.net litespeed ""; do for cur in "" 8.5.11 8.5.12 8.5.13 8.4.99 8.10.0; do for new in 8.5.12 8.5.9 8.6.0; do
|
||||
put raw $REPO:g '{"mediaType":"x","config":{},"layers":[]}'
|
||||
put cfg $REPO:g "$(cfg amd64 "$src" "$cur")"
|
||||
rc=0; "$HERE/php-emergency-guard.sh" $REPO:g "$new" >/dev/null 2>&1 || rc=$?
|
||||
got=proceed; [ "$rc" -ne 0 ] && got=refuse
|
||||
w=$(old "$src" "$cur" "$new"); n=$((n+1))
|
||||
[ "$got" = "$w" ] || { echo "FAIL guard vs old: src='$src' cur='$cur' new='$new' got=$got old=$w"; fail=1; }
|
||||
done; done; done
|
||||
echo "ok guard == old inline logic on $n single-platform combos"
|
||||
|
||||
# 6. Guard on a multi-arch index: refuses the rollback (old logic could not even read it)
|
||||
rc=0; "$HERE/php-emergency-guard.sh" $REPO:php85 8.5.9 >/dev/null 2>&1 || rc=$?; chk "index: emergency 8.5.12 vs packaged 8.5.9 refused" "$rc" 1
|
||||
rc=0; "$HERE/php-emergency-guard.sh" $REPO:php85 8.6.0 >/dev/null 2>&1 || rc=$?; chk "index: packaged newer proceeds" "$rc" 0
|
||||
# 7. fail closed vs first build
|
||||
rc=0; "$HERE/php-emergency-guard.sh" $REPO:nosuch 8.5.9 >/dev/null 2>&1 || rc=$?; chk "tag absent: proceed (first build)" "$rc" 0
|
||||
for t in garbage denied armonly orphan wrongarch; do
|
||||
rc=0; "$HERE/php-emergency-guard.sh" $REPO:$t 8.5.9 >/dev/null 2>&1 || rc=$?; chk "unreadable ($t): FAIL CLOSED" "$rc" 1
|
||||
done
|
||||
[ "$fail" -eq 0 ] && echo "ALL PASS" || { echo "FAILURES"; exit 1; }
|
||||
Executable
+54
@@ -0,0 +1,54 @@
|
||||
#!/usr/bin/env bash
|
||||
## index-platforms.test.sh — scripts/index-platforms.sh and
|
||||
## scripts/assemble-multiarch-index.sh (copy of haproxy-manager-base's) against a stub IMAGETOOLS.
|
||||
set -euo pipefail
|
||||
HERE=$(cd "$(dirname "$0")/.." && pwd)
|
||||
T=$(mktemp -d); trap 'rm -rf "$T"' EXIT
|
||||
fail=0
|
||||
chk() { if [ "$2" = "$3" ]; then echo "ok $1"; else echo "FAIL $1: got '$2' want '$3'"; fail=1; fi; }
|
||||
A=sha256:$(printf 'a%.0s' {1..64}); B=sha256:$(printf 'b%.0s' {1..64}); C=sha256:$(printf 'c%.0s' {1..64})
|
||||
cat >"$T/it" <<'STUB'
|
||||
#!/usr/bin/env bash
|
||||
# index-platforms mode: serves $FIX/<ref-with-/:@ as _> for "inspect --raw REF".
|
||||
# assemble mode (STUB_LOG set): "inspect REF --format .." -> arch from $ARCHES (ref=arch lines);
|
||||
# "create ..." -> appended to $STUB_LOG.
|
||||
if [ -n "${STUB_LOG:-}" ]; then
|
||||
if [ "$1" = create ]; then echo "create $*" >>"$STUB_LOG"; exit 0; fi
|
||||
ref=$2; a=$(grep -F "$ref=" "$ARCHES" | head -1 | cut -d= -f2); [ -n "$a" ] || exit 1
|
||||
printf '{"architecture":"%s"}' "$a"; exit 0
|
||||
fi
|
||||
[ "$2" = --raw ] || exit 2
|
||||
k=$(printf '%s' "$3" | tr '/:@' '___')
|
||||
if [ -f "$FIX/err_$k" ]; then cat "$FIX/err_$k" >&2; exit 1; fi
|
||||
[ -f "$FIX/$k" ] || { echo "ERROR: $3: not found" >&2; exit 1; }
|
||||
cat "$FIX/$k"
|
||||
STUB
|
||||
chmod +x "$T/it"; export IMAGETOOLS="$T/it" FIX=$T
|
||||
w() { printf '%s' "$2" >"$T/$(printf '%s' "$1" | tr '/:@' '___')"; }
|
||||
w r/i:idx '{"mediaType":"application/vnd.oci.image.index.v1+json","manifests":[{"digest":"'$A'","platform":{"os":"linux","architecture":"amd64"}},{"digest":"'$B'","platform":{"os":"linux","architecture":"arm64"}},{"digest":"'$C'","platform":{"os":"unknown","architecture":"unknown"},"annotations":{"vnd.docker.reference.type":"attestation-manifest"}}]}'
|
||||
w r/i:amd '{"manifests":[{"digest":"'$A'","platform":{"os":"linux","architecture":"amd64"}},{"digest":"'$C'","platform":{"os":"unknown","architecture":"unknown"},"annotations":{"vnd.docker.reference.type":"attestation-manifest"}}]}'
|
||||
w r/i:one '{"config":{"digest":"'$A'"},"layers":[]}'
|
||||
w r/i:bad 'garbage'
|
||||
printf 'ERROR: denied\n' >"$T/err_r_i_den"
|
||||
chk "two-platform index" "$("$HERE/index-platforms.sh" r/i:idx)" "index linux/amd64 linux/arm64"
|
||||
chk "amd64 index with attestation" "$("$HERE/index-platforms.sh" r/i:amd)" "index linux/amd64"
|
||||
chk "single manifest" "$("$HERE/index-platforms.sh" r/i:one)" "single"
|
||||
for pair in "nosuch 3" "bad 1" "den 1"; do set -- $pair
|
||||
rc=0; "$HERE/index-platforms.sh" r/i:$1 >/dev/null 2>&1 || rc=$?; chk "$1 -> rc $2" "$rc" "$2"; done
|
||||
|
||||
# ---- assemble-multiarch-index.sh (arm64 attach rules)
|
||||
export STUB_LOG=$T/log ARCHES=$T/arches
|
||||
R=reg/x
|
||||
printf '%s\n' "$R@$A=amd64" "$R@$B=arm64" >"$ARCHES"
|
||||
run() { : >"$STUB_LOG"; "$HERE/assemble-multiarch-index.sh" "$@" >/dev/null 2>&1; echo $?; }
|
||||
chk "both ok -> rc 0" "$(run --amd64 $A --arm64 $B --arm64-outcome success --repo $R --tag t1 --tag t2)" 0
|
||||
chk "both ok -> 1 create with 2 sources, 2 tags" "$(grep -c "create create -t $R:t1 -t $R:t2 $R@$A $R@$B\$" "$STUB_LOG")" 1
|
||||
chk "arm64 failed (digest present, stale) -> amd64 only" "$(run --amd64 $A --arm64 $B --arm64-outcome failure --repo $R --tag t1)" 0
|
||||
chk " ... create has no arm64 digest" "$(grep -c "$B" "$STUB_LOG" || true)" 0
|
||||
run --amd64 $A --repo $R --tag t1 >/dev/null
|
||||
chk "arm64 skipped -> amd64 only" "$(grep -c "$B" "$STUB_LOG" || true)" "0"
|
||||
chk "malformed amd64 digest -> refuse" "$(run --amd64 nope --repo $R --tag t1)" 1
|
||||
chk " ... nothing created" "$(grep -c create "$STUB_LOG" || true)" 0
|
||||
printf '%s\n' "$R@$A=arm64" >"$ARCHES"
|
||||
chk "amd64 digest that is not amd64 -> refuse" "$(run --amd64 $A --repo $R --tag t1)" 1
|
||||
[ "$fail" -eq 0 ] && echo "ALL PASS" || { echo FAILURES; exit 1; }
|
||||
Reference in new issue
Block a user