Merge ~ubuntu-docker-images/ubuntu-docker-images/+git/utils:update-and-check-images into ~ubuntu-docker-images/ubuntu-docker-images/+git/utils:master

Proposed by Bryce Harrington
Status: Rejected
Rejected by: Bryce Harrington
Proposed branch: ~ubuntu-docker-images/ubuntu-docker-images/+git/utils:update-and-check-images
Merge into: ~ubuntu-docker-images/ubuntu-docker-images/+git/utils:master
Diff against target: 391 lines (+379/-0)
2 files modified
software-versions.sh (+229/-0)
update-images.sh (+150/-0)
Reviewer Review Type Date Requested Status
Sergio Durigan Junior Needs Fixing
Lucas Kanashiro Needs Information
Review via email: mp+398323@code.launchpad.net

Description of the change

Towards the objective of flagging merge opportunities for OCI image updates, and to assist in mechanizing at least the simpler refreshes, this adds a pair of scripts:

update-images.sh handles the mechanics of updating the Dockerfile, building the image, and running the unit tests for verification. It uploads the images to the packager's dockerhub repos, but does not push the images to production (that's a job for push-images.sh). The packager is also responsible for pushing the git branch and filing the MP.

check-images.sh extracts version numbers from the official images, as well as local images such as generated from update-images.sh. This first version of the script is limited to five packages, so will need further development to make it more broadly useful.

I'm not wedded to these names for the scripts.

These have been developed rather independently from push-images.sh, however there are some similar functional needs between all of them. I expect that the commonly used functionality could be broken out to a shared include file. There are likely other adjustments that can be made to make all the scripts consistent with code style, variable naming, etc. as followup work.

To post a comment you must log in.
Revision history for this message
Lucas Kanashiro (lucaskanashiro) wrote :

Thanks for the scripts Bryce. I did not take a deep look into your implementation but based on your description I would say that update-images.sh does not need to upload the image to DockerHub under the packager's namespace, you could simply update the image locally and run the tests against the local image. I can see no gain in uploading it to a personal DockerHub repository, do you have a reason to do it?

review: Needs Information
Revision history for this message
Bryce Harrington (bryce) wrote :

The reason was just in case it facilitated easier testing during review. However, if not then I agree that's probably out of scope for this script.

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

I agree with Lucas: we can run the tests locally, so the part about pushing the tags to a developer namespace seems like it's not needed.

I have a few comments/questions about the script, but other than that it looks great. Thanks for doing it.

review: Needs Fixing
Revision history for this message
Bryce Harrington (bryce) wrote :

Discussion on the comments below.

I've dropped the push statement. I'll work on the function codedocs and other refinements for the next rev.

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

On Fri, Feb 19, 2021 at 10:19:00PM -0000, Sergio Durigan Junior wrote:
> > +
> > + cd "${REPOS_DIR}/${software_name}" \
> > + || die "Could not chdir to ${REPOS_DIR}/${software_name}"
> > +
> > + git checkout -f edge
>
> Ops, scratch that. Of course you will need to be inside a valid branch to create another one from it. I understand why you're using edge here now. But just as the edge tag will move as we update the images, I think the branch ought to move as well. Maybe the way to go here is to recreate the edge branch off of the current one, once the script finishes the update.
>

Yeah, like I mentioned I expect this section of code will be expanded on
as we sort out how we want to handle branch updates, but I'm not sure
how this should work yet. Next time we do updates to these branches
we'll need to majorly rework this section.

39a0596... by Bryce Harrington

check-images: Add support for updating redis

2649b8f... by Bryce Harrington

update-images: Resync shared functions with check-images

b76c036... by Bryce Harrington

Rename check-images.sh to software-versions.sh and make it sourceable

Also document each of the routines

6542054... by Bryce Harrington

Make update-versions.sh source software-versions.sh

This allows avoiding code duplication. Routines pertaining to software
version determination are consolodated to software-versions.sh.

Also document each of the routines.

c86e57d... by Bryce Harrington

update-images.sh: Don't create new branches

The original code handled the rename of 'edge' branches to the new
naming scheme. Now that this renaming is completed, there is no need
for this logic.

For now, simply expect that the branch we are going to work in already
exists. This might need further development to create new branches when
there are new major versions of software, but for now assume we'll
handle those situations manually.

f3edcd8... by Bryce Harrington

update-images.sh: Also refresh the docs

Revision history for this message
Bryce Harrington (bryce) wrote :

I've reworked both scripts a fair bit, and added code docs as requested.

Both scripts still run (for me at least). The software-versions.sh script outputs:

Software Distro Current-Image Proposed-Image
apache2 2.4.46-2ubuntu1 2.4.41-4ubuntu3.1 2.4.46-2ubuntu1
memcached 1.6.9+dfsg-1 1.6.9+dfsg-1 1.6.9+dfsg-1
mysql-8.0 8.0.23-3build1 8.0.23-3build1 8.0.23-1ubuntu2
nginx 1.18.0-6ubuntu4 1.18.0-6ubuntu4 -
postgresql-13 13.2-1 - -
redis 5:6.0.11-1 5:6.0.9-4 -

I don't consider either script 'finished' by any stretch, but this is a good checkpoint for review purposes.

Future work includes:
* Add support for telegraf(?) and the snap-based images(???)
* More error checking
* Use log()/error() from push-images.sh instead of die()
* Output version data as JSON (for feeding into merge opportunity report)
* Ability to update branches for stable Ubuntu releases too (i.e. not just the devel release)
* Ability to appropriately create branches as needed for new major versions
* Less hard-coding of package-specific information, perhaps by relying on data/<package>.yaml
* Add use of getopts and better usage()

Revision history for this message
Sergio Durigan Junior (sergiodj) :
3eb648b... by Bryce Harrington

software-versions.sh: Special-case postgres' software version

Minor version bumps of postgres are allowed in stable Ubuntu releases,
per security team policy. Thus for tagging OCI images we should
reference only the major version in this particular case.

Revision history for this message
Bryce Harrington (bryce) wrote :

I've done a reimplementation on software-versions.sh, with the goal of establishing a better git history for review, and to further generalize handling of package-specific stuff. I'll reintroduce this script with a new MP.

Regarding the update-images.sh script, there has been a bit of fluidity in tag naming policies and in our requirements for how things need updated. Due to this I don't feel we're at a point we can really nail down the requirements for this script. I'm going to withdraw this script for now; it can be reimplemented and reintroduced when the maintenance situation is more stable.

Unmerged commits

3eb648b... by Bryce Harrington

software-versions.sh: Special-case postgres' software version

Minor version bumps of postgres are allowed in stable Ubuntu releases,
per security team policy. Thus for tagging OCI images we should
reference only the major version in this particular case.

f3edcd8... by Bryce Harrington

update-images.sh: Also refresh the docs

c86e57d... by Bryce Harrington

update-images.sh: Don't create new branches

The original code handled the rename of 'edge' branches to the new
naming scheme. Now that this renaming is completed, there is no need
for this logic.

For now, simply expect that the branch we are going to work in already
exists. This might need further development to create new branches when
there are new major versions of software, but for now assume we'll
handle those situations manually.

6542054... by Bryce Harrington

Make update-versions.sh source software-versions.sh

This allows avoiding code duplication. Routines pertaining to software
version determination are consolodated to software-versions.sh.

Also document each of the routines.

b76c036... by Bryce Harrington

Rename check-images.sh to software-versions.sh and make it sourceable

Also document each of the routines

2649b8f... by Bryce Harrington

update-images: Resync shared functions with check-images

39a0596... by Bryce Harrington

check-images: Add support for updating redis

087c41b... by Bryce Harrington

Add initial version of check-images.sh script

This script can be used to examine the versions of software installed in
the docker images. Currently, this script only works with apache2,
memcached, mysql, nginx, and postgres, and has only been lightly tested
even with them.

Future work includes cli support to allow specifying the software to
be examined, better error checking, and ability to output in some
structured file format (i.e. JSON and/or YAML). Better command line
argument handling would likely also help...

Further, this copies some functionality from update-images.sh that
should be moved into a common file and sourced.

7638fb8... by Bryce Harrington

Add preliminary script 'update-images.sh'

This script is intended to facilitate simple refreshes of docker images
using the version in the current devel release of Ubuntu. This has only
been tested against apache2, memcached, mysql, nginx, and postgres.
Other images might work, but YMMV.

In particular, if adjustments need to be made to the Dockerfile to
facilitate the new software, this script probably won't be sufficient to
do the update.

Note that the docker image name, test script name, source package name,
and binary package name for a given piece of software may be
considerably different. To address that, we just brute force hack
around it by hardcoding some name translations; hopefully later on this
can be done better.

In particular, some software names include major version numbers
(e.g. to indicate API levels), such as mysql-8.0 and postgresql-13.
This script includes some logic to figure these numbers out, which may
or may not work more broadly. Again, it's hoped this can be replaced by
something more robust eventually.

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1diff --git a/software-versions.sh b/software-versions.sh
2new file mode 100755
3index 0000000..4172ce8
4--- /dev/null
5+++ b/software-versions.sh
6@@ -0,0 +1,229 @@
7+#!/usr/bin/env bash
8+# -*- coding: utf-8; mode: sh -*-
9+
10+# software-versions.sh: Determine versions of packages installed in OCI images
11+
12+progname=$(basename "${0}")
13+progdir=$(dirname "$(readlink -f "${0}")")
14+
15+if [ -z "${DOCKER_USER}" ]; then
16+ DOCKER_USER=$(docker info | grep Username: | cut -d: -f2 | xargs)
17+fi
18+if [ -z "${DISTRO_CODENAME}" ]; then
19+ DISTRO_CODENAME=$(distro-info --devel)
20+fi
21+
22+die() {
23+ echo "Error: ${*}" 1>&2
24+ exit 1
25+}
26+
27+
28+# Converts a package version number to equivalent upstream version.
29+# Strips off epoch, if any, and drops the debian version.
30+#
31+# :param v: Package version number to convert
32+upstream_version() {
33+ local v="${1#*:}"
34+ echo "${v%-*}"
35+}
36+
37+# Look up version of package in a given Ubuntu release
38+#
39+# @param source_package_name: Name of the source package to look up
40+get_source_package_version() {
41+ local source_package_name="${1}"
42+
43+ rmadison -s "${DISTRO_CODENAME}" "${source_package_name}" | cut -d\| -f2 | xargs
44+}
45+
46+# Provides the "version family" of the software
47+#
48+# For a given Ubuntu stable release, updates will be limited to certain
49+# ranges of version numbers based on API and stability policies. For
50+# example, some may get patch-level updates (i.e. M.N.1->M.N.2->M.N.3)
51+# and thus have an "M.N" version family, others may get minor-level
52+# updates (such as postrgres, which in focal had 12.4->12.5->12.6) so
53+# have an "M" version family.
54+#
55+# This routine determines this from the debian version by extracting the
56+# first two numbers from upstream version portion.
57+#
58+# @param software_name: Formal name of the upstream software
59+get_source_package_major() {
60+ # TODO: This routine should be named get_software_version_family
61+ # or get_software_main_version or something similar
62+ local software_name="${1}"
63+ local package_version=""
64+
65+ # For apache2, just return 2.4
66+ if [ "${software_name}" = "apache2" ]; then
67+ package_version=$(get_source_package_version apache2)
68+
69+ # For postgres return 12, 13, etc.
70+ elif [ "${software_name}" = "postgres" ]; then
71+ pg_api_version=$(get_source_package_version postgresql)
72+ pg_api_package="postgresql-${pg_api_version%+*}"
73+ package_version=$(get_source_package_version "${pg_api_package}")
74+
75+ # For mysql return 5.7, 8.0, etc.
76+ elif [ "${software_name}" = "mysql" ]; then
77+ package_version=$(get_source_package_version mysql-server)
78+
79+ elif [ "${software_name}" = "redis" ]; then
80+ package_version=$(get_source_package_version redis-server)
81+ fi
82+
83+ if [ -n "${package_version}" ]; then
84+ version=$(upstream_version "${package_version}")
85+ local v=( ${version//./ } )
86+ local version_major="${v[0]}"
87+ local version_minor="${v[1]}"
88+ if [ "${software_name}" = "postgres" ]; then
89+ echo "${version_major}"
90+ else
91+ echo "${version_major}.${version_minor}"
92+ fi
93+ fi
94+
95+ # For everything else, return blank
96+ echo ""
97+}
98+
99+# Determine the source package for the given software name
100+#
101+# In many cases, the software name is identical to the source package's
102+# name, however there are cases where the spelling may vary
103+# (e.g. postgres vs. postgresql), or where the source package includes
104+# part of the version number. This routine does this mapping.
105+#
106+# @param software_name: Formal name of the upstream software
107+get_software_source_package() {
108+ local software_name="${1}"
109+ local source_package_name=""
110+
111+ # Look to see if there is a <software>-N.M package
112+ local package_major=$(get_source_package_major "${software_name}")
113+ if [ -n "${package_major}" ]; then
114+ local prefix="${software_name}"
115+ if [ "${software_name}" = "postgres" ]; then
116+ prefix="postgresql"
117+ fi
118+
119+ source_package_name=$(rmadison -s "${DISTRO_CODENAME}" "${prefix}-${package_major}" | cut -d\| -f1 | xargs)
120+ if [ -n "${source_package_name}" ] && [ "${source_package_name}" != "${prefix}-${package_major}" ]; then
121+ die "error: ${source_package_name} is not ${prefix}-${package_major} as expected"
122+ fi
123+ fi
124+
125+ # If not, default to just <software>
126+ if [ -z "${source_package_name}" ]; then
127+ source_package_name="${software_name}"
128+ fi
129+
130+ echo "${source_package_name}"
131+ return 0
132+}
133+
134+# Get primary binary package for given software
135+#
136+# For some software, the binary package names differ from the source
137+# package name, so when checking installed versions we need to be careful
138+# to use the appropriate binary package name. For now, we just manually
139+# translate a few specific cases.
140+#
141+# @param software_name: Formal name of the upstream software
142+get_software_binary_package() {
143+ local software_name="${1}"
144+ local package_major=$(get_source_package_major "${software_name}")
145+
146+ if [ "${software_name}" = "mysql" ]; then
147+ # mysql-client-core-N and mysql-client-N are also installed
148+ echo "mysql-server-core-${package_major}"
149+ elif [ "${software_name}" = "postgres" ]; then
150+ echo "postgresql-${package_major}"
151+ elif [ "${software_name}" = "nginx" ]; then
152+ # nginx-common is also installed
153+ echo "nginx-full"
154+ elif [ "${software_name}" = "redis" ]; then
155+ echo "redis-server"
156+ else
157+ echo "${software_name}"
158+ fi
159+}
160+
161+# Lookup a binary package version present in a given Docker image
162+#
163+# Uses apt-cache policy to determine installed version of a specific
164+# binary package.
165+#
166+# @param image_id: The name of the image to examine
167+# @param binary_package_name: The binary package to look up version info
168+get_image_installed_package_version() {
169+ local image_id="${1}"
170+ local binary_package_name="${2}"
171+
172+ [ -n "${image_id}" ] || return 1
173+
174+ docker run --rm \
175+ --name "${image_id}-version-check" \
176+ "${image_id}" \
177+ apt-cache policy "${binary_package_name}" \
178+ | grep "Installed:" | cut -d: -f2- | xargs
179+}
180+
181+# Display a table entry for the given software
182+#
183+# This is an aide for summarizing versions of software packages in the
184+# distro, vs. what's in the existing ubuntu-namespaced Docker images.
185+# It also displays versions of the package in the user's namespace
186+# as a convenience for development work.
187+#
188+# @param software_name: Formal name of the upstream software
189+get_software_versions() {
190+ # For given software_name:
191+ # Create container from the image
192+ # Log into the container
193+ # Retrieve the version for software_name
194+ software_name="${1}"
195+ source_package_name="$(get_software_source_package ${software_name})"
196+ binary_package_name="$(get_software_binary_package ${software_name})"
197+ proposed_image_name="${DOCKER_USER}/${software_name}"
198+ current_image_name="ubuntu/${software_name}"
199+
200+ proposed_image_id=$(docker image ls | grep ^${proposed_image_name} | grep edge | awk '{print $3}' | uniq)
201+ current_image_id=$(docker image ls | grep ^${current_image_name} | grep edge | awk '{print $3}' | uniq)
202+ package_version_distro=$(get_source_package_version "${source_package_name}")
203+ package_version_current=$(get_image_installed_package_version "${current_image_id}" "${binary_package_name}")
204+ package_version_proposed=$(get_image_installed_package_version "${proposed_image_id}" "${binary_package_name}")
205+
206+ printf "%-20s %-30s %-30s %-30s\n" "${source_package_name}" \
207+ "${package_version_distro:--}" "${package_version_current:--}" "${package_version_proposed:--}"
208+}
209+
210+# Tabular report of Canonical Server's debian-based docker images
211+#
212+# Creates a summary of package version numbers for software installed
213+# in docker images that the canonical server team maintains.
214+display_version_report() {
215+ docker image pull ubuntu/apache2:edge
216+ docker image pull ubuntu/memcached:edge
217+ docker image pull ubuntu/mysql:edge
218+ docker image pull ubuntu/nginx:edge
219+ docker image pull ubuntu/postgres:edge
220+ docker image pull ubuntu/redis:edge
221+
222+ printf "%-20s %-30s %-30s %-30s\n" "Software" "Distro" "Current-Image" "Proposed-Image"
223+ get_software_versions "apache2"
224+ get_software_versions "memcached"
225+ get_software_versions "mysql"
226+ get_software_versions "nginx"
227+ get_software_versions "postgres"
228+ get_software_versions "redis"
229+}
230+
231+# If we're being run directly (i.e. not sourced), then
232+# print out the current software versions.
233+if [[ "${0}" == "${BASH_SOURCE[0]}" ]]; then
234+ display_version_report
235+fi
236diff --git a/update-images.sh b/update-images.sh
237new file mode 100755
238index 0000000..ecea08a
239--- /dev/null
240+++ b/update-images.sh
241@@ -0,0 +1,150 @@
242+#!/usr/bin/env bash
243+# -*- coding: utf-8; mode: sh -*-
244+
245+# update-images.sh: Refresh software versions for Ubuntu's OCI images
246+
247+progname=$(basename "${0}")
248+progdir=$(dirname "$(readlink -f "${0}")")
249+
250+# shellcheck source=/dev/null disable=SC1090
251+source "${progdir}/software-versions.sh"
252+
253+if ! groups | grep docker > /dev/null; then
254+ echo "Error: Must be in docker group" 1>&2
255+ exit 1
256+fi
257+
258+if [ -z "${TESTS_DIR}" ]; then
259+ TESTS_DIR="${progdir}/../server-test-scripts/oci-unit-tests"
260+fi
261+if [ -z "${REPOS_DIR}" ]; then
262+ REPOS_DIR="${progdir}/../"
263+fi
264+
265+usage() {
266+ cat >&2 <<EOF
267+Usage: ${progname} <package-name> [package-name ...]
268+
269+EOF
270+ # shellcheck source=/dev/null disable=SC2086
271+ exit "${1}"
272+}
273+
274+# Modifies contents of the Dockerfile in the current directory
275+#
276+# @param software_name: Formal name of the upstream software for the Dockerfile
277+update_dockerfile_parameters() {
278+ local software_name="${1}"
279+ local dockerfile="./Dockerfile"
280+
281+ # TODO: Given that we're in the git repository, can we determine the
282+ # software_name from just that? If so, then this can run without
283+ # argument $1.
284+
285+ if [ ! -e "${dockerfile}" ]; then
286+ die "error: Dockerfile ${dockerfile} not found at ${PWD}"
287+ fi
288+
289+ # Update global parameters
290+ sed -i "/FROM ubuntu:/c\FROM ubuntu:${DISTRO_CODENAME}" "${dockerfile}"
291+
292+ # Update per-package parameters
293+ if [ "${software_name}" = "postgres" ]; then
294+ pg_major=$(get_source_package_major postgresql)
295+ sed -i "/ENV PG_MAJOR/c\ENV PG_MAJOR ${pg_major}" "${dockerfile}"
296+ fi
297+
298+ return 0
299+}
300+
301+# Creates branch for a new Ubuntu distro series and updates package to match
302+#
303+# This routine can be run to create an updated image for a new version
304+# of Ubuntu. It sets the Dockerfile to the new Ubuntu version, and
305+# rebuilds it with the version of the software's package from that
306+# release.
307+#
308+# @param software_name: Formal name of the upstream software for the Dockerfile
309+update_image() {
310+ [ -n "${1}" ] \
311+ || usage 1
312+
313+ local software_name="${1}"
314+ local package_name=$(get_software_source_package "${software_name}")
315+ local package_version=$(get_source_package_version "${package_name}")
316+ local package_major=$(get_source_package_major "${software_name}")
317+ local upstream_version=$(upstream_version "${package_version}")
318+ # TODO: Remove +dfsg
319+ local v=( ${upstream_version//./ } )
320+ local version_major="${v[0]}"
321+ local version_minor="${v[1]}"
322+ local docker_image="${DOCKER_USER}/${software_name}:21.04"
323+ local test_script="${TESTS_DIR}/${software_name}_test.sh"
324+ local distro_release=$(distro-info --series "${DISTRO_CODENAME}" -r)
325+ local branch_name="${version_major}.${version_minor}-${distro_release}"
326+
327+ # Hack, due to postgres/postgresql discrepancy
328+ if [ "${software_name}" = "postgres" ]; then
329+ test_script="${TESTS_DIR}/postgresql_test.sh"
330+ fi
331+
332+ echo
333+ echo "########################################################"
334+ echo "# distro: ${DISTRO_CODENAME} (${distro_release})"
335+ echo "# software: ${software_name} -> ${package_name} ${package_major}"
336+ echo "# docker_image: ${docker_image}"
337+ echo "# test_script: ${test_script}"
338+ echo "# version: ${package_version}"
339+ echo "# upstream: ${upstream_version}"
340+ echo "# branch_name: ${branch_name}"
341+ echo "########################################################"
342+
343+ cd "${REPOS_DIR}/${software_name}" \
344+ || die "Could not chdir to ${REPOS_DIR}/${software_name}"
345+
346+ git checkout "${branch_name}" \
347+ || die "Could not checkout ${branch_name} from git"
348+
349+ # Update Dockerfile parameters
350+ echo
351+ echo "### Updating Dockerfile ###"
352+ update_dockerfile_parameters "${software_name}"
353+
354+ # Update the docs
355+ make all-doc \
356+ || die "Could not update the documentation"
357+
358+ # Build the image
359+ echo
360+ echo "### Building Image ###"
361+ docker build --tag "${docker_image}" . \
362+ || die "Failed to build ${docker_image} for ${package_name}"
363+
364+ git commit -a -m "Update docker image to ${package_version} for ${DISTRO_CODENAME}"
365+
366+ # Run the test
367+ echo
368+ echo "### Testing Image ###"
369+ if [ ! -e "${test_script}" ]; then
370+ die "Test script for ${software_name} not found at ${test_script}"
371+ fi
372+ cd "${TESTS_DIR}" \
373+ || die "Could not chdir to ${TESTS_DIR}"
374+ DOCKER_IMAGE="${docker_image}" bash ${test_script} \
375+ || die "Failed ${docker_image}'s test for ${package_name}"
376+
377+ exit 0
378+}
379+
380+if [[ "${0}" == "${BASH_SOURCE[0]}" ]]; then
381+ [ -n "${1}" ] \
382+ || usage 1
383+
384+ for package_name in ${@}; do
385+ echo "Updating ${package_name}"
386+ update_image "${package_name}"
387+ echo
388+ echo "---"
389+ echo
390+ done
391+fi

Subscribers

People subscribed via source and target branches

to all changes: