#!/usr/bin/env bash ############################################################################### # conformance/incluster/build-image.sh — build the pinned `conformance-runner` # image and print its digest. Closes docs/plans/l2-l3-conformance-ci-wiring.md §3 item 1. # # ./build-image.sh # build + verify + print the digest and push command # ./build-image.sh --no-verify # build only (skip the in-image tool proof) # ENGINE=docker ./build-image.sh # build with docker instead of podman # # THIS SCRIPT NEVER PUSHES. Publishing is an operator action: the registry credential is # not an agent's to hold, and the pushed digest is what a fail-closed admission policy # then trusts. So this builds locally, proves the toolchain works INSIDE the image as the # non-root uid the policy demands, and prints the exact `podman push` line for a human. # # 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. # # THE DIGEST CAVEAT — read it before pasting a digest into anything. # The digest printed here is the LOCAL manifest digest. It is normally identical to the # registry digest (podman pushes the manifest it already has rather than recompressing), # but that is a property of the push, not a guarantee. So the push command below uses # `--digestfile`: the digest the registry actually returns is the authoritative one, and # that is the one that goes into conformance/versions.env. ############################################################################### set -euo pipefail HERE="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" REPO_ROOT="$(cd "${HERE}/../.." && pwd)" ENGINE="${ENGINE:-podman}" 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' "$*"; } info() { printf ' %s\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)" # --- 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}}" # --- 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 build convenience only (a human-readable handle for the push). Admission # ignores it: only the digest is ever referenced from a Job. IMAGE_TAG="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}" "${ENGINE}" build \ --file "${HERE}/Containerfile" \ --tag "${IMAGE_REF}" \ --build-arg "CHAINSAW_VERSION=${CHAINSAW_VERSION}" \ --build-arg "KUBECTL_VERSION=${KUBECTL_VERSION}" \ --build-arg "FEDORA_VERSION=${FEDORA_VERSION}" \ "${HERE}" # --- verify: the point of the whole exercise --------------------------------- # The stock chainsaw image "had" chainsaw and still could not run ./run.sh. So prove the # tools EXECUTE, and prove they execute as the NON-ROOT uid with a READ-ONLY root # filesystem — the shape the admission policy forces. A pass here is the only evidence # this image is usable; a build that merely succeeded is not. if [ "${VERIFY}" -eq 1 ]; then log "Verifying the toolchain inside the image (non-root, read-only rootfs)" "${ENGINE}" run --rm \ --user 65532:65532 \ --read-only \ --tmpfs /workspace:rw,mode=1777 \ --entrypoint "" \ "${IMAGE_REF}" \ /bin/bash -euo pipefail -c ' echo "--- id ---"; id echo "--- HOME ---"; echo "HOME=${HOME}" echo "--- bash ---"; bash --version | head -1 echo "--- python3 ---"; python3 --version echo "--- git ---"; git --version echo "--- kubectl ---"; kubectl version --client echo "--- chainsaw ---"; chainsaw version echo "--- baked versions ---"; cat /etc/conformance-runner-versions echo "--- rootfs is read-only (expected: a failure) ---" if touch /should-fail 2>/dev/null; then echo "UNEXPECTED: rootfs is writable"; exit 1; fi echo "confirmed: / is read-only" echo "--- HOME is writable (the workspace emptyDir) ---" touch "${HOME}/.probe" && echo "confirmed: ${HOME} is writable" echo "--- uid is non-zero ---" [ "$(id -u)" -ne 0 ] && echo "confirmed: running as uid $(id -u), not root" ' log "Toolchain verified" fi # --- digest ------------------------------------------------------------------ DIGEST="$("${ENGINE}" image inspect --format '{{.Digest}}' "${IMAGE_REF}")" IMAGE_ID="$("${ENGINE}" image inspect --format '{{.Id}}' "${IMAGE_REF}")" SIZE="$("${ENGINE}" image inspect --format '{{.Size}}' "${IMAGE_REF}")" log "Built ${IMAGE_REF}" info "local manifest digest ${DIGEST}" info "image id ${IMAGE_ID}" info "size $(( SIZE / 1024 / 1024 )) MiB" cat < sha256:.... THIS is the authoritative digest, not the local one above. # conformance/versions.env CONFORMANCE_RUNNER_IMAGE="${IMAGE_REPO}@\$(cat /tmp/conformance-runner.digest)" WHY THE DIGEST AND NOT THE TAG: platform/kyverno/admission/conformance-trigger-job-shape.yaml is a fail-closed Enforce policy. It DENIES any conformance-trigger Job whose image is not ${IMAGE_REPO}@sha256:. A tag reference is denied, and so is any other repository path. TIGHTEN THE BOUNDARY AFTERWARDS: that policy's header asks for the wildcard \`${IMAGE_REPO}@sha256:*\` to be replaced with the LITERAL digest once the image exists, closing the "anyone who can push to this path" gap. That is a separate, human-reviewed PR against the security boundary. ================================================================================ EOF