Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
60 changes: 12 additions & 48 deletions .github/workflows/docker-image.yml
Original file line number Diff line number Diff line change
Expand Up @@ -89,7 +89,7 @@ jobs:
run: go mod download && go mod vendor && CTR_CMD=podman make build-ansible-cross-build-linux-arm64

- name: Verify native ARM image
run: test "$(podman image inspect --format '{{.Architecture}}' quay.io/fetchit/fetchit-ansible-arm:latest)" = arm64
run: test "$(podman image inspect --format '{{.Os}}/{{.Architecture}}' quay.io/fetchit/fetchit-ansible-arm:latest)" = linux/arm64

- name: Export image
run: podman save -o /tmp/ansible-arm.tar quay.io/fetchit/fetchit-ansible-arm:latest
Expand Down Expand Up @@ -169,7 +169,7 @@ jobs:
run: go mod download && go mod vendor && CTR_CMD=podman make build-systemd-cross-build-linux-arm64

- name: Verify native ARM image
run: test "$(podman image inspect --format '{{.Architecture}}' quay.io/fetchit/fetchit-systemd-arm:latest)" = arm64
run: test "$(podman image inspect --format '{{.Os}}/{{.Architecture}}' quay.io/fetchit/fetchit-systemd-arm:latest)" = linux/arm64

- name: Export image
run: podman save -o /tmp/systemd-arm.tar quay.io/fetchit/fetchit-systemd-arm:latest
Expand Down Expand Up @@ -1682,7 +1682,7 @@ jobs:
run: go mod download && go mod vendor && CTR_CMD=podman make build-containerized-cross-build-linux-arm64

- name: Verify native ARM image
run: test "$(podman image inspect --format '{{.Architecture}}' quay.io/fetchit/fetchit-arm:latest)" = arm64
run: test "$(podman image inspect --format '{{.Os}}/{{.Architecture}}' quay.io/fetchit/fetchit-arm:latest)" = linux/arm64

- name: Export image
run: podman save -o /tmp/fetchit-arm.tar quay.io/fetchit/fetchit-arm:latest
Expand Down Expand Up @@ -1719,9 +1719,6 @@ jobs:
- name: Load the arm image
run: podman load -i /tmp/fetchit-arm.tar

- name: create manifest
run: podman manifest create quay.io/fetchit/fetchit:latest

- name: Login to Quay
if: (github.event_name == 'push' || github.event_name == 'schedule') && (github.ref == 'refs/heads/main' || startsWith(github.ref, 'refs/tags/v'))
uses: docker/login-action@v3
Expand All @@ -1730,19 +1727,10 @@ jobs:
username: ${{ secrets.REGISTRY_USER }}
password: ${{ secrets.REGISTRY_PASSWORD }}

- name: Assemble and verify the tested platform images
- name: Verify the tested Linux platforms
run: |
test "$(podman image inspect --format '{{.Architecture}}' quay.io/fetchit/fetchit-amd:latest)" = amd64
test "$(podman image inspect --format '{{.Architecture}}' quay.io/fetchit/fetchit-arm:latest)" = arm64
podman manifest add quay.io/fetchit/fetchit:latest quay.io/fetchit/fetchit-arm:latest
podman manifest add quay.io/fetchit/fetchit:latest quay.io/fetchit/fetchit-amd:latest
podman manifest inspect quay.io/fetchit/fetchit:latest > /tmp/platform-index.json
python3 - <<'PYTHON'
import json
with open('/tmp/platform-index.json') as source:
platforms = {(m['platform']['os'], m['platform']['architecture']) for m in json.load(source)['manifests']}
assert platforms == {('linux', 'amd64'), ('linux', 'arm64')}, platforms
PYTHON
test "$(podman image inspect --format '{{.Os}}/{{.Architecture}}' quay.io/fetchit/fetchit-amd:latest)" = linux/amd64
test "$(podman image inspect --format '{{.Os}}/{{.Architecture}}' quay.io/fetchit/fetchit-arm:latest)" = linux/arm64

- name: Publish tested architecture tags and platform index
if: (github.event_name == 'push' || github.event_name == 'schedule') && (github.ref == 'refs/heads/main' || startsWith(github.ref, 'refs/tags/v'))
Expand Down Expand Up @@ -1834,22 +1822,10 @@ jobs:
username: ${{ secrets.REGISTRY_USER }}
password: ${{ secrets.REGISTRY_PASSWORD }}

- name: create manifest
run: podman manifest create quay.io/fetchit/fetchit-ansible:latest

- name: Assemble and verify the tested platform images
- name: Verify the tested Linux platforms
run: |
test "$(podman image inspect --format '{{.Architecture}}' quay.io/fetchit/fetchit-ansible-amd:latest)" = amd64
test "$(podman image inspect --format '{{.Architecture}}' quay.io/fetchit/fetchit-ansible-arm:latest)" = arm64
podman manifest add quay.io/fetchit/fetchit-ansible:latest quay.io/fetchit/fetchit-ansible-arm:latest
podman manifest add quay.io/fetchit/fetchit-ansible:latest quay.io/fetchit/fetchit-ansible-amd:latest
podman manifest inspect quay.io/fetchit/fetchit-ansible:latest > /tmp/platform-index.json
python3 - <<'PYTHON'
import json
with open('/tmp/platform-index.json') as source:
platforms = {(m['platform']['os'], m['platform']['architecture']) for m in json.load(source)['manifests']}
assert platforms == {('linux', 'amd64'), ('linux', 'arm64')}, platforms
PYTHON
test "$(podman image inspect --format '{{.Os}}/{{.Architecture}}' quay.io/fetchit/fetchit-ansible-amd:latest)" = linux/amd64
test "$(podman image inspect --format '{{.Os}}/{{.Architecture}}' quay.io/fetchit/fetchit-ansible-arm:latest)" = linux/arm64

- name: Publish tested architecture tags and platform index
if: (github.event_name == 'push' || github.event_name == 'schedule') && (github.ref == 'refs/heads/main' || startsWith(github.ref, 'refs/tags/v'))
Expand Down Expand Up @@ -1891,22 +1867,10 @@ jobs:
username: ${{ secrets.REGISTRY_USER }}
password: ${{ secrets.REGISTRY_PASSWORD }}

- name: create manifest
run: podman manifest create quay.io/fetchit/fetchit-systemd:latest

- name: Assemble and verify the tested platform images
- name: Verify the tested Linux platforms
run: |
test "$(podman image inspect --format '{{.Architecture}}' quay.io/fetchit/fetchit-systemd-amd:latest)" = amd64
test "$(podman image inspect --format '{{.Architecture}}' quay.io/fetchit/fetchit-systemd-arm:latest)" = arm64
podman manifest add quay.io/fetchit/fetchit-systemd:latest quay.io/fetchit/fetchit-systemd-arm:latest
podman manifest add quay.io/fetchit/fetchit-systemd:latest quay.io/fetchit/fetchit-systemd-amd:latest
podman manifest inspect quay.io/fetchit/fetchit-systemd:latest > /tmp/platform-index.json
python3 - <<'PYTHON'
import json
with open('/tmp/platform-index.json') as source:
platforms = {(m['platform']['os'], m['platform']['architecture']) for m in json.load(source)['manifests']}
assert platforms == {('linux', 'amd64'), ('linux', 'arm64')}, platforms
PYTHON
test "$(podman image inspect --format '{{.Os}}/{{.Architecture}}' quay.io/fetchit/fetchit-systemd-amd:latest)" = linux/amd64
test "$(podman image inspect --format '{{.Os}}/{{.Architecture}}' quay.io/fetchit/fetchit-systemd-arm:latest)" = linux/arm64

- name: Publish tested architecture tags and platform index
if: (github.event_name == 'push' || github.event_name == 'schedule') && (github.ref == 'refs/heads/main' || startsWith(github.ref, 'refs/tags/v'))
Expand Down
2 changes: 2 additions & 0 deletions .github/workflows/platform-publish.yml
Original file line number Diff line number Diff line change
Expand Up @@ -13,5 +13,7 @@ jobs:
steps:
- uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4
- uses: ./.github/actions/setup-podman
- name: Test publishing failure handling
run: bash tests/integration/publishing/test-publish-failures.sh
- name: Test archive import and platform publishing with a local registry
run: bash tests/integration/publishing/test-platform-publish.sh
28 changes: 28 additions & 0 deletions docs/documentation.rst
Original file line number Diff line number Diff line change
Expand Up @@ -105,3 +105,31 @@ the same rootful store. For a local build, use:

sudo podman build -t quay.io/fetchit/fetchit-sample-app:latest \
-f examples/sample-app/Containerfile examples/sample-app

Platform index verification
---------------------------

The publisher stages amd64 and arm64 child manifests in the destination
repository and publishes one index referencing their immutable digests.
Verification reads the index using the digest returned by the push. A local
Comment thread
cooktheryan marked this conversation as resolved.
manifest with the same name as a public ``latest`` tag can shadow registry
inspection in Podman; it must not be used as evidence of published content.
The regression in ``tests/integration/publishing/test-platform-publish.sh``
creates a conflicting local manifest under the destination's ``latest`` name,
then verifies that registry clients can pull both amd64 and arm64 from the
shared tag. ``tests/integration/publishing/test-publish-failures.sh`` checks
missing and invalid digest output and rejects non-Linux images before a push.

For an existing local index whose child manifests have already been pushed to
``quay.io/fetchit/fetchit``, the equivalent digest-based inspection is:

.. code-block:: bash

podman manifest push --all=false --format=docker --digestfile index.digest \
localhost/reviewed-index docker://quay.io/fetchit/fetchit:latest
index_digest=$(<index.digest)
podman manifest inspect "quay.io/fetchit/fetchit@$index_digest"

This checks the content of the exact pushed index, avoiding inspection of a
local manifest that shadows the mutable ``latest`` tag. The publishing script
also validates the digest format and both Linux platform descriptors.
21 changes: 15 additions & 6 deletions scripts/publish-platform-index.sh
Original file line number Diff line number Diff line change
Expand Up @@ -29,8 +29,8 @@ scratch=$(mktemp -d)
manifest="localhost/fetchit-publish-$$"
trap 'publish_status=$?; podman manifest rm "$manifest" >/dev/null 2>&1 || true; rm -rf "$scratch"; exit "$publish_status"' EXIT

test "$(podman image inspect --format '{{.Architecture}}' "$amd_image")" = amd64
test "$(podman image inspect --format '{{.Architecture}}' "$arm_image")" = arm64
test "$(podman image inspect --format '{{.Os}}/{{.Architecture}}' "$amd_image")" = linux/amd64
test "$(podman image inspect --format '{{.Os}}/{{.Architecture}}' "$arm_image")" = linux/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.
# Retain legacy architecture repositories for existing consumers.
Expand All @@ -39,8 +39,8 @@ podman push "${options[@]}" --format=v2s2 "$arm_image" "docker://$repository-arm
# 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")
amd_digest=$(<"$scratch/amd.digest")
arm_digest=$(<"$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
Expand All @@ -58,11 +58,20 @@ podman manifest add "${options[@]}" "$manifest" "docker://$repository@$arm_diges
# can recompress layers and replace the exact digests pinned above. Publish only
# 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
push_args=(
--all=false "${options[@]}" --format=docker
--digestfile "$scratch/index.digest" "$manifest" "docker://$destination"
)
if ! podman manifest push "${push_args[@]}"; then
echo "Failed to publish platform index: $destination" >&2
exit 1
fi
podman manifest inspect "${options[@]}" "$destination" > "$scratch/index.json"
[[ -s "$scratch/index.digest" ]] || { echo 'Missing or empty pushed index digest' >&2; exit 1; }
index_digest=$(<"$scratch/index.digest")
[[ "$index_digest" =~ ^sha256:[0-9a-f]{64}$ ]] || { echo 'Invalid pushed index digest' >&2; exit 1; }
Comment thread
cooktheryan marked this conversation as resolved.
# A local list named DESTINATION shadows the remote tag in manifest inspect.
# Inspect the immutable index returned by this push, not a possibly stale local tag.
podman manifest inspect "${options[@]}" "$repository@$index_digest" > "$scratch/index.json"
python3 - "$scratch/index.json" "$amd_digest" "$arm_digest" <<'PYTHON'
import json
import sys
Expand Down
6 changes: 6 additions & 0 deletions tests/integration/publishing/test-platform-publish.sh
Original file line number Diff line number Diff line change
Expand Up @@ -35,8 +35,14 @@ expect_failure bash scripts/publish-platform-index.sh 127.0.0.1:5000/test:latest
expect_failure bash scripts/publish-platform-index.sh 127.0.0.1:5000/test:latest localhost/publish-missing:amd64 localhost/publish-source:arm64
# Seed an incorrect old architecture tag: the script must replace it.
podman push --tls-verify=false localhost/publish-source:arm64 docker://127.0.0.1:5000/fetchit-test-amd:latest
# Match the production failure: a stale local list has the destination tag.
# Give it only the wrong architecture so inspecting that local tag cannot pass.
podman manifest create 127.0.0.1:5000/fetchit-test:latest
podman manifest add 127.0.0.1:5000/fetchit-test:latest localhost/publish-source:arm64
FETCHIT_REGISTRY_TLS_VERIFY=false bash scripts/publish-platform-index.sh \
127.0.0.1:5000/fetchit-test:latest localhost/publish-source:amd64 localhost/publish-source:arm64
# Remove the intentionally stale local tag before the independent remote checks.
podman manifest rm 127.0.0.1:5000/fetchit-test:latest
# Check both architecture tags before deliberately moving one.
for arch_tag in amd arm; do
podman manifest inspect --tls-verify=false "127.0.0.1:5000/fetchit-test-$arch_tag:latest" > "/tmp/$arch_tag-tag.json"
Expand Down
83 changes: 83 additions & 0 deletions tests/integration/publishing/test-publish-failures.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
#!/usr/bin/env bash
# Fault injection exercises failure paths without publishing to a real registry.
set -euo pipefail
fixture=$(mktemp -d)
trap 'rm -rf "$fixture"' EXIT
mkdir "$fixture/bin"
cat > "$fixture/bin/podman" <<'PYTHON'
#!/usr/bin/env python3
import json
import os
from pathlib import Path
import sys

args = sys.argv[1:]
root = Path(os.environ['FETCHIT_FAULT_DIRECTORY'])
case = os.environ['FETCHIT_FAULT_CASE']
with (root / 'calls').open('a') as calls:
calls.write(json.dumps(args) + '\n')
if args[:2] == ['image', 'inspect']:
architecture = 'amd64' if args[-1] == 'fixture-amd' else 'arm64'
print(('windows' if case == 'non-linux' else 'linux') + '/' + architecture)
elif args[0] == 'push':
if '--digestfile' in args:
path = Path(args[args.index('--digestfile') + 1])
path.write_text('sha256:' + ('a' if path.name == 'amd.digest' else 'b') * 64)
elif args[:2] == ['manifest', 'push']:
path = Path(args[args.index('--digestfile') + 1])
if case == 'missing':
pass
elif case == 'empty':
path.write_text('')
elif case == 'malformed':
path.write_text('not-a-digest')
elif case == 'unsupported':
path.write_text('sha512:' + 'c' * 128)
else:
path.write_text('sha256:' + 'c' * 64)
elif args[:2] == ['manifest', 'inspect']:
if args[-1].endswith('@sha256:' + 'c' * 64):
(root / 'index-inspected').touch()
print(json.dumps({'manifests': [
{'platform': {'os': 'linux', 'architecture': architecture}, 'digest': 'sha256:' + digit * 64}
for architecture, digit in [('amd64', 'a'), ('arm64', 'b')]
]}))
else:
print('{}')
elif args[:2] not in (['manifest', 'create'], ['manifest', 'add'], ['manifest', 'rm']):
raise SystemExit('Unexpected fake Podman command: ' + repr(args))
PYTHON
chmod +x "$fixture/bin/podman"
export PATH="$fixture/bin:$PATH"
for case in missing empty malformed unsupported non-linux success; do
directory="$fixture/$case"
mkdir "$directory"
status=0
FETCHIT_FAULT_CASE="$case" FETCHIT_FAULT_DIRECTORY="$directory" \
bash scripts/publish-platform-index.sh example.invalid/test:latest fixture-amd fixture-arm \
> "$directory/output" 2>&1 || status=$?
if [[ "$case" == success ]]; then
test "$status" = 0
test -f "$directory/index-inspected"
else
test "$status" != 0
test ! -e "$directory/index-inspected"
case "$case" in
missing|empty) grep -q 'Missing or empty pushed index digest' "$directory/output" ;;
malformed|unsupported) grep -q 'Invalid pushed index digest' "$directory/output" ;;
esac
fi
python3 - "$directory/calls" "$case" <<'PYTHON'
import json
import sys
calls = [json.loads(line) for line in open(sys.argv[1])]
inspections = [call for call in calls if call[:2] == ['manifest', 'inspect']]
if sys.argv[2] == 'non-linux':
assert not any(call[0] == 'push' or call[:2] == ['manifest', 'push'] for call in calls), calls
assert not inspections, inspections
else:
# Only the two child preflights may run when the index digest is invalid.
assert len(inspections) == (3 if sys.argv[2] == 'success' else 2), inspections
PYTHON
echo "Publishing fault case passed: $case"
done
Loading