#!/usr/bin/env bash ############################################################################### # infra/devcontainer/build-image.sh — build the pinned dev `toolbox` image and prove it works. # # ./build-image.sh # build + verify (tag: home-k3s-dev:local) # ./build-image.sh --no-verify # build only # IMAGE=foo:bar ./build-image.sh # build under another local name # ENGINE=docker ./build-image.sh # build with docker instead of podman # # ONE BUILD DEFINITION, TWO CALLERS. `dev.sh build` calls this for local development, and the # CI image pipeline (.gitea/workflows/images.yaml -> .gitea/workflows/scripts/images.sh) calls # it to build the image it then PUBLISHES as gitea.home.keithjackson.dev/home-network/toolbox. # Keeping the build args in exactly one place is what makes "the toolbox CI publishes" and "the # toolbox you gate with locally" the same recipe. # # THIS SCRIPT NEVER PUSHES. Publishing is CI's job (images.yaml, on push to main), not a # workstation's: an image that exists only because someone ran `podman push` from a laptop has # no build log, no recorded inputs and no verification anyone else can read. # # VERSIONS ARE NOT DEFINED HERE. infra/devcontainer/versions.env is the single source of truth. ############################################################################### set -euo pipefail HERE="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" ENGINE="${ENGINE:-podman}" IMAGE="${IMAGE:-home-k3s-dev:local}" VERIFY=1 for arg in "$@"; do case "$arg" in --no-verify) VERIFY=0 ;; -h|--help) sed -n '2,/^####/p' "${BASH_SOURCE[0]}" | sed '$d' | sed 's/^# \{0,1\}//'; exit 0 ;; *) echo "unknown argument: $arg" >&2; exit 2 ;; esac done log() { printf '\n\033[1m==> %s\033[0m\n' "$*"; } die() { printf '\033[31mERROR: %s\033[0m\n' "$*" >&2; exit 1; } command -v "${ENGINE}" >/dev/null 2>&1 || die "'${ENGINE}' not on PATH (set ENGINE=docker to use docker)" # shellcheck disable=SC1091 source "${HERE}/versions.env" # Provenance: CI passes the commit it is building, so a pulled image names its source tree. LABEL_ARGS=() [ -z "${BUILD_REVISION:-}" ] || LABEL_ARGS+=(--label "org.opencontainers.image.revision=${BUILD_REVISION}") log "Building ${IMAGE}" "$ENGINE" build "${LABEL_ARGS[@]}" \ --build-arg FEDORA_VERSION="$FEDORA_VERSION" \ --build-arg OPENTOFU_VERSION="$OPENTOFU_VERSION" \ --build-arg AGE_VERSION="$AGE_VERSION" \ --build-arg SOPS_VERSION="$SOPS_VERSION" \ --build-arg FLUX_VERSION="$FLUX_VERSION" \ --build-arg KUBECTL_VERSION="$KUBECTL_VERSION" \ --build-arg HELM_VERSION="$HELM_VERSION" \ --build-arg KYVERNO_VERSION="$KYVERNO_VERSION" \ --build-arg KYVERNO_JSON_VERSION="$KYVERNO_JSON_VERSION" \ --build-arg CHAINSAW_VERSION="$CHAINSAW_VERSION" \ --build-arg YAMLLINT_VERSION="$YAMLLINT_VERSION" \ --build-arg YQ_VERSION="$YQ_VERSION" \ --build-arg CHECK_JSONSCHEMA_VERSION="$CHECK_JSONSCHEMA_VERSION" \ --build-arg PROMTOOL_VERSION="$PROMTOOL_VERSION" \ -t "$IMAGE" -f "${HERE}/Containerfile" "${HERE}" # --- verify ------------------------------------------------------------------ # A build that succeeded is not evidence the toolbox can run the gate. Every tool the gate and # the runbooks call must EXECUTE, and the version-sensitive ones must report the version # versions.env pins — promtool above all, because ci/versions.env asserts a promtool/Prometheus # lockstep and a drifted engine evaluates alert-rule tests differently (see versions.env). if [ "${VERIFY}" -eq 1 ]; then log "Verifying the toolchain inside ${IMAGE}" "$ENGINE" run --rm --network none --entrypoint "" \ -e PROMTOOL_VERSION="${PROMTOOL_VERSION}" \ -e KUBECTL_VERSION="${KUBECTL_VERSION}" \ -e CHAINSAW_VERSION="${CHAINSAW_VERSION}" \ -e KYVERNO_VERSION="${KYVERNO_VERSION}" \ -e SOPS_VERSION="${SOPS_VERSION}" \ "$IMAGE" /bin/bash -euo pipefail -c ' echo "--- baked versions ---"; cat /etc/dev-toolchain-versions echo "--- every tool is on PATH ---" for t in tofu age age-keygen sops flux kubectl helm kyverno kyverno-json chainsaw yq \ yamllint check-jsonschema promtool ansible-playbook git make jq diff python3 \ wg qrencode; do if command -v "$t" >/dev/null 2>&1; then printf " ok %s\n" "$t"; else printf " MISSING %s\n" "$t"; missing=1; fi done [ -z "${missing:-}" ] || { echo "toolbox is missing tools"; exit 1; } echo "--- version-sensitive tools report their pins ---" check() { # name pin actual-output if printf "%s" "$3" | grep -qF "$2"; then printf " ok %-10s %s\n" "$1" "$2"; else printf " DRIFT %-10s want %s, got: %s\n" "$1" "$2" "$(printf "%s" "$3" | head -1)"; drift=1; fi } check promtool "${PROMTOOL_VERSION}" "$(promtool --version 2>&1)" check kubectl "${KUBECTL_VERSION}" "$(kubectl version --client 2>&1)" check chainsaw "${CHAINSAW_VERSION}" "$(chainsaw version 2>&1)" check kyverno "${KYVERNO_VERSION}" "$(kyverno version 2>&1)" check sops "${SOPS_VERSION}" "$(sops --version --disable-version-check 2>&1 || sops --version 2>&1)" [ -z "${drift:-}" ] || { echo "a tool does not report its pinned version"; exit 1; } echo "--- python can import what the renderer and the gate need ---" python3 -c "import yaml; print(\" PyYAML\", yaml.__version__)" ' log "Toolbox verified" fi log "Built ${IMAGE}" printf ' image id %s\n' "$("$ENGINE" image inspect --format '{{.Id}}' "$IMAGE")"