I am using Helm v3 and trying to iterate over a complex object/map in a YAML definition file for Kubernetes network policy with the following content:
values.yaml:
networkPolicies:
egress:
- service: microservice-name
destination:
- podLabels:
app=microservice-name
namespaceLabels:
company.com/microservices: microservice-name
protocol: TCP
ports:
- 8444
At the k8s definition file, I have this code:
egress-networkpolicy.yaml:
{{- range $v, $rule := .Values.networkPolicies.egress }}---
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
name: allow-kong-to-{{ $rule.service }}-egress
namespace: kong
annotations:
description: Kong egress policies
spec:
podSelector:
matchLabels:
{{- range $label,$value := $rule.podSelector }}
{{ $label }}: {{ $value }}
{{- end }}
egress:
- to:
{{- range $from := $rule.to }}
- podSelector:
matchLabels:
{{- range $label,$value := $from.podLabels }}
{{ $label }}: {{ $value }}
{{- end }}
{{- if has $from "namespaceLabels" }}
namespaceSelector:
{{- if eq ( len $from.namespaceLabels ) 0 }} {}
{{- else }}
matchLabels:
{{- range $label,$value := $from.namespaceLabels }}
{{ $label }}: {{ $value }}
{{- end }}
{{- end }}
{{- end }}
{{- end }}
{{- if has $rule "ports" }}
ports:
{{- range $port := $rule.service.ports }}
- protocol: {{ $port.protocol }}
- port: {{ $port }}
{{- end }}
{{- end }}
policyTypes:
- Egress
{{ end -}}
Unfortunately when I run helm template name-of-the-template it throws the following error:
❯ helm template name-of-the-template --debug
install.go:178: [debug] Original chart version: ""ate name-of-the-template --debug
install.go:199: [debug] CHART PATH: /Users/user/charts/name-of-the-template
Error: template: name-of-the-template/templates/egress-networkpolicy.yaml:34:13: executing "name-of-the-template/templates/egress-networkpolicy.yaml" at <has $rule "ports">: error calling has: Cannot find has on type string
helm.go:88: [debug] template: name-of-the-template/templates/egress-networkpolicy.yaml:34:13: executing "name-of-the-template/templates/egress-networkpolicy.yaml" at <has $rule "ports">: error calling has: Cannot find has on type string
I can't find the reason for Helm throwing this error at that line, but not in similar code before that line.
The has template function checks for membership in a list. In this context $rule is a mapping or dictionary (it is one of the items in the list under egress) and for that type you need to use hasKey instead.
{{- if hasKey $rule "ports" }}{{/* hasKey, not has */}}
ports:
...
{{- end }}
One thing that can simplify this slightly is to use the Go template with construct instead of if here. with acts just like if, except it also rebinds the special variable . to the conditional value if it's truthy. Accessing an undefined key in a map is okay but returns nil, which is falsey. So in this context I might write
{{- with $rule.ports }}
ports:
{{- range . }}
- protocol: {{ $rule.protocol }}
port: {{ . }}
{{- end }}
{{- end }}
(But note as I've written it that the . in the third and fifth lines are different variables, and that this usage also changes . as appears at the start of .Values or in template "name" . In this little loop that's not going to be a practical problem.)
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 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'm trying to add ingress to my nginx container.
The following ingress template gives me "parse error (<>/ingress.yaml:71: unexpected EOF)". I went through trying mark possible missing end statements, but even adding arbitrary end at the end of file didn't fix it. I am out of ideas as to what causes this EOF.
So the question is the general: What causes "unexpected EOF" in the file?
{{- if .Values.web.ingress.enabled }}
apiVersion: networking.k8s.io/v1beta1
kind: Ingress
metadata:
name: {{ .Release.Name }}-proxy-ingress
labels:
tier: intelowl
component: proxy
release: {{ .Release.Name }}
chart: {{ .Chart.Name }}
heritage: {{ .Release.Service }}
{{- with .Values.labels }}
{{ toYaml . | indent 4 }}
{{- end }} # {{- with .Values.labels }}
{{- if .Values.web.ingress.annotations }}
annotations:
{{- with .Values.web.ingress.annotations }}
{{ toYaml . | indent 4 }}
{{- end }} # {{- with .Values.web.ingress.annotations }}
{{- end }} # {{- if .Values.web.ingress.annotations }}
spec:
{{- if .Values.web.ingress.tls.enabled }}
tls:
- hosts:
- {{ .Values.web.ingress.host }}
secretName: {{ .Values.web.ingress.tls.secretName }}
{{- end }} # {{- if .Values.web.ingress.tls.enabled }}
rules:
- http:
paths:
{{- range .Values.web.ingress.precedingPaths }}
- path: {{ .path }}
backend:
service:
name: {{ .serviceName }}
port:
number: {{ .servicePort }}
{{- end }} # {{- range .Values.web.ingress.precedingPaths }}
- backend:
service:
name: {{ .Release.Name }}-proxy
port:
number: {{ ternary 443 80 .Values.web.ingress.tls.enabled }}
{{- if .Values.web.ingress.path }}
path: {{ .Values.web.ingress.path }}
{{- end }} # {{- if .Values.web.ingress.path }}
{{- range .Values.web.ingress.succeedingPaths }}
- path: {{ .path }}
backend:
service:
name: {{ .serviceName }}
port:
number: {{ .servicePort }}
{{- end }} # {{- range .Values.web.ingress.succeedingPaths }}
{{- if .Values.web.ingress.host }}
host: {{ .Values.web.ingress.host }}
{{- end }} # {{- if .Values.web.ingress.host }}
{{- end }} # {{- if .Values.web.ingress.enabled }}
Your file is generally structured like so:
{{- if .Values.someCondition }}
...
{{- end }} # {{- if .Values.someCondition }}
However, the Go text/template engine runs before any YAML parsing happens. There is not a comment in this example; there is an if statement, the matching end, and an unterminated if.
The text/template language has its own {{/* comment */}} syntax, and in principle you could use this
{{- if .Values.someCondition }}
...
{{- end }}{{/* if .Values.someCondition */}}
Beyond this, the file you show seems to have the right number of {{ end }}s.
I'd probably avoid this style myself. Usually these condition blocks are fairly short; you could break the template into multiple define named templates if that helps.
metadata:
labels:
tier: intelowl
et: cetera
{{- include "more-labels" . | indent 4 }}
{{- include "ingress-annotations" . | indent 2 }}
{{- define "more-labels" -}}
{{ with .Values.labels }}{{ toYaml . }}{{ end }}
{{- end -}}
{{- define "ingress-annotations" -}}
{{- with .Values.web.ingress.annotations }}
annotations:
{{ toYaml . | indent 2 }}
{{- end -}}
{{- end -}}
Especially for labels you might find that some of the values will be reused in all of your objects, and so including a template in the _helpers.tpl file to produce the common values will reduce some of the repetitiveness.
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 }}
When I deploy the following I get this error:
{{- if .Values.front.ingress.enabled -}}
{{- $fullName := include "marketplace.fullname" . -}}
{{- $ingressPaths := .Values.front.ingress.paths -}}
apiVersion: extensions/v1beta1
kind: Ingress
metadata:
name: {{ $fullName }}-{{ .Values.environment }}-front
labels:
app.kubernetes.io/name: {{ include "marketplace.name" . }}-{{ .Values.front.name }}
helm.sh/chart: {{ include "marketplace.chart" . }}
app.kubernetes.io/instance: {{ .Release.Name }}-{{ .Values.front.name }}
app.kubernetes.io/managed-by: {{ .Release.Service }}
{{- with .Values.front.ingress.annotations }}
annotations:
{{- toYaml . | nindent 4 }}
{{- end }}
spec:
{{- if .Values.front.ingress.tls }}
tls:
{{- range .Values.front.ingress.tls }}
- hosts:
{{- range .hosts }}
- {{ . | quote }}
{{- end }}
secretName: {{ .secretName }}
{{- end }}
{{- end }}
rules:
{{- range .Values.front.ingress.hosts }}
- host: {{ . | quote }}
http:
paths:
{{- range $ingressPaths }}
- path: /
backend:
serviceName: {{ include "marketplace.name" . }}-{{ $.Values.front.name }}
servicePort: 3000
{{- end }}
{{- end }}
{{- end }}
Error:
Error: UPGRADE FAILED: render error in "marketplace/templates/front-ingress.yaml": template: marketplace/templates/front-ingress.yaml:36:30: executing "marketplace/templates/front-ingress.yaml" at <include "marketplace...>: error calling include: template: marketplace/templates/_helpers.tpl:6:18: executing "marketplace.name" at <.Chart.Name>: can't evaluate field Chart in type string
marketplace.name is defined in _helpers.tpl:
{{- define "marketplace.name" -}}
{{- default .Chart.Name .Values.nameOverride | trunc 63 | trimSuffix "-" -}}
{{- end -}}
.Chart.Name is an internal variable and the order of preference is explained here but even setting nameOverride the error is the same.
The strange thing is if I remove this template, .Chart.Name works fine within any other template so I think the problem is related to the scopes of the range used.
Values used:
front:
ingress:
enabled: true
annotations:
kubernetes.io/ingress.class: nginx-int
nginx.ingress.kubernetes.io/rewrite-target: /
nginx.ingress.kubernetes.io/force-ssl-redirect: "false"
paths:
- /
hosts:
- myhost.mydomain.cloud
tls: []
Please see related issue.
Based on this workaround, you can store . in a variable, since inside of range loop, . refers to actual value of paths:
Also you may want to replace - path: / with - path: {{ . }}
{{- if .Values.front.ingress.enabled -}}
{{- $fullName := include "bchart.fullname" . -}}
{{- $ingressPaths := .Values.front.ingress.paths -}}
{{- $dot := . }}
apiVersion: extensions/v1beta1
kind: Ingress
metadata:
...
...
{{- range $ingressPaths }}
- path: {{ . }}
backend:
serviceName: {{ include "bchart.name" $dot }}-{{ $.Values.front.name }}
servicePort: 3000
{{- end }}