Skip to content

Commit 2582233

Browse files
committed
Use Patch instead of Update for finalizer operations
Refactor all controllers to use Patch() instead of Update() when adding or removing finalizers to improve performance, and to avoid removing non-cached fields erroneously. This is necesary because we no longer cache the last-applied-configuration annotation, so when we add/remove the finalizers, we are removing that field from the metadata. This causes issues with clients when they don't see that annotation (e.g. apply the same ClusterExtension twice). Update ClusterCatalog and ClusterExtension controllers to use Patch-based funalizer management (ClusterExtensionRevision already uses Patch()) Signed-off-by: Todd Short <[email protected]>
1 parent aae871e commit 2582233

File tree

4 files changed

+303
-25
lines changed

4 files changed

+303
-25
lines changed

internal/catalogd/controllers/core/clustercatalog_controller.go

Lines changed: 9 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,7 @@ import (
4141
ocv1 "github.com/operator-framework/operator-controller/api/v1"
4242
"github.com/operator-framework/operator-controller/internal/catalogd/storage"
4343
imageutil "github.com/operator-framework/operator-controller/internal/shared/util/image"
44+
k8sutil "github.com/operator-framework/operator-controller/internal/shared/util/k8s"
4445
)
4546

4647
const (
@@ -107,16 +108,16 @@ func (r *ClusterCatalogReconciler) Reconcile(ctx context.Context, req ctrl.Reque
107108
// Do checks before any Update()s, as Update() may modify the resource structure!
108109
updateStatus := !equality.Semantic.DeepEqual(existingCatsrc.Status, reconciledCatsrc.Status)
109110
updateFinalizers := !equality.Semantic.DeepEqual(existingCatsrc.Finalizers, reconciledCatsrc.Finalizers)
110-
unexpectedFieldsChanged := checkForUnexpectedFieldChange(existingCatsrc, *reconciledCatsrc)
111+
unexpectedFieldsChanged := k8sutil.CheckForUnexpectedFieldChange(&existingCatsrc, reconciledCatsrc)
111112

112113
if unexpectedFieldsChanged {
113114
panic("spec or metadata changed by reconciler")
114115
}
115116

116117
// Save the finalizers off to the side. If we update the status, the reconciledCatsrc will be updated
117118
// to contain the new state of the ClusterCatalog, which contains the status update, but (critically)
118-
// does not contain the finalizers. After the status update, we need to re-add the finalizers to the
119-
// reconciledCatsrc before updating the object.
119+
// does not contain the finalizers. After the status update, we will use the saved finalizers in the
120+
// CreateOrPatch()
120121
finalizers := reconciledCatsrc.Finalizers
121122

122123
if updateStatus {
@@ -125,10 +126,12 @@ func (r *ClusterCatalogReconciler) Reconcile(ctx context.Context, req ctrl.Reque
125126
}
126127
}
127128

128-
reconciledCatsrc.Finalizers = finalizers
129-
130129
if updateFinalizers {
131-
if err := r.Update(ctx, reconciledCatsrc); err != nil {
130+
// Use CreateOrPatch to update finalizers on the server
131+
if _, err := controllerutil.CreateOrPatch(ctx, r.Client, reconciledCatsrc, func() error {
132+
reconciledCatsrc.Finalizers = finalizers
133+
return nil
134+
}); err != nil {
132135
reconcileErr = errors.Join(reconcileErr, fmt.Errorf("error updating finalizers: %v", err))
133136
}
134137
}
@@ -415,13 +418,6 @@ func (r *ClusterCatalogReconciler) needsPoll(lastSuccessfulPoll time.Time, catal
415418
return nextPoll.Before(time.Now())
416419
}
417420

418-
// Compare resources - ignoring status & metadata.finalizers
419-
func checkForUnexpectedFieldChange(a, b ocv1.ClusterCatalog) bool {
420-
a.Status, b.Status = ocv1.ClusterCatalogStatus{}, ocv1.ClusterCatalogStatus{}
421-
a.Finalizers, b.Finalizers = []string{}, []string{}
422-
return !equality.Semantic.DeepEqual(a, b)
423-
}
424-
425421
type finalizerFunc func(ctx context.Context, obj client.Object) (crfinalizer.Result, error)
426422

427423
func (f finalizerFunc) Finalize(ctx context.Context, obj client.Object) (crfinalizer.Result, error) {

internal/operator-controller/controllers/clusterextension_controller.go

Lines changed: 10 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@ import (
3535
"sigs.k8s.io/controller-runtime/pkg/builder"
3636
"sigs.k8s.io/controller-runtime/pkg/client"
3737
crcontroller "sigs.k8s.io/controller-runtime/pkg/controller"
38+
"sigs.k8s.io/controller-runtime/pkg/controller/controllerutil"
3839
"sigs.k8s.io/controller-runtime/pkg/event"
3940
crhandler "sigs.k8s.io/controller-runtime/pkg/handler"
4041
"sigs.k8s.io/controller-runtime/pkg/log"
@@ -48,6 +49,7 @@ import (
4849
ocv1 "github.com/operator-framework/operator-controller/api/v1"
4950
"github.com/operator-framework/operator-controller/internal/operator-controller/conditionsets"
5051
"github.com/operator-framework/operator-controller/internal/operator-controller/labels"
52+
k8sutil "github.com/operator-framework/operator-controller/internal/shared/util/k8s"
5153
)
5254

5355
const (
@@ -135,25 +137,28 @@ func (r *ClusterExtensionReconciler) Reconcile(ctx context.Context, req ctrl.Req
135137
updateFinalizers := !equality.Semantic.DeepEqual(existingExt.Finalizers, reconciledExt.Finalizers)
136138

137139
// If any unexpected fields have changed, panic before updating the resource
138-
unexpectedFieldsChanged := checkForUnexpectedClusterExtensionFieldChange(*existingExt, *reconciledExt)
140+
unexpectedFieldsChanged := k8sutil.CheckForUnexpectedFieldChange(existingExt, reconciledExt)
139141
if unexpectedFieldsChanged {
140142
panic("spec or metadata changed by reconciler")
141143
}
142144

143145
// Save the finalizers off to the side. If we update the status, the reconciledExt will be updated
144146
// to contain the new state of the ClusterExtension, which contains the status update, but (critically)
145-
// does not contain the finalizers. After the status update, we need to re-add the finalizers to the
146-
// reconciledExt before updating the object.
147+
// does not contain the finalizers. After the status update, we will use the saved finalizers in the
148+
// CreateOrPatch()
147149
finalizers := reconciledExt.Finalizers
148150
if updateStatus {
149151
if err := r.Client.Status().Update(ctx, reconciledExt); err != nil {
150152
reconcileErr = errors.Join(reconcileErr, fmt.Errorf("error updating status: %v", err))
151153
}
152154
}
153-
reconciledExt.Finalizers = finalizers
154155

155156
if updateFinalizers {
156-
if err := r.Update(ctx, reconciledExt); err != nil {
157+
// Use CreateOrPatch to update finalizers on the server
158+
if _, err := controllerutil.CreateOrPatch(ctx, r.Client, reconciledExt, func() error {
159+
reconciledExt.Finalizers = finalizers
160+
return nil
161+
}); err != nil {
157162
reconcileErr = errors.Join(reconcileErr, fmt.Errorf("error updating finalizers: %v", err))
158163
}
159164
}
@@ -179,13 +184,6 @@ func ensureAllConditionsWithReason(ext *ocv1.ClusterExtension, reason v1alpha1.C
179184
}
180185
}
181186

182-
// Compare resources - ignoring status & metadata.finalizers
183-
func checkForUnexpectedClusterExtensionFieldChange(a, b ocv1.ClusterExtension) bool {
184-
a.Status, b.Status = ocv1.ClusterExtensionStatus{}, ocv1.ClusterExtensionStatus{}
185-
a.Finalizers, b.Finalizers = []string{}, []string{}
186-
return !equality.Semantic.DeepEqual(a, b)
187-
}
188-
189187
// SetDeprecationStatus will set the appropriate deprecation statuses for a ClusterExtension
190188
// based on the provided bundle
191189
func SetDeprecationStatus(ext *ocv1.ClusterExtension, bundleName string, deprecation *declcfg.Deprecation) {

internal/shared/util/k8s/k8s.go

Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
package k8s
2+
3+
import (
4+
"reflect"
5+
6+
"k8s.io/apimachinery/pkg/api/equality"
7+
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
8+
)
9+
10+
// CheckForUnexpectedFieldChange compares two Kubernetes objects and returns true
11+
// if their annotations, labels, or spec have changed. This is useful for detecting
12+
// unexpected modifications during reconciliation.
13+
//
14+
// The function compares:
15+
// - Annotations (via GetAnnotations)
16+
// - Labels (via GetLabels)
17+
// - Spec (using reflection to access the Spec field, with semantic equality)
18+
//
19+
// Status and finalizers are intentionally not compared, as these are expected
20+
// to change during reconciliation.
21+
//
22+
// Type parameter:
23+
// - O: The object type that implements metav1.Object and has a Spec field
24+
//
25+
// This function uses reflection to access the Spec field, so no explicit GetSpec()
26+
// method is required. The objects must have a field named "Spec".
27+
func CheckForUnexpectedFieldChange[O metav1.Object](a, b O) bool {
28+
if !equality.Semantic.DeepEqual(a.GetAnnotations(), b.GetAnnotations()) {
29+
return true
30+
}
31+
if !equality.Semantic.DeepEqual(a.GetLabels(), b.GetLabels()) {
32+
return true
33+
}
34+
35+
// Use reflection to access the Spec field
36+
aVal := reflect.ValueOf(a)
37+
bVal := reflect.ValueOf(b)
38+
39+
// Handle pointer types
40+
if aVal.Kind() == reflect.Ptr {
41+
aVal = aVal.Elem()
42+
}
43+
if bVal.Kind() == reflect.Ptr {
44+
bVal = bVal.Elem()
45+
}
46+
47+
// Get the Spec field from both objects
48+
aSpec := aVal.FieldByName("Spec")
49+
bSpec := bVal.FieldByName("Spec")
50+
51+
// If either Spec field is invalid, return false (no change detected)
52+
if !aSpec.IsValid() || !bSpec.IsValid() {
53+
return false
54+
}
55+
56+
return !equality.Semantic.DeepEqual(aSpec.Interface(), bSpec.Interface())
57+
}

0 commit comments

Comments
 (0)