How to use printf in this Helm template function? - kubernetes-helm

I created this Helm template function in my templates/_helpers.yaml file. It simply gets the gets the value of an array element (the index .Values... part), based on the passed-in environment. It works fine.
{{/*
Function to get min CPU units
*/}}
{{- define "microserviceChart.minCpuUnits" -}}
{{ index .Values.valuesPerEnvironment.cpuUnits ((pluck .Values.environment .Values.environments | first | default .Values.environments.sandbox) | int) | quote }}
{{- end }}
For example, in my values.yaml file
environments:
sandbox: 0
staging: 1
production: 2
valuesPerEnvironment:
cpuUnits: [512, 512, 1024]
so my template function returns "512", "512", "1024" based on my passed-in environment. However, can I use printf to it adds m to these values? In other words, I want it to return "1024m" for production. I tried the following but I get a syntax error
{{/*
Function to get min CPU units
*/}}
{{- define "microserviceChart.minCpuUnits" -}}
{{- printf "%dm" index .Values.valuesPerEnvironment.cpuUnits ((pluck .Values.environment .Values.environments | first | default .Values.environments.sandbox) | int) | quote }}
{{- end }}

Instead of trying to get the arguments in the right order for printf, you could include the m in the template body. You'll also need to make the "..." explicit, instead of using the quote function, to get the m inside the quotes.
Expanded out (note that {{- and -}} will delete whitespace before and after, including newlines):
{{- define "microserviceChart.minCpuUnits" -}}
"
{{- index .Values.valuesPerEnvironment.cpuUnits ((pluck .Values.environment .Values.environments | first | default .Values.environments.sandbox) | int) -}}
m"
{{- end }}

The heck with it. I just made my values.yaml like this, and I get the same result.
environments:
sandbox: 0
staging: 1
production: 2
valuesPerEnvironment:
cpuUnits: [512m, 512m, 1024m]
It'd still be cool to know if I can do this in the function, so if someone answers the actual question, I'll accept that as the answer.

Related

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 -}}

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 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

Helm - comma-separated list of dynamic strings

Is it possible, within a helm chart to create a single string which is a comma-separated representation (similar to using the ",".join() command in Python) of strings with a common prefix and a variable suffix?
For example, I have a CLI application that requires an argument like so via the extraArgs parameter in a kubernetes pod definition:
extraArgs: >-
-M {{ $.Values.global.hostname }}/100
I now have to modify this value to be over a range (i.e. from {{$.Values.global.minval}} to {{$.Values.global.maxval}}, inclusive). So, for a minval=100 and maxval=105, my chart needs to now become (note the lack of a trailing comma, and no spaces other than the space after -M):
extraArgs: >-
-M {{ $.Values.global.hostname }}/100,{{ $.Values.global.hostname }}/101,{{ $.Values.global.hostname }}/102,{{ $.Values.global.hostname }}/103,{{ $.Values.global.hostname }}/104,{{ $.Values.global.hostname }}/105
Is there some way I can execute this in a range/loop in my chart? I have several instances of this chart that will use different min/max values, and I'd like to automate this tedious task as much as I can (additionally, I do not have access to the app's source, so I can't change the CLI interface to the application).
In Python, I could accomplish this roughly by:
minval = 100
minval = 105
s = "-M "
L = []
for i in range(minval, maxval+1):
L.append("{{{{ $.Values.global.hostname }}}}/{}".format(i))
s = s + ",".join(L)
# print(s)
I'm not sure where to begin doing this in a Helm template beyond starting with the range() function.
Helm includes the sprig library of template functions which contains untilStep and join.
There is no concept of a map or each operator in sprig yet so you can construct the list in a range loop to be joined later (from here)
{{- $minval := int .Values.minval -}}
{{- $maxval := int .Values.maxval | add1 | int -}}
{{- $args := list -}}
{{- range untilStep $minval $maxval 1 -}}
{{- $args = printf "%s/%d" $hostname . | append $args -}}
{{- end }}
extraArgs: '-M {{ $args | join "," }}'

Caculating the global index for two ranges in Kubernetes Sprig/helm templates?

In a Helm Chart I have to following values
dataCenters:
- name: a
replicas: 3
- name: b
replicas: 2
When generating the template I would like my output to be like the following
server.1 = a-1
server.2 = a-2
server.3 = a-3
server.4 = b-1
server.5 = b-2
I tried this code
{{- $index := 0 -}}
{{ range $dc := .Values.cluster.dataCenters -}}
{{ range $seq := (int $dc.replicas | until) -}}
{{- $index := (add $index 1) -}}
server.{{ $index }}={{ $dc.name }}-{{ $seq }}
{{ end -}}
{{ end -}}
however in helm templates I don't thing you can reassign the value of the index as my 4th line is attempting and because of that I get out
server.1 = a-1
...
server.1 = b-2
How does one calculates the global index 0 to 4 (1 to 5 in my situation) using the Sprig/Helm templating language?
I have a way to do it that involves some trickery, heavily inspired by functional programming experience.
A Go/Helm template takes a single parameter, but the sprig library gives you the ability to create lists, and the text/template index function lets you pick things out of a list. That lets you write a "function" template that takes multiple parameters, packed into a list.
Say we want to write out a single line of this output. We need to keep track of which server number we're at (globally), which replica number we're at (within the current data center), the current data center record, and the records we haven't emitted yet. If we're past the end of the current list, then print the records for the rest of the data centers; otherwise print a single line for the current replica and repeat for the next server/replica index.
{{ define "emit-dc" -}}
{{ $server := index . 0 -}}
{{ $n := index . 1 -}}
{{ $dc := index . 2 -}}
{{ $dcs := index . 3 -}}
{{ if gt $n (int64 $dc.replicas) -}}
{{ template "emit-dcs" (list $server $dcs) -}}
{{ else -}}
server.{{ $server }}: {{ $dc.name }}-{{ $n }}
{{ template "emit-dc" (list (add1 $server) (add1 $n) $dc $dcs) -}}
{{ end -}}
{{ end -}}
At the top level, we know the index of the next server number, plus the list of data centers. If that list is empty, we're done. Otherwise we can start emitting rows from the first data center in the list.
{{ define "emit-dcs" -}}
{{ $server := index . 0 -}}
{{ $dcs := index . 1 -}}
{{ if ne 0 (len $dcs) -}}
{{ template "emit-dc" (list $server 1 (first $dcs) (rest $dcs)) -}}
{{ end -}}
{{ end -}}
Then in your actual resource definition (say, your ConfigMap definition) you can invoke this template with the first server number:
{{ template "emit-dcs" (list 1 .Values.dataCenters) -}}
Copy this all into a dummy Helm chart and you can verify the output:
% helm template .
---
# Source: x/templates/test.yaml
server.1: a-1
server.2: a-2
server.3: a-3
server.4: b-1
server.5: b-2
I suspect this trick won't work well if the number of servers goes much above the hundreds (the Go templating engine almost certainly isn't tail recursive), and this is somewhat trying to impose standard programming language methods on a templating language that isn't quite designed for it. But...it works.