Repository navigation
feat: the agent yields first, and two objects it does not ship (ADR 0068) - #106
Merged
Merged
Conversation
…068) Four questions a platform team asks at review were answered by omission. Two stay absent with a reason recorded, two become decisions: - no PodDisruptionBudget: on one replica minAvailable:1 blocks node drain and maxUnavailable:1 forbids nothing; - no topologySpreadConstraints: nothing to spread across one replica or a DaemonSet; - priorityClassName on both roles, empty by default, no PriorityClass created — a value only means something against a cluster's own classes. A system- name outside kube-system is refused at render time, since admission would reject the pods rather than lower their priority; - terminationGracePeriodSeconds 30 on the controller, 10 on the node. The controller's shutdown path has a ~10s ceiling (the receiver's own Shutdown budget); below 15 the chart refuses, because a SIGKILL before the flush pass drops up to one coverage interval of the journals with nothing to notice it by. The node runs no pass and has no loss, so its number bounds only how long the agent delays a drain. GOMEMLIMIT is set to 80% of the container limit, passed in by the downward API because the Go runtime reads the CPU limit and not the memory one. The reserve holds what GOMEMLIMIT cannot see — measured on this binary: .text 27.4 MiB + .rodata 9.9 MiB + .gopclntab 24.1 MiB of demand-paged mapping. The variable is rendered only where a memory limit is; without one the downward API substitutes the node's allocatable memory. Render tests pin all of it, including a closed list of permitted container environment variables — that list did not exist, so an env var could be added with the suite green. Five mutations checked to fail: a rendered PDB, a spread constraint, a stray env var, a removed grace period, a resourceFieldRef naming the wrong container. docs/security.md unchanged: priorityClassName and terminationGracePeriodSeconds are already-collected placement fields (ADR 0031), no payload, grant or registry row moves. make test, make lint, make chart-lint green. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01PuDDpw5fVV4UKgpzdQoqbb
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Four questions a platform team asks at review were answered by omission:
priorityClassName,PodDisruptionBudget,topologySpreadConstraintsandterminationGracePeriodSecondsappeared nowhere in the chart. Two stay absentwith the reason recorded; two become decisions. ADR 0068 carries the rationale.
The two refusals
minAvailable: 1forbids the onlyeviction there is, so the agent becomes the reason a customer cannot drain
their own node;
maxUnavailable: 1forbids nothing.DaemonSet is spread by construction.
internal/chartrender/guardrail_test.gofails if either reappears, so reversingone means arguing with a test rather than adding a file.
The two additions
priorityClassNameon both roles, empty by default. The chart creates noPriorityClass — a priority value only means something against the classes a
cluster already has — and
values.yamlcarries the manifest of anegative-valued class, which is what actually gets the agent underneath
workloads that sit at zero. A
system-name outsidekube-systemis refusedat render time: admission rejects such pods rather than scheduling them lower,
so the failure would be a DaemonSet creating no pods and not saying why.
terminationGracePeriodSeconds: 30 on the controller, 10 on the node.The controller's number comes from the shutdown path. SIGTERM cancels the root
context,
wg.Wait()waits for the watchers and the periodic flush goroutine,then the shutdown pass writes the journals. The one hard bound in it is the
receiver's own 10s
Shutdownbudget (internal/nodeintake/server.go:76);watchers stop with their context, kubelet reads are cancelled with it and
bounded at 10s regardless (ADR 0045), and the pass is local writes with no
fsync. Ceiling: about ten seconds.
Below 15 the chart refuses, because what a shorter value costs is invisible:
SIGKILL lands before the pass and up to one coverage interval — 60s
(
cmd/agent/main.go:148) — ofinventory,container_restarts,pod_disruptions,job_runs,node_lifecycleandcollection_coverageisgone with no log line, counter or spool entry to notice it by. Usage rollups
survive (cumulative counters) and
workload_metadatais deliberately not onthat pass (ADR 0027).
The node gets 10 because it runs no pass and holds nothing that survives a
restart; its exit is bounded by one uninterruptible scan pass, so the number
bounds only how long this agent can hold up somebody else's node drain.
GOMEMLIMIT
80% of the container limit, passed in by the downward API and applied by the
agent. The Go runtime reads the cgroup CPU limit and sets GOMAXPROCS; there is
no memory equivalent, so an over-budget controller is OOM-killed — SIGKILL, and
the 60s above with it.
The downward API cannot express a fraction:
resourceFieldRefdivides, roundsup and emits a bare integer, so
divisor: 1Mion a 256Mi limit yields256,which
GOMEMLIMITreads as 256 bytes. Helm cannot compute it either — noquantity parser, and the input is whatever the operator wrote. So the chart
renders
MEMORY_LIMIT_BYTESandcmd/agent/memlimit.gocallsruntime/debug.SetMemoryLimit.The fraction is the decision, and the reserve has to hold what GOMEMLIMIT cannot
see. Measured on the linux/amd64 build of this binary:
At the chart's 256Mi default, 20% is 51 MiB; 90% would leave 26 MiB, thinner
than this binary's resident text is likely to be. The variable is rendered only
where a memory limit is — without one the downward API substitutes the node's
allocatable memory, and the agent would size itself to the node.
GOMAXPROCS is deliberately not set:
go 1.26already reads the CPU limit.Tests
The chart tests named closed lists for ClusterRole resources,
nonResourceURLs,capabilities and workload kinds — but not for container environment or pod-spec
fields, so an env var could be added with the suite green. This PR adds that
list (
TestTheContainerEnvironmentNamesNothingBeyondThisList, plus a bar onenvFrom) alongside the thing it guards.Five mutations checked to fail: a rendered PodDisruptionBudget, a spread
constraint, a stray env var, a removed grace period, a
resourceFieldRefnamingthe wrong container.
docs/security.md
Not touched, and the argument rather than a guess:
priorityClassNameandterminationGracePeriodSecondsare already-collected fields of the placementblock (ADR 0031, §8), so setting them on the agent's own pods adds no field and
no source. No grant changes —
priorityclassesandpoddisruptionbudgetsareread already — and no PriorityClass object is created for the agent to read. No
payload byte moves;
internal/sink/registry.gois untouched. The env var isnamed
MEMORY_LIMIT_BYTES, notGOMEMLIMIT, so it is not among the five namesADR 0047 keeps.
Evidence
Also built and tested from
git archive HEADinto a clean tree, so this is whatwas committed rather than what is in a working directory.
🤖 Generated with Claude Code
https://claude.ai/code/session_01PuDDpw5fVV4UKgpzdQoqbb