Skip to content

Commit 0e0c86c

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 0e0c86c

File tree

11 files changed

+415
-0
lines changed

11 files changed

+415
-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: 181 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.NewPatch(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.NewPatch(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.NewPatch(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.NewPatch(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.NewPatch(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.NewPatch(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.NewPatch(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) {

pkg/client/fake/client.go

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -194,6 +194,34 @@ 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+
207+
reaction := testing.ObjectReaction(c.tracker)
208+
handled, o, err := reaction(testing.NewPatchAction(gvr, accessor.GetNamespace(), accessor.GetName(), patch.Type(), patch.Data()))
209+
if err != nil {
210+
return err
211+
}
212+
if !handled {
213+
panic("tracker could not handle patch method")
214+
}
215+
216+
j, err := json.Marshal(o)
217+
if err != nil {
218+
return err
219+
}
220+
decoder := scheme.Codecs.UniversalDecoder()
221+
_, _, err = decoder.Decode(j, nil, obj)
222+
return err
223+
}
224+
197225
func (c *fakeClient) Status() client.StatusWriter {
198226
return &fakeStatusWriter{client: c}
199227
}

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.NewPatch(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() {

pkg/client/interfaces.go

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,14 @@ func ObjectKeyFromObject(obj runtime.Object) (ObjectKey, error) {
3939
return ObjectKey{Namespace: accessor.GetNamespace(), Name: accessor.GetName()}, nil
4040
}
4141

42+
// Patch is a patch that can be applied to a Kubernetes object.
43+
type Patch interface {
44+
// Type is the PatchType of the patch.
45+
Type() types.PatchType
46+
// Data is the raw data representing the patch.
47+
Data() []byte
48+
}
49+
4250
// TODO(directxman12): is there a sane way to deal with get/delete options?
4351

4452
// Reader knows how to read and list Kubernetes objects.
@@ -65,6 +73,10 @@ type Writer interface {
6573
// Update updates the given obj in the Kubernetes cluster. obj must be a
6674
// struct pointer so that obj can be updated with the content returned by the Server.
6775
Update(ctx context.Context, obj runtime.Object, opts ...UpdateOptionFunc) error
76+
77+
// Patch patches the given obj in the Kubernetes cluster. obj must be a
78+
// struct pointer so that obj can be updated with the content returned by the Server.
79+
Patch(ctx context.Context, obj runtime.Object, patch Patch, opts ...PatchOptionFunc) error
6880
}
6981

7082
// StatusClient knows how to create a client which can update status subresource
@@ -428,3 +440,12 @@ func UpdateDryRunAll() UpdateOptionFunc {
428440
opts.DryRun = []string{metav1.DryRunAll}
429441
}
430442
}
443+
444+
// PatchOptions contains options for patch requests.
445+
type PatchOptions struct {
446+
}
447+
448+
// PatchOptionFunc is a function that mutates a PatchOptions struct. It implements
449+
// the functional options pattern. See
450+
// https://github.com/tmrts/go-patterns/blob/master/idiom/functional-options.md.
451+
type PatchOptionFunc func(*PatchOptions)

pkg/client/patch.go

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
/*
2+
Copyright 2018 The Kubernetes Authors.
3+
4+
Licensed under the Apache License, Version 2.0 (the "License");
5+
you may not use this file except in compliance with the License.
6+
You may obtain a copy of the License at
7+
8+
http://www.apache.org/licenses/LICENSE-2.0
9+
10+
Unless required by applicable law or agreed to in writing, software
11+
distributed under the License is distributed on an "AS IS" BASIS,
12+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
See the License for the specific language governing permissions and
14+
limitations under the License.
15+
*/
16+
17+
package client
18+
19+
import (
20+
"k8s.io/apimachinery/pkg/types"
21+
)
22+
23+
type patch struct {
24+
patchType types.PatchType
25+
data []byte
26+
}
27+
28+
// Type implements Patch.
29+
func (s *patch) Type() types.PatchType {
30+
return s.patchType
31+
}
32+
33+
// Data implements Patch.
34+
func (s *patch) Data() []byte {
35+
return s.data
36+
}
37+
38+
// NewPatch constructs a new Patch with the given PatchType and data.
39+
func NewPatch(patchType types.PatchType, data []byte) Patch {
40+
return &patch{patchType, data}
41+
}

0 commit comments

Comments
 (0)