Skip to content

Commit bf7a77a

Browse files
committed
feat: supports incremental backup (#8693)
1 parent 488b482 commit bf7a77a

28 files changed

+966
-151
lines changed

apis/dataprotection/v1alpha1/backup_types.go

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -183,6 +183,18 @@ type BackupStatus struct {
183183
// +optional
184184
VolumeSnapshots []VolumeSnapshotStatus `json:"volumeSnapshots,omitempty"`
185185

186+
// Records the parent backup name for incremental or differential backup.
187+
// When the parent backup is deleted, the backup will also be deleted.
188+
//
189+
// +optional
190+
ParentBackupName string `json:"parentBackupName,omitempty"`
191+
192+
// Records the base full backup name for incremental backup or differential backup.
193+
// When the base backup is deleted, the backup will also be deleted.
194+
//
195+
// +optional
196+
BaseBackupName string `json:"baseBackupName,omitempty"`
197+
186198
// Records any additional information for the backup.
187199
//
188200
// +optional

apis/dataprotection/v1alpha1/backuppolicy_types.go

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -205,6 +205,12 @@ type BackupMethod struct {
205205
// +kubebuilder:validation:Pattern:=`^[a-z0-9]([a-z0-9\.\-]*[a-z0-9])?$`
206206
Name string `json:"name"`
207207

208+
// The name of the compatible full backup method, used by incremental backups.
209+
//
210+
// +optional
211+
// +kubebuilder:validation:Pattern:=`^[a-z0-9]([a-z0-9\.\-]*[a-z0-9])?$`
212+
CompatibleMethod string `json:"compatibleMethod,omitempty"`
213+
208214
// Specifies whether to take snapshots of persistent volumes. If true,
209215
// the ActionSetName is not required, the controller will use the CSI volume
210216
// snapshotter to create the snapshot.

config/crd/bases/dataprotection.kubeblocks.io_backuppolicies.yaml

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -73,6 +73,11 @@ spec:
7373
For volume snapshot backup, the actionSet is not required, the controller
7474
will use the CSI volume snapshotter to create the snapshot.
7575
type: string
76+
compatibleMethod:
77+
description: The name of the compatible full backup method,
78+
used by incremental backups.
79+
pattern: ^[a-z0-9]([a-z0-9\.\-]*[a-z0-9])?$
80+
type: string
7681
env:
7782
description: Specifies the environment variables for the backup
7883
workload.

config/crd/bases/dataprotection.kubeblocks.io_backups.yaml

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -268,6 +268,11 @@ spec:
268268
For volume snapshot backup, the actionSet is not required, the controller
269269
will use the CSI volume snapshotter to create the snapshot.
270270
type: string
271+
compatibleMethod:
272+
description: The name of the compatible full backup method, used
273+
by incremental backups.
274+
pattern: ^[a-z0-9]([a-z0-9\.\-]*[a-z0-9])?$
275+
type: string
271276
env:
272277
description: Specifies the environment variables for the backup
273278
workload.
@@ -957,6 +962,11 @@ spec:
957962
backupRepoName:
958963
description: The name of the backup repository.
959964
type: string
965+
baseBackupName:
966+
description: |-
967+
Records the base full backup name for incremental backup or differential backup.
968+
When the base backup is deleted, the backup will also be deleted.
969+
type: string
960970
completionTimestamp:
961971
description: |-
962972
Records the time when the backup operation was completed.
@@ -1036,6 +1046,11 @@ spec:
10361046
kopiaRepoPath:
10371047
description: Records the path of the Kopia repository.
10381048
type: string
1049+
parentBackupName:
1050+
description: |-
1051+
Records the parent backup name for incremental or differential backup.
1052+
When the parent backup is deleted, the backup will also be deleted.
1053+
type: string
10391054
path:
10401055
description: |-
10411056
The directory within the backup repository where the backup data is stored.

controllers/dataprotection/actionset_controller_test.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -54,7 +54,7 @@ var _ = Describe("ActionSet Controller test", func() {
5454

5555
Context("create a actionSet", func() {
5656
It("should be available", func() {
57-
as := testdp.NewFakeActionSet(&testCtx)
57+
as := testdp.NewFakeActionSet(&testCtx, nil)
5858
Expect(as).ShouldNot(BeNil())
5959
})
6060
})

controllers/dataprotection/backup_controller.go

Lines changed: 106 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -241,6 +241,11 @@ func (r *BackupReconciler) deleteBackupFiles(reqCtx intctrlutil.RequestCtx, back
241241
// handleDeletingPhase handles the deletion of backup. It will delete the backup CR
242242
// and the backup workload(job).
243243
func (r *BackupReconciler) handleDeletingPhase(reqCtx intctrlutil.RequestCtx, backup *dpv1alpha1.Backup) (ctrl.Result, error) {
244+
// delete related backups
245+
if err := r.deleteRelatedBackups(reqCtx, backup); err != nil {
246+
return intctrlutil.RequeueWithError(err, reqCtx.Log, "")
247+
}
248+
244249
// if backup phase is Deleting, delete the backup reference workloads,
245250
// backup data stored in backup repository and volume snapshots.
246251
// TODO(ldm): if backup is being used by restore, do not delete it.
@@ -393,22 +398,6 @@ func (r *BackupReconciler) prepareBackupRequest(
393398
return nil, err
394399
}
395400
request.ActionSet = actionSet
396-
397-
// check continuous backups should have backupschedule label
398-
if request.ActionSet.Spec.BackupType == dpv1alpha1.BackupTypeContinuous {
399-
if _, ok := request.Labels[dptypes.BackupScheduleLabelKey]; !ok {
400-
return nil, fmt.Errorf("continuous backup is only allowed to be created by backupSchedule")
401-
}
402-
backupSchedule := &dpv1alpha1.BackupSchedule{}
403-
if err := request.Client.Get(reqCtx.Ctx, client.ObjectKey{Name: backup.Labels[dptypes.BackupScheduleLabelKey],
404-
Namespace: backup.Namespace}, backupSchedule); err != nil {
405-
return nil, err
406-
}
407-
if backupSchedule.Status.Phase != dpv1alpha1.BackupSchedulePhaseAvailable {
408-
return nil, fmt.Errorf("create continuous backup by failed backupschedule %s/%s",
409-
backupSchedule.Namespace, backupSchedule.Name)
410-
}
411-
}
412401
}
413402

414403
// check encryption config
@@ -424,13 +413,25 @@ func (r *BackupReconciler) prepareBackupRequest(
424413
}
425414

426415
request.BackupPolicy = backupPolicy
416+
request.BackupMethod = backupMethod
417+
418+
switch dpv1alpha1.BackupType(request.GetBackupType()) {
419+
case dpv1alpha1.BackupTypeIncremental:
420+
request, err = prepare4Incremental(request)
421+
case dpv1alpha1.BackupTypeContinuous:
422+
err = validateContinuousBackup(backup, reqCtx, request.Client)
423+
}
424+
if err != nil {
425+
return nil, err
426+
}
427+
427428
if !snapshotVolumes {
428429
// if use volume snapshot, ignore backup repo
429430
if err = HandleBackupRepo(request); err != nil {
430431
return nil, err
431432
}
432433
}
433-
request.BackupMethod = backupMethod
434+
434435
return request, nil
435436
}
436437

@@ -519,6 +520,14 @@ func (r *BackupReconciler) patchBackupStatus(
519520
request.Status.Phase = dpv1alpha1.BackupPhaseRunning
520521
request.Status.StartTimestamp = &metav1.Time{Time: r.clock.Now().UTC()}
521522

523+
// set status parent backup and base backup name
524+
if request.ParentBackup != nil {
525+
request.Status.ParentBackupName = request.ParentBackup.Name
526+
}
527+
if request.BaseBackup != nil {
528+
request.Status.BaseBackupName = request.BaseBackup.Name
529+
}
530+
522531
if err = dpbackup.SetExpirationByCreationTime(request.Backup); err != nil {
523532
return err
524533
}
@@ -743,6 +752,33 @@ func (r *BackupReconciler) deleteExternalResources(
743752
return deleteRelatedObjectList(reqCtx, r.Client, &appsv1.StatefulSetList{}, namespaces, labels)
744753
}
745754

755+
// deleteRelatedBackups deletes the related backups.
756+
func (r *BackupReconciler) deleteRelatedBackups(
757+
reqCtx intctrlutil.RequestCtx,
758+
backup *dpv1alpha1.Backup) error {
759+
backupList := &dpv1alpha1.BackupList{}
760+
labels := map[string]string{
761+
dptypes.BackupPolicyLabelKey: backup.Spec.BackupPolicyName,
762+
}
763+
if err := r.Client.List(reqCtx.Ctx, backupList,
764+
client.InNamespace(backup.Namespace), client.MatchingLabels(labels)); client.IgnoreNotFound(err) != nil {
765+
return err
766+
}
767+
for i := range backupList.Items {
768+
bp := &backupList.Items[i]
769+
// delete backups related to the current backup
770+
// files in the related backup's status.path will be deleted by its own associated deleter
771+
if bp.Status.ParentBackupName != backup.Name && bp.Status.BaseBackupName != backup.Name {
772+
continue
773+
}
774+
if err := intctrlutil.BackgroundDeleteObject(r.Client, reqCtx.Ctx, bp); err != nil {
775+
return err
776+
}
777+
reqCtx.Log.Info("delete the related backup", "backup", fmt.Sprintf("%s/%s", bp.Namespace, bp.Name))
778+
}
779+
return nil
780+
}
781+
746782
// PatchBackupObjectMeta patches backup object metaObject include cluster snapshot.
747783
func PatchBackupObjectMeta(
748784
original *dpv1alpha1.Backup,
@@ -956,3 +992,56 @@ func setClusterSnapshotAnnotation(request *dpbackup.Request, cluster *appsv1alph
956992
request.Backup.Annotations[constant.ClusterSnapshotAnnotationKey] = *clusterString
957993
return nil
958994
}
995+
996+
// validateContinuousBackup validates the continuous backup.
997+
func validateContinuousBackup(backup *dpv1alpha1.Backup, reqCtx intctrlutil.RequestCtx, cli client.Client) error {
998+
// validate if the continuous backup is created by a backupSchedule.
999+
if _, ok := backup.Labels[dptypes.BackupScheduleLabelKey]; !ok {
1000+
return fmt.Errorf("continuous backup is only allowed to be created by backupSchedule")
1001+
}
1002+
backupSchedule := &dpv1alpha1.BackupSchedule{}
1003+
if err := cli.Get(reqCtx.Ctx, client.ObjectKey{Name: backup.Labels[dptypes.BackupScheduleLabelKey],
1004+
Namespace: backup.Namespace}, backupSchedule); err != nil {
1005+
return err
1006+
}
1007+
if backupSchedule.Status.Phase != dpv1alpha1.BackupSchedulePhaseAvailable {
1008+
return fmt.Errorf("create continuous backup by failed backupschedule %s/%s",
1009+
backupSchedule.Namespace, backupSchedule.Name)
1010+
}
1011+
return nil
1012+
}
1013+
1014+
// prepare4Incremental prepares for incremental backup
1015+
func prepare4Incremental(request *dpbackup.Request) (*dpbackup.Request, error) {
1016+
// get and validate parent backup
1017+
parentBackup, err := GetParentBackup(request.Ctx, request.Client, request.Backup, request.BackupMethod)
1018+
if err != nil {
1019+
return nil, err
1020+
}
1021+
parentBackupType, err := dputils.GetBackupTypeByMethodName(request.RequestCtx,
1022+
request.Client, parentBackup.Spec.BackupMethod, request.BackupPolicy)
1023+
if err != nil {
1024+
return nil, err
1025+
}
1026+
request.ParentBackup = parentBackup
1027+
// get and validate base backup
1028+
switch parentBackupType {
1029+
case dpv1alpha1.BackupTypeFull:
1030+
request.BaseBackup = request.ParentBackup
1031+
case dpv1alpha1.BackupTypeIncremental:
1032+
baseBackup := &dpv1alpha1.Backup{}
1033+
baseBackupName := request.ParentBackup.Status.BaseBackupName
1034+
if len(baseBackupName) == 0 {
1035+
return nil, fmt.Errorf("backup %s/%s base backup name is empty",
1036+
request.ParentBackup.Namespace, request.ParentBackup.Name)
1037+
}
1038+
if err := request.Client.Get(request.Ctx, client.ObjectKey{Name: baseBackupName,
1039+
Namespace: request.ParentBackup.Namespace}, baseBackup); err != nil {
1040+
return nil, fmt.Errorf("failed to get base backup %s/%s: %w", request.ParentBackup.Namespace, baseBackupName, err)
1041+
}
1042+
request.BaseBackup = baseBackup
1043+
default:
1044+
return nil, fmt.Errorf("parent backup type is %s, but only full and incremental backup are supported", parentBackupType)
1045+
}
1046+
return request, nil
1047+
}

0 commit comments

Comments
 (0)