Fix docker image update script
[ci-management.git] / docker / scripts / update_dockerhub_prod_tags.sh
1 #! /bin/bash
2
3 # Copyright (c) 2021 Cisco and/or its affiliates.
4 # Licensed under the Apache License, Version 2.0 (the "License");
5 # you may not use this file except in compliance with the License.
6 # You may obtain a copy of the License at:
7 #
8 #     http://www.apache.org/licenses/LICENSE-2.0
9 #
10 # Unless required by applicable law or agreed to in writing, software
11 # distributed under the License is distributed on an "AS IS" BASIS,
12 # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 # See the License for the specific language governing permissions and
14 # limitations under the License.
15
16 set -euo pipefail
17 shopt -s extglob
18
19 # Log all output to stdout & stderr to a log file
20 logname="/tmp/$(basename $0).$(date -u +%Y_%m_%d_%H%M%S).log"
21 echo -e "\n*** Logging output to $logname ***\n"
22 exec > >(tee -a $logname) 2>&1
23
24 export CIMAN_DOCKER_SCRIPTS=${CIMAN_DOCKER_SCRIPTS:-"$(dirname $BASH_SOURCE)"}
25 . "$CIMAN_DOCKER_SCRIPTS/lib_common.sh"
26
27 # Global variables
28 long_bar="################################################################"
29 short_bar="-----"
30 image_not_found=""
31 image_user=""
32 image_repo=""
33 image_version=""
34 image_arch=""
35 image_name_prod=""
36 image_name_prev=""
37 image_name_new=""
38 image_realname=""
39 image_realname_prod=""
40 image_realname_prev=""
41 image_tags=""
42 image_tags_prod=""
43 image_tags_prev=""
44 image_tags_new=""
45 docker_id_prod=""
46 docker_id_prev=""
47 docker_id_new=""
48 digest_prod=""
49 digest_prev=""
50 digest_new=""
51 restore_cmd=""
52
53 usage() {
54     local script="$(basename $0)"
55     echo
56     echo "Usage: $script r[evert]  <prod image>"
57     echo "       $script p[romote] <new image> [<new image>]"
58     echo "       $script i[nspect] <prod image>"
59     echo
60     echo "  revert: swaps 'prod-<arch>' and 'prod-prev-<arch>' images"
61     echo "          <prod image>: e.g. fdiotools/builder-ubuntu1804:prod-x86_64"
62     echo
63     echo " promote: moves 'prod-<arch>' image to 'prod-prev-<arch>' tag and"
64     echo "          tags <new image> with 'prod-<arch>'"
65     echo "          <new image>: e.g. fdiotools/builder-ubuntu1804:2020_09_23_151655-x86_64"
66     echo " inspect: prints out all tags for prod-<arch> and prod-prev-<arch>"
67     echo
68     exit 1
69 }
70
71 echo_restore_cmd() {
72     echo -e "\n$long_bar\n"
73     echo "To restore tags to original state, issue the following command:"
74     echo -e "\n$restore_cmd\n\n$long_bar\n"
75 }
76
77 push_to_dockerhub() {
78     echo_restore_cmd
79     for image in "$@" ; do
80         set +e
81         echo "Pushing '$image' to docker hub..."
82         if ! docker push "$image" ; then
83             echo "ERROR: 'docker push $image' failed!"
84             exit 1
85         fi
86     done
87 }
88
89 parse_image_name() {
90     image_user="$(echo $1 | cut -d'/' -f1)"
91     image_repo="$(echo $1 | cut -d'/' -f2 | cut -d':' -f1)"
92     local tag="$(echo $1 | cut -d':' -f2)"
93     image_version="$(echo $tag | cut -d'-' -f1)"
94     image_arch="$(echo $tag | sed -e s/$image_version-//)"
95     image_name_new="${image_user}/${image_repo}:${image_version}-${image_arch}"
96     if [ "$1" != "$image_name_new" ] ; then
97         echo "ERROR: Image name parsing failed: $1 != '$image_name_new'"
98         usage
99     fi
100     if [[ "$image_version" =~ "prod" ]] ; then
101         image_name_new=""
102     fi
103     image_name_prod="${image_user}/${image_repo}:prod-${image_arch}"
104     image_name_prev="${image_user}/${image_repo}:prod-prev-${image_arch}"
105 }
106
107 format_image_tags() {
108     # Note: 'grep $image_arch' & grep -v 'prod-curr' is required due to a
109     #       bug in docker hub which returns old tags which were deleted via
110     #       the webUI, but are still retrieved by 'docker pull -a'
111     image_tags="$(docker images | grep $1 | grep $image_arch | grep -v prod-curr | sort -r | mawk '{print $1":"$2}' | tr '\n' ' ')"
112     image_realname="$(docker images | grep $1 | grep $image_arch | sort -r | grep -v prod | mawk '{print $1":"$2}' || true)"
113     if [ -z "${image_realname:-}" ] ; then
114         image_realname="$image_tags"
115     fi
116 }
117
118 get_image_id_tags() {
119     for image in "$image_name_new" "$image_name_prod" "$image_name_prev" ; do
120         if [ -z "$image" ] ; then
121             continue
122         fi
123         # ensure image exists
124         set +e
125         local image_found="$(docker images | mawk '{print $1":"$2}' | grep $image)"
126         set -e
127         if [ -z "$image_found" ] ; then
128             if [ "$image" = "$image_name_prev" ] ; then
129                 if [ "$action" = "revert" ] ; then
130                     echo "ERROR: Image '$image' not found!"
131                     echo "Unable to revert production image '$image_name_prod'!"
132                     usage
133                 else
134                     continue
135                 fi
136             else
137                 echo "ERROR: Image '$image' not found!"
138                 usage
139             fi
140         fi
141         set +e
142         local id="$(docker image inspect $image | mawk -F':' '/Id/{print $3}')"
143         local digest="$(docker image inspect $image | grep -A1 RepoDigests | grep -v RepoDigests | mawk -F':' '{print $2}')"
144         local retval="$?"
145         set -e
146         if [ "$retval" -ne "0" ] ; then
147             echo "ERROR: Docker ID not found for '$image'!"
148             usage
149         fi
150         if [ "$image" = "$image_name_prod" ] ; then
151             docker_id_prod="${id::12}"
152             digest_prod="${digest::12}"
153             format_image_tags "$docker_id_prod"
154             image_tags_prod="$image_tags"
155             if [ -z "$image_realname_prod" ] ; then
156                 image_realname_prod="$image_realname"
157             fi
158         elif [ "$image" = "$image_name_prev" ] ; then
159             docker_id_prev="${id::12}"
160             digest_prev="${digest::12}"
161             format_image_tags "$docker_id_prev"
162             image_tags_prev="$image_tags"
163             if [ -z "$image_realname_prev" ] ; then
164                 image_realname_prev="$image_realname"
165             fi
166         else
167             docker_id_new="${id::12}"
168             digest_new="${digest::12}"
169             format_image_tags "$docker_id_new" "NEW"
170             image_tags_new="$image_tags"
171         fi
172     done
173     if [ -z "$restore_cmd" ] ; then
174         restore_cmd="sudo $0 p $image_realname_prev $image_realname_prod"
175     fi
176 }
177
178 get_all_tags_from_dockerhub() {
179     local dh_repo="$image_user/$image_repo"
180     echo -e "Pulling all tags from docker hub repo '$dh_repo':\n$long_bar"
181     if ! docker pull -a "$dh_repo" ; then
182         echo "ERROR: Repository '$dh_repo' not found on docker hub!"
183         usage
184     fi
185     echo "$long_bar"
186 }
187
188 verify_image_version_date_format() {
189     version="$1"
190     # TODO: Remove regex1 when legacy nomenclature is no longer on docker hub.
191     local regex1="^[0-9]{4}_[0-1][0-9]_[0-3][0-9]_[0-2][0-9][0-5][0-9][0-5][0-9]$"
192     local regex2="^[0-9]{4}_[0-1][0-9]_[0-3][0-9]_[0-2][0-9][0-5][0-9][0-5][0-9]_UTC$"
193     if [[ "$version" =~ $regex1 ]] || [[ "$version" =~ $regex2 ]]; then
194         return 0
195     fi
196     return 1
197 }
198
199 verify_image_name() {
200     image_not_found=""
201     # Invalid user
202     if [ "$image_user" != "fdiotools" ] ; then
203         image_not_found="true"
204         echo "ERROR: invalid user '$image_user' in '$image_name_new'!"
205     fi
206     # Invalid version
207     if [ -z "$image_not_found" ] \
208            && [ "$image_version" != "prod" ] \
209            && ! verify_image_version_date_format "$image_version"  ]] ; then
210         image_not_found="true"
211         echo "ERROR: invalid version '$image_version' in '$image_name_new'!"
212     fi
213     # Invalid arch
214     if [ -z "$image_not_found" ] \
215            && ! [[ "$EXECUTOR_ARCHS" =~ .*"$image_arch".* ]] ; then
216         image_not_found="true"
217         echo "ERROR: invalid arch '$image_arch' in '$image_name_new'!"
218     fi
219     if [ -n "$image_not_found" ] ; then
220         echo "ERROR: Invalid image '$image_name_new'!"
221         usage
222     fi
223 }
224
225 docker_tag_image() {
226     echo ">>> docker tag $1 $2"
227     set +e
228     docker tag "$1" "$2"
229     local retval="$?"
230     set -e
231     if [ "$retval" -ne "0" ] ; then
232         echo "WARNING: 'docker tag $1 $2' failed!"
233     fi
234 }
235
236 docker_rmi_tag() {
237     set +e
238     echo ">>> docker rmi $1"
239     docker rmi "$1"
240     local retval="$?"
241     set -e
242     if [ "$retval" -ne "0" ] ; then
243         echo "WARNING: 'docker rmi $1' failed!"
244     fi
245 }
246
247 print_image_list() {
248     if [ -z "$2" ] ; then
249         echo "$1 Image Not Found"
250         return
251     fi
252     echo "$1 (Id $2, Digest $3):"
253     for image in $4 ; do
254         echo -e "\t$image"
255     done
256 }
257
258 inspect_images() {
259     echo -e "\n${1}Production Docker Images:"
260     echo "$short_bar"
261     if [ -n "$image_tags_new" ] ; then
262         print_image_list "NEW" "$docker_id_new" "$digest_new" "$image_tags_new"
263         echo
264     fi
265     print_image_list "prod-$image_arch" "$docker_id_prod" "$digest_prod" \
266                      "$image_tags_prod"
267     echo
268     print_image_list "prod-prev-$image_arch" "$docker_id_prev" "$digest_prev" \
269                      "$image_tags_prev"
270     echo -e "$short_bar\n"
271 }
272
273 revert_prod_image() {
274     inspect_images "EXISTING "
275     docker_tag_image "$docker_id_prod" "$image_name_prev"
276     docker_tag_image "$docker_id_prev" "$image_name_prod"
277     get_image_id_tags
278     inspect_images "REVERTED "
279
280     local yn=""
281     while true; do
282         read -p "Push Reverted tags to '$image_user/$image_repo' (yes/no)? " yn
283         case ${yn:0:1} in
284             y|Y )
285                 break ;;
286             n|N )
287                 echo -e "\nABORTING REVERT!\n"
288                 docker_tag_image $docker_id_prev $image_name_prod
289                 docker_tag_image $docker_id_prod $image_name_prev
290                 get_image_id_tags
291                 inspect_images "RESTORED LOCAL "
292                 exit 1 ;;
293             * )
294                 echo "Please answer yes or no." ;;
295         esac
296     done
297     echo
298     push_to_dockerhub $image_name_prev $image_name_prod
299     inspect_images ""
300     echo_restore_cmd
301 }
302
303 promote_new_image() {
304     inspect_images "EXISTING "
305     docker_tag_image "$docker_id_prod" "$image_name_prev"
306     docker_tag_image "$docker_id_new" "$image_name_prod"
307     get_image_id_tags
308     inspect_images "PROMOTED "
309
310     local yn=""
311     while true; do
312         read -p "Push promoted tags to '$image_user/$image_repo' (yes/no)? " yn
313         case "${yn:0:1}" in
314             y|Y )
315                 break ;;
316             n|N )
317                 echo -e "\nABORTING PROMOTION!\n"
318                 docker_tag_image "$docker_id_prev" "$image_name_prod"
319                 local restore_both="$(echo $restore_cmd | mawk '{print $5}')"
320                 if [[ -n "$restore_both" ]] ; then
321                     docker_tag_image "$image_realname_prev" "$image_name_prev"
322                 else
323                     docker_rmi_tag "$image_name_prev"
324                     image_name_prev=""
325                     docker_id_prev=""
326                 fi
327                 get_image_id_tags
328                 inspect_images "RESTORED "
329                 exit 1 ;;
330             * )
331                 echo "Please answer yes or no." ;;
332         esac
333     done
334     echo
335     push_to_dockerhub "$image_name_new" "$image_name_prev" "$image_name_prod"
336     inspect_images ""
337     echo_restore_cmd
338 }
339
340 must_be_run_as_root_or_docker_group
341
342 # Validate arguments
343 num_args="$#"
344 if [ "$num_args" -lt "1" ] ; then
345     usage
346 fi
347 action=""
348 case "$1" in
349     r?(evert))
350         action="revert"
351         if [ "$num_args" -ne "2" ] ; then
352             echo "ERROR: Invalid number of arguments: $#"
353             usage
354         fi ;;
355     p?(romote))
356         if [ "$num_args" -eq "2" ] || [ "$num_args" -eq "3" ] ; then
357             action="promote"
358         else
359             echo "ERROR: Invalid number of arguments: $#"
360             usage
361         fi ;;
362     i?(nspect))
363         action="inspect"
364         if [ "$num_args" -ne "2" ] ; then
365             echo "ERROR: Invalid number of arguments: $#"
366             usage
367         fi ;;
368     *)
369         echo "ERROR: Invalid option '$1'!"
370         usage ;;
371 esac
372 shift
373 docker login >& /dev/null
374
375 # Update local tags
376 tags_to_push=""
377 for image in "$@" ; do
378     parse_image_name "$image"
379     verify_image_name "$image"
380     get_all_tags_from_dockerhub
381     get_image_id_tags
382     if [ "$action" = "promote" ] ; then
383         if [ -n "$image_name_new" ] ; then
384             promote_new_image
385         else
386             echo "ERROR: No new image specified to promote!"
387             usage
388         fi
389     elif [ "$action" = "revert" ] ; then
390         if [ "$image_version" = "prod" ] ; then
391             revert_prod_image
392         else
393             echo "ERROR: Non-production image '$image' specified!"
394             usage
395         fi
396     else
397         if [ "$image_version" = "prod" ] ; then
398             inspect_images ""
399         else
400             echo "ERROR: Non-production image '$image' specified!"
401             usage
402         fi
403     fi
404 done