Skip to content

Commit 417f73a

Browse files
committed
Add suopport for netlink, EventStreams, see https://www.kernel.org/doc/html/latest/driver-api/connector.html
Signed-off-by: Erick Bourgeois <erick@jeb.ca>
1 parent 608a93c commit 417f73a

26 files changed

Lines changed: 3039 additions & 61 deletions

.claude/CHANGELOG.md

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

1010
---
1111

12+
## [2026-05-02 23:30] - Emergency-reclaim roadmap closeout: drain stopwatch + loop protection + integration tests
13+
14+
**Author:** Erick Bourgeois
15+
16+
### Added
17+
- `src/loop_protection.rs` (~75 LOC) + `src/loop_protection_tests.rs`
18+
(14 pure-helper tests). Module exposes `prune_old_reclaim_events`,
19+
`record_emergency_reclaim_event`, and `should_warn_rapid_re_reclaim`
20+
— all operate on `&mut VecDeque<DateTime<Utc>>` so the tests drive
21+
every branch with no clock or controller. Threshold + window come
22+
from new constants in `src/constants.rs`
23+
(`RAPID_RE_RECLAIM_THRESHOLD = 3`,
24+
`RAPID_RE_RECLAIM_WINDOW_SECS = 600`,
25+
`RAPID_RE_RECLAIM_MAX_TRACKED = 10`,
26+
`REASON_RAPID_RE_RECLAIM = "RapidReReclaim"`).
27+
- `Context.recent_reclaims:
28+
Arc<Mutex<HashMap<String, VecDeque<DateTime<Utc>>>>>` — in-memory
29+
per-SM tracker, keyed by `"namespace/name"`. Documented choice:
30+
no CRD schema bump, no status-patch round trip; controller restart
31+
resets the count, which the metric still captures on a trend basis.
32+
- `src/metrics.rs`: three new metrics with helpers —
33+
`fivespot_emergency_drain_duration_seconds` (HistogramVec,
34+
outcome={success|timeout|error}, buckets sized for the 60 s drain
35+
ceiling), `fivespot_emergency_reclaims_total` (CounterVec by
36+
namespace+name), `fivespot_rapid_re_reclaims_total` (CounterVec by
37+
namespace+name). Helpers `record_emergency_drain`,
38+
`record_emergency_reclaim`, `record_rapid_re_reclaim` plus
39+
`EmergencyDrainOutcome` enum. 4 new tests in `metrics_tests.rs`.
40+
- `src/reconcilers/helpers.rs`: drain call in `handle_emergency_remove`
41+
is now wrapped in an `Instant::now()` stopwatch; outcome classified
42+
by error message (timeout substring → Timeout, other Err → Error,
43+
Ok → Success). Per-SM emergency-reclaim counter bumped once per
44+
flow (the idempotent recovery handler short-circuits before this
45+
point on restart). Loop-protection check fires after the bump:
46+
appends timestamp, prunes old entries, emits `RapidReReclaim`
47+
Warning Event + bumps the new metric when threshold crosses. New
48+
pure helper `build_rapid_re_reclaim_event(name)`.
49+
- `tests/integration_netlink_proc.rs` — Linux-only (`#![cfg(target_os
50+
= "linux")]`) runtime test for the netlink subscriber. Two
51+
`#[ignore]` cases: one targets a spawned `/bin/true` child and
52+
asserts <100 ms detection latency per issue #40 acceptance
53+
criterion; one is a quieter "subscriber sees *some* event"
54+
smoke test.
55+
- `tests/integration_emergency_reclaim.rs` — kind-cluster annotation
56+
contract test (graceful-skip if no cluster, `#[ignore]`). Two cases:
57+
agent's `build_patch_body` round-trips through the controller's
58+
`node_reclaim_request` parser; partial annotations (reason set,
59+
requested=true absent) parse to `None`. Plus one always-on
60+
constants-pinning test.
61+
62+
### Changed
63+
- `src/reconcilers/mod.rs`: re-export `build_clear_reclaim_patch`,
64+
`node_reclaim_request`, and `ReclaimRequest` from `helpers` so the
65+
kind-cluster test can use the published API surface.
66+
- Roadmap `~/dev/roadmaps/5spot-emergency-reclaim-by-process-match.md`
67+
→ renamed `completed-5spot-emergency-reclaim-by-process-match.md`
68+
(per the user-memory rule "rename completed roadmaps with
69+
`completed-` prefix when every phase is done"). Status header
70+
rewritten to ✅ Complete; Phase 2.c rung 2 section flipped from
71+
"deferred to issue #40" to fully shipped; deferred-items section
72+
rewritten as "Closed follow-ups" with the four checkboxes ticked.
73+
74+
### Why
75+
Closes the four open items the rung 2 implementation called out as
76+
explicit follow-ups:
77+
78+
1. **Drain stopwatch** — operators now have a Prometheus histogram of
79+
actual emergency-drain duration sliced by outcome, so SLO dashboards
80+
can compute success-only P95 vs timeout-rate side by side.
81+
2. **Re-enable loop protection** — when a user re-enables a SM whose
82+
conflicting process is still running, the third reclaim within
83+
10 minutes now triggers a `RapidReReclaim` Warning Event telling the
84+
operator to stop the conflicting process before re-enabling. The
85+
loop is no longer silent.
86+
3. **Linux netlink runtime test** — issue #40 acceptance criterion
87+
"JVM launch produces a match within <100 ms" is now verifiable via
88+
`cargo test --test integration_netlink_proc -- --ignored` on a real
89+
Linux node with `CAP_NET_ADMIN`.
90+
4. **Kind-cluster integration test** — proves the agent → controller
91+
annotation contract round-trips through a real API server. Catches
92+
the entire class of "agent writes annotations the controller can't
93+
parse" regressions that no unit test would surface.
94+
95+
With these closed, every phase of the
96+
`5spot-emergency-reclaim-by-process-match.md` roadmap is shipped and
97+
the file moves to `completed-` prefix.
98+
99+
### Impact
100+
- [ ] Breaking change (no API surface change; new metrics + new event
101+
reason are additive)
102+
- [x] Requires cluster rollout (binary changes; CRD unchanged so no
103+
`kubectl apply` of the CRD YAML needed)
104+
- [ ] Config change only
105+
- [ ] Documentation only
106+
107+
### Verification
108+
- `cargo build`: clean.
109+
- `cargo test`: **450 unit tests + 3 always-on integration tests pass**;
110+
4 `#[ignore]` integration tests (2 emergency-reclaim, 2 netlink) gated
111+
on a real cluster / Linux + CAP_NET_ADMIN respectively.
112+
- `cargo fmt --check`: clean.
113+
- `cargo clippy --all-targets -- -D warnings`: clean.
114+
115+
---
116+
117+
## [2026-05-02 23:00] - Reclaim-agent rung 2: netlink proc connector subscriber
118+
119+
**Author:** Erick Bourgeois
120+
121+
### Added
122+
- `src/netlink_proc.rs` (~280 LOC) — new module exposing `ProcEvent`,
123+
`NetlinkError`, portable parsers (`parse_cn_msg`, `parse_proc_event`),
124+
and `Subscriber`. Linux impl opens an `AF_NETLINK` /
125+
`NETLINK_CONNECTOR` socket, binds to the `CN_IDX_PROC` multicast
126+
group, sends the `PROC_CN_MCAST_LISTEN` control message, and yields
127+
one `ProcEvent` per kernel `exec(2)`. Non-Linux: `Subscriber::new()`
128+
immediately returns `NetlinkError::Unsupported` so the binary still
129+
links cleanly on macOS / Windows builds.
130+
- `src/netlink_proc_tests.rs` — 14 byte-level parser tests + 1
131+
non-Linux stub contract test = **15 new tests, all running on macOS
132+
CI** without needing a kernel: exec / fork / exit classification,
133+
truncated buffers, wrong connector id, zero-length payload,
134+
cn_msg→proc_event round-trip, non-Linux `Subscriber::new` returns
135+
`Unsupported`.
136+
- `src/bin/reclaim_agent.rs`: new `--detector={auto|netlink|poll}`
137+
flag (env `RECLAIM_DETECTOR`, default `auto`). `auto` selects
138+
`netlink` on Linux and `poll` elsewhere. New `run_netlink_scanner`
139+
loop runs alongside the existing `run_scanner`; selection happens
140+
once after host-id verification, before either loop starts. The
141+
netlink loop opens the subscriber only when armed by a non-empty
142+
ConfigMap and tears it down on config-clear, so an idle agent holds
143+
no kernel resources. The `recv(2)` syscall runs on a `spawn_blocking`
144+
thread; `tokio::select!` cancels via the existing config watch
145+
channel.
146+
- `Cargo.toml`: `target.'cfg(target_os = "linux")'.dependencies.nix =
147+
{ version = "0.30", default-features = false, features = ["socket",
148+
"net", "uio"] }`. Linux-only — non-Linux builds pull in nothing new.
149+
- `deploy/node-agent/daemonset.yaml`: container `securityContext`
150+
gains `capabilities.add: [NET_ADMIN]`. The pod-level comment block
151+
rewritten to reflect that this is no longer "future work."
152+
- `.trivyignore`: new `AVD-KSV-0022` entry justifying the
153+
`CAP_NET_ADMIN` add against the existing architectural-necessity
154+
rationale pattern (matches the KSV-0010 / KSV-0012 / KSV-0023 etc.
155+
blocks already in the file).
156+
- `docs/src/concepts/emergency-reclaim.md`: new "Detector — rung 1
157+
vs rung 2" subsection with the comparison table, selection flags,
158+
the heavy-exec-storm tradeoff, and a note on why the cap is granted
159+
pod-level rather than per-detector.
160+
161+
### Changed
162+
- `src/reclaim_agent.rs`: `match_pid` visibility raised from `fn` to
163+
`pub fn` so the rung 2 netlink subscriber can resolve a kernel-pushed
164+
pid through the same match logic rung 1 uses. Body and behaviour
165+
unchanged. Existing 46 tests stay green.
166+
- `src/lib.rs`: `pub mod netlink_proc` added; module list comment
167+
block extended.
168+
- `docs/src/concepts/emergency-reclaim.md` status header updated:
169+
rung 2 is shipped, no longer "follow-up work."
170+
171+
### Why
172+
Closes the deferred Phase 2 rung 2 of
173+
`~/dev/roadmaps/5spot-emergency-reclaim-by-process-match.md` and
174+
GitHub issue #40. Rung 1 already meets the <5 s SLA, so this is
175+
optimisation, not correctness — payoff is detection latency dropping
176+
from up to 250 ms (worst-case poll phase) to <10 ms (kernel push)
177+
and idle CPU dropping to zero on quiet nodes. The detector is opt-in
178+
via `--detector=netlink`; `auto` (the default) picks netlink on Linux
179+
and poll elsewhere. Operators on heavy-exec workloads can pin
180+
`--detector=poll` to avoid the per-exec cost.
181+
182+
The netlink ABI is identical across Linux x86_64 and aarch64; both
183+
build targets in `.github/workflows/build.yaml` get rung 2 with no
184+
per-arch code. Windows is out of scope (no netlink ABI; would need an
185+
ETW-based sibling impl behind the same trait, separate effort).
186+
187+
### Impact
188+
- [ ] Breaking change (new flag is opt-in via `auto` default; existing
189+
operators with `RECLAIM_DETECTOR` unset continue on the
190+
platform-appropriate default; rung 1 stays available as `poll`)
191+
- [x] Requires cluster rollout (DaemonSet manifest changes —
192+
`kubectl apply -k deploy/node-agent/` to pick up `CAP_NET_ADMIN`
193+
and the new env var support)
194+
- [ ] Config change only
195+
- [ ] Documentation only
196+
197+
### Verification
198+
- `cargo build --bin 5spot-reclaim-agent`: clean.
199+
- `cargo test --lib netlink_proc`: 15/15 pass on macOS aarch64.
200+
- `cargo test --lib reclaim_agent`: 46/46 pass (unchanged from
201+
pre-change baseline).
202+
- `cargo fmt --check` + `cargo clippy --all-targets -- -D warnings`:
203+
clean.
204+
- Linux runtime verification deferred to CI (kind cluster) and to
205+
hardware testing — same gating as rung 1's integration test scope.
206+
The macOS dev loop uses `--detector=poll` and exercises the bin
207+
end-to-end without netlink.
208+
209+
### Trust model (unchanged)
210+
The netlink socket is opened with `CAP_NET_ADMIN` granted at the
211+
container level. The node-scoped credentials posture is unchanged —
212+
the agent still binds only to the proc-event multicast group and
213+
cannot send control messages to other netlink subsystems. The
214+
host-identity cross-check (Phase 4 of the 2026-04-25 security audit)
215+
still gates every PATCH regardless of detector choice.
216+
217+
---
218+
12219
## [2026-05-02 10:00] - Symbol-import auto-VEX (roadmap Phase 3, scope-revised)
13220

14221
**Author:** Erick Bourgeois

.trivyignore

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -173,6 +173,23 @@ AVD-KSV-0118
173173
# writable volumes whose ownership fsGroup would govern.
174174
AVD-KSV-0116
175175

176+
# KSV-0022 — Container has non-default capabilities added.
177+
# Architecturally required for rung 2 of the reclaim-agent's detection
178+
# ladder (`5spot-emergency-reclaim-by-process-match.md` Phase 2 rung 2,
179+
# GitHub issue #40). `CAP_NET_ADMIN` is required to open an
180+
# `AF_NETLINK` / `NETLINK_CONNECTOR` socket and bind to the
181+
# `CN_IDX_PROC` multicast group — the kernel-pushed event source the
182+
# subscriber in `src/netlink_proc.rs` consumes. Rung 1 (the `/proc`
183+
# poll) does not need this cap; operators who pin `--detector=poll`
184+
# never exercise it. We grant the cap once at the pod level rather than
185+
# letting the binary fall back to rung 1 silently at runtime — the
186+
# detector choice is an operator decision, not an availability
187+
# heuristic. Scope is bounded by the existing opt-in
188+
# `5spot.finos.org/reclaim-agent: enabled` nodeSelector, so the cap is
189+
# only granted on the small set of nodes the operator has explicitly
190+
# armed.
191+
AVD-KSV-0022
192+
176193
# ─────────────────────────────────────────────────────────────────────────────
177194
# Dockerfile — Dockerfile, Dockerfile.chainguard
178195
# ─────────────────────────────────────────────────────────────────────────────

Cargo.lock

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

Cargo.toml

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,14 @@ lazy_static = "1.5"
6060
warp = { version = "0.4", default-features = false, features = ["server"] }
6161
toml = "1.1"
6262

63+
# Linux-only — used by the reclaim-agent's rung 2 netlink proc connector
64+
# subscriber (`src/netlink_proc.rs`). The portable byte parsers in that
65+
# module have no `nix` dep; only the actual socket / bind / send / recv
66+
# dance does. Non-Linux builds compile the module's `Subscriber::new` to
67+
# a stub that returns `NetlinkError::Unsupported`.
68+
[target.'cfg(target_os = "linux")'.dependencies]
69+
nix = { version = "0.30", default-features = false, features = ["socket", "net", "uio"] }
70+
6371
[[bin]]
6472
name = "5spot-reclaim-agent"
6573
path = "src/bin/reclaim_agent.rs"

deploy/node-agent/daemonset.yaml

Lines changed: 12 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -129,12 +129,16 @@ spec:
129129
# - name: SKIP_HOST_ID_CHECK
130130
# value: "true"
131131
# Reading every `/proc/<pid>/comm` + `cmdline` requires root;
132-
# CAP_SYS_PTRACE is not strictly required for rung 1 but is kept
133-
# dropped. CAP_NET_ADMIN will be re-added for rung 2 (netlink
134-
# proc connector) as a separate change. See the pod-level
135-
# securityContext comment block and `.trivyignore` (KSV-0012 /
136-
# KSV-0116 / KSV-0118 / KSV-0105) for the runAsNonRoot=false
137-
# rationale.
132+
# CAP_SYS_PTRACE is not strictly required and is kept dropped.
133+
# CAP_NET_ADMIN is added for rung 2: subscribing to the
134+
# netlink proc connector (`NETLINK_CONNECTOR` socket family)
135+
# requires it. Drop ALL first, then add only the one cap we
136+
# need. See `.trivyignore` (KSV-0012 / KSV-0116 / KSV-0118 /
137+
# KSV-0105 for runAsNonRoot=false; KSV-0102 for the
138+
# CAP_NET_ADMIN add) for the architectural-necessity
139+
# rationale. Operators who pin `--detector=poll` do not
140+
# exercise the cap; it is granted at the pod level so the
141+
# binary never has to fail open at runtime.
138142
securityContext:
139143
runAsUser: 0
140144
runAsGroup: 0
@@ -145,6 +149,8 @@ spec:
145149
capabilities:
146150
drop:
147151
- ALL
152+
add:
153+
- NET_ADMIN
148154
seccompProfile:
149155
type: RuntimeDefault
150156
resources:

0 commit comments

Comments
 (0)