Components
July 16, 2026 · View on GitHub
A component in AICR is a registry entry pointing to a Helm chart or
Kustomize source that recipes can pull. The catalog lives in
recipes/registry.yaml;
per-component default values live under
recipes/components/<name>/.
Overlays bind a component to a cluster shape; bundlers turn that
binding into a deployer-specific artifact.
Most components need no Go code. The declarative path is one
registry entry plus a values.yaml. The legacy
pkg/component/generic.go::ComponentConfig
is marked Deprecated — it is unused in production. The live schema
is pkg/recipe/components.go::ComponentConfig.
For the recipe data model — overlays, mixins, criteria, merge order — see recipe.md. This page is the contributor view for adding or changing components.
Where Does My Change Go?
| I want to... | Edit | Guide |
|---|---|---|
| Make an existing chart or kustomization available to recipes | recipes/registry.yaml entry | this page |
| Set default values for the chart | recipes/components/<name>/values.yaml | this page |
| Pin a chart version for a specific cluster shape | Recipe overlay in recipes/overlays/ | recipe.md |
| Add a bundle-time validation warning | registry.yaml validations: block | validator.md |
| Add a chainsaw health check | registry.yaml healthCheck.assertFile + recipes/checks/<name>/health-check.yaml | validator.md |
| Adjust where node selectors land in chart values | registry.yaml nodeScheduling paths | this page |
Helm vs Kustomize
A component declares either helm: or kustomize: — never
both. ComponentRegistry.Validate rejects the mixed shape at load.
Use helm: when | Use kustomize: when |
|---|---|
| Upstream ships a published Helm chart | Upstream ships only a Git source with kustomization.yaml |
You need --set value overrides | You can accept no --set support |
You want nodeScheduling injection | You will configure scheduling out-of-band (Kustomize ignores Helm value paths) |
Kustomize limitations to know up front:
--set <key>:<path>=<value>flows through Helm value rendering only; Kustomize components silently ignore overrides.nodeScheduling.system/acceleratedpaths target Helm values; they do not apply to Kustomize sources.- The bundler runs
kustomize buildat bundle time and wraps the output astemplates/manifest.yamlinside the standard local-format folder (see index.md for the classification rule).
Adding a Helm Component
1. Add the registry entry to recipes/registry.yaml:
- name: my-operator
displayName: My Operator
valueOverrideKeys:
- myoperator
helm:
defaultRepository: https://charts.example.com
defaultChart: example/my-operator
defaultVersion: v1.0.0
defaultNamespace: my-operator
nodeScheduling:
system:
nodeSelectorPaths: [operator.nodeSelector]
tolerationPaths: [operator.tolerations]
2. Create recipes/components/my-operator/values.yaml with the
chart defaults you want every recipe to start from. Keep this file
minimal and widely applicable — cluster-specific tweaks belong in
values-<context>.yaml siblings referenced from an overlay.
# fullnameOverride avoids the aicr-stack- prefix on resource names.
fullnameOverride: my-operator
operator:
replicas: 1
3. Optional blocks on the registry entry:
validations:— bundle-time misconfiguration warnings (validator.md)healthCheck.assertFile:— chainsaw conformance assertions (validator.md)manifestFiles:— manifest YAMLs (paths relative to the recipes data root) bundled with the component whenever a recipe references it and the componentRef declares none; shipped in the injected-postlocal chart after the main release. Ref-declared lists take precedence.storageClassPaths:— where--storage-classis injectedpodScheduling.workload.workloadSelectorPaths— for workload-pod placementgkeCriticalPriority,hasSelfRefCRDs,manifestsUseChartCRDs— narrow service-specific quirks (see godoc onComponentConfigfor when these apply)
4. Run make bom-docs and commit the regenerated
docs/user/container-images.md in the same PR. CLAUDE.md treats this
as a hard rule whenever you change registry.yaml, a component's
values.yaml, or any chart version pin. See BOM regeneration.
5. Run make qualify — covers tests, lint, and the recipe-resolution
suite that parses every registry entry.
Adding a Kustomize Component
- name: my-kustomize-app
displayName: My Kustomize App
valueOverrideKeys:
- mykustomize
kustomize:
defaultSource: https://github.com/example/my-app
defaultPath: deploy/production
defaultTag: v1.0.0
No recipes/components/<name>/values.yaml is required — Kustomize
reads its inputs from the upstream source. Reminder: no --set
overrides, and nodeScheduling paths do not apply.
Schema Reference
Authoritative definitions live in
pkg/recipe/components.go.
One-liner per field:
| Field | Purpose |
|---|---|
name | Component identifier; must match componentRefs[].name in overlays |
displayName | Human-readable label used in CLI output and bundle templates |
valueOverrideKeys | Alt prefixes for --set <key>:path=value matching |
helm.defaultRepository | Helm repo URL injected when an overlay leaves it empty |
helm.defaultChart | Chart name (e.g. nvidia/gpu-operator) |
helm.defaultVersion | Default chart version |
helm.defaultNamespace | Install namespace |
kustomize.defaultSource | Git or OCI source URL |
kustomize.defaultPath | Subpath within the source |
kustomize.defaultTag | Git ref / OCI tag |
nodeScheduling.system | Helm value paths that receive the control-plane node selector / tolerations / taints |
nodeScheduling.accelerated | Helm value paths that receive the GPU node selector / tolerations / taints |
nodeScheduling.nodeCountPaths | Where --nodes is written |
podScheduling.workload.workloadSelectorPaths | Workload-pod placement |
storageClassPaths | Where --storage-class is written |
validations | Bundle-time component check list (validator.md) |
healthCheck.assertFile | Chainsaw assert YAML path (relative to data dir) |
manifestFiles | Default manifest YAML paths bundled when the componentRef declares none (ref-declared lists take precedence). No opt-out: an empty ref-declared list is indistinguishable from absent (len == 0 → defaults filled) — to suppress the defaults, declare a replacement list. Helm components only; the loader rejects the combination with kustomize: |
gkeCriticalPriority, hasSelfRefCRDs, manifestsUseChartCRDs | Narrow service-specific flags (see godoc) |
nodeScheduling.system vs accelerated
This is the field most contributors get wrong on first PR.
system— paths into chart values for workloads that must land on management / control-plane nodes (e.g., operators, controllers, webhooks). The bundler writes the--system-node-selectorand--system-node-tolerationvalues here.accelerated— paths into chart values for workloads that must land on GPU nodes (e.g., device-plugin DaemonSets, driver-validation, DCGM exporters). The bundler writes the--accelerated-node-selectorand--accelerated-node-tolerationvalues here.
Concrete example from gpu-operator:
nodeScheduling:
system:
nodeSelectorPaths:
- operator.nodeSelector
- node-feature-discovery.master.nodeSelector
tolerationPaths:
- operator.tolerations
accelerated:
nodeSelectorPaths:
- daemonsets.nodeSelector
- node-feature-discovery.worker.nodeSelector
tolerationPaths:
- daemonsets.tolerations
Wrong column = workloads land on the wrong node class. A DaemonSet
placed under system will miss GPU nodes; an operator under
accelerated will refuse to schedule on a cluster with tainted GPU
nodes only.
valueOverrideKeys
--set <key>:<path>=<value> matches via GetByOverrideKey:
- The component
nameis checked first. - Each entry in
valueOverrideKeysis then checked.
For gpu-operator with valueOverrideKeys: [gpuoperator], both
--set gpu-operator:driver.version=... and
--set gpuoperator:driver.version=... resolve to the same component.
Pick a key that is easier to type (no hyphens) and document it in the
displayName-adjacent comments if non-obvious. Override keys are
globally unique — ComponentRegistry.Validate rejects duplicates.
Exposing Scheduling Knobs Without New Flags
A recurring first-PR instinct is to add a new CLI flag for every placement knob a chart exposes — one flag for the controller, one for the workers, one for a sidecar, and so on. Don't. AICR already models node placement as value paths, not flags, so the existing primitives cover arbitrary chart depth without growing the CLI surface.
Two separate mechanisms are at work — keep them distinct:
Path routing. nodeScheduling is registry metadata that maps chart
value paths onto the system / accelerated node classes (see
the section above). It selects
which chart paths receive a node selector / toleration; it does not
participate in value precedence. The bundler fans --system-node-selector
/ --accelerated-node-selector (and the matching *-toleration flags)
out to every declared path, so one flag covers N workloads.
Value precedence. Once a path is targeted, its value is resolved across layers, lowest precedence first:
- Component value defaults — ship sane defaults in
recipes/components/<name>/values.yaml, or per-recipe in an overlaycomponentRefs[].valuesFile/ inlineoverrides. This is where the common case (e.g. a managed cluster's standard node labels) works out of the box with no flags at all. - Deploy-time overrides —
--set <key>:<path>=<value>reaches any value path for last-mile deviations;--dynamic <key>:<path>defers a path to install time (it is stripped fromvalues.yamlintocluster-values.yamlfor the operator to fill in).
Merge order is base → valuesFile → overlay overrides →
--set / --dynamic, so a bundle-time override always wins over a
recipe default. The node-class flags (--system-node-selector et al.)
write into their routed paths at bundle time, alongside --set.
Deciding where a knob belongs:
| Situation | Where it goes | Why |
|---|---|---|
| Workload must run on management / control-plane nodes | Path under nodeScheduling.system | Inherits --system-node-selector automatically |
| Workload must run on GPU nodes | Path under nodeScheduling.accelerated | Inherits --accelerated-node-selector automatically |
| A pool that is neither (e.g. a dedicated CPU-worker pool) | Leave it out of nodeScheduling; target via --set <key>:that.path.nodeSelector.<label>=<value> | The two-class model intentionally covers only system vs GPU; everything else is reachable by path |
| Value is cluster-specific and unknown at bundle time | --dynamic <key>:<path> | Lands in cluster-values.yaml for install-time entry |
"When omitted, inherit from the node class" needs no special logic — a
path listed under system / accelerated is the inheritance.
Caveat: the chart must actually render the path. A nodeSelector
path only takes effect if the upstream chart template renders it. Some
charts honor only affinity / tolerations and silently drop
nodeSelector, so a declared path becomes a no-op. Verify against the
chart's templates before adding a path. If the chart does not support
it, either pin placement through the affinity path the chart does
render (as a values.yaml default) or omit the path and document the
limitation in a registry comment. The slinky-slurm-operator entry in
registry.yaml
is a worked example of the latter.
deploymentOrder
RecipeResult.DeploymentOrder is derived, not authored.
TopologicalSort in pkg/recipe/metadata_store.go orders components
by componentRefs[].dependencyRefs declared in the overlay. When no
dependencies are declared, the order falls back to the order in
which components are listed in the overlay's componentRefs. Express
ordering by declaring dependencyRefs on the dependent component, not
by writing a separate deploymentOrder block.
Local Format and Bundle Classification
The bundler emits a uniform NNN-<component>/ layout via
pkg/bundler/deployer/localformat.
Classification (single source of truth in localformat.classify):
| Recipe shape | Folder kind |
|---|---|
helm.defaultRepository set, no manifestFiles | KindUpstreamHelm |
helm.defaultRepository set + manifestFiles | KindUpstreamHelm + KindLocalHelm (-post injected) |
helm.defaultRepository == "" + manifestFiles | KindLocalHelm |
kustomize.*Tag or *Path set | KindLocalHelm (kustomize build → templates/manifest.yaml) |
If both helm and kustomize fields are populated, Validate
rejects the registry entry — there is no precedence rule because the
shape is invalid. manifestFiles are added post-chart; preManifestFiles
ship at sync-wave N-1 (e.g., a Namespace with PSS labels the chart
pods depend on).
Deployers
AICR ships five output adapters in
pkg/bundler/deployer/:
helm, helmfile, argocd, argocd-helm, flux. Each calls
localformat.Write() and then layers its own orchestration files
(deploy.sh, helmfile.yaml, Argo Application CRs, Flux
HelmReleases). Components do not need to be deployer-aware —
the bundler renders per-deployer from one component definition.
Deployment ordering
One model, three projections. Every deployer derives ordering from
the same source: each component's declared dependencyRefs. What
differs is how faithfully a deployer's native mechanism can express
that dependency graph. For the concurrent deployers, components with no
path between them are independent and roll out concurrently, while a
real dependency gates (the dependent waits for its dependency to be
healthy). The NNN-<name>/ folder numbers reflect DeploymentOrder
(a topological serialization) for readability, and drive the two
deliberately linear paths — the helm deploy.sh and --serial mode —
which install strictly in that order regardless of tiers.
pkg/recipe exposes the graph two ways, and deployers pick whichever
fits their mechanism:
TopologicalSort→ a flatDeploymentOrder(one valid serialization).ComponentRefsTopologicalLevels→ dependency-depth tiers, where a tier holds exactly the components whose dependencies are all satisfied by earlier tiers (so a tier's members are mutually independent).
flux — the exact DAG. Flux's dependsOn is a native dependency
graph, so the flux deployer projects each component's declared
dependencyRefs directly onto it: a HelmRelease's dependsOn names
exactly its dependencies' terminal releases, nothing more. This is the
most faithful and most parallel rendering — a component waits only for
what it actually needs — and it reads naturally to flux users because
dependsOn mirrors dependencyRefs one-for-one. See declaredDependsOn.
argocd / argocd-helm — tiers as sync-wave bands. Argo CD's
sync-wave is a single integer per Application. Applications sharing a
wave sync together, and Argo advances to a higher wave only once the
current one is healthy — ordered bands, a partial order rather than a
total one. Because a component gets one integer, it cannot express "wait
for A but not B" when A and B sit at the same depth, so the DAG is
approximated by tiers: wave = tier*4 + phase, where the
per-component phase orders -pre → primary → -post → -readiness.
The stride-4 band width keeps consecutive tiers disjoint, so a tier's
readiness gate still blocks the next tier. The cost of the coarser
banding is mild over-constraint (a component waits for its whole prior
tier, not just its own dependencies). See waveForFolder.
helmfile — tiers as sequential sub-helmfiles. Emits one
level-N.yaml sub-helmfile per tier, processed in sequence so each
tier's CRDs land in the cluster's REST mapper before the next tier's
plan is rendered (issue #914). Within a sub-helmfile, needs: edges
chain only a component's own -pre → primary → -post releases;
independent components in the tier carry no edge, so helmfile applies
them concurrently. Cross-tier ordering is the sub-helmfile sequence, not
per-release needs: — the same tier approximation as argocd. See
buildHelmfile.
helm — deliberately serial. The deploy.sh installs one component
at a time in DeploymentOrder, trading parallelism for the simplest
possible shell. The readiness gates already sequence it correctly.
Disabling parallelism (--serial)
aicr bundle --serial forces every deployer to install components
strictly one at a time in DeploymentOrder, an escape hatch for
reproducing the pre-parallelism ordering or bisecting a misbehaving
rollout. It affects the four concurrent deployers: argocd and
argocd-helm fall back to a linear sync-wave per folder, flux chains each
HelmRelease dependsOn to the previous component instead of
projecting the DAG, and helmfile chains every release to its predecessor
via needs: (one linear apply chain) instead of only within a
component. The helm deploy.sh is already serial, so the flag is a
no-op for it. Off by default.
See index.md for the deployer matrix.
BOM Regeneration
docs/user/container-images.md is rendered fresh from each Helm
chart's actual templates by make bom-docs. Run it and commit the
regenerated file in the same PR whenever you:
- Add or remove a component
- Bump a chart version (in
registry.yaml, an overlay, or a mixin) - Change a
values.yamlin a way that affects which images render (image-repo override, subchart enable/disable, etc.)
The BOM's version column and component set are gated at PR time
(TestCommittedBOMVersionsMatchRegistry plus the bom-freshness
merge-gate job), so a missed regen after a version or component-set
change fails CI. Not gated at PR time is rendered-image drift — a
chart pulling a new image with no pin change on our side; make bom-check (a full re-render comparison) is its opt-in blocking
check, and the weekly BOM-refresh workflow auto-detects it and opens a
PR. Still run make bom-docs locally on any chart-touching change.
Boundary: Components Are Metadata
A component entry describes what to deploy and where its values
land. Components do not carry apply, wait, uninstall, rollback,
or readiness-polling logic — those concerns belong to the deployer
that consumes the bundle. If you find yourself writing custom apply
code inside the bundler or under pkg/component/, you are on the
wrong side of the boundary — see
index.md "What AICR Is Not".
See Also
- recipe.md — overlays, mixins, criteria, the recipe data model
- validator.md — bundle-time component validation checks
- validator.md — chainsaw health checks and validator runner
- index.md — contributor index and architectural boundary
- integrator/recipe-development.md — end-user recipe authoring
- user/component-catalog.md — end-user component catalog
pkg/recipe/components.go—ComponentConfigsource of truthrecipes/registry.yaml— live component catalog