What would be the helm function to get value from a complex map? - kubernetes-helm

We have several values that are AWS region dependent. So I have this in my values.yaml file
regional_values:
us-east-1:
repository: 01234567890.dkr.ecr.us-east-1.amazonaws.com
serviceSecurityGroup1:
- sg-01234567
- sg-12345678
- sg-23456789
...
us-west-1:
repository: 98765432109.dkr.ecr.us-east-1.amazonaws.com
serviceSecurityGroup1:
- sg-34567890
- sg-45678901
- sg-56789012
...
When I do a helm install I pass the aws_region
$ helm install myApp myChart/ --set aws_region
What would be a helper function to return say .Values.regional_values.us-east-1.repository, so I get 01234567890.dkr.ecr.us-east-1.amazonaws.com?
{{/*
Get the repository from the AWS region
*/}}
{{- define "microservice-base-chart.reponame" -}}
{{/* I know this syntax is wrong */}}
{{- get .Values.regional_values .Values.aws_region.repository }}
{{- end }}

This worked, but what's a better way? Is there a way to streamline the $idx := and the printf lines into one?
{{/*
Get the repository from the AWS region
*/}}
{{- define "microservice-base-chart.reponame" -}}
{{- $idx := index .Values.regional_values .Values.aws_region }}
{{- printf "%s" $idx.repository }}
{{- end }}

Related

How to merge helm chart and vault agent tempate?

I have two helm chart templates.
{{- define "helpers.config.tpl" -}}
debug: "true"
db:
app:
dialect: "mysql"
endpoint: {{`{{ with secret "` }}{{ default "common/data/mydata" $.Values.vaultSecretName }}{{`" -}}{{ .Data.data.db }}{{- end }}`}}
{{- end -}}
{{- define "helpers.config" -}}
{{- $default := fromYaml ( include "helpers.config.tpl" . ) -}}
{{- $conf := .Values.configFile -}}
{{- toYaml (merge $conf $default ) }}
{{- end -}}
Also I have values.yaml like below.
configFile:
db:
app:
conn: 300
I want to add or modify attributes just passing values via values.yaml file. But I got error like Error: 'error converting YAML to JSON: yaml: line 86: did not find expected key' . How do I solve this issue?
The Vault Agent Template syntax includes an extra layer of templating, which gets produced by helpers.config.tpl. If you invoke that template with empty .Values you will get out
debug: "true"
db:
app:
dialect: "mysql"
endpoint: {{ with secret "common/data/mydata" -}}{{ .Data.data.db }}{{- end }}
That's not valid YAML, though: YAML { ... } should be dictionary syntax, not Go template syntax. You have to force this string to be interpreted as a string.
Probably the most robust way is to use YAML block scalar syntax, indenting the thing you're trying to emit but applying no other escaping:
endpoint: |-
{{`{{ with secret "` }}{{ default "common/data/mydata" $.Values.vaultSecretName }}{{`" -}}{{ .Data.data.db }}{{- end }}`}}
Thanks #DavidMaze
I solved this issue. It's kinda tricky, though. I formed my yaml document like below.
endpoint: |-
{{`{{ with secret "`}}{{ default "common/data/mydata" $.Values.vaultSecretName }}{{`" -}}
{{ .Data.data.db }}
{{- end }}`}}

How to check for a non-existent dictionary value within a helm template?

In my values config file, I have an array of dictionaries as follows:
connects_to
- name: myname
release: optional release
- name: another name
Note that name will always be provided but release may or may not be. In my template, I have:
{{- if .Values.connects_to }}
app.openshift.io/connects-to: '
{{- range .Values.connects_to -}}
{{- if .release -}}
{{- .release -}}-{{- .name -}},
{{- else -}}
{{- $.Release.Name -}}-{{- .name -}},
{{- end -}}
{{- end -}}
'
{{- end }}
which gives the error:
can't evaluate field release in type interface {}
I have also tried using "hasKey" as follows:
{{- if .Values.connects_to }}
app.openshift.io/connects-to: '
{{- range .Values.connects_to -}}
{{- if hasKey . "release" -}}
{{- .release -}}-{{- .name -}},
{{- else -}}
{{- $.Release.Name -}}-{{- .name -}},
{{- end -}}
{{- end -}}
'
{{- end }}
which gives the error:
wrong type for value; expected map[string]interface {}; got string
How would I accomplish this check without having to specify a value for "release" every time? My helm version:
version.BuildInfo{Version:"v3.2.3+4.el8", GitCommit:"2160a65177049990d1b76efc67cb1a9fd21909b1", GitTreeState:"clean", GoVersion:"go1.13.4"}
This actually works with the "hasKey" approach, there was an error in the values definition.
For people looking for a possible cause for the error (wrong type for value; expected map[string]interface {}; got string).
Given the template
{{- if hasKey .Values.somedict "valueX" -}}
{{- .Values.somedict.valueX -}}
{{- end -}}
And using 2 values.yaml (eg: helm template . --values Values1.yaml --values Values2.yaml).
Values1.yaml:
somedict:
valueA: 1
Values2.yaml:
# Causes error
somedict:
# Works
somedict: {}
# Leaving out "somedict" also work
Regarding #DieterDP 's response https://stackoverflow.com/a/72360797/17143221
I am getting the same issue when using this template.
This can be solved by using parentheses around the optional dict as a mean of null-safety.
template:
# Fails when somedict doesn't exist
{{- if hasKey .Values.somedict "valueX" -}}
{{- .Values.somedict.valueX -}}
{{- end -}}
# Works when somedict doesn't exist
{{- if hasKey (.Values.somedict) "valueX" -}}
{{- .Values.somedict.valueX -}}
{{- end -}}
values.yaml:
# without "somedict"

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"

Adding news lines when defining collection

I am trying to define a collection (dict), and I would like to add a new line on each definition (for readability), Eg:
{{ $deployment := dict
"Release" .Release
"Chart" .Chart
"Values" .Values }}
But when I do this, helm respond a parse error :
Error: parse error in "XXX": template: XXX:2: unclosed action
Error: UPGRADE FAILED: parse error in "XXX": template: XXX:2: unclosed action
Is there a way in HELM to do this?
I achieved this by defining the dict first and then setting one key per line.
{{- $myDict := dict "" "" -}}
{{- $_ := set $myDict "myKey1" "myValue1" -}}
{{- $_ := set $myDict "myKey2" "myValue2" -}}
{{- $_ := set $myDict "myKey3" "myValue3" -}}
{{- $_ := set $myDict "myKey4" "myValue4" -}}
Bonus Tip: Since dict get function is available seemingly in only helm3 and later, you can use this hack to get a value from a dict to a string.
{{/* Hack needed until helm 3 which has 'get' for 'dict' */}}
{{- $myValue3Var := pluck "myKey3" $myDict | first -}}
TLDR;
It's impossible to declare dict in multiline way, like with Perl fat comma operator.
Please check the reference of "Sprig: Template functions for Go templates."
Instead you could use this sort of hacky way to achieve similar result:
Keep each key value pair in separate line, in Global Values file for readability:
# values.yaml
--
global:
someMap:
coffee: robusta
origin: Angola
crema: yes
Define helper template in _helpers.tpl:
{{- define "mychart.labels.standard"}}
{{- $global := default (dict) .Values.global.someMap -}}
Release: {{ .Release.Name | quote }}
Chart: {{ .Chart.Name }}
Values:
{{- $global := default (dict) .Values.global.someMap -}}
{{- range $key, $value := $global }}
{{ $key }}: {{ $value }}
{{- end }}
{{- end -}}
Include it in another template:
helm_data:
{{- $global := default (dict) .Values.global -}}
{{- range $key, $value := $global }}
{{ $key }}: {{ $value }}
{{- end }}
{{ include "mychart.labels.standard" . | nindent 0 -}}
Render it to verify the result (helm template --name dict-chart .)
---
# Source: mychart/templates/data_type.yaml
helm_data:
someMap: map[crema:true origin:Angola coffee:robusta]
Release: "dict-chart"
Chart: mychart
Values:
coffee: robusta
crema: true
origin: Angol
It seems it's impossible to do so. The Helm templating system is basically the Go templating system. As stated in the Go templating docs:
Except for raw strings, actions may not span newlines, although comments can.
For people coming across this question, this functionality works in recent versions of HELM. For me, OPs example works as-is (Helm v3.8.2).
(I came across this question myself due to a mismatched ) in my template.)

How best to say a value is required in a helm chart?

I am doing this now:
value: {{ required "A valid .Values.foo entry required!" .Values.foo }}
But to give this same message for all required values in the templates is cumbersome and clutters the templates in my opinion.
Is there a better way where we could define it outside the template \ or a cleaner way to do it within the template itself?
You could do something by taking advantage of range and the fact that null will fail the required check. So in your values.yaml you could have this section for required env vars:
reqEnv:
- name: "VAR1"
value: null
- name: "VAR2"
value: null
And in the env section of the Deployment you then have:
{{- range .Values.reqEnv }}
{{ .name }}: {{ required "A value must be entered for all reqEnv entries" .value }}
{{- end }}
Then the user gets an error unless they set all required values of the reqEnv section in their values file or as paramters. Unfortunately what you lose by doing this is the detail of which var is missing. This could be why the official helm charts seem to prefer using required in the way that you already are.
Define the required values on top of your manifest as variables utilizing the required function.
E.g. deployment.yaml:
{{- $name := .Values.name | required ".Values.name is required." -}}
---
apiVersion: apps/v1
kind: Deployment
metadata:
name: {{ $name }}
....
You can use helm lint with --strict flag to check undefined values
$ helm lint --strict .
==> Linting .
[INFO] Chart.yaml: icon is recommended
[ERROR] templates/: render error in "mychart/templates/service.yaml": template: mychart/templates/service.yaml:10:19: executing "mychart/templates/service.yaml" at <.Values.foo>: map has no entry for key "foo"
Error: 1 chart(s) linted, 1 chart(s) failed
To expose name of missing item to required text you can do something like this:
{{- range $field, $my_key := $data }}
{{- if hasKey $dic1 $my_key }}
{{ $field }}: {{ index $dic1 $my_key | b64enc}}
{{- else if hasKey $dic2 $my_key }}
{{ $field }}: {{ index $dic2 $my_key | b64enc}}
{{- else }}
{{ $field }}: {{ required (printf "key %s is missing" $my_key) nil }}
{{- end }}
{{- end }}
I've come up with an alternative hack trying to solve this problem. It's questionable whether this is actually any better that the built in solution, but I thought it might be worth noting down here as an option.
Add a function to your _helpers.tpl (or wherever):
{{/*
Require and include a value
*/}}
{{- define "require" -}}
{{- $scope := index . 0 -}}
{{- $name := index . 1 -}}
{{required (print "Missing required value: " $name) (index $scope "Values" $name)}}
{{- end}}
Then in your template call it with:
value: {{ include "require" (list . "foo") }}
If a value is missing, it errors with message:
Missing required value: foo
Otherwise, it inserts the value.
Edit: To make this work for nested values, you need a slightly more complex helper:
{{/*
Index a nested component
*/}}
{{- define "indexNested" -}}
{{- $message := index . 0 -}}
{{- $object := index . 1 -}}
{{- $path := (mustRegexSplit "\\." (index . 2) -1) -}}
{{- range $path -}}
{{- if not $object -}}
{{ fail $message }}
{{- end -}}
{{- $object = index $object . -}}
{{- end -}}
{{ required $message $object }}
{{- end}}
{{/*
Require and include a value
*/}}
{{- define "require" -}}
{{- $scope := index . 0 -}}
{{- $name := index . 1 -}}
{{ include "indexNested" (list (print "Missing required value: " $name) $scope.Values $name) }}
{{- end}}
Now you can access the foo.bar value with:
{{ include "require" (list . "foo.bar") }}