summaryrefslogtreecommitdiff
path: root/upgrade_docker
blob: 4e5796c982fe537042d1c3fb92b9d59a41b99a7b (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
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"