Operator decision on PR #81: no CI job may reach a privileged daemon. The act runner is untouched. - platform/buildkit (+ clusters/base/buildkit.yaml): moby/buildkit v0.34.1-rootless pinned by digest, uid 1000, not privileged; caps drop ALL + SETUID/SETGID, seccomp/AppArmor Unconfined, read-only rootfs (each measured under podman). cert-manager CA + server/client certs, ClusterIP :1234, default-deny with ingress only from the act-runner pod (DinD masquerades jobs to it). No Kyverno exception: every applied policy matches app-namespace=true only. - Each Containerfile names its shipped stage and ends in a `verify` stage holding every former `docker run` assertion (uid 65532, RUN --network=none, writable-path inventory standing in for the read-only rootfs). The old `&& echo` checks that could never fail are real assertions now. - images.sh drives buildkitd with a pinned buildx (sha256-checked) via bake; PRs build verify targets with agent-runner FROM the in-run toolbox (target: context); main verifies, then pushes. Fails loudly when the builder secrets are absent or buildkitd is unreachable. - infra/gitea/buildkit-client-secret.sh copies the client cert into Actions secrets. Co-Authored-By: Claude Opus 5.5 <[email protected]>
152 lines
7.9 KiB
Bash
Executable File
152 lines
7.9 KiB
Bash
Executable File
#!/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."
|