Skip to content

Commit b002d31

Browse files
committed
Implement Patch methods
create Patch, PatchOptions and PatchOptionFunc add patch method to * Client * unstructuredClient * typedClient implement utility to create merge patches add tests for both clients and for the utility
1 parent 276610b commit b002d31

File tree

9 files changed

+429
-0
lines changed

9 files changed

+429
-0
lines changed

Gopkg.lock

Lines changed: 1 addition & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

pkg/client/client.go

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -130,6 +130,15 @@ func (c *client) Delete(ctx context.Context, obj runtime.Object, opts ...DeleteO
130130
return c.typedClient.Delete(ctx, obj, opts...)
131131
}
132132

133+
// Patch implements client.Client
134+
func (c *client) Patch(ctx context.Context, obj runtime.Object, patch Patch, opts ...PatchOptionFunc) error {
135+
_, ok := obj.(*unstructured.Unstructured)
136+
if ok {
137+
return c.unstructuredClient.Patch(ctx, obj, patch, opts...)
138+
}
139+
return c.typedClient.Patch(ctx, obj, patch, opts...)
140+
}
141+
133142
// Get implements client.Client
134143
func (c *client) Get(ctx context.Context, key ObjectKey, obj runtime.Object) error {
135144
_, ok := obj.(*unstructured.Unstructured)

pkg/client/client_test.go

Lines changed: 221 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,9 +18,12 @@ package client_test
1818

1919
import (
2020
"context"
21+
"encoding/json"
2122
"fmt"
2223
"sync/atomic"
2324

25+
"k8s.io/apimachinery/pkg/types"
26+
2427
. "github.com/onsi/ginkgo"
2528
. "github.com/onsi/gomega"
2629
appsv1 "k8s.io/api/apps/v1"
@@ -62,6 +65,7 @@ var _ = Describe("Client", func() {
6265
var count uint64 = 0
6366
var replicaCount int32 = 2
6467
var ns = "default"
68+
var mergePatch []byte
6569

6670
BeforeEach(func(done Done) {
6771
atomic.AddUint64(&count, 1)
@@ -88,6 +92,15 @@ var _ = Describe("Client", func() {
8892
Spec: corev1.NodeSpec{},
8993
}
9094
scheme = kscheme.Scheme
95+
var err error
96+
mergePatch, err = json.Marshal(map[string]interface{}{
97+
"metadata": map[string]interface{}{
98+
"annotations": map[string]interface{}{
99+
"foo": "bar",
100+
},
101+
},
102+
})
103+
Expect(err).NotTo(HaveOccurred())
91104

92105
close(done)
93106
}, serverSideTimeoutSeconds)
@@ -964,6 +977,174 @@ var _ = Describe("Client", func() {
964977
})
965978
})
966979

980+
Describe("Patch", func() {
981+
Context("with structured objects", func() {
982+
It("should patch an existing object from a go struct", func(done Done) {
983+
cl, err := client.New(cfg, client.Options{})
984+
Expect(err).NotTo(HaveOccurred())
985+
Expect(cl).NotTo(BeNil())
986+
987+
By("initially creating a Deployment")
988+
dep, err := clientset.AppsV1().Deployments(ns).Create(dep)
989+
Expect(err).NotTo(HaveOccurred())
990+
991+
By("patching the Deployment")
992+
err = cl.Patch(context.TODO(), dep, client.ConstantPatch(types.MergePatchType, mergePatch))
993+
Expect(err).NotTo(HaveOccurred())
994+
995+
By("validating patched Deployment has new annotation")
996+
actual, err := clientset.AppsV1().Deployments(ns).Get(dep.Name, metav1.GetOptions{})
997+
Expect(err).NotTo(HaveOccurred())
998+
Expect(actual).NotTo(BeNil())
999+
Expect(actual.Annotations["foo"]).To(Equal("bar"))
1000+
1001+
close(done)
1002+
})
1003+
1004+
It("should patch an existing object non-namespace object from a go struct", func(done Done) {
1005+
cl, err := client.New(cfg, client.Options{})
1006+
Expect(err).NotTo(HaveOccurred())
1007+
Expect(cl).NotTo(BeNil())
1008+
1009+
By("initially creating a Node")
1010+
node, err := clientset.CoreV1().Nodes().Create(node)
1011+
Expect(err).NotTo(HaveOccurred())
1012+
1013+
By("patching the Node")
1014+
nodeName := node.Name
1015+
err = cl.Patch(context.TODO(), node, client.ConstantPatch(types.MergePatchType, mergePatch))
1016+
Expect(err).NotTo(HaveOccurred())
1017+
1018+
By("validating the Node no longer exists")
1019+
actual, err := clientset.CoreV1().Nodes().Get(nodeName, metav1.GetOptions{})
1020+
Expect(err).NotTo(HaveOccurred())
1021+
Expect(actual).NotTo(BeNil())
1022+
Expect(actual.Annotations["foo"]).To(Equal("bar"))
1023+
1024+
close(done)
1025+
})
1026+
1027+
It("should fail if the object does not exists", func(done Done) {
1028+
cl, err := client.New(cfg, client.Options{})
1029+
Expect(err).NotTo(HaveOccurred())
1030+
Expect(cl).NotTo(BeNil())
1031+
1032+
By("Patching node before it is ever created")
1033+
err = cl.Patch(context.TODO(), node, client.ConstantPatch(types.MergePatchType, mergePatch))
1034+
Expect(err).To(HaveOccurred())
1035+
1036+
close(done)
1037+
})
1038+
1039+
PIt("should fail if the object doesn't have meta", func() {
1040+
1041+
})
1042+
1043+
It("should fail if the object cannot be mapped to a GVK", func(done Done) {
1044+
By("creating client with empty Scheme")
1045+
emptyScheme := runtime.NewScheme()
1046+
cl, err := client.New(cfg, client.Options{Scheme: emptyScheme})
1047+
Expect(err).NotTo(HaveOccurred())
1048+
Expect(cl).NotTo(BeNil())
1049+
1050+
By("initially creating a Deployment")
1051+
dep, err := clientset.AppsV1().Deployments(ns).Create(dep)
1052+
Expect(err).NotTo(HaveOccurred())
1053+
1054+
By("patching the Deployment fails")
1055+
err = cl.Patch(context.TODO(), dep, client.ConstantPatch(types.MergePatchType, mergePatch))
1056+
Expect(err).To(HaveOccurred())
1057+
Expect(err.Error()).To(ContainSubstring("no kind is registered for the type"))
1058+
1059+
close(done)
1060+
})
1061+
1062+
PIt("should fail if the GVK cannot be mapped to a Resource", func() {
1063+
1064+
})
1065+
})
1066+
Context("with unstructured objects", func() {
1067+
It("should patch an existing object from a go struct", func(done Done) {
1068+
cl, err := client.New(cfg, client.Options{})
1069+
Expect(err).NotTo(HaveOccurred())
1070+
Expect(cl).NotTo(BeNil())
1071+
1072+
By("initially creating a Deployment")
1073+
dep, err := clientset.AppsV1().Deployments(ns).Create(dep)
1074+
Expect(err).NotTo(HaveOccurred())
1075+
1076+
By("patching the Deployment")
1077+
depName := dep.Name
1078+
u := &unstructured.Unstructured{}
1079+
scheme.Convert(dep, u, nil)
1080+
u.SetGroupVersionKind(schema.GroupVersionKind{
1081+
Group: "apps",
1082+
Kind: "Deployment",
1083+
Version: "v1",
1084+
})
1085+
err = cl.Patch(context.TODO(), u, client.ConstantPatch(types.MergePatchType, mergePatch))
1086+
Expect(err).NotTo(HaveOccurred())
1087+
1088+
By("validating patched Deployment has new annotation")
1089+
actual, err := clientset.AppsV1().Deployments(ns).Get(depName, metav1.GetOptions{})
1090+
Expect(err).NotTo(HaveOccurred())
1091+
Expect(actual).NotTo(BeNil())
1092+
Expect(actual.Annotations["foo"]).To(Equal("bar"))
1093+
1094+
close(done)
1095+
})
1096+
1097+
It("should patch an existing object non-namespace object from a go struct", func(done Done) {
1098+
cl, err := client.New(cfg, client.Options{})
1099+
Expect(err).NotTo(HaveOccurred())
1100+
Expect(cl).NotTo(BeNil())
1101+
1102+
By("initially creating a Node")
1103+
node, err := clientset.CoreV1().Nodes().Create(node)
1104+
Expect(err).NotTo(HaveOccurred())
1105+
1106+
By("patching the Node")
1107+
nodeName := node.Name
1108+
u := &unstructured.Unstructured{}
1109+
scheme.Convert(node, u, nil)
1110+
u.SetGroupVersionKind(schema.GroupVersionKind{
1111+
Group: "",
1112+
Kind: "Node",
1113+
Version: "v1",
1114+
})
1115+
err = cl.Patch(context.TODO(), u, client.ConstantPatch(types.MergePatchType, mergePatch))
1116+
Expect(err).NotTo(HaveOccurred())
1117+
1118+
By("validating pathed Node has new annotation")
1119+
actual, err := clientset.CoreV1().Nodes().Get(nodeName, metav1.GetOptions{})
1120+
Expect(err).NotTo(HaveOccurred())
1121+
Expect(actual).NotTo(BeNil())
1122+
Expect(actual.Annotations["foo"]).To(Equal("bar"))
1123+
1124+
close(done)
1125+
})
1126+
1127+
It("should fail if the object does not exist", func(done Done) {
1128+
cl, err := client.New(cfg, client.Options{})
1129+
Expect(err).NotTo(HaveOccurred())
1130+
Expect(cl).NotTo(BeNil())
1131+
1132+
By("Patching node before it is ever created")
1133+
u := &unstructured.Unstructured{}
1134+
scheme.Convert(node, u, nil)
1135+
u.SetGroupVersionKind(schema.GroupVersionKind{
1136+
Group: "",
1137+
Kind: "Node",
1138+
Version: "v1",
1139+
})
1140+
err = cl.Patch(context.TODO(), node, client.ConstantPatch(types.MergePatchType, mergePatch))
1141+
Expect(err).To(HaveOccurred())
1142+
1143+
close(done)
1144+
})
1145+
})
1146+
})
1147+
9671148
Describe("Get", func() {
9681149
Context("with structured objects", func() {
9691150
It("should fetch an existing object for a go struct", func(done Done) {
@@ -1984,6 +2165,46 @@ var _ = Describe("DelegatingReader", func() {
19842165
})
19852166
})
19862167

2168+
var _ = Describe("Patch", func() {
2169+
Describe("CreateMergePatch", func() {
2170+
var cm *corev1.ConfigMap
2171+
2172+
BeforeEach(func() {
2173+
cm = &corev1.ConfigMap{
2174+
ObjectMeta: metav1.ObjectMeta{
2175+
Namespace: metav1.NamespaceDefault,
2176+
Name: "cm",
2177+
},
2178+
}
2179+
})
2180+
2181+
It("creates a merge patch with the modifications applied during the mutation", func() {
2182+
const (
2183+
annotationKey = "test"
2184+
annotationValue = "foo"
2185+
)
2186+
2187+
By("creating a merge patch")
2188+
patch := client.MergeFrom(cm.DeepCopy())
2189+
2190+
By("returning a patch with type MergePatch")
2191+
Expect(patch.Type()).To(Equal(types.MergePatchType))
2192+
2193+
By("retrieving modifying the config map")
2194+
metav1.SetMetaDataAnnotation(&cm.ObjectMeta, annotationKey, annotationValue)
2195+
2196+
By("computing the patch data")
2197+
data, err := patch.Data(cm)
2198+
2199+
By("returning no error")
2200+
Expect(err).NotTo(HaveOccurred())
2201+
2202+
By("returning a patch with data only containing the annotation change")
2203+
Expect(data).To(Equal([]byte(fmt.Sprintf(`{"metadata":{"annotations":{"%s":"%s"}}}`, annotationKey, annotationValue))))
2204+
})
2205+
})
2206+
})
2207+
19872208
type fakeReader struct {
19882209
Called int
19892210
}

pkg/client/fake/client.go

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -194,6 +194,38 @@ func (c *fakeClient) Update(ctx context.Context, obj runtime.Object, opts ...cli
194194
return c.tracker.Update(gvr, obj, accessor.GetNamespace())
195195
}
196196

197+
func (c *fakeClient) Patch(ctx context.Context, obj runtime.Object, patch client.Patch, opts ...client.PatchOptionFunc) error {
198+
gvr, err := getGVRFromObject(obj, c.scheme)
199+
if err != nil {
200+
return err
201+
}
202+
accessor, err := meta.Accessor(obj)
203+
if err != nil {
204+
return err
205+
}
206+
data, err := patch.Data(obj)
207+
if err != nil {
208+
return err
209+
}
210+
211+
reaction := testing.ObjectReaction(c.tracker)
212+
handled, o, err := reaction(testing.NewPatchAction(gvr, accessor.GetNamespace(), accessor.GetName(), patch.Type(), data))
213+
if err != nil {
214+
return err
215+
}
216+
if !handled {
217+
panic("tracker could not handle patch method")
218+
}
219+
220+
j, err := json.Marshal(o)
221+
if err != nil {
222+
return err
223+
}
224+
decoder := scheme.Codecs.UniversalDecoder()
225+
_, _, err = decoder.Decode(j, nil, obj)
226+
return err
227+
}
228+
197229
func (c *fakeClient) Status() client.StatusWriter {
198230
return &fakeStatusWriter{client: c}
199231
}

pkg/client/fake/client_test.go

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,8 @@ limitations under the License.
1717
package fake
1818

1919
import (
20+
"encoding/json"
21+
2022
. "github.com/onsi/ginkgo"
2123
. "github.com/onsi/gomega"
2224

@@ -205,6 +207,30 @@ var _ = Describe("Fake client", func() {
205207
Expect(obj).To(Equal(cm))
206208
})
207209
})
210+
211+
It("should be able to Patch", func() {
212+
By("Patching a deployment")
213+
mergePatch, err := json.Marshal(map[string]interface{}{
214+
"metadata": map[string]interface{}{
215+
"annotations": map[string]interface{}{
216+
"foo": "bar",
217+
},
218+
},
219+
})
220+
Expect(err).NotTo(HaveOccurred())
221+
err = cl.Patch(nil, dep, client.ConstantPatch(types.JSONPatchType, mergePatch))
222+
Expect(err).NotTo(HaveOccurred())
223+
224+
By("Getting the patched deployment")
225+
namespacedName := types.NamespacedName{
226+
Name: "test-deployment",
227+
Namespace: "ns1",
228+
}
229+
obj := &appsv1.Deployment{}
230+
err = cl.Get(nil, namespacedName, obj)
231+
Expect(err).NotTo(HaveOccurred())
232+
Expect(obj.Annotations["foo"]).To(Equal("bar"))
233+
})
208234
}
209235

210236
Context("with default scheme.Scheme", func() {

0 commit comments

Comments
 (0)