A container registry that uses the AT Protocol for manifest storage and S3 for blob storage.
atcr.io
docker
container
atproto
go
1#!/bin/bash
2set -e
3
4# Usage function
5usage() {
6 echo "Usage: $0 <source-image> [target-image]"
7 echo ""
8 echo "Examples:"
9 echo " $0 ghcr.io/evanjarrett/myapp:latest"
10 echo " $0 ghcr.io/evanjarrett/myapp:latest atcr.io/evan.jarrett.net/myapp:latest"
11 echo ""
12 echo "If target-image is not specified, it will use atcr.io/<username>/<repo>:<tag>"
13 exit 1
14}
15
16# Check arguments
17if [ $# -lt 1 ]; then
18 usage
19fi
20
21SOURCE_IMAGE="$1"
22TARGET_IMAGE="${2:-}"
23
24# Parse source image to extract components
25# Format: [registry/]repository[:tag|@digest]
26parse_image_ref() {
27 local ref="$1"
28 local registry=""
29 local repository=""
30 local tag="latest"
31
32 # Remove digest if present (we'll fetch the manifest-list)
33 ref="${ref%@*}"
34
35 # Extract tag
36 if [[ "$ref" == *:* ]]; then
37 tag="${ref##*:}"
38 ref="${ref%:*}"
39 fi
40
41 # Extract registry and repository
42 if [[ "$ref" == */*/* ]]; then
43 # Has registry
44 registry="${ref%%/*}"
45 repository="${ref#*/}"
46 else
47 # No registry, assume Docker Hub
48 registry="docker.io"
49 repository="$ref"
50 fi
51
52 echo "$registry" "$repository" "$tag"
53}
54
55# Parse source image
56read -r SOURCE_REGISTRY SOURCE_REPO SOURCE_TAG <<< "$(parse_image_ref "$SOURCE_IMAGE")"
57
58# If no target specified, auto-generate it
59if [ -z "$TARGET_IMAGE" ]; then
60 # Extract just the repo name (last component)
61 REPO_NAME="${SOURCE_REPO##*/}"
62 # Try to extract username from source
63 if [[ "$SOURCE_REPO" == */* ]]; then
64 USERNAME="${SOURCE_REPO%/*}"
65 USERNAME="${USERNAME##*/}"
66 else
67 USERNAME="default"
68 fi
69 TARGET_IMAGE="atcr.io/${USERNAME}/${REPO_NAME}:${SOURCE_TAG}"
70fi
71
72# Parse target image
73read -r TARGET_REGISTRY TARGET_REPO TARGET_TAG <<< "$(parse_image_ref "$TARGET_IMAGE")"
74
75echo "=== Migrating multi-arch image ==="
76echo "Source: ${SOURCE_REGISTRY}/${SOURCE_REPO}:${SOURCE_TAG}"
77echo "Target: ${TARGET_REGISTRY}/${TARGET_REPO}:${TARGET_TAG}"
78echo ""
79
80# Full source reference
81SOURCE_REF="${SOURCE_REGISTRY}/${SOURCE_REPO}:${SOURCE_TAG}"
82TARGET_REF="${TARGET_REGISTRY}/${TARGET_REPO}:${TARGET_TAG}"
83
84# Fetch the manifest list
85echo ">>> Fetching manifest list from source..."
86MANIFEST_JSON=$(docker manifest inspect "$SOURCE_REF" 2>/dev/null || {
87 echo "Error: Failed to fetch manifest list. This may not be a multi-arch image."
88 echo "Trying as single-arch image..."
89
90 # Try pulling as single image
91 docker pull "$SOURCE_REF"
92 docker tag "$SOURCE_REF" "$TARGET_REF"
93 docker push "$TARGET_REF"
94 echo "=== Migration complete (single-arch) ==="
95 exit 0
96})
97
98# Check if this is a manifest list
99MEDIA_TYPE=$(echo "$MANIFEST_JSON" | jq -r '.mediaType // .schemaVersion')
100if [[ ! "$MEDIA_TYPE" =~ "manifest.list" ]] && [[ ! "$MEDIA_TYPE" =~ "index" ]]; then
101 echo "Warning: Source appears to be a single-arch image, not a manifest list."
102 docker pull "$SOURCE_REF"
103 docker tag "$SOURCE_REF" "$TARGET_REF"
104 docker push "$TARGET_REF"
105 echo "=== Migration complete (single-arch) ==="
106 exit 0
107fi
108
109echo "Found multi-arch manifest list"
110echo ""
111
112# Extract platform information and digests (skip unknown/unknown for cosign artifacts)
113PLATFORMS=$(echo "$MANIFEST_JSON" | jq -r '.manifests[] | select(.platform.os != "unknown" and .platform.architecture != "unknown") | "\(.platform.os)|\(.platform.architecture)|\(.platform.variant // "")|\(.digest)"')
114
115# Arrays to store pushed images for manifest creation
116declare -a PUSHED_IMAGES
117declare -a PLATFORM_INFO
118
119# Process each platform
120while IFS='|' read -r os arch variant digest; do
121 # Create platform tag (e.g., "linux-amd64" or "linux-arm-v7")
122 PLATFORM_TAG="${os}-${arch}"
123 if [ -n "$variant" ]; then
124 PLATFORM_TAG="${PLATFORM_TAG}-${variant}"
125 fi
126
127 echo ">>> Processing ${os}/${arch}${variant:+/$variant}..."
128 echo " Digest: $digest"
129
130 # Pull by digest
131 echo " Pulling image..."
132 docker pull "${SOURCE_REGISTRY}/${SOURCE_REPO}@${digest}"
133
134 # Tag for target
135 TARGET_PLATFORM_REF="${TARGET_REGISTRY}/${TARGET_REPO}:${TARGET_TAG}-${PLATFORM_TAG}"
136 echo " Tagging as: ${TARGET_PLATFORM_REF}"
137 docker tag "${SOURCE_REGISTRY}/${SOURCE_REPO}@${digest}" "${TARGET_PLATFORM_REF}"
138
139 # Push platform-specific image
140 echo " Pushing..."
141 docker push "${TARGET_PLATFORM_REF}"
142
143 # Store for manifest creation
144 PUSHED_IMAGES+=("${TARGET_PLATFORM_REF}")
145 PLATFORM_INFO+=("${os}|${arch}|${variant}")
146
147 echo ""
148done <<< "$PLATFORMS"
149
150# Create multi-arch manifest
151echo ">>> Creating multi-arch manifest..."
152MANIFEST_CREATE_CMD="docker manifest create ${TARGET_REF}"
153for img in "${PUSHED_IMAGES[@]}"; do
154 MANIFEST_CREATE_CMD="${MANIFEST_CREATE_CMD} --amend ${img}"
155done
156
157eval "$MANIFEST_CREATE_CMD"
158echo ""
159
160# Annotate each platform
161echo ">>> Annotating manifest with platform information..."
162for i in "${!PUSHED_IMAGES[@]}"; do
163 IFS='|' read -r os arch variant <<< "${PLATFORM_INFO[$i]}"
164
165 ANNOTATE_CMD="docker manifest annotate ${TARGET_REF} ${PUSHED_IMAGES[$i]} --os ${os} --arch ${arch}"
166 if [ -n "$variant" ]; then
167 ANNOTATE_CMD="${ANNOTATE_CMD} --variant ${variant}"
168 fi
169
170 echo " Annotating ${os}/${arch}${variant:+/$variant}..."
171 eval "$ANNOTATE_CMD"
172done
173echo ""
174
175# Push the manifest list
176echo ">>> Pushing multi-arch manifest..."
177docker manifest push "${TARGET_REF}"
178echo ""
179
180echo "=== Migration complete! ==="
181echo "You can now pull: docker pull ${TARGET_REF}"
182echo ""
183echo "Migrated platforms:"
184for i in "${!PLATFORM_INFO[@]}"; do
185 IFS='|' read -r os arch variant <<< "${PLATFORM_INFO[$i]}"
186 echo " - ${os}/${arch}${variant:+/$variant}"
187done