.agents/skills/plugin-helm/SKILL.md
Transform kustomize YAML → Helm chart for operator distribution.
Default: Reads dist/install.yaml, writes to dist/chart/
Flags: --manifests=<input> and/or --output-dir=<output>
After code changes in pkg/plugins/optional/helm/v2alpha/:
make install - Build and install updated binarymake generate-charts - Regenerate all sample charts in testdatamake verify-helm - Run yamllint + helm lint + kube-linterNever commit without running make verify-helm.
## = Documentation/description# = Commented-out spec value## line between description and spec## Description of the field
##
# fieldName: value
Uncomment a value only if:
Keep commented if:
All values.yaml fields MUST be conditional in templates. If a user removes a field from values.yaml, the chart must still work.
Required patterns:
Use {{- with }} for optional blocks:
{{- with .Values.manager.affinity }}
affinity: {{ toYaml . | nindent 10 }}
{{- end }}
Use {{- if }} with fallback for required fields:
resources:
{{- if .Values.manager.resources }}
{{- toYaml .Values.manager.resources | nindent 10 }}
{{- else }}
{}
{{- end }}
Never: Direct value reference without conditionals
# WRONG - breaks if removed from values.yaml
affinity: {{ toYaml .Values.manager.affinity | nindent 10 }}
When to use | default vs conditionals:
| default for fields extracted from kustomize - they are always present in values.yaml| default for fields that can be legitimately set to 0 (e.g., replicas for scale-to-zero). Helm treats 0 as falsy and will use the default instead, breaking the intended behavior.imagePullPolicy), use {{- with }} conditionals to let K8s apply its own defaults when not specified:
{{- with .Values.manager.image.pullPolicy }}
imagePullPolicy: {{ . }}
{{- end }}
replicas, image.repository are always extracted from kustomize - use direct references:
replicas: {{ .Values.manager.replicas }}
image: "{{ .Values.manager.image.repository }}..."
When adding new values.yaml fields, always add conditionals in templates.
## lineGood:
## Container image pull secrets for private registries
##
# imagePullSecrets:
# - name: regcred
Bad:
# This field is used by the template engine to render imagePullSecrets
# into the deployment spec when the value is provided by the user
# imagePullSecrets: []
Good:
// TemplatePorts replaces port numbers with Helm template references.
// Uses suffix matching to avoid false positives when project name contains "webhook".
func TemplatePorts(yamlContent string, resource *unstructured.Unstructured) string {
Bad:
// This function processes the yaml content and templates the ports
// by checking if it's a webhook or metrics service and then using
// regular expressions to replace the port numbers with values
func TemplatePorts(yamlContent string, resource *unstructured.Unstructured) string {
The plugin uses two different Machinery approaches:
Location: pkg/plugins/optional/helm/v2alpha/scaffolds/internal/kustomize/
All Kubernetes resources from dist/install.yaml are converted using the templater:
templates/DynamicTemplate wrapper (pre-rendered content, not Go templates)IfExistsAction = OverwriteFile)manager/manager.yaml, rbac/*.yaml, webhook/*.yamlKey file: dynamic_template.go - Wraps pre-rendered templates for Machinery
Location: pkg/plugins/optional/helm/v2alpha/scaffolds/internal/templates/
Static chart files use standard Machinery Go templates:
Chart.yaml - Never overwrites (IfExistsAction = SkipFile)values.yaml, NOTES.txt, _helpers.tpl, .helmignore - Respect --force flagImportant: Never delete the templates/ directory. Let Machinery overwrite files in place.
When modifying what appears in values.yaml:
Extraction struct for available extracted datatemplates/crd/ not crds/Decision: CRDs are placed in templates/crd/ instead of the standard crds/ directory.
Rationale: Helm's crds/ directory never upgrades CRDs on helm upgrade. For Kubernetes operators, CRD updates are critical (new API fields, validation changes). Placing CRDs in templates/crd/ ensures they upgrade with the chart.
Trade-off: Cannot mix CRDs and Custom Resources in the same chart (Helm installation order limitation).
Action: Preserve this placement. Do not move CRDs to crds/ directory.
The plugin uses Kubebuilder's Machinery framework for file management:
Never overwrite: Chart.yaml (user-managed version info)
Overwrite only with --force: values.yaml, NOTES.txt, _helpers.tpl, .helmignore, .github/workflows/test-chart.yml
Always overwrite: All templates/ resources (manager, rbac, webhook, etc.)
Important: Use Machinery's IfExistsAction to control overwrite behavior. Never delete the templates/ directory or individual files - let Machinery overwrite them in place. This preserves file timestamps and avoids breaking user workflows (e.g., open editors, file watchers).
Plugin documentation must stay synchronized with code changes.
When modifying plugin behavior:
docs/book/src/plugins/available/helm-v2-alpha.md to reflect changesExtraction struct{{- with }} or {{- if }} - see Template Conditional Pattern)make install && make generate-charts && make verify-helmmake generate-chartsmake install and run plugin on test projectmake generate-charts to update all samplesmake verify-helmAll tests use Ginkgo v2 + Gomega for behavior-driven development:
var _ = Describe("Feature Name", func() {
Context("Scenario description", func() {
It("should do specific behavior", func() {
// Arrange, Act, Assert
Expect(result).To(Equal(expected))
})
})
})
*_test.go): Fast, isolated component tests//go:build integration): End-to-end chart generation# Unit tests only (fast)
make test-unit
# Integration tests (requires kubebuilder binary in PATH)
make test-integration
# Helm chart validation (yamllint, helm lint, kube-linter)
make verify-helm
When adding features, ensure tests cover:
BDD pattern example:
Describe("Webhook port extraction", func() {
Context("when webhook is present in kustomize", func() {
It("should extract port and add to values.yaml", func() { ... })
It("should use port conditionally in template", func() { ... })
})
Context("when webhook is not present", func() {
It("should not add webhook section to values.yaml", func() { ... })
It("should not render webhook port in template", func() { ... })
})
Context("when user removes webhook from values.yaml", func() {
It("should not break template rendering", func() { ... })
})
})
Before submitting PR:
make test-unit passesmake test-integration passesmake install succeedsmake generate-charts succeedsmake verify-helm passescd testdata/project-v4-with-plugins && make helm-deploy IMG=test:latest{{- with }} or {{- if }}) for all values--manifests and --output-dir flagsSee references/REFERENCE.md for: