Skip to content

feat(chart): support multiple agent fleets in one release#1028

Merged
Defilan merged 4 commits into
defilantech:mainfrom
joryirving:feat/issue-994-agents-map
Jul 10, 2026
Merged

feat(chart): support multiple agent fleets in one release#1028
Defilan merged 4 commits into
defilantech:mainfrom
joryirving:feat/issue-994-agents-map

Conversation

@joryirving

@joryirving joryirving commented Jul 9, 2026

Copy link
Copy Markdown
Collaborator

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 HelmRelease of 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 "another agent with 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

  • Backward compatibility: the legacy 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). Leaving agents unset keeps an in-place upgrade a no-op. The foreman.agents helper 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.
  • Deep-merge defaults: each agents.<name> entry deep-merges over .Values.agent via mustMergeOverwrite, 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 new agents.<name>.coderGitSecret / coderGitSecretKey fields that fall back to chart-level coder.gitCredentialsSecret).
  • Per-agent naming: explicit entries render as <fullname>-<agentKey>-agent (Deployment / SA / ClusterRole / ClusterRoleBinding) and <fullname>-<agentKey>-gate-cache (PVC), truncated to 63 chars, so two fleets never collide.
  • Singletons stay singleton: the operator Deployment, webhook Service / Secret / ValidatingWebhookConfiguration, all six foreman CRDs, and the coder SA / Role / RoleBinding still render once per release regardless of how many agents are listed.
  • enabled: false skip: an agents.<name>.enabled: false entry (or omitting agents entirely) drops that agent's resources cleanly without affecting the others.
  • Tests: added charts/foreman/tests/agents_test.yaml with 15 helm-unittest cases covering the legacy single-agent shape, multi-agent rendering, per-agent field propagation (gitRemoteURL, commit-author, coder-git-secret, githubToken), the enabled: false skip, and the singleton invariants. The foreman-chart CI job in .github/workflows/helm-chart.yml now installs the plugin and runs the suite; both charts/llmkube/tests/* and charts/foreman/tests/* are covered.

Verified locally: helm lint charts/foreman clean, helm template renders 21 resources at default (unchanged from before the patch) and 26 with the multi-agent fixture, helm unittest charts/foreman 15/15 pass, kubeconform 0 invalid on both renders, make fmt/vet/lint/test/lint-all all 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

  • Tests added/updated
  • make test passes locally
  • make lint passes locally
  • Commit messages follow conventional commits
  • All commits are signed off (git commit -s) per DCO
  • AI assistance (if any) is disclosed above, per CONTRIBUTING.md
  • Documentation updated (if user-facing change)

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>
@joryirving joryirving requested a review from Defilan as a code owner July 9, 2026 17:02
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

codecov Bot commented Jul 9, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.

📢 Thoughts on this report? Let us know!

@Defilan Defilan left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 error

That 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>
@joryirving

Copy link
Copy Markdown
Collaborator Author

Fixed in fb4da01. Moved the deep-merge ahead of the enabled gate in all three agent templates so the check evaluates the merged $agent.enabled, not the raw $cfg.enabled. Added an omitted-enabled fixture entry (replicaCount: 5, no enabled field) and two new test cases asserting it renders both a Deployment and a gate-cache PVC. Updated the SA/CR/CRB count to 9 and the PVC count to 2 to reflect the new 3-enabled-agent fixture.

Legacy render verified byte-identical to main (filtered for auto-generated cert material); helm lint clean; helm unittest charts/foreman 18/18 pass; make fmt/vet/lint/lint-all clean.

Two follow-ups you flagged as non-blocking — happy to address either or both in this PR or as separate issues, your call:

  1. _implicit_ reserved key — currently documented in the helper comment but not in values.yaml. I can move the note to the agents: block so users see it without reading the helper source.
  2. 63-char truncation collision on long <release-name>-<agentKey>-<role> combos — currently truncates silently with trunc 63 | trimSuffix "-", which can produce identical names from distinct inputs. A defensive guard (fail with a clear error when the post-truncation name is a duplicate, or warn via helm template --debug) would catch this at install time rather than at first apply.
  3. Map fields (nodeLabels etc.) deep-merge so a per-agent entry can add but not remove defaults — worth a NOTES.txt line so users know to set the whole map rather than expecting partial removal.

Let me know which (if any) you'd like folded in here vs. split out.

@Defilan Defilan left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

@Defilan Defilan merged commit a9a6c6b into defilantech:main Jul 10, 2026
24 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

feat(chart): support multiple agent fleets in one release (agents map)

2 participants