Repository navigation
Conversation
Codex/cordon aware capacity
…e74a397d24b60916fda3576e77d85e1e3b3e3 chore: pin images for 077e74a
The jq filter defaulted a missing Gateway to "" inside select() but then printed .Gateway itself, so an IPAM entry with no gateway passed the IPv4 test and jq -r emitted "null". The caller's guard only rejected an empty value, so the stub was told to listen on "null:0" and died resolving it as a hostname: listen on null:0: listen tcp: lookup null on 127.0.0.53:53: server misbehaving Docker on the CI runner records the kind network that way; locally both IPAM entries carry gateways, which is why this only failed in Actions. Drop gatewayless entries with `.Gateway // empty`, fall back to the default route the control-plane container actually uses, and return nothing unless the result parses as an IPv4 address. Claude-Session: https://claude.ai/code/session_01BBxWEqjEe4vNe62EAEnGzy
go.mod pointed at v0.1.1-0.20260820213702-4e56982dc1a7, the pseudo-version for commit 4e56982. That commit is what v0.2.0 tags, so this is the same code under its released name rather than a dependency change. Claude-Session: https://claude.ai/code/session_01BBxWEqjEe4vNe62EAEnGzy
Arda/fix kind bridge address
There was a problem hiding this comment.
Cursor Bugbot has reviewed your changes using default effort and found 1 potential issue.
Autofix Details
Bugbot Autofix prepared a fix for the issue found in the latest run.
- ✅ Fixed: Bridge lookup aborts past check
- Added || true to kind_bridge_address call to prevent errexit from aborting before the empty-string check and diagnostic message can run.
You can send follow-ups to the cloud agent here.
When kind_bridge_address returns non-zero, prevent errexit from aborting the script before the empty-string check can run and display the intended diagnostic message about IPAM and control-plane fallbacks.
An unhealthy enrolled node was falling through to enroll() after a couple of failed restarts, and every unreadable-CLI status (exit 127) was treated as unenrolled -- each occurrence minted a fresh single-use enrollment token and created a duplicate host row in Central. This is what spammed the dashboard in the 2026-08-24 incident. - ensureEnrolled now determines "is this node enrolled" once: trust thunder status's own AuthTokenConfigured field when it has an answer, otherwise fall back to reading THUNDERD_AUTH_TOKEN directly from the host's /etc/thunder/thunderd.env through the daemon's read-only /host mount (nodeEnrolled/hostAuthTokenConfigured). An unreadable status or env file is never mistaken for "unenrolled". - While enrolled, the only repairs are a token-less restart, or (when `thunder status` itself won't run) a tokenless CLI reinstall via install.sh's anonymous binary-only mode. Neither ever escalates to enroll(). - A durable per-outage repair budget (repair_budget.go) caps repair attempts at 5, persisted on the host so a pod restart mid-outage doesn't re-arm it; past the cap the daemon logs once and cools off for an hour, trying only a restart, never an enroll. The budget clears on the next healthy pass. - Before any repair command, sweep dangling thunderd.service enable-symlinks under /etc|/run/systemd/system's .wants/.requires dirs (exact name, verified dangling) and daemon-reload if anything was removed -- the same GC hazard from the incident, defended here for nodes whose CLI predates the thundernetes-side fix. Deviations from the plan, both intentional, flagged for review: - The saved-token-invalid-per-Central branch of enroll() is not implemented: the daemon has no SDK path to call the token-validate endpoint with the node's own device credential (it only lives in the thundernetes CLI's unexported centralDeviceAuthClient), so per the plan's own fallback this stays a human decision. - "CLI too old for restart" is not detected as a separate signal from "CLI missing": RunShell does not return command output, so there is no reliable way to distinguish a stale-flag failure from any other restart failure. Both cases are bounded by the same repair budget and cool-off instead.
…ool-off Requested change: once 5 repair attempts have failed for the current outage, stop repairing entirely -- no more restarts, CLI reinstalls, or enrolls of any kind -- rather than retrying a bare restart once an hour. Removes the now-dead cool-off timestamp/flag and the clock hook that only existed to test it; repairBudgetMarker is just an attempt count. The budget still resets on the next healthy pass.
Stamp the repair budget marker with version.Get() (already wired to
-ldflags at build time). When the running daemon's version differs
from the marker's, discard the old budget and log once ("new daemon
version, retrying repairs") before continuing -- a new image may
contain a fix a spent budget from the old one shouldn't block.
Also cut the multi-sentence comments added across B1-B4 down to one
or two lines each, dropping incident/cross-repo references while
keeping the "(by claude)" tags.
The daemon image rarely changes; the thunderd distribution does, and
repairs install whatever it currently serves. Fetch
<ArtifactBaseURL>/sha256sums.txt (anonymous) and stamp thunderd's
checksum in the marker, but only once the budget is already spent --
healthy nodes never poll for it, and it rides the existing reconcile
cadence rather than a separate timer.
A changed checksum resets the budget and reattempts the repair in the
same pass, logging once ("distribution serves a new thunderd, retrying
repairs"). A fetch error leaves the budget untouched and given up,
with no log spam. The daemon-version reset from the previous commit
stays as a secondary trigger; both keys live in the marker.
thunder up never replaces an existing binary, so a checksum-triggered retry now runs thunder update instead -- only for that one retry pass; every other attempt in the round still uses the normal action (thunder up for restarts). Marker gains AttemptedChecksum (the sha the retry installed) and FailedChecksum (a sha whose retry round failed and must not reset the budget again): the reset condition is now served-sha != Attempted AND served-sha != Failed. When an update-triggered round spends the budget a second time, thunder update --rollback is tried once (best-effort -- today's CLI has no --rollback flag, so this normally fails, logs one line, and the checksum is still recorded as failed) before the usual give-up log. repairAttempt takes two new optional callbacks (updateAction, rollbackAction) so enroll/CLI-reinstall, where none of this applies, pass nil and are unaffected.
Arda/repair loop cap on the dra side for auto-updates.
…c1421c33771b44f4d63096998f22097fb4b89 chore: pin images for 5edc142
There was a problem hiding this comment.
Cursor Bugbot has reviewed your changes using default effort and found 1 potential issue.
Autofix Details
Bugbot Autofix prepared a fix for the issue found in the latest run.
- ✅ Fixed: Enrollment budget blocks later recovery
- Removed repairAttempt wrapper from enrollment path to allow unlimited retries via reconcile loop backoff, preventing permanent enrollment stalls when NVIDIA driver or Thunder API appears after initial failures.
Or push these changes by commenting:
@cursor push db6069dd7d
Preview (db6069dd7d)
diff --git a/internal/daemon/reconcile.go b/internal/daemon/reconcile.go
--- a/internal/daemon/reconcile.go
+++ b/internal/daemon/reconcile.go
@@ -189,15 +189,13 @@ func (r *reconciler) ensureEnrolled(ctx context.Context, cfg Config) error {
if !r.nodeEnrolled(cfg, status, statusErr) {
log.Printf("thunderd unhealthy on node %s after %d check(s); enrolling", cfg.Node, r.unhealthy)
- attempted, err := r.repairAttempt(ctx, cfg, func(ctx context.Context) error {
- return r.enroll(ctx, cfg)
- }, nil, nil)
- if err != nil {
- return err
+ if err := r.sweepDanglingSymlinks(ctx); err != nil {
+ log.Printf("node %s: could not sweep dangling thunderd symlinks, enrolling anyway: %v", cfg.Node, err)
}
- if attempted {
- r.unhealthy, r.transitional = 0, 0
+ if err := r.enroll(ctx, cfg); err != nil {
+ return err
}
+ r.unhealthy, r.transitional = 0, 0
return nil
}
@@ -189,15 +189,13 @@ func (r *reconciler) ensureEnrolled(ctx context.Context, cfg Config) error {
if !r.nodeEnrolled(cfg, status, statusErr) {
log.Printf("thunderd unhealthy on node %s after %d check(s); enrolling", cfg.Node, r.unhealthy)
- attempted, err := r.repairAttempt(ctx, cfg, func(ctx context.Context) error {
- return r.enroll(ctx, cfg)
- }, nil, nil)
- if err != nil {
- return err
+ if err := r.sweepDanglingSymlinks(ctx); err != nil {
+ log.Printf("node %s: could not sweep dangling thunderd symlinks, enrolling anyway: %v", cfg.Node, err)
}
- if attempted {
- r.unhealthy, r.transitional = 0, 0
+ if err := r.enroll(ctx, cfg); err != nil {
+ return err
}
+ r.unhealthy, r.transitional = 0, 0
return nil
}You can send follow-ups to the cloud agent here.
| if err != nil { | ||
| return fmt.Errorf("encode repair budget marker: %w", err) | ||
| } | ||
| command := "set -eu; umask 077; mkdir -p /var/lib/thunder; tmp=$(mktemp /var/lib/thunder/.repair-budget.XXXXXX); trap 'rm -f \"$tmp\"' EXIT; printf %s " + shellQuote(string(data)) + " > \"$tmp\"; mv \"$tmp\" " + shellQuote(repairBudgetMarkerPath) + "; trap - EXIT" |
There was a problem hiding this comment.
prob don't need to have a repair budget, it's ok if it restarts a bunch of times
Review feedback (brianmodel): a pod restart resetting the repair counter is an acceptable tradeoff for not writing a marker file to the host. Delete repair_budget.go's marker/store machinery entirely (the mktemp/mv shell write included) and keep the same transitions -- 5-attempt cap with one give-up log, checksum-triggered reset retried via `thunder update`, failed-sha blacklist, one-shot rollback, reset on a healthy pass -- as plain fields on *reconciler instead. Also drops the daemon-version reset and its DaemonVersion field: with state in memory, a new daemon image is a new pod is fresh state already, so the mechanism is redundant. Tests: dropped the persistence-across-reconciler-recreation case (no longer applicable) and the daemon-version reset test (mechanism gone); the give-up/healthy-reset test now runs against a single reconciler instance. Checksum retry, blacklist, and rollback tests adapted to read state off the reconciler directly instead of a fake store.
Root cause: the publish job's permissions block grants contents, packages, and id-token but not pull-requests, so the final "Open chart values pull request" step's `gh pr create` (using GITHUB_TOKEN) is refused by the API on every run. The branch push just before it succeeds under contents: write, which is why automation/image-tags-* branches exist on their own with no PR -- they had to be opened by hand every time. Adding pull-requests: write lets that same token open the PR the step already builds.
Arda/repair budget in memory
User rule: no multi-line bash embedded as Go strings. Replaces sweepDanglingSymlinksCommand (a set -eu/for/trap shell script run through RunShell) with danglingThunderdSymlinks, which does the detection in Go against the read-only /host mount: filepath.Glob over the four .wants/.requires locations, os.Lstat to confirm each thunderd.service entry is a symlink, os.Stat to confirm the target is gone. sweepDanglingSymlinks then issues plain argv commands through the runner (no shell) -- one `rm -f` naming every dangling path found, then `systemctl daemon-reload` only if something was removed -- and issues nothing at all when detection finds nothing dangling. Behavior is unchanged: exact "thunderd.service" name, both locations under /etc and /run, dangling-only, reload only on removal. Tests: scriptedRunner gained generic CombinedOutput recording (it only tracked the `thunder status` script before) and a call-order sequence spanning both CombinedOutput and RunShell, since the sweep and the repair action it precedes can now go through either. Rewrote the two tests that pinned the sweep to the old script string to instead plant a real dangling symlink under a temp HostRoot and assert on the rm/daemon-reload commands and their ordering before the repair; added a case asserting no commands are issued when nothing is dangling, and a direct unit test for danglingThunderdSymlinks covering a live symlink, a plain file, and an unrelated name, none of which should be touched.
Arda/sweep in go
There was a problem hiding this comment.
Cursor Bugbot has reviewed your changes using default effort and found 1 potential issue.
Bugbot Autofix prepared a fix for the issue found in the latest run.
- ✅ Fixed: Absolute symlink check uses container root
- Modified danglingThunderdSymlinks to read symlink targets and resolve absolute paths against hostRoot before checking existence, preventing valid host symlinks from being incorrectly deleted.
You can send follow-ups to the cloud agent here.
Reviewed by Cursor Bugbot for commit 66a20fa. Configure here.
The danglingThunderdSymlinks function was incorrectly checking if symlink targets exist by using os.Stat on the container-mounted path. When symlinks have absolute targets (e.g., /lib/systemd/system/thunderd.service), os.Stat resolves them from the container root instead of the host filesystem. This caused valid systemd enable-symlinks on the host to appear dangling, leading to their incorrect deletion during the sweep. The fix reads the symlink target and explicitly resolves absolute paths against hostRoot before checking existence, matching the previous nsenter-based behavior.
…7d20bbefc842d5dd7bc6b8acab7e3f73d498f chore: pin images for ce67d20
Configure PostHog without repeated thunderd restarts
…89810ae48ed1a4e39ff75faf93e8a2a80e99c chore: pin images for e7c8981


What changed
Why
Checks
make checkmake test-localsafe on a cluster that is not ours.
Note
High Risk
Changes privileged daemon host enrollment/repair logic and operator schedulable capacity; misbehavior could strand nodes or mis-advertise GPUs until rolled back.
Overview
This PR extends the node daemon and operator, bumps thunder-sdk to v0.2.0, and refreshes chart defaults and integration tests.
Helm / daemon configuration: Adds
daemon.extraEnvso operators can inject extra container env vars (including secrets viavalueFrom). Variables namedTHUNDERD_ENV_<key>are merged into the host’s/etc/thunder/thunderd.envonce at daemon startup (atomic write via stdin, no thunderd restart). Chart schema, DaemonSet template, values, and README document rollout behavior (pod restart required for env/secret changes).Daemon repair behavior: Reworks unhealthy-node handling so enrolled nodes are never re-enrolled (avoids duplicate hosts and enrollment-token spam). Repairs go through a capped budget (
repairAttempt: restart, CLI reinstall without token, danglingthunderd.servicesymlink sweep, optionalthunder update/ rollback when distribution checksum changes). Enrollment is reserved for nodes with no auth token on disk.Operator capacity: Hosts with
configurations.cordonedno longer contribute physical GPU capacity; live client counts still set the advertised floor when every host is cordoned. Local kind tests assert cordon/uncordon ResourceSlice and DeviceClass behavior.Misc:
publish-imagesworkflow gainspull-requests: write;kind_bridge_addressis more reliable for the registry stub; operator/daemon image tags updated in values.Reviewed by Cursor Bugbot for commit 3b73073. Bugbot is set up for automated code reviews on this repo. Configure here.