T4 · kind and Kueue¶
Done 2026-10-28 on autopilot/T4, CI-driven (inbox #51 option (b), assumed) · k8s-mini up in 58 s and k8s-kueue in 66 s on ubuntu-24.04 · synthetic Nextflow pipeline with -profile k8s green on 26.10.4 and 25.10.9, each head under its own ServiceAccount token · 10 fixtures, 106 kB · the §4 Roles' insufficiency, measured in the spike, is confirmed by replacement Roles that see 0 refusals (403) · task pods get no token · 4 CI runs, the last green in 3 min 37 s · review-code approve

The §4 refusals in the figure come from the spike's runs under the spec Roles (note, ServiceAccounts); the rest comes from the T4 runs below.
What changed¶
The spike t4-ci covered T4's scope; this step keeps what it proved and drops the rest.
- Cluster profiles, dev-environment §4.2:
dev/k8s/cluster.sh up mini|kueue,down mini|kueue.k8s-miniis one kind node;k8s-kueueis a control plane and 2 workers, with KueueKUEUE_VERSION. Both get the namespacespectio-workwith aResourceQuota, and itsdefaultServiceAccount does not mount a token in pods (dev/k8s/base.yaml).- Kueue comes from the release's
manifests.yaml, checked againstKUEUE_MANIFEST_SHA256. It is set up with two ClusterQueues and two LocalQueues (dev/k8s/kueue-queues.yaml); the script fails if they are missing after the retries, and waits until the visibility API answers. - The tools come from
dev/bootstrap.sh, at the pins ofdev/versions.env. The task image is read from thek8sprofile of the synthetic pipeline and pre-pulled on each node. - The repository is mounted into every node at its own path, so that task pods can see the pipeline and its work directory under
.scenarios/(newly gitignored).
dev/versions.env: two new pins:KUEUE_MANIFEST_SHA256, computed, because no checksum is published;NF_K8S_25_10_VERSION=1.2.3.
- Scenarios,
tests/k8s/scenarios.sh. Each scenario fails on an unexpected outcome.failures: one OOMKilled pod, a Job and a Pod over their deadline, and a Job whose pod exits 3.nf25andnf26: the synthetic pipeline with-profile test,k8splustests/k8s/kind.config. Each runs twice under the head's own token: a happy run that must exit 0, and a tool-error run that must exit 1. No API call may be refused (403). Each grant that neither run used is listed asunused-grants(tests/k8s/api_calls.py). The task pod must have nokube-api-access-*volume, and the head's ServiceAccount must be denied the calls of the deny list.kueue-wait: one Workload admitted and two waiting, with their visibility positions.runner: the runner's work done with its own token, then the deny list pluscreate pods, which must all be denied.- The ServiceAccount kubeconfigs are written to a private temporary directory outside the repository (task pods mount the repository), and never hold the admin key.
- RBAC,
tests/k8s/rbac.yaml: the replacement Roles, which differ from §4 (see Findings). -
CI,
.github/workflows/pipelines-k8s.yml, two parallel jobs:mini:failures nf25;kueue:kueue-wait nf26 runner.
It runs on a push to any branch that touches its paths, nightly, and on manual dispatch. It has
permissions: contents: read, no secrets, a 30-minute timeout, actions pinned by SHA, and no${{ }}inrun:. Its fixtures go to the artifactsk8s-fixtures-miniandk8s-fixtures-kueue. Likedev-env.yml,ci.ymldoes not call it, so it is not part ofci-ok. - Fixtures,tests/fixtures/k8s/: 10 JSON files, 106 kB, byte-identical to the artifacts of run 40672184666.managedFieldsand the last-applied annotation are removed. A_fixtureobject records the Kubernetes and Kueue or Nextflow versions and the source run.File Holds nf-26.10.4-task-job.json,nf-26.10.4-task-pod.json,nf-26.10.4-task-events.jsonQC_STATS S01 on 26.10.4: its Job, its pod and their events nf-25.10.9-task-pod.json,nf-25.10.9-task-events.jsonthe same task on 25.10.9, which creates a bare Pod and no Job failures-pods.json,failures-jobs.json,failures-events.jsonOOMKilledexit 137; podDeadlineExceeded; JobFailedwithBackoffLimitExceededorDeadlineExceeded; exit 3kueue-workloads.json,kueue-pending-workloads.jsonthree Workloads (one admitted, two waiting) and the visibility answer, positions 0 and 1 -
Spec rows, dev-environment: the
k8s-minirow of §4.2 and thepipelines-k8s.ymlrow of §11, which this step builds, now say what was built and what is still planned. - Research note, copied from
spike/t4-cias evidence: Spike t4-ci, added to the nav. - Not done here:
- nf-core/demo
-profile test,docker, whichpipelines-real.ymlalready runs in its PR tier; - SeaweedFS and the Snakemake Kubernetes executor,
k8s-rwx, Kueue preemption, and the previous Kueue minor: none of them is in T4's prompt.
- nf-core/demo
Why¶
Prompt: next-phase.md · T4. It asks for the k8s-mini and k8s-kueue profiles, the synthetic pipeline with -profile k8s, the pod, Job, event and Workload objects as fixtures, and a check of the §4 ServiceAccounts. kind cannot run in a Claude Code cloud session, so the loop runs in CI: option (b) of inbox #51, assumed — pending Thomas. No decision-log entry: the step decided no behaviour; the §4 differences wait for TRIAGE3.
How to test¶
On a machine whose Docker runs on cgroup v2 (not a Claude Code cloud session), from the repository root:
bash dev/bootstrap.sh && bash dev/k8s/cluster.sh up kueue && \
for s in kueue-wait nf26 runner failures nf25; do bash tests/k8s/scenarios.sh "$s" || break; done
Each scenario prints a RESULT line and exits 1 on an unexpected outcome. Fixtures land in .scenarios/k8s/fixtures/. bash dev/k8s/cluster.sh down kueue removes the cluster. In CI, push a change under tests/k8s/, or dispatch pipelines-k8s.yml once it is on main.
Evidence¶
CI runs of pipelines-k8s.yml on autopilot/T4:
| Run | Commit | Result | Wall clock |
|---|---|---|---|
| 40668895003 | 42ac189 |
mini green. kueue red: kueue-wait waited 2 minutes for a second QuotaReserved=False condition that never came |
4 min 51 s |
| 40669626241 | 4b95db9 |
kueue red: one jobs/status read answered 404 (the Job was already deleted), and the check counted it as a refusal |
3 min 35 s |
| 40670116723 | 9f516a4 |
green; only 403 counts as a refusal | 3 min 08 s |
| 40672184666 | 7fe53af |
green, after the round-1 review fixes: no token in task pods, the wider deny list. The fixtures come from this run | 3 min 37 s |
| 40673184674 | 1e3aa52 |
red: after merging autopilot/integration, cluster.sh found no task image digest in the k8s profile (#32 moved it to params.python_image) |
25 s |
| 40673827899 | 4c9c64c |
mini red: the heads' deny check, now run with the head's own token (CR-T4-05), reported create pods/exec allowed to nf-head-25; can-i had read exec as a pod name (CR-T4-08) |
3 min 19 s |
| 40674240654 | 007683c |
green, the final code: every deny check with --subresource, each ServiceAccount with its own token |
3 min 38 s |
Timings in run 40672184666:
| Job | Bootstrap | Cluster up | Scenarios |
|---|---|---|---|
mini |
14 s | 58 s (kind 48 s) | 2 min 11 s: failures 43 s, nf25 87 s (happy run 70 s, error run 16 s) |
kueue |
14 s | 66 s (kind 40 s, Kueue 24 s) | 80 s: kueue-wait 2 s, nf26 73 s (54 s and 18 s), runner 5 s |
Result lines of run 40672184666:
RESULT failures: [{"pod":"fx-exit3-dh8nb","reason":null,"terminated":{"reason":"Error","exitCode":3}},{"pod":"fx-exit3-qswld","reason":null,"terminated":{"reason":"Error","exitCode":3}},{"pod":"fx-oom-hc7wn","reason":null,"terminated":{"reason":"OOMKilled","exitCode":137}},{"pod":"fx-pod-deadline","reason":"DeadlineExceeded","terminated":{"reason":"Error","exitCode":137}}]
RESULT nf25: happy exit=0 error exit=1 {"refused":[],"used":["pods create","pods delete","pods/status get"]} unused-grants=[]
RESULT nf26: happy exit=0 error exit=1 {"refused":[],"used":["batch:jobs create","batch:jobs delete","batch:jobs/status get","pods delete","pods list","pods/status get"]} unused-grants=[]
RESULT kueue-wait: [{"name":"job-fx-kq-1-04f50","positionInLocalQueue":0,"positionInClusterQueue":0},{"name":"job-fx-kq-3-7e5a5","positionInLocalQueue":1,"positionInClusterQueue":1}]
RESULT runner: sufficient; 9 out-of-Role calls denied
refusedlists every refused call with its HTTP code; only 403 fails the check. In run 40670116723 thenf26list held{"call":"batch:jobs/status get","code":404,"count":1}, a read racing the Job's deletion, which the check ignored; in run 40672184666 it is empty.- Runner, allowed with its own token: create the head Job (as
nf-head-26), wait for it, list its pods and read its log; read events, quotas and limit ranges, Workloads, and the visibility answer; delete the Job. - Denied, to the runner and to both heads (
kubectl auth can-i, each with its own token, inspectio-work):get secrets,list nodes,create pods --subresource=exec,create serviceaccounts --subresource=token,create rolebindings,impersonate serviceaccounts,create jobs -n default,create pods -n default; andcreate podsto the runner andnf-head-26(nf-head-25needs it). - No token in task pods: no fixture has a
kube-api-access-*volume; in run 40670116723 every pod had one.
Review (the review record):
CODE step=T4 review=approve b/M/m=0/1/6 security=approve b/M/m=0/0/6 rounds=3 simplify=notrun -0/+0 tests=pass
code-reviewerandsecurity-reviewer: approve in round 1, with minors only. Six minors are fixed in7fe53af, one is recorded below (CR-T4-03), and two go to TRIAGE3 (SEC-T4-03, SEC-T4-05). Round 2 reviewed7fe53afand found that the heads' deny check ran in namespacedefaultand proved nothing (CR-T4-05, major) plus two minors; fixed in955a038, approved in round 3 (review record).shellcheck -xandbash -npass ondev/k8s/cluster.shandtests/k8s/scenarios.sh; the fixes were then proved by run 40672184666.
Docs gate: python3 tools/docs/check.py and mkdocs build --strict, 0 failures (see the step report).
Figures and media¶
t4-kind-kueuesketch, light and dark, at the top of this page:python3 tools/diagrams/t4_kind_kueue.py.- No cast or screenshot:
cluster.shandscenarios.share development scripts with no user flow, and How to test shows the commands and theirRESULTlines.
Findings and open questions¶
Differences with the specs, for TRIAGE3; the specs are not edited. The insufficiency of the §4 Roles was measured in spike runs 40662141233 and 40663207755. scenarios.sh tests the replacement Roles of tests/k8s/rbac.yaml: T4 runs show them sufficient (0 refusals, 403) and, for the two heads, minimal (unused-grants=[]). The runner Role's minimality is not measured, because there is no runner code yet: persistentvolumeclaims: get and watch on pods, events and Workloads are granted and never used, and the visibility rule is narrowed to get, against get, list, watch in §4.
- Nextflow 26.10 engine-head Role (schedulers §4): not sufficient and over-broad; note, Findings item 1.
- Nextflow 25.10 engine-head Role (§4): lacks
pods/status: get; closes "25.10 needingpods: create"; note item 1. - Runner Role (§4): lacks
workloads.kueue.x-k8s.io: get, list, watch; note item 2. - Nextflow 25.10 needs
nf-k8snamed (§4 and thek8sprofile); note item 3. - "Queued" comes from the visibility API, not from Workload conditions (§4). The visibility API listed a pending Workload whose
QuotaReserved=Falsecondition came late or never:- run 40672184666:
job-fx-kq-1had onlyPodsReady=False/WaitForStart, at position 0; - run 40669626241:
job-fx-kq-3had onlyPodsReady=False/WaitForStart, at position 1; - run 40668895003: the second pending Workload had no
QuotaReserved=Falseafter 2 minutes.
- run 40672184666:
- An OOM kill produces no event, only
state.terminatedin the pod; note item 4. - The head can get a 404 on
jobs/statuswhen it reads a Job that it is deleting. That is not a permission refusal, so a check must not count it as one. - The "Required checks" paragraph (dev-environment §11) says the pipeline workflows are reusable workflows that
ci.ymlcalls.pipelines-k8s.ymlis standalone (push with a path filter, nightly, manual), so it is outsideci-ok. Thepipelines-k8s.ymlrow of §11 now says so; it no longer listshelm/kind-action(the build usesdev/bootstrap.shanddev/k8s/cluster.sh), and nf-core/demo stays inpipelines-real.yml. Thehelm installof the chart waits for the chart. - The
k8s-minirow (§4.2) listed SeaweedFS, the Snakemake Kubernetes executor and a runner Role. SeaweedFS and the Snakemake executor are not built: T4's prompt does not ask for them, and the row now marks them as planned. The runner Role is not installed byup mini: the scenarios applytests/k8s/rbac.yamlthemselves (CR-T4-03). - Other spike findings:
FailedScheduling: Insufficient cpuis back-pressure (note item 6); task pods mounted thedefaulttoken (note item 7), fixed here (SEC-T4-02); inbox #51's "cannot download artifacts" does not hold (note item 8), and this step's fixtures came from artifacts.
Inputs for TRIAGE3 from the security review (not fixed here):
- SEC-T4-03: whoever can create Jobs or Pods in the shared
spectio-workcan run as any ServiceAccount there, mount a hostPath, and delete another run's pods. Proposal: one namespace per run or per user, with PodSecurityrestrictedand a policy that pinsserviceAccountName. - SEC-T4-05: the
nf-k8s1.2.3 plugin is fetched by version, the Kueue controller image is referenced by tag, and the whole bootstrap runs in the job. Proposal: pin the plugin by checksum and the Kueue image by digest, and bootstrap only the tools the job uses.
Open question. Should pipelines-k8s.yml join ci.yml and ci-ok? It takes about 3 to 4 minutes, but kind adds a flake risk.