#!/bin/bash # verify-mirror.sh — prove that the public mirror serves exactly what upstream # serves, and mark every verified ref with a git tag. # # For each repo/tag pair it compares the *index* digest advertised by gcr.io # with the digest advertised by the mirror. Only when both agree does it pull # the image by digest through the public hostname (the real client path) and # create the marker tag `synced---`. # # Marker tags are created locally; the workflow pushes them. Writing the marker # last is the whole point: its existence means "verified pullable by digest", # not merely "upstream has a newer tag". # # Exit status: # 0 every ref resolved; new refs were verified and marked (or none changed) # 1 the mirror answered with an error, or upstream lost a ref we mirror # 2 usage / configuration error # # Environment: # MIRROR public registry host (default img.dieschoenewolke.de) # UPSTREAM upstream registry host (default gcr.io) # REPOS space separated repo names under distroless/ (default the four) # TAGS space separated tags (default the four upstream variants) # VERIFY_PULL new (default) | always | never # PLATFORM platform used for the verification pull (default linux/amd64) set -uo pipefail MIRROR="${MIRROR:-img.dieschoenewolke.de}" UPSTREAM="${UPSTREAM:-gcr.io}" REPOS="${REPOS:-static base cc python3}" TAGS="${TAGS:-latest nonroot debug debug-nonroot}" VERIFY_PULL="${VERIFY_PULL:-new}" PLATFORM="${PLATFORM:-linux/amd64}" ACCEPT='application/vnd.oci.image.index.v1+json,application/vnd.docker.distribution.manifest.list.v2+json,application/vnd.oci.image.manifest.v1+json,application/vnd.docker.distribution.manifest.v2+json' say() { printf '%s\n' "$*"; } warn() { printf '%s\n' "$*" >&2; } case "$VERIFY_PULL" in new | always | never) ;; *) warn "VERIFY_PULL must be new, always or never" exit 2 ;; esac command -v curl >/dev/null || { warn "curl is required" exit 2 } command -v jq >/dev/null || { warn "jq is required" exit 2 } if [ "$VERIFY_PULL" != "never" ]; then command -v docker >/dev/null || { warn "docker is required unless VERIFY_PULL=never" exit 2 } fi # Ask for its manifest. Sets PROBE_STATUS (HTTP code, 000 on transport # error) and PROBE_DIGEST (empty when the registry sent no digest header). probe() { # [authorization-header] local url=$1 auth=${2:-} headers headers=$(mktemp) if [ -n "$auth" ]; then PROBE_STATUS=$(curl -sS --max-time 120 -H "$auth" -H "Accept: $ACCEPT" \ -D "$headers" -o /dev/null -w '%{http_code}' "$url") || PROBE_STATUS=000 else PROBE_STATUS=$(curl -sS --max-time 120 -H "Accept: $ACCEPT" \ -D "$headers" -o /dev/null -w '%{http_code}' "$url") || PROBE_STATUS=000 fi PROBE_DIGEST=$(tr -d '\r' <"$headers" \ | awk 'tolower($1) == "docker-content-digest:" { print $2; exit }') rm -f "$headers" } upstream_token() { # curl -fsS --max-time 60 \ "https://$UPSTREAM/v2/token?scope=repository:distroless/$1:pull&service=$UPSTREAM" \ | jq -r '.token // empty' } new_markers="" failures=0 pending=0 unchanged=0 verified=0 for repo in $REPOS; do token=$(upstream_token "$repo") || token="" if [ -z "$token" ]; then # Without a token we cannot compare anything, so this is a real failure. warn "FAIL distroless/$repo: no anonymous gcr.io token" failures=$((failures + 1)) continue fi for tag in $TAGS; do ref="distroless/$repo:$tag" probe "https://$UPSTREAM/v2/distroless/$repo/manifests/$tag" "Authorization: Bearer $token" up=$PROBE_DIGEST if [ "$PROBE_STATUS" != "200" ] || [ -z "$up" ]; then warn "FAIL $ref: upstream answered $PROBE_STATUS without a digest" failures=$((failures + 1)) continue fi probe "https://$MIRROR/v2/distroless/$repo/manifests/$tag" mirror=$PROBE_DIGEST case "$PROBE_STATUS" in 200) ;; 404) # zot has not mirrored this tag yet (new upstream tag, or a # pull-through that failed). Nothing to mark; the next run # retries after zot's 2h sync. say "PENDING $ref upstream=${up#sha256:} (not mirrored yet)" pending=$((pending + 1)) continue ;; *) warn "FAIL $ref: mirror answered $PROBE_STATUS" failures=$((failures + 1)) continue ;; esac if [ -z "$mirror" ]; then warn "FAIL $ref: mirror answered 200 without a digest header" failures=$((failures + 1)) continue fi if [ "$mirror" != "$up" ]; then # The tag moved upstream and zot's periodic sync has not caught up. say "PENDING $ref upstream=${up#sha256:} mirror=${mirror#sha256:}" pending=$((pending + 1)) continue fi short=${up#sha256:} short=${short:0:12} marker="synced-$repo-$tag-$short" marked=0 git rev-parse -q --verify "refs/tags/$marker" >/dev/null 2>&1 && marked=1 if [ "$marked" -eq 1 ] && [ "$VERIFY_PULL" != "always" ]; then say "OK $ref ${up#sha256:}" unchanged=$((unchanged + 1)) continue fi if [ "$VERIFY_PULL" = "never" ]; then say "NEW $ref ${up#sha256:} (pull verification disabled)" else if ! docker pull --quiet --platform "$PLATFORM" "$MIRROR/$ref@$up" >/dev/null; then warn "FAIL $ref: digest matches but the image does not pull by digest" failures=$((failures + 1)) continue fi docker image rm "$MIRROR/$ref@$up" >/dev/null 2>&1 || true say "OK $ref ${up#sha256:} (pulled by digest)" fi if [ "$marked" -eq 1 ]; then unchanged=$((unchanged + 1)) continue fi verified=$((verified + 1)) if ! git tag "$marker"; then warn "FAIL $ref: could not create marker tag $marker" failures=$((failures + 1)) continue fi # One marker per line: the publish step reads the file line by line and # hands each line to git push as a single refspec. A space separated # line becomes one bogus refspec ("fatal: invalid refspec"). new_markers="${new_markers}${marker}"$'\n' done done say "" say "verified=$verified unchanged=$unchanged pending=$pending failures=$failures" if [ -n "$new_markers" ]; then printf '%s' "$new_markers" >"${MARKERS_FILE:-/tmp/mirror-markers}" fi [ "$failures" -eq 0 ] || exit 1 exit 0