@@ -22,6 +22,7 @@ package dataprotection
2222import (
2323 "context"
2424 "encoding/json"
25+ "errors"
2526 "fmt"
2627 "slices"
2728 "strconv"
@@ -35,6 +36,7 @@ import (
3536 appsv1 "k8s.io/api/apps/v1"
3637 batchv1 "k8s.io/api/batch/v1"
3738 corev1 "k8s.io/api/core/v1"
39+ rbacv1 "k8s.io/api/rbac/v1"
3840 apierrors "k8s.io/apimachinery/pkg/api/errors"
3941 "k8s.io/apimachinery/pkg/api/resource"
4042 metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
@@ -46,6 +48,7 @@ import (
4648 ctrl "sigs.k8s.io/controller-runtime"
4749 "sigs.k8s.io/controller-runtime/pkg/client"
4850 "sigs.k8s.io/controller-runtime/pkg/client/fake"
51+ "sigs.k8s.io/controller-runtime/pkg/controller/controllerutil"
4952
5053 kbappsv1 "github.com/apecloud/kubeblocks/apis/apps/v1"
5154 dpv1alpha1 "github.com/apecloud/kubeblocks/apis/dataprotection/v1alpha1"
@@ -70,6 +73,178 @@ func (c *getErrorClient) Get(context.Context, client.ObjectKey, client.Object, .
7073 return fmt .Errorf ("get failed" )
7174}
7275
76+ type statusPatchCountingClient struct {
77+ client.Client
78+ statusPatchCount int
79+ statusPatchErr error
80+ }
81+
82+ func (c * statusPatchCountingClient ) Status () client.SubResourceWriter {
83+ return & statusPatchCountingWriter {
84+ SubResourceWriter : c .Client .Status (),
85+ count : & c .statusPatchCount ,
86+ patchErr : c .statusPatchErr ,
87+ }
88+ }
89+
90+ type statusPatchCountingWriter struct {
91+ client.SubResourceWriter
92+ count * int
93+ patchErr error
94+ }
95+
96+ func (w * statusPatchCountingWriter ) Patch (
97+ ctx context.Context ,
98+ obj client.Object ,
99+ patch client.Patch ,
100+ opts ... client.SubResourcePatchOption ,
101+ ) error {
102+ * w .count = * w .count + 1
103+ if w .patchErr != nil {
104+ return w .patchErr
105+ }
106+ return w .SubResourceWriter .Patch (ctx , obj , patch , opts ... )
107+ }
108+
109+ func TestDeleteBackupFilesRecordsTerminatingNamespaceBlockerWithoutStoppingRetry (t * testing.T ) {
110+ g := NewWithT (t )
111+ scheme := runtime .NewScheme ()
112+ g .Expect (corev1 .AddToScheme (scheme )).To (Succeed ())
113+ g .Expect (batchv1 .AddToScheme (scheme )).To (Succeed ())
114+ g .Expect (rbacv1 .AddToScheme (scheme )).To (Succeed ())
115+ g .Expect (dpv1alpha1 .AddToScheme (scheme )).To (Succeed ())
116+
117+ now := metav1 .Now ()
118+ namespace := & corev1.Namespace {ObjectMeta : metav1.ObjectMeta {
119+ Name : "terminating-backup" ,
120+ DeletionTimestamp : & now ,
121+ Finalizers : []string {"test.kubeblocks.io/finalizer" },
122+ }}
123+ backup := & dpv1alpha1.Backup {
124+ ObjectMeta : metav1.ObjectMeta {
125+ Name : "backup" ,
126+ Namespace : namespace .Name ,
127+ UID : types .UID ("backup-uid" ),
128+ DeletionTimestamp : & now ,
129+ Finalizers : []string {dptypes .DataProtectionFinalizerName },
130+ },
131+ Spec : dpv1alpha1.BackupSpec {DeletionPolicy : dpv1alpha1 .BackupDeletionPolicyDelete },
132+ Status : dpv1alpha1.BackupStatus {
133+ Phase : dpv1alpha1 .BackupPhaseDeleting ,
134+ BackupRepoName : "repo" ,
135+ Path : "/backups/backup" ,
136+ },
137+ }
138+ repo := & dpv1alpha1.BackupRepo {ObjectMeta : metav1.ObjectMeta {Name : backup .Status .BackupRepoName }}
139+ baseClient := fake .NewClientBuilder ().
140+ WithScheme (scheme ).
141+ WithStatusSubresource (& dpv1alpha1.Backup {}).
142+ WithObjects (namespace , backup , repo ).
143+ Build ()
144+ countingClient := & statusPatchCountingClient {Client : baseClient }
145+ reconciler := & BackupReconciler {Client : countingClient , Scheme : scheme }
146+ reqCtx := intctrlutil.RequestCtx {Ctx : context .Background ()}
147+
148+ err := reconciler .deleteBackupFiles (reqCtx , backup .DeepCopy ())
149+ var terminatingErr * backupNamespaceTerminatingError
150+ g .Expect (errors .As (err , & terminatingErr )).To (BeTrue ())
151+ g .Expect (err .Error ()).To (ContainSubstring ("is terminating; cannot create worker resources to delete backup files" ))
152+ g .Expect (countingClient .statusPatchCount ).To (Equal (1 ))
153+
154+ stored := & dpv1alpha1.Backup {}
155+ g .Expect (baseClient .Get (reqCtx .Ctx , client .ObjectKeyFromObject (backup ), stored )).To (Succeed ())
156+ g .Expect (stored .Status .FailureReason ).To (Equal (err .Error ()))
157+ g .Expect (controllerutil .ContainsFinalizer (stored , dptypes .DataProtectionFinalizerName )).To (BeTrue ())
158+
159+ jobs := & batchv1.JobList {}
160+ g .Expect (baseClient .List (reqCtx .Ctx , jobs , client .InNamespace (namespace .Name ))).To (Succeed ())
161+ g .Expect (jobs .Items ).To (BeEmpty ())
162+ serviceAccounts := & corev1.ServiceAccountList {}
163+ g .Expect (baseClient .List (reqCtx .Ctx , serviceAccounts , client .InNamespace (namespace .Name ))).To (Succeed ())
164+ g .Expect (serviceAccounts .Items ).To (BeEmpty ())
165+ roleBindings := & rbacv1.RoleBindingList {}
166+ g .Expect (baseClient .List (reqCtx .Ctx , roleBindings , client .InNamespace (namespace .Name ))).To (Succeed ())
167+ g .Expect (roleBindings .Items ).To (BeEmpty ())
168+
169+ err = reconciler .deleteBackupFiles (reqCtx , stored .DeepCopy ())
170+ g .Expect (errors .As (err , & terminatingErr )).To (BeTrue ())
171+ g .Expect (countingClient .statusPatchCount ).To (Equal (1 ))
172+ }
173+
174+ func TestDeleteBackupFilesDoesNotRecordOrdinaryDeletingError (t * testing.T ) {
175+ g := NewWithT (t )
176+ scheme := runtime .NewScheme ()
177+ g .Expect (batchv1 .AddToScheme (scheme )).To (Succeed ())
178+ g .Expect (dpv1alpha1 .AddToScheme (scheme )).To (Succeed ())
179+
180+ backup := & dpv1alpha1.Backup {ObjectMeta : metav1.ObjectMeta {
181+ Name : "backup" ,
182+ Namespace : "default" ,
183+ UID : types .UID ("backup-uid" ),
184+ Finalizers : []string {dptypes .DataProtectionFinalizerName },
185+ }}
186+ baseClient := fake .NewClientBuilder ().
187+ WithScheme (scheme ).
188+ WithStatusSubresource (& dpv1alpha1.Backup {}).
189+ WithObjects (backup ).
190+ Build ()
191+ countingClient := & statusPatchCountingClient {Client : baseClient }
192+ reconciler := & BackupReconciler {
193+ Client : & getErrorClient {Client : countingClient },
194+ Scheme : scheme ,
195+ }
196+
197+ err := reconciler .deleteBackupFiles (intctrlutil.RequestCtx {Ctx : context .Background ()}, backup .DeepCopy ())
198+ g .Expect (err ).To (MatchError ("get failed" ))
199+ g .Expect (countingClient .statusPatchCount ).To (Equal (0 ))
200+ g .Expect (backup .Status .FailureReason ).To (BeEmpty ())
201+ }
202+
203+ func TestDeleteBackupFilesReturnsStatusPatchError (t * testing.T ) {
204+ g := NewWithT (t )
205+ scheme := runtime .NewScheme ()
206+ g .Expect (corev1 .AddToScheme (scheme )).To (Succeed ())
207+ g .Expect (batchv1 .AddToScheme (scheme )).To (Succeed ())
208+ g .Expect (dpv1alpha1 .AddToScheme (scheme )).To (Succeed ())
209+
210+ now := metav1 .Now ()
211+ namespace := & corev1.Namespace {ObjectMeta : metav1.ObjectMeta {
212+ Name : "terminating-backup" ,
213+ DeletionTimestamp : & now ,
214+ Finalizers : []string {"test.kubeblocks.io/finalizer" },
215+ }}
216+ backup := & dpv1alpha1.Backup {
217+ ObjectMeta : metav1.ObjectMeta {
218+ Name : "backup" ,
219+ Namespace : namespace .Name ,
220+ UID : types .UID ("backup-uid" ),
221+ Finalizers : []string {dptypes .DataProtectionFinalizerName },
222+ },
223+ Status : dpv1alpha1.BackupStatus {
224+ Phase : dpv1alpha1 .BackupPhaseDeleting ,
225+ BackupRepoName : "repo" ,
226+ Path : "/backups/backup" ,
227+ },
228+ }
229+ repo := & dpv1alpha1.BackupRepo {ObjectMeta : metav1.ObjectMeta {Name : backup .Status .BackupRepoName }}
230+ baseClient := fake .NewClientBuilder ().
231+ WithScheme (scheme ).
232+ WithStatusSubresource (& dpv1alpha1.Backup {}).
233+ WithObjects (namespace , backup , repo ).
234+ Build ()
235+ patchErr := errors .New ("status patch failed" )
236+ countingClient := & statusPatchCountingClient {Client : baseClient , statusPatchErr : patchErr }
237+ reconciler := & BackupReconciler {Client : countingClient , Scheme : scheme }
238+
239+ err := reconciler .deleteBackupFiles (intctrlutil.RequestCtx {Ctx : context .Background ()}, backup .DeepCopy ())
240+ g .Expect (err ).To (MatchError (patchErr ))
241+ g .Expect (countingClient .statusPatchCount ).To (Equal (1 ))
242+
243+ stored := & dpv1alpha1.Backup {}
244+ g .Expect (baseClient .Get (context .Background (), client .ObjectKeyFromObject (backup ), stored )).To (Succeed ())
245+ g .Expect (stored .Status .FailureReason ).To (BeEmpty ())
246+ }
247+
73248func TestSyncJobActions (t * testing.T ) {
74249 tests := []struct {
75250 name string
0 commit comments