Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: supports incremental backup in release-0.9 #8757

Merged
merged 2 commits into from
Jan 7, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 12 additions & 0 deletions apis/dataprotection/v1alpha1/backup_types.go
Original file line number Diff line number Diff line change
Expand Up @@ -183,6 +183,18 @@ type BackupStatus struct {
// +optional
VolumeSnapshots []VolumeSnapshotStatus `json:"volumeSnapshots,omitempty"`

// Records the parent backup name for incremental or differential backup.
// When the parent backup is deleted, the backup will also be deleted.
//
// +optional
ParentBackupName string `json:"parentBackupName,omitempty"`

// Records the base full backup name for incremental backup or differential backup.
// When the base backup is deleted, the backup will also be deleted.
//
// +optional
BaseBackupName string `json:"baseBackupName,omitempty"`

// Records any additional information for the backup.
//
// +optional
Expand Down
6 changes: 6 additions & 0 deletions apis/dataprotection/v1alpha1/backuppolicy_types.go
Original file line number Diff line number Diff line change
Expand Up @@ -205,6 +205,12 @@ type BackupMethod struct {
// +kubebuilder:validation:Pattern:=`^[a-z0-9]([a-z0-9\.\-]*[a-z0-9])?$`
Name string `json:"name"`

// The name of the compatible full backup method, used by incremental backups.
//
// +optional
// +kubebuilder:validation:Pattern:=`^[a-z0-9]([a-z0-9\.\-]*[a-z0-9])?$`
CompatibleMethod string `json:"compatibleMethod,omitempty"`

// Specifies whether to take snapshots of persistent volumes. If true,
// the ActionSetName is not required, the controller will use the CSI volume
// snapshotter to create the snapshot.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,11 @@ spec:
For volume snapshot backup, the actionSet is not required, the controller
will use the CSI volume snapshotter to create the snapshot.
type: string
compatibleMethod:
description: The name of the compatible full backup method,
used by incremental backups.
pattern: ^[a-z0-9]([a-z0-9\.\-]*[a-z0-9])?$
type: string
env:
description: Specifies the environment variables for the
backup workload.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,11 @@ spec:
For volume snapshot backup, the actionSet is not required, the controller
will use the CSI volume snapshotter to create the snapshot.
type: string
compatibleMethod:
description: The name of the compatible full backup method,
used by incremental backups.
pattern: ^[a-z0-9]([a-z0-9\.\-]*[a-z0-9])?$
type: string
env:
description: Specifies the environment variables for the backup
workload.
Expand Down
15 changes: 15 additions & 0 deletions config/crd/bases/dataprotection.kubeblocks.io_backups.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -268,6 +268,11 @@ spec:
For volume snapshot backup, the actionSet is not required, the controller
will use the CSI volume snapshotter to create the snapshot.
type: string
compatibleMethod:
description: The name of the compatible full backup method, used
by incremental backups.
pattern: ^[a-z0-9]([a-z0-9\.\-]*[a-z0-9])?$
type: string
env:
description: Specifies the environment variables for the backup
workload.
Expand Down Expand Up @@ -957,6 +962,11 @@ spec:
backupRepoName:
description: The name of the backup repository.
type: string
baseBackupName:
description: |-
Records the base full backup name for incremental backup or differential backup.
When the base backup is deleted, the backup will also be deleted.
type: string
completionTimestamp:
description: |-
Records the time when the backup operation was completed.
Expand Down Expand Up @@ -1036,6 +1046,11 @@ spec:
kopiaRepoPath:
description: Records the path of the Kopia repository.
type: string
parentBackupName:
description: |-
Records the parent backup name for incremental or differential backup.
When the parent backup is deleted, the backup will also be deleted.
type: string
path:
description: |-
The directory within the backup repository where the backup data is stored.
Expand Down
2 changes: 1 addition & 1 deletion controllers/dataprotection/actionset_controller_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,7 @@ var _ = Describe("ActionSet Controller test", func() {

Context("create a actionSet", func() {
It("should be available", func() {
as := testdp.NewFakeActionSet(&testCtx)
as := testdp.NewFakeActionSet(&testCtx, nil)
Expect(as).ShouldNot(BeNil())
})
})
Expand Down
123 changes: 106 additions & 17 deletions controllers/dataprotection/backup_controller.go
Original file line number Diff line number Diff line change
Expand Up @@ -241,6 +241,11 @@ func (r *BackupReconciler) deleteBackupFiles(reqCtx intctrlutil.RequestCtx, back
// handleDeletingPhase handles the deletion of backup. It will delete the backup CR
// and the backup workload(job).
func (r *BackupReconciler) handleDeletingPhase(reqCtx intctrlutil.RequestCtx, backup *dpv1alpha1.Backup) (ctrl.Result, error) {
// delete related backups
if err := r.deleteRelatedBackups(reqCtx, backup); err != nil {
return intctrlutil.RequeueWithError(err, reqCtx.Log, "")
}

// if backup phase is Deleting, delete the backup reference workloads,
// backup data stored in backup repository and volume snapshots.
// TODO(ldm): if backup is being used by restore, do not delete it.
Expand Down Expand Up @@ -393,22 +398,6 @@ func (r *BackupReconciler) prepareBackupRequest(
return nil, err
}
request.ActionSet = actionSet

// check continuous backups should have backupschedule label
if request.ActionSet.Spec.BackupType == dpv1alpha1.BackupTypeContinuous {
if _, ok := request.Labels[dptypes.BackupScheduleLabelKey]; !ok {
return nil, fmt.Errorf("continuous backup is only allowed to be created by backupSchedule")
}
backupSchedule := &dpv1alpha1.BackupSchedule{}
if err := request.Client.Get(reqCtx.Ctx, client.ObjectKey{Name: backup.Labels[dptypes.BackupScheduleLabelKey],
Namespace: backup.Namespace}, backupSchedule); err != nil {
return nil, err
}
if backupSchedule.Status.Phase != dpv1alpha1.BackupSchedulePhaseAvailable {
return nil, fmt.Errorf("create continuous backup by failed backupschedule %s/%s",
backupSchedule.Namespace, backupSchedule.Name)
}
}
}

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

request.BackupPolicy = backupPolicy
request.BackupMethod = backupMethod

switch dpv1alpha1.BackupType(request.GetBackupType()) {
case dpv1alpha1.BackupTypeIncremental:
request, err = prepare4Incremental(request)
case dpv1alpha1.BackupTypeContinuous:
err = validateContinuousBackup(backup, reqCtx, request.Client)
}
if err != nil {
return nil, err
}

if !snapshotVolumes {
// if use volume snapshot, ignore backup repo
if err = HandleBackupRepo(request); err != nil {
return nil, err
}
}
request.BackupMethod = backupMethod

return request, nil
}

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

// set status parent backup and base backup name
if request.ParentBackup != nil {
request.Status.ParentBackupName = request.ParentBackup.Name
}
if request.BaseBackup != nil {
request.Status.BaseBackupName = request.BaseBackup.Name
}

if err = dpbackup.SetExpirationByCreationTime(request.Backup); err != nil {
return err
}
Expand Down Expand Up @@ -743,6 +752,33 @@ func (r *BackupReconciler) deleteExternalResources(
return deleteRelatedObjectList(reqCtx, r.Client, &appsv1.StatefulSetList{}, namespaces, labels)
}

// deleteRelatedBackups deletes the related backups.
func (r *BackupReconciler) deleteRelatedBackups(
reqCtx intctrlutil.RequestCtx,
backup *dpv1alpha1.Backup) error {
backupList := &dpv1alpha1.BackupList{}
labels := map[string]string{
dptypes.BackupPolicyLabelKey: backup.Spec.BackupPolicyName,
}
if err := r.Client.List(reqCtx.Ctx, backupList,
client.InNamespace(backup.Namespace), client.MatchingLabels(labels)); client.IgnoreNotFound(err) != nil {
return err
}
for i := range backupList.Items {
bp := &backupList.Items[i]
// delete backups related to the current backup
// files in the related backup's status.path will be deleted by its own associated deleter
if bp.Status.ParentBackupName != backup.Name && bp.Status.BaseBackupName != backup.Name {
continue
}
if err := intctrlutil.BackgroundDeleteObject(r.Client, reqCtx.Ctx, bp); err != nil {
return err
}
reqCtx.Log.Info("delete the related backup", "backup", fmt.Sprintf("%s/%s", bp.Namespace, bp.Name))
}
return nil
}

// PatchBackupObjectMeta patches backup object metaObject include cluster snapshot.
func PatchBackupObjectMeta(
original *dpv1alpha1.Backup,
Expand Down Expand Up @@ -956,3 +992,56 @@ func setClusterSnapshotAnnotation(request *dpbackup.Request, cluster *appsv1alph
request.Backup.Annotations[constant.ClusterSnapshotAnnotationKey] = *clusterString
return nil
}

// validateContinuousBackup validates the continuous backup.
func validateContinuousBackup(backup *dpv1alpha1.Backup, reqCtx intctrlutil.RequestCtx, cli client.Client) error {
// validate if the continuous backup is created by a backupSchedule.
if _, ok := backup.Labels[dptypes.BackupScheduleLabelKey]; !ok {
return fmt.Errorf("continuous backup is only allowed to be created by backupSchedule")
}
backupSchedule := &dpv1alpha1.BackupSchedule{}
if err := cli.Get(reqCtx.Ctx, client.ObjectKey{Name: backup.Labels[dptypes.BackupScheduleLabelKey],
Namespace: backup.Namespace}, backupSchedule); err != nil {
return err
}
if backupSchedule.Status.Phase != dpv1alpha1.BackupSchedulePhaseAvailable {
return fmt.Errorf("create continuous backup by failed backupschedule %s/%s",
backupSchedule.Namespace, backupSchedule.Name)
}
return nil
}

// prepare4Incremental prepares for incremental backup
func prepare4Incremental(request *dpbackup.Request) (*dpbackup.Request, error) {
// get and validate parent backup
parentBackup, err := GetParentBackup(request.Ctx, request.Client, request.Backup, request.BackupMethod)
if err != nil {
return nil, err
}
parentBackupType, err := dputils.GetBackupTypeByMethodName(request.RequestCtx,
request.Client, parentBackup.Spec.BackupMethod, request.BackupPolicy)
if err != nil {
return nil, err
}
request.ParentBackup = parentBackup
// get and validate base backup
switch parentBackupType {
case dpv1alpha1.BackupTypeFull:
request.BaseBackup = request.ParentBackup
case dpv1alpha1.BackupTypeIncremental:
baseBackup := &dpv1alpha1.Backup{}
baseBackupName := request.ParentBackup.Status.BaseBackupName
if len(baseBackupName) == 0 {
return nil, fmt.Errorf("backup %s/%s base backup name is empty",
request.ParentBackup.Namespace, request.ParentBackup.Name)
}
if err := request.Client.Get(request.Ctx, client.ObjectKey{Name: baseBackupName,
Namespace: request.ParentBackup.Namespace}, baseBackup); err != nil {
return nil, fmt.Errorf("failed to get base backup %s/%s: %w", request.ParentBackup.Namespace, baseBackupName, err)
}
request.BaseBackup = baseBackup
default:
return nil, fmt.Errorf("parent backup type is %s, but only full and incremental backup are supported", parentBackupType)
}
return request, nil
}
Loading
Loading