diff --git a/pkg/client/client.go b/pkg/client/client.go index 05b9eba2b4..004cc3d385 100644 --- a/pkg/client/client.go +++ b/pkg/client/client.go @@ -21,6 +21,8 @@ import ( "fmt" "reflect" + "k8s.io/apimachinery/pkg/types" + "k8s.io/apimachinery/pkg/api/meta" "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" "k8s.io/apimachinery/pkg/runtime" @@ -121,6 +123,15 @@ func (c *client) Delete(ctx context.Context, obj runtime.Object, opts ...DeleteO return c.typedClient.Delete(ctx, obj, opts...) } +// Patch implements client.Client +func (c *client) Patch(ctx context.Context, pt types.PatchType, data []byte, obj runtime.Object, subresources ...string) error { + _, ok := obj.(*unstructured.Unstructured) + if ok { + return c.unstructuredClient.Patch(ctx, pt, data, obj, subresources...) + } + return c.typedClient.Patch(ctx, pt, data, obj, subresources...) +} + // Get implements client.Client func (c *client) Get(ctx context.Context, key ObjectKey, obj runtime.Object) error { _, ok := obj.(*unstructured.Unstructured) diff --git a/pkg/client/client_test.go b/pkg/client/client_test.go index 3ab5c12de7..a026b0d1b8 100644 --- a/pkg/client/client_test.go +++ b/pkg/client/client_test.go @@ -18,9 +18,12 @@ package client_test import ( "context" + "encoding/json" "fmt" "sync/atomic" + "k8s.io/apimachinery/pkg/types" + . "github.com/onsi/ginkgo" . "github.com/onsi/gomega" appsv1 "k8s.io/api/apps/v1" @@ -62,6 +65,7 @@ var _ = Describe("Client", func() { var count uint64 = 0 var replicaCount int32 = 2 var ns = "default" + var mergePatch []byte BeforeEach(func(done Done) { atomic.AddUint64(&count, 1) @@ -88,6 +92,15 @@ var _ = Describe("Client", func() { Spec: corev1.NodeSpec{}, } scheme = kscheme.Scheme + var err error + mergePatch, err = json.Marshal(map[string]interface{}{ + "metadata": map[string]interface{}{ + "annotations": map[string]interface{}{ + "foo": "bar", + }, + }, + }) + Expect(err).NotTo(HaveOccurred()) close(done) }, serverSideTimeoutSeconds) @@ -918,6 +931,174 @@ var _ = Describe("Client", func() { }) }) + Describe("Patch", func() { + Context("with structured objects", func() { + It("should patch an existing object from a go struct", func(done Done) { + cl, err := client.New(cfg, client.Options{}) + Expect(err).NotTo(HaveOccurred()) + Expect(cl).NotTo(BeNil()) + + By("initially creating a Deployment") + dep, err := clientset.AppsV1().Deployments(ns).Create(dep) + Expect(err).NotTo(HaveOccurred()) + + By("patching the Deployment") + err = cl.Patch(context.TODO(), types.MergePatchType, mergePatch, dep) + Expect(err).NotTo(HaveOccurred()) + + By("validating patched Deployment has new annotation") + actual, err := clientset.AppsV1().Deployments(ns).Get(dep.Name, metav1.GetOptions{}) + Expect(err).NotTo(HaveOccurred()) + Expect(actual).NotTo(BeNil()) + Expect(actual.Annotations["foo"]).To(Equal("bar")) + + close(done) + }) + + It("should patch an existing object non-namespace object from a go struct", func(done Done) { + cl, err := client.New(cfg, client.Options{}) + Expect(err).NotTo(HaveOccurred()) + Expect(cl).NotTo(BeNil()) + + By("initially creating a Node") + node, err := clientset.CoreV1().Nodes().Create(node) + Expect(err).NotTo(HaveOccurred()) + + By("patching the Node") + nodeName := node.Name + err = cl.Patch(context.TODO(), types.MergePatchType, mergePatch, node) + Expect(err).NotTo(HaveOccurred()) + + By("validating the Node no longer exists") + actual, err := clientset.CoreV1().Nodes().Get(nodeName, metav1.GetOptions{}) + Expect(err).NotTo(HaveOccurred()) + Expect(actual).NotTo(BeNil()) + Expect(actual.Annotations["foo"]).To(Equal("bar")) + + close(done) + }) + + It("should fail if the object does not exists", func(done Done) { + cl, err := client.New(cfg, client.Options{}) + Expect(err).NotTo(HaveOccurred()) + Expect(cl).NotTo(BeNil()) + + By("Patching node before it is ever created") + err = cl.Patch(context.TODO(), types.MergePatchType, mergePatch, node) + Expect(err).To(HaveOccurred()) + + close(done) + }) + + PIt("should fail if the object doesn't have meta", func() { + + }) + + It("should fail if the object cannot be mapped to a GVK", func(done Done) { + By("creating client with empty Scheme") + emptyScheme := runtime.NewScheme() + cl, err := client.New(cfg, client.Options{Scheme: emptyScheme}) + Expect(err).NotTo(HaveOccurred()) + Expect(cl).NotTo(BeNil()) + + By("initially creating a Deployment") + dep, err := clientset.AppsV1().Deployments(ns).Create(dep) + Expect(err).NotTo(HaveOccurred()) + + By("patching the Deployment fails") + err = cl.Patch(context.TODO(), types.MergePatchType, mergePatch, dep) + Expect(err).To(HaveOccurred()) + Expect(err.Error()).To(ContainSubstring("no kind is registered for the type")) + + close(done) + }) + + PIt("should fail if the GVK cannot be mapped to a Resource", func() { + + }) + }) + Context("with unstructured objects", func() { + It("should patch an existing object from a go struct", func(done Done) { + cl, err := client.New(cfg, client.Options{}) + Expect(err).NotTo(HaveOccurred()) + Expect(cl).NotTo(BeNil()) + + By("initially creating a Deployment") + dep, err := clientset.AppsV1().Deployments(ns).Create(dep) + Expect(err).NotTo(HaveOccurred()) + + By("patching the Deployment") + depName := dep.Name + u := &unstructured.Unstructured{} + scheme.Convert(dep, u, nil) + u.SetGroupVersionKind(schema.GroupVersionKind{ + Group: "apps", + Kind: "Deployment", + Version: "v1", + }) + err = cl.Patch(context.TODO(), types.MergePatchType, mergePatch, u) + Expect(err).NotTo(HaveOccurred()) + + By("validating patched Deployment has new annotation") + actual, err := clientset.AppsV1().Deployments(ns).Get(depName, metav1.GetOptions{}) + Expect(err).NotTo(HaveOccurred()) + Expect(actual).NotTo(BeNil()) + Expect(actual.Annotations["foo"]).To(Equal("bar")) + + close(done) + }) + + It("should patch an existing object non-namespace object from a go struct", func(done Done) { + cl, err := client.New(cfg, client.Options{}) + Expect(err).NotTo(HaveOccurred()) + Expect(cl).NotTo(BeNil()) + + By("initially creating a Node") + node, err := clientset.CoreV1().Nodes().Create(node) + Expect(err).NotTo(HaveOccurred()) + + By("patching the Node") + nodeName := node.Name + u := &unstructured.Unstructured{} + scheme.Convert(node, u, nil) + u.SetGroupVersionKind(schema.GroupVersionKind{ + Group: "", + Kind: "Node", + Version: "v1", + }) + err = cl.Patch(context.TODO(), types.MergePatchType, mergePatch, u) + Expect(err).NotTo(HaveOccurred()) + + By("validating pathed Node has new annotation") + actual, err := clientset.CoreV1().Nodes().Get(nodeName, metav1.GetOptions{}) + Expect(err).NotTo(HaveOccurred()) + Expect(actual).NotTo(BeNil()) + Expect(actual.Annotations["foo"]).To(Equal("bar")) + + close(done) + }) + + It("should fail if the object does not exist", func(done Done) { + cl, err := client.New(cfg, client.Options{}) + Expect(err).NotTo(HaveOccurred()) + Expect(cl).NotTo(BeNil()) + + By("Patching node before it is ever created") + u := &unstructured.Unstructured{} + scheme.Convert(node, u, nil) + u.SetGroupVersionKind(schema.GroupVersionKind{ + Group: "", + Kind: "Node", + Version: "v1", + }) + err = cl.Patch(context.TODO(), types.MergePatchType, mergePatch, node) + Expect(err).To(HaveOccurred()) + + close(done) + }) + }) + }) + Describe("Get", func() { Context("with structured objects", func() { It("should fetch an existing object for a go struct", func(done Done) { diff --git a/pkg/client/fake/client.go b/pkg/client/fake/client.go index dff226ac37..dbae86c17a 100644 --- a/pkg/client/fake/client.go +++ b/pkg/client/fake/client.go @@ -23,6 +23,8 @@ import ( "os" "strings" + "k8s.io/apimachinery/pkg/types" + "k8s.io/apimachinery/pkg/api/meta" "k8s.io/apimachinery/pkg/runtime" "k8s.io/apimachinery/pkg/runtime/schema" @@ -150,6 +152,34 @@ func (c *fakeClient) Update(ctx context.Context, obj runtime.Object) error { return c.tracker.Update(gvr, obj, accessor.GetNamespace()) } +func (c *fakeClient) Patch(ctx context.Context, pt types.PatchType, data []byte, obj runtime.Object, subresources ...string) error { + gvr, err := getGVRFromObject(obj, c.scheme) + if err != nil { + return err + } + accessor, err := meta.Accessor(obj) + if err != nil { + return err + } + + reaction := testing.ObjectReaction(c.tracker) + handled, o, err := reaction(testing.NewPatchAction(gvr, accessor.GetNamespace(), accessor.GetName(), data)) + if err != nil { + return err + } + if !handled { + panic("tracker could not handle patch method") + } + + j, err := json.Marshal(o) + if err != nil { + return err + } + decoder := scheme.Codecs.UniversalDecoder() + _, _, err = decoder.Decode(j, nil, obj) + return err +} + func (c *fakeClient) Status() client.StatusWriter { return &fakeStatusWriter{client: c} } diff --git a/pkg/client/fake/client_test.go b/pkg/client/fake/client_test.go index ac05f9302a..d50dd9b930 100644 --- a/pkg/client/fake/client_test.go +++ b/pkg/client/fake/client_test.go @@ -17,6 +17,8 @@ limitations under the License. package fake import ( + "encoding/json" + . "github.com/onsi/ginkgo" . "github.com/onsi/gomega" @@ -135,6 +137,30 @@ var _ = Describe("Fake client", func() { Expect(err).To(BeNil()) Expect(list.Items).To(HaveLen(0)) }) + + It("should be able to Patch", func() { + By("Patching a deployment") + mergePatch, err := json.Marshal(map[string]interface{}{ + "metadata": map[string]interface{}{ + "annotations": map[string]interface{}{ + "foo": "bar", + }, + }, + }) + Expect(err).NotTo(HaveOccurred()) + err = cl.Patch(nil, types.JSONPatchType, mergePatch, dep) + Expect(err).NotTo(HaveOccurred()) + + By("Getting the patched deployment") + namespacedName := types.NamespacedName{ + Name: "test-deployment", + Namespace: "ns1", + } + obj := &appsv1.Deployment{} + err = cl.Get(nil, namespacedName, obj) + Expect(err).NotTo(HaveOccurred()) + Expect(obj.Annotations["foo"]).To(Equal("bar")) + }) } Context("with default scheme.Scheme", func() { diff --git a/pkg/client/interfaces.go b/pkg/client/interfaces.go index 36d0fce620..8bd45e1250 100644 --- a/pkg/client/interfaces.go +++ b/pkg/client/interfaces.go @@ -65,6 +65,10 @@ type Writer interface { // Update updates the given obj in the Kubernetes cluster. obj must be a // struct pointer so that obj can be updated with the content returned by the Server. Update(ctx context.Context, obj runtime.Object) error + + // Patch patches the given obj in the Kubernetes cluster. obj must be a + // struct pointer so that obj can be updated with the content returned by the Server. + Patch(ctx context.Context, pt types.PatchType, data []byte, obj runtime.Object, subresources ...string) error } // StatusClient knows how to create a client which can update status subresource diff --git a/pkg/client/typed_client.go b/pkg/client/typed_client.go index 26cb81d8ba..7b0b240a3b 100644 --- a/pkg/client/typed_client.go +++ b/pkg/client/typed_client.go @@ -19,6 +19,8 @@ package client import ( "context" + "k8s.io/apimachinery/pkg/types" + "k8s.io/apimachinery/pkg/runtime" ) @@ -78,6 +80,23 @@ func (c *typedClient) Delete(ctx context.Context, obj runtime.Object, opts ...De Error() } +// Patch implements client.Client +func (c *typedClient) Patch(ctx context.Context, pt types.PatchType, data []byte, obj runtime.Object, subresources ...string) error { + o, err := c.cache.getObjMeta(obj) + if err != nil { + return err + } + return o.Patch(pt). + NamespaceIfScoped(o.GetNamespace(), o.isNamespaced()). + Resource(o.resource()). + SubResource(subresources...). + Name(o.GetName()). + Body(data). + Context(ctx). + Do(). + Into(obj) +} + // Get implements client.Client func (c *typedClient) Get(ctx context.Context, key ObjectKey, obj runtime.Object) error { r, err := c.cache.getResource(obj) diff --git a/pkg/client/unstructured_client.go b/pkg/client/unstructured_client.go index 2ce0b19eb8..d8bc8f6fb4 100644 --- a/pkg/client/unstructured_client.go +++ b/pkg/client/unstructured_client.go @@ -21,6 +21,8 @@ import ( "fmt" "strings" + "k8s.io/apimachinery/pkg/types" + "k8s.io/apimachinery/pkg/api/meta" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" @@ -87,6 +89,24 @@ func (uc *unstructuredClient) Delete(_ context.Context, obj runtime.Object, opts return err } +// Patch implements client.Client +func (uc *unstructuredClient) Patch(_ context.Context, pt types.PatchType, data []byte, obj runtime.Object, subresources ...string) error { + u, ok := obj.(*unstructured.Unstructured) + if !ok { + return fmt.Errorf("unstructured client did not understand object: %T", obj) + } + r, err := uc.getResourceInterface(u.GroupVersionKind(), u.GetNamespace()) + if err != nil { + return err + } + i, err := r.Patch(u.GetName(), pt, data, metav1.UpdateOptions{}, subresources...) + if err != nil { + return err + } + u.Object = i.Object + return nil +} + // Get implements client.Client func (uc *unstructuredClient) Get(_ context.Context, key ObjectKey, obj runtime.Object) error { u, ok := obj.(*unstructured.Unstructured)