Search code examples
kuberneteskubernetes-helmgo-templatessprig-template-functions

Passing values to include function inside range, using defaults with merge


Given this deployment.yaml

apiVersion: apps/v1
kind: Deployment
metadata:
  name: deployment
spec:
  revisionHistoryLimit: 5
  template:
    spec:
      containers:
      {{- include "app.container" (merge .Values.app $) | nindent 8 }}
      {{- include "ports" (merge .Values.app ) | nindent 8 }}
      {{- range $k, $v := .Values.extraContainers }}
      {{- $nameDict := dict "name" $k -}}
        {{- include "app.container" (mustMergeOverwrite $.Values.app $nameDict $v)  | nindent 8 }}
        {{- include "ports" (merge $nameDict $v ) | nindent 8 }}
      {{- end }}

This helpers file...

{{/* vim: set filetype=helm: */}}

{{/*
app container base
*/}}
{{- define "app.containerBase" -}}
- name: {{ .name | default "app" }}
  image: {{ .image.name }}:{{ .image.tag }}
  {{- if .command }}
  command:
    {{- range .command }}
    - {{ . | quote }}
    {{- end }}
  {{- end }}
  {{- if .args }}
  args:
    {{- range .args }}
    - {{ . | quote }}
      {{- end }}
  {{- end }}
    {{- range .envVars }}
    - name: {{ .name }}
      {{- with .value }}
      value: {{ . | quote }}
      {{- end }}
      {{- with .valueFrom }}
      valueFrom:
        {{- toYaml . | nindent 8 }}
      {{- end }}
    {{- end }}
  {{- if or .Values.configMaps .Values.secrets .Values.configMapRef }}
  envFrom:
    {{- if .Values.configMaps }}
    - configMapRef:
        name: "{{- include "app.fullname" $ }}"
    {{- end }}
    {{- range .Values.configMapRef }}
    - configMapRef:
        name: {{ . }}
    {{- end }}
    {{- range $name, $idk := $.Values.secrets }}
    - secretRef:
        name: "{{- include "app.fullname" $ }}-{{ $name }}"
    {{- end }}
  {{- end }}
{{- end -}}

{{/*
app container
*/}}
{{- define "app.container" -}}
{{- include "app.containerBase" . }}
  resources:
    limits:
      cpu: {{ .resources.limits.cpu | default "100m" | quote }}
      memory: {{ .resources.limits.memory | default "128Mi" | quote }}
      {{- if .resources.limits.ephemeralStorage }}
      ephemeral-storage: {{ .resources.limits.ephemeralStorage }}
      {{- end }}
    requests:
      cpu: {{ .resources.requests.cpu | default "100m" | quote }}
      memory: {{ .resources.requests.memory | default "128Mi" | quote }}
      {{- if .resources.requests.ephemeralStorage }}
      ephemeral-storage: {{ .resources.requests.ephemeralStorage }}
      {{- end }}
  securityContext:
    runAsNonRoot: true
    runAsUser: {{ .securityContext.runAsUser }}
    runAsGroup: {{ .securityContext.runAsGroup }}
    allowPrivilegeEscalation: false
{{- end -}}

{{/*
ports
*/}}
{{- define "ports" -}}
{{- if .port }}
  ports:
  - containerPort: {{ .port }}
  protocol: {{ .protocol | default "tcp" | upper }}
  {{- range .extraPorts}}
    - containerPort: {{ required ".port is required." .port }}
    {{- if .name }}
      name: {{ .name }}
    {{- end }}
    {{- if .protocol }}
      protocol: {{ .protocol | upper }}
    {{- end }}
  {{- end }}
{{- end }}
{{- end -}}



This values.yaml

extraContainers:
  extra-container2:
    image:
      name: xxx.dkr.ecr.eu-west-1.amazonaws.com/foobar-two
      tag: test
    command:
      - sleep
      - 10
    envVars:
      - name: FOO
        value: BAR
    probes:
      readinessProbe:
        path: /ContainerTwoReadinessPath
      livenessProbe:
        path: /ContainerTwoLivenessPath
    resources:
      limits:
        cpu: 200Mi
        memory: 2Gi
      requests:
        cpu: 200Mi
        memory: 2Gi
  extra-container3:
    image:
      name: xx.dkr.ecr.eu-west-1.amazonaws.com/foobar-three
      tag: latest
    command:
      - sleep
      - 10
    envVars:
      - name: FOO
        value: BAZ
    probes:
      readinessProbe:
        enabled: false
      livenessProbe:
        enabled: false

app:
  image:
    name: xx.dkr.ecr.eu-west-1.amazonaws.com/foobar
    tag: latest
  probes:
    readinessProbe:
      path: /_readiness
      enabled: true
    livenessProbe:
      path: /_liveness
      enabled: true
  port: 100
  resources:
    limits:
      cpu: 100Mi
      memory: 1Gi
    requests:
      cpu: 100Mi
      memory: 1Gi

Why does helm template result in the below:

---
# Source: corp-service/templates/deployment.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
  name: deployment
spec:
  revisionHistoryLimit: 5
  template:
    spec:
      containers:
        - name: app
          image: xx.dkr.ecr.eu-west-1.amazonaws.com/foobar:latest
          resources:
            limits:
              cpu: "100Mi"
              memory: "1Gi"
            requests:
              cpu: "100Mi"
              memory: "1Gi"
          securityContext:
            runAsNonRoot: true
            runAsUser: 2000
            runAsGroup: 2000
            allowPrivilegeEscalation: false
        
          ports:
          - containerPort: 100
          protocol: TCP
        - name: extra-container2
          image: xx.dkr.ecr.eu-west-1.amazonaws.com/foobar-two:test
          command:
            - "sleep"
            - "10"
            - name: FOO
              value: "BAR"
          resources:
            limits:
              cpu: "200Mi"
              memory: "2Gi"
            requests:
              cpu: "200Mi"
              memory: "2Gi"
          securityContext:
            runAsNonRoot: true
            runAsUser: 2000
            runAsGroup: 2000
            allowPrivilegeEscalation: false
        
        - name: extra-container3
          image: xx.dkr.ecr.eu-west-1.amazonaws.com/foobar-three:latest
          command:
            - "sleep"
            - "10"
            - name: FOO
              value: "BAZ"
          resources:
            limits:
              cpu: "200Mi"
              memory: "2Gi"
            requests:
              cpu: "200Mi"
              memory: "2Gi"
          securityContext:
            runAsNonRoot: true
            runAsUser: 2000
            runAsGroup: 2000
            allowPrivilegeEscalation: false

i.e why does extra-container3 have resources: set with values from extra-container2 instead of taking them from the default set of resources found under .values.app.

essentially, i want extracontainers to use default values from .values.app unless i explicitly set them inside the extracontainers section. however it seems that if a previous iteration of the loop over extracontainers defines some values, they are used in the next iteration if they are not overriden.

the resources for extra-container3 i am expecting are:

resources:
    limits:
      cpu: 100Mi
      memory: 1Gi
    requests:
      cpu: 100Mi
      memory: 1Gi

what am i doing wrong?


Solution

  • So there are a couple of things going on here, but mostly the answer is that (mergeMustOverwrite) mutates the $dest map, which causes your range to "remember" the last value it saw, which according to your question isn't the behavior you want. The simplest answer is to use (deepCopy $.Values.app) as the $dest, but there's an asterisk to that due to another bug we'll cover in a second

           {{- $nameDict := dict "name" $k -}}
    -        {{- include "app.container" (mustMergeOverwrite $.Values.app $nameDict $v)  | nindent 8 }}
    +        {{- include "app.container" (mustMergeOverwrite (deepCopy $.Values.app) $nameDict $v)  | nindent 8 }}
             {{- include "ports" (merge $nameDict $v ) | nindent 8 }}
           {{- end }}
    

    You see, (deepCopy $.Values.app) stack-overflows helm because of your inexplicable use of (merge .Values.app $) drags in Chart, Files, Capabilities and the whole world. I'm guessing your _helpers.tpl must have been copy-pasted from somewhere else, which explains the erroneous relative .Values reference inside that define. One way to fix that is to remove the .Values.configMaps, so it will track the .app context like I expect you meant, or you can change the first (merge) to artificially create a "Values": {} item just to keep the template from blowing up when in tries to reference .app.Values.configMaps. The correct one will depend on what you were intending, but (merge .Values.app $) is almost certainly not it

    so, either:

    --- a/templates/_helpers.tpl
    +++ b/templates/_helpers.tpl
    @@ -28,13 +28,13 @@ app container base
             {{- toYaml . | nindent 8 }}
           {{- end }}
         {{- end }}
    -  {{- if or .Values.configMaps .Values.secrets .Values.configMapRef }}
    +  {{- if or .configMaps .secrets .configMapRef }}
       envFrom:
    -    {{- if .Values.configMaps }}
    +    {{- if .configMaps }}
         - configMapRef:
             name: "{{- include "app.fullname" $ }}"
         {{- end }}
    -    {{- range .Values.configMapRef }}
    +    {{- range .configMapRef }}
         - configMapRef:
             name: {{ . }}
         {{- end }}
    

    or

           containers:
    -      {{- include "app.container" (merge .Values.app $) | nindent 8 }}
    +      {{- include "app.container" (merge .Values.app (dict "Values" (dict))) | nindent 8 }}
           {{- include "ports" (merge .Values.app ) | nindent 8 }}