fix(telemetry): the token Secret has to precede the flag (backend#2400) - #801
Conversation
The Collector could not be enabled on any edge in the fleet. The chart's
daemonset refuses the release while the token Secret is absent; jobs-manager
writes that Secret only when the chart hands it the coordinates; the chart
handed them over only once the Collector was enabled. `enabled=false` was
therefore the only reachable state, on every edge, and every edge auto-upgrades
to the published chart hourly.
Neither half was wrong on its own, which is why both passed review: refusing
the release beats CrashLoopBackOff on 200 nodes, and an edge with the feature
off should make no API calls for it. Nothing tested the pair.
THREE gates read `telemetryCollector.enabled`, not one, and ungating fewer than
all three would have shipped a fix that looked applied and did nothing --
`sync_telemetry_token_secret` logs and returns False rather than raising, so a
403 on the write is invisible from the release:
* jobs-manager-deployment.yaml -- the TELEMETRY_TOKEN_SECRET_* env vars
* telemetry-token-rbac.yaml -- the Role and RoleBinding, i.e. the 403
* node-agents-namespace.yaml -- the namespace, i.e. a 404 on an edge with
resourceMonitor: false
The `fail` guard in telemetry-collector-daemonset.yaml is untouched and stays
exactly as strict. It was never wrong; its precondition was unreachable. This
makes the precondition satisfiable, not optional.
`tracebloc.nodeAgentsInUse` is NOT widened to cover the token RBAC. Measured, a
constant-true helper grants auto-upgrade and image-refresh DaemonSet rights in a
namespace holding no DaemonSets -- 4 extra RBAC objects for no benefit. It keeps
its meaning, narrowed in the docstring to POD-BEARING tenants, and the namespace
(now always occupied by a pod-less Role) drops the tenant condition instead.
The test is the deliverable. scripts/tests/telemetry-token-bootstrap.sh renders
the whole chart with the Collector off and again with it on, projects the
bootstrap surface out of both -- the writer's env by container, the Role that
grants it `secrets`, the binding that points at that Role, whether the target
namespace is created -- and asserts the two are identical, while the Collector's
own workload stays absent from the off render. It names no Secret, namespace,
key or verb: every selection is chased through references out of the render, so
there is no second copy of the rule to drift. It fails closed -- an empty
projection is a finding, not agreement between two absences.
Mutation-proved by re-gating each of the three in turn, anchors asserted applied
each time: all three redden the guard AND helm unittest. The namespace mutation
reddens only the resourceMonitor=false column, which is why the guard runs both.
Verified: helm unittest 545 passed (34 suites), make drift 16 guards green,
shellcheck -S warning -x clean, gitleaks clean on the new file.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
There was a problem hiding this comment.
Cursor Bugbot has reviewed your changes and found 1 potential issue.
❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.
Reviewed by Cursor Bugbot for commit d238495. Configure here.
…ts (backend#2400) Bugbot High on #801, and it is right. Ungating the token Role/RoleBinding and the namespace they live in left auto-upgrade's node-agents Role still behind `tracebloc.nodeAgentsInUse`. On an edge with `resourceMonitor: false` and the Collector off, the chart then puts two objects in a namespace the auto-upgrade ServiceAccount cannot write, so the next hourly `helm upgrade --atomic --wait` 403s and ROLLS BACK -- and no later chart lands on that edge either, with nothing red anywhere. That is backend#953's failure mode in its expensive form, and it is the SAME paired-construct break this ticket exists to fix, one template over: one gate moved and its pair did not. auto-upgrade re-applies the WHOLE chart, so its reach has to follow "does the chart put anything in that namespace" -- now unconditional -- and not "does a pod run there", which is what the helper answers and what image-refresh still needs. Its gate keeps the one condition that is still load-bearing: when node-agents IS the release namespace, the release-namespace Role already covers it. The old comment claimed the pair rendered "exactly when those objects exist". It was a restatement of another template's gate, and it is what made this invisible. THE COMPOSED PROPERTY, in node-agents-namespace-safety.sh rather than a new file, because it is the same question about the same namespace off the same render -- and because the first implication passing while the second failed is precisely what happened here. Second implication: if a combination populates that namespace, some ServiceAccount from the RELEASE namespace must hold unrestricted rights there. The applier is DERIVED -- whichever SA a RoleBinding binds to a Role granting `*` on `*` -- not matched by name, so renaming auto-upgrade cannot silence it. Fails closed with the existing vacuity counter. node-agents-tenancy.sh's failure message no longer prescribes `tracebloc.nodeAgentsInUse`: after this, auto-upgrade correctly does not read it. Mutation matrix, anchors asserted applied each time (bytes printed), each gate re-gated in turn: bootstrap ns-safety unittest jobs-manager env RED green RED token Role/RoleBinding RED green RED node-agents namespace RED RED RED auto-upgrade reach green RED RED The greens are the two guards owning different properties -- the bootstrap guard does not inspect the applier, ns-safety does not inspect env vars. Every mutation reddens the guard that owns it, and all four redden helm unittest. Verified: helm unittest 548 passed (34 suites), make drift 16 guards green, shellcheck -S warning -x clean across scripts/tests. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
saqlainsyed007
left a comment
There was a problem hiding this comment.
Verified the deadlock and the fix against the chart — a real fleet-wide bug, fixed at the right layer.
The deadlock is real and both halves were individually correct (which is why both passed review and nothing caught the pair): telemetryCollector.enabled=false → jobs-manager gets no TELEMETRY_TOKEN_SECRET_* env → writes no Secret → the daemonset's fail-guard refuses enabled=true while the Secret is absent → stuck at false, on every edge auto-upgrading to client-1.9.63 hourly.
The fix breaks it at the precondition, correctly: the token env, the write-RBAC (Role/RoleBinding jobs-manager needs, or it 403s), and the node-agents-namespace Secret now render unconditionally — the Secret is the flag's precondition, so it must exist before anyone can flip the flag. The daemonset fail-guard keeps its full strength (still refuses release with no Secret), and the Collector mounts the Secret optional: true, so an edge with telemetry off just carries one unused Secret and one idempotent re-auth write — the cost, stated rather than hidden.
The safety implications are handled: making the token Role/RoleBinding render with no pods in the node-agents namespace falsified the old "renders exactly when a pod runs there" claim, so node-agents-namespace-safety.sh is updated to assert the implication that actually holds now, and a new telemetry-token-bootstrap.sh guard is added to DRIFT_GUARDS to pin the unconditional rendering — the pair that was previously untested. CI green (helm tests + all quality/drift guards), no threads. LGTM.

Fixes tracebloc/backend#2400.
The deadlock
The Collector could not be enabled on any edge. Measured on
tb-client-dev-templates, chartclient-1.9.63— the published version every edge auto-upgrades to hourly.Neither half was wrong on its own, which is why both passed review. Refusing the release beats CrashLoopBackOff on 200 nodes; an edge with the feature off should make no API calls for it. Nothing tested the pair.
Three gates, not one
All three read
telemetryCollector.enabled. Ungating fewer than all three ships a fix that looks applied and does nothing, becausesync_telemetry_token_secretis fail-soft — it logs, returnsFalse, never raises — so a 403 on the write is invisible from the release:jobs-manager-deployment.yamltelemetry-token-rbac.yamlnode-agents-namespace.yamlresourceMonitor: falseAll three were found exactly where the ticket said.
What is not changed
The
failguard intelemetry-collector-daemonset.yamlis untouched and stays exactly as strict. It was never wrong — its precondition was unreachable. This makes the precondition satisfiable, not optional.No
client-runtimechange.sync_telemetry_token_secretalready does the right thing the moment the env is present: create-then-patch-on-409, fail-soft, and the token it writes is the backend token jobs-manager already holds (jobs_manager.py:4676at startup,:4734on rebind). Nothing about the token's availability ever depended on the flag. (One docstring there now describes the old behaviour; a one-line correction follows in its own PR.)One deviation from the ticket, with the measurement
The ticket asked for
tracebloc.nodeAgentsInUseto be extended so the namespace covers the token sync. The token sync is unconditional, so that third disjunct is a constanttrue— which makes the whole helper a constanttrue. Rendered both ways withresourceMonitor=false, enabled=false, that costs:Widening RBAC as a side effect of a telemetry fix is the opposite direction from backend#1992, so instead:
nodeAgentsInUsekeeps its meaning, narrowed in the docstring to POD-BEARING tenants — which is what its three remaining consumers actually ask (DaemonSet-managing RBAC, and the pull Secret those pods use).node-agents-namespace.yamldrops the tenant condition rather than extending it. With a pod-less occupant now always present, the namespace is unconditional and a predicate over it would be a constanttruewearing a question mark. Its other two conditions (namespace.create, release-namespace collision) are unchanged and both still bite — pinned by a test.Same outcome the ticket wanted; no unrelated grant.
The test — the deliverable, not an extra
scripts/tests/telemetry-token-bootstrap.sh, registered inDRIFT_GUARDS(now 16). It asserts the composed property, because a fix that changes a template and tests that template repeats the mistake exactly.It renders the whole chart with the Collector off and again with it on, projects the bootstrap surface out of both — the writer's env keyed by container, the Role that grants it
secrets, the binding that points at that Role, whether the target namespace is created — and asserts the two are identical, while the Collector's own workload stays absent from the off render (so the equality can never be satisfied by shipping a DaemonSet to every node by default).resourceMonitordefaults totrue, so the default alone would pass with the namespace gate still in place.Mutation results — each of the three re-gated in turn
Anchors asserted applied every time (byte deltas printed), so no mutation was inert:
envdiffers, both columnsrolesdiffers, both columnsnamespace_createddiffers,resourceMonitor=falsecolumnThe namespace mutation reddens only the
resourceMonitor=falsecolumn — which is precisely why the guard runs both.helm-unittest changes are inversions of assertions the fix makes false, not additions on top of them;
jobs_manager_test.yaml'senv[0]positional assertion is switched to selectCLIENT_IDby name, since the token block now renders above it.Verification
Chart
versionandappVersionboth bumped to1.9.65.The operator sequence this ships
Once
1.9.65is published, on a clean edge:tracebloc-telemetry-tokenintotracebloc-node-agentson its next authentication.helm upgrade tracebloc tracebloc/client -n <ns> --reuse-values --set telemetryCollector.enabled=true— one step, and thefailguard now passes because its precondition is satisfied.A fresh install with
telemetryCollector.enabled=truefrom the start is still refused, correctly: jobs-manager has not run yet, so nothing has written the Secret. Install with it off, let it authenticate once, then flip.🤖 Generated with Claude Code
Note
Medium Risk
Changes Helm RBAC and namespace creation so token Role/RoleBinding and auto-upgrade reach into node-agents even with all pod tenants off. Least-privilege Secret write is unchanged, but every edge now gets extra objects and an unused token Secret write path.
Overview
Fixes a fleet-wide deadlock: the Collector DaemonSet refuses install until its token Secret exists, but jobs-manager only received Secret coordinates and write RBAC when
telemetryCollector.enabledwas already true.Bootstrap is now independent of the flag.
TELEMETRY_TOKEN_SECRET_*always lands on the jobs-manager writer container,telemetry-token-rbac.yamlalways grants create/get/patch on that Secret, and the node-agents Namespace is created whenevernamespace.createis set (still skipping a release-namespace collision). The DaemonSetfailguard is unchanged; the Collector workload still does not render when the flag is off.tracebloc.nodeAgentsInUsestays pod-bearing only so image-refresh and pull Secrets are not granted in an empty namespace. Auto-upgrade’s node-agents Role/RoleBinding instead follows “the chart puts anything there,” so hourly--atomicticks cannot 403 on the always-rendered token RBAC.A new drift guard
telemetry-token-bootstrap.shcompares off vs on renders of the bootstrap surface (env, Role, binding, namespace) and requires the Collector workload to stay absent when disabled. Chart version 1.9.65.Reviewed by Cursor Bugbot for commit abe4d79. Bugbot is set up for automated code reviews on this repo. Configure here.