You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
tls: null in an override panics the render in every chart using the tri-state TLS helper #3705
helm template packages/apps/qdrant --set tls=null fails with nil pointer evaluating interface {}.enabled in the TLS helper. The helper reads .Values.tls.enabled to implement the tri-state where an unset value follows external, and it guards the value rather than the map, so a tls key present and explicitly null dereferences nil.
Four charts carry that helper and all four fail the same way:
values.yaml declares tls: {} in each, so a default install is safe. The failure needs an override that sets tls to null, which is what a values file written as tls: with nothing under it produces.
Same shape as the index on a nil .Values._cluster problem, and worth fixing the same way: guard the map, not the value. nats shows the two interact. It dies on index .Values._cluster "cluster-domain" with index of untyped nil before it ever reaches the TLS helper, so its TLS behaviour is only observable once _cluster is supplied, and anyone checking the four charts one by one will read that first failure as a different bug.
Note for whoever confirms this: the helper does not live in the same file in all four. Three keep it in _tls.tpl and qdrant in _helpers.tpl, so a check that opens one filename finds one chart and concludes the problem is local to it.
Good catch consolidating these. Looking at the current state of the four templates, the picture is a bit more nuanced than all four being equally broken:
All four helpers use the same kindIs "invalid" .Values.tls.enabled guard[1][2][3][4], which correctly distinguishes an unset .Values.tls.enabled from an explicit false — but none of them guard .Values.tls itself before drilling into it. When tls: null is passed, Helm coalescing wipes the tls: {} default from values.yaml, leaving .Values.tls as nil, and accessing .enabled on nil panics.
The fix pattern you described (guard the map, not the value) is the right approach — something like:
{{- $tls := .Values.tls | default dict -}}
{{- if kindIs "invalid" $tls.enabled -}}
...
{{- end -}}
There's also a prior issue #3611 that reported this same bug specifically for NATS and mentioned Qdrant as also affected.
Regarding the NATS _cluster interaction you noted: NATS uses index .Values._cluster "cluster-domain" without a nil guard [5], so it'll hit index of untyped nil before even reaching the TLS helper when _cluster isn't supplied. The existing safe pattern for _cluster is index (.Values._cluster | default dict) "cluster-domain"[6], which some operator charts already use.
The mechanism in the description is off by one step. tls is not present-and-null at render time, it is absent: Helm's null semantics delete the key from the chart defaults, and they only do that when a default is declared, which values.yaml does with tls: {}. So .Values.tls is nil and the nested read panics.
Measured alongside it: the schema is active and does reject a wrong type. --set tls=5 gives values don't meet the specifications of the schema(s), while --set tls=null passes validation and reaches the render. If the key were present carrying null, type: object would have rejected it exactly as it rejected 5. That is the evidence for "absent" rather than "present and null".
The fix does not change: guard the map, not the value.
Still unverified, and worth stating rather than assuming: whether an explicit null in a resource submitted to the aggregated apiserver survives to the chart values at all, or is defaulted back to {} by the structural schema first. The Helm-level barrier demonstrably does not exist; the apiserver-level one is untested.
Adding the write path side of this, since it produces the same nulls.
cozystack-api applies schema defaults only when it converts HelmRelease to Application, that is on read (rest.go:1229). On write the spec goes into HelmRelease .spec.values as is (rest.go:1619), no defaulting and no schema validation, so tls: typed by a user reaches helm untouched and gets deleted together with the chart default, exactly as you describe.
Where the field is required by schema the same input fails earlier: validation rejects the whole release with missing property, naming a property that the spec does contain. #3956 repairs that half on the write path, required fields only. The optional half stays here, and guarding the map in the helper also covers talos: in the kubernetes chart, which dies the same way on .Values.talos.version.
Activity
dosubot commentedon Aug 9, 2026
Good catch consolidating these. Looking at the current state of the four templates, the picture is a bit more nuanced than all four being equally broken:
All four helpers use the same
kindIs "invalid" .Values.tls.enabledguard [1] [2] [3] [4], which correctly distinguishes an unset.Values.tls.enabledfrom an explicitfalse— but none of them guard.Values.tlsitself before drilling into it. Whentls: nullis passed, Helm coalescing wipes thetls: {}default fromvalues.yaml, leaving.Values.tlsas nil, and accessing.enabledon nil panics.The fix pattern you described (guard the map, not the value) is the right approach — something like:
There's also a prior issue #3611 that reported this same bug specifically for NATS and mentioned Qdrant as also affected.
Regarding the NATS
_clusterinteraction you noted: NATS usesindex .Values._cluster "cluster-domain"without a nil guard [5], so it'll hitindex of untyped nilbefore even reaching the TLS helper when_clusterisn't supplied. The existing safe pattern for_clusterisindex (.Values._cluster | default dict) "cluster-domain"[6], which some operator charts already use.To reply, just mention @dosu.
Docs are dead. Just use Dosu.
lexfrei commentedon Aug 9, 2026
The mechanism in the description is off by one step.
tlsis not present-and-null at render time, it is absent: Helm's null semantics delete the key from the chart defaults, and they only do that when a default is declared, whichvalues.yamldoes withtls: {}. So.Values.tlsis nil and the nested read panics.Measured alongside it: the schema is active and does reject a wrong type.
--set tls=5givesvalues don't meet the specifications of the schema(s), while--set tls=nullpasses validation and reaches the render. If the key were present carrying null,type: objectwould have rejected it exactly as it rejected5. That is the evidence for "absent" rather than "present and null".The fix does not change: guard the map, not the value.
Still unverified, and worth stating rather than assuming: whether an explicit null in a resource submitted to the aggregated apiserver survives to the chart values at all, or is defaulted back to
{}by the structural schema first. The Helm-level barrier demonstrably does not exist; the apiserver-level one is untested.myasnikovdaniil commentedon Aug 25, 2026
Adding the write path side of this, since it produces the same nulls.
cozystack-api applies schema defaults only when it converts HelmRelease to Application, that is on read (
rest.go:1229). On write the spec goes into HelmRelease.spec.valuesas is (rest.go:1619), no defaulting and no schema validation, sotls:typed by a user reaches helm untouched and gets deleted together with the chart default, exactly as you describe.Where the field is required by schema the same input fails earlier: validation rejects the whole release with
missing property, naming a property that the spec does contain. #3956 repairs that half on the write path, required fields only. The optional half stays here, and guarding the map in the helper also coverstalos:in the kubernetes chart, which dies the same way on.Values.talos.version.fix(api): repair an empty required field instead of failing the relea…