Skip to content

Commit 6ea858f

Browse files
authored
Add a finalizer to the NSTemplateTier to prevent its deletion (#1182)
1 parent ba6bdb7 commit 6ea858f

File tree

5 files changed

+116
-38
lines changed

5 files changed

+116
-38
lines changed

controllers/nstemplatetier/nstemplatetier_controller.go

Lines changed: 56 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -10,9 +10,10 @@ import (
1010
"github.com/codeready-toolchain/host-operator/controllers/toolchainconfig"
1111
"github.com/codeready-toolchain/host-operator/pkg/templates/nstemplatetiers"
1212
"github.com/codeready-toolchain/toolchain-common/pkg/condition"
13-
"github.com/redhat-cop/operator-utils/pkg/util"
13+
"github.com/codeready-toolchain/toolchain-common/pkg/hash"
1414
corev1 "k8s.io/api/core/v1"
1515

16+
"github.com/redhat-cop/operator-utils/pkg/util"
1617
"k8s.io/apimachinery/pkg/api/errors"
1718
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
1819
"k8s.io/apimachinery/pkg/runtime"
@@ -62,9 +63,11 @@ func (r *Reconciler) Reconcile(ctx context.Context, request ctrl.Request) (ctrl.
6263
logger.Error(err, "unable to get the current NSTemplateTier")
6364
return reconcile.Result{}, fmt.Errorf("unable to get the current NSTemplateTier: %w", err)
6465
}
65-
// if the NSTemplateTier is being deleted, then do nothing
6666
if util.IsBeingDeleted(tier) {
67-
return reconcile.Result{}, nil
67+
return reconcile.Result{}, r.handleDeletion(ctx, tier)
68+
}
69+
if err := r.addFinalizer(ctx, tier); err != nil {
70+
return reconcile.Result{}, err
6871
}
6972

7073
_, err := toolchainconfig.GetToolchainConfig(r.Client)
@@ -183,7 +186,6 @@ func (r *Reconciler) ensureTTRforTemplate(ctx context.Context, nsTmplTier *toolc
183186
return false, "", err
184187
}
185188
return true, ttrName, nil
186-
187189
}
188190

189191
func (r *Reconciler) createNewTierTemplateRevision(ctx context.Context, nsTmplTier *toolchainv1alpha1.NSTemplateTier, tierTemplate *toolchainv1alpha1.TierTemplate) (string, error) {
@@ -255,6 +257,56 @@ func (r *Reconciler) updateStatus(ctx context.Context, tmplTier *toolchainv1alph
255257
return r.Client.Status().Update(ctx, tmplTier)
256258
}
257259

260+
func (r *Reconciler) handleDeletion(ctx context.Context, tier *toolchainv1alpha1.NSTemplateTier) error {
261+
list := &toolchainv1alpha1.SpaceList{}
262+
if err := r.Client.List(ctx, list,
263+
runtimeclient.InNamespace(tier.GetNamespace()),
264+
runtimeclient.HasLabels{hash.TemplateTierHashLabelKey(tier.GetName())}); err != nil {
265+
return err
266+
}
267+
268+
hasSpaces := false
269+
270+
for _, s := range list.Items {
271+
if util.IsBeingDeleted(&s) {
272+
// As an optimization, we skip the spaces that are being deleted.
273+
// There is no functional relationship between the spaces and the tier, we just don't want
274+
// there to be spaces that refer to a non-existing tier. So if the space is being deleted,
275+
// we can pretend like it doesn't already exist.
276+
//
277+
// This speeds up the deletion of the tier because it doesn't linger around for the spaces
278+
// that are being deleted (which can take a good amount of time).
279+
continue
280+
}
281+
// we found a space that is not being deleted and that references our tier.
282+
hasSpaces = true
283+
break
284+
}
285+
286+
if hasSpaces {
287+
// leave the finalizer in place - it's still being used
288+
return fmt.Errorf("NSTemplateTier is still used by some spaces")
289+
}
290+
291+
// Remove finalizer from the NSTemplateTier, it's no longer used anywhere
292+
// so we can let the cluster proceed with the deletion.
293+
util.RemoveFinalizer(tier, toolchainv1alpha1.FinalizerName)
294+
return r.Client.Update(ctx, tier)
295+
}
296+
297+
func (r *Reconciler) addFinalizer(ctx context.Context, tier *toolchainv1alpha1.NSTemplateTier) error {
298+
// Add the finalizer if it is not present
299+
if !util.HasFinalizer(tier, toolchainv1alpha1.FinalizerName) {
300+
logger := log.FromContext(ctx)
301+
logger.Info("adding finalizer on NSTemplateTier")
302+
util.AddFinalizer(tier, toolchainv1alpha1.FinalizerName)
303+
if err := r.Client.Update(ctx, tier); err != nil {
304+
return err
305+
}
306+
}
307+
return nil
308+
}
309+
258310
func FailedCondition(reason string, cause string) toolchainv1alpha1.Condition {
259311
return toolchainv1alpha1.Condition{
260312
Type: toolchainv1alpha1.ConditionReady,

controllers/nstemplatetier/nstemplatetier_controller_test.go

Lines changed: 50 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ import (
1313
"github.com/codeready-toolchain/host-operator/pkg/apis"
1414
tiertest "github.com/codeready-toolchain/host-operator/test/nstemplatetier"
1515
"github.com/codeready-toolchain/host-operator/test/tiertemplaterevision"
16+
"github.com/codeready-toolchain/toolchain-common/pkg/hash"
1617
"github.com/codeready-toolchain/toolchain-common/pkg/test"
1718
templatev1 "github.com/openshift/api/template/v1"
1819
"github.com/stretchr/testify/assert"
@@ -27,6 +28,7 @@ import (
2728
"k8s.io/apimachinery/pkg/util/validation"
2829
"k8s.io/client-go/kubernetes/scheme"
2930
runtimeclient "sigs.k8s.io/controller-runtime/pkg/client"
31+
"sigs.k8s.io/controller-runtime/pkg/controller/controllerutil"
3032
"sigs.k8s.io/controller-runtime/pkg/reconcile"
3133
)
3234

@@ -37,9 +39,7 @@ const (
3739
func TestReconcile(t *testing.T) {
3840
// given
3941
t.Run("failures", func(t *testing.T) {
40-
4142
t.Run("unable to get NSTemplateTier", func(t *testing.T) {
42-
4343
t.Run("tier not found", func(t *testing.T) {
4444
// given
4545
base1nsTier := tiertest.Base1nsTier(t, tiertest.CurrentBase1nsTemplates)
@@ -75,12 +75,11 @@ func TestReconcile(t *testing.T) {
7575
assert.Equal(t, reconcile.Result{}, res) // no explicit requeue
7676
})
7777
})
78-
7978
})
8079

8180
t.Run("revisions management", func(t *testing.T) {
8281
// given
83-
base1nsTier := tiertest.Base1nsTier(t, tiertest.CurrentBase1nsTemplates,
82+
base1nsTier := tiertest.Base1nsTier(t, tiertest.CurrentBase1nsTemplates, tiertest.WithFinalizer(),
8483
// the tiertemplate revision CR should have a copy of those parameters
8584
tiertest.WithParameter("DEPLOYMENT_QUOTA", "60"),
8685
)
@@ -194,15 +193,14 @@ func TestReconcile(t *testing.T) {
194193
require.Len(t, ttrs.Items, len(tierTemplatesRefs)) // it's one TTR per each tiertemplate in the NSTemplateTier
195194
})
196195
})
197-
198196
})
199197

200198
t.Run("revision field is set but TierTemplateRevision CRs are missing, they should be created", func(t *testing.T) {
201199
// given
202200
// the NSTemplateTier has already the status.revisions field populated
203201
// but the TierTemplateRevision CRs are missing
204202
tierTemplates := initTierTemplates(t, withTemplateObjects(crq), base1nsTier.Name)
205-
base1nsTierWithRevisions := tiertest.Base1nsTier(t, tiertest.CurrentBase1nsTemplates,
203+
base1nsTierWithRevisions := tiertest.Base1nsTier(t, tiertest.CurrentBase1nsTemplates, tiertest.WithFinalizer(),
206204
// the tiertemplate revision CR should have a copy of those parameters
207205
tiertest.WithParameter("DEPLOYMENT_QUOTA", "60"),
208206
)
@@ -240,7 +238,7 @@ func TestReconcile(t *testing.T) {
240238
// the TierTemplateRevision CRs are missing, and their name are based on the tier name.
241239
// Making the TierName already 63chars long we test that the TTR name stays within 63 chars
242240
veryLongTierName := "somerandomstringtomakethenamelongerthan63chars12345678912345678"
243-
tierWithVeryLongName := tiertest.Tier(t, veryLongTierName, tiertest.NSTemplateTierSpecWithTierName(veryLongTierName),
241+
tierWithVeryLongName := tiertest.Tier(t, veryLongTierName, tiertest.NSTemplateTierSpecWithTierName(veryLongTierName), tiertest.WithFinalizer(),
244242
tiertest.WithParameter("DEPLOYMENT_QUOTA", "60"),
245243
)
246244
tierTemplatesWithLongNames := initTierTemplates(t, withTemplateObjects(crq), tierWithVeryLongName.Name)
@@ -264,7 +262,6 @@ func TestReconcile(t *testing.T) {
264262
})
265263

266264
t.Run("errors", func(t *testing.T) {
267-
268265
t.Run("error when TierTemplate is missing ", func(t *testing.T) {
269266
// given
270267
// make sure revisions field is nill before starting the test
@@ -285,46 +282,69 @@ func TestReconcile(t *testing.T) {
285282
// and the TierTemplateRevision CRs are not created
286283
tiertemplaterevision.AssertThatTTRs(t, cl, base1nsTier.GetNamespace()).DoNotExist()
287284
})
288-
289285
})
290-
291286
})
292287

293-
t.Run("if being deleted, then do nothing", func(t *testing.T) {
288+
t.Run("block deletion if used", func(t *testing.T) {
294289
// given
295290
tierBeingDeleted := base1nsTier.DeepCopy()
296291
tierBeingDeleted.DeletionTimestamp = &metav1.Time{Time: time.Now()}
297-
tierBeingDeleted.Finalizers = []string{"dummy"}
298-
r, req, cl := prepareReconcile(t, tierBeingDeleted.Name, tierBeingDeleted)
299-
called := false
300-
cl.MockGet = func(ctx context.Context, key runtimeclient.ObjectKey, obj runtimeclient.Object, opts ...runtimeclient.GetOption) error {
301-
if called {
302-
return fmt.Errorf("should not call Get more than once")
303-
}
304-
called = true
305-
return cl.Client.Get(ctx, key, obj, opts...)
306-
}
307-
cl.MockCreate = func(ctx context.Context, obj runtimeclient.Object, opts ...runtimeclient.CreateOption) error {
308-
return fmt.Errorf("should not call Create")
292+
controllerutil.AddFinalizer(tierBeingDeleted, toolchainv1alpha1.FinalizerName)
293+
space := &toolchainv1alpha1.Space{
294+
ObjectMeta: metav1.ObjectMeta{
295+
Name: "test-space",
296+
Namespace: tierBeingDeleted.Namespace,
297+
Labels: map[string]string{hash.TemplateTierHashLabelKey(tierBeingDeleted.Name): "1234"},
298+
},
309299
}
310-
cl.MockStatusUpdate = func(ctx context.Context, obj runtimeclient.Object, opts ...runtimeclient.SubResourceUpdateOption) error {
311-
return fmt.Errorf("should not call StatusUpdate")
300+
301+
r, req, cl := prepareReconcile(t, tierBeingDeleted.Name, tierBeingDeleted, space)
302+
303+
// when
304+
res, err := r.Reconcile(context.TODO(), req)
305+
inCluster := &toolchainv1alpha1.NSTemplateTier{}
306+
gerr := cl.Get(context.TODO(), runtimeclient.ObjectKeyFromObject(tierBeingDeleted), inCluster)
307+
308+
// then
309+
require.Error(t, err)
310+
require.Empty(t, res)
311+
require.NoError(t, gerr)
312+
require.Len(t, inCluster.Finalizers, 1)
313+
require.Contains(t, inCluster.Finalizers, toolchainv1alpha1.FinalizerName)
314+
})
315+
t.Run("space being deleted doesn't block tier deletion", func(t *testing.T) {
316+
// given
317+
tierBeingDeleted := base1nsTier.DeepCopy()
318+
tierBeingDeleted.DeletionTimestamp = &metav1.Time{Time: time.Now()}
319+
controllerutil.AddFinalizer(tierBeingDeleted, toolchainv1alpha1.FinalizerName)
320+
space := &toolchainv1alpha1.Space{
321+
ObjectMeta: metav1.ObjectMeta{
322+
Name: "test-space",
323+
Namespace: tierBeingDeleted.Namespace,
324+
Labels: map[string]string{hash.TemplateTierHashLabelKey(tierBeingDeleted.Name): "1234"},
325+
DeletionTimestamp: &metav1.Time{Time: time.Now()},
326+
Finalizers: []string{"dummy"},
327+
},
312328
}
313329

330+
r, req, cl := prepareReconcile(t, tierBeingDeleted.Name, tierBeingDeleted, space)
331+
314332
// when
315333
res, err := r.Reconcile(context.TODO(), req)
334+
inCluster := &toolchainv1alpha1.NSTemplateTier{}
335+
gerr := cl.Get(context.TODO(), runtimeclient.ObjectKeyFromObject(tierBeingDeleted), inCluster)
316336

317337
// then
318338
require.NoError(t, err)
319-
assert.Empty(t, res.Requeue)
339+
require.Empty(t, res)
340+
require.True(t, errors.IsNotFound(gerr))
320341
})
321342
})
322-
323343
}
324344

325345
func TestUpdateNSTemplateTier(t *testing.T) {
326346
// given
327-
base1nsTier := tiertest.Base1nsTier(t, tiertest.CurrentBase1nsTemplates,
347+
base1nsTier := tiertest.Base1nsTier(t, tiertest.CurrentBase1nsTemplates, tiertest.WithFinalizer(),
328348
// the tiertemplate revision CR should have a copy of those parameters
329349
tiertest.WithParameter("DEPLOYMENT_QUOTA", "60"),
330350
)
@@ -454,13 +474,11 @@ func TestUpdateNSTemplateTier(t *testing.T) {
454474
HasStatusTierTemplateRevisions(tierTemplatesRefs).Tier()
455475
require.NotEqual(t, tierBeingUpdated.Status.Revisions, newNSTmplTier.Status.Revisions)
456476
})
457-
458477
})
459-
460478
}
461479

462480
func newTestCRQ(podsCount string) unstructured.Unstructured {
463-
var crq = unstructured.Unstructured{Object: map[string]interface{}{
481+
crq := unstructured.Unstructured{Object: map[string]interface{}{
464482
"kind": "ClusterResourceQuota",
465483
"metadata": map[string]interface{}{
466484
"name": "for-{{.SPACE_NAME}}-deployments",
@@ -512,6 +530,7 @@ func prepareReconcile(t *testing.T, name string, initObjs ...runtimeclient.Objec
512530
Client: cl,
513531
Scheme: s,
514532
}
533+
515534
return r, reconcile.Request{
516535
NamespacedName: types.NamespacedName{
517536
Name: name,

go.mod

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ module github.com/codeready-toolchain/host-operator
22

33
require (
44
cloud.google.com/go/recaptchaenterprise/v2 v2.13.0
5-
github.com/codeready-toolchain/api v0.0.0-20250506092100-39b4862e1271
5+
github.com/codeready-toolchain/api v0.0.0-20250605152105-383ffe6cac27
66
github.com/codeready-toolchain/toolchain-common v0.0.0-20250506093954-2b65ad3a2e12
77
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect
88
github.com/go-logr/logr v1.4.2

go.sum

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -38,8 +38,8 @@ github.com/cloudflare/circl v1.1.0/go.mod h1:prBCrKB9DV4poKZY1l9zBXg2QJY7mvgRvtM
3838
github.com/cloudflare/circl v1.6.1 h1:zqIqSPIndyBh1bjLVVDHMPpVKqp8Su/V+6MeDzzQBQ0=
3939
github.com/cloudflare/circl v1.6.1/go.mod h1:uddAzsPgqdMAYatqJ0lsjX1oECcQLIlRpzZh3pJrofs=
4040
github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc=
41-
github.com/codeready-toolchain/api v0.0.0-20250506092100-39b4862e1271 h1:AHrFr/aPuJ4+0zHw4sFXcfMA92kChy12JAPS5bLODlw=
42-
github.com/codeready-toolchain/api v0.0.0-20250506092100-39b4862e1271/go.mod h1:20258od6i5+jP406Z76YaI2ow/vc7URwsDU2bokpkRE=
41+
github.com/codeready-toolchain/api v0.0.0-20250605152105-383ffe6cac27 h1:g1ivSPPHTC96RHp8S/gRmqODWgoHyivq+/d5kSI0pEs=
42+
github.com/codeready-toolchain/api v0.0.0-20250605152105-383ffe6cac27/go.mod h1:20258od6i5+jP406Z76YaI2ow/vc7URwsDU2bokpkRE=
4343
github.com/codeready-toolchain/toolchain-common v0.0.0-20250506093954-2b65ad3a2e12 h1:w54sojJJ8PsHZzK1mC+/EUBrQ9F2sC/k7JUVc8LSqK4=
4444
github.com/codeready-toolchain/toolchain-common v0.0.0-20250506093954-2b65ad3a2e12/go.mod h1:TrMvD0sP69wI6Rouzfs7OsOUSj4CGn/ZiIdiDBAFQjk=
4545
github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=

test/nstemplatetier/nstemplatetier.go

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import (
88
"github.com/codeready-toolchain/toolchain-common/pkg/hash"
99
"github.com/codeready-toolchain/toolchain-common/pkg/test"
1010
"github.com/codeready-toolchain/toolchain-common/pkg/test/nstemplateset"
11+
"sigs.k8s.io/controller-runtime/pkg/controller/controllerutil"
1112

1213
"github.com/stretchr/testify/require"
1314
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
@@ -245,6 +246,12 @@ func WithParameter(name, value string) TierOption {
245246
}
246247
}
247248

249+
func WithFinalizer() TierOption {
250+
return func(nt *toolchainv1alpha1.NSTemplateTier) {
251+
controllerutil.AddFinalizer(nt, toolchainv1alpha1.FinalizerName)
252+
}
253+
}
254+
248255
// OtherTier returns an "other" NSTemplateTier
249256
func OtherTier(t *testing.T, options ...TierOption) *toolchainv1alpha1.NSTemplateTier {
250257
return Tier(t, "other", toolchainv1alpha1.NSTemplateTierSpec{

0 commit comments

Comments
 (0)