Skip to content

Subchart default nil values shadow global values via pluck (regression from #31644) #31971

@cmarker-gl

Description

@cmarker-gl

What happened?

After the fix for #31643 (PR #31644, backported to 3.20.x via #31829), nil values declared in a subchart's values.yaml are now preserved in the merged values map during coalescing. When a template uses Sprig's pluck to implement a local-then-global-then-default fallback pattern, these nil values are returned as the first match, shadowing the global value.

This is a regression introduced in 3.20.1 by the backport of #31644 (PR #31829). Helm 3.20.0 is not affected. Helm 4.1.1 is also not affected.

The root cause is in coalesceTablesFullKey in pkg/chartutil/coalesce.go. The srcOriginalNonNil guard added by #31644 (backported to v3 via #31829) correctly prevents deletion of user-specified nils, but it also prevents cleanup of nil values that come from chart defaults during subsequent coalesce passes.

Before #31644, the unconditional delete(dst, key) at ok && !merge && dv == nil would clean up these nils. With the new srcOriginalNonNil[key] guard, nils from chart defaults survive into the final values map.

Note: Helm v4 has the same coalesceTablesFullKey code but is not affected — it likely handles this through a different code path elsewhere in the v4 chart processing pipeline.

See also: gitlab-org/charts/gitlab#6366 — the downstream GitLab chart issue reporting this behavior.

What did you expect to happen?

Nil values from subchart chart defaults (not explicitly set by the user) should not appear in the final merged values. They should either be stripped or not copied in the first place, so that pluck falls through to the global value.

How can we reproduce it (as minimally and precisely as possible)?

No custom chart needed — this reproduces with the published GitLab Helm chart, which uses a pluck-based fallback pattern in its templates.

Setup

helm repo add gitlab https://charts.gitlab.io/
helm repo update
helm pull gitlab/gitlab --version 9.10.1 --untar
cd gitlab
helm dependency build .

Test with Helm 3.19.x (working)

helm template . \
  --set certmanager-issuer.email=test \
  -s charts/gitlab/charts/webservice/templates/ingress.yaml \
  | grep cert

Expected output (cert-manager annotations rendered):

    cert-manager.io/issuer: "release-name-issuer"
    acme.cert-manager.io/http01-edit-in-place: "true"

Test with Helm 3.20.1 (broken)

helm template . \
  --set certmanager-issuer.email=test \
  -s charts/gitlab/charts/webservice/templates/ingress.yaml \
  | grep cert

Expected output: same as 3.19.x above (cert-manager annotations rendered)

Actual output: empty — annotations silently dropped

Version matrix

Helm Version Result
3.19.4 Working
3.19.5 Working
3.20.0 Working
3.20.1 Broken
4.1.1 Working

What the template does

The GitLab chart's _helpers.tpl uses this pattern:

{{- if (pluck "configureCertmanager" .Values.ingress .Values.global.ingress (dict "configureCertmanager" false) | first) -}}
  cert-manager.io/issuer: "{{ .Release.Name }}-issuer"
{{- end -}}

The subchart's values.yaml declares ingress.configureCertmanager: (nil). The parent chart sets global.ingress.configureCertmanager: true. pluck iterates the maps in order and returns the first match — including nil. Since nil is falsy, the if block evaluates to false and the annotations are silently dropped.

Helm version

Details
$ helm version
# Affected: v3.20.1
# Working:  v3.19.4, v3.19.5, v3.20.0, v4.1.1

Kubernetes version

Details
$ kubectl version
# N/A — this is a helm template issue, no cluster required

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions