How to fetch a value inside an array using helm template - kubernetes-helm

I have the following structure:
label:
foo:
bar:
- x: hello
y: hallo
z: hola
In order to reach the value of z, I am currently doing:
{{ $bar := pick .Values.label.foo "bar" }}
{{ $firstItem := first $bar }}
{{ $myValue := get $firstItem "z" }}
Is there a more concise way to do this? I tried something like pick .Values.label.foo[0].z but that does not work

Since the values structure you show is just simple string-keyed dictionaries and lists, you don't need functions like pick or get; you can just use the . operator to retrieve a specific key from the containing dictionary.
{{ $bar := .Values.label.foo.bar }}
{{ $firstItem := first $bar }}
{{ $myValue := $firstItem.z }}
Then you can replace the variable references with their expressions, using (...) parenthesis grouping if needed.
{{ $myValue := (first .Values.label.foo.bar).z }}
You can also use the standard index function here; I believe it is legal to mix array and map keys. This will give you a single call, though a mix of indexing syntax.
{{ $myValue := index .Values.label.foo.bar 0 "z" }}
{{ $myValue := index .Values "label" "foo" "bar" 0 "z" }}

Related

Helm equals to one of multiple values

I want to have a condition that will be considered as "true" if .Values.envName is "dev" + the release namespace name is one of a closed list. otherwise, it should be "false".
Tried the following, but seems like it gets unexpected "false" when I ran it with .Values.envName = dev & .Values.envName = ns-1:
env:
- name: MY_ENV
{{- if and (eq .Values.envName "dev") (regexMatch "^(?!^ns-1$)(?!^ns-2$).*$" .Release.Namespace)}}
value: 'true'
{{- else }}
value: 'false'
{{- end }}
A general note - if there is a better way to use eq with multiple possible values please let me know.
You can use has to test if some item is in a list; and then you can use list to construct a list of known items.
So for your example, if the test is that envName is exactly dev, and the namespace is either ns-1 or ns-2, then you can say
env:
{{- $isDev := eq .Values.envName "dev" }}
{{- $isNs := list "ns-1" "ns-2" | has .Release.Namespace }}
- name: MY_ENV
value: {{ and $isDev $isNs | toString | quote }}
You could do this case with a regular expression too. The start-of-string test ^ and the end-of-string test $ generally need to be outside parentheses; I wouldn't worry about "non-greedy matches" if you're just trying to determine whether a string matches without extracting things. Here I might write either of
{{- $isNs := regexMatch "^ns-[12]$" .Release.Namespace }}
{{- $isNs := regexMatch "^ns-1|ns-2$" .Release.Namespace }}
If you've got an enumerated list of values that don't neatly fall into a regex, using has and list is probably clearer.

Go template: is there any item in list of objects with a specific attribute value?

I'm using helm (sprig, go templates). I'm trying to build guards to selectively include stuff in my helm chart, but only if one of the components needs them.
So, I have a list:
- name: foo
flag1: true
flag2: false
flag3: false
- name: bar
flag1: false
flag2: true
flag3: false
I want to do something akin to a (pseudocode) list.any(flag), where over a variable length list, if I passed in flag1 or flag2 I'd get back true, but flag3 would get me false. If possible, I'd like to be able to ask about a different flag without repeating myself each time.
Is there a concise way to accomplish this? Can it be done?
The set of things that are and aren't possible in Go templates can be a little mysterious. A named template always returns a string, but an empty string is logically "false", so it should be possible to write a template call like
{{- if (include "list.any" (list .Values.options "flag2")) }}
...
{{- end }}
A template only takes a single parameter, so in the call we've packed the multiple inputs we need into a list. We've also used the Helm-specific include function to invoke a template and get its output as a string.
How can the template work? Template range loops don't have break or return actions or any other way to stop early. If we only want to output the "success" value once, this means we need to manually iterate through the list. For reasonably short lists, a recursive template call works here.
(For this specific thing, outputting yes or yesyesyes would both be non-empty and therefore logically "true", so you could use a range loop here successfully. This would not work for an equivalent list.all, though.)
In the template definition
{{- define "list.any" -}}
...
{{- end -}}
we need to start by unpacking the parameter list
{{- $list := index . 0 -}}
{{- $search := index . 1 -}}
We only do something if the list is non-empty.
{{- if $list -}}
...
{{- end -}}
If it is non-empty, we can split out its first element. We expect that to be a map, so we can look up the requested key in that with the standard index function. This will return nil if the key is absent and false if it's false, both of which are logically false; if it's true then the if test will pass.
{{- if index (first $list) $search -}}
...
{{- else -}}
...
{{- end -}}
If we do find the item, we can write out a success value and not do anything else
yes
If we don't, then we can recursively call ourselves with the remainder of the list.
{{- include "list.any" (list (rest $list) $search) -}}
Combining that all together gives this template (indented for clarity, the - markers will consume all of the whitespace):
{{- define "list.any" -}}
{{- $list := index . 0 -}}
{{- $search := index . 1 -}}
{{- if index (first $list) $search -}}
yes
{{- else -}}
{{- include "list.any" (list (rest $list) $search) -}}
{{- end -}}
{{- end -}}

Usage of variables in Helm

I'm using a Helm chart and I was wondering how I can define a value by default. In my case, I wanna define a date when it isn't defined in values.yaml and I have the following code:
{{- if ne .Value.persistence.home.restoreBackup.date "" }}
{{- $bkDate := .Value.persistence.home.restoreBackup.date }}
{{- else }}
{{- $bkDate := "2022-01-01" }}
{{- end }}
I wanna set $bkDate to an specific date if it is not defined in .Value.persistence.home.restoreBackup.date but when I try to print $bkDate it is empty.
Do you know what is wrong here?
The Go text/template documentation notes (under "Variables"):
A variable's scope extends to the "end" action of the control structure ("if", "with", or "range") in which it is declared....
This essentially means you can't define a variable inside an if block and have it visible outside the block.
For what you want to do, the Helm default function provides a straightforward workaround. You can unconditionally define the variable, but its value is the Helm value or else some default if that's not defined.
{{- $bkDate := .Value.persistence.home.restoreBackup.date | default "2022-01-01" -}}
(Note that a couple of things other than empty-string are "false" for purposes of default, including "unset", nil, and empty-list, but practically this won't matter to you.)
If you need more complex logic than this then the ternary function could meet your needs {{- $var := $someCondition | ternary "trueValue" "falseValue" -}} but this leads to hard-to-read expressions, and it might be better to refactor to avoid this.
Try
{{- if ((.Value.persistence.home.restoreBackup).date) }}
{{- $bkDate := .Value.persistence.home.restoreBackup.date }}
{{- else }}
{{- $bkDate := "2022-01-01" }}
{{- end }}
You can check the with option : https://helm.sh/docs/chart_template_guide/control_structures/#modifying-scope-using-with
{{ with PIPELINE }}
# restricted scope
{{ end }}

How can I get data from values.yaml by a specific name? Helm templating question

If I have a values.yaml that looks something like this:
imageName:
portInfo:
numberOfPorts: 11
startingPort: 7980
env:
- name: "MIN_PORT"
value: 7980
- name: "MAX_PORT"
value: 7990
Right now I have a function in _helpers.tpl that takes the .Values.portInfo.numberOfPorts and .Values.portInfo.startingPort and will loop through for each port value something like this into deployments.yaml:
- containerPort: 7980
protocol: TCP
- containerPort: 7981
...
This is what that function looks like:
{{- define "ports.list" -}}
{{- $dbPorts := (.Values.issdbImage.portInfo.numberOfPorts | int) }}
{{- $startingPort := (.Values.issdbImage.portInfo.startingPort | int) }}
{{- range $i := until $dbPorts }}
- containerPort: {{ add $startingPort $i }}
protocol: TCP
{{- end }}
{{- end}}
What I want to do instead is use the function to instead grab the values under MIN_PORT and MAX_PORT and do the same thing.
Is this possible? And, if so, I'd appreciate suggestions on how this can be accomplished.
Thanks!
That env: data structure will be hard to traverse from Helm template code. The values you show can easily be computed, and instead of trying to inject a complete environment-variable block in Helm values, it might be easier to construct the environment variables in your template.
# templates/deployment.yaml -- NOT values.yaml
env:
{{- $p := .Values.imageName.portInfo }}
- name: MIN_PORT
value: {{ $p.startingPort }}
- name: MAX_PORT
value: {{ add $p.startingPort (sub $p.numberOfPorts 1) }}
If you really absolutely can't change the values format, this is possible. Write a helper function to get a value from the .Values.env list:
{{/* Get a value from an association list, like a container env:
array. Each item of the alist is a dictionary with keys
`name:` and `value:`. Call this template with a list of two
items, the alist itself and the key to look up; outputs the
value as a string (an empty string if not found, all values
concatenated together if there are duplicates). */}}
{{- define "alist.get" -}}
{{- $alist := index . 0 -}}
{{- $needle := index . 1 -}}
{{- range $k, $v := $alist -}}
{{- if eq $k $needle -}}
{{- $v -}}
{{- end -}}
{{- end -}}
{{- end -}}
Then in your generator template, you can call this with .Values.env, using the Helm-specific include function to invoke a template and get its result as a string, and then atoi to convert that string to a number.
{{- define "ports.list" -}}
{{- $startingPort := include "alist.get" (list .Values.env "MIN_PORT") | atoi }}
{{- $endingPort := include "alist.get" (list .Values.env "MAX_PORT") | atoi }}
{{- range untilStep $startingPort (add $endingPort 1) 1 -}}
- containerPort: {{ . }}
protocol: TCP
{{ end -}}
{{- end -}}
This approach is both more complex and more fragile than directly specifying the configuration parameters in values.yaml. Helm doesn't have great support for unit-testing complex templates (I've rigged up a test framework using helm template and shunit2 in the past) and there's some risk of it going wrong.

Helm optional nested variables

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 }}
Any thoughts are much appreciated.
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.
Most charts will default the parent object to a empty map in values.yaml so it always exists.
foo: {}
Then {{ if .Values.foo.bar }} works.
If that's not possible, test both keys:
{{ if .Values.foo }}
{{ if .Values.foo.bar }}
{{ .Values.foo.bar }}
{{ end }}
{{ end }}
Using the and function doesn't work in this case due to and evaluating all parameters, even if the first is falsey.
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" }}
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.
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 }}
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" "")
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 }}