Using a defined helper as an iteration for a range - kubernetes-helm

While recently writing a helm chart, I ran into an issue that I couldn't find a solution to.
Basically, I am trying to range off of a defined value (labels), but appear to be running into an issue because the defined value is a string and not a map. I tried to convert it with toYaml to no avail:
{{- range $key, $value := ( include "myChart.selectorLabels" . | toYaml ) }}
- key: {{ $key }}
operator: In
values:
- {{ $value }}
{{- end }}
Example error with toYaml:
COMBINED OUTPUT:
Error: Failed to render chart: exit status 1: Error: template: myApp/templates/deployment.yaml:103:77: executing "myApp/templates/deployment.yaml" at <toYaml>: range can't iterate over |-
app.kubernetes.io/name: myApp
app.kubernetes.io/instance: env-myApp
conf-sha: 0b92fad469486fedb6ad012c1b5f22c
Example error without toYaml (note lack of linebreak before first value):
COMBINED OUTPUT:
Error: Failed to render chart: exit status 1: Error: template: myApp/templates/deployment.yaml:103:73: executing "myApp/templates/deployment.yaml" at <.>: range can't iterate over app.kubernetes.io/name: myApp
app.kubernetes.io/instance: env-myApp
conf-sha: 0b92fad469486fedb6ad012c1b5f22c
Expected render:
- key: app.kubernetes.io/name
operator: In
values:
- myApp
- key: app.kubernetes.io/instance
operator: In
values:
- env-myApp
- key: conf-sha
operator: In
values:
- 0b92fad469486fedb6ad012c1b5f22c
I think there must be a more elegant solution to this in helm's templating that I am missing.

Helm has two extension functions to convert between strings and complex YAML structures. You're calling toYaml which takes an arbitrary object and serializes it to a YAML string. What you actually have is the string result from include which happens to be parseable YAML, and you need the opposite function, fromYaml.
{{- range $key, $value := ( include "myChart.selectorLabels" . | fromYaml ) }}
{{/*- not toYaml ^^^^ */}}
Neither function is especially well-documented. They are specific to Helm. There are corresponding toJson and fromJson that work similarly.

Related

Helm: render variable in a block inserted with toYaml

I have a variable block configuring triggers in a Keda scaled object like so:
# [...]
autoscaling:
enabled: true
triggers:
- type: prometheus
metadata:
serverAddress: http://prometheus-server.prometheus.svc.cluster.local
metricName: prometheus_redis_message_count
query: avg(redis_queue_job_states_total{state=~"waiting|active", namespace="{{ $.Release.Namespace }}", queue="reports"}) without (instance, pod, pod_template_hash, state)
threshold: '1'
activationThreshold: '1'
# [...]
as you can see, autoscaling.triggers[0].metadata.query contains a variable ( .Release.Namespace ) which does not get rendered when using the toYaml function (which is understood).
Is there a way, maybe with some named template magic or so, to put all items in triggers into my rendered template while also rendering vars contained inside (like .Release.Namespace?)
Right now I have it like this (which does work for now so I can proceed), but obviously is not great because it's way to static (won't allow multiple triggers or even other trigger types in the future):
# [...]
- type: prometheus
metadata:
serverAddress: http://prometheus-server.prometheus.svc.cluster.local
metricName: prometheus_redis_message_count
{{- with index .autoscaling.triggers 0 }}
query: {{ tpl .metadata.query $ }}
{{- end }}
threshold: '1'
activationThreshold: '1'
# [...]
edit:
thanks to David's reply I was able to write it like so:
triggers:
{{- range .autoscaling.triggers }}
- type: {{ .type }}
metadata:
query: {{ tpl .metadata.query $ }}
serverAddress: {{ tpl .metadata.serverAddress $ }}
threshold: {{ tpl .metadata.threshold $ | quote }}
activationThreshold: {{ tpl .metadata.activationThreshold $ | quote}}
metricName: {{ tpl .metadata.metricName $ }}
{{- end }}
which makes it a valid array and also templates all the fields. Anyway, thanks for the help!
The quick-and-dirty answer is to just run tpl on the output of toYaml. toYaml takes an arbitrary structure and returns a string, but at that point it's just a string and there's nothing particularly special about it.
triggers:
{{ tpl (toYaml .Values.autoscaling.triggers) . | indent 2 }}
I'm not clear that anything more nuanced is possible without iterating through the entire list and running tpl on each individual structure.
triggers:
{{- range .Values.autoscaling.triggers }}
- type: {{ .type }}
metadata:
query: {{ tpl .metadata.query $ }}
{{- end }}
Neither the core Go templating language nor the Sprig extensions have any sort of generic "map" function that could operate on the values of a nested object while retaining its structure.

Is there a native way to access the scope inside a range from inside a function?

In my template I use a range with a param like this:
{{- range $key, $value := .Values.myParam }}
---
fieldOne: $value.fieldOne
fieldTwo: $value.fieldTwo
myFunc: {{- include "myFunc" $ }}
{{ end }}
I want myFunc to get access global values (.Values.global.*) AND the scoped variables like $value.fieldOne and $value.fieldTwo. Is this possible?
If I don't pass myFunc $ it won't have access to globals. If don't pass it $value it won't have access to the range scoped values.
And I'd like to maintain the standard .Values.global.* referencing syntax for accessing globals inside myFunc- I don't want to create my own special map object the function has to read differently.
Currently I am doing this:
myFunc: {{- include "myFunc" (merge $ $value) }}
Then in myFunc I can access this:
{{- define "myFunc" -}}
# Get globals in the usual way
{{- .Values.global.myGlobal }}
# Get my scoped values for the current iteration
{{- .fieldOne }}
{{- .fieldTwo }}
{{- end -}}
Is this the only way to do this? It seems to work reasonably well, but I want to confirm there isn't a builtin like ., but for range scoped variables I can use inside myFunc.
EDIT
I discovered an issue with my approach, it appears calling merge inside the function is mutating the root map.
For example doing this:
# helper function
{{- define "myFunc" -}}
# Get globals in the usual way
someGlobal: {{- .Values.global.myGlobal }}
# Get my scoped values for the current iteration
fieldOne: {{- .fieldOne }}
fieldTwo: {{- .fieldTwo }}
{{- end -}}
# In a template
{{- range $key, $value := .Values.myParam }}
---
apiVersion: v1
kind: ConfigMap
metadata:
name: $value.fieldOne
data:
{{- include "myFunc" (merge $ $value) }}
---
apiVersion: v1
kind: ConfigMap
metadata:
name: $value.fieldOne
data:
{{- include "myFunc" (merge $ $value) }}
{{ end }}
When I look at the outputted config maps the values returned from myFunc are the same. I also noticed I can do this:
# In a template
{{- range $key, $value := .Values.myParam }}
---
apiVersion: v1
kind: ConfigMap
metadata:
name: $value.fieldOne
data:
{{- include "myFunc" (merge $ $value) }}
---
apiVersion: v1
kind: ConfigMap
metadata:
name: $value.fieldOne
data:
someGlobal: {{ .Values.global.myGlobal }}
fieldOne: {{ $value.fieldOne }}
fieldTwo: {{ $value.fieldTwo }
{{ end }}
In the second ConfigMap the values are the same as the first. It's like calling merge inside myFunc is mutating the root map in place.
Ultimately I had to move the logic out of myFunc and call merge directly in the template to avoid this- doing that each iteration correctly gets its item's values.
A Go text/template template only takes one parameter. As with other programming languages, a function (template) can't access local variables in its caller, only things that are explicitly passed in parameters.
In short: the way you have it now is more or less as good as it's possible to do, given the constraints of the template language. If you want to directly reference .Values then the single template parameter must be a dictionary object and not something else.

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.

Helm 3, convert array of objects in values.yaml into comma separated string

I have the following structure in values.yaml file:
upstreams:
- service_name: service_a
mapped_port: 1000
- service_name: service_b
mapped_port: 1001
I want this to be rendered like this in my deployment.yaml template:
"service-upstreams": "service_a:1000, service_b:1001"
Any idea how to do that?
You need to iterate over the objects in the upstreams value and concatenate the values.
Definition of upstream helper template
templates/_helpers.tpl
{{- define "sample.upstreams" -}}
{{- if .Values.upstreams }}
{{- range .Values.upstreams }}{{(print .service_name ":" .mapped_port ) }},{{- end }}
{{- end }}
{{- end }}
Using the template (in this example as a label)
templates/deployment.yaml
metadata:
labels:
{{- if .Values.upstreams }}
service-upstreams: {{- include "sample.upstreams" . | trimSuffix "," | quote }}
{{- end }}
I'm not trimming the comma in the template because trimSuffix does not accept curly brackets as part of an argument
I would refer to the answer David Maze linked and range documentation

Add suffix to each list member in helm template

I'm trying to iterate over a list in a helm template, and add a suffix to each member.
I currently have this block of code that does exactly that:
{{- range $host := .Values.ingress.hosts }}
{{- $subdomain := initial (initial (splitList "." $host)) | join "." }}
{{- $topLevelDomain := last (splitList "." $host) }}
{{- $secondLevelDomain := last (initial (splitList "." $host)) }}
- host: {{- printf " %s-%s.%s.%s" $subdomain $environment $secondLevelDomain $topLevelDomain | trimSuffix "-" }}
{{- end }}
Since I need to do the exact same manipulation twice in the same file, I want to create a new list, called $host-with-env, that will contain the suffix I'm looking for. That way I can only perform this operation once.
Problem is - I've no idea how to create an empty list in helm - so I can't append items from the existing list into the new one.
Any idea how can I achieve this?
I'm also fine with altering the existing list but every manipulation I apply to the list seems to apply to the scope of the foreach I apply to it.
Any ideas how to go about this?
It might not be quite clear what result are you trying to achieve, it will be helpful to add your input, like your values.yaml and the desired output. However, I added an example that answers your question.
Inspired by this answer, you can use dictionary.
This code will add suffix to all .Values.ingress.hosts and put them into $hostsWithEnv dictionary into a list, which can be accessed by myhosts key
values.yaml
ingress:
hosts:
- one
- two
configmap.yaml
{{- $hostsWithEnv := dict "myhosts" (list) -}}
{{- range $host := .Values.ingress.hosts -}}
{{- $var := printf "%s.domain.com" $host | append $hostsWithEnv.myhosts | set $hostsWithEnv "myhosts" -}}
{{- end }}
apiVersion: v1
kind: ConfigMap
metadata:
name: my-configmap
data:
{{- range $hostsWithEnv.myhosts}}
- host: {{- printf " %s" . | trimSuffix "-" }}
{{- end }}
output
$ helm install --debug --dry-run .
[debug] Created tunnel using local port: '62742'
...
# Source: mychart/templates/configmap.yaml
apiVersion: v1
kind: ConfigMap
metadata:
name: my-configmap
data:
- host: one.domain.com
- host: two.domain.com