Skip to content

Commit 21e90d9

Browse files
tmshortclaude
andcommitted
Use Patch instead of Update for finalizer operations
Refactor all controllers to use client.Patch() instead of Update() when adding or removing finalizers to reduce conflicts and improve performance. 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() and RemoveFinalizer() 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 d204888 commit 21e90d9

File tree

9 files changed

+208
-205
lines changed

9 files changed

+208
-205
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"
@@ -391,12 +389,11 @@ func run() error {
391389
},
392390
}
393391

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

402399
cl := mgr.GetClient()
@@ -443,11 +440,11 @@ func run() error {
443440
}
444441

445442
ceReconciler := &controllers.ClusterExtensionReconciler{
446-
Client: cl,
447-
Resolver: resolver,
448-
ImageCache: imageCache,
449-
ImagePuller: imagePuller,
450-
Finalizers: clusterExtensionFinalizers,
443+
Client: cl,
444+
Resolver: resolver,
445+
ImageCache: imageCache,
446+
ImagePuller: imagePuller,
447+
FinalizerHandlers: clusterExtensionFinalizerHandlers,
451448
}
452449
ceController, err := ceReconciler.SetupWithManager(mgr, ctrlBuilderOpts...)
453450
if err != nil {
@@ -464,9 +461,9 @@ func run() error {
464461
}
465462

466463
if features.OperatorControllerFeatureGate.Enabled(features.BoxcutterRuntime) {
467-
err = setupBoxcutter(mgr, ceReconciler, preflights, clusterExtensionFinalizers, regv1ManifestProvider)
464+
err = setupBoxcutter(mgr, ceReconciler, preflights, regv1ManifestProvider)
468465
} else {
469-
err = setupHelm(mgr, ceReconciler, preflights, ceController, clusterExtensionFinalizers, regv1ManifestProvider)
466+
err = setupHelm(mgr, ceReconciler, preflights, ceController, regv1ManifestProvider)
470467
}
471468
if err != nil {
472469
setupLog.Error(err, "unable to setup lifecycler")
@@ -531,7 +528,6 @@ func setupBoxcutter(
531528
mgr manager.Manager,
532529
ceReconciler *controllers.ClusterExtensionReconciler,
533530
preflights []applier.Preflight,
534-
clusterExtensionFinalizers crfinalizer.Registerer,
535531
regv1ManifestProvider applier.ManifestProvider,
536532
) error {
537533
coreClient, err := corev1client.NewForConfig(mgr.GetConfig())
@@ -560,13 +556,9 @@ func setupBoxcutter(
560556
// This finalizer was added by the Helm applier for ClusterExtensions created
561557
// before BoxcutterRuntime was enabled. Boxcutter doesn't use contentmanager,
562558
// so we just need to acknowledge the finalizer to allow deletion to proceed.
563-
err = clusterExtensionFinalizers.Register(controllers.ClusterExtensionCleanupContentManagerCacheFinalizer, finalizers.FinalizerFunc(func(ctx context.Context, obj client.Object) (crfinalizer.Result, error) {
559+
ceReconciler.FinalizerHandlers[controllers.ClusterExtensionCleanupContentManagerCacheFinalizer] = func(ctx context.Context, ext *ocv1.ClusterExtension) error {
564560
// No-op: Boxcutter doesn't use contentmanager, so no cleanup is needed
565-
return crfinalizer.Result{}, nil
566-
}))
567-
if err != nil {
568-
setupLog.Error(err, "unable to register content manager cleanup finalizer for boxcutter")
569-
return err
561+
return nil
570562
}
571563

572564
// TODO: add support for preflight checks
@@ -638,7 +630,6 @@ func setupHelm(
638630
ceReconciler *controllers.ClusterExtensionReconciler,
639631
preflights []applier.Preflight,
640632
ceController crcontroller.Controller,
641-
clusterExtensionFinalizers crfinalizer.Registerer,
642633
regv1ManifestProvider applier.ManifestProvider,
643634
) error {
644635
coreClient, err := corev1client.NewForConfig(mgr.GetConfig())
@@ -677,14 +668,9 @@ func setupHelm(
677668
}
678669

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

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

internal/catalogd/controllers/core/clustercatalog_controller.go

Lines changed: 33 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

@@ -422,29 +416,6 @@ func checkForUnexpectedFieldChange(a, b ocv1.ClusterCatalog) bool {
422416
return !equality.Semantic.DeepEqual(a, b)
423417
}
424418

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
443-
}
444-
r.finalizers = f
445-
return nil
446-
}
447-
448419
func (r *ClusterCatalogReconciler) deleteStoredCatalog(catalogName string) {
449420
r.storedCatalogsMu.Lock()
450421
defer r.storedCatalogsMu.Unlock()

internal/catalogd/controllers/core/clustercatalog_controller_test.go

Lines changed: 23 additions & 8 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
{
@@ -379,7 +384,7 @@ func TestCatalogdControllerReconcile(t *testing.T) {
379384
expectedCatalog: &ocv1.ClusterCatalog{
380385
ObjectMeta: metav1.ObjectMeta{
381386
Name: "catalog",
382-
Finalizers: []string{},
387+
Finalizers: nil,
383388
DeletionTimestamp: &metav1.Time{Time: time.Date(2023, time.October, 10, 4, 19, 0, 0, time.UTC)},
384389
},
385390
Spec: ocv1.ClusterCatalogSpec{
@@ -660,7 +665,7 @@ func TestCatalogdControllerReconcile(t *testing.T) {
660665
expectedCatalog: &ocv1.ClusterCatalog{
661666
ObjectMeta: metav1.ObjectMeta{
662667
Name: "catalog",
663-
Finalizers: []string{},
668+
Finalizers: nil,
664669
},
665670
Spec: ocv1.ClusterCatalogSpec{
666671
Source: ocv1.CatalogSource{
@@ -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)