Skip to content

Commit 22a7de0

Browse files
authored
Merge pull request #425 from fluxcd/delete-before-finalize
Handle delete before adding finalizer
2 parents 7c6540c + bb31481 commit 22a7de0

File tree

5 files changed

+95
-10
lines changed

5 files changed

+95
-10
lines changed

internal/controller/imagepolicy_controller.go

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -179,18 +179,20 @@ func (r *ImagePolicyReconciler) Reconcile(ctx context.Context, req ctrl.Request)
179179
r.Metrics.RecordDuration(ctx, obj, start)
180180
}()
181181

182+
// Examine if the object is under deletion.
183+
if !obj.ObjectMeta.DeletionTimestamp.IsZero() {
184+
return r.reconcileDelete(ctx, obj)
185+
}
186+
182187
// Add finalizer first if it doesn't exist to avoid the race condition
183188
// between init and delete.
189+
// Note: Finalizers in general can only be added when the deletionTimestamp
190+
// is not set.
184191
if !controllerutil.ContainsFinalizer(obj, imagev1.ImagePolicyFinalizer) {
185192
controllerutil.AddFinalizer(obj, imagev1.ImagePolicyFinalizer)
186193
return ctrl.Result{Requeue: true}, nil
187194
}
188195

189-
// Examine if the object is under deletion.
190-
if !obj.ObjectMeta.DeletionTimestamp.IsZero() {
191-
return r.reconcileDelete(ctx, obj)
192-
}
193-
194196
// Call subreconciler.
195197
result, retErr = r.reconcile(ctx, serialPatcher, obj)
196198
return

internal/controller/imagepolicy_controller_test.go

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,12 +28,50 @@ import (
2828
corev1 "k8s.io/api/core/v1"
2929
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
3030
"k8s.io/client-go/tools/record"
31+
ctrl "sigs.k8s.io/controller-runtime"
32+
"sigs.k8s.io/controller-runtime/pkg/client"
3133
"sigs.k8s.io/controller-runtime/pkg/client/fake"
3234

3335
imagev1 "github.com/fluxcd/image-reflector-controller/api/v1beta2"
3436
"github.com/fluxcd/image-reflector-controller/internal/policy"
3537
)
3638

39+
func TestImagePolicyReconciler_deleteBeforeFinalizer(t *testing.T) {
40+
g := NewWithT(t)
41+
42+
namespaceName := "imagepolicy-" + randStringRunes(5)
43+
namespace := &corev1.Namespace{
44+
ObjectMeta: metav1.ObjectMeta{Name: namespaceName},
45+
}
46+
g.Expect(k8sClient.Create(ctx, namespace)).ToNot(HaveOccurred())
47+
t.Cleanup(func() {
48+
g.Expect(k8sClient.Delete(ctx, namespace)).NotTo(HaveOccurred())
49+
})
50+
51+
imagePolicy := &imagev1.ImagePolicy{}
52+
imagePolicy.Name = "test-imagepolicy"
53+
imagePolicy.Namespace = namespaceName
54+
imagePolicy.Spec = imagev1.ImagePolicySpec{
55+
ImageRepositoryRef: meta.NamespacedObjectReference{
56+
Name: "foo",
57+
},
58+
Policy: imagev1.ImagePolicyChoice{},
59+
}
60+
// Add a test finalizer to prevent the object from getting deleted.
61+
imagePolicy.SetFinalizers([]string{"test-finalizer"})
62+
g.Expect(k8sClient.Create(ctx, imagePolicy)).NotTo(HaveOccurred())
63+
// Add deletion timestamp by deleting the object.
64+
g.Expect(k8sClient.Delete(ctx, imagePolicy)).NotTo(HaveOccurred())
65+
66+
r := &ImagePolicyReconciler{
67+
Client: k8sClient,
68+
EventRecorder: record.NewFakeRecorder(32),
69+
}
70+
// NOTE: Only a real API server responds with an error in this scenario.
71+
_, err := r.Reconcile(ctx, ctrl.Request{NamespacedName: client.ObjectKeyFromObject(imagePolicy)})
72+
g.Expect(err).NotTo(HaveOccurred())
73+
}
74+
3775
func TestImagePolicyReconciler_getImageRepository(t *testing.T) {
3876
testImageRepoName := "test-repo"
3977
testNamespace1 := "test-ns1" // Default namespace of ImagePolicy.

internal/controller/imagerepository_controller.go

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -166,18 +166,20 @@ func (r *ImageRepositoryReconciler) Reconcile(ctx context.Context, req ctrl.Requ
166166
r.Metrics.RecordDuration(ctx, obj, start)
167167
}()
168168

169+
// Examine if the object is under deletion.
170+
if !obj.ObjectMeta.DeletionTimestamp.IsZero() {
171+
return r.reconcileDelete(ctx, obj)
172+
}
173+
169174
// Add finalizer first if it doesn't exist to avoid the race condition
170175
// between init and delete.
176+
// Note: Finalizers in general can only be added when the deletionTimestamp
177+
// is not set.
171178
if !controllerutil.ContainsFinalizer(obj, imagev1.ImageRepositoryFinalizer) {
172179
controllerutil.AddFinalizer(obj, imagev1.ImageRepositoryFinalizer)
173180
return ctrl.Result{Requeue: true}, nil
174181
}
175182

176-
// Examine if the object is under deletion.
177-
if !obj.ObjectMeta.DeletionTimestamp.IsZero() {
178-
return r.reconcileDelete(ctx, obj)
179-
}
180-
181183
// Return if the object is suspended.
182184
if obj.Spec.Suspend {
183185
log.Info("reconciliation is suspended for this object")

internal/controller/imagerepository_controller_test.go

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@ import (
2828
corev1 "k8s.io/api/core/v1"
2929
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
3030
"k8s.io/client-go/tools/record"
31+
ctrl "sigs.k8s.io/controller-runtime"
3132
"sigs.k8s.io/controller-runtime/pkg/client"
3233
"sigs.k8s.io/controller-runtime/pkg/client/fake"
3334

@@ -63,6 +64,40 @@ func (db mockDatabase) Tags(repo string) ([]string, error) {
6364
return db.TagData, nil
6465
}
6566

67+
func TestImageRepositoryReconciler_deleteBeforeFinalizer(t *testing.T) {
68+
g := NewWithT(t)
69+
70+
namespaceName := "imagerepo-" + randStringRunes(5)
71+
namespace := &corev1.Namespace{
72+
ObjectMeta: metav1.ObjectMeta{Name: namespaceName},
73+
}
74+
g.Expect(k8sClient.Create(ctx, namespace)).ToNot(HaveOccurred())
75+
t.Cleanup(func() {
76+
g.Expect(k8sClient.Delete(ctx, namespace)).NotTo(HaveOccurred())
77+
})
78+
79+
imagerepo := &imagev1.ImageRepository{}
80+
imagerepo.Name = "test-gitrepo"
81+
imagerepo.Namespace = namespaceName
82+
imagerepo.Spec = imagev1.ImageRepositorySpec{
83+
Interval: metav1.Duration{Duration: interval},
84+
Image: "test-image",
85+
}
86+
// Add a test finalizer to prevent the object from getting deleted.
87+
imagerepo.SetFinalizers([]string{"test-finalizer"})
88+
g.Expect(k8sClient.Create(ctx, imagerepo)).NotTo(HaveOccurred())
89+
// Add deletion timestamp by deleting the object.
90+
g.Expect(k8sClient.Delete(ctx, imagerepo)).NotTo(HaveOccurred())
91+
92+
r := &ImageRepositoryReconciler{
93+
Client: k8sClient,
94+
EventRecorder: record.NewFakeRecorder(32),
95+
}
96+
// NOTE: Only a real API server responds with an error in this scenario.
97+
_, err := r.Reconcile(ctx, ctrl.Request{NamespacedName: client.ObjectKeyFromObject(imagerepo)})
98+
g.Expect(err).NotTo(HaveOccurred())
99+
}
100+
66101
func TestImageRepositoryReconciler_setAuthOptions(t *testing.T) {
67102
testImg := "example.com/foo/bar"
68103
testSecretName := "test-secret"

internal/controller/suite_test.go

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@ import (
2929
"k8s.io/client-go/kubernetes/scheme"
3030
"k8s.io/client-go/tools/record"
3131
ctrl "sigs.k8s.io/controller-runtime"
32+
"sigs.k8s.io/controller-runtime/pkg/client"
3233

3334
"github.com/fluxcd/pkg/runtime/controller"
3435
"github.com/fluxcd/pkg/runtime/testenv"
@@ -53,6 +54,7 @@ const (
5354
)
5455

5556
var (
57+
k8sClient client.Client
5658
testEnv *testenv.Environment
5759
testBadgerDB *badger.DB
5860
ctx = ctrl.SetupSignalHandler()
@@ -69,6 +71,12 @@ func TestMain(m *testing.M) {
6971
testEnv = testenv.New(testenv.WithCRDPath(filepath.Join("..", "..", "config", "crd", "bases")))
7072

7173
var err error
74+
// Initialize a cacheless client for tests that need the latest objects.
75+
k8sClient, err = client.New(testEnv.Config, client.Options{Scheme: scheme.Scheme})
76+
if err != nil {
77+
panic(fmt.Sprintf("failed to create k8s client: %v", err))
78+
}
79+
7280
var badgerDir string
7381
badgerDir, err = os.MkdirTemp("", "badger")
7482
if err != nil {

0 commit comments

Comments
 (0)