Read-only mirror — the public repositories of the Forgejo instance, refreshed every 10 minutes. The live instance (fg.dieschoenewolke.de) is reachable only over the VPN; clone here with https://git.dieschoenewolke.de/git/<owner>/<repo>.git.
summaryrefslogtreecommitdiff
path: root/scripts/verify-mirror.sh
diff options
context:
space:
mode:
Diffstat (limited to 'scripts/verify-mirror.sh')
-rwxr-xr-xscripts/verify-mirror.sh191
1 files changed, 191 insertions, 0 deletions
diff --git a/scripts/verify-mirror.sh b/scripts/verify-mirror.sh
new file mode 100755
index 0000000..42f10bf
--- /dev/null
+++ b/scripts/verify-mirror.sh
@@ -0,0 +1,191 @@
+#!/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-<repo>-<tag>-<digest12>`.
+#
+# 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 <url> for its manifest. Sets PROBE_STATUS (HTTP code, 000 on transport
+# error) and PROBE_DIGEST (empty when the registry sent no digest header).
+probe() { # <url> [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() { # <repo>
+ 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
+ new_markers="$new_markers $marker"
+ done
+done
+
+say ""
+say "verified=$verified unchanged=$unchanged pending=$pending failures=$failures"
+
+if [ -n "$new_markers" ]; then
+ printf '%s\n' "${new_markers# }" >"${MARKERS_FILE:-/tmp/mirror-markers}"
+fi
+
+[ "$failures" -eq 0 ] || exit 1
+exit 0