feat(chart): support multiple agent fleets in one release#1028
Conversation
Model agents as a map (agents:<key>: {...}) that renders one Deployment +
ServiceAccount + ClusterRole + ClusterRoleBinding + gate-cache PVC per
entry, suffixed with the agent key, so multiple heterogeneous fleets
coexist in one chart release. Operator / webhook / CRDs / coder RBAC
stay singleton — the cluster-scoped pieces never need to be turned off
for a second fleet.
Backward compat: leaving agents unset continues to render the legacy
single-agent layout under the existing names (<fullname>-agent,
<fullname>-gate-cache) so an in-place upgrade is a no-op. Each
agents.<name> entry deep-merges over the legacy agent: block so users
only restate fields that differ from the defaults.
Per-agent overrides: replicaCount, image, roles, nodeLabels,
nodeSelector/affinity/tolerations, resources, gitRemoteURL, githubToken,
commitAuthor*, gateCache, and the coder git-secret pass-through
(coderGitSecret / coderGitSecretKey). Resource names use the shape
<fullname>-<agentKey>-agent (Deployment / SA / CR / CRB) and
<fullname>-<agentKey>-gate-cache (PVC).
Adds a helm-unittest suite covering the legacy path, the multi-fleet
path, per-agent field propagation, the enabled: false skip, and the
singleton invariants, plus a CI step that runs it.
Fixes defilantech#994
Signed-off-by: Jory Irving <jory@jory.dev>
Helm v3.13's `helm plugin install` has no `--verify` flag, and the upstream release tarball ships a .prov file the runner cannot verify (no GPG keyring on ubuntu-latest). The original step worked locally on Helm 4.x with `--verify=false` but hard-fails on the pinned CI version. Download the v1.1.1 plugin tarball directly, unpack into $(helm env HELM_PLUGINS), and chmod +x the per-platform binaries. The plugin.yaml ships every architecture's binary in-tree so no post-install download hook is required. Signed-off-by: Jory Irving <jory@jory.dev>
helm-unittest v1.1.1 ships a plugin.yaml with a top-level `platformHooks` block that Helm v3.13's plugin loader does not recognize — the loader strict-decodes plugin.yaml and rejects unknown fields, so `helm plugin list` after extraction returns `unknown field "platformHooks"` and the install step hard-fails before the unit tests run. Drop the block with awk (it lives at the end of the file, indented under `platformHooks:`). The `platformCommand` map picks the right binary per OS/arch on its own, so the plugin runs without the post-install download hook (which is what `platformHooks` would have invoked anyway — that hook just downloads the binary on a cold install, but v1.1.1 ships every binary in-tree). Signed-off-by: Jory Irving <jory@jory.dev>
Codecov Report✅ All modified and coverable lines are covered by tests. 📢 Thoughts on this report? Let us know! |
Defilan
left a comment
There was a problem hiding this comment.
Nice work, Jory. Pulled it down and verified the important bits: default render is byte-identical to main, the deepCopy-per-iteration keeps the merge safe, per-agent RBAC binds to the right SA, and check-helm-rbac + helm unittest (15/15) are green.
One fix before merge. All three agent templates gate on {{- if $cfg.enabled }}, the raw entry evaluated before the deep-merge, so enabled is the only field that doesn't inherit from .Values.agent. An entry that omits it renders nothing, silently:
agents:
gpu: {replicaCount: 2} # no enabled -> zero resources, no errorThat contradicts the "only restate what differs" model, and the fixture misses it because every entry sets enabled explicitly. Can you compute $agent before the gate and check $agent.enabled, and add a fixture case for the omitted-enabled path?
Minor, non-blocking: _implicit_ is an unguarded reserved key; the 63-char truncation can collide on long release-name + key combos; map fields like nodeLabels deep-merge so a per-agent entry can add but not remove defaults (worth a NOTES line).
Fix the enabled gate and I'm good to merge.
The `{{- if $cfg.enabled }}` gate in agent-deployment,
agent-rbac, and gate-cache-pvc evaluated the raw per-agent entry
before the deep-merge, so `enabled` was the one field that did
NOT inherit from .Values.agent. An entry that omitted it rendered
zero resources silently, contradicting the "only restate what
differs" model documented in values.yaml (defilantech#994 review feedback).
Move the merge ahead of the gate and check $agent.enabled so the
inheritance is consistent with every other field. Add a fixture
entry (omitted-enabled: replicaCount: 5, no enabled) and a test
case asserting it renders a Deployment + gate-cache PVC. Update
the existing SA/CR/CRB and PVC count assertions to reflect the
new 3-enabled-agent fixture.
Legacy single-agent path is unchanged: the implicit sentinel
entry merges the legacy .Values.agent over itself, so the
$agent.enabled check equals .Values.agent.enabled.
Signed-off-by: Jory Irving <jory@jory.dev>
|
Fixed in fb4da01. Moved the deep-merge ahead of the Legacy render verified byte-identical to main (filtered for auto-generated cert material); Two follow-ups you flagged as non-blocking — happy to address either or both in this PR or as separate issues, your call:
Let me know which (if any) you'd like folded in here vs. split out. |
Defilan
left a comment
There was a problem hiding this comment.
Thanks Jory, this nails it. Verified all three templates now gate on $agent.enabled after the merge, so an omitted enabled inherits the default and renders (I reproduced the previously-silent gpu case). Backward-compat is byte-stable (default still 21 resources, legacy names unchanged), the new omitted-enabled fixtures cover it, unittest is 18/18, and check-helm-rbac + lint are green. Nice bonus hardening the plugin install too. Approving.
What
Model the foreman chart's agents as a map (
agents: <key>: {...}) that renders one Deployment + ServiceAccount + ClusterRole + ClusterRoleBinding + gate-cache PVC per entry, suffixed with the agent key. Operator / webhook / CRDs / coder RBAC stay singleton, so multiple heterogeneous agent fleets coexist in one chart release instead of needing a second release (which would collide on the chart's cluster-scoped singletons).Why
Running a second differently-configured agent today means a second
HelmReleaseof the same chart, which fights the primary release for ownership of the CRDs, the webhook, the operator, and the fixed-name coder SA. The companion CRD-toggle (#998) made that workaround work; this issue makes it unnecessary by collapsing "anotheragentwith different values" into the same chart, one release. Concrete use cases in the issue: a fork-pinned dogfood agent with its own PAT/role/commit-identity, per-language or per-model coder pools, GPU vs remote-model pools with distinct nodeLabels.Fixes #994
How
agent:block continues to render the exact resources it always has (Deployment / SA / ClusterRole / ClusterRoleBinding / PVC), with the exact same names (<fullname>-agent,<fullname>-gate-cache). Leavingagentsunset keeps an in-place upgrade a no-op. Theforeman.agentshelper wraps the legacy block under a sentinel key (_implicit_) so a single template path renders both shapes; legacy users must not pass that key explicitly.agents.<name>entry deep-merges over.Values.agentviamustMergeOverwrite, so users opting into the map only restate what differs from the legacy defaults (replicaCount,image,roles,nodeLabels,nodeSelector/affinity/tolerations,resources,gitRemoteURL,githubToken,commitAuthor*,gateCache, and a per-agent coder-git-secret pass-through via newagents.<name>.coderGitSecret/coderGitSecretKeyfields that fall back to chart-levelcoder.gitCredentialsSecret).<fullname>-<agentKey>-agent(Deployment / SA / ClusterRole / ClusterRoleBinding) and<fullname>-<agentKey>-gate-cache(PVC), truncated to 63 chars, so two fleets never collide.enabled: falseskip: anagents.<name>.enabled: falseentry (or omittingagentsentirely) drops that agent's resources cleanly without affecting the others.charts/foreman/tests/agents_test.yamlwith 15 helm-unittest cases covering the legacy single-agent shape, multi-agent rendering, per-agent field propagation (gitRemoteURL,commit-author,coder-git-secret,githubToken), theenabled: falseskip, and the singleton invariants. The foreman-chart CI job in.github/workflows/helm-chart.ymlnow installs the plugin and runs the suite; bothcharts/llmkube/tests/*andcharts/foreman/tests/*are covered.Verified locally:
helm lint charts/foremanclean,helm templaterenders 21 resources at default (unchanged from before the patch) and 26 with the multi-agent fixture,helm unittest charts/foreman15/15 pass,kubeconform0 invalid on both renders,make fmt/vet/lint/test/lint-allall clean.Assisted-by: LLM (Minimax-M3 via opencode, generated the chart template refactor, helm-unittest suite, and CI step; I designed the backward-compat path, verified every render, ran the gate locally, and own the review conversation).
Checklist
make testpasses locallymake lintpasses locallygit commit -s) per DCO