Find the latest tag for Docker images
And use them to update Kubernetes deployments
There's a very easy way to get the latest version of almost any Docker image:
image-repository/image-name:latest
But there are a lot of reasons you might not want to do that. For instance, the Kubernetes documentation states:
You should avoid using the :latest tag when deploying containers in production as it is harder to track which version of the image is running and more difficult to roll back properly.
Instead, specify a meaningful tag such as v1.42.0 and/or a digest.
So if we want to use a meaningful tag or digest but we want an easy way to update an image to the latest version, how do we figure out what that is?
It depends on which repository you're using, but I was able to find an API for the three repositories I use for my stack that makes it possible to automate this: Docker Hub, the GitHub Container Repository, and LinuxServer.
I'll walk through each one and then tie them together into a script at the end that looks through a set of Kubernetes or Docker deployment files and updates all the images found to the latest tag or digest.
Docker Hub
You can list the tags for any image in the Docker Hub using the endpoint:
https://hub.docker.com/v2/repositories/{image-name}/tags
By default this will list the 10 most recent tags, which can be increased using the page_size query parameter.
The API lists the tag name and digest for each tag, alongside a lot of info that we don't care about right now.
Sometimes the digest on the latest version is an exact match to the digest on a non-latest tag, which makes it easy to pull out the correct tag name. When this isn't the case, you can parse the tag name as a semver and fall back to the digest of the latest tag if that fails.
All together this can be done with (given a image_name variable):
# Find latest tag by matching digest of latest tag
tag_url="https://hub.docker.com/v2/repositories/${image_name}/tags?page_size=50"
tag_data=$(curl -s "${tag_url}" -H 'Content-Type: application/json')
latest_digest=$(echo "$tag_data" | jq -r '.results[] | select(.name == "latest") | .digest' 2>/dev/null)
latest_version=$(echo "$tag_data" | jq -r 'first(.results[] | select(.digest == "'$latest_digest'") | select(.name != "latest") | .name)' 2>/dev/null)
if [ -z "$latest_version" ]; then
# Easy match to latest failed, parse versions..
log_verbose "[$template_path] Unable to match 'latest' digest to symver, pulling in first symver tag.."
all_tags=$(echo "$tag_data" | jq -r '.results[] | select(.name | test("^v?[0-9]+\\.[0-9]+(\\.[0-9]+)?")) | .name' 2>/dev/null)
latest_version=$(echo "$all_tags" | sort -V | tail -1)
fi
if [ -z "$latest_version" ]; then
# All version matching failed, use digest from latest..
log_verbose "[$template_path] Unable to find latest semver tag, using digest.."
uses_digest=true
fi
GitHub Container Repository
The GitHub Container Repository API is a little more complicated because it requires authentication and doesn't provide a digest, so the nicest way to process images from it is to hope the tags can be parsed nicely as semver.
For all public images, you can get a token for accessing the API for that image from the endpoint:
https://ghcr.io/token\?scope\="repository:{image-name}:pull
You can then get the list of tags from the endpoint:
https://ghcr.io/v2/{short-name}/tags/list
Like with the Docker Hub, this endpoint lists the number of results by default which in this case can be increased using the n query parameter.
Given an image_name variable, you can get the latest version tag using:
ghcr_token=$(curl https://ghcr.io/token\?scope\="repository:${image_name}:pull" 2>/dev/null | jq -r '.token')
all_tags=$(curl -H "Authorization: Bearer $ghcr_token" "https://ghcr.io/v2/${image_name}/tags/list?n=5000" 2>/dev/null | jq -r '.tags[] | select(. | test("^v?[0-9]+\\.[0-9]+(\\.[0-9]+)?"))')
latest_version=$(echo "$all_tags" | sort -V | tail -1)
LinuxServer
The API for LinuxServer provides the simplest way to get the latest tag for an image. All images hosted by LinuxServer along with the latest tag are returned by the endpoint:
https://api.linuxserver.io/api/v1/images
Again, given an image_name variable, you can get the latest version of an image using:
linuxserver_url="https://api.linuxserver.io/api/v1/images"
linuxserver_data=$(curl -s "${linuxserver_url}" -H 'Content-Type: application/json')
latest_version=$(echo "$linuxserver_data" | jq -r '.data.repositories.linuxserver[] | select(.name == "'$image_name'") | .version')
# Trim extra data, only keep symver (fails for some images, works for others)
# latest_version=$(echo "$latest_version" | grep -Eo '^v?[0-9]+\.[0-9]+(\.[0-9]{1,3}\b)?')
Tying it all together to update Kubernetes images
As promised, here's a script that will parse Kubernetes and Docker deployment files, identifying images and updating them to the latest version found in the repositories we walked through above (curl and jq are dependencies for this to work):
#!/bin/sh
set -e
is_verbose=0
OPTIND=1
while getopts "v" opt; do
case "$opt" in
v) is_verbose=1
;;
esac
done
shift $((OPTIND-1))
function log_verbose()
{
if [ $is_verbose = 1 ]; then
echo "${@:-$(</dev/stdin)}"
fi
}
function log_info()
{
echo "${@:-$(</dev/stdin)}"
}
function log_error()
{
echo "${@:-$(</dev/stdin)}" >&2
}
default_template_paths=( $(find . -type f -name '*.yaml') ) # Breaks on whitespace in filenames, maybe I'll fix this later?
found_images="$(grep -Eo 'image:\s*[a-zA-Z0-9\:\@\/\.\-]+' "${@:-${default_template_paths[@]}}")"
while read search_result; do
# Parse the results from grep
template_path="${search_result%%:*}"
found_image="${search_result#*image: *}"
# Skip files with no image specified
if [ -z "${found_image}" ]; then
continue
fi
# Split into parts
image_name=$(echo "$found_image" | cut -d "@" -f 1 | cut -d ":" -f 1)
cur_version=$(echo "$found_image" | cut -d "@" -sf 2)
if [ -z "$cur_version" ]; then
cur_version=$(echo "$found_image" | cut -d ":" -sf 2)
fi
cur_version=${cur_version:-latest}
log_verbose "[$template_path] Found image $image_name"
log_verbose "[$template_path] Current version is: $cur_version"
# Pre-fetch data for linuxserver
linuxserver_url="https://api.linuxserver.io/api/v1/images"
linuxserver_data=$(curl -s "${linuxserver_url}" -H 'Content-Type: application/json')
uses_digest=false
if [ $(echo "$image_name" | cut -d "/" -f 1) = 'lscr.io' ]; then
# Find latest version using linuxserver api
short_name=$(echo "$image_name" | cut -d "/" -f 3)
latest_version=$(echo "$linuxserver_data" | jq -r '.data.repositories.linuxserver[] | select(.name == "'$short_name'") | .version')
# Trim extra data, only keep symver (fails for some images)
# latest_version=$(echo "$latest_version" | grep -Eo '^v?[0-9]+\.[0-9]+(\.[0-9]{1,3}\b)?')
elif [ $(echo "$image_name" | cut -d "/" -f 1) = 'ghcr.io' ]; then
# Find latest version from ghcr
short_name=$(echo "$image_name" | grep -Eo '[^/]+/[^/]+$')
ghcr_token=$(curl https://ghcr.io/token\?scope\="repository:$short_name:pull" 2>/dev/null | jq -r '.token')
all_tags=$(curl -H "Authorization: Bearer $ghcr_token" "https://ghcr.io/v2/$short_name/tags/list?n=5000" 2>/dev/null | jq -r '.tags[] | select(. | test("^v?[0-9]+\\.[0-9]+(\\.[0-9]+)?"))')
latest_version=$(echo "$all_tags" | sort -V | tail -1)
else
# Find latest version from docker hub
tag_url="https://hub.docker.com/v2/repositories/${image_name}/tags?page_size=50"
tag_data=$(curl -s "${tag_url}" -H 'Content-Type: application/json')
latest_digest=$(echo "$tag_data" | jq -r '.results[] | select(.name == "latest") | .digest' 2>/dev/null)
latest_version=$(echo "$tag_data" | jq -r 'first(.results[] | select(.digest == "'$latest_digest'") | select(.name != "latest") | .name)' 2>/dev/null)
if [ -z "$latest_version" ]; then
# Easy match to latest failed, parse versions..
log_verbose "[$template_path] Unable to match 'latest' digest to symver, pulling in first symver tag.."
all_tags=$(echo "$tag_data" | jq -r '.results[] | select(.name | test("^v?[0-9]+\\.[0-9]+(\\.[0-9]+)?")) | .name' 2>/dev/null)
latest_version=$(echo "$all_tags" | sort -V | tail -1)
fi
if [ -z "$latest_version" ]; then
# All version matching failed, use digest from latest..
log_verbose "[$template_path] Unable to find latest semver tag, using digest.."
uses_digest=true
fi
fi
if [ "$uses_digest" = true ]; then
log_verbose "[$template_path] Latest version is: ${latest_digest}"
new_image="$image_name@$latest_digest"
else
log_verbose "[$template_path] Latest version is: ${latest_version}"
new_image="$image_name:$latest_version"
fi
# Check whether we already use the latest version
if [ "$found_image" = "$new_image" ]; then
log_info "[$template_path] Current version of $image_name matches latest tag found, continuing.."
echo
continue
fi
# Validate by inspecting manifest
( docker manifest inspect "$new_image" | log_verbose ) || (
log_error "[$template_path] Failed to find image $new_image"
echo
continue
)
# Perform update
log_info "[$template_path] Updating $image_name:"
log_info "[$template_path] Old image: $found_image"
log_info "[$template_path] New image: $new_image"
echo
sed -i "s*$found_image*$new_image*g" "$template_path"
git add "$template_path"
done <<< "$found_images"
You can either run this script from a directory containing deployment files and it will update images in any yaml files in the current directory (including subdirectories), or you can pass in yaml files as arguments for it to update.
You can also pass in the -v argument to get more verbose logging.


