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
Check · Review · Land · Live done — Landed and live in production.
scout/forge:scout/2287-docs-cluster-md-says-exactly-what-an-ope → main 1 commit fork
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 | }); |
new above
offline