Signed-off-by: Andrew "Tianon" Page <admwiggin@gmail.com>
| ... | ... |
@@ -1,5 +1,5 @@ |
| 1 | 1 |
#!/bin/bash |
| 2 |
-set -e |
|
| 2 |
+set -eo pipefail |
|
| 3 | 3 |
|
| 4 | 4 |
# hello-world latest ef872312fe1b 3 months ago 910 B |
| 5 | 5 |
# hello-world latest ef872312fe1bbc5e05aae626791a47ee9b032efa8f3bda39cc0be7b56bfe59b9 3 months ago 910 B |
| ... | ... |
@@ -31,8 +31,19 @@ mkdir -p "$dir" |
| 31 | 31 |
# hacky workarounds for Bash 3 support (no associative arrays) |
| 32 | 32 |
images=() |
| 33 | 33 |
rm -f "$dir"/tags-*.tmp |
| 34 |
+manifestJsonEntries=() |
|
| 35 |
+doNotGenerateManifestJson= |
|
| 34 | 36 |
# repositories[busybox]='"latest": "...", "ubuntu-14.04": "..."' |
| 35 | 37 |
|
| 38 |
+# bash v4 on Windows CI requires CRLF separator |
|
| 39 |
+newlineIFS=$'\n' |
|
| 40 |
+if [ "$(go env GOHOSTOS)" = 'windows' ]; then |
|
| 41 |
+ major=$(echo ${BASH_VERSION%%[^0.9]} | cut -d. -f1)
|
|
| 42 |
+ if [ "$major" -ge 4 ]; then |
|
| 43 |
+ newlineIFS=$'\r\n' |
|
| 44 |
+ fi |
|
| 45 |
+fi |
|
| 46 |
+ |
|
| 36 | 47 |
while [ $# -gt 0 ]; do |
| 37 | 48 |
imageTag="$1" |
| 38 | 49 |
shift |
| ... | ... |
@@ -48,30 +59,187 @@ while [ $# -gt 0 ]; do |
| 48 | 48 |
|
| 49 | 49 |
imageFile="${image//\//_}" # "/" can't be in filenames :)
|
| 50 | 50 |
|
| 51 |
- token="$(curl -sSL "https://auth.docker.io/token?service=registry.docker.io&scope=repository:$image:pull" | jq --raw-output .token)" |
|
| 51 |
+ token="$(curl -fsSL "https://auth.docker.io/token?service=registry.docker.io&scope=repository:$image:pull" | jq --raw-output '.token')" |
|
| 52 | 52 |
|
| 53 |
- manifestJson="$(curl -sSL -H "Authorization: Bearer $token" "https://registry-1.docker.io/v2/$image/manifests/$digest")" |
|
| 53 |
+ manifestJson="$( |
|
| 54 |
+ curl -fsSL \ |
|
| 55 |
+ -H "Authorization: Bearer $token" \ |
|
| 56 |
+ -H 'Accept: application/vnd.docker.distribution.manifest.v2+json' \ |
|
| 57 |
+ -H 'Accept: application/vnd.docker.distribution.manifest.v1+json' \ |
|
| 58 |
+ "https://registry-1.docker.io/v2/$image/manifests/$digest" |
|
| 59 |
+ )" |
|
| 54 | 60 |
if [ "${manifestJson:0:1}" != '{' ]; then
|
| 55 | 61 |
echo >&2 "error: /v2/$image/manifests/$digest returned something unexpected:" |
| 56 | 62 |
echo >&2 " $manifestJson" |
| 57 | 63 |
exit 1 |
| 58 | 64 |
fi |
| 59 | 65 |
|
| 60 |
- layersFs=$(echo "$manifestJson" | jq --raw-output '.fsLayers | .[] | .blobSum') |
|
| 66 |
+ imageIdentifier="$image:$tag@$digest" |
|
| 61 | 67 |
|
| 62 |
- IFS=$'\n' |
|
| 63 |
- # bash v4 on Windows CI requires CRLF separator |
|
| 64 |
- if [ "$(go env GOHOSTOS)" = 'windows' ]; then |
|
| 65 |
- major=$(echo ${BASH_VERSION%%[^0.9]} | cut -d. -f1)
|
|
| 66 |
- if [ "$major" -ge 4 ]; then |
|
| 67 |
- IFS=$'\r\n' |
|
| 68 |
- fi |
|
| 69 |
- fi |
|
| 70 |
- layers=( ${layersFs} )
|
|
| 71 |
- unset IFS |
|
| 68 |
+ schemaVersion="$(echo "$manifestJson" | jq --raw-output '.schemaVersion')" |
|
| 69 |
+ case "$schemaVersion" in |
|
| 70 |
+ 2) |
|
| 71 |
+ mediaType="$(echo "$manifestJson" | jq --raw-output '.mediaType')" |
|
| 72 |
+ |
|
| 73 |
+ case "$mediaType" in |
|
| 74 |
+ application/vnd.docker.distribution.manifest.v2+json) |
|
| 75 |
+ configDigest="$(echo "$manifestJson" | jq --raw-output '.config.digest')" |
|
| 76 |
+ imageId="${configDigest#*:}" # strip off "sha256:"
|
|
| 77 |
+ |
|
| 78 |
+ configFile="$imageId.json" |
|
| 79 |
+ curl -fsSL \ |
|
| 80 |
+ -H "Authorization: Bearer $token" \ |
|
| 81 |
+ "https://registry-1.docker.io/v2/$image/blobs/$configDigest" \ |
|
| 82 |
+ -o "$dir/$configFile" |
|
| 83 |
+ |
|
| 84 |
+ layersFs="$(echo "$manifestJson" | jq --raw-output --compact-output '.layers[]')" |
|
| 85 |
+ IFS="$newlineIFS" |
|
| 86 |
+ layers=( $layersFs ) |
|
| 87 |
+ unset IFS |
|
| 88 |
+ |
|
| 89 |
+ echo "Downloading '$imageIdentifier' (${#layers[@]} layers)..."
|
|
| 90 |
+ layerId= |
|
| 91 |
+ layerFiles=() |
|
| 92 |
+ for i in "${!layers[@]}"; do
|
|
| 93 |
+ layerMeta="${layers[$i]}"
|
|
| 94 |
+ |
|
| 95 |
+ layerMediaType="$(echo "$layerMeta" | jq --raw-output '.mediaType')" |
|
| 96 |
+ layerDigest="$(echo "$layerMeta" | jq --raw-output '.digest')" |
|
| 97 |
+ |
|
| 98 |
+ # save the previous layer's ID |
|
| 99 |
+ parentId="$layerId" |
|
| 100 |
+ # create a new fake layer ID based on this layer's digest and the previous layer's fake ID |
|
| 101 |
+ layerId="$(echo "$parentId"$'\n'"$layerDigest" | sha256sum | cut -d' ' -f1)" |
|
| 102 |
+ # this accounts for the possibility that an image contains the same layer twice (and thus has a duplicate digest value) |
|
| 103 |
+ |
|
| 104 |
+ mkdir -p "$dir/$layerId" |
|
| 105 |
+ echo '1.0' > "$dir/$layerId/VERSION" |
|
| 106 |
+ |
|
| 107 |
+ if [ ! -s "$dir/$layerId/json" ]; then |
|
| 108 |
+ parentJson="$(printf ', parent: "%s"' "$parentId")" |
|
| 109 |
+ addJson="$(printf '{ id: "%s"%s }' "$layerId" "${parentId:+$parentJson}")"
|
|
| 110 |
+ # this starter JSON is taken directly from Docker's own "docker save" output for unimportant layers |
|
| 111 |
+ jq "$addJson + ." > "$dir/$layerId/json" <<-'EOJSON' |
|
| 112 |
+ {
|
|
| 113 |
+ "created": "0001-01-01T00:00:00Z", |
|
| 114 |
+ "container_config": {
|
|
| 115 |
+ "Hostname": "", |
|
| 116 |
+ "Domainname": "", |
|
| 117 |
+ "User": "", |
|
| 118 |
+ "AttachStdin": false, |
|
| 119 |
+ "AttachStdout": false, |
|
| 120 |
+ "AttachStderr": false, |
|
| 121 |
+ "Tty": false, |
|
| 122 |
+ "OpenStdin": false, |
|
| 123 |
+ "StdinOnce": false, |
|
| 124 |
+ "Env": null, |
|
| 125 |
+ "Cmd": null, |
|
| 126 |
+ "Image": "", |
|
| 127 |
+ "Volumes": null, |
|
| 128 |
+ "WorkingDir": "", |
|
| 129 |
+ "Entrypoint": null, |
|
| 130 |
+ "OnBuild": null, |
|
| 131 |
+ "Labels": null |
|
| 132 |
+ } |
|
| 133 |
+ } |
|
| 134 |
+ EOJSON |
|
| 135 |
+ fi |
|
| 136 |
+ |
|
| 137 |
+ case "$layerMediaType" in |
|
| 138 |
+ application/vnd.docker.image.rootfs.diff.tar.gzip) |
|
| 139 |
+ layerTar="$layerId/layer.tar" |
|
| 140 |
+ layerFiles=( "${layerFiles[@]}" "$layerTar" )
|
|
| 141 |
+ # TODO figure out why "-C -" doesn't work here |
|
| 142 |
+ # "curl: (33) HTTP server doesn't seem to support byte ranges. Cannot resume." |
|
| 143 |
+ # "HTTP/1.1 416 Requested Range Not Satisfiable" |
|
| 144 |
+ if [ -f "$dir/$layerTar" ]; then |
|
| 145 |
+ # TODO hackpatch for no -C support :'( |
|
| 146 |
+ echo "skipping existing ${layerId:0:12}"
|
|
| 147 |
+ continue |
|
| 148 |
+ fi |
|
| 149 |
+ curl -fSL --progress \ |
|
| 150 |
+ -H "Authorization: Bearer $token" \ |
|
| 151 |
+ "https://registry-1.docker.io/v2/$image/blobs/$layerDigest" \ |
|
| 152 |
+ -o "$dir/$layerTar" |
|
| 153 |
+ ;; |
|
| 154 |
+ |
|
| 155 |
+ *) |
|
| 156 |
+ echo >&2 "error: unknown layer mediaType ($imageIdentifier, $layerDigest): '$layerMediaType'" |
|
| 157 |
+ exit 1 |
|
| 158 |
+ ;; |
|
| 159 |
+ esac |
|
| 160 |
+ done |
|
| 161 |
+ |
|
| 162 |
+ # change "$imageId" to be the ID of the last layer we added (needed for old-style "repositories" file which is created later -- specifically for older Docker daemons) |
|
| 163 |
+ imageId="$layerId" |
|
| 164 |
+ |
|
| 165 |
+ # munge the top layer image manifest to have the appropriate image configuration for older daemons |
|
| 166 |
+ imageOldConfig="$(jq --raw-output --compact-output '{ id: .id } + if .parent then { parent: .parent } else {} end' "$dir/$imageId/json")"
|
|
| 167 |
+ jq --raw-output "$imageOldConfig + del(.history, .rootfs)" "$dir/$configFile" > "$dir/$imageId/json" |
|
| 72 | 168 |
|
| 73 |
- history=$(echo "$manifestJson" | jq '.history | [.[] | .v1Compatibility]') |
|
| 74 |
- imageId=$(echo "$history" | jq --raw-output .[0] | jq --raw-output .id) |
|
| 169 |
+ manifestJsonEntry="$( |
|
| 170 |
+ echo '{}' | jq --raw-output '. + {
|
|
| 171 |
+ Config: "'"$configFile"'", |
|
| 172 |
+ RepoTags: ["'"${image#library\/}:$tag"'"],
|
|
| 173 |
+ Layers: '"$(echo '[]' | jq --raw-output ".$(for layerFile in "${layerFiles[@]}"; do echo " + [ \"$layerFile\" ]"; done)")"'
|
|
| 174 |
+ }' |
|
| 175 |
+ )" |
|
| 176 |
+ manifestJsonEntries=( "${manifestJsonEntries[@]}" "$manifestJsonEntry" )
|
|
| 177 |
+ ;; |
|
| 178 |
+ |
|
| 179 |
+ *) |
|
| 180 |
+ echo >&2 "error: unknown manifest mediaType ($imageIdentifier): '$mediaType'" |
|
| 181 |
+ exit 1 |
|
| 182 |
+ ;; |
|
| 183 |
+ esac |
|
| 184 |
+ ;; |
|
| 185 |
+ |
|
| 186 |
+ 1) |
|
| 187 |
+ if [ -z "$doNotGenerateManifestJson" ]; then |
|
| 188 |
+ echo >&2 "warning: '$imageIdentifier' uses schemaVersion '$schemaVersion'" |
|
| 189 |
+ echo >&2 " this script cannot (currently) recreate the 'image config' to put in a 'manifest.json' (thus any schemaVersion 2+ images will be imported in the old way, and their 'docker history' will suffer)" |
|
| 190 |
+ echo >&2 |
|
| 191 |
+ doNotGenerateManifestJson=1 |
|
| 192 |
+ fi |
|
| 193 |
+ |
|
| 194 |
+ layersFs="$(echo "$manifestJson" | jq --raw-output '.fsLayers | .[] | .blobSum')" |
|
| 195 |
+ IFS="$newlineIFS" |
|
| 196 |
+ layers=( $layersFs ) |
|
| 197 |
+ unset IFS |
|
| 198 |
+ |
|
| 199 |
+ history="$(echo "$manifestJson" | jq '.history | [.[] | .v1Compatibility]')" |
|
| 200 |
+ imageId="$(echo "$history" | jq --raw-output '.[0]' | jq --raw-output '.id')" |
|
| 201 |
+ |
|
| 202 |
+ echo "Downloading '$imageIdentifier' (${#layers[@]} layers)..."
|
|
| 203 |
+ for i in "${!layers[@]}"; do
|
|
| 204 |
+ imageJson="$(echo "$history" | jq --raw-output ".[${i}]")"
|
|
| 205 |
+ layerId="$(echo "$imageJson" | jq --raw-output '.id')" |
|
| 206 |
+ imageLayer="${layers[$i]}"
|
|
| 207 |
+ |
|
| 208 |
+ mkdir -p "$dir/$layerId" |
|
| 209 |
+ echo '1.0' > "$dir/$layerId/VERSION" |
|
| 210 |
+ |
|
| 211 |
+ echo "$imageJson" > "$dir/$layerId/json" |
|
| 212 |
+ |
|
| 213 |
+ # TODO figure out why "-C -" doesn't work here |
|
| 214 |
+ # "curl: (33) HTTP server doesn't seem to support byte ranges. Cannot resume." |
|
| 215 |
+ # "HTTP/1.1 416 Requested Range Not Satisfiable" |
|
| 216 |
+ if [ -f "$dir/$layerId/layer.tar" ]; then |
|
| 217 |
+ # TODO hackpatch for no -C support :'( |
|
| 218 |
+ echo "skipping existing ${layerId:0:12}"
|
|
| 219 |
+ continue |
|
| 220 |
+ fi |
|
| 221 |
+ curl -fSL --progress -H "Authorization: Bearer $token" "https://registry-1.docker.io/v2/$image/blobs/$imageLayer" -o "$dir/$layerId/layer.tar" # -C - |
|
| 222 |
+ done |
|
| 223 |
+ ;; |
|
| 224 |
+ |
|
| 225 |
+ *) |
|
| 226 |
+ echo >&2 "error: unknown manifest schemaVersion ($imageIdentifier): '$schemaVersion'" |
|
| 227 |
+ exit 1 |
|
| 228 |
+ ;; |
|
| 229 |
+ esac |
|
| 230 |
+ |
|
| 231 |
+ echo |
|
| 75 | 232 |
|
| 76 | 233 |
if [ -s "$dir/tags-$imageFile.tmp" ]; then |
| 77 | 234 |
echo -n ', ' >> "$dir/tags-$imageFile.tmp" |
| ... | ... |
@@ -79,30 +247,6 @@ while [ $# -gt 0 ]; do |
| 79 | 79 |
images=( "${images[@]}" "$image" )
|
| 80 | 80 |
fi |
| 81 | 81 |
echo -n '"'"$tag"'": "'"$imageId"'"' >> "$dir/tags-$imageFile.tmp" |
| 82 |
- |
|
| 83 |
- echo "Downloading '${image}:${tag}@${digest}' (${#layers[@]} layers)..."
|
|
| 84 |
- for i in "${!layers[@]}"; do
|
|
| 85 |
- imageJson=$(echo "$history" | jq --raw-output .[${i}])
|
|
| 86 |
- imageId=$(echo "$imageJson" | jq --raw-output .id) |
|
| 87 |
- imageLayer=${layers[$i]}
|
|
| 88 |
- |
|
| 89 |
- mkdir -p "$dir/$imageId" |
|
| 90 |
- echo '1.0' > "$dir/$imageId/VERSION" |
|
| 91 |
- |
|
| 92 |
- echo "$imageJson" > "$dir/$imageId/json" |
|
| 93 |
- |
|
| 94 |
- # TODO figure out why "-C -" doesn't work here |
|
| 95 |
- # "curl: (33) HTTP server doesn't seem to support byte ranges. Cannot resume." |
|
| 96 |
- # "HTTP/1.1 416 Requested Range Not Satisfiable" |
|
| 97 |
- if [ -f "$dir/$imageId/layer.tar" ]; then |
|
| 98 |
- # TODO hackpatch for no -C support :'( |
|
| 99 |
- echo "skipping existing ${imageId:0:12}"
|
|
| 100 |
- continue |
|
| 101 |
- fi |
|
| 102 |
- token="$(curl -sSL "https://auth.docker.io/token?service=registry.docker.io&scope=repository:$image:pull" | jq --raw-output .token)" |
|
| 103 |
- curl -SL --progress -H "Authorization: Bearer $token" "https://registry-1.docker.io/v2/$image/blobs/$imageLayer" -o "$dir/$imageId/layer.tar" # -C - |
|
| 104 |
- done |
|
| 105 |
- echo |
|
| 106 | 82 |
done |
| 107 | 83 |
|
| 108 | 84 |
echo -n '{' > "$dir/repositories"
|
| ... | ... |
@@ -120,6 +264,12 @@ echo -n $'\n}\n' >> "$dir/repositories" |
| 120 | 120 |
|
| 121 | 121 |
rm -f "$dir"/tags-*.tmp |
| 122 | 122 |
|
| 123 |
+if [ -z "$doNotGenerateManifestJson" ] && [ "${#manifestJsonEntries[@]}" -gt 0 ]; then
|
|
| 124 |
+ echo '[]' | jq --raw-output ".$(for entry in "${manifestJsonEntries[@]}"; do echo " + [ $entry ]"; done)" > "$dir/manifest.json"
|
|
| 125 |
+else |
|
| 126 |
+ rm -f "$dir/manifest.json" |
|
| 127 |
+fi |
|
| 128 |
+ |
|
| 123 | 129 |
echo "Download of images into '$dir' complete." |
| 124 | 130 |
echo "Use something like the following to load the result into a Docker daemon:" |
| 125 | 131 |
echo " tar -cC '$dir' . | docker load" |