github.com/1aal/kubeblocks@v0.0.0-20231107070852-e1c03e598921/controllers/dataprotection/backup_controller.go (about)

     1  /*
     2  Copyright (C) 2022-2023 ApeCloud Co., Ltd
     3  
     4  This file is part of KubeBlocks project
     5  
     6  This program is free software: you can redistribute it and/or modify
     7  it under the terms of the GNU Affero General Public License as published by
     8  the Free Software Foundation, either version 3 of the License, or
     9  (at your option) any later version.
    10  
    11  This program is distributed in the hope that it will be useful
    12  but WITHOUT ANY WARRANTY; without even the implied warranty of
    13  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
    14  GNU Affero General Public License for more details.
    15  
    16  You should have received a copy of the GNU Affero General Public License
    17  along with this program.  If not, see <http://www.gnu.org/licenses/>.
    18  */
    19  
    20  package dataprotection
    21  
    22  import (
    23  	"context"
    24  	"encoding/json"
    25  	"fmt"
    26  	"reflect"
    27  	"time"
    28  
    29  	vsv1beta1 "github.com/kubernetes-csi/external-snapshotter/client/v3/apis/volumesnapshot/v1beta1"
    30  	vsv1 "github.com/kubernetes-csi/external-snapshotter/client/v6/apis/volumesnapshot/v1"
    31  	appsv1 "k8s.io/api/apps/v1"
    32  	batchv1 "k8s.io/api/batch/v1"
    33  	corev1 "k8s.io/api/core/v1"
    34  	apierrors "k8s.io/apimachinery/pkg/api/errors"
    35  	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
    36  	k8sruntime "k8s.io/apimachinery/pkg/runtime"
    37  	"k8s.io/client-go/rest"
    38  	"k8s.io/client-go/tools/record"
    39  	"k8s.io/utils/clock"
    40  	ctrl "sigs.k8s.io/controller-runtime"
    41  	"sigs.k8s.io/controller-runtime/pkg/builder"
    42  	"sigs.k8s.io/controller-runtime/pkg/client"
    43  	"sigs.k8s.io/controller-runtime/pkg/controller"
    44  	"sigs.k8s.io/controller-runtime/pkg/controller/controllerutil"
    45  	"sigs.k8s.io/controller-runtime/pkg/log"
    46  
    47  	appsv1alpha1 "github.com/1aal/kubeblocks/apis/apps/v1alpha1"
    48  	dpv1alpha1 "github.com/1aal/kubeblocks/apis/dataprotection/v1alpha1"
    49  	"github.com/1aal/kubeblocks/pkg/constant"
    50  	"github.com/1aal/kubeblocks/pkg/controller/model"
    51  	intctrlutil "github.com/1aal/kubeblocks/pkg/controllerutil"
    52  	"github.com/1aal/kubeblocks/pkg/dataprotection/action"
    53  	dpbackup "github.com/1aal/kubeblocks/pkg/dataprotection/backup"
    54  	dperrors "github.com/1aal/kubeblocks/pkg/dataprotection/errors"
    55  	dptypes "github.com/1aal/kubeblocks/pkg/dataprotection/types"
    56  	dputils "github.com/1aal/kubeblocks/pkg/dataprotection/utils"
    57  	"github.com/1aal/kubeblocks/pkg/dataprotection/utils/boolptr"
    58  	viper "github.com/1aal/kubeblocks/pkg/viperx"
    59  )
    60  
    61  // BackupReconciler reconciles a Backup object
    62  type BackupReconciler struct {
    63  	client.Client
    64  	Scheme     *k8sruntime.Scheme
    65  	Recorder   record.EventRecorder
    66  	RestConfig *rest.Config
    67  	clock      clock.RealClock
    68  }
    69  
    70  // +kubebuilder:rbac:groups=dataprotection.kubeblocks.io,resources=backups,verbs=get;list;watch;create;update;patch;delete
    71  // +kubebuilder:rbac:groups=dataprotection.kubeblocks.io,resources=backups/status,verbs=get;update;patch
    72  // +kubebuilder:rbac:groups=dataprotection.kubeblocks.io,resources=backups/finalizers,verbs=update
    73  
    74  // +kubebuilder:rbac:groups=batch,resources=jobs,verbs=get;list;watch;create;update;patch;delete
    75  // +kubebuilder:rbac:groups=snapshot.storage.k8s.io,resources=volumesnapshots,verbs=get;list;watch;create;update;patch;delete
    76  // +kubebuilder:rbac:groups=snapshot.storage.k8s.io,resources=volumesnapshots/finalizers,verbs=update;patch
    77  // +kubebuilder:rbac:groups=snapshot.storage.k8s.io,resources=volumesnapshotclasses,verbs=get;list;watch;create;update;patch;delete
    78  // +kubebuilder:rbac:groups=snapshot.storage.k8s.io,resources=volumesnapshotclasses/finalizers,verbs=update;patch
    79  
    80  // Reconcile is part of the main kubernetes reconciliation loop which aims to
    81  // move the current state of the backup closer to the desired state.
    82  func (r *BackupReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) {
    83  	// setup common request context
    84  	reqCtx := intctrlutil.RequestCtx{
    85  		Ctx:      ctx,
    86  		Req:      req,
    87  		Log:      log.FromContext(ctx).WithValues("backup", req.NamespacedName),
    88  		Recorder: r.Recorder,
    89  	}
    90  
    91  	// get backup object, and return if not found
    92  	backup := &dpv1alpha1.Backup{}
    93  	if err := r.Client.Get(reqCtx.Ctx, reqCtx.Req.NamespacedName, backup); err != nil {
    94  		return intctrlutil.CheckedRequeueWithError(err, reqCtx.Log, "")
    95  	}
    96  
    97  	reqCtx.Log.V(1).Info("reconcile", "backup", req.NamespacedName, "phase", backup.Status.Phase)
    98  
    99  	// if backup is being deleted, set backup phase to Deleting. The backup
   100  	// reference workloads, data and volume snapshots will be deleted by controller
   101  	// later when the backup status.phase is deleting.
   102  	if !backup.GetDeletionTimestamp().IsZero() && backup.Status.Phase != dpv1alpha1.BackupPhaseDeleting {
   103  		patch := client.MergeFrom(backup.DeepCopy())
   104  		backup.Status.Phase = dpv1alpha1.BackupPhaseDeleting
   105  		if err := r.Client.Status().Patch(reqCtx.Ctx, backup, patch); err != nil {
   106  			return intctrlutil.RequeueWithError(err, reqCtx.Log, "")
   107  		}
   108  	}
   109  
   110  	switch backup.Status.Phase {
   111  	case "", dpv1alpha1.BackupPhaseNew:
   112  		return r.handleNewPhase(reqCtx, backup)
   113  	case dpv1alpha1.BackupPhaseRunning:
   114  		return r.handleRunningPhase(reqCtx, backup)
   115  	case dpv1alpha1.BackupPhaseCompleted:
   116  		return r.handleCompletedPhase(reqCtx, backup)
   117  	case dpv1alpha1.BackupPhaseDeleting:
   118  		return r.handleDeletingPhase(reqCtx, backup)
   119  	default:
   120  		return intctrlutil.Reconciled()
   121  	}
   122  }
   123  
   124  // SetupWithManager sets up the controller with the Manager.
   125  func (r *BackupReconciler) SetupWithManager(mgr ctrl.Manager) error {
   126  	b := ctrl.NewControllerManagedBy(mgr).
   127  		For(&dpv1alpha1.Backup{}).
   128  		WithOptions(controller.Options{
   129  			MaxConcurrentReconciles: viper.GetInt(maxConcurDataProtectionReconKey),
   130  		}).
   131  		Owns(&batchv1.Job{})
   132  
   133  	if intctrlutil.InVolumeSnapshotV1Beta1() {
   134  		b.Owns(&vsv1beta1.VolumeSnapshot{}, builder.Predicates{})
   135  	} else {
   136  		b.Owns(&vsv1.VolumeSnapshot{}, builder.Predicates{})
   137  	}
   138  	return b.Complete(r)
   139  }
   140  
   141  // deleteBackupFiles deletes the backup files stored in backup repository.
   142  func (r *BackupReconciler) deleteBackupFiles(reqCtx intctrlutil.RequestCtx, backup *dpv1alpha1.Backup) error {
   143  	deleteBackup := func() error {
   144  		// remove backup finalizers to delete it
   145  		patch := client.MergeFrom(backup.DeepCopy())
   146  		controllerutil.RemoveFinalizer(backup, dptypes.DataProtectionFinalizerName)
   147  		return r.Patch(reqCtx.Ctx, backup, patch)
   148  	}
   149  
   150  	deleter := &dpbackup.Deleter{
   151  		RequestCtx: reqCtx,
   152  		Client:     r.Client,
   153  		Scheme:     r.Scheme,
   154  	}
   155  
   156  	status, err := deleter.DeleteBackupFiles(backup)
   157  	switch status {
   158  	case dpbackup.DeletionStatusSucceeded:
   159  		return deleteBackup()
   160  	case dpbackup.DeletionStatusFailed:
   161  		failureReason := err.Error()
   162  		if backup.Status.FailureReason == failureReason {
   163  			return nil
   164  		}
   165  		backupPatch := client.MergeFrom(backup.DeepCopy())
   166  		backup.Status.FailureReason = failureReason
   167  		r.Recorder.Event(backup, corev1.EventTypeWarning, "DeleteBackupFilesFailed", failureReason)
   168  		return r.Status().Patch(reqCtx.Ctx, backup, backupPatch)
   169  	case dpbackup.DeletionStatusDeleting,
   170  		dpbackup.DeletionStatusUnknown:
   171  		// wait for the deletion job completed
   172  		return err
   173  	}
   174  	return err
   175  }
   176  
   177  // handleDeletingPhase handles the deletion of backup. It will delete the backup CR
   178  // and the backup workload(job/statefulset).
   179  func (r *BackupReconciler) handleDeletingPhase(reqCtx intctrlutil.RequestCtx, backup *dpv1alpha1.Backup) (ctrl.Result, error) {
   180  	// if backup phase is Deleting, delete the backup reference workloads,
   181  	// backup data stored in backup repository and volume snapshots.
   182  	// TODO(ldm): if backup is being used by restore, do not delete it.
   183  	if err := r.deleteExternalResources(reqCtx, backup); err != nil {
   184  		return intctrlutil.RequeueWithError(err, reqCtx.Log, "")
   185  	}
   186  
   187  	if backup.Spec.DeletionPolicy == dpv1alpha1.BackupDeletionPolicyRetain {
   188  		r.Recorder.Event(backup, corev1.EventTypeWarning, "Retain", "can not delete the backup if deletionPolicy is Retain")
   189  		return intctrlutil.Reconciled()
   190  	}
   191  
   192  	if err := r.deleteVolumeSnapshots(reqCtx, backup); err != nil {
   193  		return intctrlutil.RequeueWithError(err, reqCtx.Log, "")
   194  	}
   195  
   196  	if err := r.deleteBackupFiles(reqCtx, backup); err != nil {
   197  		return intctrlutil.RequeueWithError(err, reqCtx.Log, "")
   198  	}
   199  	return intctrlutil.Reconciled()
   200  }
   201  
   202  func (r *BackupReconciler) handleNewPhase(
   203  	reqCtx intctrlutil.RequestCtx,
   204  	backup *dpv1alpha1.Backup) (ctrl.Result, error) {
   205  	request, err := r.prepareBackupRequest(reqCtx, backup)
   206  	if err != nil {
   207  		return r.updateStatusIfFailed(reqCtx, backup.DeepCopy(), backup, err)
   208  	}
   209  
   210  	// set and patch backup object meta, including labels, annotations and finalizers
   211  	// if the backup object meta is changed, the backup object will be patched.
   212  	if wait, err := r.patchBackupObjectMeta(backup, request); err != nil {
   213  		return r.updateStatusIfFailed(reqCtx, backup, request.Backup, err)
   214  	} else if wait {
   215  		return intctrlutil.Reconciled()
   216  	}
   217  
   218  	// set and patch backup status
   219  	if err = r.patchBackupStatus(backup, request); err != nil {
   220  		return r.updateStatusIfFailed(reqCtx, backup, request.Backup, err)
   221  	}
   222  	return intctrlutil.Reconciled()
   223  }
   224  
   225  // prepareBackupRequest prepares a request for a backup, with all references to
   226  // other kubernetes objects, and validate them.
   227  func (r *BackupReconciler) prepareBackupRequest(
   228  	reqCtx intctrlutil.RequestCtx,
   229  	backup *dpv1alpha1.Backup) (*dpbackup.Request, error) {
   230  	request := &dpbackup.Request{
   231  		Backup:     backup.DeepCopy(),
   232  		RequestCtx: reqCtx,
   233  		Client:     r.Client,
   234  	}
   235  
   236  	if request.Annotations == nil {
   237  		request.Annotations = make(map[string]string)
   238  	}
   239  
   240  	if request.Labels == nil {
   241  		request.Labels = make(map[string]string)
   242  	}
   243  
   244  	backupPolicy, err := getBackupPolicyByName(reqCtx, r.Client, backup.Spec.BackupPolicyName)
   245  	if err != nil {
   246  		return nil, err
   247  	}
   248  
   249  	targetPods, err := getTargetPods(reqCtx, r.Client,
   250  		backup.Annotations[dptypes.BackupTargetPodLabelKey], backupPolicy)
   251  	if err != nil || len(targetPods) == 0 {
   252  		return nil, fmt.Errorf("failed to get target pods by backup policy %s/%s",
   253  			backupPolicy.Namespace, backupPolicy.Name)
   254  	}
   255  
   256  	if len(targetPods) > 1 {
   257  		return nil, fmt.Errorf("do not support more than one target pods")
   258  	}
   259  
   260  	backupMethod := getBackupMethodByName(backup.Spec.BackupMethod, backupPolicy)
   261  	if backupMethod == nil {
   262  		return nil, intctrlutil.NewNotFound("backupMethod: %s not found",
   263  			backup.Spec.BackupMethod)
   264  	}
   265  
   266  	// backupMethod should specify snapshotVolumes or actionSetName, if we take
   267  	// snapshots to back up volumes, the snapshotVolumes should be set to true
   268  	// and the actionSetName is not required, if we do not take snapshots to back
   269  	// up volumes, the actionSetName is required.
   270  	snapshotVolumes := boolptr.IsSetToTrue(backupMethod.SnapshotVolumes)
   271  	if !snapshotVolumes && backupMethod.ActionSetName == "" {
   272  		return nil, fmt.Errorf("backup method %s should specify snapshotVolumes or actionSetName", backupMethod.Name)
   273  	}
   274  
   275  	if backupMethod.ActionSetName != "" {
   276  		actionSet, err := getActionSetByName(reqCtx, r.Client, backupMethod.ActionSetName)
   277  		if err != nil {
   278  			return nil, err
   279  		}
   280  		if actionSet.Spec.BackupType != dpv1alpha1.BackupTypeFull {
   281  			return nil, fmt.Errorf("only support backup type Full for actionSet %s", actionSet.Name)
   282  		}
   283  		request.ActionSet = actionSet
   284  	}
   285  
   286  	request.BackupPolicy = backupPolicy
   287  	if !snapshotVolumes {
   288  		// if use volume snapshot, ignore backup repo
   289  		if err = r.handleBackupRepo(request); err != nil {
   290  			return nil, err
   291  		}
   292  	}
   293  
   294  	request.BackupMethod = backupMethod
   295  	request.TargetPods = targetPods
   296  	return request, nil
   297  }
   298  
   299  // handleBackupRepo handles the backup repo, and get the backup repo PVC. If the
   300  // PVC is not present, it will add a special label and wait for the backup repo
   301  // controller to create the PVC.
   302  func (r *BackupReconciler) handleBackupRepo(request *dpbackup.Request) error {
   303  	repo, err := r.getBackupRepo(request.Ctx, request.Backup, request.BackupPolicy)
   304  	if err != nil {
   305  		return err
   306  	}
   307  	request.BackupRepo = repo
   308  
   309  	if repo.Status.Phase != dpv1alpha1.BackupRepoReady {
   310  		return dperrors.NewBackupRepoIsNotReady(repo.Name)
   311  	}
   312  
   313  	switch {
   314  	case repo.AccessByMount():
   315  		pvcName := repo.Status.BackupPVCName
   316  		if pvcName == "" {
   317  			return dperrors.NewBackupPVCNameIsEmpty(repo.Name, request.Spec.BackupPolicyName)
   318  		}
   319  		pvc := &corev1.PersistentVolumeClaim{}
   320  		pvcKey := client.ObjectKey{Namespace: request.Req.Namespace, Name: pvcName}
   321  		if err = r.Client.Get(request.Ctx, pvcKey, pvc); err != nil {
   322  			// will wait for the backuprepo controller to create the PVC,
   323  			// so ignore the NotFound error
   324  			return client.IgnoreNotFound(err)
   325  		}
   326  		// backupRepo PVC exists, record the PVC name
   327  		if err == nil {
   328  			request.BackupRepoPVC = pvc
   329  		}
   330  	case repo.AccessByTool():
   331  		toolConfigSecretName := repo.Status.ToolConfigSecretName
   332  		if toolConfigSecretName == "" {
   333  			return dperrors.NewToolConfigSecretNameIsEmpty(repo.Name)
   334  		}
   335  		secret := &corev1.Secret{}
   336  		secretKey := client.ObjectKey{Namespace: request.Req.Namespace, Name: toolConfigSecretName}
   337  		if err = r.Client.Get(request.Ctx, secretKey, secret); err != nil {
   338  			// will wait for the backuprepo controller to create the secret,
   339  			// so ignore the NotFound error
   340  			return client.IgnoreNotFound(err)
   341  		}
   342  		if err == nil {
   343  			request.ToolConfigSecret = secret
   344  		}
   345  	}
   346  	return nil
   347  }
   348  
   349  func (r *BackupReconciler) patchBackupStatus(
   350  	original *dpv1alpha1.Backup,
   351  	request *dpbackup.Request) error {
   352  	request.Status.FormatVersion = dpbackup.FormatVersion
   353  	request.Status.Path = dpbackup.BuildBackupPath(request.Backup, request.BackupPolicy.Spec.PathPrefix)
   354  	request.Status.Target = request.BackupPolicy.Spec.Target
   355  	request.Status.BackupMethod = request.BackupMethod
   356  	if request.BackupRepo != nil {
   357  		request.Status.BackupRepoName = request.BackupRepo.Name
   358  	}
   359  	if request.BackupRepoPVC != nil {
   360  		request.Status.PersistentVolumeClaimName = request.BackupRepoPVC.Name
   361  	}
   362  	// init action status
   363  	actions, err := request.BuildActions()
   364  	if err != nil {
   365  		return err
   366  	}
   367  	request.Status.Actions = make([]dpv1alpha1.ActionStatus, len(actions))
   368  	for i, act := range actions {
   369  		request.Status.Actions[i] = dpv1alpha1.ActionStatus{
   370  			Name:       act.GetName(),
   371  			Phase:      dpv1alpha1.ActionPhaseNew,
   372  			ActionType: act.Type(),
   373  		}
   374  	}
   375  
   376  	// update phase to running
   377  	request.Status.Phase = dpv1alpha1.BackupPhaseRunning
   378  	request.Status.StartTimestamp = &metav1.Time{Time: r.clock.Now().UTC()}
   379  
   380  	if err = setExpirationByCreationTime(request.Backup); err != nil {
   381  		return err
   382  	}
   383  	return r.Client.Status().Patch(request.Ctx, request.Backup, client.MergeFrom(original))
   384  }
   385  
   386  // patchBackupObjectMeta patches backup object metaObject include cluster snapshot.
   387  func (r *BackupReconciler) patchBackupObjectMeta(
   388  	original *dpv1alpha1.Backup,
   389  	request *dpbackup.Request) (bool, error) {
   390  	targetPod := request.TargetPods[0]
   391  
   392  	// get KubeBlocks cluster and set labels and annotations for backup
   393  	// TODO(ldm): we should remove this dependency of cluster in the future
   394  	cluster := getCluster(request.Ctx, r.Client, targetPod)
   395  	if cluster != nil {
   396  		if err := setClusterSnapshotAnnotation(request.Backup, cluster); err != nil {
   397  			return false, err
   398  		}
   399  		if err := r.setConnectionPasswordAnnotation(request); err != nil {
   400  			return false, err
   401  		}
   402  		request.Labels[dptypes.ClusterUIDLabelKey] = string(cluster.UID)
   403  	}
   404  
   405  	for _, v := range getClusterLabelKeys() {
   406  		request.Labels[v] = targetPod.Labels[v]
   407  	}
   408  
   409  	request.Labels[constant.AppManagedByLabelKey] = constant.AppName
   410  	request.Labels[dptypes.BackupTypeLabelKey] = request.GetBackupType()
   411  	// wait for the backup repo controller to prepare the essential resource.
   412  	wait := false
   413  	if request.BackupRepo != nil {
   414  		request.Labels[dataProtectionBackupRepoKey] = request.BackupRepo.Name
   415  		if (request.BackupRepo.AccessByMount() && request.BackupRepoPVC == nil) ||
   416  			(request.BackupRepo.AccessByTool() && request.ToolConfigSecret == nil) {
   417  			request.Labels[dataProtectionWaitRepoPreparationKey] = trueVal
   418  			wait = true
   419  		}
   420  	}
   421  
   422  	// set annotations
   423  	request.Annotations[dptypes.BackupTargetPodLabelKey] = targetPod.Name
   424  
   425  	// set finalizer
   426  	controllerutil.AddFinalizer(request.Backup, dptypes.DataProtectionFinalizerName)
   427  
   428  	if reflect.DeepEqual(original.ObjectMeta, request.ObjectMeta) {
   429  		return wait, nil
   430  	}
   431  
   432  	return wait, r.Client.Patch(request.Ctx, request.Backup, client.MergeFrom(original))
   433  }
   434  
   435  // getBackupRepo returns the backup repo specified by the backup object or the policy.
   436  // if no backup repo specified, it will return the default one.
   437  func (r *BackupReconciler) getBackupRepo(ctx context.Context,
   438  	backup *dpv1alpha1.Backup,
   439  	backupPolicy *dpv1alpha1.BackupPolicy) (*dpv1alpha1.BackupRepo, error) {
   440  	// use the specified backup repo
   441  	var repoName string
   442  	if val := backup.Labels[dataProtectionBackupRepoKey]; val != "" {
   443  		repoName = val
   444  	} else if backupPolicy.Spec.BackupRepoName != nil && *backupPolicy.Spec.BackupRepoName != "" {
   445  		repoName = *backupPolicy.Spec.BackupRepoName
   446  	}
   447  	if repoName != "" {
   448  		repo := &dpv1alpha1.BackupRepo{}
   449  		if err := r.Client.Get(ctx, client.ObjectKey{Name: repoName}, repo); err != nil {
   450  			if apierrors.IsNotFound(err) {
   451  				return nil, intctrlutil.NewNotFound("backup repo %s not found", repoName)
   452  			}
   453  			return nil, err
   454  		}
   455  		return repo, nil
   456  	}
   457  	// fallback to use the default repo
   458  	return getDefaultBackupRepo(ctx, r.Client)
   459  }
   460  
   461  func (r *BackupReconciler) handleRunningPhase(
   462  	reqCtx intctrlutil.RequestCtx,
   463  	backup *dpv1alpha1.Backup) (ctrl.Result, error) {
   464  	request, err := r.prepareBackupRequest(reqCtx, backup)
   465  	if err != nil {
   466  		return r.updateStatusIfFailed(reqCtx, backup.DeepCopy(), backup, err)
   467  	}
   468  
   469  	// there are actions not completed, continue to handle following actions
   470  	actions, err := request.BuildActions()
   471  	if err != nil {
   472  		return r.updateStatusIfFailed(reqCtx, backup, request.Backup, err)
   473  	}
   474  
   475  	actionCtx := action.Context{
   476  		Ctx:              reqCtx.Ctx,
   477  		Client:           r.Client,
   478  		Recorder:         r.Recorder,
   479  		Scheme:           r.Scheme,
   480  		RestClientConfig: r.RestConfig,
   481  	}
   482  
   483  	// check all actions status, if any action failed, update backup status to failed
   484  	// if all actions completed, update backup status to completed, otherwise,
   485  	// continue to handle following actions.
   486  	for i, act := range actions {
   487  		status, err := act.Execute(actionCtx)
   488  		if err != nil {
   489  			return r.updateStatusIfFailed(reqCtx, backup, request.Backup, err)
   490  		}
   491  		request.Status.Actions[i] = mergeActionStatus(&request.Status.Actions[i], status)
   492  
   493  		switch status.Phase {
   494  		case dpv1alpha1.ActionPhaseCompleted:
   495  			updateBackupStatusByActionStatus(&request.Status)
   496  			continue
   497  		case dpv1alpha1.ActionPhaseFailed:
   498  			return r.updateStatusIfFailed(reqCtx, backup, request.Backup,
   499  				fmt.Errorf("action %s failed, %s", act.GetName(), status.FailureReason))
   500  		case dpv1alpha1.ActionPhaseRunning:
   501  			// update status
   502  			if err = r.Client.Status().Patch(reqCtx.Ctx, request.Backup, client.MergeFrom(backup)); err != nil {
   503  				return intctrlutil.CheckedRequeueWithError(err, reqCtx.Log, "")
   504  			}
   505  			return intctrlutil.Reconciled()
   506  		}
   507  	}
   508  
   509  	// all actions completed, update backup status to completed
   510  	request.Status.Phase = dpv1alpha1.BackupPhaseCompleted
   511  	request.Status.CompletionTimestamp = &metav1.Time{Time: r.clock.Now().UTC()}
   512  	if !request.Status.StartTimestamp.IsZero() {
   513  		// round the duration to a multiple of seconds.
   514  		duration := request.Status.CompletionTimestamp.Sub(request.Status.StartTimestamp.Time).Round(time.Second)
   515  		request.Status.Duration = &metav1.Duration{Duration: duration}
   516  	}
   517  	if request.Spec.RetentionPeriod != "" {
   518  		// set expiration time
   519  		duration, err := request.Spec.RetentionPeriod.ToDuration()
   520  		if err != nil {
   521  			return r.updateStatusIfFailed(reqCtx, backup, request.Backup, fmt.Errorf("failed to parse retention period %s, %v", request.Spec.RetentionPeriod, err))
   522  		}
   523  		if duration.Seconds() > 0 {
   524  			request.Status.Expiration = &metav1.Time{
   525  				Time: request.Status.CompletionTimestamp.Add(duration),
   526  			}
   527  		}
   528  	}
   529  	r.Recorder.Event(backup, corev1.EventTypeNormal, "CreatedBackup", "Completed backup")
   530  	if err = r.Client.Status().Patch(reqCtx.Ctx, request.Backup, client.MergeFrom(backup)); err != nil {
   531  		return intctrlutil.CheckedRequeueWithError(err, reqCtx.Log, "")
   532  	}
   533  	return intctrlutil.Reconciled()
   534  }
   535  
   536  func mergeActionStatus(original, new *dpv1alpha1.ActionStatus) dpv1alpha1.ActionStatus {
   537  	as := new.DeepCopy()
   538  	if original.StartTimestamp != nil {
   539  		as.StartTimestamp = original.StartTimestamp
   540  	}
   541  	return *as
   542  }
   543  
   544  func updateBackupStatusByActionStatus(backupStatus *dpv1alpha1.BackupStatus) {
   545  	for _, act := range backupStatus.Actions {
   546  		if act.TotalSize != "" && backupStatus.TotalSize == "" {
   547  			backupStatus.TotalSize = act.TotalSize
   548  		}
   549  		if act.TimeRange != nil && backupStatus.TimeRange == nil {
   550  			backupStatus.TimeRange = act.TimeRange
   551  		}
   552  	}
   553  }
   554  
   555  // handleCompletedPhase handles the backup object in completed phase.
   556  // It will delete the reference workloads.
   557  func (r *BackupReconciler) handleCompletedPhase(
   558  	reqCtx intctrlutil.RequestCtx,
   559  	backup *dpv1alpha1.Backup) (ctrl.Result, error) {
   560  	if err := r.deleteExternalResources(reqCtx, backup); err != nil {
   561  		return intctrlutil.CheckedRequeueWithError(err, reqCtx.Log, "")
   562  	}
   563  
   564  	return intctrlutil.Reconciled()
   565  }
   566  
   567  func (r *BackupReconciler) updateStatusIfFailed(
   568  	reqCtx intctrlutil.RequestCtx,
   569  	original *dpv1alpha1.Backup,
   570  	backup *dpv1alpha1.Backup,
   571  	err error) (ctrl.Result, error) {
   572  	sendWarningEventForError(r.Recorder, backup, err)
   573  	backup.Status.Phase = dpv1alpha1.BackupPhaseFailed
   574  	backup.Status.FailureReason = err.Error()
   575  
   576  	// set expiration time for failed backup, make sure the failed backup will be
   577  	// deleted after the expiration time.
   578  	_ = setExpirationByCreationTime(backup)
   579  
   580  	if errUpdate := r.Client.Status().Patch(reqCtx.Ctx, backup, client.MergeFrom(original)); errUpdate != nil {
   581  		return intctrlutil.CheckedRequeueWithError(errUpdate, reqCtx.Log, "")
   582  	}
   583  	return intctrlutil.CheckedRequeueWithError(err, reqCtx.Log, "")
   584  }
   585  
   586  // deleteExternalJobs deletes the external jobs.
   587  func (r *BackupReconciler) deleteExternalJobs(reqCtx intctrlutil.RequestCtx, backup *dpv1alpha1.Backup) error {
   588  	jobs := &batchv1.JobList{}
   589  	if err := r.Client.List(reqCtx.Ctx, jobs,
   590  		client.InNamespace(backup.Namespace),
   591  		client.MatchingLabels(dpbackup.BuildBackupWorkloadLabels(backup))); err != nil {
   592  		return client.IgnoreNotFound(err)
   593  	}
   594  
   595  	deleteJob := func(job *batchv1.Job) error {
   596  		if err := dputils.RemoveDataProtectionFinalizer(reqCtx.Ctx, r.Client, job); err != nil {
   597  			return err
   598  		}
   599  		if !job.DeletionTimestamp.IsZero() {
   600  			return nil
   601  		}
   602  		reqCtx.Log.V(1).Info("delete job", "job", job)
   603  		if err := intctrlutil.BackgroundDeleteObject(r.Client, reqCtx.Ctx, job); err != nil {
   604  			return err
   605  		}
   606  		return nil
   607  	}
   608  
   609  	for i := range jobs.Items {
   610  		if err := deleteJob(&jobs.Items[i]); err != nil {
   611  			return err
   612  		}
   613  	}
   614  	return nil
   615  }
   616  
   617  func (r *BackupReconciler) deleteVolumeSnapshots(reqCtx intctrlutil.RequestCtx,
   618  	backup *dpv1alpha1.Backup) error {
   619  	deleter := &dpbackup.Deleter{
   620  		RequestCtx: reqCtx,
   621  		Client:     r.Client,
   622  	}
   623  	return deleter.DeleteVolumeSnapshots(backup)
   624  }
   625  
   626  // deleteExternalStatefulSet deletes the external statefulSet.
   627  func (r *BackupReconciler) deleteExternalStatefulSet(reqCtx intctrlutil.RequestCtx, backup *dpv1alpha1.Backup) error {
   628  	key := client.ObjectKey{
   629  		Namespace: backup.Namespace,
   630  		Name:      backup.Name,
   631  	}
   632  	sts := &appsv1.StatefulSet{}
   633  	if err := r.Client.Get(reqCtx.Ctx, key, sts); err != nil {
   634  		return client.IgnoreNotFound(err)
   635  	} else if !model.IsOwnerOf(backup, sts) {
   636  		return nil
   637  	}
   638  
   639  	patch := client.MergeFrom(sts.DeepCopy())
   640  	controllerutil.RemoveFinalizer(sts, dptypes.DataProtectionFinalizerName)
   641  	if err := r.Client.Patch(reqCtx.Ctx, sts, patch); err != nil {
   642  		return err
   643  	}
   644  
   645  	if !sts.DeletionTimestamp.IsZero() {
   646  		return nil
   647  	}
   648  
   649  	reqCtx.Log.V(1).Info("delete statefulSet", "statefulSet", sts)
   650  	return intctrlutil.BackgroundDeleteObject(r.Client, reqCtx.Ctx, sts)
   651  }
   652  
   653  // deleteExternalResources deletes the external workloads that execute backup.
   654  // Currently, it only supports two types of workloads: statefulSet and job.
   655  func (r *BackupReconciler) deleteExternalResources(
   656  	reqCtx intctrlutil.RequestCtx, backup *dpv1alpha1.Backup) error {
   657  	if err := r.deleteExternalStatefulSet(reqCtx, backup); err != nil {
   658  		return err
   659  	}
   660  	return r.deleteExternalJobs(reqCtx, backup)
   661  }
   662  
   663  // setConnectionPasswordAnnotation sets the encrypted password of the connection credential to the backup's annotations
   664  func (r *BackupReconciler) setConnectionPasswordAnnotation(request *dpbackup.Request) error {
   665  	encryptPassword := func() (string, error) {
   666  		target := request.BackupPolicy.Spec.Target
   667  		if target == nil || target.ConnectionCredential == nil {
   668  			return "", nil
   669  		}
   670  		secret := &corev1.Secret{}
   671  		if err := r.Client.Get(request.Ctx, client.ObjectKey{Name: target.ConnectionCredential.SecretName, Namespace: request.Namespace}, secret); err != nil {
   672  			return "", err
   673  		}
   674  		e := intctrlutil.NewEncryptor(viper.GetString(constant.CfgKeyDPEncryptionKey))
   675  		ciphertext, err := e.Encrypt(secret.Data[target.ConnectionCredential.PasswordKey])
   676  		if err != nil {
   677  			return "", err
   678  		}
   679  		return ciphertext, nil
   680  	}
   681  	backupType := dputils.GetBackupType(request.ActionSet, request.BackupMethod.SnapshotVolumes)
   682  	if backupType == dpv1alpha1.BackupTypeFull {
   683  		// save the connection credential password for cluster.
   684  		ciphertext, err := encryptPassword()
   685  		if err != nil {
   686  			return err
   687  		}
   688  		if ciphertext != "" {
   689  			request.Backup.Annotations[dptypes.ConnectionPasswordKey] = ciphertext
   690  		}
   691  	}
   692  	return nil
   693  }
   694  
   695  // getClusterObjectString gets the cluster object and convert it to string.
   696  func getClusterObjectString(cluster *appsv1alpha1.Cluster) (*string, error) {
   697  	// maintain only the cluster's spec and name/namespace.
   698  	newCluster := &appsv1alpha1.Cluster{
   699  		Spec: cluster.Spec,
   700  		ObjectMeta: metav1.ObjectMeta{
   701  			Namespace: cluster.Namespace,
   702  			Name:      cluster.Name,
   703  		},
   704  		TypeMeta: cluster.TypeMeta,
   705  	}
   706  	clusterBytes, err := json.Marshal(newCluster)
   707  	if err != nil {
   708  		return nil, err
   709  	}
   710  	clusterString := string(clusterBytes)
   711  	return &clusterString, nil
   712  }
   713  
   714  // setClusterSnapshotAnnotation sets the snapshot of cluster to the backup's annotations.
   715  func setClusterSnapshotAnnotation(backup *dpv1alpha1.Backup, cluster *appsv1alpha1.Cluster) error {
   716  	clusterString, err := getClusterObjectString(cluster)
   717  	if err != nil {
   718  		return err
   719  	}
   720  	if clusterString == nil {
   721  		return nil
   722  	}
   723  	if backup.Annotations == nil {
   724  		backup.Annotations = map[string]string{}
   725  	}
   726  	backup.Annotations[constant.ClusterSnapshotAnnotationKey] = *clusterString
   727  	return nil
   728  }
   729  
   730  func setExpirationByCreationTime(backup *dpv1alpha1.Backup) error {
   731  	// if expiration is already set, do not update it.
   732  	if backup.Status.Expiration != nil {
   733  		return nil
   734  	}
   735  
   736  	duration, err := backup.Spec.RetentionPeriod.ToDuration()
   737  	if err != nil {
   738  		return fmt.Errorf("failed to parse retention period %s, %v", backup.Spec.RetentionPeriod, err)
   739  	}
   740  
   741  	// if duration is zero, the backup will be kept forever.
   742  	// Do not set expiration time for it.
   743  	if duration.Seconds() == 0 {
   744  		return nil
   745  	}
   746  
   747  	var expiration *metav1.Time
   748  	if backup.Status.StartTimestamp != nil {
   749  		expiration = &metav1.Time{
   750  			Time: backup.Status.StartTimestamp.Add(duration),
   751  		}
   752  	} else {
   753  		expiration = &metav1.Time{
   754  			Time: backup.CreationTimestamp.Add(duration),
   755  		}
   756  	}
   757  	backup.Status.Expiration = expiration
   758  	return nil
   759  }