#!/usr/bin/env bash ############################################################################### # conformance/incluster/build-image.sh — build the pinned `conformance-runner` # image and prove it works. Closes docs/plans/l2-l3-conformance-ci-wiring.md §3 item 1. # # ./build-image.sh # build the verify target, then the image # ./build-image.sh --print-build-args # print KEY=VALUE build args (what CI consumes) # ./build-image.sh --no-verify # build only (skip the in-image tool proof) # ./build-image.sh --verify-clone # ALSO really clone this repo with conformance-clone. # # Opt-in because it needs the Gitea host reachable; the # # default verify is offline so an air-gapped build is # # still fully checked rather than half-checked. # ENGINE=docker ./build-image.sh # build with docker instead of podman # IMAGE_TAG=foo ./build-image.sh # override the local tag (CI uses this) # # THIS SCRIPT NEVER PUSHES — and nobody pushes this image from a workstation either. # Publishing is done by CI: .gitea/workflows/images.yaml builds the SAME Containerfile on # rootless BuildKit (platform/buildkit/), taking its build args from `--print-build-args`, and # on a push to `main` pushes it to the Gitea registry and records the registry-returned digest # in the job summary; Renovate then opens the pull request that moves conformance/versions.env # onto that digest. The proof that the image works is the Containerfile's `verify` stage, which # this script (podman locally) and CI both build. # # VERSIONS ARE NOT DEFINED HERE. They are read from infra/devcontainer/versions.env, the # single source of truth (CHAINSAW_VERSION, KUBECTL_VERSION, FEDORA_VERSION) — the same # pins the toolbox gate runs with. Nothing in this file invents a version. # # NO DIGEST IS PRINTED. A manifest digest does not exist until a registry holds the manifest, # and the only digest anything may pin is the one the REGISTRY returns. CI captures that at # push time (.gitea/workflows/scripts/images.sh) and Renovate carries it into # conformance/versions.env. ############################################################################### set -euo pipefail HERE="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" REPO_ROOT="$(cd "${HERE}/../.." && pwd)" ENGINE="${ENGINE:-podman}" TAG_OVERRIDE="${IMAGE_TAG:-}" VERIFY=1 VERIFY_CLONE=0 PRINT_ARGS=0 for arg in "$@"; do case "$arg" in --no-verify) VERIFY=0 ;; --print-build-args) PRINT_ARGS=1 ;; --verify-clone) VERIFY_CLONE=1 ;; -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' "$*"; } info() { printf ' %s\n' "$*"; } die() { printf '\033[31mERROR: %s\033[0m\n' "$*" >&2; exit 1; } # --- pins: sourced, never invented ------------------------------------------- TOOLCHAIN_ENV="${REPO_ROOT}/infra/devcontainer/versions.env" [ -f "${TOOLCHAIN_ENV}" ] || die "missing ${TOOLCHAIN_ENV} — the pins live there, not here" # shellcheck disable=SC1090 source "${TOOLCHAIN_ENV}" : "${CHAINSAW_VERSION:?CHAINSAW_VERSION not set in ${TOOLCHAIN_ENV}}" : "${KUBECTL_VERSION:?KUBECTL_VERSION not set in ${TOOLCHAIN_ENV}}" : "${FEDORA_VERSION:?FEDORA_VERSION not set in ${TOOLCHAIN_ENV}}" # THE build args, in ONE place; `--print-build-args` hands them to the CI pipeline, which builds # this Containerfile on rootless BuildKit. BUILD_ARGS=("CHAINSAW_VERSION=${CHAINSAW_VERSION}" "KUBECTL_VERSION=${KUBECTL_VERSION}" "FEDORA_VERSION=${FEDORA_VERSION}") if [ "${PRINT_ARGS}" -eq 1 ]; then printf '%s\n' "${BUILD_ARGS[@]}"; exit 0; fi command -v "${ENGINE}" >/dev/null 2>&1 || die "'${ENGINE}' not on PATH (set ENGINE=docker to use docker)" # --- image name: fixed by the admission policy ------------------------------- # platform/kyverno/admission/conformance-trigger-job-shape.yaml admits ONLY # gitea.home.keithjackson.dev/home-network/conformance-runner@sha256:*. The repository path # is therefore not a preference; a different path is a DENIED Job. Do not "improve" it here. IMAGE_REPO="gitea.home.keithjackson.dev/home-network/conformance-runner" # The tag is a local handle only. CI re-tags what it publishes (git SHA + `main`), and # admission ignores tags entirely: only the digest is ever referenced from a Job. IMAGE_TAG="${TAG_OVERRIDE:-chainsaw${CHAINSAW_VERSION}-kubectl${KUBECTL_VERSION}-fc${FEDORA_VERSION}}" IMAGE_REF="${IMAGE_REPO}:${IMAGE_TAG}" log "Building ${IMAGE_REF}" info "engine ${ENGINE} ($(${ENGINE} --version 2>/dev/null | head -1))" info "pins from ${TOOLCHAIN_ENV}" info " CHAINSAW ${CHAINSAW_VERSION}" info " KUBECTL ${KUBECTL_VERSION}" info " FEDORA ${FEDORA_VERSION}" # 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}") ARG_FLAGS=() for a in "${BUILD_ARGS[@]}"; do ARG_FLAGS+=(--build-arg "$a"); done # --- verify: the point of the whole exercise --------------------------------- # The stock chainsaw image "had" chainsaw and still could not run ./run.sh. So the proof is that # the tools EXECUTE, as the NON-ROOT uid, with no network, that conformance-clone is not writable # by that uid and refuses every hostile input, and that nothing outside /workspace is writable. # That proof is the Containerfile's `verify` stage, built here and in CI alike (CI has no # container runtime — rootless BuildKit only). See the stage's header for what a build step can # and cannot reproduce of the Job's shape. A build that merely succeeded is not evidence. if [ "${VERIFY}" -eq 1 ]; then log "Verifying the toolchain (Containerfile target: verify)" "${ENGINE}" build "${ARG_FLAGS[@]}" --target verify --file "${HERE}/Containerfile" "${HERE}" log "Toolchain verified" fi "${ENGINE}" build "${LABEL_ARGS[@]}" "${ARG_FLAGS[@]}" --target conformance-runner \ --file "${HERE}/Containerfile" --tag "${IMAGE_REF}" "${HERE}" # --- verify the clone helper against the REAL repository (opt-in: needs network) ---- # LOCAL ONLY: this one `run`s the image, which needs a container engine, so CI does not do it. # The negative cases above prove conformance-clone refuses bad input. This proves the # positive case: it lands the EXACT commit asked for and can prove it did. Without this, the # rev-parse assertion is only an unexercised line of shell. if [ "${VERIFY_CLONE}" -eq 1 ]; then log "Verifying conformance-clone against the real repository" SHA="$(git -C "${REPO_ROOT}" rev-parse HEAD)" info "requesting commit ${SHA}" "${ENGINE}" run --rm \ --user 65532:65532 \ --read-only \ --tmpfs /workspace:rw,mode=1777 \ --network host \ --entrypoint "" \ -e "CONFORMANCE_GIT_SHA=${SHA}" \ "${IMAGE_REF}" \ /bin/bash -euo pipefail -c ' conformance-clone echo "--- assert rev-parse HEAD == the requested SHA ---" got="$(git -C /workspace/src rev-parse HEAD)" echo "requested: ${CONFORMANCE_GIT_SHA}" echo "HEAD: ${got}" [ "${got}" = "${CONFORMANCE_GIT_SHA}" ] || { echo "MISMATCH"; exit 1; } echo "confirmed: the working tree is at the requested commit" test -x /workspace/src/conformance/run.sh && echo "confirmed: run.sh present and executable" ' log "conformance-clone verified against the real repository" fi # --- result ------------------------------------------------------------------ IMAGE_ID="$("${ENGINE}" image inspect --format '{{.Id}}' "${IMAGE_REF}")" SIZE="$("${ENGINE}" image inspect --format '{{.Size}}' "${IMAGE_REF}")" log "Built ${IMAGE_REF}" info "image id ${IMAGE_ID}" info "size $(( SIZE / 1024 / 1024 )) MiB" info "Not pushed. CI publishes this image from main (.gitea/workflows/images.yaml) and" info "Renovate moves conformance/versions.env onto the registry-returned digest."