Skip to content

fix: omit null/empty labels and annotations keys from diffs (#1064) - #1068

Merged
yxxhero merged 3 commits into
masterfrom
fix-1064-empty-labels
Sep 9, 2026
Merged

fix: omit null/empty labels and annotations keys from diffs (#1064)#1068
yxxhero merged 3 commits into
masterfrom
fix-1064-empty-labels

Conversation

@yxxhero

@yxxhero yxxhero commented Sep 9, 2026

Copy link
Copy Markdown
Collaborator

Summary

Fixes #1064

helm diff shows a confusing labels-only diff entry like:

  metadata:
    finalizers:
    - externalsecrets.external-secrets.io/externalsecret-cleanup
-   labels:
    name: webhook-token

when one side of the diff renders an empty labels key and the other omits it.

Supersedes #1067 (same content, now from an in-repo branch; includes the reworked test layout).

Root cause

Kubernetes treats a null or empty labels/annotations map exactly like an absent key, but charts frequently render the empty key anyway, e.g. via a conditional block:

metadata:
  name: webhook-token
  labels:
{{- with .Values.labels }}
{{ toYaml . | indent 4 }}
{{- end }}

Two gaps left that noise in the diff:

  1. manifest.Parse kept the raw text of both sides untouched for the plain (text) diff, so a stored manifest containing a bare labels: (null) or labels: {} produced a meaningless - labels: entry against a new chart revision that omits the key. This was confirmed live in CI: a release installed from a chart with a bare labels: block, diffed against a chart version without it, printed exactly the - labels: line from the issue.
  2. deleteStatusAndTidyMetadata/pruneNestedMap (live-object tidy used by --three-way-merge and --take-ownership) only handled map-typed values, so a null "labels": null value survived pruning with the same effect.

Fix

  1. stripEmptyMetadataKeys drops labels:/annotations: lines from each parsed manifest whenever the parsed value is null or an empty mapping. The removal is line-based so surrounding text keeps its original formatting (# Source comments, key order, indentation); the parsed document is only consulted to confirm the key really carries no content, so keys with entries (including nested spec.template.metadata.labels) are untouched.
  2. pruneNestedMap removes the key when its value is null, in addition to the existing removal when the pruned map ends up empty.

Testing

  • Unit tests: null labels/annotations keys are removed by the tidy; null/empty/{} labels lines are stripped by Parse while labels with content and nested pod-template labels are kept.
  • Issue reproduction tests live in scripts/issues/<issue-number>.sh with a shared helper library and a unified runner (scripts/issues/run.sh, runs all or specific issues). The CI integration job runs them against the kind cluster. Test scripts/issues/1064.sh covers (plain + three-way-merge):
    • A: chart resource without any labels
    • B: release installed with an explicit managed-by label, diffed against a chart version that dropped it
    • C: chart rendering a bare labels: (null) key vs a version without the block — the exact Bug: helm-diff shows - labels, if resource contains only app.kubernetes.io/managed-by label #1064 symptom (- labels:)
    • D: custom resource (unstructured/CRD path, like the ExternalSecret in the issue) with finalizers and no labels
    • E: flux-style extra labels on the live object
    • F: --take-ownership
    • G: --dry-run=server
    • H: explicit empty labels: {} map — produces - labels: {} without the fix

Signed-off-by: yxxhero aiopsclub@163.com

Charts commonly render a bare `labels:` (null) key when a conditional
labels block is empty. deleteStatusAndTidyMetadata only pruned map-typed
values, so a null labels/annotations key survived pruning and showed up
as a confusing "- labels:" diff entry against the other side that has
no such key.

pruneNestedMap now removes the key when its value is null, in addition
to removing it when the pruned map ends up empty.

Also adds a CI reproduction script exercising no-labels charts, dropped
label chart versions, bare/null labels blocks, custom resources,
flux-style labels, --take-ownership, --dry-run=server and empty labels
maps against a real kind cluster.

Fixes #1064

Signed-off-by: yxxhero <aiopsclub@163.com>
…fests

Kubernetes treats a null or empty labels/annotations map exactly like an
absent key, but charts frequently render the empty key anyway (e.g. via
a conditional labels block that renders a bare "labels:" line).

A raw textual diff between such a manifest and one that omits the key
reports a meaningless change:

  metadata:
    finalizers:
    - externalsecrets.external-secrets.io/externalsecret-cleanup
  - labels:
    name: webhook-token

This is the confusion reported in #1064: after the managed-by label is
pruned, the leftover empty key still shows as "- labels:".

stripEmptyMetadataKeys now drops "labels:"/"annotations:" lines from
each parsed manifest whenever the parsed value is null or an empty
mapping. The removal is line-based so surrounding text keeps its
original formatting (# Source comments, key order, indentation); the
parsed document is consulted only to confirm the key really carries no
content, so keys with entries (including nested spec.template.metadata
labels) are untouched.

The CI reproduction script now asserts that the plain diffs for the
bare-null-labels and empty-flow-map-labels chart variants stay clean.

Fixes #1064

Signed-off-by: yxxhero <aiopsclub@163.com>
Issue reproduction tests now live in scripts/issues/<issue-number>.sh
with a shared helper library (scripts/issues/lib.sh) and a unified
runner:

  scripts/issues/run.sh          # run all issue tests
  scripts/issues/run.sh 1064     # run a specific issue test

Each test fails when its issue is reproduced and streams the full diff
output so CI logs show the actual behavior. The CI integration job now
invokes the runner instead of a single-purpose script.

Signed-off-by: yxxhero <aiopsclub@163.com>
@yxxhero
yxxhero merged commit 8a2c4ed into master Sep 9, 2026
25 checks passed
yxxhero added a commit to helmfile/helmfile that referenced this pull request Sep 11, 2026
helm-diff v3.15.13 omits null/empty labels and annotations keys from
diffs (databus23/helm-diff#1068), which broke the suppress-output-line-
regex integration test: the ValidatingWebhookConfiguration manifest no
longer renders a bare 'annotations:' key under metadata.

Add new golden files suffixed -after-helm-diff-3.15.13 (helm3, helm4,
live variants) and select them via version_ge in the test case, keeping
the existing 3.11.0 and pre-3.11.0 goldens for older plugin versions.

Signed-off-by: yxxhero <aiopsclub@163.com>
yxxhero added a commit to helmfile/helmfile that referenced this pull request Sep 11, 2026
* build(deps): bump helm-diff to v3.15.13

Update helm-diff plugin version from v3.15.12 to v3.15.13 across
Dockerfiles, recommended version constant, CI matrix, and
integration test default.

Signed-off-by: yxxhero <aiopsclub@163.com>

* fix(ci): add golden files for helm-diff >= 3.15.13

helm-diff v3.15.13 omits null/empty labels and annotations keys from
diffs (databus23/helm-diff#1068), which broke the suppress-output-line-
regex integration test: the ValidatingWebhookConfiguration manifest no
longer renders a bare 'annotations:' key under metadata.

Add new golden files suffixed -after-helm-diff-3.15.13 (helm3, helm4,
live variants) and select them via version_ge in the test case, keeping
the existing 3.11.0 and pre-3.11.0 goldens for older plugin versions.

Signed-off-by: yxxhero <aiopsclub@163.com>

* refactor(test): drop legacy helm-diff golden files

CI only exercises the recommended helm-diff version, so replace the
version-suffixed golden files (-after-helm-diff-3.11.0,
-after-helm-diff-3.15.13) with a single set of base golden files
(diff, diff-live, diff-helm4, diff-live-helm4) containing the
v3.15.13 output, and remove the now-dead version gating.

Also drop the 'install semver' CI step, whose only consumer was the
removed gating.

Signed-off-by: yxxhero <aiopsclub@163.com>

* fix(test): remove helm-diff version gating from golden file selection

The previous commit deleted the version-suffixed golden files but the
selection gating was accidentally left behind, making the test reference
non-existent files for helm-diff >= 3.15.13.

Signed-off-by: yxxhero <aiopsclub@163.com>

---------

Signed-off-by: yxxhero <aiopsclub@163.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Bug: helm-diff shows - labels, if resource contains only app.kubernetes.io/managed-by label

1 participant