From a8d6dd127cdcc9b87bb825b205360b290ea21113 Mon Sep 17 00:00:00 2001 From: Ryan Cook Date: Thu, 8 Oct 2026 16:36:11 -0400 Subject: [PATCH 1/3] fix: preserve child digests when publishing platform indexes Signed-off-by: Ryan Cook --- scripts/publish-platform-index.sh | 5 +++- .../publishing/test-platform-publish.sh | 27 +++++++++++++++++++ 2 files changed, 31 insertions(+), 1 deletion(-) diff --git a/scripts/publish-platform-index.sh b/scripts/publish-platform-index.sh index 41f0575d..9f4ae5b6 100644 --- a/scripts/publish-platform-index.sh +++ b/scripts/publish-platform-index.sh @@ -43,7 +43,10 @@ done podman manifest create "$manifest" podman manifest add "${options[@]}" "$manifest" "docker://$repository-amd@$amd_digest" podman manifest add "${options[@]}" "$manifest" "docker://$repository-arm@$arm_digest" -podman manifest push --all "${options[@]}" --format=docker "$manifest" "docker://$destination" +# Child manifests and blobs already exist in this repository. Copying them again +# can recompress layers and replace the exact digests pinned above. Publish only +# the index; keep the registry verification below strict. +podman manifest push --all=false "${options[@]}" --format=docker "$manifest" "docker://$destination" podman manifest inspect "${options[@]}" "$destination" > "$scratch/index.json" python3 - "$scratch/index.json" "$amd_digest" "$arm_digest" <<'PYTHON' import json diff --git a/tests/integration/publishing/test-platform-publish.sh b/tests/integration/publishing/test-platform-publish.sh index 3615bde1..b51610d5 100644 --- a/tests/integration/publishing/test-platform-publish.sh +++ b/tests/integration/publishing/test-platform-publish.sh @@ -56,3 +56,30 @@ for arch in amd64 arm64; do done podman manifest inspect --tls-verify=false 127.0.0.1:5000/fetchit-test:latest > /tmp/retagged-index.json cmp /tmp/original-index.json /tmp/retagged-index.json + +# Recompression must not rewrite the already-published child manifests. Force +# different gzip levels for the architecture pushes and any attempted index-side +# child copies. This reproduces the main-branch failure with tiny test images. +wrapper_dir=$(mktemp -d) +real_podman=$(command -v podman) +export FETCHIT_TEST_REAL_PODMAN="$real_podman" +cat > "$wrapper_dir/podman" <<'WRAPPER' +#!/usr/bin/env bash +set -euo pipefail +if [[ ${1:-} == push ]]; then + shift + exec "$FETCHIT_TEST_REAL_PODMAN" push --compression-format=gzip --compression-level=1 --force-compression "$@" +elif [[ ${1:-} == manifest && ${2:-} == push ]]; then + shift 2 + exec "$FETCHIT_TEST_REAL_PODMAN" manifest push --compression-format=gzip --compression-level=9 --force-compression "$@" +fi +exec "$FETCHIT_TEST_REAL_PODMAN" "$@" +WRAPPER +chmod +x "$wrapper_dir/podman" +PATH="$wrapper_dir:$PATH" FETCHIT_REGISTRY_TLS_VERIFY=false bash scripts/publish-platform-index.sh \ + 127.0.0.1:5000/fetchit-recompression:latest localhost/publish-source:amd64 localhost/publish-source:arm64 +rm -rf "$wrapper_dir" +for arch in amd64 arm64; do + loaded=$(podman pull --quiet --tls-verify=false --policy=always --platform "linux/$arch" 127.0.0.1:5000/fetchit-recompression:latest) + test "$(podman image inspect --format '{{.Architecture}}' "$loaded")" = "$arch" +done From 781f27722b42b263fbd72fec946023cbd95b8bd1 Mon Sep 17 00:00:00 2001 From: Ryan Cook Date: Thu, 8 Oct 2026 16:37:29 -0400 Subject: [PATCH 2/3] fix: stage child manifests in the platform index repository Signed-off-by: Ryan Cook --- scripts/publish-platform-index.sh | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/scripts/publish-platform-index.sh b/scripts/publish-platform-index.sh index 9f4ae5b6..a7350ccd 100644 --- a/scripts/publish-platform-index.sh +++ b/scripts/publish-platform-index.sh @@ -33,16 +33,20 @@ test "$(podman image inspect --format '{{.Architecture}}' "$amd_image")" = amd64 test "$(podman image inspect --format '{{.Architecture}}' "$arm_image")" = arm64 # A push may convert the archive's manifest and update its storage metadata. Do # not reuse an index assembled from those local instances before the pushes. -podman push "${options[@]}" --format=v2s2 --digestfile "$scratch/amd.digest" "$amd_image" "docker://$repository-amd:latest" -podman push "${options[@]}" --format=v2s2 --digestfile "$scratch/arm.digest" "$arm_image" "docker://$repository-arm:latest" +# Retain legacy architecture repositories for existing consumers. +podman push "${options[@]}" --format=v2s2 "$amd_image" "docker://$repository-amd:latest" +podman push "${options[@]}" --format=v2s2 "$arm_image" "docker://$repository-arm:latest" +# Registries require every index child manifest in the index's own repository. +podman push "${options[@]}" --format=v2s2 --digestfile "$scratch/amd.digest" "$amd_image" "docker://$repository:amd64" +podman push "${options[@]}" --format=v2s2 --digestfile "$scratch/arm.digest" "$arm_image" "docker://$repository:arm64" amd_digest=$(cat "$scratch/amd.digest") arm_digest=$(cat "$scratch/arm.digest") for digest in "$amd_digest" "$arm_digest"; do [[ "$digest" =~ ^sha256:[0-9a-f]{64}$ ]] || { echo 'Invalid pushed image digest' >&2; exit 1; } done podman manifest create "$manifest" -podman manifest add "${options[@]}" "$manifest" "docker://$repository-amd@$amd_digest" -podman manifest add "${options[@]}" "$manifest" "docker://$repository-arm@$arm_digest" +podman manifest add "${options[@]}" "$manifest" "docker://$repository@$amd_digest" +podman manifest add "${options[@]}" "$manifest" "docker://$repository@$arm_digest" # Child manifests and blobs already exist in this repository. Copying them again # can recompress layers and replace the exact digests pinned above. Publish only # the index; keep the registry verification below strict. From 41eba7e11515b4365db40b6715877634326b2bea Mon Sep 17 00:00:00 2001 From: Ryan Cook Date: Thu, 8 Oct 2026 16:38:30 -0400 Subject: [PATCH 3/3] test: fail before publishing indexes with unavailable children Signed-off-by: Ryan Cook --- scripts/publish-platform-index.sh | 15 +++++++++++++-- .../publishing/test-platform-publish.sh | 8 ++++++++ 2 files changed, 21 insertions(+), 2 deletions(-) diff --git a/scripts/publish-platform-index.sh b/scripts/publish-platform-index.sh index a7350ccd..07f36cf1 100644 --- a/scripts/publish-platform-index.sh +++ b/scripts/publish-platform-index.sh @@ -44,13 +44,24 @@ arm_digest=$(cat "$scratch/arm.digest") for digest in "$amd_digest" "$arm_digest"; do [[ "$digest" =~ ^sha256:[0-9a-f]{64}$ ]] || { echo 'Invalid pushed image digest' >&2; exit 1; } done +# Fetch by immutable digest from the destination repository before publishing. +for digest in "$amd_digest" "$arm_digest"; do + if ! podman manifest inspect "${options[@]}" "$repository@$digest" > /dev/null; then + echo "Missing child manifest in destination repository: $repository@$digest" >&2 + exit 1 + fi +done podman manifest create "$manifest" podman manifest add "${options[@]}" "$manifest" "docker://$repository@$amd_digest" podman manifest add "${options[@]}" "$manifest" "docker://$repository@$arm_digest" # Child manifests and blobs already exist in this repository. Copying them again # can recompress layers and replace the exact digests pinned above. Publish only -# the index; keep the registry verification below strict. -podman manifest push --all=false "${options[@]}" --format=docker "$manifest" "docker://$destination" +# the index with --all=false; Podman 5 defaults --all to true. Keep both pinned +# child digests unchanged. Child publication and index publication are separate. +if ! podman manifest push --all=false "${options[@]}" --format=docker "$manifest" "docker://$destination"; then + echo "Failed to publish platform index: $destination" >&2 + exit 1 +fi podman manifest inspect "${options[@]}" "$destination" > "$scratch/index.json" python3 - "$scratch/index.json" "$amd_digest" "$arm_digest" <<'PYTHON' import json diff --git a/tests/integration/publishing/test-platform-publish.sh b/tests/integration/publishing/test-platform-publish.sh index b51610d5..2ed7646e 100644 --- a/tests/integration/publishing/test-platform-publish.sh +++ b/tests/integration/publishing/test-platform-publish.sh @@ -66,6 +66,10 @@ export FETCHIT_TEST_REAL_PODMAN="$real_podman" cat > "$wrapper_dir/podman" <<'WRAPPER' #!/usr/bin/env bash set -euo pipefail +if [[ ${FETCHIT_TEST_MISSING_CHILD:-false} == true && ${1:-} == manifest && ${2:-} == inspect && "$*" == *@sha256:* ]]; then + echo 'Simulated missing destination child manifest' >&2 + exit 125 +fi if [[ ${1:-} == push ]]; then shift exec "$FETCHIT_TEST_REAL_PODMAN" push --compression-format=gzip --compression-level=1 --force-compression "$@" @@ -78,6 +82,10 @@ WRAPPER chmod +x "$wrapper_dir/podman" PATH="$wrapper_dir:$PATH" FETCHIT_REGISTRY_TLS_VERIFY=false bash scripts/publish-platform-index.sh \ 127.0.0.1:5000/fetchit-recompression:latest localhost/publish-source:amd64 localhost/publish-source:arm64 +# If a destination child cannot be read, fail before creating a public index. +expect_failure env PATH="$wrapper_dir:$PATH" FETCHIT_TEST_MISSING_CHILD=true FETCHIT_REGISTRY_TLS_VERIFY=false \ + bash scripts/publish-platform-index.sh 127.0.0.1:5000/fetchit-missing-child:latest localhost/publish-source:amd64 localhost/publish-source:arm64 +expect_failure podman manifest inspect --tls-verify=false 127.0.0.1:5000/fetchit-missing-child:latest rm -rf "$wrapper_dir" for arch in amd64 arm64; do loaded=$(podman pull --quiet --tls-verify=false --policy=always --platform "linux/$arch" 127.0.0.1:5000/fetchit-recompression:latest)