...
1#!/usr/bin/env bash
2set -euE
3
4# Usage: ./docker/base-python.docker.gen > docker/.base-python.docker.stamp
5#
6# base-python.docker.gen is essentially just a 4 line script:
7#
8# iidfile=$(mktemp)
9# trap 'rm -f "$iidfile"' EXIT
10# docker build --iidfile="$iidfile" docker/base-python >&2
11# cat "$iidfile"
12#
13# However, it has "optimizations" because that `docker build` is
14# really slow and painful:
15#
16# 0. (not an speed improvement itself, but nescessary for what
17# follows) generate a deterministic Docker tag based on the
18# inputs to the image; a sort of content-addressable scheme that
19# doesn't rely on having built the image first.
20#
21# 1. Rather than building the image locally, try to pull it
22# pre-build from any of the following Docker repos:
23#
24# - $BASE_PYTHON_REPO
25# - ${DEV_REGISTRY}/base-python
26# - docker.io/emissaryingress/base-python
27#
28# 2. If we do build it locally (because it couldn't be pulled), then
29# try pushing it to those Docker repos, so that
30# others/our-future-self can benefit from (1).
31
32OFF=''
33BLD=''
34RED=''
35GRN=''
36BLU=''
37if tput setaf 0 &>/dev/null; then
38 OFF="$(tput sgr0)"
39 BLD="$(tput bold)"
40 RED="$(tput setaf 1)"
41 GRN="$(tput setaf 2)"
42 BLU="$(tput setaf 4)"
43fi
44
45msg() {
46 # shellcheck disable=SC2059
47 printf "${BLU} => [${0##*/}]${OFF} $1${OFF}\n" "${@:2}" >&2
48}
49
50stat_busy() {
51 # shellcheck disable=SC2059
52 printf "${BLU} => [${0##*/}]${OFF} $1...${OFF}" "${@:2}" >&2
53}
54
55stat_done() {
56 # shellcheck disable=SC2059
57 printf " ${1:-done}${OFF}\n" >&2
58}
59
60statnl_busy() {
61 stat_busy "$@"
62 printf '\n' >&2
63}
64
65statnl_done() {
66 # shellcheck disable=SC2059
67 printf "${BLU} => [${0##*/}]${OFF} ...${1:-done}${OFF}\n" >&2
68}
69
70error() {
71 # shellcheck disable=SC2059
72 printf "${RED} => [${0##*/}] ${BLD}error:${OFF} $1${OFF}\n" "${@:2}" >&2
73}
74
75# Usage: tag=$(print-tag)
76#
77# print-tag generates and prints a Docker tag (without the leading
78# "REPO:" part) for the image, based on the inputs to the image.
79#
80# The inputs we care about (i.e. the things that should trigger a
81# rebuild) are:
82#
83# - The `docker/base-python/Dockerfile` file.
84#
85# - Whatever unpinned remote 3rd-party resources that Dockerfile
86# pulls in (mostly the Alpine package repos); but because we don't
87# have the whole repos as a file on disk, we fall back to a
88# truncated timestamp. This means that we rebuild periodically to
89# make sure we don't fall too far behind and then get surprised
90# when a rebuild is required for Dockerfile changes.) We have
91# defined "enough time" as a few days. See the variable
92# "build_every_n_days" below.
93print-tag() {
94 python3 -c '
95import datetime, hashlib
96
97# Arrange these 2 variables to reduce the likelihood that build_every_n_days
98# passes in the middle of a CI workflow; have it happen weekly during the
99# weekend.
100build_every_n_days = 7 # Periodic rebuild even if Dockerfile does not change
101epoch = datetime.datetime(2020, 11, 8, 5, 0) # 1AM EDT on a Sunday
102
103age = int((datetime.datetime.now() - epoch).days / build_every_n_days)
104age_start = epoch + datetime.timedelta(days=age*build_every_n_days)
105
106dockerfilehash = hashlib.sha256(open("docker/base-python/Dockerfile", "rb").read()).hexdigest()
107
108print("%sx%s-%s" % (age_start.strftime("%Y%m%d"), build_every_n_days, dockerfilehash[:16]))
109'
110}
111
112main() {
113 local tag
114 tag=$(print-tag)
115
116 # `repos` is a list of Docker repos where the base-python image
117 # gets pulled-from/pushed-to.
118 #
119 # When pulling, we go down the list until we find a repo we
120 # can successfully pull from; returning after the first
121 # success; if we make through the list without a success, then
122 # we build the image locally.
123 #
124 # When pushing, we attempt to push to *every* repo, but ignore
125 # failures unless they *all* fail.
126 local repos=()
127
128 # add_repo REPO appends a repo to ${repos[@]}; if ${repos[@]}
129 # doesn't already contain REPO.
130 add_repo() {
131 local needle straw
132 needle="$1"
133 # The `${repos[@]:+…}` non-emptiness check seems
134 # pointless here, but it's important because macOS
135 # still has Bash 3.2, and prior to Bash 4.4 (Sept
136 # 2016), there was a bug where it would consider an
137 # empty array to be unset, triggering `set -u`. The
138 # other usages of "${repos[@]}" don't need this
139 # because this is the only one where it might still be
140 # empty.
141 for straw in ${repos[@]:+"${repos[@]}"}; do
142 if [[ "$straw" == "$needle" ]]; then
143 return
144 fi
145 done
146 repos+=("$needle")
147 }
148 if [[ -n "${BASE_PYTHON_REPO:-}" ]]; then
149 add_repo "$BASE_PYTHON_REPO"
150 fi
151 if [[ -n "${DEV_REGISTRY:-}" ]]; then
152 add_repo "${DEV_REGISTRY}/base-python"
153 fi
154 # We always include docker.io/emissaryingress/base-python as a
155 # fallback, because rebuilding orjson takes so long that we
156 # really want a cache-hit if at all possible.
157 add_repo 'docker.io/emissaryingress/base-python'
158
159 # Download
160 local id=''
161 for repo in "${repos[@]}"; do
162 stat_busy 'Checking if %q exists locally' "$repo:$tag"
163 if docker image inspect "$repo:$tag" &>/dev/null; then
164 stat_done "${GRN}yes"
165 id=$(docker image inspect "$repo:$tag" --format='{{.Id}}')
166 break
167 fi
168 stat_done "${RED}no"
169
170 stat_busy 'Checking if %q can be pulled' "$repo:$tag"
171 if docker pull "$repo:$tag" &>/dev/null; then
172 stat_done "${GRN}yes"
173 id=$(docker image inspect "$repo:$tag" --format='{{.Id}}')
174 break
175 fi
176 stat_done "${RED}no"
177 done
178
179 if [[ -z "$id" ]]; then
180 # Build
181 statnl_busy 'Building %q locally' "base-python:$tag"
182 iidfile=$(mktemp)
183 trap 'rm -f "$iidfile"' RETURN
184 docker build --iidfile="$iidfile" docker/base-python >&2
185 id=$(cat "$iidfile")
186 statnl_done 'done building'
187
188 # Push
189 pushed=0
190 for repo in "${repos[@]}"; do
191 statnl_busy 'Attempting to push %q' "$repo:$tag"
192 docker tag "$id" "$repo:$tag" >&2
193 if docker push "$repo:$tag" >&2; then
194 statnl_done "${GRN}pushed"
195 pushed=1
196 continue
197 fi
198 statnl_done "${RED}failed to push"
199 done
200 if ! (( pushed )); then
201 error "Could not push locally-built image to any remote repositories"
202 return 1
203 fi
204 fi
205
206 printf '%s\n' "$id"
207}
208
209main "$@"
View as plain text