helm: Tpl funcion does not work in range

I am pretty new to Helm and not really sure this is indeed an issue or an intended behaviour since I haven’t managed to find enough documentation about the tpl function.

I am trying to use the tpl function in a range over a list of objects like the following one:

anything:
  - name: "Ciao"
    value: "value1"
  - name: "Mondo"
    value: "value2"

I want that for each object a string like {{ .name }} is evaluated giving as result “Ciao” at the first iteration and “Mondo” at the second iteration. I have tried to implement this behaviour as follows:

{{- range .Values.anything }}
  trigger: {{ tpl "{{ .name }}" . }}
{{- end }}

Helm is not able to render the template and I get the following error: Error: render error in "tpl-bug/templates/configmap.yaml": template: tpl-bug/templates/configmap.yaml:5:17: executing "tpl-bug/templates/configmap.yaml" at <tpl "{{ .name }}" .>: error calling tpl: Cannot retrieve Template.Basepath from values inside tpl function: {{ .name }} (BasePath is not a value)

I have developed a minimal chart that reproduces the issue: tpl-bug.zip

To me it looks like tpl always requires the root scope $ in order to be able to find Template.BasePath, at least this is what I was able to understand from the code in engine.go:

basePath, err := vals.PathValue("Template.BasePath")
		if err != nil {
			return "", fmt.Errorf("Cannot retrieve Template.Basepath from values inside tpl function: %s (%s)", tpl, err.Error())
}

Is this the intended behaviour? If yes how could I achieve the same result anyway? I have simplified a lot my real scenario, in my real case the string “{{ .name }}” comes from an external resource.

Output of helm version:

Client: &version.Version{SemVer:"v2.14.1", GitCommit:"5270352a09c7e8b6e8c9593002a73535276507c0", GitTreeState:"clean"} Server: &version.Version{SemVer:"v2.14.1", GitCommit:"5270352a09c7e8b6e8c9593002a73535276507c0", GitTreeState:"clean"}

Output of kubectl version:

Client Version: version.Info{Major:"1", Minor:"15", GitVersion:"v1.15.0", GitCommit:"e8462b5b5dc2584fdcd18e6bcfe9f1e4d970a529", GitTreeState:"clean", BuildDate:"2019-06-19T16:40:16Z", GoVersion:"go1.12.5", Compiler:"gc", Platform:"linux/amd64"} Server Version: version.Info{Major:"1", Minor:"13", GitVersion:"v1.13.5", GitCommit:"2166946f41b36dea2c4626f90a77706f426cdea2", GitTreeState:"clean", BuildDate:"2019-03-25T15:19:22Z", GoVersion:"go1.11.5", Compiler:"gc", Platform:"linux/amd64"}

Cloud Provider/Platform (AKS, GKE, Minikube etc.): Microsoft Azure

About this issue

  • Original URL
  • State: closed
  • Created 5 years ago
  • Reactions: 5
  • Comments: 15 (1 by maintainers)

Most upvoted comments

I spend some time to find a workaround for this, so, this might be useful for someone:

{{- range $key, $value := .Values.list }}
{{- $_ := set $ "key" $key }}
{{- $_ := set $ "value" $value }}
{{- tpl ($.Files.Get "sub.yaml") $ }}
{{- end }}

Inside the nested template the global context of the parent template is accessible as usual through dot, the range variables are accessible like .key, .value.

To use the tpl funciton inside a range loop , i m passing $ (global ctx) instead of .

{{ range .Values.list }} {{ tpl .item.template $ }} {{ end }}

For anyone who stumbles upon this issues, one solution is to pass the Template object into the tpl function alongside your other values via dict.

See this implementation:

kind: ConfigMap
apiVersion: v1
metadata:
    name: {{ include "config-map-name" . | quote }}
    labels:
        {{- include "config-map-labels" . | nindent 8 }}
    annotations:
        {{- /*
        Use checksums to track config template/value changes.
        If the checksum of this file is then used elsewhere (ie by a deployment) when config template/values changes,
        this files checksum changes which would cause the consumer to update itself to take in these changes.
        */}}
        checksum/config-nginx-server.conf: {{ $.Files.Get (print .Template.BasePath "config/nginx/server.conf") | sha256sum | quote }}
        checksum/values-sites: {{ .Values.sites | toString | sha256sum | quote }}
data:
    {{- range $siteName, $siteConfig := .Values.sites }}
    {{ $siteName }}.conf: |-
        {{- tpl ($.Files.Get "config/nginx/server.conf") (dict "Values" $siteConfig "Template" $.Template) | nindent 8 }}
    {{- end }}

@bacongobbler this seems like the ‘cleanest’ approach, i wonder if it’s worth noting in documentation?

At runtime, the rendering engine needs to know the path of the original template as well as the name of the template, both of which cannot be inferred ahead of time. tpl checks those values in order to know which template it is modifying. Looking at the code, that seems like the intended behaviour. The issue here is the range function: when inside a range function, the scope of dot (.) is limited to the scope of the range. In other words, your call to tpl is the equivalent of

{{ tpl "{{ .name }}" .Values.anything }}

Because the tpl function relies on objects that are in the root scope of the dot (.) value (like .Template), the rendering fails. Replacing . with $ as you pointed out should allow the tpl function to work.

If you can determine another way to find out which template is calling tpl, we’d happily accept a PR.

@chudytom that should work find yes.

Obviously the structure of the dict depends on how the template used in the tpl function expects its values. This structure can be anything and the Values key is not necessarily required.

For example, if the template looks for values at: .someValue .anotherValue, your tpl call could look like

{{ tpl "file.yaml" (dict "someValue" $someValue "anotherValue" $anotherValue "Template" $.Template) }}

Another example, if the template looks for values at .data.someValue .data.AnotherValue, your tpl call could look like

{{ $data := dict "someValue" "foo" "anotherValue" "bar" }}
{{ tpl "file.yaml" (dict "data" $data "Template" $.Template) }}

As long as you pass through the root .Template object it should all work fine.

Hope that clears things up.

@zffocussss $ is a reference to the global scope. For example, these two lines mean the same (if they’re located in the top scope):

{{ .Values.list }}
{{ $.Values.list }}

Now let’s take a look at the following values.yaml:

foo: bar
list:
- Values:
    foo: 1
- Values:
    foo: 2
{{ .Values.foo }} returns "bar"
{{ $.Values.foo }} also returns "bar"
{{ range .Values.list }}
    {{ .Values.foo }} returns 1 and then 2
    {{ $.Values.foo }} returns bar
{{ end }}

I seem to be running into the same issue here and cannot figure out how to apply @Zebradil workaround values.yaml:

 app_role:
  - app: test
    namespace: test-namespace
    roles:
      test-admin:
        rules: |
          resources:
            - "*"
          verbs:
            - "*"
      test-analytics:
        rules: |
          resources:
            - "pods"
            - "configmaps"
          verbs:
            - "get"
            - "list"
  - app: test2
    namespace: test2-namespace
    roles:
      test2-admin:
        rules: |
          resources:
            - "*"
          verbs:
            - "*"

templates/roles.yaml

{{- range .Values.app_role }}
{{- template "roles" . }}
{{- end }}

templates/_helpers.tpl

{{- define "roles" }}
{{- range $key, $value := .roles }}
---
kind: Role
apiVersion: rbac.authorization.k8s.io/v1
metadata:
  namespace: {{ $.namespace }}
  name: {{ $key }}
rules:
- apiGroups: [""]
  {{ tpl .rules . | indent 4 }}
  {{- end }}
{{- end -}}

@wmiller112 did you figure it out?