Skip to content

Commit 5ca3f6e

Browse files
author
Per Goncalves da Silva
committed
Call RevisionEngine.Teardown when CER is archived
When a ClusterExtensionRevision transitions to the Archived lifecycle state, invoke the revision engine's Teardown method to clean up managed resources that are no longer part of the active revision. This ensures resources removed between bundle versions (e.g. a ConfigMap present in v1.0.0 but absent in v1.2.0) are deleted from the cluster. Changes: - Move RevisionEngine creation before the teardown check so it is available for both teardown and reconcile paths - Pass RevisionEngine and boxcutter Revision into teardown() - Call revisionEngine.Teardown() for archived revisions and handle incomplete teardown (requeue after 5s) and errors (return error for controller retry) - Remove redundant lifecycle state check and fix swallowed teardown error - Add unit tests for incomplete teardown requeue and teardown error propagation (teardown coverage: 68.8% -> 93.8%) - Add dummy-configmap to v1.0.0 test bundle (absent in v1.2.0) and assert it is removed in the "Each update creates a new revision" e2e test Signed-off-by: Per Goncalves da Silva <pegoncal@redhat.com>
1 parent e17412a commit 5ca3f6e

File tree

4 files changed

+145
-14
lines changed

4 files changed

+145
-14
lines changed

internal/operator-controller/controllers/clusterextensionrevision_controller.go

Lines changed: 28 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -140,11 +140,13 @@ func (c *ClusterExtensionRevisionReconciler) reconcile(ctx context.Context, rev
140140
return ctrl.Result{}, fmt.Errorf("converting to boxcutter revision: %v", err)
141141
}
142142

143+
//
144+
// Teardown
145+
//
143146
if !rev.DeletionTimestamp.IsZero() || rev.Spec.LifecycleState == ocv1.ClusterExtensionRevisionLifecycleStateArchived {
144-
return c.teardown(ctx, rev)
147+
return c.teardown(ctx, revision, rev)
145148
}
146149

147-
revVersion := rev.GetAnnotations()[labels.BundleVersionKey]
148150
//
149151
// Reconcile
150152
//
@@ -203,6 +205,7 @@ func (c *ClusterExtensionRevisionReconciler) reconcile(ctx context.Context, rev
203205
}
204206
}
205207

208+
revVersion := rev.GetAnnotations()[labels.BundleVersionKey]
206209
if !rres.InTransition() {
207210
markAsProgressing(rev, ocv1.ReasonSucceeded, fmt.Sprintf("Revision %s has rolled out.", revVersion))
208211
} else {
@@ -275,18 +278,32 @@ func (c *ClusterExtensionRevisionReconciler) reconcile(ctx context.Context, rev
275278
return ctrl.Result{}, nil
276279
}
277280

278-
func (c *ClusterExtensionRevisionReconciler) teardown(ctx context.Context, rev *ocv1.ClusterExtensionRevision) (ctrl.Result, error) {
279-
if err := c.TrackingCache.Free(ctx, rev); err != nil {
280-
markAsAvailableUnknown(rev, ocv1.ClusterExtensionRevisionReasonReconciling, err.Error())
281+
func (c *ClusterExtensionRevisionReconciler) teardown(ctx context.Context, revision *boxcutter.Revision, cer *ocv1.ClusterExtensionRevision) (ctrl.Result, error) {
282+
if err := c.TrackingCache.Free(ctx, cer); err != nil {
283+
markAsAvailableUnknown(cer, ocv1.ClusterExtensionRevisionReasonReconciling, err.Error())
281284
return ctrl.Result{}, fmt.Errorf("error stopping informers: %v", err)
282285
}
283-
284-
// Ensure conditions are set before removing the finalizer when archiving
285-
if rev.Spec.LifecycleState == ocv1.ClusterExtensionRevisionLifecycleStateArchived && markAsArchived(rev) {
286-
return ctrl.Result{}, nil
286+
if cer.Spec.LifecycleState == ocv1.ClusterExtensionRevisionLifecycleStateArchived {
287+
revisionEngine, err := c.RevisionEngineFactory.CreateRevisionEngine(ctx, cer)
288+
if err != nil {
289+
setRetryingConditions(cer, fmt.Sprintf("error archiving: %v", err))
290+
return ctrl.Result{}, fmt.Errorf("failed to create revision engine: %v", err)
291+
}
292+
tdres, err := revisionEngine.Teardown(ctx, *revision)
293+
if err != nil {
294+
setRetryingConditions(cer, fmt.Sprintf("error archiving: %v", err))
295+
return ctrl.Result{}, fmt.Errorf("error tearing down revision: %v", err)
296+
}
297+
if tdres != nil && !tdres.IsComplete() {
298+
setRetryingConditions(cer, "tearing down revision")
299+
return ctrl.Result{RequeueAfter: 5 * time.Second}, nil
300+
}
301+
// Ensure conditions are set before removing the finalizer when archiving
302+
if markAsArchived(cer) {
303+
return ctrl.Result{}, nil
304+
}
287305
}
288-
289-
if err := c.removeFinalizer(ctx, rev, clusterExtensionRevisionTeardownFinalizer); err != nil {
306+
if err := c.removeFinalizer(ctx, cer, clusterExtensionRevisionTeardownFinalizer); err != nil {
290307
return ctrl.Result{}, fmt.Errorf("error removing teardown finalizer: %v", err)
291308
}
292309
return ctrl.Result{}, nil

internal/operator-controller/controllers/clusterextensionrevision_controller_test.go

Lines changed: 108 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -641,9 +641,11 @@ func Test_ClusterExtensionRevisionReconciler_Reconcile_Deletion(t *testing.T) {
641641
existingObjs func() []client.Object
642642
revisionResult machinery.RevisionResult
643643
revisionEngineTeardownFn func(*testing.T) func(context.Context, machinerytypes.Revision, ...machinerytypes.RevisionTeardownOption) (machinery.RevisionTeardownResult, error)
644+
revisionEngineFactoryErr error
644645
validate func(*testing.T, client.Client)
645646
trackingCacheFreeFn func(context.Context, client.Object) error
646647
expectedErr string
648+
expectedResult ctrl.Result
647649
}{
648650
{
649651
name: "teardown finalizer is removed",
@@ -775,6 +777,109 @@ func Test_ClusterExtensionRevisionReconciler_Reconcile_Deletion(t *testing.T) {
775777
require.Equal(t, int64(1), cond.ObservedGeneration)
776778
},
777779
},
780+
{
781+
name: "set Progressing:True:Retrying and requeue when archived revision teardown is incomplete",
782+
revisionResult: mockRevisionResult{},
783+
existingObjs: func() []client.Object {
784+
ext := newTestClusterExtension()
785+
rev1 := newTestClusterExtensionRevision(t, clusterExtensionRevisionName, ext, testScheme)
786+
rev1.Finalizers = []string{
787+
"olm.operatorframework.io/teardown",
788+
}
789+
rev1.Spec.LifecycleState = ocv1.ClusterExtensionRevisionLifecycleStateArchived
790+
return []client.Object{rev1, ext}
791+
},
792+
revisionEngineTeardownFn: func(t *testing.T) func(ctx context.Context, rev machinerytypes.Revision, opts ...machinerytypes.RevisionTeardownOption) (machinery.RevisionTeardownResult, error) {
793+
return func(ctx context.Context, rev machinerytypes.Revision, opts ...machinerytypes.RevisionTeardownOption) (machinery.RevisionTeardownResult, error) {
794+
return &mockRevisionTeardownResult{
795+
isComplete: false,
796+
}, nil
797+
}
798+
},
799+
expectedResult: ctrl.Result{RequeueAfter: 5 * time.Second},
800+
validate: func(t *testing.T, c client.Client) {
801+
rev := &ocv1.ClusterExtensionRevision{}
802+
err := c.Get(t.Context(), client.ObjectKey{
803+
Name: clusterExtensionRevisionName,
804+
}, rev)
805+
require.NoError(t, err)
806+
cond := meta.FindStatusCondition(rev.Status.Conditions, ocv1.ClusterExtensionRevisionTypeProgressing)
807+
require.NotNil(t, cond)
808+
require.Equal(t, metav1.ConditionTrue, cond.Status)
809+
require.Equal(t, ocv1.ClusterExtensionRevisionReasonRetrying, cond.Reason)
810+
require.Equal(t, "tearing down revision", cond.Message)
811+
812+
// Finalizer should still be present
813+
require.Contains(t, rev.Finalizers, "olm.operatorframework.io/teardown")
814+
},
815+
},
816+
{
817+
name: "return error and set retrying conditions when archived revision teardown fails",
818+
revisionResult: mockRevisionResult{},
819+
existingObjs: func() []client.Object {
820+
ext := newTestClusterExtension()
821+
rev1 := newTestClusterExtensionRevision(t, clusterExtensionRevisionName, ext, testScheme)
822+
rev1.Finalizers = []string{
823+
"olm.operatorframework.io/teardown",
824+
}
825+
rev1.Spec.LifecycleState = ocv1.ClusterExtensionRevisionLifecycleStateArchived
826+
return []client.Object{rev1, ext}
827+
},
828+
revisionEngineTeardownFn: func(t *testing.T) func(ctx context.Context, rev machinerytypes.Revision, opts ...machinerytypes.RevisionTeardownOption) (machinery.RevisionTeardownResult, error) {
829+
return func(ctx context.Context, rev machinerytypes.Revision, opts ...machinerytypes.RevisionTeardownOption) (machinery.RevisionTeardownResult, error) {
830+
return nil, fmt.Errorf("teardown failed: connection refused")
831+
}
832+
},
833+
expectedErr: "error tearing down revision",
834+
validate: func(t *testing.T, c client.Client) {
835+
rev := &ocv1.ClusterExtensionRevision{}
836+
err := c.Get(t.Context(), client.ObjectKey{
837+
Name: clusterExtensionRevisionName,
838+
}, rev)
839+
require.NoError(t, err)
840+
cond := meta.FindStatusCondition(rev.Status.Conditions, ocv1.ClusterExtensionRevisionTypeProgressing)
841+
require.NotNil(t, cond)
842+
require.Equal(t, metav1.ConditionTrue, cond.Status)
843+
require.Equal(t, ocv1.ClusterExtensionRevisionReasonRetrying, cond.Reason)
844+
require.Contains(t, cond.Message, "teardown failed: connection refused")
845+
846+
// Finalizer should still be present
847+
require.Contains(t, rev.Finalizers, "olm.operatorframework.io/teardown")
848+
},
849+
},
850+
{
851+
name: "return error and set retrying conditions when factory fails to create engine during archived teardown",
852+
revisionResult: mockRevisionResult{},
853+
existingObjs: func() []client.Object {
854+
ext := newTestClusterExtension()
855+
rev1 := newTestClusterExtensionRevision(t, clusterExtensionRevisionName, ext, testScheme)
856+
rev1.Finalizers = []string{
857+
"olm.operatorframework.io/teardown",
858+
}
859+
rev1.Spec.LifecycleState = ocv1.ClusterExtensionRevisionLifecycleStateArchived
860+
return []client.Object{rev1, ext}
861+
},
862+
revisionEngineTeardownFn: func(t *testing.T) func(ctx context.Context, rev machinerytypes.Revision, opts ...machinerytypes.RevisionTeardownOption) (machinery.RevisionTeardownResult, error) {
863+
return nil
864+
},
865+
revisionEngineFactoryErr: fmt.Errorf("token getter failed"),
866+
expectedErr: "failed to create revision engine",
867+
validate: func(t *testing.T, c client.Client) {
868+
rev := &ocv1.ClusterExtensionRevision{}
869+
err := c.Get(t.Context(), client.ObjectKey{
870+
Name: clusterExtensionRevisionName,
871+
}, rev)
872+
require.NoError(t, err)
873+
cond := meta.FindStatusCondition(rev.Status.Conditions, ocv1.ClusterExtensionRevisionTypeProgressing)
874+
require.NotNil(t, cond)
875+
require.Equal(t, metav1.ConditionTrue, cond.Status)
876+
require.Equal(t, ocv1.ClusterExtensionRevisionReasonRetrying, cond.Reason)
877+
require.Contains(t, cond.Message, "token getter failed")
878+
879+
// Finalizer should still be present
880+
require.Contains(t, rev.Finalizers, "olm.operatorframework.io/teardown")
881+
},
882+
},
778883
{
779884
name: "revision is torn down when in archived state and finalizer is removed",
780885
revisionResult: mockRevisionResult{},
@@ -833,9 +938,10 @@ func Test_ClusterExtensionRevisionReconciler_Reconcile_Deletion(t *testing.T) {
833938
},
834939
teardown: tc.revisionEngineTeardownFn(t),
835940
}
941+
factory := &mockRevisionEngineFactory{engine: mockEngine, createErr: tc.revisionEngineFactoryErr}
836942
result, err := (&controllers.ClusterExtensionRevisionReconciler{
837943
Client: testClient,
838-
RevisionEngineFactory: &mockRevisionEngineFactory{engine: mockEngine},
944+
RevisionEngineFactory: factory,
839945
TrackingCache: &mockTrackingCache{
840946
client: testClient,
841947
freeFn: tc.trackingCacheFreeFn,
@@ -847,7 +953,7 @@ func Test_ClusterExtensionRevisionReconciler_Reconcile_Deletion(t *testing.T) {
847953
})
848954

849955
// reconcile cluster extension revision
850-
require.Equal(t, ctrl.Result{}, result)
956+
require.Equal(t, tc.expectedResult, result)
851957
if tc.expectedErr != "" {
852958
require.Contains(t, err.Error(), tc.expectedErr)
853959
} else {

test/e2e/features/update.feature

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -181,7 +181,7 @@ Feature: Update ClusterExtension
181181
Then bundle "test-operator.1.3.0" is installed in version "1.3.0"
182182

183183
@BoxcutterRuntime
184-
Scenario: Each update creates a new revision
184+
Scenario: Each update creates a new revision and resources not present in the new revision are removed from the cluster
185185
Given ClusterExtension is applied
186186
"""
187187
apiVersion: olm.operatorframework.io/v1
@@ -212,6 +212,8 @@ Feature: Update ClusterExtension
212212
And ClusterExtensionRevision "${NAME}-2" reports Progressing as True with Reason Succeeded
213213
And ClusterExtensionRevision "${NAME}-2" reports Available as True with Reason ProbesSucceeded
214214
And ClusterExtensionRevision "${NAME}-1" is archived
215+
# dummy-config map exists only in v1.0.0 - once the v1.0.0 revision is archived, it should be gone from the cluster
216+
And resource "configmap/dummy-configmap" is eventually not found
215217

216218
@BoxcutterRuntime
217219
Scenario: Report all active revisions on ClusterExtension
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
apiVersion: v1
2+
kind: ConfigMap
3+
metadata:
4+
name: dummy-configmap
5+
data:
6+
why: "this config map does not exist in higher versions of the bundle - it is useful to test whether resources removed between versions are removed from the cluster as well"

0 commit comments

Comments
 (0)