Skip to content

Commit dde83b7

Browse files
committed
fix(dataprotection): surface terminating namespace blocker
1 parent 116b639 commit dde83b7

2 files changed

Lines changed: 207 additions & 1 deletion

File tree

controllers/dataprotection/backup_controller.go

Lines changed: 32 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ package dataprotection
2121

2222
import (
2323
"context"
24+
"errors"
2425
"fmt"
2526
"reflect"
2627
"strings"
@@ -71,6 +72,14 @@ type BackupReconciler struct {
7172
clock clock.RealClock
7273
}
7374

75+
type backupNamespaceTerminatingError struct {
76+
namespace string
77+
}
78+
79+
func (e *backupNamespaceTerminatingError) Error() string {
80+
return fmt.Sprintf("backup namespace %q is terminating; cannot create worker resources to delete backup files, delete the Backup and wait until it is gone before deleting the namespace", e.namespace)
81+
}
82+
7483
// +kubebuilder:rbac:groups=dataprotection.kubeblocks.io,resources=backups,verbs=get;list;watch;create;update;patch;delete
7584
// +kubebuilder:rbac:groups=dataprotection.kubeblocks.io,resources=backups/status,verbs=get;update;patch
7685
// +kubebuilder:rbac:groups=dataprotection.kubeblocks.io,resources=backups/finalizers,verbs=update
@@ -248,8 +257,30 @@ func (r *BackupReconciler) deleteBackupFiles(reqCtx intctrlutil.RequestCtx, back
248257
case dpbackup.DeletionStatusDeleting,
249258
dpbackup.DeletionStatusUnknown:
250259
// wait for the deletion job completed
260+
return r.recordBackupDeletionBlocker(reqCtx, backup, err)
261+
}
262+
return err
263+
}
264+
265+
func (r *BackupReconciler) recordBackupDeletionBlocker(
266+
reqCtx intctrlutil.RequestCtx,
267+
backup *dpv1alpha1.Backup,
268+
err error,
269+
) error {
270+
var terminatingErr *backupNamespaceTerminatingError
271+
if !errors.As(err, &terminatingErr) {
251272
return err
252273
}
274+
275+
failureReason := err.Error()
276+
if backup.Status.FailureReason == failureReason {
277+
return err
278+
}
279+
backupPatch := client.MergeFrom(backup.DeepCopy())
280+
backup.Status.FailureReason = failureReason
281+
if patchErr := r.Status().Patch(reqCtx.Ctx, backup, backupPatch); patchErr != nil {
282+
return patchErr
283+
}
253284
return err
254285
}
255286

@@ -259,7 +290,7 @@ func (r *BackupReconciler) ensureWorkerServiceAccountForBackupDeletion(reqCtx in
259290
return "", fmt.Errorf("failed to get backup namespace %q before deleting backup files: %w", namespace, err)
260291
}
261292
if !ns.DeletionTimestamp.IsZero() {
262-
return "", fmt.Errorf("backup namespace %q is terminating; cannot create worker resources to delete backup files, delete the Backup and wait until it is gone before deleting the namespace", namespace)
293+
return "", &backupNamespaceTerminatingError{namespace: namespace}
263294
}
264295
// TODO: update the mcMgr param
265296
return EnsureWorkerServiceAccount(reqCtx, r.Client, namespace, nil)

controllers/dataprotection/backup_controller_test.go

Lines changed: 175 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ package dataprotection
2222
import (
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+
73248
func TestSyncJobActions(t *testing.T) {
74249
tests := []struct {
75250
name string

0 commit comments

Comments
 (0)