Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
47 changes: 47 additions & 0 deletions .github/workflows/helm-chart.yml
Original file line number Diff line number Diff line change
Expand Up @@ -755,6 +755,53 @@ jobs:
helm lint charts/foreman
echo "✅ foreman lint passed"

- name: Install helm-unittest
# Helm v3.13's `helm plugin install` will not skip signature
# verification (no --verify flag in 3.x), and the runner has no
# GPG keyring to verify the upstream release signature against.
# Pull the v1.1.1 plugin tarball directly, unpack into
# $(helm env HELM_PLUGINS), and strip `platformHooks` from
# plugin.yaml — that field was added after 3.13 and v3.13's
# plugin loader rejects unknown YAML keys, so the install hard-
# fails on the next `helm plugin list`.
run: |
set -euo pipefail
HELM_PLUGINS_DIR="$(helm env HELM_PLUGINS)"
PLUGIN_DIR="${HELM_PLUGINS_DIR}/unittest"
rm -rf "${PLUGIN_DIR}"
mkdir -p "${PLUGIN_DIR}"
curl -fsSL \
https://github.com/helm-unittest/helm-unittest/releases/download/v1.1.1/unittest-1.1.1.tgz \
| tar -xz -C "${PLUGIN_DIR}" --strip-components=1
chmod +x "${PLUGIN_DIR}"/untt-*
# Helm 3.13 strict-decodes plugin.yaml and rejects unknown
# keys; v1.1.1 added `platformHooks` (post-install hook map),
# which Helm 3.x's loader has never heard of. Drop the block
# with awk — it lives at the end of the file, indented under
# `platformHooks:`. The `platformCommand` map still picks the
# right binary per OS/arch, so the plugin runs without the
# post-install download hook.
awk '
/^platformHooks:/ { skip = 1; next }
skip && /^[^ ]/ { skip = 0 }
skip { next }
{ print }
' "${PLUGIN_DIR}/plugin.yaml" > "${PLUGIN_DIR}/plugin.yaml.tmp"
mv "${PLUGIN_DIR}/plugin.yaml.tmp" "${PLUGIN_DIR}/plugin.yaml"
# Trigger Helm's plugin discovery by listing once; HELM_PLUGINS
# is read on every invocation, so this picks the plugin up.
helm plugin list > /dev/null
helm unittest --help > /dev/null
echo "✅ helm-unittest installed"

- name: Run foreman chart unit tests
# Covers the agents: map multi-fleet (#994) shape plus the legacy
# single-agent backward-compat path. Fails CI on any drift in the
# expected resource names, suffixes, or per-agent field flow.
run: |
helm unittest charts/foreman
echo "✅ foreman chart unit tests passed"

- name: Template foreman with defaults
# foreman is a sibling chart to llmkube (no Helm subchart
# relationship), so no helm-repo wiring is required to render
Expand Down
40 changes: 28 additions & 12 deletions charts/foreman/templates/NOTES.txt
Original file line number Diff line number Diff line change
Expand Up @@ -6,25 +6,41 @@ Components:
{{- else }}
- foreman-operator DISABLED (operator.enabled=false)
{{- end }}
{{- if .Values.agent.enabled }}
- foreman-agent Deployment {{ include "foreman.fullname" . }}-agent
mode: {{ .Values.agent.mode }}
roles: {{ join "," .Values.agent.roles }}
{{- if .Values.agent.gateCache.enabled }}
gate cache PVC: {{ include "foreman.gateCache.pvcName" . }} ({{ .Values.agent.gateCache.size }})
{{- if or .Values.agents (and .Values.agent .Values.agent.enabled) }}
- foreman-agents ({{ len (include "foreman.agents" . | fromYaml) }} fleet{{ if gt (len (include "foreman.agents" . | fromYaml)) 1 }}s{{ end }}):
{{- range $name, $cfg := include "foreman.agents" . | fromYaml }}
{{- if $cfg.enabled }}
{{- $defaults := dict }}
{{- if $.Values.agent }}{{ $defaults = deepCopy $.Values.agent }}{{ end }}
{{- $agent := mustMergeOverwrite $defaults $cfg }}
{{- $ctx := dict "Release" $.Release "Values" $.Values "Chart" $.Chart "agentName" $name "agentConfig" $agent }}
- {{ include "foreman.agent.resourceName" $ctx }} mode: {{ $agent.mode }} roles: {{ join "," $agent.roles }}
{{- if $agent.gateCache.enabled }}
gate cache PVC: {{ include "foreman.gateCache.pvcName" $ctx }} ({{ $agent.gateCache.size }})
{{- end }}
{{- end }}
{{- end }}
{{- else }}
- foreman-agent DISABLED (agent.enabled=false)
{{- end }}

Next steps:
1. Watch the operator come up:
kubectl -n {{ include "foreman.namespace" . }} logs -l app.kubernetes.io/component=operator -f
{{- if .Values.agent.enabled }}
2. Confirm the agent registered itself as a FleetNode:
kubectl get fleetnodes
kubectl -n {{ include "foreman.namespace" . }} logs -l app.kubernetes.io/component=operator -f
{{- if or .Values.agents (and .Values.agent .Values.agent.enabled) }}
2. Confirm the agent(s) registered themselves as FleetNodes:
kubectl get fleetnodes
{{- end }}
3. Apply an Agent CR and an AgenticTask referencing it (examples
under examples/foreman/ in the repo).
under examples/foreman/ in the repo).

{{- if .Values.agents }}

You installed foreman in multi-fleet mode (agents: map, #994). To add
another fleet — e.g. a fork-pinned dogfood agent or a per-language
coder pool — extend the agents: map and re-run `helm upgrade`. Each
agent gets its own Deployment, ServiceAccount, ClusterRole, and gate
cache PVC, suffixed with the agent key, so they never collide.
{{- end }}

The Foreman docs live at https://llmkube.com/docs/foreman.
The Foreman docs live at https://llmkube.com/docs/foreman.
67 changes: 59 additions & 8 deletions charts/foreman/templates/_helpers.tpl
Original file line number Diff line number Diff line change
Expand Up @@ -86,13 +86,54 @@ ServiceAccount name for the foreman-operator.
{{- end }}

{{/*
ServiceAccount name for the foreman-agent.
ServiceAccount name for the foreman-agent. Expects a context dict with
"agentName" (the entry from .Values.agents, or the legacy sentinel
"_implicit_" when .Values.agent: is used unchanged) and "agentConfig"
(the per-agent values dict). When the sentinel is passed, the legacy
"<fullname>-agent" name is preserved so an existing install's SA is not
renamed on upgrade; an explicit agents.<name> entry always suffixes the
agent key so multiple agents never collide.
*/}}
{{- define "foreman.agent.serviceAccountName" -}}
{{- if .Values.agent.serviceAccount.create }}
{{- default (printf "%s-agent" (include "foreman.fullname" .)) .Values.agent.serviceAccount.name }}
{{- if .agentConfig.serviceAccount.create }}
{{- if eq .agentName "_implicit_" }}
{{- default (printf "%s-agent" (include "foreman.fullname" .)) .agentConfig.serviceAccount.name }}
{{- else }}
{{- default "default" .Values.agent.serviceAccount.name }}
{{- default (printf "%s-%s-agent" (include "foreman.fullname" .) .agentName | trunc 63 | trimSuffix "-") .agentConfig.serviceAccount.name }}
{{- end }}
{{- else }}
{{- default "default" .agentConfig.serviceAccount.name }}
{{- end }}
{{- end }}

{{/*
Default resource name for one foreman-agent (Deployment / ClusterRole /
ClusterRoleBinding). Same shape as the SA default — "<fullname>-agent"
for the legacy sentinel and "<fullname>-<agentName>-agent" for an
explicit agents.<name> entry — but with no per-agent override since the
SA already carries that escape hatch.
*/}}
{{- define "foreman.agent.resourceName" -}}
{{- if eq .agentName "_implicit_" }}
{{- printf "%s-agent" (include "foreman.fullname" .) }}
{{- else }}
{{- printf "%s-%s-agent" (include "foreman.fullname" .) .agentName | trunc 63 | trimSuffix "-" }}
{{- end }}
{{- end }}

{{/*
The agents map to render. When the user opts into the multi-fleet form
(.Values.agents is set) the map is returned verbatim. Otherwise the
legacy top-level .Values.agent: block is wrapped under a sentinel key
("_implicit_") so a single template path can render both shapes and the
legacy install's resource names are preserved on upgrade (#994). Callers
must not pass "_implicit_" as an explicit agents.* key.
*/}}
{{- define "foreman.agents" -}}
{{- if .Values.agents }}
{{- .Values.agents | toYaml }}
{{- else }}
{{- dict "_implicit_" .Values.agent | toYaml }}
{{- end }}
{{- end }}

Expand Down Expand Up @@ -135,10 +176,12 @@ Foreman operator image (registry/repo:tag or registry/repo@digest).
{{- end }}

{{/*
Foreman agent image (registry/repo:tag or registry/repo@digest).
Foreman agent image (registry/repo:tag or registry/repo@digest). Expects
the per-agent context dict with "agentConfig" (.Values.agents.<name> or
.Values.agent) and the chart context (Release/Values/Chart) at the root.
*/}}
{{- define "foreman.agent.image" -}}
{{- $img := .Values.agent.image -}}
{{- $img := .agentConfig.image -}}
{{- $repo := $img.repository -}}
{{- if $img.registry -}}
{{- $repo = printf "%s/%s" $img.registry $img.repository -}}
Expand All @@ -152,10 +195,18 @@ Foreman agent image (registry/repo:tag or registry/repo@digest).
{{- end }}

{{/*
Gate cache PVC name.
Gate cache PVC name for a single agent. Expects the per-agent context
dict; the legacy install (sentinel "_implicit_") keeps "<fullname>-gate-cache"
so an upgrade does not rename an in-use PVC. Explicit agents.<name>
entries get "<fullname>-<agentName>-gate-cache" so multiple agents each
own a distinct PVC. Users can still override via .agentConfig.gateCache.name.
*/}}
{{- define "foreman.gateCache.pvcName" -}}
{{- default (printf "%s-gate-cache" (include "foreman.fullname" .)) .Values.agent.gateCache.name }}
{{- $suffix := "" -}}
{{- if ne .agentName "_implicit_" -}}
{{- $suffix = printf "-%s" .agentName -}}
{{- end -}}
{{- default (printf "%s%s-gate-cache" (include "foreman.fullname" .) $suffix) .agentConfig.gateCache.name }}
{{- end }}

{{/*
Expand Down
Loading
Loading