Skip to content

Instantly share code, notes, and snippets.

@achetronic
Created July 2, 2022 19:19
Show Gist options
  • Select an option

  • Save achetronic/2db363e6c2fbecd42ae67512fbea50ca to your computer and use it in GitHub Desktop.

Select an option

Save achetronic/2db363e6c2fbecd42ae67512fbea50ca to your computer and use it in GitHub Desktop.
Find the tag of a Docker image having only the SHA256
#!/bin/bash
SHA256_HASH="5bb4faffc8b35e2702b2ffa78e982b979d7b66db29bd55b0c58de8fa745df661"
for i in {1..1000}
do
echo "Looking into page: $i"
curl "https://registry.hub.docker.com/v2/repositories/apache/superset/tags/?page=$i" \
| jq '.results[] | select(.["images"][]["digest"] == "sha256:'${SHA256_HASH}'")'
done
@ThomDietrich
Copy link

To find your SHA256_HASH:

 docker images --digests

@achetronic
Copy link
Author

@ThomDietrich @aryehb Thank you for the contribution mates!

This is used more as a life-saver trick when you only have the sha256 and you need to find the tag pointing to that digest in the repo to fix it in your deployment manifests

@Kyu
Copy link

Kyu commented Jan 11, 2025

Putting it together:

SHA256_HASH='5bb4faffc8b35e2702b2ffa78e982b979d7b66db29bd55b0c58de8fa745df661'
NAMESPACE='apache'
REPO_NAME='superset'

for i in {1..1000}; do 
    if [ $i -eq 100 ]; then
        echo -e "\e[35mSleeping for 7 seconds on page $i...\e[0m"
        sleep 7
    fi
    echo "Looking into page: $i"
    result=$(curl -s "https://registry.hub.docker.com/v2/repositories/$NAMESPACE/$REPO_NAME/tags/?page=$i" | jq -r ".results[] | select(.[\"images\"][][\"digest\"] == \"sha256:$hash\" or .digest == \"sha256:$SHA256_HASH\")") || break
    if [ ! -z "$result" ]; then
        echo "$result" | jq '.'
        break
    fi
done

@net
Copy link

net commented Mar 13, 2025

For official images like https://hub.docker.com/_/node the namespace is "library".

Also, I think @Kyu meant for $hash to be $SHA256_HASH. Fixed:

#!/bin/bash

SHA256_HASH="5bb4faffc8b35e2702b2ffa78e982b979d7b66db29bd55b0c58de8fa745df661"
NAMESPACE='apache'
REPO_NAME='superset'

for i in {1..1000}; do 
    if [ $i -eq 100 ]; then
        echo -e "\e[35mSleeping for 7 seconds on page $i...\e[0m"
        sleep 7
    fi

    echo "Looking into page: $i"

    result=$(
        curl -s "https://registry.hub.docker.com/v2/repositories/$NAMESPACE/$REPO_NAME/tags/?page=$i" \
        | jq -r ".results[] | select(.[\"images\"][][\"digest\"] == \"sha256:$SHA256_HASH\" or .digest == \"sha256:$SHA256_HASH\")"
    ) || break

    if [ ! -z "$result" ]; then
        echo "$result" | jq '.'
        break
    fi
done

@ilkkapoutanen-61n
Copy link

ilkkapoutanen-61n commented Jul 31, 2025

For the benefit of anyone else who happens to hit this, if what you have is the hash of a particular tag of a multi-arch image, this approach does not work.

Ha, the later iterations do work! It's the or .digest == [...] part which does the trick I think.

@ilkkapoutanen-61n
Copy link

Anyway, for posterity, here's my iteration of the script, with simplistic command line argument handling, sleeping every 100 pages and adding the sha256: prefix if not given.

#!/bin/bash
set -eou pipefail

SHA256_HASH=""
IMAGE=""

while [[ $# -gt 0 ]]; do
  case "$1" in
  --hash)
    shift
    SHA256_HASH="$1"
    shift
    ;;
  --image)
    shift
    IMAGE="$1"
    shift
    ;;
  *)
    echo "Unknown argument \"$1\"" >&2
    echo "Usage: $0 --hash HASH --image REPO/IMAGE" >&2
    exit 1
    ;;
  esac
done

if [[ -z "$SHA256_HASH" || -z "$IMAGE" ]]; then
  echo "Usage: $0 --hash HASH --image REPO/IMAGE" >&2
  exit 1
fi

if [[ ! "${SHA256_HASH:0:7}" == "sha256:" ]]; then
  SHA256_HASH="sha256:${SHA256_HASH}"
fi

for i in {1..1000}; do
  if [[ $(expr $i % 100) -eq 0 ]]; then
    echo "Sleeping for 7 seconds on page $i..."
    sleep 7
  fi

  echo "Looking into page: $i"

  result=$(
    curl "https://registry.hub.docker.com/v2/repositories/$IMAGE/tags/?page=$i" |
      jq '.results[] | select(.["images"][]["digest"] == "'${SHA256_HASH}'" or .digest == "'${SHA256_HASH}'")'
  ) || break

  if [[ ! -z "$result" ]]; then
    echo "$result" | jq .
    break
  fi
done

@jeff-hykin
Copy link

jeff-hykin commented Oct 20, 2025

Continuing the trend, this scripts automates both getting the hash and getting tags with the hash (no jq needed).

usage:

# if you have the digest:
docker_tag_pinner 'FROM electronuserland/builder:wine@sha256:8bb6fa0f99a00a5b845521910508958ebbb682b59221f0aa4b82102c22174164'
# if you don't:
docker_tag_pinner 'FROM electronuserland/builder:wine' 
Screen Shot 2025-10-20 at 10 17 32 AM

Get deno
curl -fsSL https://deno.land/install.sh | sh

Script

#!/usr/bin/env -S deno run --allow-all
import $ from "https://esm.sh/@jsr/[email protected]/mod.ts"

//
// check arg
//
let fromStatment = Deno.args[0].trim()
// ex: fromStatment="docker.io/electronuserland/builder:wine@sha256:8bb6fa0f99a00a5b845521910508958ebbb682b59221f0aa4b82102c22174164"


//
// parse the FROM statement
//
// remove prefixy stuff
fromStatment = fromStatment.replace(/^FROM\s+/, "")
fromStatment = fromStatment.replace(/^docker\.io\//, "")
var [namespace, repo, tag] = fromStatment.split(/\/|:/g)
if (!tag) {
    tag = "latest"
}
if (!namespace || !repo) {
    console.error("Invalid FROM statement:", fromStatment)
    console.error("I'd usually expect an argument like:\n   FROM docker.io/electronuserland/builder:wine")
    console.error("OR:\n    FROM docker.io/electronuserland/builder:wine@sha256:8bb6fa0f99a00a5b845521910508958ebbb682b59221f0aa4b82102c22174164")
    Deno.exit(1)
}

if (tag.includes("@")) {
    var [tag, hashVersion] = tag.split("@")
}

//
// if digest missing get with `docker images --digests` 
//
try_again: while (true) {
    if (!hashVersion) {
        console.log(`trying to get the hash by running: docker images --digests --format '{{json .}}'`)
        const imagesText = await $`docker images --digests --format '{{json .}}'`.text()
        const rows = imagesText.split("\n").map(JSON.parse)
        const relevantRows = rows.filter((each) => each.Repository.includes(`${namespace}/${repo}`) && each.Tag === tag)
        if (relevantRows.length === 0) {
            // need to pull it
            if (confirm("No digest found for this image. Can I pull it to get the digest?")) {
                await $`docker pull ${namespace}/${repo}`
                continue try_again
            } else {
                console.error("No digest found for this image. Please provide the digest manually. (add @DIGEST_HASH to the end of the from statement)")
                Deno.exit(1)
            }
        }
        hashVersion = relevantRows[0].Digest
        console.log(`digest is: ${hashVersion}`)
    }
    break
}

hashVersion = hashVersion.replace(/^sha256:/, "")

//
// fetch the tag pages
//
let i = 0
while (true) {
    i++
    if (i % 100 === 0) {
        console.log(`\x1b[35mSleeping for 7 seconds on page ${i}...\x1b[0m`)
        await new Promise((resolve) => setTimeout(resolve, 7000))
    }
    
    console.log(`\x1b[35mLooking into page: ${i}\x1b[0m`)
    const url = `https://registry.hub.docker.com/v2/repositories/${namespace}/${repo}/tags/?page=${i}`

    const res = await fetch(url)
    if (!res.ok) {
        console.error(`Error fetching page ${i}: ${res.statusText}`)
        break
    }

    const data = await res.json()
    const results = data.results ?? []
    const matchesWithCorrectHash = results.filter((each) => each.digest === `sha256:${hashVersion}` || (each.images || []).some((each) => each.digest === `sha256:${hashVersion}`))
    const usefulMatches = matchesWithCorrectHash.filter(each=>each.name!=tag)
    if (usefulMatches.length > 0) {
        console.log(`Found ${usefulMatches.length} matches:`)
        for (const each of usefulMatches) {
            console.log(`    \x1b[34mFROM docker.io/${namespace}/${repo}:${each.name}\x1b[0m   # last updated: ${monthsAgo(each.last_updated)} months ago`)
        }
        if (confirm("\nThere could be more matches on other pages. Should I STOP here?")) {
            Deno.exit(0)
        }
    }
}

// helper function
function monthsAgo(dateString) {
    const pastDate = new Date(dateString)
    const today = new Date()

    let yearsDiff = today.getFullYear() - pastDate.getFullYear()
    let monthsDiff = today.getMonth() - pastDate.getMonth()

    let totalMonths = yearsDiff * 12 + monthsDiff

    // If the day of the month hasn't been reached yet, subtract one month
    if (today.getDate() < pastDate.getDate()) {
        totalMonths -= 1
    }

    return totalMonths
}

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment