Skip to content

Commit bfb7a16

Browse files
tmshortclaude
andcommitted
Use SSA instead of Update for finalizer operations
Refactor all controllers to use server-side-apply 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() 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 35e38aa commit bfb7a16

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"
@@ -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: 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)