From ff4892f7957e210efbf077f4fbb6cb5195500a9f Mon Sep 17 00:00:00 2001 From: Keith Jackson Date: Tue, 29 Sep 2026 19:14:12 -0500 Subject: [PATCH] docs(decisions-log): handoff for the conformance-runner image (PR #18) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit CLAUDE.md section 7. Records what was built, what was VERIFIED by running it, and — more usefully — what was not: * the image has never run in the cluster; every proof above is local podman * it is not pushed, so CONFORMANCE_RUNNER_IMAGE_DIGEST is empty on purpose * L2/L3 still do not run in CI; this image is a prerequisite, not the closure And one constraint the plan does not account for, which the next agent would otherwise hit at runtime: run.sh runs the offline L0/L1 checks for any selection that is not offline-only, and those need flux/promtool/kyverno/kustomize. So BOTH of the plan's documented invocation shapes -- `--layer all --strict-coverage` and a bare `--requirements ` -- fail on this image. `--layer l2` / `--layer l3` work, and are verified to. That is a decision for the plan owner, not a detail. Also records a measurement rather than an assumption: from inside the image as uid 65532 the Gitea host verifies against the stock ca-certificates (Let's Encrypt YR2) and an anonymous `ls-remote` succeeds, so the init-container clone needs no internal CA bundle -- and anonymous read is currently open, which means the plan's read-only deploy token buys authorisation hygiene, not reachability. Co-Authored-By: Claude Opus 5 (1M context) --- DECISIONS-LOG.md | 115 +++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 115 insertions(+) diff --git a/DECISIONS-LOG.md b/DECISIONS-LOG.md index 588a5f3..c86ffd6 100644 --- a/DECISIONS-LOG.md +++ b/DECISIONS-LOG.md @@ -5109,3 +5109,118 @@ next L2/L3 piece on top of unmerged branches would create a rebase tangle for no cannot land any faster. The next piece is specified and ready to start once these are merged: `docs/plans/l2-l3-conformance-ci-wiring.md` §3, with §4's credential model now decided and its boundary landed in #13. + +--- + +## 2026-09-29 — the in-cluster conformance Job could never have run; built the image it needs (PR #18) + +Closes `docs/plans/l2-l3-conformance-ci-wiring.md` §3 item 1. Branch +`feat/conformance-runner-image`, PR +[#18](https://gitea.home.keithjackson.dev/Home-Network/home-network-k3s-impl/pulls/18). **Not +merged** — an agent cannot, and should not. + +### The defect, stated plainly + +`conformance/incluster/chainsaw-job.yaml` pinned `ghcr.io/kyverno/chainsaw:v0.2.12` **and** +`command: ["./run.sh"]`. Both lines could not be true. That image carries the chainsaw binary +and essentially nothing else — no `bash`, no `python3`, no `kubectl`, no `git`. The Job would +have failed at exec of `./run.sh` before a single test ran, and the only place that would have +surfaced is a pod log nobody was reading. This is the same failure shape as the last three +entries in this log: a manifest that *described* a working mechanism, with nothing measuring +whether the mechanism existed. + +### What landed + +- `conformance/incluster/Containerfile` — the `conformance-runner` image: bash + python3 + + chainsaw + kubectl + `git-core` + tar/gzip/coreutils/diffutils. Two-stage (a full Fedora + fetches the binaries and verifies kubectl's published `sha256`; the runtime layer is + `fedora-minimal` and carries no downloader). +- `conformance/incluster/build-image.sh` — builds, **proves the toolchain by executing it**, + prints the digest, and **never pushes**. +- `conformance/versions.env` — `CONFORMANCE_RUNNER_IMAGE`, digest **empty** pending the push. +- `conformance/incluster/README.md` — documents both, and flags the Job manifest's stale `image:`. + +`infra/devcontainer/versions.env` stays the single source of truth. `build-image.sh` sources it +and injects `CHAINSAW_VERSION` / `KUBECTL_VERSION` / `FEDORA_VERSION` as `--build-arg`; the +Containerfile's `ARG` defaults are **empty** and asserted in its first `RUN`, so a build that +forgets a pin fails loudly rather than baking a stale version. No new pin was invented. + +### VERIFIED — by running the image, not by a green 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 entrypoint in the same shape against the worktree: `./run.sh --lint`, +`--coverage-only`, and `--layer l2 --no-cluster` all **rc 0**, emitting `coverage.md`, +`coverage.json` and `report.junit.xml`. 408 MiB. + +Two things the admission boundary forced, both of which would otherwise have been discovered +at first run: `USER` must be **numeric** (`runAsNonRoot` with no `runAsUser` makes the kubelet +reject a named user), and every writable path must live under `/workspace` (the pod is +`readOnlyRootFilesystem` and the policy admits no third volume, so there is no `/tmp`). git's +`safe.directory` is therefore written to `/etc/gitconfig` at build time. +`platform/kyverno/admission/conformance-trigger-job-shape.yaml` was **not modified**. + +### NOT VERIFIED — state this before building on it + +- **The image has never run in the cluster.** Everything above is local podman. Admission has + not been exercised against it, because there is no digest to reference yet. +- **The image is not pushed**, so `CONFORMANCE_RUNNER_IMAGE_DIGEST` is empty. Empty on purpose: + it fails visibly in whatever renders the Job, whereas a guessed digest would look like an + `ImagePullBackOff` cluster fault. Pushing needs a registry credential and is an operator + action — `build-image.sh` prints the exact commands. +- The init-container's clone path **is** verified end to end, and this one was measured rather + than assumed: from inside the image, as uid 65532, `python3` completes a verifying TLS + handshake to `gitea.home.keithjackson.dev` against the stock `ca-certificates` + (issuer `Let's Encrypt / YR2`, notAfter `Dec 12 02:16:57 2026 GMT`) and + `git ls-remote https://…/home-network-k3s-impl.git HEAD` returns `20ea70d` with **no + credential**. So no internal CA bundle is needed, and anonymous read is currently open — the + plan's read-only deploy token buys authorisation hygiene, not reachability. +- L2/L3 still do not run in CI (`vars.CLUSTER_AVAILABLE` unset). This image is a prerequisite + for closing that hole, not the closure. + +### A REAL CONSTRAINT THE PLAN DOES NOT ACCOUNT FOR — read before wiring T-6.1b + +`run.sh` runs the selected **offline L0/L1 checks** before it touches the cluster, for **any** +selection that is not offline-only. Those checks shell out to `flux`, `promtool`, `kyverno` and +`kustomize`. This image deliberately omits them — 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 plan's own invocation shapes do not work on this image.** `--layer all +--strict-coverage` (the documented phase-exit shape) and a bare `--requirements ` (the +documented merge-gate shape) both drag the L0 checks in and will fail on a missing tool. The Job +must use `--layer l2` / `--layer l3`, which is verified above to resolve the offline selection +to nothing. + +**This is a decision for the plan owner, not a detail:** either the Job selects cluster-gated +layers only, or the runner grows the full L0 toolchain, or `run.sh` learns to skip offline +checks when their tools are absent (which would be a fail-open and is the wrong answer). It is +documented in the Containerfile header and the incluster README so it cannot be rediscovered +from a pod log. + +### Follow-ups this creates + +1. **Operator:** `podman login` + `podman push --digestfile`, then a PR setting + `CONFORMANCE_RUNNER_IMAGE_DIGEST` from the digestfile — the digest the *registry* returned, + not the local manifest digest the build printed. +2. **Then tighten the boundary**, as that policy's own header asks: replace its `@sha256:*` + wildcard with the literal digest, closing the residual "anyone who can push to this path" + gap. Separate, human-reviewed PR — it edits the security boundary. +3. **T-6.1b:** decide the Job's `args` per the constraint above, and stop `chainsaw-job.yaml` + carrying an image reference that admission would deny. + +### Gate + +`DEV_KUBECONFIG=/dev/null ENGINE= infra/devcontainer/dev.sh run bash -c './ci/gate.sh'` +→ **rc 3, `MERGE GATE: UNDETERMINED`. Nothing FAILED.** + + [ ok ] lint [ ok ] l0 [ ok ] render-golden [ ok ] l1 + [skip] spec-immutability (sibling spec repo not checked out — CLAUDE.md §3 tolerates this one) + [skip] gitea-settings (SETTINGS_API_TOKEN not set) + [skip] conformance (CLUSTER_AVAILABLE != true) + +Touched requirements resolved to `R-SEC-02 R-TEST-01 R-TEST-02 R-TEST-03 R-TEST-04`; all five +remain **L2/L3-UNVERIFIED**. This is not a green run and is not offered as one. + +PR #18's CI is **expected to fail on `approval-policy`** — `conformance/**` is a +security-relevant path requiring explicit human approval. That is the control working.