diff --git a/pkg/client/client_test.go b/pkg/client/client_test.go index 021fbeb0d8..079458f527 100644 --- a/pkg/client/client_test.go +++ b/pkg/client/client_test.go @@ -2516,6 +2516,43 @@ U5wwSivyi7vmegHKmblOzNVKA5qPO8zWzqBC Expect(hasDep).To(BeTrue()) }) + It("should fetch unstructured collection of objects when setting a non-list kind", func(ctx SpecContext) { + // While it is not ideal to omit the List suffix it can easily happen. + // As the client is using TrimSuffix(gvk.Kind,"List") this also works. + // As it works it is part of our API and we should test it accordingly. + By("create an initial object") + _, err := clientset.AppsV1().Deployments(ns).Create(ctx, dep, metav1.CreateOptions{}) + Expect(err).NotTo(HaveOccurred()) + + cl, err := client.New(cfg, client.Options{}) + Expect(err).NotTo(HaveOccurred()) + + By("listing all objects of that type in the cluster") + deps := &unstructured.UnstructuredList{} + deps.SetGroupVersionKind(schema.GroupVersionKind{ + Group: "apps", + Kind: "Deployment", + Version: "v1", + }) + err = cl.List(ctx, deps) + Expect(err).NotTo(HaveOccurred()) + + Expect(deps.Items).NotTo(BeEmpty()) + hasDep := false + for _, item := range deps.Items { + Expect(item.GroupVersionKind()).To(Equal(schema.GroupVersionKind{ + Group: "apps", + Kind: "Deployment", + Version: "v1", + })) + if item.GetName() == dep.Name && item.GetNamespace() == dep.Namespace { + hasDep = true + break + } + } + Expect(hasDep).To(BeTrue()) + }) + It("should fetch unstructured collection of objects, even if scheme is empty", func(ctx SpecContext) { By("create an initial object") _, err := clientset.AppsV1().Deployments(ns).Create(ctx, dep, metav1.CreateOptions{}) diff --git a/pkg/client/fake/client.go b/pkg/client/fake/client.go index 62067cb19c..05d71bac76 100644 --- a/pkg/client/fake/client.go +++ b/pkg/client/fake/client.go @@ -1701,6 +1701,12 @@ func (c *fakeClient) addToSchemeIfUnknownAndUnstructuredOrPartial(obj runtime.Ob return err } + if isUnstructuredList || isPartialList { + if !strings.HasSuffix(gvk.Kind, "List") { + gvk.Kind += "List" + } + } + if !c.scheme.Recognizes(gvk) { c.scheme.AddKnownTypeWithName(gvk, obj) } diff --git a/pkg/client/fake/client_test.go b/pkg/client/fake/client_test.go index 6c71d680c0..1eed4d5a6d 100644 --- a/pkg/client/fake/client_test.go +++ b/pkg/client/fake/client_test.go @@ -2939,6 +2939,40 @@ var _ = Describe("Fake client", func() { Expect(result.Object["spec"]).To(Equal(map[string]any{"other": "data"})) }) + It("supports server-side apply of a custom resource via Apply method after List with a non-list kind", func(ctx SpecContext) { + cl := NewClientBuilder().Build() + + // Previously this List call lead to addToSchemeIfUnknownAndUnstructuredOrPartial adding FakeResource as UnstructuredList to the scheme. + // This broke the subsequent SSA call as it was trying to create a new FakeResource object as an UnstructuredList. + // After a fix this List call leads to addToSchemeIfUnknownAndUnstructuredOrPartial adding FakeResourceList as UnstructuredList to the + // scheme even if the UnstructuredList here is missing the List suffix. + objList := &unstructured.UnstructuredList{} + objList.SetAPIVersion("custom/v1") + objList.SetKind("FakeResource") + Expect(cl.List(ctx, objList)).To(Succeed()) + + obj := &unstructured.Unstructured{} + obj.SetAPIVersion("custom/v1") + obj.SetKind("FakeResource") + obj.SetName("foo") + result := obj.DeepCopy() + + Expect(unstructured.SetNestedField(obj.Object, map[string]any{"some": "data"}, "spec")).To(Succeed()) + + applyConfig := client.ApplyConfigurationFromUnstructured(obj) + Expect(cl.Apply(ctx, applyConfig, &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{"some": "data"})) + + 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"})) + }) + It("sets the fieldManager in create, patch and update", func(ctx SpecContext) { owner := "test-owner" cl := client.WithFieldOwner(