From 7fb375a4991a8745478768b600a6687da8203002 Mon Sep 17 00:00:00 2001 From: Stefan Bueringer Date: Thu, 18 Dec 2025 16:24:12 +0100 Subject: [PATCH] Fix fake client Apply with Unstructured ApplyConfiguration + resourceVersion unset MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Stefan Büringer buringerst@vmware.com --- pkg/client/client_test.go | 40 ++++++++++++++++++++++++++++ pkg/client/fake/client_test.go | 19 +++++++++++++ pkg/client/fake/versioned_tracker.go | 7 +++-- 3 files changed, 64 insertions(+), 2 deletions(-) diff --git a/pkg/client/client_test.go b/pkg/client/client_test.go index b2890c385d..878927f467 100644 --- a/pkg/client/client_test.go +++ b/pkg/client/client_test.go @@ -955,6 +955,7 @@ U5wwSivyi7vmegHKmblOzNVKA5qPO8zWzqBC Expect(actualData).To(BeComparableTo(data)) Expect(actualData).To(BeComparableTo(obj.Object["data"])) + // Apply with ResourceVersion set data = map[string]any{ "a-new-key": "a-new-value", } @@ -974,6 +975,28 @@ U5wwSivyi7vmegHKmblOzNVKA5qPO8zWzqBC Expect(actualData).To(BeComparableTo(data)) Expect(actualData).To(BeComparableTo(obj.Object["data"])) + + // Apply with ResourceVersion unset + obj.SetResourceVersion("") + data = map[string]any{ + "another-new-key": "another-new-value", + } + obj.Object["data"] = data + unstructured.RemoveNestedField(obj.Object, "metadata", "managedFields") + + err = cl.Apply(ctx, client.ApplyConfigurationFromUnstructured(obj), &client.ApplyOptions{FieldManager: "test-manager"}) + Expect(err).NotTo(HaveOccurred()) + + cm, err = clientset.CoreV1().ConfigMaps(obj.GetNamespace()).Get(ctx, obj.GetName(), metav1.GetOptions{}) + Expect(err).NotTo(HaveOccurred()) + + actualData = map[string]any{} + for k, v := range cm.Data { + actualData[k] = v + } + + Expect(actualData).To(BeComparableTo(data)) + Expect(actualData).To(BeComparableTo(obj.Object["data"])) }) }) @@ -999,6 +1022,23 @@ U5wwSivyi7vmegHKmblOzNVKA5qPO8zWzqBC Expect(cm.Data).To(BeComparableTo(data)) Expect(cm.Data).To(BeComparableTo(obj.Data)) + // Apply with ResourceVersion set + data = map[string]string{ + "a-new-key": "a-new-value", + } + obj.Data = data + + err = cl.Apply(ctx, obj, &client.ApplyOptions{FieldManager: "test-manager"}) + Expect(err).NotTo(HaveOccurred()) + + cm, err = clientset.CoreV1().ConfigMaps(ptr.Deref(obj.GetNamespace(), "")).Get(ctx, ptr.Deref(obj.GetName(), ""), metav1.GetOptions{}) + Expect(err).NotTo(HaveOccurred()) + + Expect(cm.Data).To(BeComparableTo(data)) + Expect(cm.Data).To(BeComparableTo(obj.Data)) + + // Apply with ResourceVersion unset + obj.ResourceVersion = ptr.To("") data = map[string]string{ "a-new-key": "a-new-value", } diff --git a/pkg/client/fake/client_test.go b/pkg/client/fake/client_test.go index 1901b3051d..209ccc67fe 100644 --- a/pkg/client/fake/client_test.go +++ b/pkg/client/fake/client_test.go @@ -2875,11 +2875,20 @@ var _ = Describe("Fake client", func() { Expect(cl.Get(ctx, client.ObjectKeyFromObject(cm), cm)).To(Succeed()) Expect(cm.Data).To(BeComparableTo(map[string]string{"some": "data"})) + // Apply with ResourceVersion set obj.Data = map[string]string{"other": "data"} Expect(cl.Apply(ctx, obj, &client.ApplyOptions{FieldManager: "test-manager"})).To(Succeed()) Expect(cl.Get(ctx, client.ObjectKeyFromObject(cm), cm)).To(Succeed()) Expect(cm.Data).To(BeComparableTo(map[string]string{"other": "data"})) + + // Apply with ResourceVersion unset + obj.ResourceVersion = ptr.To("") + obj.Data = map[string]string{"another": "data"} + Expect(cl.Apply(ctx, obj, &client.ApplyOptions{FieldManager: "test-manager"})).To(Succeed()) + + Expect(cl.Get(ctx, client.ObjectKeyFromObject(cm), cm)).To(Succeed()) + Expect(cm.Data).To(BeComparableTo(map[string]string{"another": "data"})) }) It("returns a conflict when trying to Create an object with UID set through Apply", func(ctx SpecContext) { @@ -2931,12 +2940,22 @@ var _ = Describe("Fake client", func() { Expect(cl.Get(ctx, client.ObjectKeyFromObject(result), result)).To(Succeed()) Expect(result.Object["spec"]).To(Equal(map[string]any{"some": "data"})) + // Apply with ResourceVersion set Expect(unstructured.SetNestedField(obj.Object, map[string]any{"other": "data"}, "spec")).To(Succeed()) applyConfig2 := client.ApplyConfigurationFromUnstructured(obj) Expect(cl.Apply(ctx, applyConfig2, &client.ApplyOptions{FieldManager: "test-manager"})).To(Succeed()) Expect(cl.Get(ctx, client.ObjectKeyFromObject(result), result)).To(Succeed()) Expect(result.Object["spec"]).To(Equal(map[string]any{"other": "data"})) + + // Apply with ResourceVersion unset + obj.SetResourceVersion("") + Expect(unstructured.SetNestedField(obj.Object, map[string]any{"another": "data"}, "spec")).To(Succeed()) + applyConfig3 := client.ApplyConfigurationFromUnstructured(obj) + Expect(cl.Apply(ctx, applyConfig3, &client.ApplyOptions{FieldManager: "test-manager"})).To(Succeed()) + + Expect(cl.Get(ctx, client.ObjectKeyFromObject(result), result)).To(Succeed()) + Expect(result.Object["spec"]).To(Equal(map[string]any{"another": "data"})) }) It("supports server-side apply of a custom resource via Apply method after List with a non-list kind", func(ctx SpecContext) { diff --git a/pkg/client/fake/versioned_tracker.go b/pkg/client/fake/versioned_tracker.go index bc1eaeb951..bbe3ac9b0d 100644 --- a/pkg/client/fake/versioned_tracker.go +++ b/pkg/client/fake/versioned_tracker.go @@ -265,11 +265,14 @@ func (t versionedTracker) updateObject( // apiserver accepts such a patch, but it does so we just copy that behavior. // Kubernetes apiserver behavior can be checked like this: // `kubectl patch configmap foo --patch '{"metadata":{"annotations":{"foo":"bar"},"resourceVersion":null}}' -v=9` - case bytes. - Contains(debug.Stack(), []byte("sigs.k8s.io/controller-runtime/pkg/client/fake.(*fakeClient).Patch")): + case bytes.Contains(debug.Stack(), []byte("sigs.k8s.io/controller-runtime/pkg/client/fake.(*fakeClient).Patch")): // We apply patches using a client-go reaction that ends up calling the trackers Update. As we can't change // that reaction, we use the callstack to figure out if this originated from the "fakeClient.Patch" func. accessor.SetResourceVersion(oldAccessor.GetResourceVersion()) + case bytes.Contains(debug.Stack(), []byte("sigs.k8s.io/controller-runtime/pkg/client/fake.(*fakeClient).Apply")): + // We apply patches using a client-go reaction that ends up calling the trackers Update. As we can't change + // that reaction, we use the callstack to figure out if this originated from the "fakeClient.Apply" func. + accessor.SetResourceVersion(oldAccessor.GetResourceVersion()) } }