summaryrefslogtreecommitdiff
path: root/upgrade_docker
diff options
context:
space:
mode:
authorJesper Jensen <jesper@jnsn.dev>2025-12-28 11:48:19 +0100
committerJesper Jensen <jesper@jnsn.dev>2025-12-28 11:48:19 +0100
commit2f26047f850093374513b78ac2d7579b6f087bd9 (patch)
tree720e13017314bfd23ebd97f071dd6da63fe20bf3 /upgrade_docker
parent8114c134c11a7b84f1dc29a4df7de6ecfe764958 (diff)
Add upgrade_docker script
Diffstat (limited to 'upgrade_docker')
-rwxr-xr-xupgrade_docker280
1 files changed, 280 insertions, 0 deletions
diff --git a/upgrade_docker b/upgrade_docker
new file mode 100755
index 0000000..4e5796c
--- /dev/null
+++ b/upgrade_docker
@@ -0,0 +1,280 @@
+#!/bin/bash
+# vim: et
+set -Eeuo pipefail
+
+# @PERF: Currently this script only supports processing a single file at
+# a time. Ideally it should probably support multiple files. That would also
+# allow it to cache some of the results of fetching new versions instead of
+# having to fetch them every time. As it stands right now we could only cache
+# that for one file, and the chance that you're using the same image multiple
+# times in one docker file seems vanishingly small
+
+function help {
+ cat <<EOH
+Usage:
+ $0 [options] [--] <FILE>
+Upgrade the image versions in FILE to latest version
+
+Options:
+ -v <level> Increase verbosity (repeatable)
+ -d Dry run (don't apply the updates)
+ -t, --type <TYPE> Type of file (defaults to DOCKER)
+ --insecure Disable SSL verification
+ -h, --help You are looking at it
+EOH
+}
+
+function vecho {
+ LEVEL="$1"
+ shift
+ if [[ $VERBOSE -ge $LEVEL ]]; then
+ echo -e "$@" >&2
+ fi
+}
+
+VERBOSE=0
+DRY_RUN=false
+POSITIONAL=()
+CURL_OPTS=""
+TYPE="DOCKER"
+
+# Read arguments
+while [[ "$#" -gt 0 ]]; do
+ case "$1" in
+ '-h'|'--help')
+ help
+ exit 0
+ ;;
+ '-v')
+ VERBOSE=$((VERBOSE + 1))
+ shift
+ if [[ "$#" -gt 0 ]] && [[ "$1" =~ ^[0-9]+$ ]]; then
+ VERBOSE="$1"
+ shift
+ fi
+ ;;
+ '-d'|'--dry')
+ shift
+ TYPE=$1
+ shift
+ ;;
+ '-t'|'--type')
+ DRY_RUN=true
+ shift
+ ;;
+ '--insecure')
+ CURL_OPTS="$CURL_OPTS --insecure"
+ shift
+ ;;
+ *)
+ POSITIONAL+=("$1")
+ shift
+ ;;
+ esac
+done
+
+if [[ ${#POSITIONAL[@]} -lt 1 ]]; then
+ echo "Too few arguments" >&2
+ help >&2
+ exit 1
+fi
+
+if [[ ${#POSITIONAL[@]} -gt 1 ]]; then
+ echo "Too many arguments" >&2
+ help >&2
+ exit 1
+fi
+
+case "${TYPE^^}" in
+ 'DOCKER')
+ TYPE="DOCKER"
+ ;;
+ 'KUBE')
+ TYPE="KUBE"
+ ;;
+ *)
+ echo "Unknown TYPE"
+ exit 1
+ ;;
+esac
+
+FILE="${POSITIONAL[0]}"
+
+function rel_next {
+ while IFS= read -r line; do
+ vecho 2 "Processing link header \"$line\""
+ regex="^<([^>]*)>(; ([^=]*)=([^;]*))*$"
+ if ! [[ $line =~ $regex ]]; then
+ printf "Could not parse link header %s\n", "$line" >&2
+ exit 1
+ fi
+
+ link_param_len=$(((${#BASH_REMATCH[@]} - 2) / 3))
+ vecho 3 "Link contains $link_param_len params"
+
+ for((i=0; i<link_param_len; i++)); do
+ rematch_index=$((i * 3 + 2))
+ name="${BASH_REMATCH[$rematch_index+1]}"
+ value="${BASH_REMATCH[$rematch_index+2]}"
+
+ vecho 4 "Param $i is $name=$value"
+
+ if [[ "$name" == "rel" ]] && [[ "$value" == "\"next\"" ]]; then
+ vecho 4 "Param $i matches"
+ printf "%s\n" "${BASH_REMATCH[1]}"
+ fi
+ done
+ done < <(jq -r -s '.[1].link // [] | .[]')
+}
+
+function version_index {
+ regex="^.+-([^-]+)$"
+ if ! [[ $1 =~ $regex ]]; then
+ printf "Tag did not match version scheme %s\n" "$1"
+ fi
+
+ echo "${BASH_REMATCH[1]}"
+}
+
+function is_newer_than {
+ # This comparison function only works for our very weird versioning scheme
+
+ build_type=$(rev <<<"$2" | cut -s -d - -f 2 | rev)
+ if [[ "$build_type" != "RB" ]]; then
+ vecho 2 "$2 is not a release build"
+ return 1
+ fi
+
+ # If we haven't selected a version, select this one
+ if [[ -z "$1" ]]; then
+ return 0
+ fi
+
+ # @SPEED: This is a really slow way of doing this check. Since all the tags
+ # should have the same prefix there's a potential speedup where we read
+ # that length out and just skip that many bytes instead of doing this whole
+ # regex thing
+ i1=$(version_index "$1")
+ i2=$(version_index "$2")
+ vecho 2 "Compare ${i1}[$1] < ${i2}[$2]"
+ (( $i1 < $i2 ))
+}
+
+function find_latest_version_of {
+ HOST="$1"
+ IMAGE="$2"
+ vecho 2 "Resolving latest version of $IMAGE"
+ version=""
+
+ next_link="https://$HOST/v2/$IMAGE/tags/list?n=100"
+ while true; do
+ # CURL -w (write-out) writes the specified thing AFTER printing the
+ # response body. The headers will be printed AFTER the body
+ http_response=$(curl $CURL_OPTS -n -f --silent "$next_link" -w "%{header_json}")
+ if [[ $? -ne 0 ]]; then
+ printf "Fetching version of %s failed: %s\n" "$IMAGE" "$http_response" >&2
+ exit 1
+ fi
+
+ while IFS= read -r line; do
+ if is_newer_than "$version" "$line"; then
+ version="$line"
+ fi
+ done < <(jq -r -s '.[0].tags | .[]' <<<"$http_response")
+
+ vecho 2 "Page done, looking for next page"
+ next_link=$(rel_next <<<"$http_response")
+ if [[ "$next_link" == "" ]]; then
+ break;
+ else
+ vecho 2 "Next page located at $next_link"
+ fi
+ done
+
+ vecho 1 "Latest version of $IMAGE is $version"
+ echo "$version"
+}
+
+function images_in_file {
+ # -r is specifically omitted here to allow backslash to escape newlines
+ while IFS=" " read ln cmd rest; do
+ if [[ "$cmd" != "FROM" ]]; then
+ continue
+ fi
+ vecho 3 "The args to FROM is $rest"
+ image=$(cut -d " " -f 1 <<<"$rest")
+ vecho 1 "Image $image extracted"
+ # the docker FROM command takes an optional argument that starts with
+ # --, we don't support that so try and detect it to bail
+ if [[ "$image" == "-*" ]]; then
+ printf "Options to FROM are not supported\n" >&2
+ exit 1
+ fi
+ echo "$ln:$image"
+ # We wan't to track the line number to hopefully constrain the coming
+ # update to the correct part of the dockerfile. Ideally we'd track the
+ # ftell to be completely accurate, but that's difficult (if not impossible)
+ # in bash
+ # The sed removes comment, it skips the first 5 chars to skip the line
+ # number (4 numbers for the line and one space as a separator)
+ done < <(nl -w4 -s" " "$1" | sed '/^.{5}\s*#/d' -)
+}
+
+function images_in_kube {
+ # -r is specifically omitted here to allow backslash to escape newlines
+ while IFS=" " read ln content; do
+ regex="^(-\s)?image:\s+([a-zA-Z0-9:/.@]+)\s*$"
+ if ! [[ $content =~ $regex ]]; then
+ continue
+ fi
+
+ image="${BASH_REMATCH[2]}"
+ echo "$ln:$image"
+ # We wan't to track the line number to hopefully constrain the coming
+ # update to the correct part of the file. Ideally we'd track the ftell to
+ # be completely accurate, but that's difficult (if not impossible) in bash
+ # The sed removes comment, it skips the first 5 chars to skip the line
+ # number (4 numbers for the line and one space as a separator)
+ done < <(nl -w4 -s" " "$1" | sed '/^.{5}\s*#/d' -)
+}
+
+function search_for_newer_version {
+ while IFS= read -r line; do
+ ln=$(cut -d":" -f 1 <<<"$line")
+ content=$(cut -d":" -f 2- <<<"$line")
+ image_name=$(cut -s -d "@" -f 1 <<<"$content")
+ digest=$(cut -s -d "@" -f 2 <<<"$content")
+ tag=$(cut -s -d ":" -f 2 <<<"$image_name")
+ host=$(cut -s -d "/" -f 1 <<<"$image_name")
+ image=$(cut -d ":" -f 1 <<<"$image_name" | cut -d "/" -f 2)
+
+ new_tag=$(find_latest_version_of "$host" "$image")
+ if [[ "$tag" == "$new_tag" ]]; then
+ continue
+ fi
+
+ vecho 1 "Image $image on line $ln was $tag should be updated to $new_tag"
+ echo "$ln:$host:$image:$tag:$new_tag"
+ done
+}
+
+function generate_script {
+ # Format the colon separated values as a sed script to be run later (or
+ # printed if dry-run is on)
+ awk -F":" '{print $1 "s/" $2 "\\/" $3 ":" $4 "/" $2 "\\/" $3 ":" $5 "/"}'
+}
+
+if [[ $DRY_RUN == "false" ]]; then
+ function apply_script {
+ sed -f- -i "$1"
+ }
+else
+ function apply_script {
+ script=$(cat)
+ printf "Would have applied sed script:\n"
+ printf "%s\n" "$script"
+ }
+fi
+
+# Digests are excluded since don't really use them
+images_in_kube "$FILE" | search_for_newer_version #| generate_script | apply_script "$FILE"