|
| 1 | +#!/usr/bin/env bash |
| 2 | + |
| 3 | +# Host-side helpers for assembling a `docker build` command. |
| 4 | +# |
| 5 | +# Each function appends flags to a caller-owned bash array, passed by name. |
| 6 | +# docker_utils_append_args does the appending; see utils.sh for why it's written |
| 7 | +# the way it is. |
| 8 | + |
| 9 | +if [[ "${_DOCKER_BUILD_HOST_SH_LOADED:-}" == "1" ]]; then |
| 10 | + return 0 |
| 11 | +fi |
| 12 | +readonly _DOCKER_BUILD_HOST_SH_LOADED=1 |
| 13 | + |
| 14 | +# shellcheck source=exports/docker/utils.sh |
| 15 | +source "$(cd "$(dirname "${BASH_SOURCE[0]}")/.." &>/dev/null && pwd)/utils.sh" |
| 16 | + |
| 17 | +# Proxy variables forwarded into the build, in both spellings because tools |
| 18 | +# disagree about which they read. |
| 19 | +readonly _DOCKER_BUILD_PROXY_VARS=( |
| 20 | + HTTP_PROXY http_proxy |
| 21 | + HTTPS_PROXY https_proxy |
| 22 | + ALL_PROXY all_proxy |
| 23 | + NO_PROXY no_proxy |
| 24 | +) |
| 25 | + |
| 26 | +# Echoes <value> with any URL credentials replaced by <replacement> (removed when |
| 27 | +# that is omitted). |
| 28 | +# |
| 29 | +# Proxy URLs and git remotes both carry credentials in practice -- |
| 30 | +# `http://user:token@proxy.corp:8080`, `https://user:token@github.com/org/repo` -- |
| 31 | +# and this library would otherwise copy them into an image label or a build log, |
| 32 | +# where they outlive the build. |
| 33 | +# |
| 34 | +# Args: <value> [replacement] |
| 35 | +_docker_build_replace_userinfo() { |
| 36 | + local value="$1" replacement="${2:-}" |
| 37 | + # Greedy `.*://` so the last scheme wins, and `[^/@]*` keeps the match inside |
| 38 | + # the authority: an `@` later in a path is not credentials. |
| 39 | + if [[ "${value}" =~ ^(.*://)[^/@]*@(.*)$ ]]; then |
| 40 | + printf '%s%s%s' "${BASH_REMATCH[1]}" "${replacement}" "${BASH_REMATCH[2]}" |
| 41 | + return 0 |
| 42 | + fi |
| 43 | + printf '%s' "${value}" |
| 44 | +} |
| 45 | + |
| 46 | +# Appends `--build-arg` flags for any set proxy variables, and picks a network |
| 47 | +# mode. |
| 48 | +# |
| 49 | +# A proxy on the host's loopback address is unreachable from the build |
| 50 | +# container's default bridge network, so `--network host` is selected |
| 51 | +# automatically in that case. DOCKER_NETWORK overrides the choice entirely. |
| 52 | +# |
| 53 | +# Args: <cmd-array-name> |
| 54 | +docker_build_add_proxy_args() { |
| 55 | + if (( $# != 1 )) || [[ -z "$1" ]]; then |
| 56 | + echo >&2 "ERROR: docker_build_add_proxy_args requires a command array" |
| 57 | + return 2 |
| 58 | + fi |
| 59 | + local cmd_name="$1" |
| 60 | + |
| 61 | + local has_loopback_proxy=false |
| 62 | + local var value |
| 63 | + for var in "${_DOCKER_BUILD_PROXY_VARS[@]}"; do |
| 64 | + value="${!var:-}" |
| 65 | + [[ -z "${value}" ]] && continue |
| 66 | + docker_utils_append_args "${cmd_name}" "--build-arg" "${var}=${value}" |
| 67 | + # Match the host after either the scheme or userinfo credentials, so |
| 68 | + # http://user:pass@127.0.0.1:8080 is recognized too. Also accept a bare |
| 69 | + # trailing host with no port or path. |
| 70 | + if [[ "${var}" != *NO_PROXY* && "${var}" != *no_proxy* ]] \ |
| 71 | + && [[ "${value}" =~ (://|@)(localhost|127\.0\.0\.1|\[::1\])([:/]|$) ]]; then |
| 72 | + has_loopback_proxy=true |
| 73 | + fi |
| 74 | + done |
| 75 | + |
| 76 | + if [[ -n "${DOCKER_NETWORK:-}" ]]; then |
| 77 | + docker_utils_append_args "${cmd_name}" "--network" "${DOCKER_NETWORK}" |
| 78 | + elif [[ "${has_loopback_proxy}" == "true" ]]; then |
| 79 | + docker_utils_append_args "${cmd_name}" "--network" "host" |
| 80 | + fi |
| 81 | +} |
| 82 | + |
| 83 | +# Appends a `--build-arg` for each named environment variable that is set and |
| 84 | +# non-empty. Lets consumers forward their own build knobs (package mirrors, for |
| 85 | +# instance) without this library knowing their names. |
| 86 | +# |
| 87 | +# Args: <cmd-array-name> [var-name...] |
| 88 | +docker_build_add_env_build_args() { |
| 89 | + if (( $# < 1 )) || [[ -z "$1" ]]; then |
| 90 | + echo >&2 "ERROR: docker_build_add_env_build_args requires a command array" |
| 91 | + return 2 |
| 92 | + fi |
| 93 | + local cmd_name="$1" |
| 94 | + shift |
| 95 | + |
| 96 | + local var value |
| 97 | + for var in "$@"; do |
| 98 | + value="${!var:-}" |
| 99 | + [[ -n "${value}" ]] && docker_utils_append_args "${cmd_name}" \ |
| 100 | + "--build-arg" "${var}=${value}" |
| 101 | + done |
| 102 | + # Explicit: the `&&` above leaves a nonzero status when the last variable is |
| 103 | + # unset, which would abort callers running under `errexit`. |
| 104 | + return 0 |
| 105 | +} |
| 106 | + |
| 107 | +# Appends `--pull` unless DOCKER_PULL is "false", so builds refresh their base |
| 108 | +# image by default. |
| 109 | +# |
| 110 | +# Args: <cmd-array-name> |
| 111 | +docker_build_add_pull_arg() { |
| 112 | + if (( $# != 1 )) || [[ -z "$1" ]]; then |
| 113 | + echo >&2 "ERROR: docker_build_add_pull_arg requires a command array" |
| 114 | + return 2 |
| 115 | + fi |
| 116 | + [[ "${DOCKER_PULL:-true}" != "false" ]] && docker_utils_append_args "$1" "--pull" |
| 117 | + return 0 |
| 118 | +} |
| 119 | + |
| 120 | +# Appends OCI source-provenance labels derived from the git repo at <repo-dir>. |
| 121 | +# A no-op outside a git work tree. |
| 122 | +# |
| 123 | +# Args: <cmd-array-name> <repo-dir> |
| 124 | +docker_build_add_oci_labels() { |
| 125 | + if (( $# != 2 )) || [[ -z "$1" || -z "$2" ]]; then |
| 126 | + echo >&2 "ERROR: docker_build_add_oci_labels requires a command array and a repo directory" |
| 127 | + return 2 |
| 128 | + fi |
| 129 | + local cmd_name="$1" repo_dir="$2" |
| 130 | + |
| 131 | + command -v git &>/dev/null || return 0 |
| 132 | + git -C "${repo_dir}" rev-parse --is-inside-work-tree &>/dev/null || return 0 |
| 133 | + |
| 134 | + local revision |
| 135 | + if revision="$(git -C "${repo_dir}" rev-parse HEAD 2>/dev/null)"; then |
| 136 | + docker_utils_append_args "${cmd_name}" \ |
| 137 | + "--label" "org.opencontainers.image.revision=${revision}" |
| 138 | + fi |
| 139 | + |
| 140 | + local remote_url |
| 141 | + if remote_url="$(git -C "${repo_dir}" remote get-url origin 2>/dev/null)"; then |
| 142 | + # Credentials stripped: a label travels with the image to every registry |
| 143 | + # and `docker inspect` that ever sees it. |
| 144 | + remote_url="$(_docker_build_replace_userinfo "${remote_url}")" |
| 145 | + docker_utils_append_args "${cmd_name}" \ |
| 146 | + "--label" "org.opencontainers.image.source=${remote_url}" |
| 147 | + fi |
| 148 | + return 0 |
| 149 | +} |
| 150 | + |
| 151 | +# Echoes and runs the assembled command. |
| 152 | +# |
| 153 | +# Args: <cmd-array-name> |
| 154 | +docker_build_run() { |
| 155 | + if (( $# != 1 )) || [[ -z "$1" ]]; then |
| 156 | + echo >&2 "ERROR: docker_build_run requires a command array" |
| 157 | + return 2 |
| 158 | + fi |
| 159 | + local length |
| 160 | + length="$(docker_utils_array_length "$1")" || return 2 |
| 161 | + if (( length == 0 )); then |
| 162 | + echo >&2 "ERROR: docker_build_run got an empty command array: $1" |
| 163 | + return 2 |
| 164 | + fi |
| 165 | + |
| 166 | + if ! docker buildx version &>/dev/null; then |
| 167 | + echo >&2 "ERROR: docker buildx is required (Docker 23 or newer)." |
| 168 | + return 1 |
| 169 | + fi |
| 170 | + |
| 171 | + # Copied out by name, since the array belongs to the caller. |
| 172 | + local cmd |
| 173 | + eval "cmd=(\"\${$1[@]}\")" |
| 174 | + |
| 175 | + # The echoed line is for humans and CI logs, so proxy credentials are masked |
| 176 | + # there. The command itself runs with the values untouched. |
| 177 | + local arg printable=() |
| 178 | + for arg in "${cmd[@]}"; do |
| 179 | + printable+=("$(_docker_build_replace_userinfo "${arg}" "***@")") |
| 180 | + done |
| 181 | + echo "Running: ${printable[*]}" |
| 182 | + |
| 183 | + "${cmd[@]}" |
| 184 | +} |
| 185 | + |
| 186 | +# The common composition: proxy args, forwarded build args, pull, labels, run. |
| 187 | +# CA trust is deliberately not included -- it's opt-in and the consumer calls |
| 188 | +# ca_trust_add_build_args itself. |
| 189 | +# |
| 190 | +# Args: <cmd-array-name> <repo-dir> [build-arg-var-name...] |
| 191 | +docker_build_finalize() { |
| 192 | + if (( $# < 2 )) || [[ -z "$1" || -z "$2" ]]; then |
| 193 | + echo >&2 "ERROR: docker_build_finalize requires a command array and a repo directory" |
| 194 | + return 2 |
| 195 | + fi |
| 196 | + local cmd_name="$1" repo_dir="$2" |
| 197 | + shift 2 |
| 198 | + |
| 199 | + docker_build_add_proxy_args "${cmd_name}" || return |
| 200 | + docker_build_add_env_build_args "${cmd_name}" "$@" || return |
| 201 | + docker_build_add_pull_arg "${cmd_name}" || return |
| 202 | + docker_build_add_oci_labels "${cmd_name}" "${repo_dir}" || return |
| 203 | + docker_build_run "${cmd_name}" |
| 204 | +} |
0 commit comments