A Helm chart that installs the NVIDIA AI stack on a Kubernetes cluster by running NVIDIA AI Cluster Runtime (AICR) in a Job. AICR's version-locked, dependency-ordered recipes become installable by anything that speaks Helm — helm install, Argo CD, Flux, or a k0rdent MultiClusterService.
Why this chart:
- GitOps-native delivery. AICR is a CLI; this chart is the declarative envelope that lets fleet managers and reconcilers deliver it.
- The validated matrix, not hand-picked versions. One install converges a cluster on NVIDIA's tested combination of driver, operators, scheduler, and monitoring — component versions and ordering come from the pinned AICR release.
- Recipes become named, versioned artifacts. An org data pack registers your own criteria value (
service: yourorg) and carries your environment profile — every cluster installs the same recipe by name, with zero per-cluster flags. - Supply chain, end to end. Checksum-pinned binary with an optional reviewed strict pin, captured CTRF verdicts, and evidence bundles published signed or unsigned-then-sign-later.
- Operable and tested. What resolved, what passed, and the proof all land in ConfigMaps; the chart itself ships with a mutation-tested render suite and a reproducible end-to-end test.
The chart is one Kubernetes Job plus its ServiceAccount and RBAC. On the target cluster it:
- downloads a pinned
aicrrelease binary and verifies its checksum, - pulls your org's
--dataextension pack when one is configured, - runs
aicr recipewith the criteria supplied through chart values, and captures the resolved recipe to a ConfigMap, - runs
aicr bundleto render the recipe into a Helm bundle, - verifies the rendered bundle (
aicr verify, closed-world checksums) and executes itsdeploy.shto install the stack, - optionally runs
aicr validate, captures the verdict to a ConfigMap, and publishes the evidence bundle to an OCI registry.
AICR is consumed unmodified — no wrapper logic, no duplicated behavior — so component versions and install ordering always come from the pinned upstream release.
v0.3.0 is experimental. It targets connected clusters. The install path is the proven core; data packs, validation, and evidence publishing are opt-in values, default off.
There is no default accelerator — state your criteria (see "Which criteria?" below):
helm install nvidia-aicr oci://ghcr.io/mirantis/charts/nvidia-aicr \
--version 0.3.0 \
--namespace nvidia-aicr --create-namespace \
--set accelerator=h100Upgrading from 0.2.0? Do these in order (details in "k0s clusters" and the "Upgrading a cluster converged on an earlier pin" bullet under Scope and limitations):
- Republish org packs with
apiVersion: aicr.run/v1beta1first. This is safe while still on chart 0.2.0, because aicr v0.20.0 reads v1beta1 (verified 2026-10-22; re-verify with.tmp/aicr-0.20.0 recipe --data hack/e2e/acme-aicr-pack --service acme --accelerator h100 --intent training -o /dev/null).- Replace
environment.k0sContainerdwithservice: k0s(Kubernetes >= 1.34).- Replace
service: self-managed/self/vanillawithservice: any.- Before re-running over a converged cluster, run
aicr upgrade-check --from cluster --to <recipe> --deployer helm. gpu-operator, nvsentinel and nvidia-dra-driver-gpu currently report blocked.A k0rdent MCS template bump (
nvidia-aicr-0-3-0) rolls an install Job to every matched cluster, so step 4 applies per cluster.
Watch it converge, then check what actually resolved:
kubectl logs -n nvidia-aicr -l app.kubernetes.io/name=nvidia-aicr --tail=-1 -f
kubectl get cm aicr-recipe -n nvidia-aicr -o jsonpath='{.data.summary\.txt}'This is the intended production path. AICR selects recipes by matching criteria against its catalog — there is no "recipe name" flag. An org pack closes that gap: registering a private service value in an overlay makes it a valid CLI input (upstream: "adding a new value to an overlay automatically makes it a valid CLI input"), so the value is the recipe's name. The same overlay carries your environment facts (containerd socket, driver posture, fleet constraints) and your calibrated validators, so a resolved bundle needs zero per-cluster flags.
A minimal pack, for a fictional org "acme":
acme-aicr-pack/
├── registry.yaml # required stub even with no component additions
├── overlays/
│ └── acme-h200-training.yaml # registers service=acme; bakes in env profile
└── validators/
└── catalog.yaml # org validators, pinned by image digest
Package the directory as an OCI artifact (oras push), then point the chart at it:
dataPack: "ghcr.io/acme/acme-aicr-pack:1.0.0"
dataPackSecret: "aicr-pack-pull" # for private registries; see below
service: acme
accelerator: h200
intent: trainingSee upstream Data Extension for pack authoring.
dataPackSecret names an existing Secret of type kubernetes.io/dockerconfigjson in the release namespace; the pull initContainer authenticates with it. Empty means anonymous (public artifacts only).
kubectl create secret docker-registry aicr-pack-pull -n nvidia-aicr \
--docker-server=ghcr.io --docker-username=<user> --docker-password=<token>A bad or missing credential fails the initContainer visibly — fail-closed, never a silent fallback to anonymous.
If your reconciler can inject secrets as values without putting them in git
— Flux HelmRelease valuesFrom: secretKeyRef, or k0rdent
MultiClusterService serviceSpec.services[].valuesFrom — dataPackAuth.dockerconfigjson
lets the chart create the Secret instead. The credential then also lives in Helm release
storage on the target cluster: that is the trade-off you accept.
The default remains: the chart never creates the Secret.
On a k0rdent fleet, kcm bundles Sveltos: keep one wrapper Secret on the management cluster
and a ClusterProfile whose selector matches the same labels your
MultiClusterService uses — Sveltos applies the wrapped dockerconfigjson
(and its namespace) to every matched child, and rotation is one kubectl apply on the mothership. Worked example with ordering caveats:
examples/fleet/fleet-secret-distribution.yaml
The same Secret serves all three credential consumers (pack pull, evidence
publish, validator pulls — see "Private validator images"). Create the
distribution before labeling clusters into the fleet, or order two
MultiClusterServices with dependsOn. If validate.publish.registrySecret
names a Secret that has not landed yet, the Job's pod cannot start (the
Secret is a required volume), so nothing runs until the Secret lands or
job.activeDeadlineSeconds expires; the Job is never green. If
publish is enabled with no registrySecret at all, the push is anonymous, a
private registry rejects it, and with validate.publish.failOnError: false
(the default) the Job still goes green with no published evidence. Enterprise secret
stores: External Secrets Operator on the children fills the same role;
the chart needs nothing special.
A pack's validator catalog may reference private images. Those are pulled by the kubelet in the namespace where aicr validate runs its validator Jobs (default aicr-validation) — dataPackSecret does not cover them. aicr exposes the knobs natively; pass them through validateArgs:
validateArgs:
- --namespace
- nvidia-aicr # the release namespace
- --image-pull-secret
- acme-registry-credWith --namespace set to the release namespace, the same dockerconfigjson Secret used for dataPackSecret can serve as the validator pull secret — one robot token, one Secret object, all three consumers (pack pull, evidence push, validator pull). That --namespace is the load-bearing part: without it the validator Jobs run in aicr-validation, where the Secret does not exist. The stock validators and the snapshot agent (ghcr.io/nvidia/aicr) are public and need none of this. Note that the one-Secret story is about dataPackSecret: dataPackAuth creates a pack-pull Secret only, and validate.publish.registrySecret / --image-pull-secret still need a Secret you supply.
Recipe selection is criteria assembly against AICR's catalog (service, accelerator, intent, os, platform). Two failure modes matter:
- Over-specification fails hard: naming a dimension the matching overlays don't declare is rejected outright (e.g.
intent: trainingwithservice: any: a stated intent needs a service that declares it, and at v1.2.0 those areaks,bcm,eks,k0s,kind,lke,ocp,rke2,ckswithos: ubuntu,gkewithos: cos, andokewithos: ol; verified 2026-11-05). The error message lists supported combinations — but you only see it in the Job log, minutes afterhelm installreturned. - Under-specification degrades silently: fewer stated dimensions resolve a thinner stack with exit 0 and no warning.
accelerator: a100alone resolves 12 components; a fully-specified EKS training leaf (--service eks --accelerator h100 --intent training --os ubuntu) resolves 15 (aicr v1.2.0, verified 2026-11-05).
Defend against both:
aicr recipe list --gpu h200 # before: which combinations exist for your GPU
kubectl get cm aicr-recipe -n <ns> -o jsonpath='{.data.summary\.txt}' # after: what resolvedThe chart refuses to render with no criteria at all: state an accelerator, or set dataPack (your pack owns criteria), or state a concrete service plus intent (accelerator-less leaves like bcm+inference exist; service: any does not count). Extra arguments (--nodes, --criteria-strict) pass through recipeArgs.
| Key | Default | Meaning |
|---|---|---|
accelerator |
"" (required)* |
Target accelerator: h100, h200, gb200, gb300, b200, b300, a100, l40, l40s, rtx-pro-6000, vr200. *Render fails unless this, dataPack, or concrete service+intent is set |
intent |
"" |
Recipe intent (training, inference). Empty omits the flag. A stated intent must be declared by the matching overlays, so it needs a service that provides it (not any) — see "Which criteria?" |
os |
"" |
Node OS (ubuntu, rhel, rocky, cos, amazonlinux, ol, sles, talos). Empty omits the flag |
platform |
"" |
Workload platform (kubeflow, dynamo, kueue, nim, runai, slurm). Empty omits the flag |
service |
any |
Service overlay: aks, bcm, cks, eks, generic, gke, k0s, kind, lke, metal3, mk8s, nvke, ocp, oke, rke2, a pack-registered value (e.g. acme), or any for self-managed clusters. generic and metal3 are accepted by the CLI but have no recipe for most accelerators (e.g. --service metal3 --accelerator h100 exits 2 with no recipe provides service; v1.2.0, verified 2026-11-05). Use any, not self-managed/self/vanilla: at v1.2.0 those names map to generic, which has no recipe for most accelerators (see "k0s clusters" and the CHANGELOG). Use k0s on k0s clusters |
dataPack |
"" |
OCI reference (no scheme) of your org's --data extension pack |
dataPackSecret |
"" |
Existing dockerconfigjson Secret for private pack registries |
dataPackAuth.dockerconfigjson |
"" |
Opt-in: raw dockerconfigjson content; the chart creates <release>-pack-auth and uses it for the pack pull. Mutually exclusive with dataPackSecret. For reconcilers with native secret-to-values injection (Flux valuesFrom, kcm MCS serviceSpec.services[].valuesFrom); see "Private pack registries" for the exposure trade-off |
dataPackPlainHTTP / dataPackInsecureTLS |
false |
Pack-pull transport for lab registries (plain HTTP / untrusted TLS) — production registries need neither |
orasImage |
ghcr.io/oras-project/oras:v1.2.0@sha256:0087224dd0decc354b5b0689068fbbc40cd5dc3dbf65fcb3868dfbd363dc790b |
initContainer image that pulls dataPack; pinned to the multi-arch index covering linux/amd64 and linux/arm64 |
recipeConfigMap |
aicr-recipe |
ConfigMap receiving the resolved recipe + summary; "" disables |
aicrVersion |
v1.2.0 |
Pinned aicr release tag |
aicrArch |
amd64 |
Binary architecture: amd64 or arm64 |
aicrSha256 |
{amd64: <v1.2.0 hash>, arm64: <v1.2.0 hash>} |
Strict pin keyed by architecture; the entry matching aicrArch is used. Update on every aicrVersion bump. See "Supply-chain verification" |
validate.enabled |
false |
Run aicr validate after the install; see "Validation" |
validate.phases |
[deployment] |
Validation phases to run |
validate.failOnError |
false |
When true, a failed validation fails the Job — always after the verdict is captured |
validate.resultConfigMap |
aicr-validate-result |
ConfigMap receiving the CTRF verdict |
validate.emitEvidence |
false |
Also capture the --emit-attestation evidence bundle |
validate.evidenceConfigMap |
aicr-evidence-bundle |
ConfigMap receiving the gzipped bundle (+ pointer.yaml once published) |
validate.publish.enabled |
false |
Push the evidence bundle to an OCI registry; requires emitEvidence. See "Publishing evidence" |
validate.publish.mode |
signed |
signed (in-cluster keyless signing) or unsigned (sign later, identity never enters the cluster) |
validate.publish.ref |
"" |
OCI push reference, e.g. ghcr.io/myorg/aicr-evidence. Required when enabled |
validate.publish.identityTokenSecret |
{name: "", key: token} |
signed mode: existing Secret holding a short-lived OIDC token, exposed as COSIGN_IDENTITY_TOKEN |
validate.publish.registrySecret |
"" |
Existing dockerconfigjson Secret for the evidence registry push |
validate.publish.failOnError |
false |
A publish failure fails the Job only when true |
validate.publish.insecureTLS / .plainHTTP |
false |
Self-signed / plain-HTTP registries (local testing) |
storageClass |
"" |
If non-empty, passed to aicr bundle --storage-class |
recipeArgs |
[] |
Recommended: arguments passed to aicr recipe; one list element is one complete argument, preserving spaces, apostrophes and glob characters. Quote every element, including numbers and booleans. Appended after recipeFlags |
recipeFlags |
"" |
Legacy extra flags for aicr recipe. Split on whitespace; no shell quoting is interpreted, and matching glob patterns expand. Use recipeArgs when an argument needs whitespace, quotes or a literal * |
bundleArgs |
[] |
Recommended: arguments passed to aicr bundle, after bundleFlags; one list element is one complete argument. Quote every element, including numbers and booleans. Applied after environment.*, so operator values win |
bundleFlags |
"" |
Legacy extra flags for aicr bundle — see "Tainted GPU nodes". Split on whitespace; no shell quoting is interpreted, and matching glob patterns expand |
validateArgs |
[] |
Recommended: arguments passed to aicr validate, after validateFlags; one list element is one complete argument. Quote every element, including numbers and booleans. See "Private validator images" |
validateFlags |
"" |
Legacy extra flags for aicr validate (--namespace, --image-pull-secret, --node-selector). Split on whitespace; no shell quoting is interpreted, and matching glob patterns expand |
job.extraEnv |
[] |
Extra env for the installer container, verbatim (e.g. AICR_NCCL_FABRIC) |
environment.preinstalledDriver |
false |
Nodes already carry an NVIDIA driver — prefer encoding this in your org pack. service: k0s does not imply it; see "k0s clusters" |
deploy.enabled |
true |
false = assert-only run: skip bundle+deploy entirely and validate the cluster against the resolved recipe. Requires validate.enabled; see "Validation postures" |
deployArgs |
[] |
Recommended: arguments passed to the bundle's deploy.sh, after deployFlags; one list element is one complete argument, preserving spaces, apostrophes and glob characters. Quote every element, including numbers and booleans. The non-empty legacy default still applies; set deployFlags: "" for list-only arguments |
deployFlags |
--best-effort --no-wait |
Legacy flags for deploy.sh (accepts --best-effort, --no-wait, --retries N). Split on whitespace; no shell quoting is interpreted, and matching glob patterns expand. Use deployArgs for structured arguments |
runtimeImage |
alpine/k8s:1.34.0@sha256:b5f6edfeac5279f3e182d938d1ffecb62f7c980756ac4b6b66d7f0d566782f77 |
Job image; pinned to the multi-arch index covering linux/amd64 and linux/arm64. Must include bash, curl, kubectl, and helm |
job.* |
see values.yaml |
Job backoff, TTL, deadline, and resource settings |
job.tolerations |
[] |
Tolerations for the installer pod itself — required on clusters whose only workers are tainted GPU nodes; see "Tainted GPU nodes" |
job.nodeSelector / job.affinity / job.priorityClassName |
empty | Remaining scheduling controls for the installer pod |
Structured argument rendering, literal space/apostrophe/glob handling, and
placement after each legacy string were exercised by hack/verify-render.sh
with Helm v4.3.0 and bash 5.3.20(1)-release, verified 2026-11-05. The image
index digests were resolved with Docker 29.9.2 and curl 8.7.1 on the same date;
CONTRIBUTING's release checklist refreshes them.
With validate.publish.enabled: true, the Job pushes the emitted evidence bundle to publish.ref via aicr evidence publish after the verdict is captured. The pointer.yaml locator lands on the evidence ConfigMap either way. Two trust modes:
mode: signed — keyless Sigstore signing in-cluster. The identity token from identityTokenSecret is exposed as COSIGN_IDENTITY_TOKEN. Understand what this means: the token lives inside a pod whose ServiceAccount is bound to cluster-admin, and anyone who can read pods in that namespace during the run can sign as that identity until it expires. Use only short-lived, per-run tokens — and note the token must still be valid when the publish step runs, which is after deploy.sh and validation, potentially tens of minutes after install. Public Sigstore does not trust self-managed cluster issuers, so the token is BYO (mint → write Secret → install):
kubectl create secret generic cosign-token -n nvidia-aicr --from-literal=token="$OIDC_TOKEN"mode: unsigned — the bundle is pushed with an empty signer block; identity never enters the cluster. Sign later from a trusted host with Sigstore egress:
aicr evidence sign <pointer.yaml> --relocateThis is upstream's own fork-based CI flow, and the recommended posture for regulated environments and scheduled runs.
The registry must support the OCI 1.1 Referrers API for signature attachment (GHCR, GitLab, Harbor ≥ 2.8, ECR, Google Artifact Registry, ACR, Artifactory); upstream falls back to a tag schema otherwise. The push itself authenticates via registrySecret (dockerconfigjson), scoped to the publish invocation only.
The chart is a normal OCI Helm chart: any reconciler that speaks Helm delivers it. Two worked examples; the same values block works everywhere, including plain helm install -f values.yaml.
One caveat applies to every reconciler: do not reconcile new values while an install is still running — see "Scope and limitations".
apiVersion: source.toolkit.fluxcd.io/v1
kind: HelmRepository
metadata: {name: nvidia-aicr, namespace: flux-system}
spec:
type: oci
url: oci://ghcr.io/mirantis/charts
interval: 1h
---
apiVersion: helm.toolkit.fluxcd.io/v2
kind: HelmRelease
metadata: {name: nvidia-aicr, namespace: nvidia-aicr}
spec:
interval: 1h
chart:
spec:
chart: nvidia-aicr
version: 0.3.0
sourceRef: {kind: HelmRepository, name: nvidia-aicr, namespace: flux-system}
values:
service: acme
accelerator: h200
intent: training
dataPack: "ghcr.io/acme/acme-aicr-pack:1.0.0"
dataPackSecret: "aicr-pack-pull"Argo CD users: the same chart/values as an Application with an OCI repo source.
The chart is published to the k0rdent catalog as a ServiceTemplate. Label the GPU ClusterDeployments you want converged; one MultiClusterService installs — and validates — the NVIDIA runtime on all of them:
apiVersion: k0rdent.mirantis.com/v1beta1
kind: MultiClusterService
metadata:
name: nvidia-runtime-training
namespace: kcm-system
spec:
clusterSelector:
matchLabels:
gpu-fleet: training # your label on GPU ClusterDeployments
serviceSpec:
services:
- template: nvidia-aicr-0-3-0
name: nvidia-aicr
namespace: nvidia-aicr
values: |
dataPack: "ghcr.io/acme/acme-aicr-pack:1.0.0"
dataPackSecret: "aicr-pack-pull"
service: acme
accelerator: h200
intent: training
bundleArgs:
- --accelerated-node-toleration
- nvidia.com/gpu=present:NoSchedule
deployFlags: "" # replace the legacy default with list-only args
deployArgs: [--retries, "1"] # strict: wait and gate on the verdict
validate:
enabled: true
phases: [deployment]
failOnError: true
emitEvidence: true
publish: {enabled: true, mode: unsigned, ref: ghcr.io/acme/aicr-evidence, registrySecret: aicr-pack-pull}dataPackSecret must exist on each child before the Job runs — see "Landing the Secret on fleet children".
Production clusters commonly taint GPU nodes (e.g. nvidia.com/gpu=present:NoSchedule). AICR injects tolerations into the rendered charts when asked; pass the arguments through bundleArgs:
bundleArgs:
- --accelerated-node-toleration
- nvidia.com/gpu=present:NoScheduleWithout this, GPU-targeted components stay Pending on tainted nodes. Related scheduling flags (--system-node-selector, --system-node-toleration, --workload-gate, --set component:path=value) pass through the same way.
bundleArgs covers the components AICR deploys — not the installer Job itself. On a cluster whose only worker pool is tainted, the Job's own pod will never schedule, and it burns the full activeDeadlineSeconds (3 h by default) before failing with DeadlineExceeded. Tolerate the taint for the installer too:
job:
tolerations:
- key: nvidia.com/gpu
operator: Equal
value: present
effect: NoScheduleNote that aicr's --set is scalar-only; list or object values need --set-json (or --set-file). That matters for the k0s example below, whose toolkit.env is a list.
On k0s, set service: k0s. Upstream AICR accepts it as a criteria value from v0.22.0 (aicr v0.20.0 rejects it with invalid service type: k0s; verified 2026-10-22). On v1.2.0 (verified 2026-11-05, from the overlays at tag v1.2.0, unchanged from v1.1.2, and aicr recipe), upstream's k0s overlay sets two things and nothing else:
- the GPU operator toolkit's containerd environment (
CONTAINERD_CONFIG=/etc/k0s/containerd.d/nvidia.toml,CONTAINERD_SOCKET=/run/k0s/containerd.sock,CONTAINERD_RUNTIME_CLASS=nvidia). Without it the toolkit configures the default containerd, reports success, and GPU pods then fail withno runtime for "nvidia" is configured; - the constraint
K8s.server.version >= 1.34.
The k0s-training overlay adds only the training intent. service: k0s does not imply a preinstalled driver. The driver facts (GPU operator driver.enabled: false and migManager.enabled: false, DRA driver nvidiaDriverRoot: /, NVSentinel labeler.assumeDriverInstalled: true and the janitor's hostDriverRootPath: /) and the OS.release.ID=ubuntu, VERSION_ID=24.04 constraints live only in upstream's h200-k0s-ubuntu-training leaf, which resolves for accelerator: h200, intent: training, os: ubuntu:
service: k0s
accelerator: h200
intent: training
os: ubuntu # selects the validated h200-k0s-ubuntu-training leafWithout os, --service k0s --accelerator h200 resolves the generic k0s stack (12 components, no driver facts), and --service k0s --accelerator h200 --intent training fails with specify os (valid: ubuntu) unless an org pack supplies the leaf. On k0s with a host driver and no such leaf, you still need environment.preinstalledDriver: true (or the equivalent in your pack):
service: k0s
accelerator: h100
environment:
preinstalledDriver: true # nodes already carry an NVIDIA driverenvironment.k0sContainerd was removed in chart 0.3.0 (breaking). Any value other than false (or absent/null) fails the render, including the quoted string "false" (verified 2026-10-22 with --set-string), and the message says what to do instead (it contains environment.k0sContainerd was removed in chart 0.3.0). service: k0s is the exact replacement. Two floors apply: service: k0s needs aicr >= v0.22.0, so overriding aicrVersion below that fails in aicr with invalid service type: k0s; and the overlay requires Kubernetes >= 1.34.
preinstalledDriver stays for every service with a host driver, k0s or not. It turns the GPU operator's driver install off and points the DRA driver at the host root; aicr's driver-ownership coherence check requires both, and aicr bundle also fails closed unless the NVSentinel labeler is told the driver is host-provided, so the toggle sets all three together. It is applied before bundleFlags and bundleArgs, so you can still override any of it. Its structured long form (preferred):
bundleArgs:
- --set
- gpuoperator:driver.enabled=false
- --set
- dradriver:nvidiaDriverRoot=/
- --set
- nv-sentinel:labeler.assumeDriverInstalled=trueThe legacy string equivalent is whitespace-split and does not interpret shell quoting:
bundleFlags: '--set gpuoperator:driver.enabled=false --set dradriver:nvidiaDriverRoot=/ --set nv-sentinel:labeler.assumeDriverInstalled=true'Org pack with its own service value. If your pack registers a private service (so service: k0s is not an option), the k0s containerd wiring is yours to carry. The cleaner home is the pack's overlay (the toolkit.env override under the gpu-operator component). The escape hatch without a pack change is bundleArgs; the full structured long form, including the list-valued toolkit.env (aicr's --set is scalar-only, hence --set-json):
bundleArgs:
- --set
- gpuoperator:driver.enabled=false
- --set
- dradriver:nvidiaDriverRoot=/
- --set
- nv-sentinel:labeler.assumeDriverInstalled=true
- --set-json
- 'gpuoperator:toolkit.env=[{"name": "CONTAINERD_CONFIG", "value": "/etc/k0s/containerd.d/nvidia.toml"}, {"name": "CONTAINERD_SOCKET", "value": "/run/k0s/containerd.sock"}, {"name": "CONTAINERD_RUNTIME_CLASS", "value": "nvidia"}]'Unlike the legacy form, the JSON argument can contain spaces and the inner double quotes are passed through unchanged (the chart's array rendering was checked with Helm v4.3.0 and this exact JSON bundled successfully with aicr v1.2.0, verified 2026-11-05). The legacy string form requires whitespace-free JSON because it is split on whitespace and performs no quote removal:
bundleFlags: '--set gpuoperator:driver.enabled=false --set dradriver:nvidiaDriverRoot=/ --set nv-sentinel:labeler.assumeDriverInstalled=true --set-json gpuoperator:toolkit.env=[{"name":"CONTAINERD_CONFIG","value":"/etc/k0s/containerd.d/nvidia.toml"},{"name":"CONTAINERD_SOCKET","value":"/run/k0s/containerd.sock"},{"name":"CONTAINERD_RUNTIME_CLASS","value":"nvidia"}]'packs/k0s-h200-training is an illustrative worked example of an org pack: it layers on upstream's service: k0s overlays and carries one example fleet's facts as overrides. Copy it and change the facts to match your own fleet; it is not a product service.
Self-managed clusters: use service: any. Earlier docs recommended the names self-managed, self and vanilla. At v1.2.0 those aliases resolve to generic, not any, and generic has no recipe for most accelerators (aicr recipe --service self-managed --accelerator h100 exits 2 with no recipe provides service 'generic'; verified 2026-11-05). The chart does not guard against this at render time, so the failure shows up in the Job log.
The chart's k0s path was verified end to end on 8× H200 SXM bare metal (k0s v1.36.2, kcm 1.9.0) with aicr v0.20.0 in 2026-08: all components installed under strict flags and aicr's deployment phase passed 4/4. That run used the removed environment.k0sContainerd toggle; the service: k0s path has been checked offline only (recipe resolution against the v1.1.2 binary on 2026-10-22 and the v1.2.0 binary on 2026-11-05), not on hardware.
With validate.enabled: true, the Job runs aicr validate against the freshly installed stack and publishes the CTRF verdict to validate.resultConfigMap — whether validation passes or fails (aicr's own per-phase CTRF ConfigMaps are removed at its run cleanup, so the chart owns durable capture; only after capture does failOnError apply). Read the verdict with:
kubectl get cm aicr-validate-result -n <ns> -o jsonpath='{.data.ctrf\.json}' | jq .results.summaryWith validate.emitEvidence: true, the in-toto attestation bundle is also captured (gzipped, size-guarded):
kubectl get cm aicr-evidence-bundle -n <ns> -o jsonpath='{.binaryData.evidence\.tgz}' \
| base64 -d > evidence.tgz && mkdir -p evidence && tar xzf evidence.tgz -C evidence
aicr evidence verify ./evidenceOperational notes: when validation is enabled, drop --no-wait from deployFlags (so deploy.sh waits for convergence before the verdict) and prefer --retries 1 — the default retry/backoff schedule can stretch a single slow component toward an hour, and the Job's activeDeadlineSeconds (default 10800s) is the overall cap. Keep validation off on GPU-less clusters (kind included) — the deployment phase legitimately fails while GPU pods sit Pending.
Two different things get verified with the same machinery. Rollout verification (the deployment phase, validators shipped in aicr) asks "is the cluster running what the recipe says?". Capability qualification (the performance/conformance phases, validators mostly from your org pack) asks "does this hardware actually deliver?" — burn-in, NCCL floors, storage thresholds. Qualification consumes what rollout installed (DCGM, device plugin, drivers); it never provides it.
aicr validate installs or upgrades no recipe component. It does create short-lived objects of its own: each validator runs as a Kubernetes Job and, without --snapshot, a snapshot agent captures the cluster first (both in --namespace, with per-run RBAC that is removed at the end unless --no-cleanup; aicr v1.2.0 validate --help, 2026-11-05). Performance validators may also start their own benchmark workloads. The chart's deploy.enabled toggle and validate.phases therefore span four postures (worked values in examples/):
| Posture | deploy.enabled |
phases |
When |
|---|---|---|---|
| Install & prove | true |
[deployment] |
day-1, fleet converge — examples/day1-install.yaml |
| Qualify hardware | false |
[performance] + org pack |
commissioning burn-in, benchmarks — examples/qualify-hardware.yaml |
| Re-certify / drift check | false |
[deployment] |
scheduled, pre-upgrade — examples/drift-check.yaml |
| Full acceptance | true |
[deployment, performance] |
new fleet, one shot |
With deploy.enabled: false the Job still pulls the pack and resolves + captures the recipe (the contract being asserted), then skips bundle+deploy entirely. Rendering fails if combined with validate.enabled: false — a Job that neither deploys nor validates would be a no-op.
What the verdicts mean when something is missing (upstream semantics): validation fails closed. A cluster that flunks the readiness pre-flight exits 2 and deploys no validators; a recipe component that is absent or unhealthy fails the deployment phase (exit 8) — on an assert-only run, that is the drift check working; an optional prerequisite (GPU nodes, an operator CRD a check needs) triggers an explicit skip guard — CTRF skipped, named reason, exit 0, not a failure. Inconclusive checks (other) fail closed like failures. validate.failOnError decides whether a non-zero verdict fails the Job.
The Job downloads the pinned release tarball and verifies its sha256 against the release's aicr_checksums.txt before executing anything (fail-closed). For stricter provenance, set aicrSha256.<arch> to the expected tarball hash: that anchors trust in reviewed chart values rather than in release assets fetched at runtime. An empty entry skips only this extra pin — the checksums-file verification always runs.
Before executing deploy.sh, the Job runs aicr verify on the rendered
bundle — upstream's closed-world gate: every file is checked against the
bundle's checksums.txt, and any addition, removal, or modification fails
the Job. In-Job bundles are unattested, so this verifies integrity, not
provenance (and external --data packs cap upstream's trust level at
attested regardless).
The Job pod runs as nonroot (UID 65532, matching upstream's own image user) with a read-only root filesystem; all work happens in an emptyDir mounted at /tmp.
The Job's ServiceAccount is bound to cluster-admin. AICR bundles install cluster-scoped resources (CRDs for DRA, NVSentinel, and the Prometheus operator, namespaces, and RBAC in those namespaces), so a namespaced role is not sufficient. The binding is scoped to the Job's ServiceAccount and is removed with the release.
-
Kubernetes 1.32 is the chart-wide minimum (
kubeVersion: ">= 1.32.0-0", enforced by Helm at template/lint time; Helm v4.3.0, verified 2026-11-05). The pinned default criteria resolveK8s.server.version >= 1.32(.tmp/aicr-1.2.0 recipe --service any --accelerator h100; aicr v1.2.0, verified 2026-11-05). Recipe-specific floors can be stricter:service: k0srequires Kubernetes >= 1.34, as its resolved constraint states. Other recipes may impose their own constraints, so check the recipe rather than treating 1.32 as sufficient for every target. -
Evidence bundles published in
signedmode place the identity token in the install pod (see "Publishing evidence");unsignedmode exists precisely so identity never has to enter the cluster. -
On clusters without GPUs (kind included), GPU-dependent pods stay Pending and
--best-effortkeeps the install going. That is what the k0rdent catalog's kind e2e exercises: the delivery mechanics, not GPU functionality. -
Component-failure detection is belt-and-braces. aicr v1.2.0's
deploy.sh(same logic as v1.1.2's; read from a generated bundle, 2026-11-05) exits 1 on a component failure when--best-effortis absent; with--best-effortit continues past failed components, printsDeployment completed with failures (--best-effort):and exits 0; and it exits 2 when every component installed but the DRA kubelet-plugin restart was deferred because the GPU operator's driver migration had not settled (if a component also failed, it exits 1). The Job fails on any non-zero deploy exit and independently detects the failure report: without--best-effort, component failures fail the Job; with it, they are logged and the Job succeeds. Exit 2 is the exception under--best-effort: the Job logsWARN: deploy.sh deferred the DRA kubelet-plugin restart (exit 2)and continues to validation, only when deploy.sh's own lineDRA kubelet plugin restart blocked, retry needed for:is in the log and no failure report is (any other exit 2 stays fatal). To finish, let the operator converge and re-run the install (helm upgradecreates a fresh Job, which re-runsdeploy.sh). Without--best-effort, exit 2 fails the Job. Partial installs cannot masquerade as silent success either way. -
Upgrading a cluster converged on an earlier pin is not a plain re-run. A fresh install is unaffected. Re-running chart 0.3.0 over a cluster that chart 0.2.0 (aicr v0.20.0) converged crosses component boundaries that upstream's
aicr upgrade-checkcurrently reports as blocked. Indicative result forservice: any+h100, v0.20.0 to v1.2.0 (run 2026-11-05 with the v1.2.0 binary; v1.2.0'supgrade-checkrefuses a v0.20.0-generated recipe file,aicr.run/v1alpha2 was retired in v1.0.0, so this used that recipe with its header rewritten toaicr.run/v1, which is not an upstream-supported input): gpu-operator v26.3.3 to v26.7.2 blocked ("stops at =26.7.1"; "no record describes an upgrade starting from v26.3.3. The earliest recorded starting point is 26.7.0, and no record assesses the move from v26.3.3 to it"); nvsentinel v1.20.0 to v1.28.0 blocked ("stops at 1.25.0"; "The earliest recorded starting point is 1.22.0"); nvidia-dra-driver-gpu 0.4.1 to 0.5.1 blocked ("stops at 0.5.1"; "The earliest recorded starting point is 0.5.0"); for each of the three upstream says "Read the component's upstream release notes for that move before applying it, then re-run this check". nodewright-operator v0.17.1 to v0.20.0 manual, with a namespace relocationskyhook -> nodewrightthat no record assesses; nfd 0.19.0 to 0.20.1 manual (3 steps, of which the two RBAC steps, widen-nrt-rbac and narrow-nrt-rbac, apply "only when your nfd override sets topologyUpdater.rbac.create=false"; upgrade-nfd always applies; "chart-managed RBAC (AICR's default) needs nothing"); kai-scheduler v0.14.1 to v0.16.9 unknown ("breaking boundary"). A cluster converged on an unreleased v1.1.2 build of this chart sees only nfd 0.19.0 to 0.20.1 manual (with chart-managed RBAC, only the upgrade itself, which re-running the chart performs) and nvidia-dra-driver-gpu 0.5.0 to 0.5.1 safe (upgrade-checkfrom a v1.1.2 recipe to the v1.2.0 one, 2026-11-05). The bundle'sUPGRADING.mdis not enough on its own: it covers only the boundary the bundle's pinned version lands in, not earlier ones you cross. Required first step: from a workstation with cluster access, runaicr upgrade-check --from cluster --to <recipe> --deployer helm(--from clusteris the only supported source for a cluster converged on the old pin) and treat its verdict as authoritative; for a blocked component, first bring it to the earliest recorded starting point the check names, following that component's upstream release notes, then re-run the check, and re-run the chart only once nothing is blocked. The chart does not runupgrade-checkfor you. -
Air-gapped clusters are not supported by this chart version. The Job downloads the
aicrbinary from GitHub releases and the bundle pulls charts from upstream registries. AICR itself supports air-gapped delivery (aicr bundle --vendor-charts, the air-gap mirror workflow, a CycloneDX SBOM per deployable image); exposing it here needs a mirrored binary source and registry overrides. Planned for a future chart version. -
Re-running the Job re-applies the bundle (
helm upgradesemantics downstream); strict idempotency guarantees are not part of this version. Observed on re-runs: transient webhook/cert-rotation failures recovered by deploy.sh's retries, and orphaned-CRD warnings. -
Do not upgrade while an install is still running. Each revision renders a new Job (
<release>-install-<revision>), and Helm deletes the previous revision's Job because it is absent from the new manifest — which SIGKILLsdeploy.shmid-run and can leave downstream releases stuck inpending-install/pending-upgrade.ttlSecondsAfterFinishedonly reaps Jobs that have finished. Let an install converge before changing values. -
The bundle installs the Nodewright/Skyhook operator, but no
Skyhookcustom resources — nothing reboots on install alone. If you later define tuning policies, node reboots happen asynchronously; on a self-managed management cluster that restarts the management plane too. -
No drift detection between runs and no CRD status surface: the Job converges the cluster once per release revision, and its status is the Job exit code plus the ConfigMaps it writes (
aicr-recipe, the CTRF verdict, the evidence bundle). For continuous reconciliation, run the chart under a GitOps controller.
hack/verify-render.sh— render assertions for every conditional path (mutation-tested; run by the lint and release workflows).hack/e2e/run-kind-e2e.sh— customer-shaped end-to-end on a throwaway kind cluster: private org data pack from an htpasswd registry, pack-named recipe resolution (hack/e2e/acme-aicr-pack/), full install, validation, unsigned evidence publish, and a fail-closed negative test. Local infrastructure only; ~20–25 min.- The signed publish path has no automated test: public Sigstore does not trust local cluster issuers, so it needs a Fulcio-trusted OIDC identity (e.g. a GitHub Actions runner). The chart's signed-mode template plumbing is covered by the render assertions.
This chart is not affiliated with NVIDIA. AICR does not currently publish a Helm chart for the install step; if upstream adopts one, this chart is intended to be superseded by it.
Apache 2.0 — see LICENSE.