Skip to content

Commit 97d5a33

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 053e415 commit 97d5a33

File tree

5 files changed

+306
-25
lines changed

5 files changed

+306
-25
lines changed

api/v1/helpers.go

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
package v1
2+
3+
// GetSpec returns the Spec field of the ClusterExtension.
4+
// This method is provided to satisfy generic interfaces that require a GetSpec() method.
5+
func (c *ClusterExtension) GetSpec() ClusterExtensionSpec {
6+
return c.Spec
7+
}
8+
9+
// GetSpec returns the Spec field of the ClusterCatalog.
10+
// This method is provided to satisfy generic interfaces that require a GetSpec() method.
11+
func (c *ClusterCatalog) GetSpec() ClusterCatalogSpec {
12+
return c.Spec
13+
}
14+
15+
// GetSpec returns the Spec field of the ClusterExtensionRevision.
16+
// This method is provided to satisfy generic interfaces that require a GetSpec() method.
17+
func (c *ClusterExtensionRevision) GetSpec() ClusterExtensionRevisionSpec {
18+
return c.Spec
19+
}

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: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
package k8s
2+
3+
import (
4+
"k8s.io/apimachinery/pkg/api/equality"
5+
)
6+
7+
// ObjectWithSpec represents any Kubernetes custom resource that embeds metav1.ObjectMeta
8+
// and has a Spec field. The type parameter T should be the Spec type.
9+
type ObjectWithSpec[T any] interface {
10+
GetAnnotations() map[string]string
11+
GetLabels() map[string]string
12+
GetSpec() T
13+
}
14+
15+
// CheckForUnexpectedFieldChange compares two Kubernetes objects and returns true
16+
// if their annotations, labels, or spec have changed. This is useful for detecting
17+
// unexpected modifications during reconciliation.
18+
//
19+
// The function compares:
20+
// - Annotations (via GetAnnotations)
21+
// - Labels (via GetLabels)
22+
// - Spec (via GetSpec, using semantic equality)
23+
//
24+
// Status and finalizers are intentionally not compared, as these are expected
25+
// to change during reconciliation.
26+
//
27+
// Type parameters:
28+
// - T: The Spec type for the object (e.g., ClusterExtensionSpec, ClusterCatalogSpec)
29+
// - O: The object type that implements ObjectWithSpec[T]
30+
//
31+
// This function works with any object that implements the ObjectWithSpec interface,
32+
// which requires GetAnnotations(), GetLabels(), and GetSpec() methods.
33+
func CheckForUnexpectedFieldChange[T any, O ObjectWithSpec[T]](a, b O) bool {
34+
if !equality.Semantic.DeepEqual(a.GetAnnotations(), b.GetAnnotations()) {
35+
return true
36+
}
37+
if !equality.Semantic.DeepEqual(a.GetLabels(), b.GetLabels()) {
38+
return true
39+
}
40+
return !equality.Semantic.DeepEqual(a.GetSpec(), b.GetSpec())
41+
}

0 commit comments

Comments
 (0)