We use helm charts to deploy to kubernetes and helm to generate those charts. Unfortunately I'm not familiar with helm or helm templates (and only moderately familiar with kubernetes) so in asking the question below I may use incorrect terminology (in fact I may have done already in this paragraph) so please bear with me as I get up to speed.
I have a helm template, foo.yaml, that resembles the following:
{{- define "env.variables" }}
echo "--- Setting env variables ---"
export foo={{ .Values.global_vars.foo }}
{{- end }}
apiVersion: v1
kind: Secret
type: Opaque
metadata:
name: "{{ .Values.global_vars.bar }}-set-env-vars"
data:
set-env-vars.sh: {{ include "env.variables" . | b64enc }}
As you can see it defines a script that creates some environment variables. I am working on a requirement to only create those variables where some condition is true (.Values.global_vars.baz == 1)
I suppose I could achieve this by doing something like:
{{- define "env.variables" }}
if [ {{ .Values.global_vars.baz }} = 1 ]
then
echo "--- Setting env variables ---"
export foo={{ .Values.global_vars.foo }}
fi
{{- end }}
apiVersion: v1
kind: Secret
type: Opaque
metadata:
name: "{{ .Values.global_vars.bar }}-set-env-vars"
data:
set-env-vars.sh: {{ include "env.variables" . | b64enc }}
but that doesn't feel like a very elegant way of doing it. Can I put a conditional expression into data that only includes the script where the condition is met. Something like this:
{{- define "env.variables" }}
echo "--- Setting env variables ---"
export foo={{ .Values.global_vars.foo }}
{{- end }}
{{- define "no.env.variables" }}
echo "--- No env variables to set ---"
{{- end }}
apiVersion: v1
kind: Secret
type: Opaque
metadata:
name: "{{ .Values.global_vars.bar }}-set-env-vars"
data:
set-env-vars.sh: {{ if {{ .Values.global_vars.bar }} = 1 then include "env.variables" else include "no.env.variables" . | b64enc }}
Hope that makes sense. This is literally my first ever excursion into helm so am looking for some noob guidance.
The fancy {{ }} things in the helm files are GO templates. They have several built-in features, one of them are control structures, like if-else. They allow you to render part of the template conditionally.
You can add a condition to the env.variables named template:
{{ define "env.variables" }}
{{ if eq .Values.global_vars.baz "1" }}
echo "--- Setting env variables ---"
export foo={{ .Values.global_vars.foo }}
{{ else }}
echo "--- No env variables to set ---"
{{ end }}
{{ end }}
data:
set-env-vars.sh: {{ include "env.variables" . | b64enc }}
or to the set-env-vars.sh value:
{{- define "env.variables" }}
echo "--- Setting env variables ---"
export foo={{ .Values.global_vars.foo }}
{{- end }}
{{- define "no.env.variables" }}
echo "--- No env variables to set ---"
{{- end }}
data:
set-env-vars.sh: {{ if eq .Values.global_vars.baz "1" }}
{{- include "env.variables" . | b64enc }}
{{- else }}
{{- include "env.variables" . | b64enc }}
{{- end }}
The effect would be the same.
One confusing thing may be the if eq .Values.global_vars.baz "1" syntax. The eq is a function, and the next values are its arguments (GO templates do not have equals operator).
Related
I need to reduce my 'if else code' in my helm chart template
How can I do that ?.
{{- if .Values.global }}
{{- if .Values.global.namespace }}
namespace: {{ .Values.global.namespace }}
{{- else }}
namespace: {{ .Values.namespace }}
{{- end }}
{{- else }}
namespace: {{ .Values.namespace }}
{{- end}}
name: {{.Values.name}}
You could use a variable and also {{with}} (which sets the dot), e.g.:
{{- $ns := .Values.namespace -}}
{{- with .Values.global }}{{ with.namespace }}{{ $ns = . }}{{end}{{ end -}}
namespace: {{ $ns }}
name: {{.Values.name}}
"If x is truthy, then use its value, otherwise use y" is what the Helm (Sprig) default function does. You could replace the inner conditional with
namespace: {{ .Values.global.namespace | default .Values.namespace }}
The outer conditional is trickier. The problem you're trying to work around here is, if .Values.global isn't defined, it will evaluate to nil, and then .Values.global.namespace is an error. The usual approach I use here is to again use default to get an empty dictionary if it isn't defined, at which point you can successfully do a lookup.
So you should be able to replace the entire block with
{{- $global := .Values.global | default dict }}
namespace: {{ $global.namespace | default .Values.namespace }}
I am wondering if there is a more efficient way to exclude any yaml keys which do not have a value set.
My current approach is to wrap each key in an if statement...
container:
spec:
{{- if values.spec.x }}
x: {{ values.spec.x }}
{{- end}}
{{- if values.spec.y }}
y: {{ values.spec.y }}
{{- end}}
{{- if values.spec.z }}
z: {{ values.spec.z }}
{{- end}}
e.g.
for each child of container.spec:
if the value != null:
include as child of spec
else:
exclude from spec
I thought about wrapping the above in a _helper.tpl function to try to keep the main template tidy, but it would still include writing multiple if statements.
Is there a better way of doing the above?
Thanks!
You can directly translate that pseudocode into Helm chart logic. The trick is that a Go template range loop is basically equivalent to a "for" loop in most languages. So:
container:
spec:
{{- range $key, $value := .Values.spec }}
{{- if ne $value nil }}
{{ $key }}: {{ $value }}
{{- end }}
{{- end }}
If you can just omit the unused keys from the values, then this becomes simpler and safer. Helm includes a lightly-documented toYaml function that will render an arbitrary structure as YAML, but you can't really do any filtering or other preprocessing before writing it out.
container:
spec:
{{ .Values.spec | toYaml | indent 4 }}
Vault is integrated with K8s cluster as side car and this cluster is deployed by using helm chart.
As a part of helm chart below is code :
vault.hashicorp.com/agent-inject-template-dbsecret: |
{{`{{- with secret "path" -}}
export USER="{{ .Data.data.user }}"
export PASSWORD="{{ .Data.data.password }}"
{{- end }}`}}
Now it is required to use same helm chart for different environment and per environment path can be different so planning to store it in values file and use it here.
Tried with {{ .Values.secretPath }} but value is not getting populated.
vault.hashicorp.com/agent-inject-template-dbsecret: |
{{`{{- with secret "{{ .Values.secretPath }}" -}}
export USER="{{ .Data.data.user }}"
export PASSWORD="{{ .Data.data.password }}"
{{- end }}`}}
I would like to know how can i keep this path values as dynamic and pass it from values file so same chart can be used in different environment.
Thanks in advance.
Here's what's going on with the template: that annotation's value is itself a Go text/template template – Vault is using the same underlying templating engine as Helm – and so wrapping the template text in curly braces and backticks causes the template text itself to be written out.
# looks up "some" in `.`, then looks up "expression" in that
{{ .some.expression }}
# the string "{{ .some.string }}"
{{`{{ .some.string }}`}}
There are other syntaxes to include double curly braces in the output, beyond quoting the entire string, for example
# also outputs "{{ .some.string }}"
{{ "{{" }} .some.string }}
which starts with a template expression outputting a double open curly brace, and then the rest is text.
That means you can combine this with other template expressions; for example
# also outputs "{{ .some.string }}"
{{ $variable := ".some.string" -}}
{{ "{{" }} {{ $variable }} }}
Similar but longer and possibly easier to read,
# also outputs "{{ .some.string }}"
{{ $open := "{{" -}}
{{ $variable := ".some.string" -}}
{{ $close := "}}" -}}
{{ $open }} {{ $variable }} {{ $close }}
You can combine this technique with your original template to include double curly braces in the output, but use a Helm template expression to reference the Helm .Values structure.
vault.hashicorp.com/agent-inject-template-dbsecret: |
{{ "{{" }}- with secret "{{ .Values.secretPath }}" -}}
export USER="{{ "{{" }} .Data.data.user }}"
export PASSWORD="{{ "{{" }} .Data.data.password }}"
{{ "{{" }}- end }}
This form converts every {{ expected in the output to {{ "{{" }}, and then leaves the Helm-level expression {{ .Values.secretPath }} as-is.
I a have a couple of charts for different products. In the first one a helper was written to build out the repo, image name and tag/version. This works but as the other Chart is quite different I've gone through a simpler approach but it does not work. I get the error,
error calling include: template: MYChart/templates/_helpers.tpl:94:28: executing "getImageName" at <.Values.registryName>: nil pointer evaluating interface {}.registryName
This is the helper.
{{/*
This allows us to not have image: .Values.xxxx.ssss/.Values.xxx.xxx:.Values.ssss
in every single template.
*/}}
{{- define "imageName" -}}
{{- $registryName := .Values.registryName -}}
{{- $imageName := .Values.imageName -}}
{{- $tag := .Chart.AppVersion -}}
{{- printf "%s/%s:%s" $registryName $imageName $tag -}}
{{- end -}}
These are the values
registry:
registryName: "index.docker.io/myrepo"
image_Name: "myimage"
Calling a value like the above in a _helper.tpl should work, there are plenty of examples that use this approach. What am I missing?
The template file :
{{- $root := . -}}
{{- $FullChartName := include "myapp.fullname" . -}}
{{- $ChartName := include "myapp.name" . -}}
{{- range $worker, $parameter := .Values.workerPods }}
---
apiVersion: apps/v1
kind: Deployment
metadata:
name: {{ $parameter.name }}-worker
spec:
replicas: {{ $parameter.replicas }}
selector:
matchLabels:
app.kubernetes.io/name: {{ $parameter.name }}-worker
app.kubernetes.io/instance: {{ $root.Release.Name }}
template:
metadata:
labels:
app.kubernetes.io/name: {{ $parameter.name }}-worker
app.kubernetes.io/instance: {{ $root.Release.Name }}
autoscale: "true"
annotations:
{{- if $root.Values.worker.annotations }}
{{ toYaml $root.Values.worker.annotations | indent 8 }}
{{- end }}
spec:
imagePullSecrets:
- name: myapp-registry-credentials
containers:
- name: {{ $parameter.name }}-worker
image: {{ template "imageName" . }}
imagePullPolicy: {{ $root.Values.worker.image.pullPolicy }}
command: ["/bin/sh"]
args: ["-c", "until /usr/bin/pg_isready -h $DATABASE_HOST; do sleep 2; done; bundle exec rake jobs:work"]
{{- range $container, $containerResources := $root.Values.containers }}
{{- if eq $container $parameter.size }}
resources:
{{- toYaml $containerResources.resources | nindent 12 }}
{{- end }}
{{- end }}
envFrom:
- configMapRef:
name: common-env
- secretRef:
name: myapp-secrets
volumeMounts:
- name: mnt-data
mountPath: "/mnt/data"
volumes:
- name: mnt-data
persistentVolumeClaim:
claimName: myapp-pvc
{{- with $root.Values.nodeSelector }}
nodeSelector:
{{- toYaml . | nindent 8 }}
{{- end }}
{{- with $root.Values.affinity }}
affinity:
{{- toYaml . | nindent 8 }}
{{- end }}
{{- with $root.Values.tolerations }}
tolerations:
{{- toYaml . | nindent 8 }}
{{- end }}
{{- end }}
I also tried this approach and added the following to the Chart.yaml but got a similar error, I'll be honest and wasn't sure this would even work but would be interested to hear other's thoughts.
annotations:
image: "myimage"
registry: "index.docker.io/myrepo"
And the helper looked like this.
{{/*
This allows us to not have image: .Values.xxxx.ssss/.Values.xxx.xxx:.Values.ssss
in every single template.
*/}}
{{- define "imageName" -}}
{{- $registryName := .Chart.Annotations.registry -}}
{{- $imageName := .Chart.Annotations.image -}}
{{- $tag := .Chart.AppVersion -}}
{{- printf "%s/%s:%s" $registryName $imageName $tag -}}
{{- end -}}
You're calling the template with the wrong parameter. Reducing the Helm template file to the bare minimum to demonstrate this:
{{- $root := . -}}
{{- range $worker, $parameter := .Values.workerPods }}
image: {{ template "imageName" . }}
imagePullPolicy: {{ $root.Values.worker.image.pullPolicy }}
{{- end }}
The standard Go text/template range statement rebinds the . variable (I believe to the same thing as $parameter). So then when you call the imageName template, its parameter isn't the Helm root value but rather the block from the values file; .Values is undefined and returns nil; and then .Values.registryName is a lookup on nil which produces the error you see.
One standard workaround to this is to save . to a variable outside the range loop and use that variable everywhere you would have used .. And in fact you already do this, the $root.Values.worker... reference in the following line should work correctly. You just need to change this at the point of call:
image: {{ template "imageName" $root }}
I currently have a helm template for a deployment defined as
apiVersion: apps/v1
kind: Deployment
metadata:
name: demo
labels:
{{- include "demo.labels" . | nindent 4 }}
app.kubernetes.io/component: "server"
spec:
replicas: {{ .Values.replicaCount }}
selector:
matchLabels:
app.kubernetes.io/name: demo
app.kubernetes.io/instance: {{ .Release.Name }}
app.kubernetes.io/component: "server"
template:
metadata:
{{- with .Values.deployment.annotations }}
annotations:
{{- toYaml . | nindent 8 }}
{{- end }}
For the annotations, it works fine as we can pass in annotations from our values.yml. However, now I also want to add in a set of vault annotations that have predefined values into the template:
{{- if .Values.vault.enabled -}}
vault.hashicorp.com/agent-inject: {{ .Values.vault.enabled | quote }}
vault.hashicorp.com/agent-cache-enable: "true"
vault.hashicorp.com/agent-cache-use-auto-auth-token: "force"
vault.hashicorp.com/role: {{ .Values.vault.role | quote }}
vault.hashicorp.com/ca-cert: "/run/secrets/kubernetes.io/serviceaccount/ca.crt"
vault.hashicorp.com/agent-init-first: "true"
traffic.sidecar.istio.io/excludeOutboundPorts: "8200"
{{- $systemcontext := .Values.vault.systemcontext -}}
{{- $releasename := .Release.Name -}}
{{- range .Values.vault.secretkeys}}
{{- $secretpath := printf "kv/%s/restricted/%s/%s" $systemcontext $releasename . }}
{{- $annotatefilename := printf "vault.hashicorp.com/agent-inject-secret-%s.yaml" . }}
{{ $annotatefilename }}: {{ $secretpath }}
{{ $annotatefilename }}: |
{{ printf "%s%s%s" `{{- with secret ` ($secretpath | quote) ` -}}{{ range $k, $v := .Data.data }}{{ $k }}: {{ $v }}
{{ end }}{{- end -}}`}}
{{- end -}}
How can I define the template so that it can render both sets of annotations, even if vault.enabled=false, or deployment.annotations is of empty value?
Example our values.yml:
deployment:
annotations:
test-annotation: "hello world"
test2-annotations: "foo"
vault:
enabled: true
role: myrole
systemcontext: "foo"
Thanks
You can define the additional set of annotations as a named template, that emits key: value pairs, aligned at the first column.
{{- define "annotations.vault" }}
{{- if .Values.vault.enabled -}}
vault.hashicorp.com/agent-inject: {{ .Values.vault.enabled | quote }}
...
{{ end -}}
{{ end -}}
Then when you need to use it, you can use the Helm include extension to invoke it. This returns a string, and so you can combine it with indent to indent it appropriately.
Your original template code uses with to skip the annotations: block entirely if nothing is present, so you can use this same technique at the top level. (You need to be careful that the template emits nothing, not even a new line, if the control is disabled.)
metadata:
labels: { as: above }
{{- with include "annotations.vault . }}
{{- . | indent 4 }}
{{- end }}
Inside the pod spec, there are two possibly places the annotations could come from. The easiest way to create a syntactically valid annotations: block is to include an artificial key: value pair:
spec:
template:
metadata:
annotations:
_: '' # meaningless, but forces a YAML dictionary
{{- with .Values.deployment.annotations }}
{{- toYaml . | indent 8 }}
{{- end }}
{{- with include "annotations.vault" . }}
{{- indent 8 . }}
{{- end }}
Or, you could capture both annotation sets into variables, and do logic based on that.
spec:
template:
metadata:
{{- $a := .Values.deployment.annotations }}
{{/* if $a then (toYaml $a) else "" end */}}
{{- $manual := $a | ternary (toYaml $a) "" }}
{{- $vault := include "annotations.vault" . }}
{{- $annotations := printf "%s%s" $manual $vault }}
{{- with $annotations }}
annotations: {{- nindent 8 . }}
{{- end }}