Skip to content

Commit 3197843

Browse files
authored
Update CALM document to be inline with all new changes (#42)
Signed-off-by: Erick Bourgeois <erick@jeb.ca>
1 parent c09831c commit 3197843

2 files changed

Lines changed: 228 additions & 2 deletions

File tree

.claude/CHANGELOG.md

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,23 @@ The format is based on the regulated environment requirements:
99

1010
---
1111

12+
## [2026-04-21 15:00] - Model emergency-reclaim in CALM — ADD retrofit
13+
14+
**Author:** Erick Bourgeois
15+
16+
### Added
17+
- `docs/architecture/calm/architecture.json`: 4 new nodes — `service-workload-kubernetes-api` (workload cluster API server as a distinct trust boundary from the management cluster API), `service-reclaim-agent` (node-side DaemonSet), `data-asset-reclaim-request-annotation` (agent→controller signalling surface on the Node), `data-asset-reclaim-agent-configmap` (per-node projection in `5spot-system`).
18+
- `docs/architecture/calm/architecture.json`: 4 new relationships — `rel-controller-workload-kube-api` (controller writes into the workload cluster: ConfigMap projection, Node label, Node annotation clear; audit-attribution control NIST AU-2/AU-10 via distinct field manager `5spot-controller-reclaim-agent`), `rel-reclaim-agent-workload-kube-api` (agent watches its ConfigMap + annotates its own Node; node-scoped-credentials control NIST AC-6), `rel-reclaim-configmap-stored-in-workload-api` (composed-of), `rel-reclaim-annotation-on-node` (composed-of).
19+
- `docs/architecture/calm/architecture.json`: new `flow-emergency-reclaim` with 7 transitions mapping the load-bearing ordering contract (drain → Machine delete → `spec.schedule.enabled=false` → annotation clear → phase Disabled). Flow-level `ordering-idempotence` control (NIST SI-7) documents why step 5 must precede step 6 and how the `PHASE_EMERGENCY_REMOVE` match arm closes the crash window.
20+
- Per-node controls on `service-reclaim-agent`: `least-privilege` (NIST AC-3/AC-6, evidence `deploy/node-agent/rbac.yaml`) and `container-hardening` (NIST SP 800-190 CM-6/SI-7, evidence `deploy/node-agent/daemonset.yaml` + `.trivyignore` architectural-necessity suppressions).
21+
22+
### Changed
23+
- `docs/architecture/calm/architecture.json`: extended `rel-workload-cluster-contains-node` to include `service-workload-kubernetes-api` and `service-reclaim-agent` alongside the existing `network-physical-node`.
24+
- `docs/src/architecture/{flows.md,system.md}`: regenerated by `make calm-diagrams` from the updated model (gitignored — CI regenerates).
25+
26+
### Why
27+
The user just committed the ADD (Architecture Driven Development) rule: any architectural change must land in CALM before code. An audit against the two most-recent features found the **emergency-reclaim work is architecturally material** (new node-side component, new trust boundary, new cross-cluster signalling protocol, new `EmergencyRemove` phase, new RBAC scope in `5spot-system`) but was not reflected in CALM. This commit closes that gap — it is the shakedown test for the ADD rule, done now so the workflow is proven before the rule applies to future features. The separately-audited node-taints feature is not architectural under ADD (CRD-field + existing Node-watch pattern only) and is intentionally **not** retrofitted here.
28+
1229
## [2026-04-21 17:00] - Integration test for node-taint reconcile on a real cluster (Phase 8)
1330

1431
**Author:** Erick Bourgeois
@@ -155,6 +172,13 @@ Phase 2 of the user-defined Node taints roadmap. Separating the status type + co
155172
### Impact
156173
- [ ] Breaking change
157174
- [ ] Requires cluster rollout
175+
- [x] Config change only
176+
- [x] Documentation-only code change (CALM model + rendered mermaid only; no Rust, no deploy manifests touched)
177+
178+
### Verification
179+
- `make calm-validate` → 0 errors, 0 warnings, 0 info/hints.
180+
- `make calm-diagrams` → regenerates `docs/src/architecture/flows.md` (now 3 flows: activation, deactivation, emergency-reclaim) and `docs/src/architecture/system.md` (14 nodes including the new 4, workload-cluster subgraph now contains API server + reclaim agent).
181+
- `make docs``mkdocs build` passes; every hand-authored mermaid component in `docs/src/concepts/emergency-reclaim.md` has a corresponding CALM node.
158182
- [x] Config change only (CRD re-apply picks up the `appliedNodeTaints` status schema; existing CRs are unaffected because the field defaults to empty)
159183
- [ ] Documentation only
160184

docs/architecture/calm/architecture.json

Lines changed: 204 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -168,6 +168,68 @@
168168
"node-type": "service",
169169
"name": "Kubelet",
170170
"description": "Node-local kubelet performing HTTP liveness (/healthz) and readiness (/readyz) probes against the controller."
171+
},
172+
{
173+
"unique-id": "service-workload-kubernetes-api",
174+
"node-type": "service",
175+
"name": "Workload Cluster Kubernetes API Server",
176+
"description": "API server of the CAPI-managed workload cluster (e.g. k0smotron-hosted k0s). Distinct trust boundary from the management cluster API: accepts its own tokens, its own RBAC, and owns the Node objects plus the 5spot-system namespace that backs emergency reclaim.",
177+
"interfaces": [
178+
{
179+
"unique-id": "workload-kube-api-https",
180+
"definition-url": "https://calm.finos.org/release/1.2/prototype/interface/http-endpoint.json",
181+
"config": {
182+
"port": 6443,
183+
"protocol": "HTTPS"
184+
}
185+
}
186+
]
187+
},
188+
{
189+
"unique-id": "service-reclaim-agent",
190+
"node-type": "service",
191+
"name": "5-Spot Reclaim Agent",
192+
"description": "Node-side DaemonSet on the workload cluster that watches /proc for processes matching ScheduledMachine.spec.killIfCommands and, on match, writes reclaim annotations onto its own Node object. Runs in-pod with hostPID=true and host /proc mounted read-only; consumes its per-node ConfigMap via a kube API watcher scoped by field selector metadata.name=reclaim-agent-<NODE_NAME> in namespace 5spot-system. Distinct field manager (5spot-reclaim-agent) attributes every write.",
193+
"controls": {
194+
"least-privilege": {
195+
"description": "Agent holds a namespaced Role in 5spot-system granting get,list,watch on ConfigMaps only, plus the kubelet node-scoped token for patching its own Node's annotations. No cluster-wide writes.",
196+
"requirements": [
197+
{
198+
"requirement-url": "https://csrc.nist.gov/pubs/sp/800/53/r5/upd1/final",
199+
"config": {
200+
"framework": "NIST SP 800-53 Rev. 5",
201+
"controls": ["AC-3", "AC-6"],
202+
"evidence": "deploy/node-agent/rbac.yaml"
203+
}
204+
}
205+
]
206+
},
207+
"container-hardening": {
208+
"description": "Explicit pod-level securityContext, read-only root filesystem, host /proc mounted read-only. Architectural-necessity Trivy suppressions (KSV-0010/0012/0020/0021/0023/0105/0118/0121) are documented with rationale in .trivyignore.",
209+
"requirements": [
210+
{
211+
"requirement-url": "https://csrc.nist.gov/pubs/sp/800/190/final",
212+
"config": {
213+
"framework": "NIST SP 800-190",
214+
"controls": ["CM-6", "SI-7"],
215+
"evidence": "deploy/node-agent/daemonset.yaml"
216+
}
217+
}
218+
]
219+
}
220+
}
221+
},
222+
{
223+
"unique-id": "data-asset-reclaim-request-annotation",
224+
"node-type": "data-asset",
225+
"name": "Reclaim Request Annotation",
226+
"description": "Ephemeral annotation triple on a workload Node written by the reclaim agent and read by the 5-Spot controller: 5spot.finos.org/reclaim-requested=\"true\", /reclaim-reason, /reclaim-requested-at. Cleared by the controller as step 6 of the 7-step emergency-reclaim ordering contract."
227+
},
228+
{
229+
"unique-id": "data-asset-reclaim-agent-configmap",
230+
"node-type": "data-asset",
231+
"name": "Per-Node Reclaim Agent ConfigMap",
232+
"description": "Per-node ConfigMap named reclaim-agent-<node-name> in namespace 5spot-system on the workload cluster. Controller-projected from ScheduledMachine.spec.killIfCommands via server-side apply with field manager 5spot-controller-reclaim-agent; consumed by the agent via a kube runtime watcher scoped by field selector. Empty killIfCommands deletes the ConfigMap; 404-on-delete is benign by contract."
171233
}
172234
],
173235

@@ -190,11 +252,15 @@
190252
},
191253
{
192254
"unique-id": "rel-workload-cluster-contains-node",
193-
"description": "When a schedule is active, the physical node becomes a worker in the CAPI-managed workload cluster.",
255+
"description": "When a schedule is active, the physical node becomes a worker in the CAPI-managed workload cluster. The workload cluster also hosts its own API server and, where ScheduledMachine.spec.killIfCommands is set, the 5-Spot reclaim agent as a DaemonSet.",
194256
"relationship-type": {
195257
"deployed-in": {
196258
"container": "ecosystem-workload-cluster",
197-
"nodes": ["network-physical-node"]
259+
"nodes": [
260+
"network-physical-node",
261+
"service-workload-kubernetes-api",
262+
"service-reclaim-agent"
263+
]
198264
}
199265
}
200266
},
@@ -331,6 +397,84 @@
331397
"nodes": ["data-asset-scheduledmachine-cr"]
332398
}
333399
}
400+
},
401+
{
402+
"unique-id": "rel-controller-workload-kube-api",
403+
"description": "Controller reaches into the workload cluster API to project the per-node reclaim-agent ConfigMap (server-side apply), stamp the 5spot.finos.org/reclaim-agent=enabled label onto backing Nodes, watch Node updates for the reclaim-requested annotation, issue best-effort drains, and clear the reclaim annotations as step 6 of the emergency-reclaim contract.",
404+
"relationship-type": {
405+
"connects": {
406+
"source": { "node": "service-five-spot-controller" },
407+
"destination": {
408+
"node": "service-workload-kubernetes-api",
409+
"interfaces": ["workload-kube-api-https"]
410+
}
411+
}
412+
},
413+
"protocol": "HTTPS",
414+
"controls": {
415+
"audit-attribution": {
416+
"description": "All controller writes to the workload cluster API use the distinct field manager '5spot-controller-reclaim-agent' so audit logs attribute projection writes separately from the main reconciler. Supports non-repudiation in regulated-banking review.",
417+
"requirements": [
418+
{
419+
"requirement-url": "https://csrc.nist.gov/pubs/sp/800/53/r5/upd1/final",
420+
"config": {
421+
"framework": "NIST SP 800-53 Rev. 5",
422+
"controls": ["AU-2", "AU-10"],
423+
"evidence": "src/reconcilers/helpers.rs::reconcile_reclaim_agent_provision"
424+
}
425+
}
426+
]
427+
}
428+
}
429+
},
430+
{
431+
"unique-id": "rel-reclaim-agent-workload-kube-api",
432+
"description": "Reclaim agent uses the workload cluster API for two scoped operations: a field-selector-scoped watch on its own per-node ConfigMap in 5spot-system, and a merge-patch on its own Node's annotations via the kubelet node-scoped token on process match.",
433+
"relationship-type": {
434+
"connects": {
435+
"source": { "node": "service-reclaim-agent" },
436+
"destination": {
437+
"node": "service-workload-kubernetes-api",
438+
"interfaces": ["workload-kube-api-https"]
439+
}
440+
}
441+
},
442+
"protocol": "HTTPS",
443+
"controls": {
444+
"node-scoped-credentials": {
445+
"description": "Agent writes to Node objects only via the kubelet node-scoped token (patch nodes/<self>). ConfigMap reads are gated by a namespaced Role/RoleBinding in 5spot-system granting get,list,watch only. No cluster-wide API access.",
446+
"requirements": [
447+
{
448+
"requirement-url": "https://csrc.nist.gov/pubs/sp/800/53/r5/upd1/final",
449+
"config": {
450+
"framework": "NIST SP 800-53 Rev. 5",
451+
"controls": ["AC-6"],
452+
"evidence": "deploy/node-agent/rbac.yaml"
453+
}
454+
}
455+
]
456+
}
457+
}
458+
},
459+
{
460+
"unique-id": "rel-reclaim-configmap-stored-in-workload-api",
461+
"description": "The per-node reclaim-agent ConfigMap is persisted in the workload cluster API server's etcd, namespace 5spot-system.",
462+
"relationship-type": {
463+
"composed-of": {
464+
"container": "service-workload-kubernetes-api",
465+
"nodes": ["data-asset-reclaim-agent-configmap"]
466+
}
467+
}
468+
},
469+
{
470+
"unique-id": "rel-reclaim-annotation-on-node",
471+
"description": "The reclaim-request annotation triple lives on the physical Node's metadata and is the agent→controller signalling surface for emergency reclaim.",
472+
"relationship-type": {
473+
"composed-of": {
474+
"container": "network-physical-node",
475+
"nodes": ["data-asset-reclaim-request-annotation"]
476+
}
477+
}
334478
}
335479
],
336480

@@ -424,6 +568,64 @@
424568
]
425569
}
426570
}
571+
},
572+
{
573+
"unique-id": "flow-emergency-reclaim",
574+
"name": "Emergency Reclaim (process-match eject)",
575+
"description": "A matched process on a workload node triggers an ASAP eject that skips graceful drain. The 7-step ordering contract is load-bearing: spec.schedule.enabled=false (step 5) must be written BEFORE the annotation clear (step 6) so a crash between them is replay-safe. Full narrative in docs/src/concepts/emergency-reclaim.md.",
576+
"transitions": [
577+
{
578+
"sequence-number": 1,
579+
"relationship-unique-id": "rel-reclaim-agent-workload-kube-api",
580+
"description": "Agent detects a process matching spec.killIfCommands via /proc scan and PATCHes the reclaim annotation triple (5spot.finos.org/reclaim-requested=\"true\", /reclaim-reason, /reclaim-requested-at) onto its own Node via the kubelet node-scoped token. Field manager: 5spot-reclaim-agent."
581+
},
582+
{
583+
"sequence-number": 2,
584+
"relationship-unique-id": "rel-controller-workload-kube-api",
585+
"description": "Controller Node watch fires. check_emergency_reclaim reads the annotation, transitions status.phase = EmergencyRemove, and emits Event Reason: EmergencyReclaim with the annotation's reason string.",
586+
"direction": "destination-to-source"
587+
},
588+
{
589+
"sequence-number": 3,
590+
"relationship-unique-id": "rel-controller-workload-kube-api",
591+
"description": "Best-effort kubectl drain --grace-period=0 --force --disable-eviction against the workload cluster, bounded by EMERGENCY_DRAIN_TIMEOUT_SECS=60. Failure is log-and-continue — the eject has already been committed to."
592+
},
593+
{
594+
"sequence-number": 4,
595+
"relationship-unique-id": "rel-capi-core-reconciles-machine",
596+
"description": "Controller deletes the CAPI Machine on the management cluster — no PDB respect, no graceful shutdown. CAPI propagates deletion to providers and the physical node leaves the workload cluster."
597+
},
598+
{
599+
"sequence-number": 5,
600+
"relationship-unique-id": "rel-controller-kube-api",
601+
"description": "Controller PATCHes ScheduledMachine spec.schedule.enabled=false (the loop-breaker: without this, the next schedule window re-adds the node and the agent re-fires forever) and emits Event Reason: EmergencyReclaimDisabledSchedule. Load-bearing: must run BEFORE step 6."
602+
},
603+
{
604+
"sequence-number": 6,
605+
"relationship-unique-id": "rel-controller-workload-kube-api",
606+
"description": "Controller PATCHes the Node to null all three reclaim annotations. Best-effort: a crash between steps 5 and 6 is replay-safe because the agent re-annotates on the next poll cycle if the matched process is still running."
607+
},
608+
{
609+
"sequence-number": 7,
610+
"relationship-unique-id": "rel-controller-kube-api",
611+
"description": "Controller transitions ScheduledMachine status.phase = Disabled. Re-enable is a manual operator action (kubectl patch ... spec.schedule.enabled=true)."
612+
}
613+
],
614+
"controls": {
615+
"ordering-idempotence": {
616+
"description": "The load-bearing ordering of steps 5 (disable-schedule PATCH) before 6 (annotation-clear PATCH) guarantees crash-recovery idempotence. A crash after step 6 but before step 7 is caught by the PHASE_EMERGENCY_REMOVE match arm in reconcile(), which finishes the transition to Disabled on the next reconcile.",
617+
"requirements": [
618+
{
619+
"requirement-url": "https://csrc.nist.gov/pubs/sp/800/53/r5/upd1/final",
620+
"config": {
621+
"framework": "NIST SP 800-53 Rev. 5",
622+
"controls": ["SI-7", "AU-2"],
623+
"evidence": "src/reconcilers/helpers.rs::handle_emergency_remove, docs/src/concepts/emergency-reclaim.md"
624+
}
625+
}
626+
]
627+
}
628+
}
427629
}
428630
],
429631

0 commit comments

Comments
 (0)