Helm's render is not a pure function of (chart, values) - it depends on live cluster state at install time (lookup, .Capabilities, hooks, generated values, post-render mutation), so a naive helm template preview is untrustworthy for serious charts. The answer is not to re-render on every preview; it is to render once at import, capturing the live-dependence as data, then preview/diff the held data. The quirks become a one-time import problem. This surface names every preview-breaking quirk and says whether it is shift-left-resolved (captured into config pre-deploy) or runtime residue (declared + witnessed, never faked).
Of 26 tracked Helm quirk axes: 19 shift-left (config-resolvable, previewable), 3 runtime residue (witnessed after apply), 4 lifecycle/day-2 (not a preview question). The unpreviewable surface is small and named.
Quirk -> preview disposition
| Quirk axis | Disposition | Mechanism | Coverage tier | Previewable |
|---|---|---|---|---|
capability-profile | shift-left | capability profile pins kubeVersion + apiVersions | partly-tracked | partial |
crds | shift-left | CRDs in the render, or staged as a target fact | tracked-and-surfaced | yes |
dependency-alias | shift-left | locked dependency | tracked-by-lock-not-front-door | partial |
dependency-lock | shift-left | dependencies locked -> deterministic render | tracked-and-surfaced | yes |
explicit-extension-slot-control-points | shift-left | captured as control points / values | tracked-and-surfaced | yes |
files-get | shift-left | bundled files vendored with the chart | source-scanned-not-surfaced | partial |
generated-facts | shift-left | generated-fact receipt (capture once, reuse) | tracked-and-surfaced | yes |
global-values | shift-left | deterministic given captured values (scan gap) | not-scanned | partial |
helm-flag-profile | shift-left | pinned renderer flags in the render receipt | partly-tracked | partial |
helm-version-branching | shift-left | pin the Helm version in the render receipt | not-scanned | partial |
hook-phase | shift-left | hook-minted object declared as a target fact; hook execution witnessed | partly-tracked | partial |
import-values | shift-left | deterministic given captured values (scan gap) | not-scanned | partial |
install-vs-upgrade | shift-left | render-once is install-shaped; upgrade reuse via held data | tracked-and-surfaced | yes |
library-chart | shift-left | locked dependency | tracked-by-lock-not-front-door | partial |
lookup-target-facts | shift-left | target fact (read from config or live, pre-deploy) | tracked-and-surfaced | yes |
required-or-fail | shift-left | required values captured as an input contract | tracked-and-surfaced | yes |
semver-compare | shift-left | deterministic given pinned versions | source-scanned-not-surfaced | partial |
tpl-extension-slots | shift-left | deterministic given captured values | tracked-and-surfaced | yes |
values-schema | shift-left | values.schema.json input contract | tracked-and-surfaced | yes |
getHostByName | runtime-residue | DNS at render; bind a captured fact or witness | not-scanned | no (witness) |
post-renderer | runtime-residue | final applied object differs from helm template; pin a recipe stage or witness | not-scanned | no (witness) |
time-uuid-functions | runtime-residue | nondeterministic per render; capture-once or witness | source-scanned-not-surfaced | no (witness) |
crd-upgrade-behavior | lifecycle | CRD schema upgrade safety (day-2, not preview) | disclosed-not-complete | n/a (day-2) |
hook-delete-policy | lifecycle | uninstall/rerun cleanup behavior (day-2, not preview) | source-scanned-not-surfaced | n/a (day-2) |
hook-weight-ordering | lifecycle | hook sequencing (day-2, not preview) | source-scanned-not-surfaced | n/a (day-2) |
resource-policy-keep | lifecycle | uninstall/prune orphans (day-2, not preview) | not-scanned | n/a (day-2) |
Shift-left adoption across the catalog
- 199/199 variants pin a
capabilityProfile(capabilities shifted left) - 50 variants declare
targetFacts(lookup / hook-minted state shifted left) - 40
collector/target-facts.shscripts (read a fact from config or live, pre-deploy) - 2 generated-fact receipts (generated values captured, not regenerated)
Per-chart readiness (anchor charts)
| Chart | capabilities pinned | declared prerequisites | generated-fact captured | verdict |
|---|---|---|---|---|
ingress-nginx/ingress-nginx/4.15.1 | yes | yes (witnessed) | no | previewable + declared prerequisites (witnessed) |
jetstack/cert-manager/v1.20.2 | yes | yes (witnessed) | no | previewable + declared prerequisites (witnessed) |
bitnami/redis/25.5.3 | yes | none | yes | fully previewable pre-deploy |
prometheus-community/kube-prometheus-stack/85.3.3 | yes | yes (witnessed) | no | previewable + declared prerequisites (witnessed) |
Worked example: ingress-nginx
ingress-nginx is the hard case - it has all three classic preview-breakers.
- Naive
helm templateis wrong: the admission webhook certificate is minted by a hook Job at install (absent from a no-hooks render), the webhookcaBundleis hook-patched afterwards, and the emitted PDB/Ingress apiVersions depend on the target's.Capabilities. - Shift-left preview is right: the committed render is captured with a pinned
capabilityProfile(kubeVersion: 1.30.0), and the hook-minted admission cert is declared as atargetFact(requiredSecrets: ingress-nginx/ingress-nginx-admission, "normally created by Helm hook jobs"), resolvable from config or live via the collector (record/livemodes). The preview now states exactly what deploys and what must pre-exist. - Residue: the cert content and the live
caBundleare not pre-rendered - they are witnessed after apply (cub-scout), not faked.
Honest residue (the genuinely unpreviewable surface)
These cannot be made a pure function of captured config; the honest answer is declare + witness, never a fake preview:
getHostByName- DNS at render; bind a captured fact or witnesspost-renderer- final applied object differs from helm template; pin a recipe stage or witnesstime-uuid-functions- nondeterministic per render; capture-once or witness
Next (breadth + rigor upgrades)
- A full top-100 roll-up: join
data/chart-facts/chart-facts.csv(which quirks each chart has) with recipe artifacts to give a per-chart previewable verdict for the whole catalog. - A live
helm templatevs shift-left render diff for ingress-nginx, committed as evidence (turns the worked example from described to measured). - Promote the
not-scannedresidue axes (post-renderer, getHostByName, helm-version-branching) into scanner detection so a chart cannot hide an unpreviewable dependence.
Regenerate
npm run preview-readiness:generate
npm run preview-readiness:verify