Skip to content

Commit 27ce7df

Browse files
committed
Initial phase 1-3 of a per-node Kata config delivery
Closes: #77 Signed-off-by: Erick Bourgeois <erick@jeb.ca>
1 parent ad7b3ea commit 27ce7df

25 files changed

Lines changed: 2544 additions & 4 deletions

.claude/CHANGELOG.md

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

1010
---
1111

12+
## [2026-06-09 16:30] - Implement spec.kata workload-read delivery (ADR 0002)
13+
14+
**Author:** Erick Bourgeois
15+
16+
### Changed
17+
- `src/crd.rs`: rename `spec.kataConfigRef`/`KataConfigRef``spec.kata`/`KataConfig`;
18+
add `namespace` field (workload-cluster target, default `5spot-system`) with
19+
schema + default; docs reflect workload-cluster resolution.
20+
- `src/reconcilers/helpers.rs`: replace the per-node ConfigMap projection
21+
(`reconcile_kata_config_provision`, `build_kata_config_configmap`,
22+
`per_node_kata_configmap_name`, `extract_*`, `resolve_kata_config_content`)
23+
with `reconcile_kata_config_delivery` (read-only workload `get_opt` of the
24+
source object + namespace probe → Node opt-in label/annotation, or fail-fast
25+
`SourceNotFound`/`TargetNamespaceMissing`), `build_kata_config_ref_annotation_patch`,
26+
and the `KataDeliveryOutcome` enum. No controller ConfigMap/Secret writes.
27+
- `src/reconcilers/scheduled_machine.rs`: call site reads on the workload client,
28+
logs the fail-fast outcome.
29+
- `src/constants.rs`: add `KATA_CONFIG_REF_ANNOTATION`; drop the
30+
projection-only `KATA_CONFIG_CONFIGMAP_PREFIX` / `KATA_CONFIG_DATA_KEY` /
31+
`KATA_CONFIG_FIELD_MANAGER`.
32+
- `src/crd_tests.rs`, `src/reconcilers/helpers_tests.rs`: migrate tests to the
33+
new contract (namespace default/override, annotation builders, mock-API
34+
delivery tests for present / source-missing / namespace-missing / disabled);
35+
remove obsolete projection tests. 555 lib tests pass.
36+
- `src/bin/crddoc.rs`: regenerate API docs text for `spec.kata`.
37+
- `deploy/crds/scheduledmachine.yaml`, `docs/src/reference/api.md`: regenerated.
38+
- `deploy/deployment/rbac/clusterrole.yaml`: kata is read-only (`configmaps: get`
39+
+ new `namespaces: get`); the `configmaps` write verbs are re-attributed to the
40+
reclaim-agent projection (the honest KSV-0049 owner). `.trivyignore`: re-add
41+
`AVD-KSV-0049` attributed to reclaim.
42+
- `docs/architecture/calm/architecture.json`: kata flow updated to read-not-project;
43+
`make calm-validate` clean; diagrams regenerated.
44+
- `docs/adr/0002-kata-config-delivery-via-spec-kata.md`: §5/Consequences corrected
45+
— kata adds no write privilege but does NOT eliminate KSV-0049 (reclaim still
46+
holds it).
47+
48+
### Why
49+
Implements the approved ADR 0002 redesign: 5-Spot resolves the Flux-delivered
50+
kata object on the workload cluster and opts the Node in (or fails fast with a
51+
status reason), instead of projecting per-node ConfigMaps. Removes duplication,
52+
the controller-owned sync burden, and kata's contribution to the ConfigMap write
53+
grant.
54+
55+
### Impact
56+
- [x] Breaking change (CRD field `spec.kataConfigRef``spec.kata`; unreleased)
57+
- [ ] Requires cluster rollout
58+
- [ ] Config change only
59+
- [ ] Documentation only
60+
61+
## [2026-06-09 15:00] - Redesign kata config delivery (ADR 0002): workload-read, no writes
62+
63+
**Author:** Erick Bourgeois
64+
65+
### Changed
66+
- `docs/adr/0002-kata-config-delivery-via-spec-kata.md`: rewritten (and renamed
67+
from `0002-per-node-kata-config-delivery.md`) to the accepted design — replace
68+
`spec.kataConfigRef` with `spec.kata`; the controller **resolves** the kata
69+
object on the workload cluster (via `kubeconfig-<clusterName>`) and either opts
70+
the Node in (label + reference annotation) or **fails fast** with a status
71+
reason (`SourceNotFound` / `TargetNamespaceMissing`). 5-Spot performs **no**
72+
ConfigMap/Secret writes and **no** namespace creation for kata delivery; the
73+
config must pre-exist (Flux-delivered). Target namespace defaults to
74+
`5spot-system`, overridable. KSV-0049 eliminated by construction.
75+
- `docs/adr/0003-in-pod-host-service-restart-via-nsenter.md`: aligned with the
76+
above — controller reads (not projects); agent consumes the object via the
77+
workload kube API keyed by Node annotation (no ConfigMap volume); `spec.kata`.
78+
- `docs/adr/README.md`: index reflects the consolidated set (0001–0003).
79+
- Removed the two intermediate, never-committed-to-main ADRs (the per-node
80+
`spec.kataConfigRef` projection and its KSV-0049 ownership/RBAC defense) and
81+
reverted this session's per-node documentation (concept page, `.trivyignore`
82+
`AVD-KSV-0049`, threat-model rows, `clusterrole.yaml` / `helpers.rs` comments).
83+
84+
### Why
85+
Review against the platform model (tenants create nothing; Flux delivers config;
86+
a tenant namespace backs 1+ k0smotron control planes; the agent runs on the
87+
workload cluster the controller already has kubeconfig access to) rejected the
88+
per-node ConfigMap projection: wrong granularity, controller-owned duplication, a
89+
sync burden 5-Spot shouldn't own, and an unnecessary ConfigMap write grant. The
90+
accepted design makes 5-Spot a read-only resolver + opt-in labeler that fails
91+
fast when config is absent.
92+
93+
### Impact
94+
- [ ] Breaking change
95+
- [ ] Requires cluster rollout
96+
- [ ] Config change only
97+
- [x] Documentation only (ADR/design; implementation tracked separately)
98+
99+
## [2026-06-09 01:00] - Kata config delivery Phase 3 — node agent host-sync engine
100+
101+
**Author:** Erick Bourgeois
102+
103+
### Changed
104+
- `src/kata_config_agent.rs`: new host-filesystem sync engine — `sha256_hex`,
105+
`file_sha256`, `SyncAction` + `decide_action` (pure decision), `atomic_write`
106+
(temp + `rename`, mode 0644, parent-dir create, crash-safe), `remove_if_present`,
107+
and `sync_once``SyncOutcome` (Wrote/Deleted/Unchanged). Drift self-heals.
108+
- `src/kata_config_agent_tests.rs`: 22 unit tests (SHA-256 vectors, decision
109+
matrix, atomic write incl. mode/parent-dirs/no-temp-leak/crash-safety,
110+
idempotent unlink, end-to-end sync incl. drift correction + GitOps delete).
111+
- `src/bin/kata_config_agent.rs`: new `5spot-kata-config-agent` binary — clap
112+
CLI (`SOURCE_PATH`/`DEST_PATH`/`NODE_NAME`/`POLL_INTERVAL_SECS`/`--oneshot`)
113+
driving a drift-watch poll loop over `sync_once`.
114+
- `src/lib.rs`: export `kata_config_agent` module.
115+
- `Cargo.toml`: add `sha2` dependency (already in the graph transitively) and
116+
the `5spot-kata-config-agent` bin target.
117+
118+
### Why
119+
Phase 3 of ADR 0003 — land the drop-in on the host and self-heal drift. The
120+
applied-hash node annotation and the `nsenter` host k0s-service restart are
121+
Phase 4; the DaemonSet manifest, agent RBAC, `.trivyignore`, and metrics land
122+
in Phase 4 with the final (privileged) security posture so they are decided as
123+
one unit. The drift-watch is a deliberate node-local poll — out-of-band host
124+
edits raise no Kubernetes event to watch on.
125+
126+
### Impact
127+
- [ ] Breaking change
128+
- [ ] Requires cluster rollout
129+
- [ ] Config change only
130+
- [ ] Documentation only
131+
132+
> New binary only; not yet deployed (no DaemonSet until Phase 4). No controller
133+
> or CRD behavior change. Verified end-to-end via `--oneshot`: write → no-op →
134+
> delete, parent dirs created, no temp-file leakage.
135+
136+
---
137+
138+
## [2026-06-09 00:00] - Kata config delivery Phase 2 — controller projection + RBAC
139+
140+
**Author:** Erick Bourgeois
141+
142+
### Changed
143+
- `src/constants.rs`: new kata-config constants — `KATA_CONFIG_LABEL`
144+
(`5spot.finos.org/kata-config`), `KATA_CONFIG_LABEL_ENABLED`,
145+
`KATA_CONFIG_NAMESPACE`, `KATA_CONFIG_CONFIGMAP_PREFIX`,
146+
`KATA_CONFIG_DATA_KEY`, `KATA_CONFIG_FIELD_MANAGER`
147+
(`5spot-controller-kata-config`).
148+
- `src/reconcilers/helpers.rs`: kata projection — pure builders
149+
`per_node_kata_configmap_name`, `build_kata_config_configmap`,
150+
`build_kata_config_label_patch`; content extractors
151+
`extract_kata_content_from_configmap` / `_from_secret`; async
152+
`resolve_kata_config_content` and `reconcile_kata_config_provision`
153+
(resolve source → stamp label → SSA per-node ConfigMap; tear down on
154+
`None`, 404-on-delete benign). Mirrors the reclaim-agent projection.
155+
- `src/reconcilers/helpers_tests.rs`: 18 new tests — pure builders/extractors
156+
(happy/missing-key/non-UTF-8) plus five mock-client orchestrator tests
157+
(ConfigMap source, Secret source w/ custom key, tear-down, 404-benign,
158+
source-404 short-circuit).
159+
- `src/reconcilers/scheduled_machine.rs`: `provision_kata_config_best_effort`
160+
wired into `handle_active_phase` beside reclaim/taints — child-cluster-scoped
161+
writes, management-cluster source read, non-fatal.
162+
- `deploy/deployment/rbac/clusterrole.yaml`: new `configmaps`
163+
get/create/patch/delete rule (first such rule); secrets rule comment notes
164+
kataConfigRef source reads.
165+
166+
### Why
167+
Phase 2 of ADR 0002 — project the referenced kata drop-in into a per-node
168+
ConfigMap on the child cluster and stamp the opt-in label, so the Phase 3 node
169+
agent can consume it. Implements the `rel-controller-kata-config-projection`
170+
relationship already modeled in CALM.
171+
172+
### Impact
173+
- [ ] Breaking change
174+
- [x] Requires cluster rollout
175+
- [ ] Config change only
176+
- [ ] Documentation only
177+
178+
> Apply the updated controller ClusterRole (new `configmaps` rule) before
179+
> rolling out the controller, or kata-config projection will fail with RBAC
180+
> Forbidden. Behavior is inert for ScheduledMachines without `kataConfigRef`.
181+
182+
---
183+
184+
## [2026-06-08 01:00] - Kata config delivery Phase 1 — CRD `spec.kataConfigRef`
185+
186+
**Author:** Erick Bourgeois
187+
188+
### Changed
189+
- `src/crd.rs`: new `kata_config_ref: Option<KataConfigRef>` field on
190+
`ScheduledMachineSpec`; new `KataConfigRef` struct (kind/name/key/destPath/
191+
restartService, `deny_unknown_fields`, in-namespace only) and
192+
`KataConfigSourceKind` enum (`ConfigMap`/`Secret`); four field schema
193+
functions and three default functions.
194+
- `src/crd_tests.rs`: 12 new TDD tests (serde defaults, round-trip, PascalCase
195+
kind, rejects invalid kind / unknown fields, required kind+name, optional in
196+
schema, name/destPath/restartService schema bounds). Existing spec literals
197+
updated with `kata_config_ref: None`.
198+
- `src/reconcilers/{scheduled_machine_tests,helpers_tests,child_client_tests}.rs`,
199+
`tests/integration_child_kubeconfig.rs`: spec literals updated with
200+
`kata_config_ref: None`.
201+
- `src/bin/crddoc.rs`: documents the new `kataConfigRef` field + example.
202+
- `deploy/crds/scheduledmachine.yaml`, `docs/src/reference/api.md`: regenerated
203+
(`make crds``make crddoc`).
204+
205+
### Why
206+
Phase 1 of ADR 0002 — establish the `spec.kataConfigRef` CRD contract (types,
207+
schema bounds, serde defaults) test-first before the controller projection
208+
(Phase 2) and node agent (Phases 3–4). No behavioral/controller code yet.
209+
210+
### Impact
211+
- [ ] Breaking change
212+
- [ ] Requires cluster rollout
213+
- [x] Config change only
214+
- [ ] Documentation only
215+
216+
> CRD schema change (additive, optional field). Apply the regenerated
217+
> `deploy/crds/scheduledmachine.yaml` to pick up `kataConfigRef`; existing
218+
> ScheduledMachines are unaffected (field is optional, omitted when unset).
219+
220+
---
221+
222+
## [2026-06-08 00:00] - ADR + CALM: per-node Kata config delivery (design only)
223+
224+
**Author:** Erick Bourgeois
225+
226+
### Changed
227+
- `docs/adr/0002-per-node-kata-config-delivery.md`: new ADR (Proposed) — the
228+
`spec.kataConfigRef` contract and controller-side per-node ConfigMap
229+
projection, mirroring the reclaim-agent projection; in-namespace source only
230+
(like `KubeconfigSecretRef`).
231+
- `docs/adr/0003-in-pod-host-service-restart-via-nsenter.md`: new ADR (Proposed)
232+
— the privileged `5spot-kata-config-agent` DaemonSet that writes the host
233+
drop-in and restarts the host k0s service via `nsenter -t 1 … systemctl
234+
restart`, with the applied-hash annotation as the restart-loop guard.
235+
- `docs/adr/README.md`: index updated with ADR-0002 and ADR-0003.
236+
- `docs/architecture/calm/architecture.json`: modeled the feature — new nodes
237+
`service-kata-config-agent` and `data-asset-kata-config-configmap`; new
238+
relationships `rel-controller-kata-config-projection`,
239+
`rel-kata-agent-workload-kube-api`, `rel-kata-agent-writes-host`,
240+
`rel-kata-configmap-stored-in-workload-api`; new flow
241+
`flow-kata-config-delivery`; `adrs` linked. `make calm-validate` passes;
242+
`make calm-diagrams` renders.
243+
244+
### Why
245+
ADD requires the decision to be recorded and modeled before code. This entry
246+
covers the ADR + CALM deliverables only; no Rust, CRD, or manifest changes yet —
247+
implementation (TDD) is gated on review of these two ADRs.
248+
249+
### Impact
250+
- [ ] Breaking change
251+
- [ ] Requires cluster rollout
252+
- [ ] Config change only
253+
- [x] Documentation only
254+
255+
---
256+
12257
## [2026-05-31 00:00] - Adopt Architecture Driven Development (ADD) methodology
13258

14259
**Author:** Erick Bourgeois

.trivyignore

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,23 @@ AVD-KSV-0048
4141
# which is impractical for a cluster-wide controller.
4242
AVD-KSV-0041
4343

44+
# KSV-0049 — ClusterRole should not have create/update/patch/delete on
45+
# 'configmaps'.
46+
#
47+
# The `configmaps: create/patch/delete` verbs belong to the **reclaim-agent**
48+
# per-node ConfigMap projection (`reclaim-agent-<node>`), NOT to kata config
49+
# delivery (kata is read-only — `configmaps: get` — per ADR 0002). They are
50+
# exercised only in the degenerate single-cluster case where the resolved
51+
# client is the controller's own ServiceAccount; in true multi-cluster the
52+
# projection rides the `kubeconfig-<clusterName>` identity, not this token.
53+
# Scope is `get, create, patch, delete` — NOT `update`, `deletecollection`,
54+
# `impersonate`, or `*`. A ClusterRole cannot be scoped by name prefix or
55+
# namespace, so the grant is cluster-wide; the blast radius is bounded by
56+
# convention (the controller writes only the `reclaim-agent-*` names it owns,
57+
# via a dedicated SSA field manager). Fully removing the verbs would require
58+
# moving the reclaim projection off the controller SA — tracked separately.
59+
AVD-KSV-0049
60+
4461
# ─────────────────────────────────────────────────────────────────────────────
4562
# Deployment — deploy/deployment/deployment.yaml
4663
# ─────────────────────────────────────────────────────────────────────────────

Cargo.lock

Lines changed: 1 addition & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Cargo.toml

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -68,6 +68,9 @@ lazy_static = "1.5"
6868
# enabled explicitly.
6969
warp = { version = "0.4", default-features = false, features = ["server"] }
7070
toml = "1.1"
71+
# SHA-256 for the kata-config agent's content/drift hashing and (Phase 4)
72+
# the applied-hash node annotation. RustCrypto, pure-Rust, no system deps.
73+
sha2 = "0.10"
7174

7275
# Linux-only — used by the reclaim-agent's rung 2 netlink proc connector
7376
# subscriber (`src/netlink_proc.rs`). The portable byte parsers in that
@@ -81,6 +84,10 @@ nix = { version = "0.31", default-features = false, features = ["socket", "net",
8184
name = "5spot-reclaim-agent"
8285
path = "src/bin/reclaim_agent.rs"
8386

87+
[[bin]]
88+
name = "5spot-kata-config-agent"
89+
path = "src/bin/kata_config_agent.rs"
90+
8491
[dev-dependencies]
8592
tokio-test = "0.4"
8693
mockall = "0.14"

deploy/crds/scheduledmachine.yaml

Lines changed: 83 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -134,6 +134,89 @@ spec:
134134
- kind
135135
- spec
136136
type: object
137+
kata:
138+
description: |-
139+
Optional reference to a `Secret` or `ConfigMap` on the **workload
140+
cluster** holding a Kata containerd drop-in to deliver to the node(s) this
141+
resource owns.
142+
143+
When set, the controller resolves the referenced object on the workload
144+
cluster (via the `kubeconfig-<clusterName>` Secret) in `kata.namespace`
145+
(default `5spot-system`). If it is present, the controller stamps the
146+
`5spot.finos.org/kata-config=enabled` opt-in label **and** a reference
147+
annotation on the backing Node; the `5spot-kata-config-agent` DaemonSet —
148+
scheduled onto labelled nodes — reads the object from the workload API,
149+
writes the drop-in to `destPath`, and restarts `restartService` so
150+
containerd reloads it. If the object (or its namespace) is absent, the
151+
controller does **not** label the Node and reports a fail-fast status
152+
condition — 5-Spot never creates the object (it must pre-exist,
153+
Flux-delivered).
154+
155+
This is config *delivery*, not a Kata install: the `/opt/kata` binaries
156+
remain `kata-deploy`'s responsibility, and the existing
157+
`katacontainers.io/kata-runtime` opt-in label is unaffected. See ADR 0002
158+
and ADR 0003.
159+
nullable: true
160+
properties:
161+
destPath:
162+
default: /etc/k0s/container.d/kata-containers.toml
163+
description: |-
164+
Absolute host path the agent writes the drop-in to. Defaults to
165+
`/etc/k0s/container.d/kata-containers.toml` (the k0s containerd drop-in
166+
directory). Configurable because layouts vary (k0s-in-docker, kairos,
167+
vanilla containerd).
168+
maxLength: 4096
169+
minLength: 1
170+
pattern: ^/[^\u0000]*$
171+
type: string
172+
key:
173+
default: kata-containers.toml
174+
description: |-
175+
Key within the source's `data` map whose value is the drop-in content.
176+
Defaults to `kata-containers.toml`.
177+
maxLength: 253
178+
minLength: 1
179+
pattern: ^[A-Za-z0-9._-]+$
180+
type: string
181+
kind:
182+
description: Whether the source is a `ConfigMap` or a `Secret`. Required (no default).
183+
enum:
184+
- ConfigMap
185+
- Secret
186+
type: string
187+
name:
188+
description: |-
189+
Name of the source object on the workload cluster. RFC-1123 DNS subdomain
190+
(max 253 chars).
191+
maxLength: 253
192+
minLength: 1
193+
pattern: ^[a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*$
194+
type: string
195+
namespace:
196+
default: 5spot-system
197+
description: |-
198+
Workload-cluster namespace the agent reads the object from. Defaults to
199+
`5spot-system` (the agent's own namespace, so no cross-namespace agent
200+
RBAC is needed). Override to place config in a per-tenant namespace.
201+
maxLength: 63
202+
minLength: 1
203+
pattern: ^[a-z0-9]([-a-z0-9]*[a-z0-9])?$
204+
type: string
205+
restartService:
206+
default: k0sworker.service
207+
description: |-
208+
systemd unit the node agent restarts (via `nsenter`) so containerd
209+
reloads the drop-in. Defaults to `k0sworker.service`; override with
210+
`k0scontroller.service` on single-node / controller-runs-workloads
211+
layouts.
212+
maxLength: 255
213+
minLength: 1
214+
pattern: ^[A-Za-z0-9@._-]+\.service$
215+
type: string
216+
required:
217+
- kind
218+
- name
219+
type: object
137220
killIfCommands:
138221
description: |-
139222
Optional list of process patterns that trigger an emergency node

0 commit comments

Comments
 (0)