The committed L2/L3 Job pins `ghcr.io/kyverno/chainsaw:v0.2.12` with
`command: ["./run.sh"]`. Those two lines cannot both be true. That image carries
the chainsaw binary and essentially nothing else -- no bash, no python3, no
kubectl, no git -- so the Job would have failed at exec of `./run.sh`, before a
single test ran. Nothing in the tree said so.
This adds the image that makes the Job runnable (plan
docs/plans/l2-l3-conformance-ci-wiring.md section 3 item 1):
* conformance/incluster/Containerfile -- bash + python3 + chainsaw + kubectl +
git-core + tar/gzip/coreutils (the base64-tar results channel of item 3).
Two-stage: a full Fedora fetches the release binaries and verifies kubectl's
published sha256; the runtime layer is fedora-minimal and carries no
downloader. Versions are --build-arg-injected from
infra/devcontainer/versions.env (CHAINSAW_VERSION, KUBECTL_VERSION,
FEDORA_VERSION) -- the single source of truth -- so the chainsaw and kubectl in
the runner are the same pins the toolbox gate runs with. No new pin is invented
here, and the ARG defaults are EMPTY so a build that forgets one fails loudly
instead of baking a stale version.
* conformance/incluster/build-image.sh -- builds it, PROVES the toolchain by
executing it inside the image, and prints the digest. It never pushes: the
registry credential is not an agent's to hold and publishing the artifact a
fail-closed policy then trusts is an operator action. It prints the exact
`podman push --digestfile ...` line instead.
* conformance/versions.env -- CONFORMANCE_RUNNER_IMAGE, with the digest
deliberately EMPTY and a loud block saying the operator fills it in after the
push. Empty is a visible failure in whatever renders the Job; a guessed digest
would be an ImagePullBackOff dressed up as a cluster fault.
WHAT THE ADMISSION BOUNDARY FORCED, AND WHY THE IMAGE OBEYS IT
platform/kyverno/admission/conformance-trigger-job-shape.yaml is Enforce +
failurePolicy: Fail and it is the R-SEC-02 boundary. It is NOT modified here.
Two of its clauses are load-bearing on the image itself:
* `runAsNonRoot: true` with no explicit runAsUser means the kubelet rejects an
image whose USER is a name it cannot resolve to a uid. So USER is NUMERIC
(65532), not a friendly name.
* The pod is readOnlyRootFilesystem and the policy admits only the `workspace`
and `results` volumes -- there is no writable /tmp and one cannot be added
without editing the boundary. So HOME, XDG_CACHE_HOME, XDG_CONFIG_HOME and
TMPDIR all point into /workspace, and git's safe.directory config is written
to /etc/gitconfig at BUILD time because a runtime --global write would fail.
VERIFIED BY RUNNING IT, not by a successful build
As uid 65532 with --read-only: bash 5.3.9, Python 3.14.7, git 2.55.0,
kubectl v1.36.4 (client), chainsaw 0.2.12; / confirmed read-only, /workspace
writable, uid confirmed non-zero. Then the real thing -- `./run.sh --lint`,
`--coverage-only` and `--layer l2 --no-cluster` all rc 0 inside the image
against this worktree, emitting coverage.md/.json and report.junit.xml.
KNOWN LIMIT, FLAGGED NOT PAPERED OVER
run.sh runs the selected offline L0/L1 checks before it touches the cluster for
ANY selection that is not offline-only, and those checks shell out to flux,
promtool, kyverno and kustomize, which this image does not carry. Baking the
whole devcontainer toolchain into the runner would roughly double its size to
re-run work ci/l0.sh and ci/l1.sh already did offline in the same CI run. So the
Job must select the cluster-gated layers only (`--layer l2` / `--layer l3`);
`--layer all` and a bare `--requirements <IDs>` would drag the L0 checks in and
fail on a missing tool. `--layer l2` is verified above to resolve the offline
selection to nothing. Choosing the Job's args belongs to T-6.1b; the constraint
is documented in the Containerfile header and the incluster README rather than
left for someone to discover from a pod log.
chainsaw-job.yaml's `image:` is left stale ON PURPOSE -- there is no digest to
put there yet, and the trigger renders that template per run (plan item 4). The
README now says so instead of letting the placeholder read as a pin.
Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
Text-only cleanup after the upgrade campaign. Updates stale version
strings, prose, and comments to the verified current facts; no
Kubernetes behavior or version pins changed except the kubectl
toolchain bump (below).
Verified facts applied: k3s v1.36.4+k3s1, Debian 13 trixie,
Cilium 1.20.1 / Gateway API v1.6.1, Longhorn 1.12.1, node RAM 24 GiB,
dedicated virtio2 SSD etcd disk per node, Velero chart 12.x / v1.18,
Grafana 13.2.0.
- Version strings across infra/ and platform/ READMEs + comments
- kubectl toolchain 1.31.4 -> 1.36.4 (devcontainer/ci/conformance
versions.env; 1.31 vs a 1.36 cluster exceeded the +-1 skew)
- Retired-single-apps-Kustomization refs -> apps-wave-0/1/2 + apps-vms
with per-wave SOPS decryption (flux/clusters docs + bootstrap.sh)
- COMPLETED/historical banners on the three executed plan docs
- Resolved TODO(WS-SEC) markers -> Renovate-maintained ranges
- Appended dated DECISIONS-LOG entry (2026-08-30) superseding the
2026-08-26 LIVE STATE line
Left as revalidation items (not blind-edited): external-snapshotter
v8.2.0 vs 1.36, Cilium ext_authz/forward-auth relevance under 1.20.1,
conformance ImageVolume feature-gate assertions under 1.36.
Co-Authored-By: Claude Opus 4.8 <[email protected]>