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
Related
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.
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.
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.
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
I'm trying to use a helm template helper to filter out values from a list in my values.yaml file, based on the value of one of the keys in each list member.
My chart is currently comprised of these files -
values.yaml -
namespaces:
- name: filter
profiles:
- nonProduction
- name: dont-filter
profiles:
- production
clusterProfile: production
templates/namespaces.yaml
apiVersion: v1
kind: List
items:
{{ $filteredList := include "filteredNamespaces" . }}
{{ range $filteredList }}
{{ .name }}
{{- end -}}
templates/_profile-match.tpl
{{/* vim: set filetype=mustache: */}}
{{- define "filteredNamespaces" -}}
{{ $newList := list }}
{{- range .Values.namespaces }}
{{- if has $.Values.clusterProfile .profiles -}}
{{ $newList := append $newList . }}
{{- end -}}
{{ end -}}
{{ $newList }}
{{- end -}}
The problem is that within my helper file, the $newList variable is only populated within the scope of the range loop, and I end up with an empty list returning to the namespaces.yaml template.
Is there any way around this issue? Am I taking the wrong approach for solving this?
For all that Go templates are almost general-purpose functions, they have a couple of limitations. One of those limitations is that they only ever return a string; you can't write basic functional helpers like map or filter because you can't return the resulting list.
The more straightforward approach to doing filtering as you've shown is to move it up to the point of the caller (and possibly repeat the condition if it's needed in multiple places):
items:
{{- range .Values.namespaces }}
{{- if has $.Values.clusterProfile .profiles }}
- {{ .name }}
{{- end }}
{{- end }}
A hacky way to make this work as you have it is to marshal the list to some other string-based format, like JSON:
{{- define "filteredNamespaces" -}}
...
{{ toJson $newList }}
{{- end -}}
{{- range include "filteredNamespaces" . | fromJson -}}...{{- end -}}
Also remember that you can inject Helm values files using the helm install -f option. So rather than listing every permutation of option and then filtering out the ones you don't want, you could restructure this so that namespaces: only contains the list of namespaces you actually want to use, but then you use a different values file for each profile.
I had to deal with a very similar problem. I'm giving a minimal example how to filter and use the generated template as a list (Helm 3).
_helpers.tpl:
{{- define "FilteredList" -}}
{{ $newList := list }}
{{- range .Values.my_list }}
{{ $newList = append $newList .pvc }}
{{- end }}
{{ toJson $newList }}
{{- end }}
pvc.yaml:
{{- range include "FilteredList" . | fromJsonArray }}
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
name: {{ . }}
spec:
resources:
requests:
storage: 1G
---
{{- end }}
values.yaml:
my_list:
- name: foo
pvc: pvc-one
- name: foo2
pvc: pvc-two
- name: foo3
pvc: pvc-three
Generates 3 PVC resources as you would expect.
Mind 2 things here, which differ from the question and accepted answer:
When appending and overriding the list, we use = instead of :=. Note the example the Helm 3 docs for "append" provide: $new = append $myList 6.
I actually don't know why = is required, a quick research from my side was without any result.
Recover the list from the JSON string using fromJsonArray. This function is not documented yet which is very unfortunate and the main reason i provide this extra answer. You can find it in Helm's source however.
We faced a similar problem and thanks to the answer provided from nichoio we were able to solve this. Just for google's sake, I'll provide it here as well :)
Our values.yaml included all of our secret references, and we wanted to filter those secrets, based upon which service is currently being deployed.
We added a new array in the values.yaml (named selectedSecrets), which was a list of secrets that are needed for the current deployment. Then, with the help of a function, only the secret references are passed to the deployment, which are included in the selectedSecrets. Please note, that the following function returns the original, complete list of secretRefs if no selectedSecrets are present.
{{- define "FilteredSecrets" -}}
{{ $result := .Values.secretRefs }}
{{ $selectedSecrets := .Values.selectedSecrets }}
{{- if gt (len $selectedSecrets) 0 -}}
{{ $result = list }}
{{ range $secret := .Values.secretRefs }}
{{ if has $secret.name $selectedSecrets }}
{{ $result = append $result $secret }}
{{- end -}}
{{- end }}
{{- end -}}
{{ toJson $result }}
{{- end }}
The relevant part from the deployment:
containers:
- env:
{{- range include "FilteredSecrets" . | fromJsonArray }}
{{- range $env := .envs }}
- name: {{ $env.name }}
valueFrom:
secretKeyRef:
key: {{ $env.name }}
name: {{ .name }}
optional: {{ $env.optional }}
{{- end }}
{{- end }}