Skip to content

Commit 8d94f66

Browse files
authored
Merge pull request #235 from adracus/feature.patch
⚠️ Implement Patch method
2 parents 276610b + cd1f7a6 commit 8d94f66

File tree

9 files changed

+504
-0
lines changed

9 files changed

+504
-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: 271 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,224 @@ 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+
It("should respect passed in update options", func() {
1067+
By("creating a new client")
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 with dry-run")
1077+
err = cl.Patch(context.TODO(), dep, client.ConstantPatch(types.MergePatchType, mergePatch), client.UpdatePatchWith(client.UpdateDryRunAll()))
1078+
Expect(err).NotTo(HaveOccurred())
1079+
1080+
By("validating patched Deployment doesn't have the new annotation")
1081+
actual, err := clientset.AppsV1().Deployments(ns).Get(dep.Name, metav1.GetOptions{})
1082+
Expect(err).NotTo(HaveOccurred())
1083+
Expect(actual).NotTo(BeNil())
1084+
Expect(actual.Annotations).NotTo(HaveKey("foo"))
1085+
})
1086+
})
1087+
Context("with unstructured objects", func() {
1088+
It("should patch an existing object from a go struct", func(done Done) {
1089+
cl, err := client.New(cfg, client.Options{})
1090+
Expect(err).NotTo(HaveOccurred())
1091+
Expect(cl).NotTo(BeNil())
1092+
1093+
By("initially creating a Deployment")
1094+
dep, err := clientset.AppsV1().Deployments(ns).Create(dep)
1095+
Expect(err).NotTo(HaveOccurred())
1096+
1097+
By("patching the Deployment")
1098+
depName := dep.Name
1099+
u := &unstructured.Unstructured{}
1100+
scheme.Convert(dep, u, nil)
1101+
u.SetGroupVersionKind(schema.GroupVersionKind{
1102+
Group: "apps",
1103+
Kind: "Deployment",
1104+
Version: "v1",
1105+
})
1106+
err = cl.Patch(context.TODO(), u, client.ConstantPatch(types.MergePatchType, mergePatch))
1107+
Expect(err).NotTo(HaveOccurred())
1108+
1109+
By("validating patched Deployment has new annotation")
1110+
actual, err := clientset.AppsV1().Deployments(ns).Get(depName, metav1.GetOptions{})
1111+
Expect(err).NotTo(HaveOccurred())
1112+
Expect(actual).NotTo(BeNil())
1113+
Expect(actual.Annotations["foo"]).To(Equal("bar"))
1114+
1115+
close(done)
1116+
})
1117+
1118+
It("should patch an existing object non-namespace object from a go struct", func(done Done) {
1119+
cl, err := client.New(cfg, client.Options{})
1120+
Expect(err).NotTo(HaveOccurred())
1121+
Expect(cl).NotTo(BeNil())
1122+
1123+
By("initially creating a Node")
1124+
node, err := clientset.CoreV1().Nodes().Create(node)
1125+
Expect(err).NotTo(HaveOccurred())
1126+
1127+
By("patching the Node")
1128+
nodeName := node.Name
1129+
u := &unstructured.Unstructured{}
1130+
scheme.Convert(node, u, nil)
1131+
u.SetGroupVersionKind(schema.GroupVersionKind{
1132+
Group: "",
1133+
Kind: "Node",
1134+
Version: "v1",
1135+
})
1136+
err = cl.Patch(context.TODO(), u, client.ConstantPatch(types.MergePatchType, mergePatch))
1137+
Expect(err).NotTo(HaveOccurred())
1138+
1139+
By("validating pathed Node has new annotation")
1140+
actual, err := clientset.CoreV1().Nodes().Get(nodeName, metav1.GetOptions{})
1141+
Expect(err).NotTo(HaveOccurred())
1142+
Expect(actual).NotTo(BeNil())
1143+
Expect(actual.Annotations["foo"]).To(Equal("bar"))
1144+
1145+
close(done)
1146+
})
1147+
1148+
It("should fail if the object does not exist", func(done Done) {
1149+
cl, err := client.New(cfg, client.Options{})
1150+
Expect(err).NotTo(HaveOccurred())
1151+
Expect(cl).NotTo(BeNil())
1152+
1153+
By("Patching node before it is ever created")
1154+
u := &unstructured.Unstructured{}
1155+
scheme.Convert(node, u, nil)
1156+
u.SetGroupVersionKind(schema.GroupVersionKind{
1157+
Group: "",
1158+
Kind: "Node",
1159+
Version: "v1",
1160+
})
1161+
err = cl.Patch(context.TODO(), node, client.ConstantPatch(types.MergePatchType, mergePatch))
1162+
Expect(err).To(HaveOccurred())
1163+
1164+
close(done)
1165+
})
1166+
1167+
It("should respect passed-in update options", func() {
1168+
By("creating a new client")
1169+
cl, err := client.New(cfg, client.Options{})
1170+
Expect(err).NotTo(HaveOccurred())
1171+
Expect(cl).NotTo(BeNil())
1172+
1173+
By("initially creating a Deployment")
1174+
dep, err := clientset.AppsV1().Deployments(ns).Create(dep)
1175+
Expect(err).NotTo(HaveOccurred())
1176+
1177+
By("patching the Deployment")
1178+
depName := dep.Name
1179+
u := &unstructured.Unstructured{}
1180+
scheme.Convert(dep, u, nil)
1181+
u.SetGroupVersionKind(schema.GroupVersionKind{
1182+
Group: "apps",
1183+
Kind: "Deployment",
1184+
Version: "v1",
1185+
})
1186+
err = cl.Patch(context.TODO(), u, client.ConstantPatch(types.MergePatchType, mergePatch), client.UpdatePatchWith(client.UpdateDryRunAll()))
1187+
Expect(err).NotTo(HaveOccurred())
1188+
1189+
By("validating patched Deployment does not have the new annotation")
1190+
actual, err := clientset.AppsV1().Deployments(ns).Get(depName, metav1.GetOptions{})
1191+
Expect(err).NotTo(HaveOccurred())
1192+
Expect(actual).NotTo(BeNil())
1193+
Expect(actual.Annotations).NotTo(HaveKey("foo"))
1194+
})
1195+
})
1196+
})
1197+
9671198
Describe("Get", func() {
9681199
Context("with structured objects", func() {
9691200
It("should fetch an existing object for a go struct", func(done Done) {
@@ -1984,6 +2215,46 @@ var _ = Describe("DelegatingReader", func() {
19842215
})
19852216
})
19862217

2218+
var _ = Describe("Patch", func() {
2219+
Describe("CreateMergePatch", func() {
2220+
var cm *corev1.ConfigMap
2221+
2222+
BeforeEach(func() {
2223+
cm = &corev1.ConfigMap{
2224+
ObjectMeta: metav1.ObjectMeta{
2225+
Namespace: metav1.NamespaceDefault,
2226+
Name: "cm",
2227+
},
2228+
}
2229+
})
2230+
2231+
It("creates a merge patch with the modifications applied during the mutation", func() {
2232+
const (
2233+
annotationKey = "test"
2234+
annotationValue = "foo"
2235+
)
2236+
2237+
By("creating a merge patch")
2238+
patch := client.MergeFrom(cm.DeepCopy())
2239+
2240+
By("returning a patch with type MergePatch")
2241+
Expect(patch.Type()).To(Equal(types.MergePatchType))
2242+
2243+
By("retrieving modifying the config map")
2244+
metav1.SetMetaDataAnnotation(&cm.ObjectMeta, annotationKey, annotationValue)
2245+
2246+
By("computing the patch data")
2247+
data, err := patch.Data(cm)
2248+
2249+
By("returning no error")
2250+
Expect(err).NotTo(HaveOccurred())
2251+
2252+
By("returning a patch with data only containing the annotation change")
2253+
Expect(data).To(Equal([]byte(fmt.Sprintf(`{"metadata":{"annotations":{"%s":"%s"}}}`, annotationKey, annotationValue))))
2254+
})
2255+
})
2256+
})
2257+
19872258
type fakeReader struct {
19882259
Called int
19892260
}

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
}

0 commit comments

Comments
 (0)