Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Support Patch request in client #95

Closed
wants to merge 1 commit into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 17 additions & 0 deletions pkg/client/client.go
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ import (
"k8s.io/apimachinery/pkg/api/meta"
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/apimachinery/pkg/runtime/serializer"
"k8s.io/apimachinery/pkg/types"
"k8s.io/client-go/kubernetes/scheme"
"k8s.io/client-go/rest"
"sigs.k8s.io/controller-runtime/pkg/client/apiutil"
Expand Down Expand Up @@ -124,6 +125,22 @@ func (c *client) Delete(ctx context.Context, obj runtime.Object) error {
Error()
}

// Patch implements client.Client
func (c *client) 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).
Do().
Into(obj)
}

// Get implements client.Client
func (c *client) Get(ctx context.Context, key ObjectKey, obj runtime.Object) error {
r, err := c.cache.getResource(obj)
Expand Down
160 changes: 160 additions & 0 deletions pkg/client/client_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ package client_test

import (
"context"
"encoding/json"
"fmt"
"sync/atomic"

Expand All @@ -28,6 +29,7 @@ import (
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/apimachinery/pkg/types"
"sigs.k8s.io/controller-runtime/pkg/client"

kscheme "k8s.io/client-go/kubernetes/scheme"
Expand Down Expand Up @@ -499,6 +501,164 @@ var _ = Describe("Client", func() {
})
})

Describe("Patch", 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")
mergePatch, err := json.Marshal(map[string]interface{}{
"metadata": map[string]interface{}{
"annotations": map[string]interface{}{
"foo": "bar",
},
},
})
Expect(err).NotTo(HaveOccurred())

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 new object from an unstructured object", 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("updating and encoding the Deployment as unstructured")
var u runtime.Unstructured = &unstructured.Unstructured{}
dep.Annotations = map[string]string{"foo": "bar"}
scheme.Convert(dep, u, nil)

By("patching the 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(context.TODO(), types.MergePatchType, mergePatch, u)
Expect(err).NotTo(HaveOccurred())

By("fetching newly created unstructured 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())

node, err := clientset.CoreV1().Nodes().Create(node)
Expect(err).NotTo(HaveOccurred())

By("patching the object")
mergePatch, err := json.Marshal(map[string]interface{}{
"metadata": map[string]interface{}{
"annotations": map[string]interface{}{
"foo": "bar",
},
},
})
Expect(err).NotTo(HaveOccurred())

err = cl.Patch(context.TODO(), types.MergePatchType, mergePatch, node)
Expect(err).NotTo(HaveOccurred())

By("validate patched Node had new annotation")
actual, err := clientset.CoreV1().Nodes().Get(node.Name, 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 non-existent object")
mergePatch, err := json.Marshal(map[string]interface{}{
"metadata": map[string]interface{}{
"annotations": map[string]interface{}{
"foo": "bar",
},
},
})
Expect(err).NotTo(HaveOccurred())
err = cl.Patch(context.TODO(), types.MergePatchType, mergePatch, dep)
Expect(err).To(HaveOccurred())

close(done)
})

It("should fail if the object does not pass server-side validation", func() {

})

It("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")
mergePatch, err := json.Marshal(map[string]interface{}{
"metadata": map[string]interface{}{
"annotations": map[string]interface{}{
"foo": "bar",
},
},
})
Expect(err).NotTo(HaveOccurred())
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)
})

It("should fail if the GVK cannot be mapped to a Resource", func() {

})
})

Describe("Get", func() {
It("should fetch an existing object for a go struct", func(done Done) {
By("first creating the Deployment")
Expand Down
4 changes: 4 additions & 0 deletions pkg/client/interfaces.go
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,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
}

// Client knows how to perform CRUD operations on Kubernetes objects.
Expand Down