...

Text file src/github.com/emissary-ingress/emissary/v3/docker/base-python.docker.gen

Documentation: github.com/emissary-ingress/emissary/v3/docker

     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