Skip to content

Commit 6ec57bf

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

File tree

2 files changed

+132
-4
lines changed

2 files changed

+132
-4
lines changed

pkg/client/client_test.go

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

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

pkg/client/patch.go

Lines changed: 68 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,35 @@ func ConstantPatch(patchType types.PatchType, data []byte) Patch {
5963
return RawPatch(patchType, data)
6064
}
6165

66+
// MergeFromWithOptimisticLock can be used if clients want to make sure a patch
67+
// is being applied to the latest resource version of an object.
68+
//
69+
// The behavior is similar to what an Update would do, without the need to send the
70+
// whole object. Usually this method is useful if you might have multiple clients
71+
// acting on the same object with different API versions.
72+
type MergeFromWithOptimisticLock struct{}
73+
74+
func (m MergeFromWithOptimisticLock) ApplyToMergeFrom(in *MergeFromOptions) {
75+
in.OptimisticLock = true
76+
}
77+
78+
// MergeFromOption is some configuration that modifies options for a merge-from patch data.
79+
type MergeFromOption interface {
80+
// ApplyToMergeFrom applies this configuration to the given patch options.
81+
ApplyToMergeFrom(*MergeFromOptions)
82+
}
83+
84+
// MergeFromOptions contains options to generate a merge-from patch data.
85+
type MergeFromOptions struct {
86+
// OptimisticLock, when true, includes `metadata.resourceVersion` into the final
87+
// patch data. If the `resourceVersion` field doesn't match what's stored,
88+
// the operation results in a conflict and clients will need to try again.
89+
OptimisticLock bool
90+
}
91+
6292
type mergeFromPatch struct {
6393
from runtime.Object
94+
opts MergeFromOptions
6495
}
6596

6697
// Type implements patch.
@@ -80,12 +111,47 @@ func (s *mergeFromPatch) Data(obj runtime.Object) ([]byte, error) {
80111
return nil, err
81112
}
82113

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

86143
// MergeFrom creates a Patch that patches using the merge-patch strategy with the given object as base.
87144
func MergeFrom(obj runtime.Object) Patch {
88-
return &mergeFromPatch{obj}
145+
return &mergeFromPatch{from: obj}
146+
}
147+
148+
// MergeFromWithOptions creates a Patch that patches using the merge-patch strategy with the given object as base.
149+
func MergeFromWithOptions(obj runtime.Object, opts ...MergeFromOption) Patch {
150+
options := &MergeFromOptions{}
151+
for _, opt := range opts {
152+
opt.ApplyToMergeFrom(options)
153+
}
154+
return &mergeFromPatch{from: obj, opts: *options}
89155
}
90156

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

0 commit comments

Comments
 (0)