- 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
104 lines
4.3 KiB
Bash
Executable File
104 lines
4.3 KiB
Bash
Executable File
#!/usr/bin/env bash
|
|
## image-label-amd64.sh REF LABEL [LABEL...]
|
|
##
|
|
## Prints the value of each LABEL (one per line, empty line if the label is
|
|
## absent) from the *linux/amd64* image behind REF, whether REF is a plain
|
|
## single-platform manifest, an OCI image index (attestation children are
|
|
## ignored) or a Docker manifest list.
|
|
##
|
|
## Why: `imagetools inspect REF --format '{{ index .Image.Config.Labels ... }}'`
|
|
## is only meaningful for a single-platform image; on a multi-arch index `.Image`
|
|
## is platform-keyed / unavailable depending on the buildx version, and the old
|
|
## callers swallowed the error (fail-open). This resolves the amd64 child
|
|
## digest from the raw index, then inspects that child directly.
|
|
##
|
|
## Exit: 0 labels printed; 3 REF does not exist in the registry (not found);
|
|
## 1 anything else (network/auth error, malformed manifest, no amd64
|
|
## image, config is not amd64, ...) with a clear message on stderr.
|
|
## Env: IMAGETOOLS overrides "docker buildx imagetools" (tests use a stub).
|
|
set -euo pipefail
|
|
IMAGETOOLS=${IMAGETOOLS:-docker buildx imagetools}
|
|
die() { echo "image-label-amd64: $*" >&2; exit 1; }
|
|
[ $# -ge 2 ] || die "usage: image-label-amd64.sh REF LABEL [LABEL...]"
|
|
ref=$1; shift
|
|
|
|
# Run an imagetools command; map "tag/manifest does not exist" to exit 3.
|
|
it() {
|
|
local out err rc=0
|
|
err=$(mktemp); trap 'rm -f "$err"' RETURN
|
|
# shellcheck disable=SC2086
|
|
out=$($IMAGETOOLS "$@" 2>"$err") || rc=$?
|
|
if [ "$rc" -ne 0 ]; then
|
|
if grep -qiE 'not found|manifest unknown|name unknown|: 404' "$err"; then
|
|
echo "image-label-amd64: $ref: not found ($(head -c 300 "$err" | tr '\n' ' '))" >&2
|
|
return 3
|
|
fi
|
|
echo "image-label-amd64: imagetools $1 failed (rc=$rc): $(head -c 300 "$err" | tr '\n' ' ')" >&2
|
|
return 1
|
|
fi
|
|
printf '%s' "$out"
|
|
}
|
|
|
|
rc=0; raw=$(it inspect --raw "$ref") || rc=$?
|
|
[ "$rc" -eq 0 ] || exit "$rc"
|
|
|
|
# Classify and, for an index, pick the single linux/amd64 child digest.
|
|
rc=0
|
|
verdict=$(printf '%s' "$raw" | python3 -I -c '
|
|
import json, sys
|
|
try:
|
|
d = json.load(sys.stdin)
|
|
except Exception as e:
|
|
print("ERR not JSON: %s" % e); sys.exit(0)
|
|
if not isinstance(d, dict):
|
|
print("ERR manifest is not a JSON object"); sys.exit(0)
|
|
mt = d.get("mediaType", "")
|
|
if isinstance(d.get("manifests"), list):
|
|
kids = [m for m in d["manifests"] if isinstance(m, dict)
|
|
and isinstance(m.get("platform"), dict)
|
|
and m["platform"].get("os") == "linux"
|
|
and m["platform"].get("architecture") == "amd64"
|
|
and "vnd.docker.reference.type" not in (m.get("annotations") or {})]
|
|
digs = [m.get("digest", "") for m in kids]
|
|
if len(digs) != 1:
|
|
print("ERR index has %d linux/amd64 images (need exactly 1)" % len(digs)); sys.exit(0)
|
|
if not digs[0].startswith("sha256:") or len(digs[0]) != 71:
|
|
print("ERR malformed amd64 child digest %r" % digs[0]); sys.exit(0)
|
|
print("INDEX " + digs[0])
|
|
elif isinstance(d.get("config"), dict) and isinstance(d.get("layers"), list):
|
|
print("SINGLE")
|
|
else:
|
|
print("ERR neither an image index nor an image manifest (mediaType=%r)" % mt)
|
|
') || rc=$?
|
|
[ "$rc" -eq 0 ] || die "could not parse manifest of $ref"
|
|
case "$verdict" in
|
|
ERR*) die "$ref: ${verdict#ERR }" ;;
|
|
SINGLE) target=$ref ;;
|
|
INDEX\ *)
|
|
base=${ref%%@*}
|
|
last=${base##*/}
|
|
[[ $last == *:* ]] && base=${base%:"${last#*:}"}
|
|
target="$base@${verdict#INDEX }" ;;
|
|
*) die "$ref: unexpected parser output" ;;
|
|
esac
|
|
|
|
rc=0; cfg=$(it inspect "$target" --format '{{json .Image}}') || rc=$?
|
|
[ "$rc" -eq 0 ] || { [ "$rc" -eq 3 ] && die "$target: not found although listed by the index"; exit "$rc"; }
|
|
|
|
printf '%s' "$cfg" | python3 -I -c '
|
|
import json, sys
|
|
try:
|
|
c = json.load(sys.stdin)
|
|
except Exception as e:
|
|
sys.stderr.write("image-label-amd64: image config is not JSON: %s\n" % e); sys.exit(1)
|
|
if not isinstance(c, dict) or c.get("architecture") != "amd64":
|
|
sys.stderr.write("image-label-amd64: image config is not linux/amd64 (architecture=%r)\n" % (c.get("architecture") if isinstance(c, dict) else None)); sys.exit(1)
|
|
cfg = c.get("config") or {}
|
|
labels = cfg.get("Labels") or {}
|
|
if not isinstance(labels, dict):
|
|
sys.stderr.write("image-label-amd64: Labels is not an object\n"); sys.exit(1)
|
|
for name in sys.argv[1:]:
|
|
v = labels.get(name, "")
|
|
print(v if isinstance(v, str) else "")
|
|
' "$@"
|