Skip to content

Conversation

@yozel
Copy link
Contributor

@yozel yozel commented Dec 4, 2025

Describe the bug

When drift detection computes a JSON patch for changes limited to metadata.annotations and metadata.labels, the generated patch targets /metadata and replaces the entire metadata object, instead of adding only annotations and labels.

This leads to an incorrect patch that would drop required metadata fields such as name, namespace, uid, etc.

To Reproduce

Given the following current object in the cluster:

---
apiVersion: v1
kind: ConfigMap
metadata:
  name: configmap-data
  namespace: default

And the desired object rendered by Helm is:

---
apiVersion: v1
kind: ConfigMap
metadata:
  name: configmap-data
  namespace: default
  annotations:
    foo: bar
  labels:
    baz: qux

Drift detection produces the following JSON patch:

[
  {
    "value": {
      "annotations": {
        "foo": "bar"
      },
      "labels": {
        "baz": "qux"
      }
    },
    "op": "add",
    "path": "/metadata"
  }
]

In JSON Patch semantics, op: "add" with path: "/metadata" on an existing key replaces the entire metadata object. This means every existing field under metadata that is not part of this value is effectively removed.

Expected behavior

The generated JSON patch should target only the changed subtrees under metadata, for example:

[
  {
    "value": {
      "foo": "bar"
    },
    "op": "add",
    "path": "/metadata/annotations"
  },
  {
    "value": {
      "baz": "qux"
    },
    "op": "add",
    "path": "/metadata/labels"
  }
]

This way, existing fields under metadata such as name, namespace, uid, resourceVersion, and any other keys are preserved.

Fix

This PR preserves metadata.name in copyAnnotationsAndLabels and that prevents the resulting metadata to be empty, which solves the issue with wrong JSON patch

@yozel yozel force-pushed the yozel/bug-fix branch 2 times, most recently from 23c3333 to d09e85d Compare January 6, 2026 15:15
@stefanprodan stefanprodan added the area/server-side-apply SSA related issues and pull requests label Jan 7, 2026
@stefanprodan
Copy link
Member

Please rebase with upstream main and force push. Thanks!

@yozel
Copy link
Contributor Author

yozel commented Jan 7, 2026

Please rebase with upstream main and force push. Thanks!

Done

Copy link
Member

@stefanprodan stefanprodan left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM

Thanks @yozel 🏅

@matheuscscp
Copy link
Member

Thanks for fixing this @yozel!

I was wondering what would happen e.g. if the object already had existing annotations, looks like the code generates the appropriate patch targeting only the changed annotations i.e. path: /metadata/annotations/annotated. I tested this by adding this patch:

diff --git a/ssa/jsondiff/testdata/empty-configmap.yaml b/ssa/jsondiff/testdata/empty-configmap.yaml
index ba47038..8e2eeec 100644
--- a/ssa/jsondiff/testdata/empty-configmap.yaml
+++ b/ssa/jsondiff/testdata/empty-configmap.yaml
@@ -3,3 +3,5 @@ apiVersion: v1
 kind: ConfigMap
 metadata:
   name: "configmap-data"
+  annotations:
+    prev: some-previous-value

The test you added showed me this:

=== RUN   TestUnstructured/ConfigMap_with_added_label_and_annotation
    /home/matheus/Documents/github.com/fluxcd/pkg/ssa/jsondiff/unstructured_test.go:915: Unstructured() mismatch (-want +got):
          &jsondiff.Diff{
                Type:          "update",
                DesiredObject: &unstructured.Unstructured{Object: {"apiVersion": string("v1"), "kind": string("ConfigMap"), "metadata": map[string]any{"annotations": map[string]any{"annotated": string("yes"), "prev": string("some-previous-value")}, "labels": map[string]any{"labeled": string("yes")}, "name": string("configmap-data"), "namespace": string("test-resource-gq8ll")}}},
                ClusterObject: &unstructured.Unstructured{Object: {"apiVersion": string("v1"), "kind": string("ConfigMap"), "metadata": map[string]any{"annotations": map[string]any{"prev": string("some-previous-value")}, "creationTimestamp": string("2026-01-08T09:33:41Z"), "managedFields": []any{map[string]any{"apiVersion": string("v1"), "fieldsType": string("FieldsV1"), "fieldsV1": map[string]any{"f:metadata": map[string]any{"f:annotations": map[string]any{"f:prev": map[string]any{}}}}, "manager": string("dummy"), ...}}, "name": string("configmap-data"), ...}}},
                Patch: jsondiff.Patch{
                        {
        -                       Value:    map[string]any{"annotated": string("yes")},
        +                       Value:    string("yes"),
                                OldValue: nil,
                                Type:     "add",
                                From:     "",
        -                       Path:     "/metadata/annotations",
        +                       Path:     "/metadata/annotations/annotated",
                                ... // 1 ignored field
                        },
                        {Value: map[string]any{"labeled": string("yes")}, Type: "add", Path: "/metadata/labels"},
                },
          }
--- FAIL: TestUnstructured/ConfigMap_with_added_label_and_annotation (0.02s)
--- FAIL: TestUnstructured (0.02s)

Can you please add a new test ConfigMap YAML with a pre-existing annotation and a test for that? Thanks! 🙏

When annotations and labels were missing on the cluster object but
present on the desired object, drift detection produced an "add
/metadata" patch. JSON Patch interprets this as replacing the entire
metadata block, dropping fields like metadata.name.

This update preserves metadata.name in copyAnnotationsAndLabels and
emits separate add operations for annotations and labels. A test
verifies the correct behavior.

Signed-off-by: Yasin Özel <yozel@nebius.com>
Copy link
Member

@matheuscscp matheuscscp left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM! 🚀

@matheuscscp matheuscscp merged commit fd7874f into fluxcd:main Jan 8, 2026
13 checks passed
@matheuscscp
Copy link
Member

@yozel ssa/v0.62.0 is tagged in case you want to bump it in helm-controller

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

area/server-side-apply SSA related issues and pull requests

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants