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

⚠️ Implement Patch method #235

Merged
merged 2 commits into from
Mar 19, 2019
Merged
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
1 change: 1 addition & 0 deletions Gopkg.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

9 changes: 9 additions & 0 deletions pkg/client/client.go
Original file line number Diff line number Diff line change
Expand Up @@ -130,6 +130,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, obj runtime.Object, patch Patch, opts ...PatchOptionFunc) error {
_, ok := obj.(*unstructured.Unstructured)
if ok {
return c.unstructuredClient.Patch(ctx, obj, patch, opts...)
}
return c.typedClient.Patch(ctx, obj, patch, opts...)
}

// Get implements client.Client
func (c *client) Get(ctx context.Context, key ObjectKey, obj runtime.Object) error {
_, ok := obj.(*unstructured.Unstructured)
Expand Down
271 changes: 271 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 @@ -964,6 +977,224 @@ 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(), dep, client.ConstantPatch(types.MergePatchType, mergePatch))
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(), node, client.ConstantPatch(types.MergePatchType, mergePatch))
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(), node, client.ConstantPatch(types.MergePatchType, mergePatch))
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(), dep, client.ConstantPatch(types.MergePatchType, mergePatch))
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() {

})

It("should respect passed in update options", func() {
By("creating a new client")
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 with dry-run")
err = cl.Patch(context.TODO(), dep, client.ConstantPatch(types.MergePatchType, mergePatch), client.UpdatePatchWith(client.UpdateDryRunAll()))
Expect(err).NotTo(HaveOccurred())

By("validating patched Deployment doesn't have the new annotation")
actual, err := clientset.AppsV1().Deployments(ns).Get(dep.Name, metav1.GetOptions{})
Expect(err).NotTo(HaveOccurred())
Expect(actual).NotTo(BeNil())
Expect(actual.Annotations).NotTo(HaveKey("foo"))
})
})
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(), u, client.ConstantPatch(types.MergePatchType, mergePatch))
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(), u, client.ConstantPatch(types.MergePatchType, mergePatch))
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(), node, client.ConstantPatch(types.MergePatchType, mergePatch))
Expect(err).To(HaveOccurred())

close(done)
})

It("should respect passed-in update options", func() {
By("creating a new client")
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(), u, client.ConstantPatch(types.MergePatchType, mergePatch), client.UpdatePatchWith(client.UpdateDryRunAll()))
Expect(err).NotTo(HaveOccurred())

By("validating patched Deployment does not have the new annotation")
actual, err := clientset.AppsV1().Deployments(ns).Get(depName, metav1.GetOptions{})
Expect(err).NotTo(HaveOccurred())
Expect(actual).NotTo(BeNil())
Expect(actual.Annotations).NotTo(HaveKey("foo"))
})
})
})

Describe("Get", func() {
Context("with structured objects", func() {
It("should fetch an existing object for a go struct", func(done Done) {
Expand Down Expand Up @@ -1984,6 +2215,46 @@ var _ = Describe("DelegatingReader", func() {
})
})

var _ = Describe("Patch", func() {
Describe("CreateMergePatch", func() {
var cm *corev1.ConfigMap

BeforeEach(func() {
cm = &corev1.ConfigMap{
ObjectMeta: metav1.ObjectMeta{
Namespace: metav1.NamespaceDefault,
Name: "cm",
},
}
})

It("creates a merge patch with the modifications applied during the mutation", func() {
const (
annotationKey = "test"
annotationValue = "foo"
)

By("creating a merge patch")
patch := client.MergeFrom(cm.DeepCopy())

By("returning a patch with type MergePatch")
Expect(patch.Type()).To(Equal(types.MergePatchType))

By("retrieving modifying the config map")
metav1.SetMetaDataAnnotation(&cm.ObjectMeta, annotationKey, annotationValue)

By("computing the patch data")
data, err := patch.Data(cm)

By("returning no error")
Expect(err).NotTo(HaveOccurred())

By("returning a patch with data only containing the annotation change")
Expect(data).To(Equal([]byte(fmt.Sprintf(`{"metadata":{"annotations":{"%s":"%s"}}}`, annotationKey, annotationValue))))
})
})
})

type fakeReader struct {
Called int
}
Expand Down
32 changes: 32 additions & 0 deletions pkg/client/fake/client.go
Original file line number Diff line number Diff line change
Expand Up @@ -194,6 +194,38 @@ func (c *fakeClient) Update(ctx context.Context, obj runtime.Object, opts ...cli
return c.tracker.Update(gvr, obj, accessor.GetNamespace())
}

func (c *fakeClient) Patch(ctx context.Context, obj runtime.Object, patch client.Patch, opts ...client.PatchOptionFunc) error {
gvr, err := getGVRFromObject(obj, c.scheme)
if err != nil {
return err
}
accessor, err := meta.Accessor(obj)
if err != nil {
return err
}
data, err := patch.Data(obj)
if err != nil {
return err
}

reaction := testing.ObjectReaction(c.tracker)
handled, o, err := reaction(testing.NewPatchAction(gvr, accessor.GetNamespace(), accessor.GetName(), patch.Type(), 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
Loading