Skip to content

Commit 6903fb2

Browse files
tmshortclaude
andcommitted
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. Create shared finalizer utilities to eliminate code duplication across controllers. 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). - Add shared finalizer.EnsureFinalizer() utilities - Update ClusterCatalog, ClusterExtension, and ClusterExtensionRevision controllers to use Patch-based finalizer management - Maintain early return behavior after adding finalizers on create - Remove unused internal/operator-controller/finalizers package - Update all unit tests to match new behavior 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <[email protected]> Signed-off-by: Todd Short <[email protected]>
1 parent f3569d5 commit 6903fb2

File tree

10 files changed

+292
-208
lines changed

10 files changed

+292
-208
lines changed

cmd/operator-controller/main.go

Lines changed: 17 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -51,7 +51,6 @@ import (
5151
"sigs.k8s.io/controller-runtime/pkg/certwatcher"
5252
"sigs.k8s.io/controller-runtime/pkg/client"
5353
crcontroller "sigs.k8s.io/controller-runtime/pkg/controller"
54-
crfinalizer "sigs.k8s.io/controller-runtime/pkg/finalizer"
5554
"sigs.k8s.io/controller-runtime/pkg/healthz"
5655
"sigs.k8s.io/controller-runtime/pkg/log"
5756
"sigs.k8s.io/controller-runtime/pkg/manager"
@@ -70,7 +69,6 @@ import (
7069
"github.com/operator-framework/operator-controller/internal/operator-controller/contentmanager"
7170
"github.com/operator-framework/operator-controller/internal/operator-controller/controllers"
7271
"github.com/operator-framework/operator-controller/internal/operator-controller/features"
73-
"github.com/operator-framework/operator-controller/internal/operator-controller/finalizers"
7472
"github.com/operator-framework/operator-controller/internal/operator-controller/resolve"
7573
"github.com/operator-framework/operator-controller/internal/operator-controller/rukpak/preflights/crdupgradesafety"
7674
"github.com/operator-framework/operator-controller/internal/operator-controller/rukpak/render"
@@ -388,12 +386,11 @@ func run() error {
388386
},
389387
}
390388

391-
clusterExtensionFinalizers := crfinalizer.NewFinalizers()
392-
if err := clusterExtensionFinalizers.Register(controllers.ClusterExtensionCleanupUnpackCacheFinalizer, finalizers.FinalizerFunc(func(ctx context.Context, obj client.Object) (crfinalizer.Result, error) {
393-
return crfinalizer.Result{}, imageCache.Delete(ctx, obj.GetName())
394-
})); err != nil {
395-
setupLog.Error(err, "unable to register finalizer", "finalizerKey", controllers.ClusterExtensionCleanupUnpackCacheFinalizer)
396-
return err
389+
// Set up finalizer handlers for ClusterExtension
390+
clusterExtensionFinalizerHandlers := map[string]controllers.FinalizerHandler{
391+
controllers.ClusterExtensionCleanupUnpackCacheFinalizer: func(ctx context.Context, ext *ocv1.ClusterExtension) error {
392+
return imageCache.Delete(ctx, ext.GetName())
393+
},
397394
}
398395

399396
cl := mgr.GetClient()
@@ -440,11 +437,11 @@ func run() error {
440437
}
441438

442439
ceReconciler := &controllers.ClusterExtensionReconciler{
443-
Client: cl,
444-
Resolver: resolver,
445-
ImageCache: imageCache,
446-
ImagePuller: imagePuller,
447-
Finalizers: clusterExtensionFinalizers,
440+
Client: cl,
441+
Resolver: resolver,
442+
ImageCache: imageCache,
443+
ImagePuller: imagePuller,
444+
FinalizerHandlers: clusterExtensionFinalizerHandlers,
448445
}
449446
ceController, err := ceReconciler.SetupWithManager(mgr, ctrlBuilderOpts...)
450447
if err != nil {
@@ -461,9 +458,9 @@ func run() error {
461458
}
462459

463460
if features.OperatorControllerFeatureGate.Enabled(features.BoxcutterRuntime) {
464-
err = setupBoxcutter(mgr, ceReconciler, preflights, clusterExtensionFinalizers, regv1ManifestProvider)
461+
err = setupBoxcutter(mgr, ceReconciler, preflights, regv1ManifestProvider)
465462
} else {
466-
err = setupHelm(mgr, ceReconciler, preflights, ceController, clusterExtensionFinalizers, regv1ManifestProvider)
463+
err = setupHelm(mgr, ceReconciler, preflights, ceController, regv1ManifestProvider)
467464
}
468465
if err != nil {
469466
setupLog.Error(err, "unable to setup lifecycler")
@@ -528,7 +525,6 @@ func setupBoxcutter(
528525
mgr manager.Manager,
529526
ceReconciler *controllers.ClusterExtensionReconciler,
530527
preflights []applier.Preflight,
531-
clusterExtensionFinalizers crfinalizer.Registerer,
532528
regv1ManifestProvider applier.ManifestProvider,
533529
) error {
534530
coreClient, err := corev1client.NewForConfig(mgr.GetConfig())
@@ -557,13 +553,9 @@ func setupBoxcutter(
557553
// This finalizer was added by the Helm applier for ClusterExtensions created
558554
// before BoxcutterRuntime was enabled. Boxcutter doesn't use contentmanager,
559555
// so we just need to acknowledge the finalizer to allow deletion to proceed.
560-
err = clusterExtensionFinalizers.Register(controllers.ClusterExtensionCleanupContentManagerCacheFinalizer, finalizers.FinalizerFunc(func(ctx context.Context, obj client.Object) (crfinalizer.Result, error) {
556+
ceReconciler.FinalizerHandlers[controllers.ClusterExtensionCleanupContentManagerCacheFinalizer] = func(ctx context.Context, ext *ocv1.ClusterExtension) error {
561557
// No-op: Boxcutter doesn't use contentmanager, so no cleanup is needed
562-
return crfinalizer.Result{}, nil
563-
}))
564-
if err != nil {
565-
setupLog.Error(err, "unable to register content manager cleanup finalizer for boxcutter")
566-
return err
558+
return nil
567559
}
568560

569561
// TODO: add support for preflight checks
@@ -636,7 +628,6 @@ func setupHelm(
636628
ceReconciler *controllers.ClusterExtensionReconciler,
637629
preflights []applier.Preflight,
638630
ceController crcontroller.Controller,
639-
clusterExtensionFinalizers crfinalizer.Registerer,
640631
regv1ManifestProvider applier.ManifestProvider,
641632
) error {
642633
coreClient, err := corev1client.NewForConfig(mgr.GetConfig())
@@ -675,14 +666,9 @@ func setupHelm(
675666
}
676667

677668
cm := contentmanager.NewManager(clientRestConfigMapper, mgr.GetConfig(), mgr.GetRESTMapper())
678-
err = clusterExtensionFinalizers.Register(controllers.ClusterExtensionCleanupContentManagerCacheFinalizer, finalizers.FinalizerFunc(func(ctx context.Context, obj client.Object) (crfinalizer.Result, error) {
679-
ext := obj.(*ocv1.ClusterExtension)
680-
err := cm.Delete(ext)
681-
return crfinalizer.Result{}, err
682-
}))
683-
if err != nil {
684-
setupLog.Error(err, "unable to register content manager cleanup finalizer")
685-
return err
669+
// Register the content manager cleanup finalizer handler
670+
ceReconciler.FinalizerHandlers[controllers.ClusterExtensionCleanupContentManagerCacheFinalizer] = func(ctx context.Context, ext *ocv1.ClusterExtension) error {
671+
return cm.Delete(ext)
686672
}
687673

688674
// now initialize the helmApplier, assigning the potentially nil preAuth

internal/catalogd/controllers/core/clustercatalog_controller.go

Lines changed: 42 additions & 62 deletions
Original file line numberDiff line numberDiff line change
@@ -34,12 +34,12 @@ import (
3434
ctrl "sigs.k8s.io/controller-runtime"
3535
"sigs.k8s.io/controller-runtime/pkg/client"
3636
"sigs.k8s.io/controller-runtime/pkg/controller/controllerutil"
37-
crfinalizer "sigs.k8s.io/controller-runtime/pkg/finalizer"
3837
"sigs.k8s.io/controller-runtime/pkg/log"
3938
"sigs.k8s.io/controller-runtime/pkg/reconcile"
4039

4140
ocv1 "github.com/operator-framework/operator-controller/api/v1"
4241
"github.com/operator-framework/operator-controller/internal/catalogd/storage"
42+
finalizerutil "github.com/operator-framework/operator-controller/internal/shared/util/finalizer"
4343
imageutil "github.com/operator-framework/operator-controller/internal/shared/util/image"
4444
)
4545

@@ -59,8 +59,6 @@ type ClusterCatalogReconciler struct {
5959

6060
Storage storage.Instance
6161

62-
finalizers crfinalizer.Finalizers
63-
6462
// TODO: The below storedCatalogs fields are used for a quick a hack that helps
6563
// us correctly populate a ClusterCatalog's status. The fact that we need
6664
// these is indicative of a larger problem with the design of one or both
@@ -106,33 +104,18 @@ func (r *ClusterCatalogReconciler) Reconcile(ctx context.Context, req ctrl.Reque
106104

107105
// Do checks before any Update()s, as Update() may modify the resource structure!
108106
updateStatus := !equality.Semantic.DeepEqual(existingCatsrc.Status, reconciledCatsrc.Status)
109-
updateFinalizers := !equality.Semantic.DeepEqual(existingCatsrc.Finalizers, reconciledCatsrc.Finalizers)
110107
unexpectedFieldsChanged := checkForUnexpectedFieldChange(existingCatsrc, *reconciledCatsrc)
111108

112109
if unexpectedFieldsChanged {
113110
panic("spec or metadata changed by reconciler")
114111
}
115112

116-
// Save the finalizers off to the side. If we update the status, the reconciledCatsrc will be updated
117-
// 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.
120-
finalizers := reconciledCatsrc.Finalizers
121-
122113
if updateStatus {
123114
if err := r.Client.Status().Update(ctx, reconciledCatsrc); err != nil {
124115
reconcileErr = errors.Join(reconcileErr, fmt.Errorf("error updating status: %v", err))
125116
}
126117
}
127118

128-
reconciledCatsrc.Finalizers = finalizers
129-
130-
if updateFinalizers {
131-
if err := r.Update(ctx, reconciledCatsrc); err != nil {
132-
reconcileErr = errors.Join(reconcileErr, fmt.Errorf("error updating finalizers: %v", err))
133-
}
134-
}
135-
136119
return res, reconcileErr
137120
}
138121

@@ -142,10 +125,6 @@ func (r *ClusterCatalogReconciler) SetupWithManager(mgr ctrl.Manager) error {
142125
defer r.storedCatalogsMu.Unlock()
143126
r.storedCatalogs = make(map[string]storedCatalogData)
144127

145-
if err := r.setupFinalizers(); err != nil {
146-
return fmt.Errorf("failed to setup finalizers: %v", err)
147-
}
148-
149128
return ctrl.NewControllerManagedBy(mgr).
150129
For(&ocv1.ClusterCatalog{}).
151130
Named("catalogd-clustercatalog-controller").
@@ -171,32 +150,47 @@ func (r *ClusterCatalogReconciler) reconcile(ctx context.Context, catalog *ocv1.
171150
return ctrl.Result{}, err
172151
}
173152

174-
// Set status.conditions[type=Progressing] to False as we are done with
175-
// all that needs to be done with the catalog
176-
updateStatusProgressingUserSpecifiedUnavailable(&catalog.Status, catalog.GetGeneration())
177-
178153
// Remove the fbcDeletionFinalizer as we do not want a finalizer attached to the catalog
179154
// when it is disabled. Because the finalizer serves no purpose now.
180-
controllerutil.RemoveFinalizer(catalog, fbcDeletionFinalizer)
155+
if err := finalizerutil.RemoveFinalizer(ctx, r.Client, catalog, fbcDeletionFinalizer); err != nil {
156+
return ctrl.Result{}, fmt.Errorf("error removing finalizer: %v", err)
157+
}
158+
159+
// Set status.conditions[type=Progressing] to True as we are done with
160+
// all that needs to be done with the catalog
161+
updateStatusProgressingUserSpecifiedUnavailable(&catalog.Status, catalog.GetGeneration())
162+
// Clear URLs, ResolvedSource, and LastUnpacked since catalog is unavailable
163+
catalog.Status.ResolvedSource = nil
164+
catalog.Status.URLs = nil
165+
catalog.Status.LastUnpacked = nil
181166

182167
return ctrl.Result{}, nil
183168
}
184169

185-
finalizeResult, err := r.finalizers.Finalize(ctx, catalog)
186-
if err != nil {
187-
return ctrl.Result{}, err
188-
}
189-
if finalizeResult.Updated || finalizeResult.StatusUpdated {
190-
// On create: make sure the finalizer is applied before we do anything
191-
// On delete: make sure we do nothing after the finalizer is removed
170+
// Handle deletion
171+
if catalog.GetDeletionTimestamp() != nil {
172+
if !controllerutil.ContainsFinalizer(catalog, fbcDeletionFinalizer) {
173+
// All finalizers removed, nothing more to do
174+
return ctrl.Result{}, nil
175+
}
176+
if err := r.deleteCatalogCache(ctx, catalog); err != nil {
177+
return ctrl.Result{}, fmt.Errorf("finalizer %q failed: %w", fbcDeletionFinalizer, err)
178+
}
179+
if err := finalizerutil.RemoveFinalizer(ctx, r.Client, catalog, fbcDeletionFinalizer); err != nil {
180+
return ctrl.Result{}, fmt.Errorf("error removing finalizer: %v", err)
181+
}
182+
// Update status to reflect that catalog is no longer serving
183+
updateStatusNotServing(&catalog.Status, catalog.GetGeneration())
192184
return ctrl.Result{}, nil
193185
}
194186

195-
if catalog.GetDeletionTimestamp() != nil {
196-
// If we've gotten here, that means the cluster catalog is being deleted, we've handled all of
197-
// _our_ finalizers (above), but the cluster catalog is still present in the cluster, likely
198-
// because there are _other_ finalizers that other controllers need to handle, (e.g. the orphan
199-
// deletion finalizer).
187+
// Ensure finalizer is present
188+
finalizerAdded, err := finalizerutil.EnsureFinalizer(ctx, r.Client, catalog, fbcDeletionFinalizer)
189+
if err != nil {
190+
return ctrl.Result{}, fmt.Errorf("error ensuring finalizer: %v", err)
191+
}
192+
// On create: make sure the finalizer is applied before we do anything else
193+
if finalizerAdded {
200194
return ctrl.Result{}, nil
201195
}
202196

@@ -419,30 +413,16 @@ func (r *ClusterCatalogReconciler) needsPoll(lastSuccessfulPoll time.Time, catal
419413
func checkForUnexpectedFieldChange(a, b ocv1.ClusterCatalog) bool {
420414
a.Status, b.Status = ocv1.ClusterCatalogStatus{}, ocv1.ClusterCatalogStatus{}
421415
a.Finalizers, b.Finalizers = []string{}, []string{}
422-
return !equality.Semantic.DeepEqual(a, b)
423-
}
424-
425-
type finalizerFunc func(ctx context.Context, obj client.Object) (crfinalizer.Result, error)
426-
427-
func (f finalizerFunc) Finalize(ctx context.Context, obj client.Object) (crfinalizer.Result, error) {
428-
return f(ctx, obj)
429-
}
430-
431-
func (r *ClusterCatalogReconciler) setupFinalizers() error {
432-
f := crfinalizer.NewFinalizers()
433-
err := f.Register(fbcDeletionFinalizer, finalizerFunc(func(ctx context.Context, obj client.Object) (crfinalizer.Result, error) {
434-
catalog, ok := obj.(*ocv1.ClusterCatalog)
435-
if !ok {
436-
panic("could not convert object to clusterCatalog")
437-
}
438-
err := r.deleteCatalogCache(ctx, catalog)
439-
return crfinalizer.Result{StatusUpdated: true}, err
440-
}))
441-
if err != nil {
442-
return err
416+
a.ManagedFields, b.ManagedFields = nil, nil
417+
a.ResourceVersion, b.ResourceVersion = "", ""
418+
// Remove kubectl's last-applied-configuration annotation which may be added by the API server
419+
if a.Annotations != nil {
420+
delete(a.Annotations, "kubectl.kubernetes.io/last-applied-configuration")
443421
}
444-
r.finalizers = f
445-
return nil
422+
if b.Annotations != nil {
423+
delete(b.Annotations, "kubectl.kubernetes.io/last-applied-configuration")
424+
}
425+
return !equality.Semantic.DeepEqual(a, b)
446426
}
447427

448428
func (r *ClusterCatalogReconciler) deleteStoredCatalog(catalogName string) {

internal/catalogd/controllers/core/clustercatalog_controller_test.go

Lines changed: 21 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -17,8 +17,10 @@ import (
1717
"go.podman.io/image/v5/docker/reference"
1818
"k8s.io/apimachinery/pkg/api/meta"
1919
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
20+
"k8s.io/apimachinery/pkg/runtime"
2021
"k8s.io/utils/ptr"
2122
ctrl "sigs.k8s.io/controller-runtime"
23+
"sigs.k8s.io/controller-runtime/pkg/client/fake"
2224
"sigs.k8s.io/controller-runtime/pkg/reconcile"
2325

2426
ocv1 "github.com/operator-framework/operator-controller/api/v1"
@@ -304,6 +306,7 @@ func TestCatalogdControllerReconcile(t *testing.T) {
304306
name: "storage finalizer not set, storage finalizer gets set",
305307
puller: &imageutil.MockPuller{
306308
ImageFS: &fstest.MapFS{},
309+
Ref: mustRef(t, "my.org/someimage@sha256:e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855"),
307310
},
308311
store: &MockStore{},
309312
catalog: &ocv1.ClusterCatalog{
@@ -332,6 +335,8 @@ func TestCatalogdControllerReconcile(t *testing.T) {
332335
},
333336
},
334337
},
338+
// Status remains empty because controller returns early after adding finalizer
339+
Status: ocv1.ClusterCatalogStatus{},
335340
},
336341
},
337342
{
@@ -802,8 +807,12 @@ func TestCatalogdControllerReconcile(t *testing.T) {
802807
},
803808
} {
804809
t.Run(tt.name, func(t *testing.T) {
810+
scheme := runtime.NewScheme()
811+
require.NoError(t, ocv1.AddToScheme(scheme))
812+
cl := fake.NewClientBuilder().WithScheme(scheme).WithObjects(tt.catalog).WithStatusSubresource(tt.catalog).Build()
813+
805814
reconciler := &ClusterCatalogReconciler{
806-
Client: nil,
815+
Client: cl,
807816
ImagePuller: tt.puller,
808817
ImageCache: tt.cache,
809818
Storage: tt.store,
@@ -812,7 +821,6 @@ func TestCatalogdControllerReconcile(t *testing.T) {
812821
if reconciler.ImageCache == nil {
813822
reconciler.ImageCache = &imageutil.MockCache{}
814823
}
815-
require.NoError(t, reconciler.setupFinalizers())
816824
ctx := context.Background()
817825

818826
res, err := reconciler.reconcile(ctx, tt.catalog)
@@ -826,6 +834,7 @@ func TestCatalogdControllerReconcile(t *testing.T) {
826834
}
827835
diff := cmp.Diff(tt.expectedCatalog, tt.catalog,
828836
cmpopts.IgnoreFields(metav1.Condition{}, "Message", "LastTransitionTime"),
837+
cmpopts.IgnoreFields(metav1.ObjectMeta{}, "ResourceVersion"),
829838
cmpopts.SortSlices(func(a, b metav1.Condition) bool { return a.Type < b.Type }))
830839
assert.Empty(t, diff, "comparing the expected Catalog")
831840
})
@@ -909,8 +918,12 @@ func TestPollingRequeue(t *testing.T) {
909918
URLs: &ocv1.ClusterCatalogURLs{Base: "URL"},
910919
LastUnpacked: ptr.To(metav1.NewTime(time.Now().Truncate(time.Second))),
911920
}
921+
scheme := runtime.NewScheme()
922+
require.NoError(t, ocv1.AddToScheme(scheme))
923+
cl := fake.NewClientBuilder().WithScheme(scheme).WithObjects(tc.catalog).WithStatusSubresource(tc.catalog).Build()
924+
912925
reconciler := &ClusterCatalogReconciler{
913-
Client: nil,
926+
Client: cl,
914927
ImagePuller: &imageutil.MockPuller{
915928
ImageFS: &fstest.MapFS{},
916929
Ref: ref,
@@ -924,7 +937,6 @@ func TestPollingRequeue(t *testing.T) {
924937
},
925938
},
926939
}
927-
require.NoError(t, reconciler.setupFinalizers())
928940
res, _ := reconciler.reconcile(context.Background(), tc.catalog)
929941
assert.InDelta(t, tc.expectedRequeueAfter, res.RequeueAfter, 2*requeueJitterMaxFactor*float64(tc.expectedRequeueAfter))
930942
})
@@ -1136,13 +1148,16 @@ func TestPollingReconcilerUnpack(t *testing.T) {
11361148
if scd == nil {
11371149
scd = map[string]storedCatalogData{}
11381150
}
1151+
scheme := runtime.NewScheme()
1152+
require.NoError(t, ocv1.AddToScheme(scheme))
1153+
cl := fake.NewClientBuilder().WithScheme(scheme).WithObjects(tc.catalog).WithStatusSubresource(tc.catalog).Build()
1154+
11391155
reconciler := &ClusterCatalogReconciler{
1140-
Client: nil,
1156+
Client: cl,
11411157
ImagePuller: &imageutil.MockPuller{Error: errors.New("mockpuller error")},
11421158
Storage: &MockStore{},
11431159
storedCatalogs: scd,
11441160
}
1145-
require.NoError(t, reconciler.setupFinalizers())
11461161
_, err := reconciler.reconcile(context.Background(), tc.catalog)
11471162
if tc.expectedUnpackRun {
11481163
assert.Error(t, err)

0 commit comments

Comments
 (0)