@@ -29,24 +29,28 @@ import (
2929
3030runtimev1 "sigs.k8s.io/cluster-api/api/runtime/v1beta2"
3131runtimecatalog "sigs.k8s.io/cluster-api/exp/runtime/catalog"
32- "sigs.k8s.io/cluster-api/util/patch"
3332)
3433
3534// MarkAsPending adds to the object's PendingHooksAnnotation the intent to execute a hook after an operation completes.
3635// Usually this function is called when an operation is starting in order to track the intent to call an After<operation> hook later in the process.
37- func MarkAsPending (ctx context.Context , c client.Client , obj client.Object , hooks ... runtimecatalog.Hook ) error {
36+ func MarkAsPending (ctx context.Context , c client.Client , obj client.Object , updateResourceVersionOnObject bool , hooks ... runtimecatalog.Hook ) error {
3837hookNames := []string {}
3938for _ , hook := range hooks {
4039hookNames = append (hookNames , runtimecatalog .HookName (hook ))
4140}
4241
43- patchHelper , err := patch .NewHelper (obj , c )
44- if err != nil {
45- return errors .Wrapf (err , "failed to mark %q hook(s) as pending" , strings .Join (hookNames , "," ))
42+ orig := obj .DeepCopyObject ().(client.Object )
43+
44+ if changed := MarkObjectAsPending (obj , hooks ... ); ! changed {
45+ return nil
4646}
4747
48- MarkObjectAsPending (obj , hooks ... )
49- if err := patchHelper .Patch (ctx , obj ); err != nil {
48+ // In some cases it is preferred to not update resourceVersion in the input object,
49+ // because this could lead to conflict errors e.g. when patching at the end of a reconcile loop.
50+ if ! updateResourceVersionOnObject {
51+ obj = obj .DeepCopyObject ().(client.Object )
52+ }
53+ if err := c .Patch (ctx , obj , client .MergeFrom (orig )); err != nil {
5054return errors .Wrapf (err , "failed to mark %q hook(s) as pending" , strings .Join (hookNames , "," ))
5155}
5256
@@ -55,7 +59,7 @@ func MarkAsPending(ctx context.Context, c client.Client, obj client.Object, hook
5559
5660// MarkObjectAsPending adds to the object's PendingHooksAnnotation the intent to execute a hook after an operation completes.
5761// Usually this function is called when an operation is starting in order to track the intent to call an After<operation> hook later in the process.
58- func MarkObjectAsPending (obj client.Object , hooks ... runtimecatalog.Hook ) {
62+ func MarkObjectAsPending (obj client.Object , hooks ... runtimecatalog.Hook ) ( changed bool ) {
5963hookNames := []string {}
6064for _ , hook := range hooks {
6165hookNames = append (hookNames , runtimecatalog .HookName (hook ))
@@ -66,8 +70,16 @@ func MarkObjectAsPending(obj client.Object, hooks ...runtimecatalog.Hook) {
6670if annotations == nil {
6771annotations = map [string ]string {}
6872}
69- annotations [runtimev1 .PendingHooksAnnotation ] = addToCommaSeparatedList (annotations [runtimev1 .PendingHooksAnnotation ], hookNames ... )
73+
74+ newAnnotationValue := addToCommaSeparatedList (annotations [runtimev1 .PendingHooksAnnotation ], hookNames ... )
75+
76+ if annotations [runtimev1 .PendingHooksAnnotation ] == newAnnotationValue {
77+ return false
78+ }
79+
80+ annotations [runtimev1 .PendingHooksAnnotation ] = newAnnotationValue
7081obj .SetAnnotations (annotations )
82+ return true
7183}
7284
7385// IsPending returns true if there is an intent to call a hook being tracked in the object's PendingHooksAnnotation.
@@ -83,30 +95,33 @@ func IsPending(hook runtimecatalog.Hook, obj client.Object) bool {
8395// MarkAsDone removes the intent to call a Hook from the object's PendingHooksAnnotation.
8496// Usually this func is called after all the registered extensions for the Hook returned an answer without requests
8597// to hold on to the object's lifecycle (retryAfterSeconds).
86- func MarkAsDone (ctx context.Context , c client.Client , obj client.Object , hooks ... runtimecatalog.Hook ) error {
87- hookNames := []string {}
88- for _ , hook := range hooks {
89- hookNames = append (hookNames , runtimecatalog .HookName (hook ))
98+ func MarkAsDone (ctx context.Context , c client.Client , obj client.Object , updateResourceVersionOnObject bool , hook runtimecatalog.Hook ) error {
99+ if ! IsPending (hook , obj ) {
100+ return nil
90101}
91102
92- patchHelper , err := patch .NewHelper (obj , c )
93- if err != nil {
94- return errors .Wrapf (err , "failed to mark %q hook(s) as done" , strings .Join (hookNames , "," ))
95- }
103+ hookName := runtimecatalog .HookName (hook )
104+
105+ orig := obj .DeepCopyObject ().(client.Object )
96106
97107// Read the annotation of the objects and add the hook to the comma separated list
98108annotations := obj .GetAnnotations ()
99109if annotations == nil {
100110annotations = map [string ]string {}
101111}
102- annotations [runtimev1 .PendingHooksAnnotation ] = removeFromCommaSeparatedList (annotations [runtimev1 .PendingHooksAnnotation ], hookNames ... )
112+ annotations [runtimev1 .PendingHooksAnnotation ] = removeFromCommaSeparatedList (annotations [runtimev1 .PendingHooksAnnotation ], hookName )
103113if annotations [runtimev1 .PendingHooksAnnotation ] == "" {
104114delete (annotations , runtimev1 .PendingHooksAnnotation )
105115}
106116obj .SetAnnotations (annotations )
107117
108- if err := patchHelper .Patch (ctx , obj ); err != nil {
109- return errors .Wrapf (err , "failed to mark %q hook(s) as done" , strings .Join (hookNames , "," ))
118+ // In some cases it is preferred to not update resourceVersion in the input object,
119+ // because this could lead to conflict errors e.g. when patching at the end of a reconcile loop.
120+ if ! updateResourceVersionOnObject {
121+ obj = obj .DeepCopyObject ().(client.Object )
122+ }
123+ if err := c .Patch (ctx , obj , client .MergeFrom (orig )); err != nil {
124+ return errors .Wrapf (err , "failed to mark %q hook as done" , hookName )
110125}
111126
112127return nil
@@ -125,16 +140,17 @@ func IsOkToDelete(obj client.Object) bool {
125140}
126141
127142// MarkAsOkToDelete adds the OkToDeleteAnnotation annotation to the object and patches it.
128- func MarkAsOkToDelete (ctx context.Context , c client.Client , obj client.Object ) error {
143+ func MarkAsOkToDelete (ctx context.Context , c client.Client , obj client.Object , updateResourceVersionOnObject bool ) error {
144+ if _ , ok := obj .GetAnnotations ()[runtimev1 .OkToDeleteAnnotation ]; ok {
145+ return nil
146+ }
147+
129148gvk , err := apiutil .GVKForObject (obj , c .Scheme ())
130149if err != nil {
131150return errors .Wrapf (err , "failed to mark %s as ok to delete: failed to get GVK for object" , klog .KObj (obj ))
132151}
133152
134- patchHelper , err := patch .NewHelper (obj , c )
135- if err != nil {
136- return errors .Wrapf (err , "failed to mark %s %s as ok to delete" , gvk .Kind , klog .KObj (obj ))
137- }
153+ orig := obj .DeepCopyObject ().(client.Object )
138154
139155annotations := obj .GetAnnotations ()
140156if annotations == nil {
@@ -143,7 +159,12 @@ func MarkAsOkToDelete(ctx context.Context, c client.Client, obj client.Object) e
143159annotations [runtimev1 .OkToDeleteAnnotation ] = ""
144160obj .SetAnnotations (annotations )
145161
146- if err := patchHelper .Patch (ctx , obj ); err != nil {
162+ // In some cases it is preferred to not update resourceVersion in the input object,
163+ // because this could lead to conflict errors e.g. when patching at the end of a reconcile loop.
164+ if ! updateResourceVersionOnObject {
165+ obj = obj .DeepCopyObject ().(client.Object )
166+ }
167+ if err := c .Patch (ctx , obj , client .MergeFrom (orig )); err != nil {
147168return errors .Wrapf (err , "failed to mark %s %s as ok to delete" , gvk .Kind , klog .KObj (obj ))
148169}
149170
0 commit comments