You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
Copy file name to clipboardExpand all lines: .claude/CHANGELOG.md
+113Lines changed: 113 additions & 0 deletions
Display the source diff
Display the rich diff
Original file line number
Diff line number
Diff line change
@@ -9,6 +9,119 @@ The format is based on the regulated environment requirements:
9
9
10
10
---
11
11
12
+
## [2026-04-19 15:30] - Sign binaries and generate SLSA provenance on push-to-main (not only on release)
13
+
14
+
**Author:** Erick Bourgeois
15
+
16
+
### Changed
17
+
-`.github/workflows/build.yaml`:
18
+
-`sign-artifacts` job: `if` flipped from `github.event_name == 'release'` to `!= 'pull_request'`. Added `attestations: write` and `contents: read` permissions. New step runs `actions/attest-build-provenance@v2` on each signed tarball (amd64 + arm64) — GitHub-native attestation in addition to the Cosign signature.
19
+
-`generate-provenance-subjects` job: `if` flipped to `!= 'pull_request'` so SLSA subject hashes are computed on push-to-main too.
20
+
-`slsa-provenance` job: `if` flipped to `!= 'pull_request'`; the reusable SLSA generator now runs on every push-to-main. `upload-assets` parameterised to `${{ github.event_name == 'release' }}` so the `.intoto.jsonl` only attaches to the GitHub Release on release events; on push-to-main it lands as a workflow artifact (verifiable via `slsa-verifier` or `gh attestation verify`).
21
+
-`upload-release-assets` unchanged — stays `if: github.event_name == 'release'`; its `needs:` on `sign-artifacts`/`slsa-provenance` resolves cleanly whether those jobs ran (push, release) or were skipped (PR).
22
+
23
+
### Why
24
+
Every merge to `main` produces a buildable, distributable artifact — it should carry the same supply-chain assurances as a release. With these changes, a `main` build now has: Cosign keyless signature on the container image (already in place), GitHub Artifact Attestation on the container image (already in place), Cosign signature on each binary tarball (new on `main`), GitHub Artifact Attestation on each binary tarball (new), and SLSA Level 3 provenance for the binaries (new on `main`). Consumers pulling a `main-YYYY-MM-DD` build can now cryptographically verify it the same way they would a release.
25
+
26
+
### Impact
27
+
-[ ] Breaking change
28
+
-[ ] Requires cluster rollout
29
+
-[x] Config change only (CI/CD workflow)
30
+
-[ ] Documentation only
31
+
32
+
### Operational note
33
+
- Push-to-main runs now take longer (extra ~3–5 min for the SLSA generator + attestations). Expected trade-off.
34
+
-`sign-artifacts` signed-tarball artifact retention is the repo default (90 days) — long enough for verification and audit, well under the release-asset lifetime.
35
+
- Binary SLSA generator dependency pinned at `@v2.1.0` (unchanged) — verify this is the latest patched version periodically.
-`.github/workflows/scorecard.yaml`: Prepended the project's standard two-line SPDX/Copyright header (`Copyright (c) 2025 Erick Bourgeois, firestoned` + `SPDX-License-Identifier: Apache-2.0`) to match every other workflow in `.github/workflows/`. Pinned the previously unpinned `github/codeql-action/upload-sarif@v3` to the full commit SHA for v3.35.2 (`ce64ddcb0d8d890d2df4a9d1c04ff297367dea2a`) with an inline version comment — brings the last step into line with the other three actions in the file which were already SHA-pinned.
45
+
46
+
### Why
47
+
OpenSSF Scorecard's own `Pinned-Dependencies` check flags float-tagged actions (`@v3`) as a supply-chain risk, so the scorecard workflow itself should score well on that check. The SPDX header is the repo-wide convention; every other workflow has it. Both changes are compliance housekeeping — no behavior change.
48
+
49
+
### Impact
50
+
-[ ] Breaking change
51
+
-[ ] Requires cluster rollout
52
+
-[x] Config change only
53
+
-[ ] Documentation only
54
+
55
+
---
56
+
57
+
## [2026-04-19 14:00] - Phases 3 + 4: Event-driven `.watches()` on CAPI Machine and Node
58
+
59
+
**Author:** Erick Bourgeois
60
+
61
+
### Changed
62
+
-`src/reconcilers/helpers.rs`: Added two pure mapper functions used as secondary-watch reverse-maps on the Controller builder:
63
+
-`machine_to_scheduled_machine(&DynamicObject) -> Vec<ObjectRef<ScheduledMachine>>` — reads the `5spot.eribourg.dev/scheduled-machine` label (constant `LABEL_SCHEDULED_MACHINE`) and emits at most one `ObjectRef`. Guards against missing/empty/whitespace label values, missing namespace, and imposter labels with similar prefixes.
64
+
-`node_to_scheduled_machines<'a, I: IntoIterator<Item = &'a ScheduledMachine>>(&Node, I) -> Vec<ObjectRef<ScheduledMachine>>` — returns all SMs whose `status.nodeRef.name` matches `node.metadata.name`. Returns multiple refs on conflict so the reconciler can surface the issue. O(N) per Node event; documented as acceptable for current scale.
65
+
-`src/reconcilers/mod.rs`: Re-exports both mappers alongside the existing `error_policy`, `evaluate_schedule`, `should_process_resource`.
66
+
-`src/main.rs`: Extended the `Controller` builder with `.watches_with(...)` on CAPI `Machine` (`ApiResource` from GVK, label-filtered `watcher::Config`) and `.watches(...)` on `Node`. The Node mapper closure captures a clone of `controller.store()` (`reflector::Store<ScheduledMachine>`) and calls `state()` on each event to avoid out-of-band API lists.
67
+
-`src/reconcilers/helpers_tests.rs`: Added 14 new unit tests — 7 for `machine_to_scheduled_machine` (positive match, missing label, no labels, empty value, whitespace value, missing namespace, wrong-prefix imposter) and 7 for `node_to_scheduled_machines` (single match, multi-match conflict, no match, empty list, SM-without-status, Node-without-name, empty `nodeRef.name`). Covers positive, negative, and exception paths per the project's 100%-coverage rule.
68
+
69
+
### Why
70
+
Closes roadmap Phases 3 + 4 of `event-driven-watches-and-status-enrichment.md`. The `ScheduledMachine` controller previously only watched its own CR — all downstream state was polled during reconciles, which CLAUDE.md explicitly forbids ("ALWAYS use event-driven programming… as opposed to polling"). With these two watches, CAPI setting `status.nodeRef` or a Node being drained now enqueues an immediate reconcile via kube-rs's watch stream (<1s debounce) instead of waiting for the next periodic requeue. The label-based reverse map on Machines uses the label we already stamp on every child — zero new ownership semantics, no `controller: true` owner references introduced, no contention with CAPI's own controllers.
71
+
72
+
### Impact
73
+
-[ ] Breaking change
74
+
-[x] Requires cluster rollout — controller now opens additional watches (CAPI `Machine` with label selector; `Node` cluster-wide)
75
+
-[ ] Config change only
76
+
-[ ] Documentation only
77
+
78
+
### Operational note
79
+
- RBAC: the controller already has `get`/`list`/`watch` on CAPI `Machine` (used for drain lookup) and needs `list`/`watch` on core `Node`. Verify `get,list,watch` on `nodes` is present in the ClusterRole before rollout; if not, add it in the same rollout.
80
+
- Observability: the existing `reconcile_queue_depth` metric will show enqueues driven by Machine and Node events in addition to SM events.
81
+
82
+
---
83
+
84
+
## [2026-04-19 10:00] - Phase 2: Close test-coverage gap for async CAPI helpers
85
+
86
+
**Author:** Erick Bourgeois
87
+
88
+
### Changed
89
+
-`src/reconcilers/helpers_tests.rs`: Added 14 `tower-test`-backed unit tests covering positive, negative, and exception paths for the three async helpers introduced in the previous Phase 2 entry:
90
+
-`fetch_capi_machine` — 200 returns `Some`, 404 returns `Ok(None)`, 500 and 403 map to `CapiError`.
91
+
-`patch_machine_refs_status` — both-fields patch asserts full body shape, single-field patches assert the other key is **omitted** (not null) so merge-patch never clears existing values, both-`None` case asserts **zero HTTP traffic**, 500 and 404 map to `KubeError`.
92
+
-`get_node_from_machine` — success returns the node name, machine-404 and nodeRef-missing both return `Ok(None)`, 500 propagates as `CapiError`.
93
+
94
+
### Why
95
+
Per durable user guidance: every function (public **and** private) must have unit tests covering the happy path, negative cases, and exception/error paths. The original Phase 2 change landed with tests only for the pure `extract_machine_refs` function — the three async helpers, which actually touch the Kubernetes API surface, were undertested. This entry closes that gap so the coverage floor now matches the project rule (not just CLAUDE.md's "public function" minimum).
96
+
97
+
### Impact
98
+
-[ ] Breaking change
99
+
-[ ] Requires cluster rollout
100
+
-[ ] Config change only
101
+
-[x] Documentation only (test-only change — no runtime behaviour modified)
102
+
103
+
---
104
+
105
+
## [2026-04-18 12:00] - Phase 2: Populate providerID and nodeRef from CAPI Machine
106
+
107
+
**Author:** Erick Bourgeois
108
+
109
+
### Changed
110
+
-`src/reconcilers/helpers.rs`: Added three new helpers — `extract_machine_refs` (pure function pulling `providerID` + full `NodeRef` out of a CAPI Machine `DynamicObject`), `fetch_capi_machine` (typed 404 → `Ok(None)` wrapper around the dynamic GET), and `patch_machine_refs_status` (merge-patches both fields onto `ScheduledMachine.status`). Refactored `get_node_from_machine` to delegate to the first two helpers for the drain path.
111
+
-`src/reconcilers/scheduled_machine.rs`: `handle_active_phase` happy path now fetches the CAPI Machine, extracts refs, and patches them onto the SM status. Failures are logged and ignored — status enrichment must never block reconciliation.
112
+
-`src/reconcilers/helpers_tests.rs`: Added 6 TDD cases for `extract_machine_refs` covering fully populated, empty, providerID-only, nodeRef-without-uid, incomplete-nodeRef-returns-None, and malformed-providerID-ignored.
113
+
114
+
### Why
115
+
Phase 2 of the event-driven watches + status enrichment roadmap. The schema landed in Phase 1; this change actually populates the new fields every time the reconciler visits an active machine, so `kubectl get sm -o jsonpath='{.status.providerID}{"\t"}{.status.nodeRef.name}'` returns meaningful values.
116
+
117
+
### Impact
118
+
-[ ] Breaking change
119
+
-[x] Requires cluster rollout — controller image carries the new reconcile logic
120
+
-[ ] Config change only
121
+
-[ ] Documentation only
122
+
123
+
---
124
+
12
125
## [2026-04-16 17:00] - Phase 1: Enrich ScheduledMachine status with providerID and full nodeRef
0 commit comments