Skip to content

Commit b51648b

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> Signed-off-by: Per G. da Silva <pegoncal@redhat.com>
1 parent 23dd207 commit b51648b

File tree

4 files changed

+157
-19
lines changed

4 files changed

+157
-19
lines changed

internal/operator-controller/controllers/clusterextensionrevision_controller.go

Lines changed: 40 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -134,24 +134,26 @@ func checkForUnexpectedClusterExtensionRevisionFieldChange(a, b ocv1.ClusterExte
134134
func (c *ClusterExtensionRevisionReconciler) reconcile(ctx context.Context, rev *ocv1.ClusterExtensionRevision) (ctrl.Result, error) {
135135
l := log.FromContext(ctx)
136136

137-
revision, opts, err := c.toBoxcutterRevision(ctx, rev)
138-
if err != nil {
139-
setRetryingConditions(rev, err.Error())
140-
return ctrl.Result{}, fmt.Errorf("converting to boxcutter revision: %v", err)
141-
}
142-
137+
//
138+
// Teardown
139+
//
143140
if !rev.DeletionTimestamp.IsZero() || rev.Spec.LifecycleState == ocv1.ClusterExtensionRevisionLifecycleStateArchived {
144141
return c.teardown(ctx, rev)
145142
}
146143

147-
revVersion := rev.GetAnnotations()[labels.BundleVersionKey]
148144
//
149145
// Reconcile
150146
//
151147
if err := c.ensureFinalizer(ctx, rev, clusterExtensionRevisionTeardownFinalizer); err != nil {
152148
return ctrl.Result{}, fmt.Errorf("error ensuring teardown finalizer: %v", err)
153149
}
154150

151+
revision, opts, err := c.toBoxcutterRevision(ctx, rev)
152+
if err != nil {
153+
setRetryingConditions(rev, err.Error())
154+
return ctrl.Result{}, fmt.Errorf("converting to boxcutter revision: %v", err)
155+
}
156+
155157
if err := c.establishWatch(ctx, rev, revision); err != nil {
156158
werr := fmt.Errorf("establish watch: %v", err)
157159
setRetryingConditions(rev, werr.Error())
@@ -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,23 +278,44 @@ 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, 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 && cer.DeletionTimestamp.IsZero() {
287+
tdres, err := c.archive(ctx, cer)
288+
if err != nil {
289+
setRetryingConditions(cer, fmt.Sprintf("error archiving: %v", err))
290+
return ctrl.Result{}, fmt.Errorf("error tearing down revision: %v", err)
291+
}
292+
if tdres != nil && !tdres.IsComplete() {
293+
setRetryingConditions(cer, "tearing down revision")
294+
return ctrl.Result{RequeueAfter: 5 * time.Second}, nil
295+
}
296+
// Ensure conditions are set before removing the finalizer when archiving
297+
if markAsArchived(cer) {
298+
return ctrl.Result{}, nil
299+
}
287300
}
288-
289-
if err := c.removeFinalizer(ctx, rev, clusterExtensionRevisionTeardownFinalizer); err != nil {
301+
if err := c.removeFinalizer(ctx, cer, clusterExtensionRevisionTeardownFinalizer); err != nil {
290302
return ctrl.Result{}, fmt.Errorf("error removing teardown finalizer: %v", err)
291303
}
292304
return ctrl.Result{}, nil
293305
}
294306

307+
func (c *ClusterExtensionRevisionReconciler) archive(ctx context.Context, cer *ocv1.ClusterExtensionRevision) (machinery.RevisionTeardownResult, error) {
308+
revision, _, err := c.toBoxcutterRevision(ctx, cer)
309+
if err != nil {
310+
return nil, fmt.Errorf("converting to boxcutter revision: %v", err)
311+
}
312+
revisionEngine, err := c.RevisionEngineFactory.CreateRevisionEngine(ctx, cer)
313+
if err != nil {
314+
return nil, fmt.Errorf("failed to create revision engine: %v", err)
315+
}
316+
return revisionEngine.Teardown(ctx, *revision)
317+
}
318+
295319
type Sourcerer interface {
296320
Source(handler handler.EventHandler, predicates ...predicate.Predicate) source.Source
297321
}

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)