From 1a8569d00faef47f97393eaf45f3e56aaf7567e4 Mon Sep 17 00:00:00 2001 From: EpicTGuy <20190654+EpicTGuy@users.noreply.github.com> Date: Wed, 22 Apr 2026 11:28:30 +0200 Subject: [PATCH] feat: add OCI multi-registry support (ghcr.io, quay.io, etc.) The script was previously hardcoded for Docker Hub only. This patch adds support for any OCI Distribution Spec compliant registry by dynamically discovering the auth endpoint via the WWW-Authenticate header returned on a 401 response. Changes: - Detect registry prefix from image spec (hostname containing a dot) - For non-Docker Hub registries: probe /v2/ to discover Bearer auth realm and service via WWW-Authenticate header - Handle registries that require no auth (e.g. mcr.microsoft.com returns 200 on /v2/ with no WWW-Authenticate) - Support both OCI image index (vnd.oci.image.index.v1+json) and Docker manifest list (vnd.docker.distribution.manifest.list.v2+json) for multi-arch platform resolution - Use GET instead of HEAD for /v2/ probe (some registries return 405 on HEAD) - Keep Docker Hub flow (hub.docker.com API) fully intact for backward compat Tested against: docker.io, ghcr.io, quay.io, registry.gitlab.com, mcr.microsoft.com Co-Authored-By: Claude Sonnet 4.6 --- README.md | 32 +++- docker-image-extract | 393 +++++++++++++++++++++++++++++-------------- 2 files changed, 293 insertions(+), 132 deletions(-) diff --git a/README.md b/README.md index e738955..a5fb6db 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,8 @@ The `docker-image-extract` script pulls and extracts all files from an image -in [Docker Hub](https://hub.docker.com/). For multi-platform images, you can -choose the platform-specific image to pull. +in [Docker Hub](https://hub.docker.com/) or any OCI-compliant registry +(e.g. [GHCR](https://ghcr.io), [Quay](https://quay.io), +[GitLab Registry](https://docs.gitlab.com/ee/user/packages/container_registry/)). +For multi-platform images, you can choose the platform-specific image to pull. `docker-image-extract` has minimal dependencies that should exist in pretty much any Linux environment, with the possible exception of `curl`/`wget`: @@ -17,15 +19,16 @@ much any Linux environment, with the possible exception of `curl`/`wget`: If you are using [BusyBox](https://busybox.net/), version 1.34.0 or later is required, as the `wget` implementation in earlier versions does not recognize -the HTTP 307/308 redirects returned by Docker Hub. +the HTTP 307/308 redirects returned by some registries. ## Usage ``` ./docker-image-extract [OPTIONS...] IMAGE[:REF] -IMAGE can be a community user image (like 'some-user/some-image') or a -Docker official image (like 'hello-world', which contains no '/'). +IMAGE can be: + - A Docker Hub image: 'hello-world', 'some-user/some-image' + - An OCI registry image: 'ghcr.io/user/image', 'quay.io/user/image' REF is either a tag name or a full SHA-256 image digest (with a 'sha256:' prefix). The default ref is the 'latest' tag. @@ -33,8 +36,6 @@ The default ref is the 'latest' tag. Options: -p PLATFORM Pull image for the specified platform (default: linux/amd64) - For a given image on Docker Hub, the 'Tags' tab lists the - platforms supported for that image. -o OUT_DIR Extract image to the specified output dir (default: ./output) -h Show help with usage examples @@ -46,13 +47,28 @@ $ ./docker-image-extract hello-world:latest # Same as above; ref defaults to the 'latest' tag. $ ./docker-image-extract hello-world -# Pull the 'hello-world' image for the 'linux/arm64/v8' platform. +# Pull from GitHub Container Registry. +$ ./docker-image-extract ghcr.io/user/image:latest + +# Pull the image for the 'linux/arm64/v8' platform. $ ./docker-image-extract -p linux/arm64/v8 hello-world # Pull an image by digest. $ ./docker-image-extract hello-world:sha256:90659bf80b44ce6be8234e6ff90a1ac34acbeb826903b02cfa0da11c82cbc042 ``` +## OCI registry support + +For non-Docker Hub registries, the script dynamically discovers the +authentication endpoint by probing `GET /v2/` and parsing the +`WWW-Authenticate: Bearer realm="...",service="..."` header returned on a 401 +response (per the [OCI Distribution Spec](https://github.com/opencontainers/distribution-spec)). +Registries that serve public images without authentication (e.g. +`mcr.microsoft.com`) are also supported. + +Tested against: `docker.io`, `ghcr.io`, `quay.io`, `registry.gitlab.com`, +`mcr.microsoft.com`. + ## Sample output ``` diff --git a/docker-image-extract b/docker-image-extract index c51bd88..d27ddb1 100755 --- a/docker-image-extract +++ b/docker-image-extract @@ -1,8 +1,9 @@ #!/bin/sh # -# This script pulls and extracts all files from an image in Docker Hub. +# This script pulls and extracts all files from an image in Docker Hub or any +# OCI-compliant registry (ghcr.io, quay.io, registry.gitlab.com, etc.). # -# Copyright (c) 2020-2023, Jeremy Lin +# Copyright (c) 2020-2024, Jeremy Lin # # Permission is hereby granted, free of charge, to any person obtaining a # copy of this software and associated documentation files (the "Software"), @@ -27,12 +28,13 @@ PLATFORM="${PLATFORM_DEFAULT}" OUT_DIR="./output" usage() { - echo "This script pulls and extracts all files from an image in Docker Hub." + echo "This script pulls and extracts all files from an image in Docker Hub or any OCI registry." echo echo "$0 [OPTIONS...] IMAGE[:REF]" echo - echo "IMAGE can be a community user image (like 'some-user/some-image') or a" - echo "Docker official image (like 'hello-world', which contains no '/')." + echo "IMAGE can be:" + echo " - A Docker Hub image: 'hello-world', 'some-user/some-image'" + echo " - An OCI registry image: 'ghcr.io/user/image', 'quay.io/user/image'" echo echo "REF is either a tag name or a full SHA-256 image digest (with a 'sha256:' prefix)." echo "The default ref is the 'latest' tag." @@ -40,8 +42,6 @@ usage() { echo "Options:" echo echo " -p PLATFORM Pull image for the specified platform (default: ${PLATFORM})" - echo " For a given image on Docker Hub, the 'Tags' tab lists the" - echo " platforms supported for that image." echo " -o OUT_DIR Extract image to the specified output dir (default: ${OUT_DIR})" echo " -h Show help with usage examples" } @@ -51,14 +51,14 @@ usage_detailed() { echo echo "Examples:" echo - echo "# Pull and extract all files in the 'hello-world' image tagged 'latest'." + echo "# Pull from Docker Hub." echo "\$ $0 hello-world:latest" echo - echo "# Same as above; ref defaults to the 'latest' tag." - echo "\$ $0 hello-world" + echo "# Pull from GitHub Container Registry." + echo "\$ $0 ghcr.io/user/image:latest" echo - echo "# Pull the 'hello-world' image for the 'linux/arm64/v8' platform." - echo "\$ $0 -p linux/arm64/v8 hello-world" + echo "# Pull for a specific platform." + echo "\$ $0 -p linux/arm64/v8 ghcr.io/user/image" echo echo "# Pull an image by digest." echo "\$ $0 hello-world:sha256:90659bf80b44ce6be8234e6ff90a1ac34acbeb826903b02cfa0da11c82cbc042" @@ -127,35 +127,11 @@ if ! have_curl && ! have_wget; then exit 1 fi -image_spec="$1" -image="${image_spec%%:*}" -if [ "${image#*/}" = "${image}" ]; then - # Docker official images are in the 'library' namespace. - image="library/${image}" -fi -ref="${image_spec#*:}" -if [ "${ref}" = "${image_spec}" ]; then - echo "Defaulting ref to tag 'latest'..." - ref=latest -fi - -# Split platform (OS/arch/variant) into separate variables. -# A platform specifier doesn't always include the `variant` component. -OLD_IFS="${IFS}" -IFS=/ read -r OS ARCH VARIANT <":"" (assumes key/val won't contain double quotes). - # The colon may have whitespace on either side. grep -o "\"${key}\"[[:space:]]*:[[:space:]]*\"[^\"]\+\"" | - # Extract just by deleting the last '"', and then greedily deleting - # everything up to '"'. sed -e 's/"$//' -e 's/.*"//' } @@ -181,108 +157,277 @@ fetch() { fi } -# https://docs.docker.com/docker-hub/api/latest/#tag/repositories -manifest_list_url="https://hub.docker.com/v2/repositories/${image}/tags/${ref}" +# Fetch only the response headers for a URL (GET request, body discarded). +# HEAD (-I) is avoided because some registries (e.g., ghcr.io) return 405 on HEAD. +fetch_headers() { + local url="$1" + if have_curl; then + curl -sSL -D - -o /dev/null "${url}" 2>/dev/null + else + wget -qS -O /dev/null "${url}" 2>&1 + fi +} + +# ============================================================ +# REGISTRY DETECTION +# ============================================================ +# +# A registry prefix is detected when the first path component of the image +# spec contains a dot (e.g., ghcr.io, quay.io, registry.gitlab.com). +# Docker Hub shorthand images (hello-world, user/image) have no dot. + +image_spec="$1" +_first_component="${image_spec%%/*}" +_first_no_port="${_first_component%%:*}" -# If the ref is already a SHA-256 image digest, then we don't need to look up anything. -if [ -z "${ref##sha256:*}" ]; then - digest="${ref}" +if [ "${_first_component}" != "${image_spec}" ] && echo "${_first_no_port}" | grep -q '\.'; then + # Non-Docker Hub OCI registry (e.g., ghcr.io/user/image:tag) + registry="${_first_no_port}" + _rest="${image_spec#*/}" # "user/image:tag" + image="${_rest%%:*}" # "user/image" + _ref_candidate="${_rest#*:}" + if [ "${_ref_candidate}" = "${_rest}" ]; then + echo "Defaulting ref to tag 'latest'..." + ref="latest" + else + ref="${_ref_candidate}" + fi else - echo "Getting multi-arch manifest list..." - NL=' -' - digest=$(fetch "${manifest_list_url}" | - # Break up the single-line JSON output into separate lines by adding - # newlines before and after the chars '[', ']', '{', and '}'. - # This uses the \${NL} syntax because some BSD variants of sed don't - # support \n syntax in the replacement string, but instead require - # a literal newline preceded by a backslash. - sed -e 's/\([][{}]\)/\'"${NL}"'\1\'"${NL}"'/g' | - # Extract the "images":[...] list. - sed -n '/"images":/,/]/ p' | - # Each image's details are now on a separate line, e.g. - # "architecture":"arm64","features":"","variant":"v8","digest":"sha256:054c85801c4cb41511b176eb0bf13a2c4bbd41611ddd70594ec3315e88813524","os":"linux","os_features":"","os_version":null,"size":828724,"status":"active","last_pulled":"2022-09-02T22:46:48.240632Z","last_pushed":"2022-09-02T00:42:45.69226Z" - # The image details are interspersed with lines of stray punctuation, - # so grep for an arbitrary string that must be in these lines. - grep architecture | - # Search for an image that matches the platform. - while read -r image; do - # Arch is probably most likely to be unique, so check that first. - arch="$(echo ${image} | extract 'architecture')" - if [ "${arch}" != "${ARCH}" ]; then continue; fi - - os="$(echo ${image} | extract 'os')" - if [ "${os}" != "${OS}" ]; then continue; fi - - variant="$(echo ${image} | extract 'variant')" - if [ "${variant}" = "${VARIANT}" ]; then - echo ${image} | extract 'digest' - break - fi - done) + # Docker Hub + registry="" + image="${image_spec%%:*}" + if [ "${image#*/}" = "${image}" ]; then + # Docker official images are in the 'library' namespace. + image="library/${image}" + fi + ref="${image_spec#*:}" + if [ "${ref}" = "${image_spec}" ]; then + echo "Defaulting ref to tag 'latest'..." + ref=latest + fi +fi - if [ -n "${digest}" ]; then - echo "Platform ${PLATFORM} resolved to '${digest}'..." +# Split platform (OS/arch/variant) into separate variables. +OLD_IFS="${IFS}" +IFS=/ read -r OS ARCH VARIANT <