TLDR
To reference global variables within a range
block, prefix them with $
which always points to the root (global) context, e.g. $.Values.ingress.ports
.
Overview
Recently I ran into an annoying problem whilst trying to fix a helm template, which left me stumped for a while. I was trying to reference a global variable in a range
loop and running into an error:
nil pointer evaluating interface {}
I was modifying the following Helm template to create an istio gateway. The template defines a load balancer that proxies connections to downstream servers.
The template loops through an array of ports defined in the .Values.service.ports
global variable, and defines a port
for each of them, it then defines a single hosts
entry:
apiVersion: networking.istio.io/v1alpha3
kind: Gateway
spec:
servers:
{{- range $name, $config := .Values.service.ports }}
- port:
number: {{ $config.ingress.port }}
name: {{ $config.ingress.name }}
protocol: {{ $config.ingress.protocol }}
{{- end }}
hosts:
{{- range .Values.ingress.hosts }}
- {{ include "service.host" . }}
{{- end }}
I wanted to change it so that for each port, the hosts
entry is repeated. To do this I did the obvious which was to modify the template to move the hosts
entry inside the ports range
loop:
apiVersion: networking.istio.io/v1alpha3
kind: Gateway
spec:
servers:
{{- range $name, $config := .Values.service.ports }}
- port:
number: {{ $config.ingress.port }}
name: {{ $config.ingress.name }}
protocol: {{ $config.ingress.protocol }}
hosts:
{{- range .Values.ingress.hosts }}
- {{ include "service.host" . }}
{{- end }}
{{- end }}
Seems simple right?
However this started throwing an error which left me stumped for an upsettingly long time:
Error: template: app/templates/routing/gateway.yaml:21:23:
executing "app/templates/routing/gateway.yaml" at <.Values.ingress.hosts>:
nil pointer evaluating interface {}.ingress
I couldn’t understand why the .Values.ingress.hosts
was valid outside of the range loop but invalid inside it.
The Solution - Helm Variable Scoping
This came down to understanding variable scoping which is kind of explained here but not for this specific error:
Variables in Helm templates are scoped to the block they are in.
When the .Values.ingress.hosts
was outside of the range
block, .Values
was valid, because
.Values
is defined in the root/global scope and that is where it is being accessed.
However once .Values.ingress.hosts
was moved into the hosts
range block, there is no .Values
defined in the local scope in the block.
To fix this, .Values
must be prefixed with $
which always points to the root (global) context so it can be accessed with a range
block:
apiVersion: networking.istio.io/v1alpha3
kind: Gateway
spec:
servers:
{{- range $name, $config := .Values.service.ports }}
- port:
number: {{ $config.ingress.port }}
name: {{ $config.ingress.name }}
protocol: {{ $config.ingress.protocol }}
hosts:
{{- range $.Values.ingress.hosts }}
- {{ include "service.host" . }}
{{- end }}
{{- end }}