Helm: Helm should preform deep merge on multiple values files

Created on 9 Feb 2018  ·  31Comments  ·  Source: helm/helm

It looks like #1620 was supposed to provide this functionality but I am seeing that a list in the last specified values file completely override the list from the previous file

I have 2 additional values files foo.yaml, and bar.yaml as follows:

foo.yaml

cronjob:
  fileName: foo_job
  schedule: "0 11 * * *"
  env:
    - name: FOO
      value: bar

bar.yaml

cronjob:
  env:
  - name: BAR
    value: foo

cronjob.yaml template

apiVersion: batch/v1beta1
kind: CronJob
metadata:
  name: {{ template "cronjob.fullname" . }}
  labels:
    app: {{ template "cronjob.name" . }}
    chart: {{ .Chart.Name }}-{{ .Chart.Version | replace "+" "_" }}
    release: {{ .Release.Name }}
    heritage: {{ .Release.Service }}
spec:
  schedule: {{ .Values.cronjob.schedule | quote }}
  startingDeadlineSeconds: {{ .Values.cronjob.startingDeadlineSeconds }}
  jobTemplate:
    spec:
      template:
        metadata:
          name: {{ template "cronjob.fullname" . }}
        spec:
          restartPolicy: Never
          imagePullSecrets:
          - name: quay-sts
          containers:
          - name: {{ template "cronjob.fullname" . }}
            imagePullPolicy: Always
            image: "{{ .Values.image.repository }}:{{ .Values.image.tag }}"
            args:
            - npm
            - start
            - {{ .Values.cronjob.fileName }}
{{ if .Values.cronjob.env }}
            env:
{{ toYaml .Values.cronjob.env | indent 12 }}
{{ end }}

When running helm install --dry-run --debug ./cronjob -f values/foo.yaml,values/bar.yaml or when running helm install --dry-run --debug ./cronjob -f values/foo.yaml -f values/bar.yaml

It outputs the following:

actual output

apiVersion: batch/v1beta1
kind: CronJob
metadata:
  name: intended-jaguar-cronjob
  labels:
    app: cronjob
    chart: cronjob-2.0.8
    release: intended-jaguar
    heritage: Tiller
spec:
  schedule: "0 11 * * *"
  startingDeadlineSeconds: 60
  jobTemplate:
    spec:
      template:
        metadata:
          name: intended-jaguar-cronjob
        spec:
          restartPolicy: Never
          imagePullSecrets:
          - name: quay-sts
          containers:
          - name: intended-jaguar-cronjob
            imagePullPolicy: Always
            image: XXXXXXXX
            args:
            - npm
            - start
            - foo_job

            env:
            - name: BAR
              value: foo

I expect the env lists from values.yaml, foo.yaml, and bar.yaml to have been merged resulting in the following:

expected output

apiVersion: batch/v1beta1
kind: CronJob
metadata:
  name: intended-jaguar-cronjob
  labels:
    app: cronjob
    chart: cronjob-2.0.8
    release: intended-jaguar
    heritage: Tiller
spec:
  schedule: "0 11 * * *"
  startingDeadlineSeconds: 60
  jobTemplate:
    spec:
      template:
        metadata:
          name: intended-jaguar-cronjob
        spec:
          restartPolicy: Never
          imagePullSecrets:
          - name: quay-sts
          containers:
          - name: intended-jaguar-cronjob
            imagePullPolicy: Always
            image: XXXXXXXX
            args:
            - npm
            - start
            - foo_job

            env:
            # anything from the env list in values.yaml
            # ...
            - name: FOO
              value: bar
            - name: BAR
              value: foo
questiosupport

Most helpful comment

We could also use this. +1

All 31 comments

I think i found the issue and ill try to open a PR

If this is implemented, it should be optional, i. e. the merge mode should be specified as a command-line flag. Anything else would be a breaking change and, I think, in most cases not desirable. The problem is that you would not be able to override defaults for lists.

Here's an example: https://github.com/kubernetes/charts/blob/5453fc144f6f9d722c6bb433790f90d7943a4fc3/stable/docker-registry/values.yaml#L19

For those coming from the Java world, here's how Maven solves this for plugin configurations: https://blog.sonatype.com/2011/01/maven-how-to-merging-plugin-configuration-in-complex-projects/

Hi @connormckelvey

Sorry you did not get the expected results and I agree that its a frustrating limitation. If you are interested in contributing a feature please create a proposal spelling out the implementation which should include a flag as @unguiculus suggested.

Thanks @unguiculus! I see now how this would be considered a breaking change.

The problem is that you would not be able to override defaults for lists.

Are you saying you wouldn't be able to override an entire list (for example overriding a list of 3 items with a list of 1 item)? What I'm proposing would allow you to override individual defaulted items in a list. Giving you more control.

I guess Im not seeing why merging maps is desirable and merging lists is not desirable, except that at the current time some projects expect a certain behavior while others expect another.

@jascott1 Thanks, I will definitely think about the proposal with my team, but what I really needed was a quick fix that I thought could be resolved with a quick PR. At this time we need to focus on our immediate problem.

Was this functionality ever added?

I also see this as a limitation, especially for projects like prometheus where the whole prometheus configuration is defined in values file. Not being able to merge on sub-keys, I have to copy almost the same config per environment with is a huge repetition of code.

https://github.com/helm/helm/pull/4806 might be relevant, in which case this should exist for 2.12.0 (not yet released; try out the canary release to test).

Just ran into this and I'm confused as to why this was closed. As @mlushpenko states this seems like a pretty major limitation for managing any reasonably sized deployment. In our case we would like to be able to combine affinity rules from multiple levels -- providing a default set of rules in the base chart that can be augmented when deployed. However, as it stands we have to repeat the default affinity rules in every values override file. Not even close to DRY.

This is compounded by the fact that it doesn't seem possible to specify default values that will be based on things like the release name which aren't known until the deployment is performed (granted that's unrelated, but it means that overriding affinity essentially has to be done on every deployment).

@sfitts have you taken a look at #4806?

@bacongobbler I have and I don't see how it applies. AFAICT, it only deals with updates made during upgrade of an already deployed chart and ensuring that existing values aren't clobbered by new ones. Certainly related, but I don't see how it helps avoid the need to repeat information when combining values files during the initial install.

We'd really like this feature as well. We have a hierarchy of values files (eg: base.yaml <- qa.yaml) which are passed to helm via -f values/base.yaml,values/qa.yaml. As others have mentioned it would be great if we could merge the qa values into the base values without having to repeat everything.

I could also use this

I know this issue is closed, but as a point of reference for those looking here and thinking of resolving the issue I'll supply a comparison with chef.

There this issue is resolved by a fixed set of priorities for values (which they call attributes). Arrays set with the same priority will be merged, while a higher priority value will replace lower priority values.

https://docs.chef.io/attributes.html#about-deep-merge

If implementing something similar in helm I think it would make sense to introduce new versions of --set and --values, say, --overrideSet and --overrideValues for high priority values. As for setting values in a chart another file called, say, overrideValues.yaml could be allowed which sets values at a higher priority than in the values.yaml file.

We could also use this. +1

I'm a bit confused. The open end of this thread gave me the impression that this merging thing is still missing in helm. After trying it out (we do helm upgrade ... -f basic.yaml -f profile_extends_basic.yaml), it seems to work correctly. At least for me the #4806 seems to have nailed it.

I'm a bit confused. The open end of this thread gave me the impression that this merging thing is still missing in helm. After trying it out (we do helm upgrade ... -f basic.yaml -f profile_extends_basic.yaml), it seems to work correctly. At least for me the #4806 seems to have nailed it.

@janotav The #4806 addressed deep merge for the upgrade scenario. This issue was about merging values during normal install.

Yes. This is feature is needed. Another use case is to specify/merge/override a subset of environment variables across different deployment environments.

This continues to be a problem with helm 2.x, does anyone know if helm 3.x have a solution for (optionally) merging arrays during install?

I too would love to see this functionality!

For the folks here who need to modify the list of environment variables you're passing into your deployment template, the solution is to define the vars and defaults as a dict rather than as a list!

values.yaml:

env:
  key1: default1
  key2: default2

values-production.yaml:

env:
  key1: prod-value

deployment.yaml:

{{- range $key, $value :=  .Values.env }}
- name: {{ $key }}
  value: {{ $value | quote }}
{{- end }}

Rendered, using values-production:

- name: key1
  value: "prod-value"
- name: key2
  value: "default2"

@tarrall Can you supply the helm commands you used as well for the merge?

I'm trying to have my helm chart contain default values which are put into a configmap, then have a environment-specific file that I merge into the defaults, but it's overriding instead.

I'm using the range $key,$value in my configmap template.

Does it matter if you're using helm2 or helm3 btw?

puppet with hiera also manages this, giving options of which kind of merge do you want. I am also repeating code because I have several pods that need to mount the exact same file from a configmap and another specific for each pod. The common should be declared in an upper hierarchy and then give the option to merge the values below

@bacongobbler isn't possible to reopen this issue taking in consideration that https://github.com/helm/helm/pull/4806 doesn't solves this problem because it does deep merge in maps and in an upgrade process. What @connormckelvey mentioned was the ability to do deep merge in arrays. This kind of deep merge should work for install and upgrade situations.

I'm surprised helm has no solution for this. The workaround @tarrall suggested doesn't scale well into complex lists involving many keys & sub-lists. @connormckelvey please could you reopen?

I am using the following script as a workaround:

import yaml
from deepmerge import always_merger

fileA = “tmp.yaml"
fileB = “feature.yaml"

with open(fileA,'r+') as f:
fileAdictionary= yaml.load(f)

with open(fileB,'r+') as f:
fileBdictionary = yaml.load(f)

result = always_merger.merge(fileAdictionary, fileBdictionary)
with open(‘newFile.yaml’,'w+') as f:
yaml.dump(result,f)

What @connormckelvey mentioned was the ability to do deep merge in arrays. This kind of deep merge should work for install and upgrade situations.

The current behaviour with merging two arrays is likely going to stay the way it is. I do not believe we plan to change the current behaviour, which is why this issue is closed.

Let's take an example. Here I have a basic values.yaml with one value as an array:

myList: [ "foo" ]

Then I run the following:

helm install [...] --set myList={"bar"}

What's the expected behaviour?

If we were to consider a "deep merge" of the two arrays, the resulting array would be [ "foo", "bar" ].

However, what if the user wanted to null out the existing list, resulting in the output being [ "bar" ]?

The problem here with a deep merge is that we cannot determine what behaviour the user expects. There is also no way to null out existing values.

This is why the current behaviour exists as-is today. The user has a way to null out values set by the chart author in the default values as well as anything set by other values files on the command line (either via --values/-f or --set, --set-string, --set-file, etc.). If they want to perform a deep merge they can do so out-of-band.

You can do this programmatically, of course, and @aknakshay describes one way to do it. But a CLI is not well-designed to convey complex cases like whether --set myList={"bar"} means the user wants to perform a replace or a deep merge.

Note that the same message was conveyed back in 2018 in an earlier comment here. To change the behaviour now to a deep merge for arrays would introduce backwards compatibility issues and could cause more harm than good.

Hope this clarifies where this proposal stands.

Merging could always be allowed via use of a command line flag just as comment 2 described it (https://github.com/helm/helm/issues/3486#issuecomment-364534501). Normally user's values would override defaults but when a command line flag is provided (e.g. --deepMergeInputs) then a deep merge is done. That's how user would handle the need to null out existing values. Is there another complication to this approach?

Not sure what "out-of-band" means.

Merging could always be allowed via use of a command line flag just as comment 2 described it (#3486 (comment)). Normally user's values would override defaults but when a command line flag is provided (e.g. --deepMergeInputs) then a deep merge is done. That's how user would handle the need to null out existing values. Is there another complication to this approach?

An all or nothing approach like that is very dangerous when having big and/or many values files. Another problem with this is that you then can't control the behaviour in a chart. I prefer the solution I outlined in https://github.com/helm/helm/issues/3486#issuecomment-477099409

I noticed recently that using toYaml will only use the yaml from the highest priority yaml file instead of doing the deep merge like it does for range.

Helm version:
version.BuildInfo{Version:"v3.1.2", GitCommit:"d878d4d45863e42fd5cff6743294a11d28a9abce", GitTreeState:"clean", GoVersion:"go1.13.8"}

In fact, only list does not support merge. We agree that all list is rewritten as map, (list in k8s generally has a key called name, and this name can be used as the key of the map)

value.yaml before

# value.yaml 

volumeMounts:
    - mountPath: /data/log/
      name: logs

volumes:
    - emptyDir: {}
      name: logs

initContainers: []

envFrom: []

env:
  - name: NODE_ENV
    value: production
  - name: PORT
    value: "3333"
  - name: SOME_SECRET
    valueFrom:
      secretKeyRef:
        key: SOME_SECRET
        name: some-secret

value.yaml after

# value.yaml

volumeMounts:
  logs:
    - mountPath: /data/log/
      name: logs

volumes:
  logs:
    - emptyDir: {}
      name: logs

initContainers: {}

envFrom: {}

env:
  NODE_ENV:
    - name: NODE_ENV
      value: production
  PORT:
    - name: PORT
      value: "3333"
  SOME_SECRET:
    - name: SOME_SECRET
      valueFrom:
        secretKeyRef:
          key: SOME_SECRET
          name: some-secret

deployment.yaml before

{{- with .Values.env }}
    env:
    {{- toYaml . | nindent 8 }}
{{- end }}

deployment.yaml after

{{- with .Values.env }}
    env:
    {{- range $key, $value := . }}
    {{- toYaml $value | nindent 8 }}
    {{- end }}
{{- end }}

This will support the merge of list.

If you want to delete a configuration

env:
  NODE_ENV: null

@tarrall 's solution didn't work for map-type values, such as valueFrom. The below template code also handles this case:

{{- range $ev_key, $ev_value :=  .Values.deployment.env }}
{{- if (typeIs "string" $ev_value) }}
          - name: {{ $ev_key }}
            value: {{ $ev_value | quote }}
{{- else }}
          - name: {{ $ev_key }}
{{ toYaml $ev_value | indent 12 }}
{{- end }}
{{- end }}
Was this page helpful?
0 / 5 - 0 ratings