Skip to content

Commit f9ce06d

Browse files
committed
✨ Add Patch MergeFrom optimistic locking option
Signed-off-by: Vince Prignano <[email protected]>
1 parent c45adcf commit f9ce06d

File tree

2 files changed

+126
-4
lines changed

2 files changed

+126
-4
lines changed

pkg/client/client_test.go

Lines changed: 64 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1209,6 +1209,42 @@ var _ = Describe("Client", func() {
12091209
close(done)
12101210
})
12111211

1212+
It("should patch an existing object from a go struct, using optimistic locking", func(done Done) {
1213+
cl, err := client.New(cfg, client.Options{})
1214+
Expect(err).NotTo(HaveOccurred())
1215+
Expect(cl).NotTo(BeNil())
1216+
1217+
By("initially creating a Deployment")
1218+
dep, err := clientset.AppsV1().Deployments(ns).Create(dep)
1219+
Expect(err).NotTo(HaveOccurred())
1220+
1221+
By("creating a patch from with optimistic lock")
1222+
patch := client.MergeFromWithOptions(dep.DeepCopy(), client.MergeFromWithOptimisticLock{})
1223+
1224+
By("adding a new annotation")
1225+
dep.Annotations = map[string]string{
1226+
"foo": "bar",
1227+
}
1228+
1229+
By("patching the Deployment")
1230+
err = cl.Patch(context.TODO(), dep, patch)
1231+
Expect(err).NotTo(HaveOccurred())
1232+
1233+
By("validating patched Deployment has new annotation")
1234+
actual, err := clientset.AppsV1().Deployments(ns).Get(dep.Name, metav1.GetOptions{})
1235+
Expect(err).NotTo(HaveOccurred())
1236+
Expect(actual).NotTo(BeNil())
1237+
Expect(actual.Annotations["foo"]).To(Equal("bar"))
1238+
1239+
By("validating that a patch should fail with conflict, when it has an outdated resource version")
1240+
dep.Annotations["should"] = "conflict"
1241+
err = cl.Patch(context.TODO(), dep, patch)
1242+
Expect(err).To(HaveOccurred())
1243+
Expect(apierrors.IsConflict(err)).To(BeTrue())
1244+
1245+
close(done)
1246+
})
1247+
12121248
It("should patch and preserve type information", func(done Done) {
12131249
cl, err := client.New(cfg, client.Options{})
12141250
Expect(err).NotTo(HaveOccurred())
@@ -2655,8 +2691,9 @@ var _ = Describe("Patch", func() {
26552691
BeforeEach(func() {
26562692
cm = &corev1.ConfigMap{
26572693
ObjectMeta: metav1.ObjectMeta{
2658-
Namespace: metav1.NamespaceDefault,
2659-
Name: "cm",
2694+
Namespace: metav1.NamespaceDefault,
2695+
Name: "cm",
2696+
ResourceVersion: "10",
26602697
},
26612698
}
26622699
})
@@ -2685,6 +2722,31 @@ var _ = Describe("Patch", func() {
26852722
By("returning a patch with data only containing the annotation change")
26862723
Expect(data).To(Equal([]byte(fmt.Sprintf(`{"metadata":{"annotations":{"%s":"%s"}}}`, annotationKey, annotationValue))))
26872724
})
2725+
2726+
It("creates a merge patch with the modifications applied during the mutation, using optimistic locking", func() {
2727+
const (
2728+
annotationKey = "test"
2729+
annotationValue = "foo"
2730+
)
2731+
2732+
By("creating a merge patch")
2733+
patch := client.MergeFromWithOptions(cm.DeepCopy(), client.MergeFromWithOptimisticLock{})
2734+
2735+
By("returning a patch with type MergePatch")
2736+
Expect(patch.Type()).To(Equal(types.MergePatchType))
2737+
2738+
By("retrieving modifying the config map")
2739+
metav1.SetMetaDataAnnotation(&cm.ObjectMeta, annotationKey, annotationValue)
2740+
2741+
By("computing the patch data")
2742+
data, err := patch.Data(cm)
2743+
2744+
By("returning no error")
2745+
Expect(err).NotTo(HaveOccurred())
2746+
2747+
By("returning a patch with data containing the annotation change and the resourceVersion change")
2748+
Expect(data).To(Equal([]byte(fmt.Sprintf(`{"metadata":{"annotations":{"%s":"%s"},"resourceVersion":"%s"}}`, annotationKey, annotationValue, cm.ResourceVersion))))
2749+
})
26882750
})
26892751
})
26902752

pkg/client/patch.go

Lines changed: 62 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,11 @@ limitations under the License.
1717
package client
1818

1919
import (
20+
"fmt"
21+
2022
jsonpatch "github.com/evanphx/json-patch"
23+
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
24+
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
2125
"k8s.io/apimachinery/pkg/runtime"
2226
"k8s.io/apimachinery/pkg/types"
2327
"k8s.io/apimachinery/pkg/util/json"
@@ -59,8 +63,29 @@ func ConstantPatch(patchType types.PatchType, data []byte) Patch {
5963
return RawPatch(patchType, data)
6064
}
6165

66+
type MergeFromWithOptimisticLock struct{}
67+
68+
func (m MergeFromWithOptimisticLock) ApplyToMergeFrom(in *MergeFromOptions) {
69+
in.OptimisticLock = true
70+
}
71+
72+
// MergeFromOption is some configuration that modifies options for a merge-from patch data.
73+
type MergeFromOption interface {
74+
// ApplyToMergeFrom applies this configuration to the given patch options.
75+
ApplyToMergeFrom(*MergeFromOptions)
76+
}
77+
78+
// MergeFromOptions contains options to generate a merge-from patch data.
79+
type MergeFromOptions struct {
80+
// OptimisticLock, when true, includes `metadata.resourceVersion` into the final
81+
// patch data. If the `resourceVersion` field doesn't match what's stored,
82+
// the operation results in a conflict and clients will need to try again.
83+
OptimisticLock bool
84+
}
85+
6286
type mergeFromPatch struct {
6387
from runtime.Object
88+
opts MergeFromOptions
6489
}
6590

6691
// Type implements patch.
@@ -80,12 +105,47 @@ func (s *mergeFromPatch) Data(obj runtime.Object) ([]byte, error) {
80105
return nil, err
81106
}
82107

83-
return jsonpatch.CreateMergePatch(originalJSON, modifiedJSON)
108+
data, err := jsonpatch.CreateMergePatch(originalJSON, modifiedJSON)
109+
if err != nil {
110+
return nil, err
111+
}
112+
113+
if s.opts.OptimisticLock {
114+
dataMap := map[string]interface{}{}
115+
if err := json.Unmarshal(data, &dataMap); err != nil {
116+
return nil, err
117+
}
118+
fromMeta, ok := s.from.(metav1.Object)
119+
if !ok {
120+
return nil, fmt.Errorf("cannot use OptimisticLock, from object %q is not a valid metav1.Object", s.from)
121+
}
122+
resourceVersion := fromMeta.GetResourceVersion()
123+
if len(resourceVersion) == 0 {
124+
return nil, fmt.Errorf("cannot use OptimisticLock, from object %q does not have any resource version we can use", s.from)
125+
}
126+
u := &unstructured.Unstructured{Object: dataMap}
127+
u.SetResourceVersion(resourceVersion)
128+
data, err = json.Marshal(u)
129+
if err != nil {
130+
return nil, err
131+
}
132+
}
133+
134+
return data, nil
84135
}
85136

86137
// MergeFrom creates a Patch that patches using the merge-patch strategy with the given object as base.
87138
func MergeFrom(obj runtime.Object) Patch {
88-
return &mergeFromPatch{obj}
139+
return &mergeFromPatch{from: obj}
140+
}
141+
142+
// MergeFromWithOptions creates a Patch that patches using the merge-patch strategy with the given object as base.
143+
func MergeFromWithOptions(obj runtime.Object, opts ...MergeFromOption) Patch {
144+
options := &MergeFromOptions{}
145+
for _, opt := range opts {
146+
opt.ApplyToMergeFrom(options)
147+
}
148+
return &mergeFromPatch{from: obj, opts: *options}
89149
}
90150

91151
// mergePatch uses a raw merge strategy to patch the object.

0 commit comments

Comments
 (0)