@@ -29,24 +29,28 @@ import (
2929
3030 runtimev1 "sigs.k8s.io/cluster-api/api/runtime/v1beta2"
3131 runtimecatalog "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 {
3837 hookNames := []string {}
3938 for _ , hook := range hooks {
4039 hookNames = 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 {
5054 return 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 ) {
5963 hookNames := []string {}
6064 for _ , hook := range hooks {
6165 hookNames = append (hookNames , runtimecatalog .HookName (hook ))
@@ -66,8 +70,16 @@ func MarkObjectAsPending(obj client.Object, hooks ...runtimecatalog.Hook) {
6670 if annotations == nil {
6771 annotations = 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
7081 obj .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
98108 annotations := obj .GetAnnotations ()
99109 if annotations == nil {
100110 annotations = map [string ]string {}
101111 }
102- annotations [runtimev1 .PendingHooksAnnotation ] = removeFromCommaSeparatedList (annotations [runtimev1 .PendingHooksAnnotation ], hookNames ... )
112+ annotations [runtimev1 .PendingHooksAnnotation ] = removeFromCommaSeparatedList (annotations [runtimev1 .PendingHooksAnnotation ], hookName )
103113 if annotations [runtimev1 .PendingHooksAnnotation ] == "" {
104114 delete (annotations , runtimev1 .PendingHooksAnnotation )
105115 }
106116 obj .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
112127 return 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+
129148 gvk , err := apiutil .GVKForObject (obj , c .Scheme ())
130149 if err != nil {
131150 return 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
139155 annotations := obj .GetAnnotations ()
140156 if annotations == nil {
@@ -143,7 +159,12 @@ func MarkAsOkToDelete(ctx context.Context, c client.Client, obj client.Object) e
143159 annotations [runtimev1 .OkToDeleteAnnotation ] = ""
144160 obj .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 {
147168 return errors .Wrapf (err , "failed to mark %s %s as ok to delete" , gvk .Kind , klog .KObj (obj ))
148169 }
149170
0 commit comments