Bundler Development Guide
Learn how to add new components to AICR.
Overview
The bundler system converts RecipeInput objects into deployment artifacts (Helm values files, Kubernetes manifests, optional custom manifests). READMEs are generated at the deployer level, not by individual component bundlers.
Architecture: Component configuration is declarative in recipes/registry.yaml — adding a component requires only a registry entry and values files, no Go code. pkg/bundler.DefaultBundler generates Helm per-component bundles from recipes; components are selected by componentRefs. CLI --set overrides flow through ApplyMapOverrides(). The registry declares paths for injecting node selectors / tolerations. Errors use pkg/errors codes.
Local Format (Shared Bundle Layout)
pkg/bundler/deployer/localformat writes the uniform numbered NNN-<component>/ bundle layout consumed by every deployer. It owns per-folder content (Chart.yaml, values.yaml, cluster-values.yaml, install.sh, templates/, upstream.env). Deployers (helm, helmfile per #632, argocd, argocd-helm, flux) call localformat.Write() and then add their own top-level orchestration files (deploy.sh, helmfile.yaml, Application CRs, etc.) — they never re-classify components or duplicate the per-folder writer.
Classification rule (single source of truth, in localformat.classify):
Load-bearing invariants (don’t violate without changing the design):
localformatnever writes deployer-specific files.deploy.sh,helmfile.yaml, argocdApplicationCRs, FluxHelmReleases — all produced by the respective deployer afterWrite()returns. This separation is what makes a single layout consumable by every deployer.install.shis never name-customized. It is rendered from one of exactly two templates (install-upstream-helm.sh.tmpl,install-local-helm.sh.tmpl), parameterized only by data (name, namespace, upstream ref). Name-keyed quirks (kai-scheduler async timeout, nodewright-operator taint cleanup, DRA restart, orphan-CRD scan) stay indeploy.shas name-matched blocks — not ininstall.sh. This is the structural barrier that prevents per-folder scripts from accumulating drift.Writeis deterministic and idempotent. Same inputs → same on-disk bytes → sameFolderslice. Map iteration is sorted; no timestamps or random suffixes are embedded.
For the full classification table, base-format invariants, and the helm deployer’s call site, see pkg/bundler/deployer/localformat/doc.go (godoc) and pkg/bundler/deployer/helm/helm.go::Generate. Further design history: ticket #662.
Quick Start
Adding a New Component (Declarative Approach)
Adding a new component requires no Go code. Simply add an entry to the component registry:
Step 1: Add to recipes/registry.yaml
Step 2: Add component values file
Create recipes/components/my-operator/values.yaml:
Step 3: Reference in recipe
Add the component to a recipe overlay in recipes/overlays/:
That’s it! The bundler system automatically:
- Loads component configuration from the registry
- Extracts values from the recipe’s valuesFile
- Applies user value overrides from CLI
--setflags - Applies node selectors and tolerations to configured paths
- Generates the per-component bundle with the component’s values and manifests
Optional: Custom Manifests
For components that need additional Kubernetes manifests (beyond the Helm chart), add them to recipes/components/<name>/manifests/:
Step 1: Create manifest file
Create the manifest under recipes/components/<name>/manifests/. Files are
rendered as Helm templates, so they can reference component values via
{{ index .Values "<component>" }} when needed. Abbreviated skeleton (the
in-tree recipes/components/network-operator/manifests/nfd-network-rule.yaml
is the complete real-world example):
Step 2: Reference in recipe
Add the manifest to the component’s manifestFiles in the recipe:
The bundler automatically includes manifest files in the component’s manifests/ directory.
When to inline values instead. If the upstream chart already exposes a
templating hook for the resource you want to ship (e.g. the gpu-operator
chart renders a dcgm-exporter ConfigMap directly from
dcgmExporter.config.data), put the content in the component’s
values.yaml instead of adding a post-manifest. Inlining keeps the resource
in the same Helm release as its consumer, so install ordering and upgrades
are handled by Helm and an extra kubectl apply pass is unnecessary.
Registry Configuration Reference
The component registry (recipes/registry.yaml) supports these fields:
Helm Component Configuration:
Kustomize Component Configuration:
Note: A component must have either helm OR kustomize configuration, not both. The system will detect the component type based on which configuration is present.
Note:
- Values are written directly to
values.yaml, not via templates - Deployment documentation (README) is generated at the deployer level (helm, argocd, flux)
- The
pkg/componentpackage provides helper utilities if custom bundler logic is needed
Best Practices
Adding Components
- Prefer declarative configuration: Add entries to
registry.yamlrather than writing Go code - Use consistent naming: component name should match the Helm chart name (e.g.,
gpu-operator) - Define
valueOverrideKeysfor user-friendly--setprefixes (e.g.,gpuoperatorallows--set gpuoperator:key=value) - Configure
nodeSchedulingpaths only for components that need workload placement - Create values files under
recipes/components/<name>/for reusable configurations
Values Files
- Keep base values (
values.yaml) minimal and widely applicable - Create overlay values (
values-<context>.yaml) for specific scenarios - Document non-obvious settings with comments
- Use consistent formatting (2-space indent for YAML)
- Override release name prefix: Use
fullnameOverrideto avoid theaicr-stack-prefix in resource names. This makes resource names cleaner and more predictable. For example, inkube-prometheus-stack/values.yaml:Without this override, resources would be namedaicr-stack-kube-prometheus-*instead ofkube-prometheus-*.
Custom Manifests
- Only add custom manifests when the Helm chart doesn’t provide needed functionality
- Use Helm template syntax (not Go templates) for manifest files
- Reference values via
{{ index .Values “component-name” }} - Make manifests conditional with
{{- if }}blocks
Testing
- Run
make testto validate all recipe data - Test recipe generation:
aicr recipe --service eks --accelerator gb200 - Test bundle generation:
aicr bundle -r recipe.yaml -o ./test-bundle - Verify generated
values.yamlcontains expected settings
Testing in a Local Kind Cluster
Step 0: Create a local kind cluster
Create a local kind cluster for end-to-end testing.
This creates a kind cluster with two nodes and starts Tilt.
Step 1: Build the aicr binary
Build the CLI with embedded recipe data and install it:
This compiles the Go code and embeds all files from recipes/ into the binary. The binary is copied to /usr/local/bin/ for global access.
Step 2: Generate the recipe
Generate a recipe optimized for Kind clusters:
This creates a recipe.yaml file with:
- Components configured for local development (reduced resources, emptyDir storage)
- GPU operator with driver installation disabled (uses host drivers via passthrough)
- cert-manager with extended startupapicheck timeout
- nvsentinel with network policy disabled
Step 3: Generate the Helm bundle
Convert the recipe into a Helm per-component bundle:
This generates a bundle/ directory containing:
deploy.sh- One-command deployment scriptREADME.md- Deployment guide with ordered stepsrecipe.yaml- Copy of input recipe<component>/values.yaml- Component-specific Helm values<component>/README.md- Per-component install/upgrade/uninstall<component>/manifests/- Additional manifests (if any)
Step 4: Deploy the bundle
Run the deployment script:
Step 5: Verify the deployment
Check that all pods are running:
All pods should show Running or Completed status. Common issues:
- Pending pods: Check for resource constraints with
kubectl describe pod <name> -n aicr-stack - CrashLoopBackOff: Check logs with
kubectl logs <pod-name> -n aicr-stack - ImagePullBackOff: Verify network connectivity and image registry access
Cleanup:
To remove the deployment:
Note: Some cluster-scoped resources (CRDs, ClusterRoles, Webhooks) may need manual cleanup:
Documentation
- Update
recipes/README.mdwhen adding new components - Document component-specific settings in values file comments
- Add examples to
examples/directory for common use cases
Common Patterns
Component Registry Structure
Components are configured in recipes/registry.yaml. Here’s an example entry:
Node Scheduling Configuration
The bundle command supports --system-node-selector, --system-node-toleration, --accelerated-node-selector, --accelerated-node-toleration, and --nodes flags.
How it works:
- Paths are defined in
registry.yamlundernodeScheduling(e.g.nodeSelectorPaths,tolerationPaths,nodeCountPaths) - The bundler automatically applies CLI flag values to those paths
- Values are written to the component’s
values.yamlin its per-component bundle directory
The --nodes flag (bundle-time only) sets the estimated GPU node count. Components that declare nodeCountPaths in the registry receive this value at those paths in their generated Helm values.
Example CLI usage:
Value Overrides
Override component values at bundle generation time:
The prefix before : matches the component’s valueOverrideKeys in the registry.
Deployer Integration
After bundlers generate deployment artifacts, deployers transform them into deployment-specific formats. The deployer framework is separate from bundlers but works with their output.
How Bundlers and Deployers Work Together
Deployment Order
Deployers respect the deploymentOrder field from the recipe to ensure components are deployed in the correct sequence:
Example Recipe with Deployment Order:
Bundler Output for Deployers
When the --deployer flag is set, bundlers generate standard artifacts that deployers then transform:
For Helm (--deployer helm, default):
- Generates per-component bundle directories with individual values.yaml
- Creates component-specific values with installation scripts
- Includes manifests and deployment instructions per component
For Argo CD (--deployer argocd):
- Bundler generates
values.yamlandmanifests/ - Deployer creates
<component>/argocd/application.yamlwith sync-wave annotations - Deployer creates
app-of-apps.yamlat bundle root - Applications use multi-source to reference values.yaml and manifests from GitOps repo
Using Deployers with Bundlers
The deployer is specified at bundle generation time:
See CLI Architecture for detailed deployer documentation.
See Also
- Architecture Overview - Complete bundler framework architecture
- CLI Architecture - Deployer framework and GitOps integration
- CLI Reference - Bundle generation commands
- API Reference - Programmatic access (recipe generation only)
- Component Registry - Declarative component configuration
- Recipe Data - Recipe and component data overview