How to make nested variables optional in Helm
Asked Answered
P

6

73

How do I make an optional block in the values file and then refer to it in the template?

For examples, say I have a values file that looks like the following:

# values.yaml
foo:
   bar: "something"

And then I have a helm template that looks like this:

{{ .Values.foo.bar }}

What if I want to make the foo.bar in the values file optional? An error is raised if the foo key does not exist in the values.

I've tried adding as an if conditional. However, this still fails if the foo key is missing:

{{ if .Values.foo.bar }}
{{ .Values.foo.bar }}
{{ end }}
Placid answered 17/1, 2020 at 22:22 Comment(0)
G
49

Most charts will default the parent object to an empty map in values.yaml so it always exists and first level checks {{ if .Values.foo.bar }} will work.

foo: {}

Test each key in order with parenthesis (added from Torrey's better solution):

{{ if ((.Values.foo).bar) }}
bar: {{ .Values.foo.bar }}
{{ end }}

Use the and function (helm 3.10+/go 1.18+ thanks @nicolauscg)

{{ if (and .Values.foo .Values.foo.bar) }}
bar: {{ .Values.foo.bar }}
{{ end }}

There is also the hasKey function included from sprig if you ever need to check the existence of a falsey or empty value:

{{ if hasKey .Values.foo "bar" }}
Grenier answered 18/1, 2020 at 1:6 Comment(5)
Thank you for your response. I tried the hasKey approach before, but didn't have the default foo: {} set. I'll give that another shot.Placid
Is it possible to cleanup/re-write the following in a cleaner way? {{ if hasKey .Values.global "rabbitmq" }} {{ .Values.global.rabbitmq.password | b64enc | quote }} {{ else }} {{ $def.RABBITMQ_PASSWORD | b64enc | quote }} {{ end }}Kendall
@Kendall I'm not sure how that applies to the nested key issue? You could this as a separate questionGrenier
Note that the and function has changed to short circuit since go1.18 go.dev/doc/go1.18, so using and is also an option on newer helm/go versionWitherspoon
Note that hasKey doesn't work if there is no foo key present (with the wrong type for value; expected map[string]interface {}; got interface {} error)Kalgan
B
132

Simple workaround

Wrap each nullable level with parentheses ().

{{ ((.Values.foo).bar) }}

Or

{{ if ((.Values.foo).bar) }}
{{ .Values.foo.bar }}
{{ end }}

How does it work?

Helm uses the go text/template and inherits the behaviours from there.

Each pair of parentheses () can be considered a pipeline.

From the doc (https://pkg.go.dev/text/template#hdr-Actions)

It is:

The default textual representation (the same as would be printed by fmt.Print)...

With the behaviour:

If the value of the pipeline is empty, no output is generated... The empty values are false, 0, any nil pointer or interface value, and any array, slice, map, or string of length zero.

As such, by wrapping each nullable level with parentheses, when they are chained, the predecessor nil pointer gracefully generates no output to the successor and so on, achieving the nested nullable fields workaround.

Bettyannbettye answered 16/8, 2021 at 17:55 Comment(11)
Cheers; this seems to work great. Where'd you find it? I'm struggling to find any docs for itAstrionics
it doesn't work for me, which helm version do you use?Aleciaaleck
Can confirm it's working on our use case, but would be great to know why it works? I also have not found any docs on why/how the parentheses change the nil behavior.Thaxter
@JameelA. Thanks for confirming. The usage is not documented in the Helm unfortunately. It is the syntax implicitly documented in the go text/template. I have added some references and a brief explanation. Hopefully this will help.Bettyannbettye
@Astrionics Thanks! I originally read this usage from a Github issue and just remembered it. Couldn't find the original thread anymore :(. I have added some reference from the go. Hopefully this will help.Bettyannbettye
@PeterJurkovic I'm using Helm 3.6.3. It should work the same for all Helm 3. Never tested on Helm 2. But the behaviour is inherited from the go template :)Bettyannbettye
This is genius! I am iterating over a list and this allows me to default on missing subkeys for each element. I am using Helm 3.9.xNeuro
OMG! This is great, a null-safe-coalescer... sort-of I had been trying to use dig for this, but it's totally useless and ugly having to use stringsTrudge
Is there any advantage of {{ if ((.Values.foo).bar) }} x: {{ .Values.foo.bar }} {{ end }} over {{ with .Values.foo }} x: {{ .bar }} {{ end }} ?Felspar
@Felspar The parentheses () allows us to inline unlimited nullable nested level, while the with on its own requires one per nullable condition. If preferable, you can combine the parentheses and the with, to specify the unlimited nullable nested scope.Bettyannbettye
This works great, thanks. The fact this is necessary makes me want to run naked around town screaming.British
G
49

Most charts will default the parent object to an empty map in values.yaml so it always exists and first level checks {{ if .Values.foo.bar }} will work.

foo: {}

Test each key in order with parenthesis (added from Torrey's better solution):

{{ if ((.Values.foo).bar) }}
bar: {{ .Values.foo.bar }}
{{ end }}

Use the and function (helm 3.10+/go 1.18+ thanks @nicolauscg)

{{ if (and .Values.foo .Values.foo.bar) }}
bar: {{ .Values.foo.bar }}
{{ end }}

There is also the hasKey function included from sprig if you ever need to check the existence of a falsey or empty value:

{{ if hasKey .Values.foo "bar" }}
Grenier answered 18/1, 2020 at 1:6 Comment(5)
Thank you for your response. I tried the hasKey approach before, but didn't have the default foo: {} set. I'll give that another shot.Placid
Is it possible to cleanup/re-write the following in a cleaner way? {{ if hasKey .Values.global "rabbitmq" }} {{ .Values.global.rabbitmq.password | b64enc | quote }} {{ else }} {{ $def.RABBITMQ_PASSWORD | b64enc | quote }} {{ end }}Kendall
@Kendall I'm not sure how that applies to the nested key issue? You could this as a separate questionGrenier
Note that the and function has changed to short circuit since go1.18 go.dev/doc/go1.18, so using and is also an option on newer helm/go versionWitherspoon
Note that hasKey doesn't work if there is no foo key present (with the wrong type for value; expected map[string]interface {}; got interface {} error)Kalgan
D
21

Use with

Look at the with operator. This limits the current scope to the level of .Values.foo, and the block is silently ignored if .foo is missing:

{{- with .Values.foo }}
  {{- .bar }}
{{- end }}
Diagnose answered 21/7, 2020 at 9:34 Comment(2)
Can confirm, works as advertised. I would accept this as the real answer.Aerobe
There are always some cases you have to use if, e.g., when you need an else. Actually, that would be super great for with to come with an else.Coupon
P
19

A technique I've used successfully is to use a variable to hold the value of the outer block, which then can use templating constructs like default and Sprig's dict helper.

{{- $foo := .Values.foo | default dict -}}
Bar is {{ $foo.bar | default "not in the values file" }}

This provides a fallback dictionary if foo isn't in the file, so then $foo is always defined and you can look up $foo.bar in it.

Pippa answered 18/1, 2020 at 11:40 Comment(0)
K
7

There is a new function implemented in sprig called dig that just does fix this issue, see here http://masterminds.github.io/sprig/dicts.html .

Is not yet released so even less likely to be in helm soon.

Meanwhile I have modified @Samuel solution to mimic the new dig function.

{{- define "dig" -}}
  {{- $mapToCheck := index . "map" -}}
  {{- $keyToFind := index . "key" -}}
  {{- $default := index . "default" -}}
  {{- $keySet := (splitList "." $keyToFind) -}}
  {{- $firstKey := first $keySet -}}
  {{- if index $mapToCheck $firstKey -}} {{/* The key was found */}}
    {{- if eq 1 (len $keySet) -}}{{/* The final key in the set implies we're done */}}
      {{- index $mapToCheck $firstKey -}}
    {{- else }}{{/* More keys to check, recurse */}}
      {{- include "dig" (dict "map" (index $mapToCheck $firstKey) "key" (join "." (rest $keySet)) "default" $default) }}
    {{- end }}
  {{- else }}{{/* The key was not found */}}
      {{- $default -}}
  {{- end }}
{{- end }}

and you can call it like this

$regKey := include "dig" (dict "map" .Values "key" "global.registryKey" "default" "") 
Kaif answered 7/12, 2020 at 19:10 Comment(1)
It seems to me that the dig function might land in helm in January (the new sprig has already been released and the merge request for helm has already been accepted; next feature release for helm is scheduled for Jan 13)Kasey
G
3

I searched around for an answer to this same question, and couldn't find anything out there. It seems you have to use a custom function, so I wrote one. Here is what I came up with. It works for my use cases, feedback/improvements are welcome.

_helpers.tpl

{{- define "hasDeepKey" -}}
  {{- $mapToCheck := index . "mapToCheck" -}}
  {{- $keyToFind := index . "keyToFind" -}}
  {{- $keySet := (splitList "." $keyToFind) -}}
  {{- $firstKey := first $keySet -}}
  {{- if index $mapToCheck $firstKey -}}{{*/ The key was found */}}
    {{- if eq 1 (len $keySet) -}}{{*/ The final key in the set implies we're done */}}
true
    {{- else }}{{*/ More keys to check, recurse */}}
      {{- include "hasDeepKey" (dict "mapToCheck" (index $mapToCheck $firstKey) "keyToFind" (join "." (rest $keySet))) }}
    {{- end }}
  {{- else }}{{/* The key was not found */}}
false
  {{- end }}
{{- end }}

values.yaml:

    {{- if eq "true" (include "hasDeepKey" (dict "mapToCheck" .Values "keyToFind" "foo.bar")) }}
      bar: {{- .Values.foo.bar }}
    {{- end }}
Gustafson answered 8/3, 2020 at 4:57 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.