helm - how to iterate over map with complex values - kubernetes-helm

In a helm chart want to iterate over a map that contains structured values.
I do know how to iterate over a map with simple string values. I also can iterate over an array that contains structured values (not shown here). But I did not manage to iterate over a map that contains structured values.
This is my directory structure containing 3 files:
templates/test.yaml
Chart.yaml
values.yaml
A simple file Chart.yaml (just for completing the showcase):
---
apiVersion: v1
appVersion: "1.0"
description: A Helm chart for Kubernetes
name: foochart
version: 0.1.0
The file values.yaml with a map that contains simple string values (label) and one that contains structured values (label1):
---
label:
fook: foo
bark: bar
label1:
fook:
name: foo
value: foo1
bark:
name: bar
value: bar2
This template test.yaml works:
---
env:
{{- range $k, $v := .Values.label }}
- name: {{ $k }}
value: {{ $v }}
{{- end }}
But when I substitute .Values.label by .Values.label1, it produces no output.
This is my command for testing:
helm template foochart
Question: Is it possible to process a map with structured values? I would like to use something like $v.name. If yes, how can I do that?

You can in fact use syntax like $v.name, if you know that $v is a variable holding an object.
env:
{{- range $k, $v := .Values.label1 }}
- name: {{ $k }}_{{ $v.name }}
value: {{ $v.value }}
{{- end }}
If you know that it has exactly the syntax you want, there is an underdocumented toYaml function that takes an arbitrary object and returns it as unindented YAML. In your example, each of the values has the form of an env: item, and if you know (or specify) that, you can write out literally:
env:
{{- range .Values.label1 }}
- {{ . | toYaml | indent 4 | trim }}
{{- else }}
[]
{{- end }}
(In this last example: I'm not assigning a variable, so . is temporarily reassigned to each value in the map, and the keys are lost; for each item, I convert it to YAML, indent it by 4 spaces, but then trim out leading and trailing whitespace; and if there are no values, I explicitly write out an empty list.)
It's usually easier to specify a format you want your values to be in, and work with that. If you for some reason can't be sure which form you have, the template language includes functions to test on a value's type, so in principle you can test:
env:
{{- range $k, $v := .Values.labelN }}
{{- if kindIs "string" $v }}
- name: {{ $k }}
value: {{ $v }}
{{- else }}
- name: {{ $v.name }}
value: {{ $v.value }}
{{- end }}
{{- else }}
[]
{{- end }}

Related

Helm - only create if nested values are set

I am wondering if there is a more efficient way to exclude any yaml keys which do not have a value set.
My current approach is to wrap each key in an if statement...
container:
spec:
{{- if values.spec.x }}
x: {{ values.spec.x }}
{{- end}}
{{- if values.spec.y }}
y: {{ values.spec.y }}
{{- end}}
{{- if values.spec.z }}
z: {{ values.spec.z }}
{{- end}}
e.g.
for each child of container.spec:
if the value != null:
include as child of spec
else:
exclude from spec
I thought about wrapping the above in a _helper.tpl function to try to keep the main template tidy, but it would still include writing multiple if statements.
Is there a better way of doing the above?
Thanks!
You can directly translate that pseudocode into Helm chart logic. The trick is that a Go template range loop is basically equivalent to a "for" loop in most languages. So:
container:
spec:
{{- range $key, $value := .Values.spec }}
{{- if ne $value nil }}
{{ $key }}: {{ $value }}
{{- end }}
{{- end }}
If you can just omit the unused keys from the values, then this becomes simpler and safer. Helm includes a lightly-documented toYaml function that will render an arbitrary structure as YAML, but you can't really do any filtering or other preprocessing before writing it out.
container:
spec:
{{ .Values.spec | toYaml | indent 4 }}

In helmcharts, how to combine a list with multiple fields per element into a string?

I have a list that has multiple fields per element, and I would need to concatenate it into a comma separated string that has fields foo and bar separated with a semicolon.
myList:
- foo: "asdf"
bar: "hnng"
- foo: "meh"
bar: "dunno"
For example, the above would need to be concatenated into "asdf;hnng,meh;dunno".
You could iterate through the list with range, but how would you then pass on that list to join? For example, the following should produce a list of strings, but how would I then pass it into join function?
{{- range .Values.myList }}
- {{.foo}};{{.bar}}
{{- end}
=>
- asdf;hnng
- meh;dunno
values.yaml
myList:
- foo: "asdf"
bar: "hnng"
- foo: "meh"
bar: "dunno"
template
apiVersion: v1
kind: ConfigMap
metadata:
name: test
data:
data: |-
{{- $str := "" }}
{{- range $i, $e := .Values.myList }}
{{- if $i }}
{{- $str = print $str "," }}
{{- end }}
{{- $str = print $str $e.foo ";" $e.bar }}
{{- end }}
{{- $str | nindent 4 }}
output
apiVersion: v1
kind: ConfigMap
metadata:
name: test
data:
data: |-
asdf;hnng,meh;dunno
The Go text/template language doesn't have a generic map function or anything else that would make it possible to transform a list into another list. You need to rewrite this to emit the whole string in one block, it's probably not going to be possible to construct an input to join.
When you iterate through the outer list, you can get the current index and then use that to decide whether to set a separator:
{{- $index, $item := range .Values.myList -}}
{{- if ne $index 0 -}},{{- end -}}
{{- $item.foo -}};{{- $item.bar -}}
{{- end -}}
It's possible to make this a lot more complicated - split out the individual item into its own template, change the loop to a recursive template call - but fundamentally you need to emit the list-item separators yourself.

How to add counter to helm range

Here is my values.yaml file:
options:
collection: "myCollection"
ttl: 100800
autoReconnect: true
reconnectTries: 3
reconnectInterval: 5
Now I'm trying to convert it into JSON in my configMap like this:
options: {
{{- range $key, $val := .Values.options }}
{{ $key }}: {{ $val | quote }},
{{- end }}
}
But I need to eliminate last comma in JSON so I'm trying to add a counter:
options: {
{{ $c := 0 | int }}
{{- range $key, $val := .Values.options }}
{{ if ne $c 0 }},{{ end }}
{{- $key }}: {{ $val | quote }}
{{ $c := $c add 1 }}
{{- end }}
}
But I'm getting following error for helm template ... command:
at <$c>: can't give argument to non-function $c
So what am I doing wrong?
Helm has an undocumented toJson template function, so if you can get your data in the right format you can just ask it to serialize it.
Managing the quoting for the embedded JSON file will be tricky. The two good options are to use a YAML block scalar, where indentation at the start of a line delimits the content, or treat it as binary data.
apiVersion: v1
kind: ConfigMap
metadata:
name: x
data:
optionsAsBlockScalar: >-
{{ .Values.options | toJson | indent 4 }}
binaryData:
optionsAsBase64: {{ .Values.options | toJson | b64enc }}
Note that this approach will preserve the native types of the objects in the JSON content; your example forces everything to strings. If you need everything to be a string then the Sprig support library contains functions to convert arbitrary objects to strings and to mutate a dictionary-type object in place, though this starts to get into the unfortunate land of writing real code in your templating language.
The simplest way to increment the counter in your case would be to replace
{{ $c := $c add 1 }}
with
{{ $c = add1 $c }}

Passing dictionary from one template to another in Helm

I'm trying to pass a dictionary from one helm template to another but it's resolved to null inside the called template.
Calling template - deployment.yaml
Called template - storageNodeAffinity
I see myDict printed as map inside deployment.yaml but inside storageNodeAffinity it's printed as null.
Eventually I need to pass nodeAffn from the values file.
deployment.yaml
{{- $myDict := dict "cpu" "amd" }}
{{- include "storageNodeAffinity" $myDict | indent 6 }}
{{printf "%q" $myDict}}
storage-affinity.tpl
{{- define "storageNodeAffinity" }}
{{/* {{- $myDict := dict "cpu" "amd" }}*/}}
{{printf "%q" .myDict}}
{{- range $key, $val := .myDict }}
- key: {{ $key }}
operator: In
values:
- {{ $val }}
{{- end }}
{{- end }}
values.yaml
nodeAffn:
disktype: "ssd"
cpu: intel
When you call a template
{{- include "storageNodeAffinity" $myDict -}}
then within the template whatever you pass as the parameter becomes the special variable .. That is, . is the dictionary itself; you don't need to use a relative path to find its values.
{{- define "storageNodeAffinity" }}
{{/* ., not .myDict */}}
{{printf "%q" .}}
{{- range $key, $val := . }}...{{ end -}}
{{- end -}}
I figured it out. The trick is to pass context of the parent variable for the variable you want to use in the called template. So here I'm passing "csAffn" as context and then using "nodeAffn" inside this context, in the called template (_additionalNodeAffinity)
_additionalNodeAffinity.tpl
{{- define "additionalNodeAffinity" }}
{{- range $key, $val := .nodeAffn }}
- key: {{ $key }}
operator: In
values:
- {{ $val }}
{{- end }}
{{- end }}
deployment.yaml
{{- include "additionalNodeAffinity" ( .Values.csAffn )
values.yaml
csAffn:
nodeAffn:
disktype: "ssd"
cpu: "intel"

How to use .Values in other variable loops

Below is my case:
{{- $v := (.Files.Get "values-productpage.yaml") | fromYaml }}.
spec:
{{- range $key, $value := $v.containers }}
containers:
- name: {{ $value.name }}
image: {{.Values.productpage_image}}:latest
Here when reaching .Values.productpage_image, it reports: can't evaluate field productpage_image in type interface {}.
Is there any usage error here? Why can I not use .Values.xxx in this loop? If I move the .Values to the first line, there is no error.
You can simply use $ to get to the root scope
Without defining what $root is, you can references .Values as $.Values from within a loop, or any other scope.
Source: https://github.com/kubeapps/kubeapps/pull/1057
It's because range changes a scope (see detailed description here https://github.com/helm/helm/blob/master/docs/chart_template_guide/control_structures.md#looping-with-the-range-action).
You can assign .Values.productpage_image to the variable outside the range and use inside.
As #abinet explained properly about the reason, I'll share my solution for that( which helped me a lot, and I hope that will save you time):
First, I saved the scope:
{{- $root := . -}}
and after that , I called the .Value inside the loop context like this:
{{ $root.Values.data }}
so basically , you code should be look like:
{{- $root := . -}}
{{- $v := (.Files.Get "values-productpage.yaml") | fromYaml }}.
spec:
{{- range $key, $value := $v.containers }}
containers:
- name: {{ $value.name }}
image: {{$root.Values.productpage_image}}:latest