github.com/moby/docker@v26.1.3+incompatible/contrib/download-frozen-image-v2.sh (about)

     1  #!/usr/bin/env bash
     2  set -eo pipefail
     3  
     4  # This script was developed for use in Moby's CI, and as such the use cases and
     5  # usability are (intentionally) limited. You may find this script useful for
     6  # educational purposes, for example, to learn how pulling images works "under
     7  # the hood", and you may be able to use it for other purposes, but it should not
     8  # be considered a "general purpose" tool for pulling images.
     9  #
    10  # The project maintainers accept contributions to this script within its intended
    11  # scope, but may not accept contributions beyond that.
    12  #
    13  # For users who have a similar need but require more flexibility/functionality,
    14  # refer the the discussion on GitHub, which mentions various alternatives that
    15  # are more suitable for other uses: https://github.com/moby/moby/issues/40857
    16  
    17  # hello-world                      latest              ef872312fe1b        3 months ago        910 B
    18  # hello-world                      latest              ef872312fe1bbc5e05aae626791a47ee9b032efa8f3bda39cc0be7b56bfe59b9   3 months ago        910 B
    19  
    20  # debian                           latest              f6fab3b798be        10 weeks ago        85.1 MB
    21  # debian                           latest              f6fab3b798be3174f45aa1eb731f8182705555f89c9026d8c1ef230cbf8301dd   10 weeks ago        85.1 MB
    22  
    23  # check if essential commands are in our PATH
    24  for cmd in curl jq; do
    25  	if ! command -v $cmd &> /dev/null; then
    26  		echo >&2 "error: \"$cmd\" not found!"
    27  		exit 1
    28  	fi
    29  done
    30  
    31  usage() {
    32  	echo "usage: $0 dir image[:tag][@digest] ..."
    33  	echo "       $0 /tmp/old-hello-world hello-world:latest@sha256:8be990ef2aeb16dbcb9271ddfe2610fa6658d13f6dfb8bc72074cc1ca36966a7"
    34  	[ -z "$1" ] || exit "$1"
    35  }
    36  
    37  dir="$1" # dir for building tar in
    38  shift || usage 1 >&2
    39  
    40  if ! [ $# -gt 0 ] && [ "$dir" ]; then
    41  	usage 2 >&2
    42  fi
    43  mkdir -p "$dir"
    44  
    45  # hacky workarounds for Bash 3 support (no associative arrays)
    46  images=()
    47  rm -f "$dir"/tags-*.tmp
    48  manifestJsonEntries=()
    49  doNotGenerateManifestJson=
    50  # repositories[busybox]='"latest": "...", "ubuntu-14.04": "..."'
    51  
    52  # bash v4 on Windows CI requires CRLF separator... and linux doesn't seem to care either way
    53  newlineIFS=$'\n'
    54  major=$(echo "${BASH_VERSION%%[^0.9]}" | cut -d. -f1)
    55  if [ "$major" -ge 4 ]; then
    56  	newlineIFS=$'\r\n'
    57  fi
    58  
    59  registryBase='https://registry-1.docker.io'
    60  authBase='https://auth.docker.io'
    61  authService='registry.docker.io'
    62  
    63  # https://github.com/moby/moby/issues/33700
    64  fetch_blob() {
    65  	local token="$1"
    66  	shift
    67  	local image="$1"
    68  	shift
    69  	local digest="$1"
    70  	shift
    71  	local targetFile="$1"
    72  	shift
    73  	local curlArgs=("$@")
    74  
    75  	local curlHeaders
    76  	curlHeaders="$(
    77  		curl -S "${curlArgs[@]}" \
    78  			-H "Authorization: Bearer $token" \
    79  			"$registryBase/v2/$image/blobs/$digest" \
    80  			-o "$targetFile" \
    81  			-D-
    82  	)"
    83  	curlHeaders="$(echo "$curlHeaders" | tr -d '\r')"
    84  	if grep -qE "^HTTP/[0-9].[0-9] 3" <<< "$curlHeaders"; then
    85  		rm -f "$targetFile"
    86  
    87  		local blobRedirect
    88  		blobRedirect="$(echo "$curlHeaders" | awk -F ': ' 'tolower($1) == "location" { print $2; exit }')"
    89  		if [ -z "$blobRedirect" ]; then
    90  			echo >&2 "error: failed fetching '$image' blob '$digest'"
    91  			echo "$curlHeaders" | head -1 >&2
    92  			return 1
    93  		fi
    94  
    95  		curl -fSL "${curlArgs[@]}" \
    96  			"$blobRedirect" \
    97  			-o "$targetFile"
    98  	fi
    99  }
   100  
   101  # handle 'application/vnd.docker.distribution.manifest.v2+json' manifest
   102  handle_single_manifest_v2() {
   103  	local manifestJson="$1"
   104  	shift
   105  
   106  	local configDigest
   107  	configDigest="$(echo "$manifestJson" | jq --raw-output '.config.digest')"
   108  	local imageId="${configDigest#*:}" # strip off "sha256:"
   109  
   110  	local configFile="$imageId.json"
   111  	fetch_blob "$token" "$image" "$configDigest" "$dir/$configFile" -s
   112  
   113  	local layersFs
   114  	layersFs="$(echo "$manifestJson" | jq --raw-output --compact-output '.layers[]')"
   115  	local IFS="$newlineIFS"
   116  	local layers
   117  	mapfile -t layers <<< "$layersFs"
   118  	unset IFS
   119  
   120  	echo "Downloading '$imageIdentifier' (${#layers[@]} layers)..."
   121  	local layerId=
   122  	local layerFiles=()
   123  	for i in "${!layers[@]}"; do
   124  		local layerMeta="${layers[$i]}"
   125  
   126  		local layerMediaType
   127  		layerMediaType="$(echo "$layerMeta" | jq --raw-output '.mediaType')"
   128  		local layerDigest
   129  		layerDigest="$(echo "$layerMeta" | jq --raw-output '.digest')"
   130  
   131  		# save the previous layer's ID
   132  		local parentId="$layerId"
   133  		# create a new fake layer ID based on this layer's digest and the previous layer's fake ID
   134  		layerId="$(echo "$parentId"$'\n'"$layerDigest" | sha256sum | cut -d' ' -f1)"
   135  		# this accounts for the possibility that an image contains the same layer twice (and thus has a duplicate digest value)
   136  
   137  		mkdir -p "$dir/$layerId"
   138  		echo '1.0' > "$dir/$layerId/VERSION"
   139  
   140  		if [ ! -s "$dir/$layerId/json" ]; then
   141  			local parentJson
   142  			parentJson="$(printf ', parent: "%s"' "$parentId")"
   143  			local addJson
   144  			addJson="$(printf '{ id: "%s"%s }' "$layerId" "${parentId:+$parentJson}")"
   145  			# this starter JSON is taken directly from Docker's own "docker save" output for unimportant layers
   146  			jq "$addJson + ." > "$dir/$layerId/json" <<- 'EOJSON'
   147  				{
   148  					"created": "0001-01-01T00:00:00Z",
   149  					"container_config": {
   150  						"Hostname": "",
   151  						"Domainname": "",
   152  						"User": "",
   153  						"AttachStdin": false,
   154  						"AttachStdout": false,
   155  						"AttachStderr": false,
   156  						"Tty": false,
   157  						"OpenStdin": false,
   158  						"StdinOnce": false,
   159  						"Env": null,
   160  						"Cmd": null,
   161  						"Image": "",
   162  						"Volumes": null,
   163  						"WorkingDir": "",
   164  						"Entrypoint": null,
   165  						"OnBuild": null,
   166  						"Labels": null
   167  					}
   168  				}
   169  			EOJSON
   170  		fi
   171  
   172  		case "$layerMediaType" in
   173  			application/vnd.docker.image.rootfs.diff.tar.gzip)
   174  				local layerTar="$layerId/layer.tar"
   175  				layerFiles=("${layerFiles[@]}" "$layerTar")
   176  				# TODO figure out why "-C -" doesn't work here
   177  				# "curl: (33) HTTP server doesn't seem to support byte ranges. Cannot resume."
   178  				# "HTTP/1.1 416 Requested Range Not Satisfiable"
   179  				if [ -f "$dir/$layerTar" ]; then
   180  					# TODO hackpatch for no -C support :'(
   181  					echo "skipping existing ${layerId:0:12}"
   182  					continue
   183  				fi
   184  				local token
   185  				token="$(curl -fsSL "$authBase/token?service=$authService&scope=repository:$image:pull" | jq --raw-output '.token')"
   186  				fetch_blob "$token" "$image" "$layerDigest" "$dir/$layerTar" --progress-bar
   187  				;;
   188  
   189  			*)
   190  				echo >&2 "error: unknown layer mediaType ($imageIdentifier, $layerDigest): '$layerMediaType'"
   191  				exit 1
   192  				;;
   193  		esac
   194  	done
   195  
   196  	# 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)
   197  	imageId="$layerId"
   198  
   199  	# munge the top layer image manifest to have the appropriate image configuration for older daemons
   200  	local imageOldConfig
   201  	imageOldConfig="$(jq --raw-output --compact-output '{ id: .id } + if .parent then { parent: .parent } else {} end' "$dir/$imageId/json")"
   202  	jq --raw-output "$imageOldConfig + del(.history, .rootfs)" "$dir/$configFile" > "$dir/$imageId/json"
   203  
   204  	local manifestJsonEntry
   205  	manifestJsonEntry="$(
   206  		echo '{}' | jq --raw-output '. + {
   207  			Config: "'"$configFile"'",
   208  			RepoTags: ["'"${image#library\/}:$tag"'"],
   209  			Layers: '"$(echo '[]' | jq --raw-output ".$(for layerFile in "${layerFiles[@]}"; do echo " + [ \"$layerFile\" ]"; done)")"'
   210  		}'
   211  	)"
   212  	manifestJsonEntries=("${manifestJsonEntries[@]}" "$manifestJsonEntry")
   213  }
   214  
   215  get_target_arch() {
   216  	if [ -n "${TARGETARCH:-}" ]; then
   217  		echo "${TARGETARCH}"
   218  		return 0
   219  	fi
   220  
   221  	if type go > /dev/null; then
   222  		go env GOARCH
   223  		return 0
   224  	fi
   225  
   226  	if type dpkg > /dev/null; then
   227  		debArch="$(dpkg --print-architecture)"
   228  		case "${debArch}" in
   229  			armel | armhf)
   230  				echo "arm"
   231  				return 0
   232  				;;
   233  			*64el)
   234  				echo "${debArch%el}le"
   235  				return 0
   236  				;;
   237  			*)
   238  				echo "${debArch}"
   239  				return 0
   240  				;;
   241  		esac
   242  	fi
   243  
   244  	if type uname > /dev/null; then
   245  		uArch="$(uname -m)"
   246  		case "${uArch}" in
   247  			x86_64)
   248  				echo amd64
   249  				return 0
   250  				;;
   251  			arm | armv[0-9]*)
   252  				echo arm
   253  				return 0
   254  				;;
   255  			aarch64)
   256  				echo arm64
   257  				return 0
   258  				;;
   259  			mips*)
   260  				echo >&2 "I see you are running on mips but I don't know how to determine endianness yet, so I cannot select a correct arch to fetch."
   261  				echo >&2 "Consider installing \"go\" on the system which I can use to determine the correct arch or specify it explicitly by setting TARGETARCH"
   262  				exit 1
   263  				;;
   264  			*)
   265  				echo "${uArch}"
   266  				return 0
   267  				;;
   268  		esac
   269  
   270  	fi
   271  
   272  	# default value
   273  	echo >&2 "Unable to determine CPU arch, falling back to amd64. You can specify a target arch by setting TARGETARCH"
   274  	echo amd64
   275  }
   276  
   277  get_target_variant() {
   278  	echo "${TARGETVARIANT:-}"
   279  }
   280  
   281  while [ $# -gt 0 ]; do
   282  	imageTag="$1"
   283  	shift
   284  	image="${imageTag%%[:@]*}"
   285  	imageTag="${imageTag#*:}"
   286  	digest="${imageTag##*@}"
   287  	tag="${imageTag%%@*}"
   288  
   289  	# add prefix library if passed official image
   290  	if [[ "$image" != *"/"* ]]; then
   291  		image="library/$image"
   292  	fi
   293  
   294  	imageFile="${image//\//_}" # "/" can't be in filenames :)
   295  
   296  	token="$(curl -fsSL "$authBase/token?service=$authService&scope=repository:$image:pull" | jq --raw-output '.token')"
   297  
   298  	manifestJson="$(
   299  		curl -fsSL \
   300  			-H "Authorization: Bearer $token" \
   301  			-H 'Accept: application/vnd.docker.distribution.manifest.v2+json' \
   302  			-H 'Accept: application/vnd.docker.distribution.manifest.list.v2+json' \
   303  			-H 'Accept: application/vnd.docker.distribution.manifest.v1+json' \
   304  			"$registryBase/v2/$image/manifests/$digest"
   305  	)"
   306  	if [ "${manifestJson:0:1}" != '{' ]; then
   307  		echo >&2 "error: /v2/$image/manifests/$digest returned something unexpected:"
   308  		echo >&2 "  $manifestJson"
   309  		exit 1
   310  	fi
   311  
   312  	imageIdentifier="$image:$tag@$digest"
   313  
   314  	schemaVersion="$(echo "$manifestJson" | jq --raw-output '.schemaVersion')"
   315  	case "$schemaVersion" in
   316  		2)
   317  			mediaType="$(echo "$manifestJson" | jq --raw-output '.mediaType')"
   318  
   319  			case "$mediaType" in
   320  				application/vnd.docker.distribution.manifest.v2+json)
   321  					handle_single_manifest_v2 "$manifestJson"
   322  					;;
   323  				application/vnd.docker.distribution.manifest.list.v2+json)
   324  					layersFs="$(echo "$manifestJson" | jq --raw-output --compact-output '.manifests[]')"
   325  					IFS="$newlineIFS"
   326  					mapfile -t layers <<< "$layersFs"
   327  					unset IFS
   328  
   329  					found=""
   330  					targetArch="$(get_target_arch)"
   331  					targetVariant="$(get_target_variant)"
   332  					# parse first level multi-arch manifest
   333  					for i in "${!layers[@]}"; do
   334  						layerMeta="${layers[$i]}"
   335  						maniArch="$(echo "$layerMeta" | jq --raw-output '.platform.architecture')"
   336  						maniVariant="$(echo "$layerMeta" | jq --raw-output '.platform.variant')"
   337  						if [[ "$maniArch" = "${targetArch}" ]] && [[ -z "${targetVariant}" || "$maniVariant" = "${targetVariant}" ]]; then
   338  							digest="$(echo "$layerMeta" | jq --raw-output '.digest')"
   339  							# get second level single manifest
   340  							submanifestJson="$(
   341  								curl -fsSL \
   342  									-H "Authorization: Bearer $token" \
   343  									-H 'Accept: application/vnd.docker.distribution.manifest.v2+json' \
   344  									-H 'Accept: application/vnd.docker.distribution.manifest.list.v2+json' \
   345  									-H 'Accept: application/vnd.docker.distribution.manifest.v1+json' \
   346  									"$registryBase/v2/$image/manifests/$digest"
   347  							)"
   348  							handle_single_manifest_v2 "$submanifestJson"
   349  							found="found"
   350  							break
   351  						fi
   352  					done
   353  					if [ -z "$found" ]; then
   354  						echo >&2 "error: manifest for ${targetArch}${targetVariant:+/${targetVariant}} is not found"
   355  						exit 1
   356  					fi
   357  					;;
   358  				*)
   359  					echo >&2 "error: unknown manifest mediaType ($imageIdentifier): '$mediaType'"
   360  					exit 1
   361  					;;
   362  			esac
   363  			;;
   364  
   365  		1)
   366  			if [ -z "$doNotGenerateManifestJson" ]; then
   367  				echo >&2 "warning: '$imageIdentifier' uses schemaVersion '$schemaVersion'"
   368  				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)"
   369  				echo >&2
   370  				doNotGenerateManifestJson=1
   371  			fi
   372  
   373  			layersFs="$(echo "$manifestJson" | jq --raw-output '.fsLayers | .[] | .blobSum')"
   374  			IFS="$newlineIFS"
   375  			mapfile -t layers <<< "$layersFs"
   376  			unset IFS
   377  
   378  			history="$(echo "$manifestJson" | jq '.history | [.[] | .v1Compatibility]')"
   379  			imageId="$(echo "$history" | jq --raw-output '.[0]' | jq --raw-output '.id')"
   380  
   381  			echo "Downloading '$imageIdentifier' (${#layers[@]} layers)..."
   382  			for i in "${!layers[@]}"; do
   383  				imageJson="$(echo "$history" | jq --raw-output ".[${i}]")"
   384  				layerId="$(echo "$imageJson" | jq --raw-output '.id')"
   385  				imageLayer="${layers[$i]}"
   386  
   387  				mkdir -p "$dir/$layerId"
   388  				echo '1.0' > "$dir/$layerId/VERSION"
   389  
   390  				echo "$imageJson" > "$dir/$layerId/json"
   391  
   392  				# TODO figure out why "-C -" doesn't work here
   393  				# "curl: (33) HTTP server doesn't seem to support byte ranges. Cannot resume."
   394  				# "HTTP/1.1 416 Requested Range Not Satisfiable"
   395  				if [ -f "$dir/$layerId/layer.tar" ]; then
   396  					# TODO hackpatch for no -C support :'(
   397  					echo "skipping existing ${layerId:0:12}"
   398  					continue
   399  				fi
   400  				token="$(curl -fsSL "$authBase/token?service=$authService&scope=repository:$image:pull" | jq --raw-output '.token')"
   401  				fetch_blob "$token" "$image" "$imageLayer" "$dir/$layerId/layer.tar" --progress-bar
   402  			done
   403  			;;
   404  
   405  		*)
   406  			echo >&2 "error: unknown manifest schemaVersion ($imageIdentifier): '$schemaVersion'"
   407  			exit 1
   408  			;;
   409  	esac
   410  
   411  	echo
   412  
   413  	if [ -s "$dir/tags-$imageFile.tmp" ]; then
   414  		echo -n ', ' >> "$dir/tags-$imageFile.tmp"
   415  	else
   416  		images=("${images[@]}" "$image")
   417  	fi
   418  	echo -n '"'"$tag"'": "'"$imageId"'"' >> "$dir/tags-$imageFile.tmp"
   419  done
   420  
   421  echo -n '{' > "$dir/repositories"
   422  firstImage=1
   423  for image in "${images[@]}"; do
   424  	imageFile="${image//\//_}" # "/" can't be in filenames :)
   425  	image="${image#library\/}"
   426  
   427  	[ "$firstImage" ] || echo -n ',' >> "$dir/repositories"
   428  	firstImage=
   429  	echo -n $'\n\t' >> "$dir/repositories"
   430  	echo -n '"'"$image"'": { '"$(cat "$dir/tags-$imageFile.tmp")"' }' >> "$dir/repositories"
   431  done
   432  echo -n $'\n}\n' >> "$dir/repositories"
   433  
   434  rm -f "$dir"/tags-*.tmp
   435  
   436  if [ -z "$doNotGenerateManifestJson" ] && [ "${#manifestJsonEntries[@]}" -gt 0 ]; then
   437  	echo '[]' | jq --raw-output ".$(for entry in "${manifestJsonEntries[@]}"; do echo " + [ $entry ]"; done)" > "$dir/manifest.json"
   438  else
   439  	rm -f "$dir/manifest.json"
   440  fi
   441  
   442  echo "Download of images into '$dir' complete."
   443  echo "Use something like the following to load the result into a Docker daemon:"
   444  echo "  tar -cC '$dir' . | docker load"