diff options
| author | Jesper Jensen <jesper@jnsn.dev> | 2025-12-28 11:48:19 +0100 |
|---|---|---|
| committer | Jesper Jensen <jesper@jnsn.dev> | 2025-12-28 11:48:19 +0100 |
| commit | 2f26047f850093374513b78ac2d7579b6f087bd9 (patch) | |
| tree | 720e13017314bfd23ebd97f071dd6da63fe20bf3 | |
| parent | 8114c134c11a7b84f1dc29a4df7de6ecfe764958 (diff) | |
Add upgrade_docker script
| -rwxr-xr-x | upgrade_docker | 280 |
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" |
