Helm named template explodes with 'unsupported value: encountered a cycle via map[string]interface' if called more than once

6/15/2021

I am trying to refactor a Helm chart for a large enterprise application with numerous deployments, services, ingresses etc and am trying to reduce copy-and-paste. Since I've not found any generally accepted DRY design patterns for Helm charts that have enterprise applications in mind, I am simply working from this guide as starting point: https://faun.pub/dry-helm-charts-for-micro-services-db3a1d6ecb80

I would like to define a named template for each high level resource, such as a deployment and call it as needed for each component in the application that I'm deploying. Using a simple ConfigMap as an example, I have come up with the following example that works perfectly as expected if I invoke the named template only once:

{{- define "mergeproblem.configmap" -}}
{{- $ := index . 0 -}}
{{- $name := index . 2 -}}
{{- with index . 1 -}}
{{- $this := dict "Values" (get .Values $name) -}}
{{- $defaultRoot := dict "Values" (omit .Values $name) -}} 
{{- $noValues := omit . "Values" -}} 
{{- with merge $noValues $this $defaultRoot -}}
apiVersion: v1
kind: ConfigMap
metadata:
  name: {{ include "mergeproblem.fullname" . }}-{{ $name }}
  labels:
    {{- include "mergeproblem.labels" . | nindent 4 }}
data:
  MergeProblemExample: |
    Values: {{ .Values | toYaml | nindent 6 }}
    Template: {{ .Template | toYaml | nindent 6 }}
    Chart: {{ .Chart | toYaml | nindent 6 }}
    Release: {{ .Release | toYaml | nindent 6 }}
{{- end -}}
{{- end -}}
{{- end -}}

The idea here is that I can call this named template, giving it the name of the resource I want to create. It will pull the defaults from the top-level .Values but merge in the .Values.$name over the top with a higher precedence so that the template contents can be relatively simple without having to perform individual merges or ternaries on every variable interpolation -- instead the main scoped input will already have the merging done on .Values.

An example of the working output if I make only a single call: https://pastebin.com/ZCJ4LTpV

I am invoking it with the following:

{{- template "mergeproblem.configmap" (list $ . "myfoo") -}}

However if I invoke it more than once like so:

{{- template "mergeproblem.configmap" (list $ . "myfoo") -}}
{{- template "mergeproblem.configmap" (list $ . "yourbar") -}}

I receive the following error from Helm:

$ helm upgrade --install --namespace mergeproblem --debug mergeproblem ./mergeproblem/
history.go:56: [debug] getting history for release mergeproblem
upgrade.go:123: [debug] preparing upgrade for mergeproblem
upgrade.go:131: [debug] performing update for mergeproblem
upgrade.go:303: [debug] creating upgraded release for mergeproblem
Error: UPGRADE FAILED: create: failed to encode release "mergeproblem": json: unsupported value: encountered a cycle via map[string]interface {}
helm.go:81: [debug] json: unsupported value: encountered a cycle via map[string]interface {}
create: failed to encode release "mergeproblem"
helm.sh/helm/v3/pkg/storage/driver.(*Secrets).Create
	/private/tmp/helm-20210414-93729-197z3ms/pkg/storage/driver/secrets.go:156
helm.sh/helm/v3/pkg/storage.(*Storage).Create
	/private/tmp/helm-20210414-93729-197z3ms/pkg/storage/storage.go:69
helm.sh/helm/v3/pkg/action.(*Upgrade).performUpgrade
	/private/tmp/helm-20210414-93729-197z3ms/pkg/action/upgrade.go:304
helm.sh/helm/v3/pkg/action.(*Upgrade).Run
	/private/tmp/helm-20210414-93729-197z3ms/pkg/action/upgrade.go:132
main.newUpgradeCmd.func2
	/private/tmp/helm-20210414-93729-197z3ms/cmd/helm/upgrade.go:155
github.com/spf13/cobra.(*Command).execute
	/Users/brew/Library/Caches/Homebrew/go_mod_cache/pkg/mod/github.com/spf13/cobra@v1.1.1/command.go:850
github.com/spf13/cobra.(*Command).ExecuteC
	/Users/brew/Library/Caches/Homebrew/go_mod_cache/pkg/mod/github.com/spf13/cobra@v1.1.1/command.go:958
github.com/spf13/cobra.(*Command).Execute
	/Users/brew/Library/Caches/Homebrew/go_mod_cache/pkg/mod/github.com/spf13/cobra@v1.1.1/command.go:895
main.main
	/private/tmp/helm-20210414-93729-197z3ms/cmd/helm/helm.go:80
runtime.main
	/usr/local/Cellar/go/1.16.3/libexec/src/runtime/proc.go:225
runtime.goexit
	/usr/local/Cellar/go/1.16.3/libexec/src/runtime/asm_amd64.s:1371
UPGRADE FAILED
main.newUpgradeCmd.func2
	/private/tmp/helm-20210414-93729-197z3ms/cmd/helm/upgrade.go:157
github.com/spf13/cobra.(*Command).execute
	/Users/brew/Library/Caches/Homebrew/go_mod_cache/pkg/mod/github.com/spf13/cobra@v1.1.1/command.go:850
github.com/spf13/cobra.(*Command).ExecuteC
	/Users/brew/Library/Caches/Homebrew/go_mod_cache/pkg/mod/github.com/spf13/cobra@v1.1.1/command.go:958
github.com/spf13/cobra.(*Command).Execute
	/Users/brew/Library/Caches/Homebrew/go_mod_cache/pkg/mod/github.com/spf13/cobra@v1.1.1/command.go:895
main.main
	/private/tmp/helm-20210414-93729-197z3ms/cmd/helm/helm.go:80
runtime.main
	/usr/local/Cellar/go/1.16.3/libexec/src/runtime/proc.go:225
runtime.goexit
	/usr/local/Cellar/go/1.16.3/libexec/src/runtime/asm_amd64.s:1371

My values.yaml looks like this:

########
#
# Component specifics
#

myfoo:
  image:
    repository: REPOSITORY-FROM-MYFOO-BLOCK
    tag: TAG-FROM-MYFOO-BLOCK
  autoscaling:
    minReplicas: 33

yourbar:
  image:
    repository: Repository-From-Yourbar-Block
    tag: Tag-From-Yourbar-Block



########
#
# Example defaults
#

replicaCount: 1

image:
  repository: default-repository-from-top-level-image-block
  pullPolicy: IfNotPresent
  # Overrides the image tag whose default is the chart appVersion.
  tag: "default-tag-from-top-level-image-block"

imagePullSecrets: []
nameOverride: ""
fullnameOverride: ""

autoscaling:
  enabled: false
  minReplicas: 1
  maxReplicas: 100
  targetCPUUtilizationPercentage: 80
  # targetMemoryUtilizationPercentage: 80
-- Nicola Worthington
kubernetes
kubernetes-helm

0 Answers