Helm, a very popular way of writing kubernetes charts, but why oh why are we templating yaml, whitespace sensitive language?
While the better way of doing it is to generate manifests with a sane language, it might not always be an option, and we're stuck with Helm for the time being. So maybe we can make our lives a little bit easier?
This is what you see in the wild.
Fragments of yaml in define
blocks,
partial yaml that you want to write out,
littered with include .... | nindent
to include fragments at the right indent level.
{{- define "app.name" -}}
my-app
{{- end }}
{{- define "app.labels" }}
{{ include "app.selectorLabels" . }}
example.com/owner: me
{{- end }}
{{- define "app.selectorLabels" }}
app.kubernetes.io/name: my-app
app.kubernetes.io/instance: {{ .Release.Name }}
{{- end }}
---
apiVersion: apps/v1
kind: Deployment
metadata:
name: {{ include "app.name" . }}
labels:
{{- include "app.labels" . | nindent 4 }}
spec:
selector:
matchLabels:
{{- include "app.selectorLabels" . | nindent 6 }}
template:
metadata:
labels:
{{- include "app.labels" . | nindent 8 }}
spec:
containers:
- name: {{ include "app.name" . }}
image: {{ .Values.image.respository }}:{{ .Values.image.tag }}
Say you want to provide a helper to generate a sidecar, you might write something like:
{{- define "sidecar" }}
name: "my-sidecar"
image: "example.com/sidecar:latest"
{{- end }}
Your instructions to users would be something like:
add
- {{- include "sidecar" . | nindent 10 }}
after deployment spec.containers
But of course this will fail when people use different list indent styles:
1list1:
2- item1
3- item2
4list2:
5 - item1
6 - item2
Which may result in errors like:
Error: YAML parse error on my-app/templates/deployment-old.yaml: error converting YAML to JSON: yaml: line 28: found character that cannot start any token
and you can't run the templates through a formatter to make them consistent.
What if instead of templating out yaml, we create a native object instead, and at the end, we marshal it into a yaml document?
{{ $appName := "my-app" }}
{{ $appSelectorLabels := dict
"app.kubernetes.io/name" "my-app"
"app.kubernetes.io/instance" .Release.Name
}}
{{ $appLabels := merge (dict
"example.com" "me"
)
$appSelectorLabels
}}
---
{{ $deploy := dict
"apiVersion" "apps/v1"
"kind" "Deployment"
"metadata" (dict
"name" $appName
"labels" $appLabels
)
"spec" (dict
"selector" (dict
"matchLabels" $appSelectorLabels
)
"template" (dict
"metadata" (dict
"labels" $appLabels
)
"spec" (dict
"containers" (list
(dict
"name" $appName
"image" (printf "%s:%s" .Values.image.repository .Values.image.tag)
)
)
)
)
)
}}
{{- toYaml $deploy -}}
Now when we want to have a helper that adds a sidecar, we can instead write a function like the following (Note that maps/dicts in Go/Helm templates passed by reference and modified in place):
{{ define "add-sidecar" }}
{{ $sidecar := dict
"name" "my-sidecar"
"image" "example.com/my-sidecar:latest"
}}
{{ $containers := concat (list $sidecar) .spec.template.spec.containers }}
{{ $_ := set .spec.template.spec "containers" $containers }}
{{- end }}
And usage instructions can be:
add
{{ include "add-sidecar $deploy }}
before{{ toYaml $deploy }}
Now there's no possibility of whitespace errors.
As a bonus, if you have values you want to pass in,
you can do {{ mergeOverrite $deploy .Values.deploymentOverrides }}