Skip to content

Commit b152c7b

Browse files
authored
short-circuit reconcile when objects are deleted (#2022)
This is necessary to ensure that we do not keep reconciling the objects as if they were not deleted. The need for this became apparent while trying to use --cascade=orphan with a ClusterExtension. In theory, that should work out of the box because, we set owner references on all managed objects. However, that was not working because our controller was fully reconciling objects with metadata.finalizers: ["orphan"], which was writing owner references back into the objects that the orphan deletion process had just removed. Ultimately this meant that the managed objects would be background deleted because they once again had an owner reference to the now-deleted ClusterExtension, which then caused the kubernetes garbage collector to clean them up. In general, it stands to reason that once we have successfully processed all of our finalizers after a deletion of an object, we should stop reconciling that object. Signed-off-by: Joe Lanford <[email protected]>
1 parent b8687cb commit b152c7b

File tree

4 files changed

+123
-0
lines changed

4 files changed

+123
-0
lines changed

internal/catalogd/controllers/core/clustercatalog_controller.go

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -196,6 +196,14 @@ func (r *ClusterCatalogReconciler) reconcile(ctx context.Context, catalog *ocv1.
196196
return ctrl.Result{}, nil
197197
}
198198

199+
if catalog.GetDeletionTimestamp() != nil {
200+
// If we've gotten here, that means the cluster catalog is being deleted, we've handled all of
201+
// _our_ finalizers (above), but the cluster catalog is still present in the cluster, likely
202+
// because there are _other_ finalizers that other controllers need to handle, (e.g. the orphan
203+
// deletion finalizer).
204+
return ctrl.Result{}, nil
205+
}
206+
199207
// TODO: The below algorithm to get the current state based on an in-memory
200208
// storedCatalogs map is a hack that helps us keep the ClusterCatalog's
201209
// status up-to-date. The fact that we need this setup is indicative of

internal/catalogd/controllers/core/clustercatalog_controller_test.go

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -766,6 +766,40 @@ func TestCatalogdControllerReconcile(t *testing.T) {
766766
},
767767
},
768768
},
769+
{
770+
name: "reconcile should be short-circuited if the clustercatalog has a deletion timestamp and all known finalizers have been removed",
771+
catalog: &ocv1.ClusterCatalog{
772+
ObjectMeta: metav1.ObjectMeta{
773+
Name: "catalog",
774+
Finalizers: []string{"finalizer"},
775+
DeletionTimestamp: &metav1.Time{Time: time.Date(2025, 6, 10, 16, 43, 0, 0, time.UTC)},
776+
},
777+
Spec: ocv1.ClusterCatalogSpec{
778+
Source: ocv1.CatalogSource{
779+
Type: ocv1.SourceTypeImage,
780+
Image: &ocv1.ImageSource{
781+
Ref: "my.org/someimage:latest",
782+
},
783+
},
784+
AvailabilityMode: ocv1.AvailabilityModeAvailable,
785+
},
786+
},
787+
expectedCatalog: &ocv1.ClusterCatalog{
788+
ObjectMeta: metav1.ObjectMeta{
789+
Name: "catalog",
790+
Finalizers: []string{"finalizer"},
791+
DeletionTimestamp: &metav1.Time{Time: time.Date(2025, 6, 10, 16, 43, 0, 0, time.UTC)}},
792+
Spec: ocv1.ClusterCatalogSpec{
793+
Source: ocv1.CatalogSource{
794+
Type: ocv1.SourceTypeImage,
795+
Image: &ocv1.ImageSource{
796+
Ref: "my.org/someimage:latest",
797+
},
798+
},
799+
AvailabilityMode: ocv1.AvailabilityModeAvailable,
800+
},
801+
},
802+
},
769803
} {
770804
t.Run(tt.name, func(t *testing.T) {
771805
reconciler := &ClusterCatalogReconciler{

internal/operator-controller/controllers/clusterextension_controller.go

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -206,6 +206,14 @@ func (r *ClusterExtensionReconciler) reconcile(ctx context.Context, ext *ocv1.Cl
206206
return ctrl.Result{}, nil
207207
}
208208

209+
if ext.GetDeletionTimestamp() != nil {
210+
// If we've gotten here, that means the cluster extension is being deleted, we've handled all of
211+
// _our_ finalizers (above), but the cluster extension is still present in the cluster, likely
212+
// because there are _other_ finalizers that other controllers need to handle, (e.g. the orphan
213+
// deletion finalizer).
214+
return ctrl.Result{}, nil
215+
}
216+
209217
l.Info("getting installed bundle")
210218
installedBundle, err := r.InstalledBundleGetter.GetInstalledBundle(ctx, ext)
211219
if err != nil {

internal/operator-controller/controllers/clusterextension_controller_test.go

Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,79 @@ func TestClusterExtensionDoesNotExist(t *testing.T) {
4848
require.NoError(t, err)
4949
}
5050

51+
func TestClusterExtensionShortCircuitsReconcileDuringDeletion(t *testing.T) {
52+
cl, reconciler := newClientAndReconciler(t)
53+
54+
installedBundleGetterCalledErr := errors.New("installed bundle getter called")
55+
checkInstalledBundleGetterCalled := func(t require.TestingT, err error, args ...interface{}) {
56+
require.Equal(t, installedBundleGetterCalledErr, err)
57+
}
58+
reconciler.InstalledBundleGetter = &MockInstalledBundleGetter{
59+
err: installedBundleGetterCalledErr,
60+
}
61+
62+
type testCase struct {
63+
name string
64+
finalizers []string
65+
shouldDelete bool
66+
expectErr require.ErrorAssertionFunc
67+
}
68+
for _, tc := range []testCase{
69+
{
70+
name: "no finalizers, not deleted",
71+
expectErr: checkInstalledBundleGetterCalled,
72+
},
73+
{
74+
name: "has finalizers, not deleted",
75+
finalizers: []string{"finalizer"},
76+
expectErr: checkInstalledBundleGetterCalled,
77+
},
78+
{
79+
name: "has finalizers, deleted",
80+
finalizers: []string{"finalizer"},
81+
shouldDelete: true,
82+
expectErr: require.NoError,
83+
},
84+
} {
85+
t.Run(tc.name, func(t *testing.T) {
86+
pkgName := fmt.Sprintf("test-pkg-%s", rand.String(6))
87+
88+
ctx := context.Background()
89+
extKey := types.NamespacedName{Name: fmt.Sprintf("cluster-extension-test-%s", rand.String(8))}
90+
91+
t.Log("When the cluster extension specifies a non-existent package")
92+
t.Log("By initializing cluster state")
93+
clusterExtension := &ocv1.ClusterExtension{
94+
ObjectMeta: metav1.ObjectMeta{
95+
Name: extKey.Name,
96+
Finalizers: tc.finalizers,
97+
},
98+
Spec: ocv1.ClusterExtensionSpec{
99+
Source: ocv1.SourceConfig{
100+
SourceType: "Catalog",
101+
Catalog: &ocv1.CatalogFilter{
102+
PackageName: pkgName,
103+
},
104+
},
105+
Namespace: "default",
106+
ServiceAccount: ocv1.ServiceAccountReference{
107+
Name: "default",
108+
},
109+
},
110+
}
111+
require.NoError(t, cl.Create(ctx, clusterExtension))
112+
if tc.shouldDelete {
113+
require.NoError(t, cl.Delete(ctx, clusterExtension))
114+
}
115+
116+
t.Log("By running reconcile")
117+
res, err := reconciler.Reconcile(ctx, ctrl.Request{NamespacedName: extKey})
118+
require.Equal(t, ctrl.Result{}, res)
119+
tc.expectErr(t, err)
120+
})
121+
}
122+
}
123+
51124
func TestClusterExtensionResolutionFails(t *testing.T) {
52125
pkgName := fmt.Sprintf("non-existent-%s", rand.String(6))
53126
cl, reconciler := newClientAndReconciler(t)

0 commit comments

Comments
 (0)