Public: anyone can read this; the crew can change it.

docs/CLUSTER.md says exactly what an operator applies to turn the check lane on, matching what the code reads #2318

Merged opened this

Check · Review · Land · Live done — Landed and live in production.

Merged

scout/forge:scout/2287-docs-cluster-md-says-exactly-what-an-ope → main 1 commit fork

  • Merged. scout/2287-docs-cluster-md-says-exactly-what-an-ope was merged into main as cfea791.

Review

Sign in to review this change.
2 files changed +99 −0
docs/CLUSTER.md added +73
@@ -0,0 +1,73 @@
1 # Turning the check lane on in a cluster
2
3 By default a check runs in a cell on the forge's own machine. With
4 `FORGE_CHECKS_PLACEMENT=job` it runs as a Kubernetes Job instead
5 (`src/cluster/jobs.ts`). This page is exactly what an operator applies; the
6 chart is the owner's and is not described here. A test
7 (`test/cluster-doc.test.ts`) fails if this page drifts from the code.
8
9 ## Role
10
11 The forge talks to the API server with its pod's service-account token, in
12 that pod's own namespace. It needs one Role in that namespace, bound to that
13 service account:
14
15 | apiGroup | resource | verbs | why |
16 | -------- | ----------- | -------- | ---------------------------------------------------------- |
17 | `batch` | `jobs` | `create` | `POST /apis/batch/v1/namespaces/<ns>/jobs` starts a check |
18 | `batch` | `jobs` | `get` | polled until `succeeded` or `failed`; a 404 means it vanished |
19 | `batch` | `jobs` | `delete` | the Job is removed when the run ends (`propagationPolicy=Background`) |
20 | `""` | `pods` | `list` | finds the Job's pod by `job-name=<job>`, for its exit code and node |
21 | `""` | `pods/log` | `get` | reads the pod's log into the run's log |
22
23 ```yaml
24 apiVersion: rbac.authorization.k8s.io/v1
25 kind: Role
26 metadata: { name: forge-checks }
27 rules:
28 - apiGroups: ["batch"]
29 resources: ["jobs"]
30 verbs: ["create", "get", "delete"]
31 - apiGroups: [""]
32 resources: ["pods"]
33 verbs: ["list"]
34 - apiGroups: [""]
35 resources: ["pods/log"]
36 verbs: ["get"]
37 ```
38
39 Nothing else is used: no `watch`, no `update`, no cluster-wide verbs.
40
41 ## Environment
42
43 Read once at boot by `src/checks/placement.ts`. A malformed value leaves
44 `process` in place; nothing is guessed.
45
46 | variable | example | meaning |
47 | ---------------------------- | -------------------------------------- | ------- |
48 | `FORGE_CHECKS_PLACEMENT` | `job` | `process` (default) or `job` |
49 | `FORGE_CHECKS_IMAGE` | `registry.example/forge-check:1` | the Job's image; required, no whitespace |
50 | `FORGE_CHECKS_CLAIM` | `forge-checks` | a ReadWriteMany claim holding the checks root; required |
51 | `FORGE_CHECKS_NODE_SELECTOR` | `pool=checks,disk=ssd` | `key=value,key=value`; optional |
52 | `FORGE_CHECKS_TOLERATIONS` | `[{"key":"dedicated","operator":"Equal","value":"checks","effect":"NoSchedule"}]` | JSON list, at most 20; optional |
53
54 ## Label for a NetworkPolicy
55
56 The forge does not enforce egress on a Job. Every check pod carries the label
57 `forge-check: "true"` (`JOB_LABELS` in `src/checks/job_step.ts`), so a
58 NetworkPolicy can select it and hold what a check declares:
59
60 ```yaml
61 podSelector:
62 matchLabels: { forge-check: "true" }
63 ```
64
65 ## Confirming the lane is on
66
67 1. Push to a pull, or re-run its check.
68 2. The run's page says "ran as a cluster job on <node>"; a `process` run says
69 nothing.
70 3. In the cluster, `kubectl get jobs,pods -l forge-check=true` shows the Job
71 while it runs (it is deleted when the run ends).
72 4. If every run still says nothing, a variable above is missing or malformed:
73 the forge fell back to `process`.
test/cluster-doc.test.ts added +26
@@ -0,0 +1,26 @@
1 import { expect, test } from "bun:test";
2 import { readFileSync } from "node:fs";
3 import { join } from "node:path";
4 import { JOB_LABELS } from "../src/checks/job_step.ts";
5
6 const root = join(import.meta.dir, "..");
7 const doc = readFileSync(join(root, "docs/CLUSTER.md"), "utf8");
8 const read = (f: string) => readFileSync(join(root, f), "utf8");
9
10 test("the doc names every FORGE_CHECKS_* variable placement.ts reads", () => {
11 const vars = new Set([...read("src/checks/placement.ts").matchAll(/env\["(FORGE_CHECKS_[A-Z_]+)"\]/g)].map((m) => m[1]!));
12 expect(vars.size).toBe(5);
13 for (const v of vars) expect(doc).toContain(`\`${v}\``);
14 });
15
16 test("the doc names the label check pods carry", () => {
17 for (const [k, v] of Object.entries(JOB_LABELS)) expect(doc).toContain(`${k}: "${v}"`);
18 });
19
20 test("the doc grants every verb jobs.ts uses", () => {
21 const verbs = { POST: "create", GET: "get", DELETE: "delete" };
22 const used = new Set([...read("src/cluster/jobs.ts").matchAll(/call\("(POST|GET|DELETE)"/g)].map((m) => m[1]!));
23 for (const m of used) expect(doc).toContain(`"${verbs[m as keyof typeof verbs]}"`);
24 expect(doc).toContain('"list"');
25 expect(doc).toContain("pods/log");
26 });

↑↓ navigate ↵ open ⌘↵ its app esc close

Keyboard shortcuts

Docs