Merge ~ubuntu-docker-images/ubuntu-docker-images/+git/utils:multi-arch-tagger into ~ubuntu-docker-images/ubuntu-docker-images/+git/utils:master

Proposed by Sergio Durigan Junior
Status: Merged
Merged at revision: 75a4c7e37ead57073f4020eb71df9c053724650f
Proposed branch: ~ubuntu-docker-images/ubuntu-docker-images/+git/utils:multi-arch-tagger
Merge into: ~ubuntu-docker-images/ubuntu-docker-images/+git/utils:master
Diff against target: 1196 lines (+1148/-0)
8 files modified
README.multi-arch-tagger.md (+67/-0)
helpers/log.sh (+49/-0)
helpers/registry-login.sh (+122/-0)
helpers/validate-args.sh (+85/-0)
list-all-images.sh (+129/-0)
list-tags-for-image.sh (+141/-0)
multi-arch-tagger.sh (+220/-0)
tag-images.sh (+335/-0)
Reviewer Review Type Date Requested Status
Paride Legovini Approve
Bryce Harrington Approve
Review via email: mp+399681@code.launchpad.net

Description of the change

Implement a multi-architecture tagger framework.

This MP consists of a series of scripts that work together to implement the multi-architecture tagging, which is one of the "missing pieces" for our OCI work.

The scripts are a bit complex, and they all work independently. The main script here is the "tag-images.sh", which encapsulates all the logic needed to properly create the "latest" and the "M.N-XX.YY_beta" tags (not to mention the special "ubuntu" base image).

I've tested this on some images and it's working OK, but most of my test was done using "echo" statements to make sure that the right commands would be invoked. We're still not ready to mass-tag everything, but I don't foresee any problems.

The aws-cli tool needs to be installed and configured locally in order to communicate with the AWS service. I've written some instructions on how to configure it in the README file.

To post a comment you must log in.
Revision history for this message
Bryce Harrington (bryce) wrote :

Initial pass of mostly just stylistic comments. I also looked for typos or coding errors but didn't find any! So nothing that actually "needs" fixed, but more just suggestions.

I haven't tried actually running the code and studying its execution, but will give that a shot for the next pass, but that'll have be for tomorrow.

I like that you broke out the log.sh to be a sourced file. I had already been thinking of breaking them out so I could use them, so that's cool.

Revision history for this message
Sergio Durigan Junior (sergiodj) wrote :

Thanks for the initial review, Bryce. Comments inline, and new version force-pushed.

Revision history for this message
Bryce Harrington (bryce) wrote :
Download full text (3.7 KiB)

Ok, I've had a chance to play with the script in earnest for a bit (just enough to be usefully confused), and have some usage suggestions.

Before I get too far into it, I'll mention upfront that if this is going to be used entirely non-interactively, then a lot of the remarks in this comment are probably going to be irrelevant. However, if you anticipate anyone might be running this by hand, then they may be worthwhile.

And first, here's how I'm running things:

stirling:~/pkg/Oci/utils$ ./list-all-images.sh -r docker -n ubuntu
Username: bryceharrington
Password:
ubuntu/nginx
ubuntu/redis
ubuntu/postgres
ubuntu/mysql
ubuntu/apache2
ubuntu/memcached
ubuntu/grafana
ubuntu/prometheus
ubuntu/cortex
ubuntu/telegraf
ubuntu/prometheus-alertmanager

$ ./list-tags-for-image.sh -r docker -n ubuntu -i redis
Username: bryceharrington
Password:
ubuntu/redis:latest
ubuntu/redis:6.0-21.04_beta
ubuntu/redis:edge
ubuntu/redis:6.0-21.04_edge
ubuntu/redis:5.0-20.04_beta
ubuntu/redis:5.0-20.04_edge

One thing that's a bit confusing is after running ./list-all-images.sh, I figured I could just cut and paste a line from its input to pass to ./list-tags-for-image.sh, however, that's wrong:

stirling:~/pkg/Oci/utils$ ./list-tags-for-image.sh -r docker -n ubuntu -i ubuntu/redis
Username: bryceharrington
Password:
E: Invalid image name 'ubuntu/redis'.

I'd probably just chalk that up to user error, but wouldn't be surprised if other rookie users make the same mistake. Again, if this is mainly intended to be used in an automated workflow, it's probably better to not have too many smarts in the argument parsing logic. You've got the --no-prefix argument if someone really needs the bare image name.

stirling:~/pkg/Oci/utils$ ./multi-arch-tagger.sh -t foobar -n ubuntu -r docker -b 20.04 -u bryceharrington -p <my-docker-password> -i redis
Username: bryceharrington
Password:
I: Tagging ubuntu/redis:20.04 as ubuntu/redis:foobar (on docker)
ERROR: Image tagging failed with status code 400

So first comment is I'm not entirely certain this is the right way to be calling this tool. Given the large number of required parameters it would help a lot to have an example or two in the help text (or in a man page). For this tool, maybe one cut-and-paste command line to test it, and then one real-world usage.

Second, I'd kind of like to be able to run this against a throwaway image in my own namespace, just to test it, but passing '-n bryceharrington' gives an error. If that's intended/desired then a --dry-run parameter might be handy to add.

Third, I was initially a bit confused why there are --username and --password arguments, yet it still prompts me for username and password. From the help text I got the impression they were two different sets of usernames and passwords, but still a bit confusing what they might be. Also, taking a password on the command line might have some security implications, although I gather this is just for testing convenience and the auth token is used more in practice?

And fourth, the error message with status code 400 is a bit cryptic. That said, I'll bet it's not really going to be possible to give a better error message since that c...

Read more...

review: Approve
Revision history for this message
Sergio Durigan Junior (sergiodj) wrote :
Download full text (7.3 KiB)

On Tuesday, March 16 2021, Bryce Harrington wrote:

> Review: Approve
>
> Ok, I've had a chance to play with the script in earnest for a bit (just enough to be usefully confused), and have some usage suggestions.

Thanks for the thorough review, Bryce.

I'm replying by email because it's easier to do inline comments.

> Before I get too far into it, I'll mention upfront that if this is
> going to be used entirely non-interactively, then a lot of the remarks
> in this comment are probably going to be irrelevant. However, if you
> anticipate anyone might be running this by hand, then they may be
> worthwhile.

Yeah, I think the only script that will be "heavily" used in an
interactive way is the "tag-images.sh", and *perhaps* the
"multi-arch-tagger.sh". I'm not expecting the other scripts to be used
interactively much, although I wanted them to support that as well.

> And first, here's how I'm running things:
>
> stirling:~/pkg/Oci/utils$ ./list-all-images.sh -r docker -n ubuntu
> Username: bryceharrington
> Password:
> ubuntu/nginx
> ubuntu/redis
> ubuntu/postgres
> ubuntu/mysql
> ubuntu/apache2
> ubuntu/memcached
> ubuntu/grafana
> ubuntu/prometheus
> ubuntu/cortex
> ubuntu/telegraf
> ubuntu/prometheus-alertmanager

Cool!

> $ ./list-tags-for-image.sh -r docker -n ubuntu -i redis
> Username: bryceharrington
> Password:
> ubuntu/redis:latest
> ubuntu/redis:6.0-21.04_beta
> ubuntu/redis:edge
> ubuntu/redis:6.0-21.04_edge
> ubuntu/redis:5.0-20.04_beta
> ubuntu/redis:5.0-20.04_edge

Cool again! :-)

> One thing that's a bit confusing is after running
> ./list-all-images.sh, I figured I could just cut and paste a line from
> its input to pass to ./list-tags-for-image.sh, however, that's wrong:
>
> stirling:~/pkg/Oci/utils$ ./list-tags-for-image.sh -r docker -n ubuntu -i ubuntu/redis
> Username: bryceharrington
> Password:
> E: Invalid image name 'ubuntu/redis'.

Yeah, that doesn't work because you're already specifying the namespace
via the "-n" parameter, and that will prepended to the image name when
composing the URL used to retrieve the information. I mean, I can
understand the initial confusion, but once you have a clearer picture of
what an "image", a "namespace" and a "registry" are, I think it makes
more sense.

On a side note, I've been thinking about reversing the "--no-prefix"
option and making it the default, because that's what we usually want as
an output.

> I'd probably just chalk that up to user error, but wouldn't be
> surprised if other rookie users make the same mistake. Again, if this
> is mainly intended to be used in an automated workflow, it's probably
> better to not have too many smarts in the argument parsing logic.
> You've got the --no-prefix argument if someone really needs the bare
> image name.

I'm expecting some of these scripts to be used interactively, as I
mentioned above, so maybe it's a good idea to write a more comprehensive
documentation. I don't think nothing fancy is needed; maybe just
extending the README file will be enough.

> stirling:~/pkg/Oci/utils$ ./multi-arch-tagger.sh -t foobar -n ubuntu -r docker -b 20.04 -u bryceharrington -p <my-docker-password> -i redis
> Username: bryceharrington
...

Read more...

Revision history for this message
Sergio Durigan Junior (sergiodj) :
Revision history for this message
Bryce Harrington (bryce) :
Revision history for this message
Paride Legovini (paride) wrote :

Great work with these scripts Sergio! I did a shellcheck run and they're almost clean, just with some nits like:

  SC2086: Double quote to prevent globbing and word splitting.

here and there, mostly about "safe" variables known to have no spaces. There are also some false positives I think . Given the high quality of the scripts it may be worth to make shellcheck fully happy, and test future MPs (possibly in CI) to keep quality high.

review: Approve
Revision history for this message
Sergio Durigan Junior (sergiodj) wrote :

Thanks for the review, Paride :-).

I have silenced everything that shellcheck flagged, and I have also addressed most (if not all) of the comments that Bryce has made.

I am merging the script now, because it is ready to be used. We can always come back and improve it if needed.

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1diff --git a/README.multi-arch-tagger.md b/README.multi-arch-tagger.md
2new file mode 100644
3index 0000000..dd33eee
4--- /dev/null
5+++ b/README.multi-arch-tagger.md
6@@ -0,0 +1,67 @@
7+Multi-arch tagger
8+=================
9+
10+The following scripts are part of the multi-architecture tagger
11+application. They are used to tag multiple architectures of an OCI
12+image without having to actually run `docker tag` in those
13+architectures.
14+
15+- `list-all-images.sh`: This script can be used to list all images
16+ from a certain registry/namespace combination.
17+
18+- `list-tags-for-image.sh`: This script can be used to list all tags
19+ pertaining to a certain image from a registry/namespace.
20+
21+- `multi-arch-tagger.sh`: This script can be used to perform the
22+ multi-architecture tagging of a certain image from a
23+ registry/namespace.
24+
25+- `tag-images.sh`: This script is a wrapper on top of
26+ `multi-arch-tagger.sh`; it iterates over the available images from a
27+ certain registry/namespace combination and tags them.
28+
29+All of these scripts accept a `--help` argument and print their
30+usages, so it should not be hard to understand how to use them.
31+
32+Most of the time, you will actually just use the `tag-images.sh`
33+script, which invokes all the other scripts in order to do its job.
34+
35+Requirements to run these scripts
36+---------------------------------
37+
38+You have to have the following programs installed in your system in
39+order to run the scripts:
40+
41+- jq
42+- curl
43+- awscli
44+
45+You don't need `docker` installed, since we use API endpoints when
46+communicating with Dockerhub. AWS doesn't support most of the API
47+endpoints yet, so that is why you need `awscli`.
48+
49+Configuring awscli for tag-images.sh
50+-------------------------------------
51+
52+The tag-images.sh script requires that you install and configure
53+`awscli` in order to tag images on AWS.
54+
55+To install the package:
56+
57+```
58+ $ apt install awscli
59+```
60+
61+To configure the software, you will need to create two profiles in
62+order to use the tagger.
63+
64+```
65+ $ aws configure --profile ubuntu
66+ ...
67+
68+ $ aws configure --profile lts
69+ ...
70+```
71+
72+Each profile needs to be properly configured using the corresponding
73+access keys, of course.
74diff --git a/helpers/log.sh b/helpers/log.sh
75new file mode 100644
76index 0000000..e3e6b9b
77--- /dev/null
78+++ b/helpers/log.sh
79@@ -0,0 +1,49 @@
80+#!/bin/bash
81+
82+# Generic logging function.
83+_log ()
84+{
85+ local type="$1"
86+ local msg="$2"
87+ local redir=/dev/stdout
88+
89+ case "$type" in
90+ "E"|"D")
91+ redir=/dev/stderr
92+ ;;
93+ "I"|"W")
94+ :
95+ ;;
96+ *)
97+ # Can't call error here, can we?
98+ echo "E: Internal error in the '_log' function!" \
99+ > /dev/stderr
100+ ;;
101+ esac
102+
103+ printf "%s: %s\n" "$type" "$msg" > $redir
104+}
105+
106+# Error logging.
107+error ()
108+{
109+ _log "E" "$1"
110+}
111+
112+# Warn logging.
113+warn ()
114+{
115+ _log "W" "$1"
116+}
117+
118+# Info logging.
119+info ()
120+{
121+ _log "I" "$1"
122+}
123+
124+# Debug logging.
125+debug ()
126+{
127+ _log "D" "$1"
128+}
129diff --git a/helpers/registry-login.sh b/helpers/registry-login.sh
130new file mode 100644
131index 0000000..fa68362
132--- /dev/null
133+++ b/helpers/registry-login.sh
134@@ -0,0 +1,122 @@
135+#!/bin/bash
136+
137+# Log into a registry.
138+
139+set -e
140+
141+readonly AWS_URL="https://public.ecr.aws"
142+readonly AWS_REGISTRY_URL="https://public.ecr.aws"
143+export AWS_URL AWS_REGISTRY_URL
144+
145+readonly DOCKERHUB_URL="https://hub.docker.com"
146+readonly DOCKERHUB_REGISTRY_URL="https://registry-1.docker.io"
147+export DOCKERHUB_URL DOCKERHUB_REGISTRY_URL
148+
149+# Log into dockerhub, asking the user and the password.
150+#
151+# This function doesn't take arguments. It sets the AUTH_TOKEN
152+# environment variable to the Authentication Token that can be used on
153+# further requests.
154+_login_docker ()
155+{
156+ if [ -z "$USERNAME" ]; then
157+ read -rp "Username: " USERNAME
158+ fi
159+ if [ -z "$PASSWORD" ]; then
160+ read -rsp "Password: " PASSWORD
161+ echo > /dev/stderr
162+ fi
163+
164+ AUTH_TOKEN=$(curl -s \
165+ -H "Content-Type: application/json" \
166+ -X POST -d '{"username": "'"${USERNAME}"'", "password": "'"${PASSWORD}"'"}' \
167+ "${DOCKERHUB_URL}/v2/users/login/" | jq -r .token)
168+
169+ if [ -z "$AUTH_TOKEN" ]; then
170+ error "There was an error while logging into the service."
171+ exit 1
172+ fi
173+
174+ if [ -n "$DEBUG" ]; then
175+ debug "This is the Dockerhub authentication token I got, in case you want to reuse it:"
176+ printf 'AUTH_TOKEN=%s\n' "$AUTH_TOKEN" > /dev/stderr
177+ fi
178+
179+ REGISTRY_USERNAME="$USERNAME"
180+ REGISTRY_PASSWORD="$PASSWORD"
181+}
182+
183+# Log into dockerhub's registry1, asking the user and the password.
184+#
185+# It takes the list of images to be used as the scope as argument. It
186+# assumes that the image names are not prefixed with "$NAMESPACE/"
187+_login_docker_registry1 ()
188+{
189+ local IMAGES_FOR_SCOPE="$1"
190+ local REGISTRY_USERNAME="$2"
191+ local REGISTRY_PASSWORD="$3"
192+ local URL="https://auth.docker.io/token?service=registry.docker.io"
193+
194+ if [ -z "$REGISTRY_USERNAME" ] || [ -z "$REGISTRY_PASSWORD" ]; then
195+ error "You must specify the registry username and password."
196+ exit 1
197+ fi
198+
199+ for image in $IMAGES_FOR_SCOPE; do
200+ URL+="&scope=repository:${NAMESPACE}/${image}:pull,push"
201+ done
202+
203+ REGISTRY_AUTH_TOKEN=$(curl -s \
204+ -u "${REGISTRY_USERNAME}:${REGISTRY_PASSWORD}" \
205+ "$URL" | jq -r .token)
206+ export REGISTRY_AUTH_TOKEN
207+
208+ if [ -z "$REGISTRY_AUTH_TOKEN" ]; then
209+ error "There was an error while logging into the registry."
210+ exit 1
211+ fi
212+
213+ export REGISTRY_URL="$DOCKERHUB_REGISTRY_URL"
214+}
215+
216+_login_aws ()
217+{
218+ # No need to login with aws since we will be using aws-cli.
219+ if ! command -v aws >& /dev/null; then
220+ error "You must have the aws-cli program installed."
221+ exit 1
222+ fi
223+
224+ if ! aws configure list --profile "$NAMESPACE" >& /dev/null; then
225+ error "You must configure aws-cli with a profile named '$NAMESPACE'."
226+ error "Please refer to the README file for instructions."
227+ exit 1
228+ fi
229+
230+ export AWS_PROFILE="$NAMESPACE"
231+}
232+
233+_login_aws_registry1 ()
234+{
235+ REGISTRY_AUTH_TOKEN=$(aws ecr-public get-authorization-token \
236+ --region us-east-1 \
237+ --output=text \
238+ --query 'authorizationData.authorizationToken')
239+
240+
241+ if [ -z "$REGISTRY_AUTH_TOKEN" ]; then
242+ error "There was an error while logging into the registry."
243+ exit 1
244+ fi
245+
246+ REGISTRY_URL="$AWS_REGISTRY_URL"
247+}
248+
249+do_login ()
250+{
251+ if [ -n "$AUTH_TOKEN" ]; then
252+ return
253+ fi
254+
255+ _login_"${REGISTRY}"
256+}
257diff --git a/helpers/validate-args.sh b/helpers/validate-args.sh
258new file mode 100644
259index 0000000..551c6f7
260--- /dev/null
261+++ b/helpers/validate-args.sh
262@@ -0,0 +1,85 @@
263+#!/bin/bash
264+
265+progname=$(basename "${0}")
266+progdir=$(dirname "$(readlink -f "${0}")")
267+
268+export progname progdir
269+
270+# shellcheck disable=SC1090
271+. "${progdir}/helpers/log.sh"
272+
273+validate_var ()
274+{
275+ local varname="$1"
276+ local varvalue
277+
278+ varvalue=$(eval "echo \$${varname}")
279+
280+ if [ -z "$varvalue" ]; then
281+ error "You must specify the ${varname} parameter."
282+ return 1
283+ fi
284+
285+ return 0
286+}
287+
288+validate_registry ()
289+{
290+ local registry="$1"
291+
292+ case "$registry" in
293+ "docker"|"aws")
294+ :
295+ ;;
296+ *)
297+ error "Invalid registry: '$registry'."
298+ return 1
299+ ;;
300+ esac
301+
302+ return 0
303+}
304+
305+validate_namespace ()
306+{
307+ local namespace="$1"
308+
309+ case "$namespace" in
310+ "ubuntu")
311+ :
312+ ;;
313+ "lts")
314+ # shellcheck disable=SC2153
315+ if [ "$REGISTRY" = "docker" ]; then
316+ error "The lts namespace is not available on dockerhub yet."
317+ return 1
318+ fi
319+ ;;
320+ *)
321+ error "Invalid namespace: '$namespace:'."
322+ return 1
323+ ;;
324+ esac
325+
326+ return 0
327+}
328+
329+validate_image ()
330+{
331+ local image="$1"
332+ local -a ALL_IMAGES
333+
334+ # shellcheck disable=SC2153
335+ # Fill the ALL_IMAGES array.
336+ readarray -t ALL_IMAGES < <("${progdir}/list-all-images.sh" \
337+ -r "$REGISTRY" \
338+ -n "$NAMESPACE" \
339+ -a "$AUTH_TOKEN")
340+
341+ if ! grep -Fwq "$image" <<< "${ALL_IMAGES[@]}"; then
342+ error "Invalid image name '$image'."
343+ return 1
344+ fi
345+
346+ return 0
347+}
348diff --git a/list-all-images.sh b/list-all-images.sh
349new file mode 100755
350index 0000000..c82b50c
351--- /dev/null
352+++ b/list-all-images.sh
353@@ -0,0 +1,129 @@
354+#!/bin/bash
355+
356+set -e
357+
358+progname=$(basename "${0}")
359+progdir=$(dirname "$(readlink -f "${0}")")
360+
361+# shellcheck disable=SC1090
362+. "${progdir}/helpers/validate-args.sh"
363+# shellcheck disable=SC1090
364+. "${progdir}/helpers/log.sh"
365+# shellcheck disable=SC1090
366+. "${progdir}/helpers/registry-login.sh"
367+
368+# Obtain the list of images for a certain repository.
369+
370+# Usage.
371+usage ()
372+{
373+ cat > /dev/stderr <<EOF
374+$progname -- Obtain the list of images from a certain repository.
375+
376+Usage:
377+
378+ $progname [-a <TOKEN>] -r <docker|aws> -n <ubuntu|lts> [-d]
379+
380+Arguments:
381+
382+ -a TOKEN|--auth-token TOKEN Specify the authorization token that
383+ should be used when connecting to the
384+ registry's API. If not provided, the
385+ script will ask for your user/password.
386+
387+ -r REGISTRY|--registry REGISTRY Registry to consult.
388+ Possible values: docker, aws
389+
390+ -n NAMESPACE|--namespace NAMESPACE: Namespace to consult.
391+ Possible values: ubuntu, lts
392+
393+ -d|--debug Print debug statements. This includes printing
394+ the authentication token if using Dockerhub.
395+
396+ -h|--help Display this usage
397+EOF
398+}
399+
400+validate_args ()
401+{
402+ local ret=0
403+
404+ validate_var REGISTRY || ret=1
405+ validate_var NAMESPACE || ret=1
406+
407+ validate_registry "$REGISTRY" || ret=1
408+ validate_namespace "$NAMESPACE" || ret=1
409+
410+ if [ "$ret" -eq 1 ]; then
411+ error "Failed to validate arguments."
412+ exit 1
413+ fi
414+
415+ do_login
416+}
417+
418+print_list_of_images_docker ()
419+{
420+ for image in $(curl -s \
421+ -H "Authorization: Bearer ${AUTH_TOKEN}" \
422+ "${DOCKERHUB_URL}/v2/repositories/${NAMESPACE}/?page_size=10000" \
423+ | jq -r '.results|.[]|.name'); do
424+ echo "$image"
425+ done
426+}
427+
428+print_list_of_images_aws ()
429+{
430+ aws --region us-east-1 ecr-public describe-repositories \
431+ | jq -r '.repositories[].repositoryName'
432+}
433+
434+print_list_of_images ()
435+{
436+ print_list_of_images_"${REGISTRY}"
437+}
438+
439+if [ $# -lt 2 ]; then
440+ error "You need to provide at least the registry (-r) and the namespace (-n) arguments."
441+ usage
442+ exit 1
443+fi
444+
445+while [ -n "$1" ]; do
446+ case "$1" in
447+ "-a"|"--auth-token")
448+ AUTH_TOKEN="$2"
449+ shift 2
450+ ;;
451+
452+ "-r"|"--registry")
453+ REGISTRY="$2"
454+ shift 2
455+ ;;
456+
457+ "-n"|"--namespace")
458+ NAMESPACE="$2"
459+ shift 2
460+ ;;
461+
462+ "-d"|"--debug")
463+ export DEBUG=1
464+ shift
465+ ;;
466+
467+ "-h"|"--help")
468+ usage
469+ exit 0
470+ ;;
471+
472+ *)
473+ error "Invalid argument '$1'."
474+ usage
475+ exit 1
476+ ;;
477+ esac
478+done
479+
480+validate_args
481+
482+print_list_of_images
483diff --git a/list-tags-for-image.sh b/list-tags-for-image.sh
484new file mode 100755
485index 0000000..305c9be
486--- /dev/null
487+++ b/list-tags-for-image.sh
488@@ -0,0 +1,141 @@
489+#!/bin/bash
490+
491+set -e
492+
493+progname=$(basename "${0}")
494+progdir=$(dirname "$(readlink -f "${0}")")
495+
496+# shellcheck disable=SC1090
497+. "${progdir}/helpers/validate-args.sh"
498+# shellcheck disable=SC1090
499+. "${progdir}/helpers/log.sh"
500+# shellcheck disable=SC1090
501+. "${progdir}/helpers/registry-login.sh"
502+
503+# Obtain the list of tags for a certain image.
504+
505+# Usage.
506+usage ()
507+{
508+ cat > /dev/stderr <<EOF
509+$progname -- Obtain the list of tags from a certain image.
510+
511+Usage:
512+
513+ $progname [-a <TOKEN>] -r <docker|aws> -n <ubuntu|lts> -i <IMAGE> [-d]
514+
515+Arguments:
516+
517+ -a TOKEN|--auth-token TOKEN Specify the authorization token that
518+ should be used when connecting to the
519+ registry's API. If not provided, the
520+ script will ask for your user/password.
521+
522+ -r REGISTRY|--registry REGISTRY Registry to consult.
523+ Possible values: docker, aws
524+
525+ -n NAMESPACE|--namespace NAMESPACE Namespace to consult.
526+ Possible values: ubuntu, lts
527+
528+ -i IMAGE|--image IMAGE Image to obtain tags from.
529+
530+ -d|--debug Print debug statements. This includes printing
531+ the authentication token if using Dockerhub.
532+
533+ -h|--help Display this usage
534+EOF
535+}
536+
537+validate_args ()
538+{
539+ local ret=0
540+
541+ validate_var REGISTRY || ret=1
542+ validate_var NAMESPACE || ret=1
543+ validate_var IMAGE || ret=1
544+
545+ validate_registry "$REGISTRY" || ret=1
546+ validate_namespace "$NAMESPACE" || ret=1
547+
548+ if [ "$ret" -eq 1 ]; then
549+ error "Failed to validate arguments."
550+ exit 1
551+ fi
552+
553+ do_login
554+
555+ validate_image "$IMAGE"
556+}
557+
558+print_tags_for_image_docker ()
559+{
560+ for tag in $(curl -s \
561+ -H "Authorization: JWT ${AUTH_TOKEN}" \
562+ "${DOCKERHUB_URL}/v2/repositories/${NAMESPACE}/${IMAGE}/tags/?page_size=10000" \
563+ | jq -r '.results|.[]|.name'); do
564+ echo "${tag}"
565+ done
566+}
567+
568+print_tags_for_image_aws ()
569+{
570+ for tag in $(aws --region us-east-1 ecr-public describe-image-tags --repository-name "${IMAGE}" \
571+ | jq -r '.imageTagDetails[].imageTag'); do
572+ echo "${tag}"
573+ done
574+}
575+
576+print_tags_for_image ()
577+{
578+ print_tags_for_image_"${REGISTRY}"
579+}
580+
581+if [ $# -lt 2 ]; then
582+ error "You need to provide at least the registry (-r), the namespace (-n) and the image (-i) arguments."
583+ usage
584+ exit 1
585+fi
586+
587+while [ -n "$1" ]; do
588+ case "$1" in
589+ "-a"|"--auth-token")
590+ AUTH_TOKEN="$2"
591+ shift 2
592+ ;;
593+
594+ "-r"|"--registry")
595+ REGISTRY="$2"
596+ shift 2
597+ ;;
598+
599+ "-n"|"--namespace")
600+ NAMESPACE="$2"
601+ shift 2
602+ ;;
603+
604+ "-i"|"--image")
605+ IMAGE="$2"
606+ shift 2
607+ ;;
608+
609+ "-d"|"--debug")
610+ export DEBUG=1
611+ shift
612+ ;;
613+
614+ "-h"|"--help")
615+ usage
616+ exit 0
617+ ;;
618+
619+ *)
620+ error "Invalid argument '$1'."
621+ usage
622+ exit 1
623+ ;;
624+ esac
625+done
626+
627+validate_args
628+
629+print_tags_for_image
630diff --git a/multi-arch-tagger.sh b/multi-arch-tagger.sh
631new file mode 100755
632index 0000000..a493fa2
633--- /dev/null
634+++ b/multi-arch-tagger.sh
635@@ -0,0 +1,220 @@
636+#!/bin/bash
637+
638+set -e
639+
640+progname=$(basename "${0}")
641+progdir=$(dirname "$(readlink -f "${0}")")
642+
643+# shellcheck disable=SC1090
644+. "${progdir}/helpers/validate-args.sh"
645+# shellcheck disable=SC1090
646+. "${progdir}/helpers/log.sh"
647+# shellcheck disable=SC1090
648+. "${progdir}/helpers/registry-login.sh"
649+
650+# Tag all architectures of an image.
651+
652+# Usage.
653+usage ()
654+{
655+ cat > /dev/stderr <<EOF
656+$progname -- Tag all architectures of an image.
657+
658+Due to the way docker's API works, you can provide an authentication
659+token for the hub.docker.com service (if you don't, the script will
660+ask for you user and password), but you *must* provide your username
661+and password for the registry1.docker.io service. This is likely the
662+same username/password you use for docker.
663+
664+Usage (when using Dockerhub):
665+
666+ $progname [-a <TOKEN>] -r docker -n <ubuntu|lts> -i <IMAGE> -s <SOURCE_TAG> -t <NEW_TAG> [-u <USERNAME>] [-p <PASSWORD>] [-d]
667+
668+Usage (when using AWS):
669+
670+ $progname [-a <TOKEN>] -r aws -n <ubuntu|lts> -i <IMAGE> -s <SOURCE_TAG> -t <NEW_TAG> [-d]
671+
672+Arguments:
673+
674+ -a TOKEN|--auth-token TOKEN Specify the authorization token that
675+ should be used when connecting to the
676+ registry's API. If not provided, the
677+ script will ask for your user/password.
678+ This is **NOT** the authentication token
679+ for the registry (i.e., not for e.g.
680+ registry1.docker.io, if you're using
681+ docker). See the -u and -p options
682+ below.
683+
684+ -r REGISTRY|--registry REGISTRY Registry to consult.
685+ Possible values: docker, aws
686+
687+ -n NAMESPACE|--namespace NAMESPACE Namespace to consult.
688+ Possible values: ubuntu, lts
689+
690+ -i IMAGE|--image IMAGE Name of an image from the repository (without
691+ namespace), for example 'postgres' or 'redis'.
692+
693+ -s SOURCE_TAG|--source-tag SOURCE_TAG Use SOURCE_TAG as the source tag when
694+ tagging the image.
695+
696+ -t NEW_TAG|--tag NEW_TAG Tag image using NEW_TAG.
697+
698+ -u USERNAME|--username USERNAME This is your Dockerhub registry username. Only
699+ required when using Dockerhub as the registry.
700+ This will be the same as your regular Dockerhub
701+ username.
702+
703+ -p PASSWORD|--password PASSWORD This is your Dockerhub registry password. Only
704+ required when using Dockerhub as the registry.
705+ This will be the same as your Dockerhub username.
706+
707+ -d|--debug Print debug statements. This includes printing
708+ the authentication token if using Dockerhub.
709+
710+ -h|--help Display this usage
711+EOF
712+}
713+
714+do_tag_image ()
715+{
716+ info "Tagging ${NAMESPACE}/${IMAGE}:${SOURCE_TAG} as ${NAMESPACE}/${IMAGE}:${TAG} (on ${REGISTRY})"
717+
718+ local tmpfile
719+ local ret
720+
721+ tmpfile=$(mktemp)
722+ trap 'rm -f ${tmpfile}' 0 INT QUIT ABRT PIPE TERM
723+
724+ curl -s -H "Authorization: Bearer ${REGISTRY_AUTH_TOKEN}" \
725+ -H "Accept: application/vnd.docker.distribution.manifest.list.v2+json" \
726+ "${REGISTRY_URL}/v2/${NAMESPACE}/${IMAGE}/manifests/${SOURCE_TAG}" \
727+ > "$tmpfile"
728+
729+ # We use "|| true" here because we want the command to complete
730+ # even if it fails. We check for the HTTP response code below.
731+ ret=$(curl -X PUT -H "Authorization: Bearer ${REGISTRY_AUTH_TOKEN}" \
732+ -H "Content-Type: application/vnd.docker.distribution.manifest.list.v2+json" \
733+ "${REGISTRY_URL}/v2/${NAMESPACE}/${IMAGE}/manifests/${TAG}" \
734+ -d "@${tmpfile}" \
735+ -s -o /dev/null -w "%{http_code}" || true)
736+
737+ rm "$tmpfile"
738+
739+ if [ "$ret" -ne 201 ]; then
740+ echo "ERROR: Image tagging failed with status code $ret"
741+ exit 1
742+ fi
743+}
744+
745+do_registry_login ()
746+{
747+ _login_"${REGISTRY}"_registry1 "$IMAGE" "$REGISTRY_USERNAME" "$REGISTRY_PASSWORD"
748+}
749+
750+validate_args ()
751+{
752+ local ret=0
753+
754+ validate_var REGISTRY || ret=1
755+ validate_var NAMESPACE || ret=1
756+ validate_var IMAGE || ret=1
757+ validate_var SOURCE_TAG || ret=1
758+ validate_var TAG || ret=1
759+
760+ validate_registry "$REGISTRY" || ret=1
761+ validate_namespace "$NAMESPACE" || ret=1
762+
763+ if [ "$ret" -eq 1 ]; then
764+ error "Failed to validate arguments."
765+ exit 1
766+ fi
767+
768+ if [ -n "$REGISTRY_USERNAME" ]; then
769+ export USERNAME="$REGISTRY_USERNAME"
770+ fi
771+ if [ -n "$REGISTRY_PASSWORD" ]; then
772+ export PASSWORD="$REGISTRY_PASSWORD"
773+ fi
774+
775+ do_login
776+
777+ validate_image "$IMAGE" || exit 1
778+
779+ if [ "$REGISTRY" = "docker" ]; then
780+ validate_var REGISTRY_USERNAME || exit 1
781+ validate_var REGISTRY_PASSWORD || exit 1
782+ fi
783+
784+ do_registry_login
785+}
786+
787+if [ $# -lt 2 ]; then
788+ error "You need to provide arguments to the script."
789+ usage
790+ exit 1
791+fi
792+
793+while [ -n "$1" ]; do
794+ case "$1" in
795+ "-a"|"--auth-token")
796+ export AUTH_TOKEN="$2"
797+ shift 2
798+ ;;
799+
800+ "-r"|"--registry")
801+ REGISTRY="$2"
802+ shift 2
803+ ;;
804+
805+ "-n"|"--namespace")
806+ NAMESPACE="$2"
807+ shift 2
808+ ;;
809+
810+ "-i"|"--image")
811+ IMAGE="$2"
812+ shift 2
813+ ;;
814+
815+ "-s"|"--source-tag")
816+ SOURCE_TAG="$2"
817+ shift 2
818+ ;;
819+
820+ "-t"|"--tag")
821+ TAG="$2"
822+ shift 2
823+ ;;
824+
825+ "-u"|"--username")
826+ REGISTRY_USERNAME="$2"
827+ shift 2
828+ ;;
829+
830+ "-p"|"--password")
831+ REGISTRY_PASSWORD="$2"
832+ shift 2
833+ ;;
834+
835+ "-d"|"--debug")
836+ export DEBUG=1
837+ shift
838+ ;;
839+
840+ "-h"|"--help")
841+ usage
842+ exit 0
843+ ;;
844+
845+ *)
846+ error "Invalid argument '$1'."
847+ usage
848+ exit 1
849+ ;;
850+ esac
851+done
852+
853+validate_args
854+
855+do_tag_image
856diff --git a/tag-images.sh b/tag-images.sh
857new file mode 100755
858index 0000000..874673a
859--- /dev/null
860+++ b/tag-images.sh
861@@ -0,0 +1,335 @@
862+#!/bin/bash
863+
864+set -e
865+
866+progname=$(basename "${0}")
867+progdir=$(dirname "$(readlink -f "${0}")")
868+
869+# shellcheck disable=SC1090
870+. "${progdir}/helpers/validate-args.sh"
871+# shellcheck disable=SC1090
872+. "${progdir}/helpers/log.sh"
873+# shellcheck disable=SC1090
874+. "${progdir}helpers/registry-login.sh"
875+
876+# Tag images from a repository.
877+
878+# The list of images to tag.
879+IMAGES=()
880+
881+# The list of all available images.
882+ALL_IMAGES=()
883+
884+# Default source tag is "edge".
885+SOURCE_TAG=edge
886+
887+# Usage.
888+usage ()
889+{
890+ cat > /dev/stderr <<EOF
891+$progname -- Tag images from a repository (multi-architecture).
892+
893+This script will tag images from a repository using a certain tag the
894+source tag.
895+
896+******************************* DISCLAIMER **************************************
897+** This script will only work with the 'latest' and the 'M.N-XX.YY_beta' tags! **
898+*********************************************************************************
899+
900+This script is needed because of a few particularities in the tagging
901+policy for our OCI images.
902+
903+The first is the fact that Launchpad will not tag anything using the
904+"latest" tag. When it finishes a build, the only tags it will
905+generate are the "edge" and the "M.N-XX.YY_edge" tags. This means
906+that we need to manually tag everything using "latest" after the
907+images have been uploaded to the registries. We also need to create
908+the "M.N-XX.YY_beta" tag, since that is not automatically created by
909+Launchpad either.
910+
911+The other problem we have to consider is that, when tagging an image,
912+we must tag all of the architectures that were uploaded. If we were
913+to do that using "docker", we would need to run it on several
914+machines, one for each supported architectures, which is not always
915+possible/feasible. By using API calls directly, we are able to tag
916+all architectures using a single machine.
917+
918+Usage:
919+
920+ $progname [-a <TOKEN>] -r <docker|aws> -n <ubuntu|lts> -s <SOURCE_TAG> [-d] [-- IMAGE_1 IMAGE_2 ... IMAGE_N]
921+
922+Arguments:
923+
924+ -r REGISTRY|--registry REGISTRY Registry to consult.
925+ Possible values: docker, aws
926+
927+ -n NAMESPACE|--namespace NAMESPACE Namespace to consult.
928+ Possible values: ubuntu, lts
929+
930+ -s TAG|--source-tag SOURCE_TAG Use SOURCE_TAG as the source tag when tagging
931+ the images. If not specified, the default
932+ is 'edge'.
933+
934+ -f|--force Force tagging, even if the script would think
935+ that it's not needed. This mostly applies for
936+ the 'M.N-XX.YY_beta' tag, which is only created
937+ if it doesn't exist.
938+
939+ -d|--debug Print debug statements. This includes printing
940+ the authentication token if using Dockerhub.
941+
942+ -- IMAGE_1 IMAGE_2 ... IMAGE_N Images to tag. If not specified,
943+ the script will obtain the list of
944+ all images from REGISTRY/NAMESPACE
945+ and tag them.
946+
947+ -h|--help Display this usage
948+EOF
949+}
950+
951+# Tag the 'ubuntu' base image.
952+#
953+# We perform the tagging of '_stable', '_beta', '_candidate', 'XX.YY'
954+# and 'RELEASE_NAME'.
955+do_tag_base_ubuntu_image ()
956+{
957+ local image="ubuntu"
958+ local -a TAG_SUFFIXES=( "stable" "beta" "candidate" )
959+
960+ if [ "$REGISTRY" != "aws" ]; then
961+ error "The 'ubuntu' base image is only present in the 'aws' registry."
962+ exit 1
963+ fi
964+
965+ local -a IMAGE_TAGS EDGE_TAGS
966+
967+ readarray -t IMAGE_TAGS < <("${progdir}./list-tags-for-image.sh" \
968+ -a "$AUTH_TOKEN" \
969+ -r "$REGISTRY" \
970+ -n "$NAMESPACE" \
971+ -i "$image")
972+
973+ readarray -t EDGE_TAGS < <(printf '%s\n' "${IMAGE_TAGS[@]}" \
974+ | grep '_edge$')
975+
976+ for edgetag in "${EDGE_TAGS[@]}"; do
977+ # We support retagging images tagged as DISTRONAME-XX.YY_edge.
978+ local EDGE_TAG_PREFIX
979+ EDGE_TAG_PREFIX=$(sed -ne 's@\([[:alpha:]]\+-[[:digit:]]\+\.[[:digit:]]\+\)_edge@\1@p' <<< "$edgetag")
980+ if [ -z "$EDGE_TAG_PREFIX" ]; then
981+ warn "Unable to obtain prefix for tag '$edgetag'."
982+ continue
983+ fi
984+
985+ local IMG_DISTRONAME
986+ local IMG_RELEASEVER
987+
988+ IMG_DISTRONAME=$(cut -d'-' -f1 <<< "$EDGE_TAG_PREFIX")
989+ IMG_RELEASEVER=$(cut -d'-' -f2 <<< "$EDGE_TAG_PREFIX")
990+ for tagsuffix in "${TAG_SUFFIXES[@]}"; do
991+ # Tag each suffix.
992+ if [ -n "$FORCE" ] || ! grep -Fwq "${IMG_RELEASEVER}_${tagsuffix}" <<< "${IMAGE_TAGS[@]}"; then
993+ info "Invoking multi-arch tagger for ${NAMESPACE}/${image}:${IMG_RELEASEVER}_${tagsuffix} (source tag: ${EDGE_TAG_PREFIX}_edge) (on ${REGISTRY})"
994+ "${progdir}./multi-arch-tagger.sh" \
995+ -a "$AUTH_TOKEN" \
996+ -r "$REGISTRY" \
997+ -n "$NAMESPACE" \
998+ -i "$image" \
999+ -s "${EDGE_TAG_PREFIX}_edge" \
1000+ -t "${IMG_RELEASEVER}_${tagsuffix}" \
1001+ -u "$USERNAME" \
1002+ -p "$PASSWORD"
1003+ fi
1004+ done
1005+
1006+ # Tag the DISTRONAME.
1007+ if [ -n "$FORCE" ] || ! grep -Fwq "${IMG_DISTRONAME}" <<< "${IMAGE_TAGS[@]}"; then
1008+ info "Invoking multi-arch tagger for ${NAMESPACE}/${image}:${IMG_DISTRONAME} (source tag: ${EDGE_TAG_PREFIX}_edge) (on ${REGISTRY})"
1009+ "${progdir}./multi-arch-tagger.sh" \
1010+ -a "$AUTH_TOKEN" \
1011+ -r "$REGISTRY" \
1012+ -n "$NAMESPACE" \
1013+ -i "$image" \
1014+ -s "${EDGE_TAG_PREFIX}_edge" \
1015+ -t "${IMG_DISTRONAME}" \
1016+ -u "$USERNAME" \
1017+ -p "$PASSWORD"
1018+ fi
1019+
1020+ # Tag the RELEASEVER
1021+ if [ -n "$FORCE" ] || ! grep -Fwq "${IMG_RELEASEVER}" <<< "${IMAGE_TAGS[@]}"; then
1022+ info "Invoking multi-arch tagger for ${NAMESPACE}/${image}:${IMG_RELEASEVER} (source tag: ${EDGE_TAG_PREFIX}_edge) (on ${REGISTRY})"
1023+ "${progdir}./multi-arch-tagger.sh" \
1024+ -a "$AUTH_TOKEN" \
1025+ -r "$REGISTRY" \
1026+ -n "$NAMESPACE" \
1027+ -i "$image" \
1028+ -s "${EDGE_TAG_PREFIX}_edge" \
1029+ -t "${IMG_RELEASEVER}" \
1030+ -u "$USERNAME" \
1031+ -p "$PASSWORD"
1032+ fi
1033+ done
1034+}
1035+
1036+do_tag_images ()
1037+{
1038+ # Tag images using the 'latest' tag.
1039+ for image in "${IMAGES[@]}"; do
1040+ info "Invoking multi-arch tagger for ${NAMESPACE}/${image}:latest (source tag: ${SOURCE_TAG}) (on ${REGISTRY})"
1041+ "${progdir}./multi-arch-tagger.sh" \
1042+ -a "$AUTH_TOKEN" \
1043+ -r "$REGISTRY" \
1044+ -n "$NAMESPACE" \
1045+ -i "$image" \
1046+ -s "$SOURCE_TAG" \
1047+ -t "latest" \
1048+ -u "$USERNAME" \
1049+ -p "$PASSWORD"
1050+ done
1051+
1052+ # Now comes the fun part. We have to determine whether the image
1053+ # needs a 'M.N-XX.YY_beta' tag, and which number should N, M, XX
1054+ # and YY be.
1055+ #
1056+ # Basically, if there is a tag named 'M.N-XX.YY_edge' without a
1057+ # corresponding '_beta' tag, then we assume that the image will
1058+ # need to receive the '_beta' tag. This is the usual case
1059+ # (currently) with images built using Launchpad.
1060+ for image in "${IMAGES[@]}"; do
1061+ if [ "$image" = "ubuntu" ]; then
1062+ # The ubuntu image is special.
1063+ do_tag_base_ubuntu_image
1064+ continue
1065+ fi
1066+
1067+ local -a IMAGE_TAGS EDGE_TAGS
1068+
1069+ readarray -t IMAGE_TAGS < <("${progdir}./list-tags-for-image.sh" \
1070+ -a "$AUTH_TOKEN" \
1071+ -r "$REGISTRY" \
1072+ -n "$NAMESPACE" \
1073+ -i "$image")
1074+
1075+ readarray -t EDGE_TAGS < <(printf '%s\n' "${IMAGE_TAGS[@]}" \
1076+ | grep '_edge$' | sort -r)
1077+
1078+ for edgetag in "${EDGE_TAGS[@]}"; do
1079+ local EDGE_TAG_PREFIX
1080+ EDGE_TAG_PREFIX=$(sed -ne 's@\([[:digit:]]\+\(\.[[:digit:]]\+\)\?-[[:digit:]]\+\.[[:digit:]]\+\)_edge@\1@p' <<< "$edgetag")
1081+ if [ -z "$EDGE_TAG_PREFIX" ]; then
1082+ warn "Unable to obtain prefix for tag '$edgetag'."
1083+ continue
1084+ fi
1085+ if [ -n "$FORCE" ] || ! grep -Fwq "${EDGE_TAG_PREFIX}_beta" <<< "${IMAGE_TAGS[@]}"; then
1086+ # If we're here, it means that there isn't a
1087+ # corresponding 'M.N-XX.YY_beta' tag associated with
1088+ # the '_edge' tag, so we need to create it.
1089+ info "Invoking multi-arch tagger for ${NAMESPACE}/${image}:${EDGE_TAG_PREFIX}_beta (source tag: ${EDGE_TAG_PREFIX}_edge) (on ${REGISTRY})"
1090+ "${progdir}./multi-arch-tagger.sh" \
1091+ -a "$AUTH_TOKEN" \
1092+ -r "$REGISTRY" \
1093+ -n "$NAMESPACE" \
1094+ -i "$image" \
1095+ -s "${EDGE_TAG_PREFIX}_edge" \
1096+ -t "${EDGE_TAG_PREFIX}_beta" \
1097+ -u "$USERNAME" \
1098+ -p "$PASSWORD"
1099+ fi
1100+ done
1101+ done
1102+}
1103+
1104+validate_args ()
1105+{
1106+ local ret=0
1107+
1108+ validate_var REGISTRY || ret=1
1109+ validate_var NAMESPACE || ret=1
1110+ validate_var SOURCE_TAG || ret=1
1111+
1112+ validate_registry "$REGISTRY" || ret=1
1113+ validate_namespace "$NAMESPACE" || ret=1
1114+
1115+ if [ "$ret" -eq 1 ]; then
1116+ error "Failed to validate arguments."
1117+ exit 1
1118+ fi
1119+
1120+ do_login
1121+
1122+ # Fill the ALL_IMAGES array.
1123+ readarray -t ALL_IMAGES < <("${progdir}./list-all-images.sh" \
1124+ -r "$REGISTRY" \
1125+ -n "$NAMESPACE" \
1126+ -a "$AUTH_TOKEN")
1127+
1128+ if [ "${#IMAGES[@]}" -eq 0 ]; then
1129+ # The user hasn't provided any image names, so just use all of
1130+ # them.
1131+ IMAGES=( "${ALL_IMAGES[@]}" )
1132+ else
1133+ # Validate the image names the user has provided.
1134+ for img in "${IMAGES[@]}"; do
1135+ validate_image "$img" || exit 1
1136+ done
1137+ fi
1138+}
1139+
1140+if [ $# -lt 2 ]; then
1141+ error "You need to provide at least the registry (-r), the namespace (-n), and the source tag (-s)."
1142+ usage
1143+ exit 1
1144+fi
1145+
1146+while [ -n "$1" ]; do
1147+ case "$1" in
1148+ "-r"|"--registry")
1149+ REGISTRY="$2"
1150+ shift 2
1151+ ;;
1152+
1153+ "-n"|"--namespace")
1154+ NAMESPACE="$2"
1155+ shift 2
1156+ ;;
1157+
1158+ "-s"|"--source-tag")
1159+ SOURCE_TAG="$2"
1160+ shift 2
1161+ ;;
1162+
1163+ "-f"|"--force")
1164+ FORCE=1
1165+ shift
1166+ ;;
1167+
1168+ "-d"|"--debug")
1169+ export DEBUG=1
1170+ shift
1171+ ;;
1172+
1173+ "-h"|"--help")
1174+ usage
1175+ exit 0
1176+ ;;
1177+
1178+ "--")
1179+ shift
1180+ while [ -n "$1" ]; do
1181+ IMAGES+=( "$1" )
1182+ shift
1183+ done
1184+ ;;
1185+
1186+ *)
1187+ error "Invalid argument '$1'."
1188+ usage
1189+ exit 1
1190+ ;;
1191+ esac
1192+done
1193+
1194+validate_args
1195+
1196+do_tag_images

Subscribers

People subscribed via source and target branches

to all changes: