Skip to content

Commit 9bf690a

Browse files
authored
feat: supports incremental backup (#8693)
1 parent 54b1963 commit 9bf690a

33 files changed

+1005
-164
lines changed

apis/dataprotection/v1alpha1/backup_types.go

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

197+
// Records the parent backup name for incremental or differential backup.
198+
// When the parent backup is deleted, the backup will also be deleted.
199+
//
200+
// +optional
201+
ParentBackupName string `json:"parentBackupName,omitempty"`
202+
203+
// Records the base full backup name for incremental backup or differential backup.
204+
// When the base backup is deleted, the backup will also be deleted.
205+
//
206+
// +optional
207+
BaseBackupName string `json:"baseBackupName,omitempty"`
208+
197209
// Records any additional information for the backup.
198210
//
199211
// +optional

apis/dataprotection/v1alpha1/backuppolicy_types.go

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

224+
// The name of the compatible full backup method, used by incremental backups.
225+
//
226+
// +optional
227+
// +kubebuilder:validation:Pattern:=`^[a-z0-9]([a-z0-9\.\-]*[a-z0-9])?$`
228+
CompatibleMethod string `json:"compatibleMethod,omitempty"`
229+
224230
// Specifies whether to take snapshots of persistent volumes. If true,
225231
// the ActionSetName is not required, the controller will use the CSI volume
226232
// snapshotter to create the snapshot.

apis/dataprotection/v1alpha1/backuppolicytemplate_types.go

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

85+
// The name of the compatible full backup method, used by incremental backups.
86+
//
87+
// +optional
88+
// +kubebuilder:validation:Pattern:=`^[a-z0-9]([a-z0-9\.\-]*[a-z0-9])?$`
89+
CompatibleMethod string `json:"compatibleMethod,omitempty"`
90+
8591
// Specifies whether to take snapshots of persistent volumes. If true,
8692
// the ActionSetName is not required, the controller will use the CSI volume
8793
// 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_backuppolicytemplates.yaml

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

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

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -298,6 +298,11 @@ spec:
298298
For volume snapshot backup, the actionSet is not required, the controller
299299
will use the CSI volume snapshotter to create the snapshot.
300300
type: string
301+
compatibleMethod:
302+
description: The name of the compatible full backup method, used
303+
by incremental backups.
304+
pattern: ^[a-z0-9]([a-z0-9\.\-]*[a-z0-9])?$
305+
type: string
301306
env:
302307
description: Specifies the environment variables for the backup
303308
workload.
@@ -1013,6 +1018,11 @@ spec:
10131018
backupRepoName:
10141019
description: The name of the backup repository.
10151020
type: string
1021+
baseBackupName:
1022+
description: |-
1023+
Records the base full backup name for incremental backup or differential backup.
1024+
When the base backup is deleted, the backup will also be deleted.
1025+
type: string
10161026
completionTimestamp:
10171027
description: |-
10181028
Records the time when the backup operation was completed.
@@ -1092,6 +1102,11 @@ spec:
10921102
kopiaRepoPath:
10931103
description: Records the path of the Kopia repository.
10941104
type: string
1105+
parentBackupName:
1106+
description: |-
1107+
Records the parent backup name for incremental or differential backup.
1108+
When the parent backup is deleted, the backup will also be deleted.
1109+
type: string
10951110
path:
10961111
description: |-
10971112
The directory within the backup repository where the backup data is stored.

controllers/apps/transformer_cluster_backup_policy.go

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -414,11 +414,12 @@ func (r *backupPolicyBuilder) syncBackupMethods(backupPolicy *dpv1alpha1.BackupP
414414
}
415415
for _, backupMethodTPL := range r.backupPolicyTPL.Spec.BackupMethods {
416416
backupMethod := dpv1alpha1.BackupMethod{
417-
Name: backupMethodTPL.Name,
418-
ActionSetName: backupMethodTPL.ActionSetName,
419-
SnapshotVolumes: backupMethodTPL.SnapshotVolumes,
420-
TargetVolumes: backupMethodTPL.TargetVolumes,
421-
RuntimeSettings: backupMethodTPL.RuntimeSettings,
417+
Name: backupMethodTPL.Name,
418+
CompatibleMethod: backupMethodTPL.CompatibleMethod,
419+
ActionSetName: backupMethodTPL.ActionSetName,
420+
SnapshotVolumes: backupMethodTPL.SnapshotVolumes,
421+
TargetVolumes: backupMethodTPL.TargetVolumes,
422+
RuntimeSettings: backupMethodTPL.RuntimeSettings,
422423
}
423424
if m, ok := oldBackupMethodMap[backupMethodTPL.Name]; ok {
424425
backupMethod = m

controllers/dataprotection/actionset_controller_test.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -55,14 +55,14 @@ var _ = Describe("ActionSet Controller test", func() {
5555

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

6363
Context("validate a actionSet", func() {
6464
It("validate withParameters", func() {
65-
as := testdp.NewFakeActionSet(&testCtx)
65+
as := testdp.NewFakeActionSet(&testCtx, nil)
6666
Expect(as).ShouldNot(BeNil())
6767
By("set invalid withParameters and schema")
6868
Expect(testapps.ChangeObj(&testCtx, as, func(action *dpv1alpha1.ActionSet) {

controllers/dataprotection/backup_controller.go

Lines changed: 107 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -245,6 +245,11 @@ func (r *BackupReconciler) deleteBackupFiles(reqCtx intctrlutil.RequestCtx, back
245245
// handleDeletingPhase handles the deletion of backup. It will delete the backup CR
246246
// and the backup workload(job).
247247
func (r *BackupReconciler) handleDeletingPhase(reqCtx intctrlutil.RequestCtx, backup *dpv1alpha1.Backup) (ctrl.Result, error) {
248+
// delete related backups
249+
if err := r.deleteRelatedBackups(reqCtx, backup); err != nil {
250+
return intctrlutil.RequeueWithError(err, reqCtx.Log, "")
251+
}
252+
248253
// if backup phase is Deleting, delete the backup reference workloads,
249254
// backup data stored in backup repository and volume snapshots.
250255
// TODO(ldm): if backup is being used by restore, do not delete it.
@@ -395,28 +400,11 @@ func (r *BackupReconciler) prepareBackupRequest(
395400
if err != nil {
396401
return nil, err
397402
}
398-
request.ActionSet = actionSet
399-
400-
// check continuous backups should have backupschedule label
401-
if request.ActionSet.Spec.BackupType == dpv1alpha1.BackupTypeContinuous {
402-
if _, ok := request.Labels[dptypes.BackupScheduleLabelKey]; !ok {
403-
return nil, fmt.Errorf("continuous backup is only allowed to be created by backupSchedule")
404-
}
405-
backupSchedule := &dpv1alpha1.BackupSchedule{}
406-
if err := request.Client.Get(reqCtx.Ctx, client.ObjectKey{Name: backup.Labels[dptypes.BackupScheduleLabelKey],
407-
Namespace: backup.Namespace}, backupSchedule); err != nil {
408-
return nil, err
409-
}
410-
if backupSchedule.Status.Phase != dpv1alpha1.BackupSchedulePhaseAvailable {
411-
return nil, fmt.Errorf("create continuous backup by failed backupschedule %s/%s",
412-
backupSchedule.Namespace, backupSchedule.Name)
413-
}
414-
}
415-
416403
// validate parameters
417404
if err := dputils.ValidateParameters(actionSet, backup.Spec.Parameters, true); err != nil {
418405
return nil, fmt.Errorf("fails to validate parameters with actionset %s: %v", actionSet.Name, err)
419406
}
407+
request.ActionSet = actionSet
420408
}
421409

422410
// check encryption config
@@ -432,13 +420,25 @@ func (r *BackupReconciler) prepareBackupRequest(
432420
}
433421

434422
request.BackupPolicy = backupPolicy
423+
request.BackupMethod = backupMethod
424+
425+
switch dpv1alpha1.BackupType(request.GetBackupType()) {
426+
case dpv1alpha1.BackupTypeIncremental:
427+
request, err = prepare4Incremental(request)
428+
case dpv1alpha1.BackupTypeContinuous:
429+
err = validateContinuousBackup(backup, reqCtx, request.Client)
430+
}
431+
if err != nil {
432+
return nil, err
433+
}
434+
435435
if !snapshotVolumes {
436436
// if use volume snapshot, ignore backup repo
437437
if err = HandleBackupRepo(request); err != nil {
438438
return nil, err
439439
}
440440
}
441-
request.BackupMethod = backupMethod
441+
442442
return request, nil
443443
}
444444

@@ -527,6 +527,14 @@ func (r *BackupReconciler) patchBackupStatus(
527527
request.Status.Phase = dpv1alpha1.BackupPhaseRunning
528528
request.Status.StartTimestamp = &metav1.Time{Time: r.clock.Now().UTC()}
529529

530+
// set status parent backup and base backup name
531+
if request.ParentBackup != nil {
532+
request.Status.ParentBackupName = request.ParentBackup.Name
533+
}
534+
if request.BaseBackup != nil {
535+
request.Status.BaseBackupName = request.BaseBackup.Name
536+
}
537+
530538
if err = dpbackup.SetExpirationByCreationTime(request.Backup); err != nil {
531539
return err
532540
}
@@ -751,6 +759,33 @@ func (r *BackupReconciler) deleteExternalResources(
751759
return deleteRelatedObjectList(reqCtx, r.Client, &appsv1.StatefulSetList{}, namespaces, labels)
752760
}
753761

762+
// deleteRelatedBackups deletes the related backups.
763+
func (r *BackupReconciler) deleteRelatedBackups(
764+
reqCtx intctrlutil.RequestCtx,
765+
backup *dpv1alpha1.Backup) error {
766+
backupList := &dpv1alpha1.BackupList{}
767+
labels := map[string]string{
768+
dptypes.BackupPolicyLabelKey: backup.Spec.BackupPolicyName,
769+
}
770+
if err := r.Client.List(reqCtx.Ctx, backupList,
771+
client.InNamespace(backup.Namespace), client.MatchingLabels(labels)); client.IgnoreNotFound(err) != nil {
772+
return err
773+
}
774+
for i := range backupList.Items {
775+
bp := &backupList.Items[i]
776+
// delete backups related to the current backup
777+
// files in the related backup's status.path will be deleted by its own associated deleter
778+
if bp.Status.ParentBackupName != backup.Name && bp.Status.BaseBackupName != backup.Name {
779+
continue
780+
}
781+
if err := intctrlutil.BackgroundDeleteObject(r.Client, reqCtx.Ctx, bp); err != nil {
782+
return err
783+
}
784+
reqCtx.Log.Info("delete the related backup", "backup", fmt.Sprintf("%s/%s", bp.Namespace, bp.Name))
785+
}
786+
return nil
787+
}
788+
754789
// PatchBackupObjectMeta patches backup object metaObject include cluster snapshot.
755790
func PatchBackupObjectMeta(
756791
original *dpv1alpha1.Backup,
@@ -922,3 +957,56 @@ func setClusterSnapshotAnnotation(request *dpbackup.Request, cluster *kbappsv1.C
922957
request.Backup.Annotations[constant.ClusterSnapshotAnnotationKey] = *clusterString
923958
return nil
924959
}
960+
961+
// validateContinuousBackup validates the continuous backup.
962+
func validateContinuousBackup(backup *dpv1alpha1.Backup, reqCtx intctrlutil.RequestCtx, cli client.Client) error {
963+
// validate if the continuous backup is created by a backupSchedule.
964+
if _, ok := backup.Labels[dptypes.BackupScheduleLabelKey]; !ok {
965+
return fmt.Errorf("continuous backup is only allowed to be created by backupSchedule")
966+
}
967+
backupSchedule := &dpv1alpha1.BackupSchedule{}
968+
if err := cli.Get(reqCtx.Ctx, client.ObjectKey{Name: backup.Labels[dptypes.BackupScheduleLabelKey],
969+
Namespace: backup.Namespace}, backupSchedule); err != nil {
970+
return err
971+
}
972+
if backupSchedule.Status.Phase != dpv1alpha1.BackupSchedulePhaseAvailable {
973+
return fmt.Errorf("create continuous backup by failed backupschedule %s/%s",
974+
backupSchedule.Namespace, backupSchedule.Name)
975+
}
976+
return nil
977+
}
978+
979+
// prepare4Incremental prepares for incremental backup
980+
func prepare4Incremental(request *dpbackup.Request) (*dpbackup.Request, error) {
981+
// get and validate parent backup
982+
parentBackup, err := GetParentBackup(request.Ctx, request.Client, request.Backup, request.BackupMethod)
983+
if err != nil {
984+
return nil, err
985+
}
986+
parentBackupType, err := dputils.GetBackupTypeByMethodName(request.RequestCtx,
987+
request.Client, parentBackup.Spec.BackupMethod, request.BackupPolicy)
988+
if err != nil {
989+
return nil, err
990+
}
991+
request.ParentBackup = parentBackup
992+
// get and validate base backup
993+
switch parentBackupType {
994+
case dpv1alpha1.BackupTypeFull:
995+
request.BaseBackup = request.ParentBackup
996+
case dpv1alpha1.BackupTypeIncremental:
997+
baseBackup := &dpv1alpha1.Backup{}
998+
baseBackupName := request.ParentBackup.Status.BaseBackupName
999+
if len(baseBackupName) == 0 {
1000+
return nil, fmt.Errorf("backup %s/%s base backup name is empty",
1001+
request.ParentBackup.Namespace, request.ParentBackup.Name)
1002+
}
1003+
if err := request.Client.Get(request.Ctx, client.ObjectKey{Name: baseBackupName,
1004+
Namespace: request.ParentBackup.Namespace}, baseBackup); err != nil {
1005+
return nil, fmt.Errorf("failed to get base backup %s/%s: %w", request.ParentBackup.Namespace, baseBackupName, err)
1006+
}
1007+
request.BaseBackup = baseBackup
1008+
default:
1009+
return nil, fmt.Errorf("parent backup type is %s, but only full and incremental backup are supported", parentBackupType)
1010+
}
1011+
return request, nil
1012+
}

0 commit comments

Comments
 (0)