I am trying to merge 2 named templates.
In simplified form it looks like this:
{{- define "common.t1" -}}
a1: "t1-a1"
a2: "t1-a2"
b1:
c1: t1-b1-c1
c2: t1-b1-c2
d1:
- name: container1
e1: t1-d1-e1
e2: t1-d1-e2
- name: container2
{{- end -}}
{{- define "this.t2" -}}
a1: "t2-a1"
a3: "t2-a3"
b1:
c1: t2-b1-c1
c3: t2-b1-c3
d1:
- name: container1
e1: t2-d1-e1
e3: t2-d1-e3
- name: container3
{{- end -}}
{{- $t1 := fromYaml (include "common.t1" .) -}}
{{- $t2 := fromYaml (include "this.t2" .) -}}
{{ toYaml (merge $t2 $t1) }}
The result is
a1: t2-a1
a2: t1-a2
a3: t2-a3
b1:
c1: t2-b1-c1
c2: t1-b1-c2
c3: t2-b1-c3
d1:
- e1: t2-d1-e1
e3: t2-d1-e3
name: container1
- name: container3
the d1 array objects are fully overriden by t2 template, but i want it to be merged with d1 array from the t1.
What i want to get is
a1: t2-a1
a2: t1-a2
a3: t2-a3
b1:
c1: t2-b1-c1
c2: t1-b1-c2
c3: t2-b1-c3
d1:
- e1: t2-d1-e1
e2: t1-d1-e2
e3: t2-d1-e3
name: container1
- name: container2
- name: container3
The purpose of merging is to have templates in common library chart that may be extended by templates in consumer chart and simplify development and maintainance of several dozens of helm charts.
For example put all common kube deployment yaml boilerplate into base template and then just write
{{ magicMerge "common.deployment" "mychart548.deployment" . }}
{{- define "mychart548.deployment" -}}
spec:
template:
spec:
containers:
- name: {{ include "common.names.name" . }}
envFrom:
- configMapRef:
name: {{ include "common.names.envConfigmapName" . }}
env:
- name: POSTGRES_PASSWORD
valueFrom:
secretKeyRef:
name: {{ include "common.names.fullname" . }}-postgress
key: postgres-password
{{- end -}}
I don't think this is possible without writing your own merge function in Helm template code, which will be unpleasant.
At a low level, the Helm merge function actually comes from an extension library called Sprig. The Sprig merge function delegates to another Go library, Mergo. At a code level there happens to be a config options for merging Go slices (lists), but there are only two options: append one to the other, or try to merge each corresponding pair of elements. Sprig, and therefore Helm, uses the latter (default) option, and therefore what I'd expect to see from your example is that, inside d1, in the second list item, in the pair of maps, in the name: key, the different strings container1 and container2 can't be merged.
More abstractly: you're trying to merge two lists by deep-comparing the individual items for equality and taking their union; you don't want to do a deeper merge. That's a little more application-specific knowledge than a generic merge function can have.
If it will be exactly two lists like this, and you're trying to output the result and not process it further (or use the include ... | toYaml trick you have already), then one possible approach is to
Write out the first list as-is
Also convert the first list to a list of serialized strings
For each item in the second list,
Serialize it
Check to see if it's in the first list (serialized)
If not, emit it
{{/*- Convert a list to a list of JSON-serialized strings. */}}
{{- define "list-to-string-list" -}}
[
{{- range $index, $item := . -}}
{{- if ne $index 0 }},{{- end -}}
{{- toJson $item | toJson -}}
{{- end -}}
]
{{- end -}}
{{- define "merge-d1" -}}
{{- $first := index . 0 -}}
{{- $second := index . 1 -}}
{{/*- emit the first list -*/}}
{{- toYaml $first -}}
{{/*- index the first list -*/}}
{{- $firstStrings := include "list-to-string-list" $first | fromJson -}}
{{/*- for each item in the second list... -*/}}
{{- range $second -}}
{{/*- ...if it's not in the indexed list... -*/}}
{{- if not (has (toJson .) $firstStrings) -*/}}
{{/*- ...emit it too -*/}}
- {{ . | toYaml | nindent 2 | trim }}
{{ end -}}
{{- end -}}
{{- end -}}
d1:
{{ include "merge-d1" (list $first.d1 $second.d1) | indent 2 }}
If you're starting to write logic like this, consider whether writing it in a Kubernetes operator in a more mainstream language like Go might be more maintainable long-term.
Related
I have k8s manifest such as below, packaged with Helm.
apiVersion: v1
kind: ServiceAccount
metadata:
{{- template "myFunction" "blah" -}}
I have _helper.tpl defining myFunction as below.
{{- define "myFunction" }}
{{- if .Values.nameOverride }}
name: {{ . }}
{{- else }}
name: "test"
{{- end }}
{{- end }}
Finally, I have values.yaml defining nameOverride: "". From my understanding, as I haven't defined anything for nameOverride, myFunction should output name: "test", and when I define something for nameOverride, the output should be name: "blah". However, I'm getting the error below.
Error: template: product-search/templates/_helpers.tpl:2:16: executing "myFunction" at <.Values.nameOverride>: can't evaluate field Values in type string
helm.go:88: [debug] template: product-search/templates/_helpers.tpl:2:16: executing
"myFunction" at <.Values.nameOverride>: can't evaluate field Values in type string
Any ideas why? Thanks!
Inside the Go text/template language, .Values is a lookup of a field named Values in the special variable .. This variable changes meaning in different context. Inside a define template, . starts off as the value of the parameter to the template, which is not necessarily the top-level Helm object.
{{ template "myFunction" "blah" }}
{{ define "myFunction" }}
{{/* with the call above, . is the string "blah" */}}
{{ end }}
A template only takes one parameter. There are a couple of things you can do here. One approach is to figure out the field value in the caller, and pass that in as the template parameter:
{{-/* Emit a name: label. The parameter is a string. */-}}
{{- define "myFunction" -}}
name: {{ . }}
{{ end -}}
{{-/* if .Values.nameOverride then "blah" else "test" */-}}
{{- $label := ternary "blah" "test" .Values.nameOverride -}}
{{ include "myFunction" $label | indent 4 }}
Another approach could be to pack the two values into a single parameter. I tend to use the Helm list function for this. However, this makes the template logic significantly more complicated.
{{/* Emit a name: label. The parameter is a list of two values,
the top-level Helm object and the alternate label name to use
if .Values.nameOverride is defined. */}}
{{- define "myFunction" -}}
{{/* . is a list of two values; unpack them */}}
{{- $top := index . 0 -}}
{{- $label := index . 1 -}}
{{/* Now we can look up .Values within the passed top-level object */}}
{{- if $top.Values.nameOverride -}}
name: {{ $label }}
{{ else -}}
name: "test"
{{ end -}}
{{- end -}}
{{/* When calling the function manually construct the parameter list */}}
{{ include "myFunction" (list . "blah") | indent 4 }}
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 }}
How do I make an optional block in the values file and then refer to it in the template?
For examples, say I have a values file that looks like the following:
# values.yaml
foo:
bar: "something"
And then I have a helm template that looks like this:
{{ .Values.foo.bar }}
What if I want to make the foo.bar in the values file optional? An error is raised if the foo key does not exist in the values.
I've tried adding as an if conditional. However, this still fails if the foo key is missing:
{{ if .Values.foo.bar }}
{{ .Values.foo.bar }}
{{ end }}
Any thoughts are much appreciated.
Simple workaround
Wrap each nullable level with parentheses ().
{{ ((.Values.foo).bar) }}
Or
{{ if ((.Values.foo).bar) }}
{{ .Values.foo.bar }}
{{ end }}
How does it work?
Helm uses the go text/template and inherits the behaviours from there.
Each pair of parentheses () can be considered a pipeline.
From the doc (https://pkg.go.dev/text/template#hdr-Actions)
It is:
The default textual representation (the same as would be printed by fmt.Print)...
With the behaviour:
If the value of the pipeline is empty, no output is generated... The empty values are false, 0, any nil pointer or interface value, and any array, slice, map, or string of length zero.
As such, by wrapping each nullable level with parentheses, when they are chained, the predecessor nil pointer gracefully generates no output to the successor and so on, achieving the nested nullable fields workaround.
Most charts will default the parent object to a empty map in values.yaml so it always exists.
foo: {}
Then {{ if .Values.foo.bar }} works.
If that's not possible, test both keys:
{{ if .Values.foo }}
{{ if .Values.foo.bar }}
{{ .Values.foo.bar }}
{{ end }}
{{ end }}
Using the and function doesn't work in this case due to and evaluating all parameters, even if the first is falsey.
There is also the hasKey function included from sprig if you ever need to check the existence of a falsey or empty value:
{{ if hasKey .Values.foo "bar" }}
A technique I've used successfully is to use a variable to hold the value of the outer block, which then can use templating constructs like default and Sprig's dict helper.
{{- $foo := .Values.foo | default dict -}}
Bar is {{ $foo.bar | default "not in the values file" }}
This provides a fallback dictionary if foo isn't in the file, so then $foo is always defined and you can look up $foo.bar in it.
Use with
Look at the with operator. This limits the current scope to the level of .Values.foo, and the block is silently ignored if .foo is missing:
{{- with .Values.foo }}
{{- .bar }}
{{- end }}
There is a new function implemented in sprig called dig that just does fix this issue, see here http://masterminds.github.io/sprig/dicts.html .
Is not yet released so even less likely to be in helm soon.
Meanwhile I have modified #Samuel solution to mimic the new dig function.
{{- define "dig" -}}
{{- $mapToCheck := index . "map" -}}
{{- $keyToFind := index . "key" -}}
{{- $default := index . "default" -}}
{{- $keySet := (splitList "." $keyToFind) -}}
{{- $firstKey := first $keySet -}}
{{- if index $mapToCheck $firstKey -}} {{/* The key was found */}}
{{- if eq 1 (len $keySet) -}}{{/* The final key in the set implies we're done */}}
{{- index $mapToCheck $firstKey -}}
{{- else }}{{/* More keys to check, recurse */}}
{{- include "dig" (dict "map" (index $mapToCheck $firstKey) "key" (join "." (rest $keySet)) "default" $default) }}
{{- end }}
{{- else }}{{/* The key was not found */}}
{{- $default -}}
{{- end }}
{{- end }}
and you can call it like this
$regKey := include "dig" (dict "map" .Values "key" "global.registryKey" "default" "")
I searched around for an answer to this same question, and couldn't find anything out there. It seems you have to use a custom function, so I wrote one. Here is what I came up with. It works for my use cases, feedback/improvements are welcome.
_helpers.tpl
{{- define "hasDeepKey" -}}
{{- $mapToCheck := index . "mapToCheck" -}}
{{- $keyToFind := index . "keyToFind" -}}
{{- $keySet := (splitList "." $keyToFind) -}}
{{- $firstKey := first $keySet -}}
{{- if index $mapToCheck $firstKey -}}{{*/ The key was found */}}
{{- if eq 1 (len $keySet) -}}{{*/ The final key in the set implies we're done */}}
true
{{- else }}{{*/ More keys to check, recurse */}}
{{- include "hasDeepKey" (dict "mapToCheck" (index $mapToCheck $firstKey) "keyToFind" (join "." (rest $keySet))) }}
{{- end }}
{{- else }}{{/* The key was not found */}}
false
{{- end }}
{{- end }}
values.yaml:
{{- if eq "true" (include "hasDeepKey" (dict "mapToCheck" .Values "keyToFind" "foo.bar")) }}
bar: {{- .Values.foo.bar }}
{{- end }}