Skip to content

Commit

Permalink
Implement Patch methods
Browse files Browse the repository at this point in the history
add patch method to
* Client
* unstructuredClient
* typedClient
add tests for both clients
  • Loading branch information
adracus committed Dec 5, 2018
1 parent 8f98fb7 commit 1a192eb
Show file tree
Hide file tree
Showing 7 changed files with 291 additions and 0 deletions.
11 changes: 11 additions & 0 deletions pkg/client/client.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -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)
Expand Down
181 changes: 181 additions & 0 deletions pkg/client/client_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -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)
Expand All @@ -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)
Expand Down Expand Up @@ -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) {
Expand Down
30 changes: 30 additions & 0 deletions pkg/client/fake/client.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -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}
}
Expand Down
26 changes: 26 additions & 0 deletions pkg/client/fake/client_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,8 @@ limitations under the License.
package fake

import (
"encoding/json"

. "github.com/onsi/ginkgo"
. "github.com/onsi/gomega"

Expand Down Expand Up @@ -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() {
Expand Down
4 changes: 4 additions & 0 deletions pkg/client/interfaces.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
19 changes: 19 additions & 0 deletions pkg/client/typed_client.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,8 @@ package client
import (
"context"

"k8s.io/apimachinery/pkg/types"

"k8s.io/apimachinery/pkg/runtime"
)

Expand Down Expand Up @@ -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)
Expand Down
20 changes: 20 additions & 0 deletions pkg/client/unstructured_client.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -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)
Expand Down

0 comments on commit 1a192eb

Please sign in to comment.