helm: nil pointer evaluating interface when upper level doesn't exist prevents usage of default function

Hello, I’ll take an example template but the issue is more global than that I think.

I define a simple Service template:

apiVersion: v1
kind: Service # https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.18/#service-v1-core
metadata:
  name: MyService
spec:
  type: {{ .Values.service.type | default "ClusterIP" }}
  ports:
    - port: 8080
      targetPort: 8080
      protocol: TCP
      name: alt-http

In my values.yaml, when I define

service:
  type: somevalue

Everything goes well. It does also when I define the following:

service: {}

But if I don’t define service at all, I get an error:

╰ helm template . --debug
install.go:159: [debug] Original chart version: ""
install.go:176: [debug] CHART PATH: /home/mtodorovic/git/helm-issue/helm-chart


Error: template: helm-chart/templates/service.yaml:6:18: executing "helm-chart/templates/service.yaml" at <.Values.service.type>: nil pointer evaluating interface {}.type
helm.go:84: [debug] template: helm-chart/templates/service.yaml:6:18: executing "helm-chart/templates/service.yaml" at <.Values.service.type>: nil pointer evaluating interface {}.type

As soon as the parent value doesn’t exist, templating fails and totally ignores the default function. I expected that if anything goes wrong in .Values.service.type evaluation, we’ll go to default "ClusterIP" but it doesn’t.

We workaround the issue with the following but it wouldn’t work as easily for big dicts (like dict.bigkey.subkey.key)

{{- $service := .Values.service | default dict -}} # workaround for https://github.com/helm/helm/issues/8026
apiVersion: v1
kind: Service # https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.18/#service-v1-core
metadata:
  name: MyService
spec:
  type: {{ $service.type | default "ClusterIP" }}
  ports:
    - port: 8080
      targetPort: 8080
      protocol: TCP
      name: alt-http

I didn’t deep dive into the code yet, is it something that looks like fixable?

Thanks and keep the nice work with Helm 🎉

Output of helm version: version.BuildInfo{Version:“v3.2.0”, GitCommit:“e11b7ce3b12db2941e90399e874513fbd24bcb71”, GitTreeState:“clean”, GoVersion:“go1.13.10”}

About this issue

  • Original URL
  • State: closed
  • Created 4 years ago
  • Reactions: 83
  • Comments: 36 (5 by maintainers)

Commits related to this issue

Most upvoted comments

this has worked for me:

{{ (((.Values.abc).def).ghi).jkl }}

The inability to go beyond 1 level deep in the usage of default is a big limitation to what you can do and kind of limits the intention of the “default” function.

From the documentation of default

This function allows you to specify a default value inside of the template, in case the value is omitted

There is no mention of depth here.

Fixing this would be a great benefit to chart devs and users.

Unfortunately, this is not something that can be fixed.

This occurs because the .Values tree is populated based on the input provided through values.yaml, --values, --set, etc. It is not populated based on how the templates consume the input. If a value is not set, .Values has no knowledge of this sub-tree, and therefore it is evaluated as a nil object when rendered through text/template.

There are two ways to mitigate this particular issue:

  • use a flat tree, where each leaf node is at the first level (i.e. .Values.serviceType rather than .Values.service.type).
  • populate the tree with empty values, so that the .Values object can be properly populated (as @michael-todorovic mentions in their workaround with service: {}). This will inform the rendering engine to create an empty leaf node.

Wouldn’t this be beautifully solved if the parse could handle optional chaining similar to Javascript in latest standard ECMAScript 2020? See https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/Optional_chaining

It would basically look like this:

spec:
  type: {{ .Values.service?.type | default "ClusterIP" }}

This could be used in other cases, such as:

spec:
  {{- if .Values.global?.internalIPFamily }}
  ipFamily: {{ .Values.global.internalIPFamily }}
  {{- end }}

Workaround which I’m using (using the example from the previous comment):

spec:
  {{- $global := .Values.global | default dict }}
  {{- if $global.internalIPFamily }}
  ipFamily: {{ $global.internalIPFamily }}
  {{- end }}  

I experience this error, even when the value is defined, when trying to expand it in a named template.

Safe nesting can be achieved through the use of parenthesis:

spec:
  type: {{ ( .Values.service | default dict ).type | default "ClusterIP" }}

That is quickly becoming ugly with multiple levels of nesting.

Safe nesting can be achieved through the use of parenthesis:

spec:
  type: {{ ( .Values.service | default dict ).type | default "ClusterIP" }}

Good catch, @saiskee.

From the documentation:

Dig can be very useful in cases where you’d like to avoid guard clauses, especially since Go’s template package’s and doesn’t shortcut. For instance and a.maybeNil a.maybeNil.iNeedThis will always evaluate a.maybeNil.iNeedThis, and panic if a lacks a maybeNil field.)

Looks like {{ dig "service" "type" "ClusterIP" .Values }} should solve OP’s original issue.

My issue was I had a range in one of the templates that was causing the issue so I need to change

image: {{ template "registry.repo.tag" . }}

to

image: {{ template "registry.repo.tag" $root }}

Thank you for your insight. I also had a range. Using the $ instead of . in order to use the global scope solved the problem:

metadata:
  name: {{ include "common.fullname" $ }}

Thanks sxyandapp, that works for me too.

The link mentioned above has info on syntax, and there’s a couple samples here, but its seems for some cases this is still not a solution, I believe I’m bumping into https://github.com/helm/helm/issues/9266.

I suppose for such cases I’ll have to use conditionals.

EDIT: or actually use (.Values | merge (dict)) instead of .Values and this will work around it. Still not the same issue as describe here, and hopefully the other one will be “fixed” (as in support for this without using merge) is added.

Looks like sprig added the new dig function, so in helm v3.5.0, you can now use dig.

My issue was I had a range in one of the templates that was causing the issue so I need to change

image: {{ template "registry.repo.tag" . }}

to

image: {{ template "registry.repo.tag" $root }}

Got the same issue, but dig is not working for me due to:

at <dig "one" "two" "three" "name" "default_name" .Values>: error calling dig: interface conversion: interface {} is chartutil.Values, not map[string]interface {}
{{ (((.Values.abc).def).ghi).jkl }}

Does that works in all Helm 3.X versions? And is it documented somewhere?

I just tested and using dig works fine 👍 I did

enable: {{ dig "nodeAutoScaling" "enable" "false" $daDict }}

Thanks for the tip @saiskee

service

this has worked for me:

{{ (((.Values.abc).def).ghi).jkl }}

虽然有很多括号,但这是目前看来最好的办法了

I created a very hacky way to evaluate dictionaries without having to worry about NPE being thrown. Here is the template:

{{- define "common.util.safewalk" -}}
{{- $top := first . -}}
{{- $string := (index . 1) -}}
{{- $matches := (splitList "." $string ) -}}
{{- $stop := false -}}
{{- range $index, $elem := $matches -}}
{{- if not $stop -}}
{{- if gt (len $elem) 0 }}
{{- $output := slice $matches 0 (add1 $index) | join "." -}}
{{- $test := (cat "{{ or (empty " $output ") (not (kindIs \"map\"" $output ")) }}") -}}
{{- $testRes := tpl $test $top }}
{{- if and (eq $testRes "true") (ne (add1 $index) (len $matches)) }}
{{- $stop = true -}}
{{ end -}}
{{ end -}}
{{ end -}}
{{ end -}}
{{- if $stop }}
{{- else }}
# {{ tpl (cat "{{ toYaml " $string "}}") $top }}
{{- end }}
{{- end -}}

(The line starting with # will be your output in this case)

Let’s say you have a values yaml:

foo:
  bar: some text

Then you can use this to get the value of foo.bar {{ include "common.util.safewalk" (list . ".Values.foo.bar") -}}

if you were to do

{{ include "common.util.safewalk" (list . ".Values.foo.baz") -}}"

it would return empty rather than throwing an error.