github.com/1aal/kubeblocks@v0.0.0-20231107070852-e1c03e598921/pkg/dataprotection/backup/deleter.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 backup
    21  
    22  import (
    23  	"fmt"
    24  	"strings"
    25  
    26  	vsv1 "github.com/kubernetes-csi/external-snapshotter/client/v6/apis/volumesnapshot/v1"
    27  	batchv1 "k8s.io/api/batch/v1"
    28  	corev1 "k8s.io/api/core/v1"
    29  	apierrors "k8s.io/apimachinery/pkg/api/errors"
    30  	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
    31  	"k8s.io/apimachinery/pkg/runtime"
    32  	"k8s.io/apimachinery/pkg/types"
    33  	"sigs.k8s.io/controller-runtime/pkg/client"
    34  	"sigs.k8s.io/controller-runtime/pkg/controller/controllerutil"
    35  
    36  	dpv1alpha1 "github.com/1aal/kubeblocks/apis/dataprotection/v1alpha1"
    37  	"github.com/1aal/kubeblocks/pkg/constant"
    38  	ctrlutil "github.com/1aal/kubeblocks/pkg/controllerutil"
    39  	dptypes "github.com/1aal/kubeblocks/pkg/dataprotection/types"
    40  	"github.com/1aal/kubeblocks/pkg/dataprotection/utils"
    41  	"github.com/1aal/kubeblocks/pkg/dataprotection/utils/boolptr"
    42  	viper "github.com/1aal/kubeblocks/pkg/viperx"
    43  )
    44  
    45  const (
    46  	deleteBackupFilesJobNamePrefix = "delete-"
    47  )
    48  
    49  type DeletionStatus string
    50  
    51  const (
    52  	DeletionStatusDeleting  DeletionStatus = "Deleting"
    53  	DeletionStatusFailed    DeletionStatus = "Failed"
    54  	DeletionStatusSucceeded DeletionStatus = "Succeeded"
    55  	DeletionStatusUnknown   DeletionStatus = "Unknown"
    56  )
    57  
    58  type Deleter struct {
    59  	ctrlutil.RequestCtx
    60  	Client client.Client
    61  	Scheme *runtime.Scheme
    62  }
    63  
    64  // DeleteBackupFiles builds a job to delete backup files, and returns the deletion status.
    65  // If the deletion job exists, it will check the job status and return the corresponding
    66  // deletion status.
    67  func (d *Deleter) DeleteBackupFiles(backup *dpv1alpha1.Backup) (DeletionStatus, error) {
    68  	backupMethod := backup.Status.BackupMethod
    69  	if backupMethod != nil && boolptr.IsSetToTrue(backupMethod.SnapshotVolumes) {
    70  		// if the backup is volume snapshot, ignore to delete files
    71  		return DeletionStatusSucceeded, nil
    72  	}
    73  	jobKey := BuildDeleteBackupFilesJobKey(backup)
    74  	job := &batchv1.Job{}
    75  	exists, err := ctrlutil.CheckResourceExists(d.Ctx, d.Client, jobKey, job)
    76  	if err != nil {
    77  		return DeletionStatusUnknown, err
    78  	}
    79  
    80  	// if deletion job exists, check its status
    81  	if exists {
    82  		_, finishedType, msg := utils.IsJobFinished(job)
    83  		switch finishedType {
    84  		case batchv1.JobComplete:
    85  			return DeletionStatusSucceeded, nil
    86  		case batchv1.JobFailed:
    87  			return DeletionStatusFailed,
    88  				fmt.Errorf("deletion backup files job \"%s\" failed, you can delete it to re-delete the backup files, %s", job.Name, msg)
    89  		}
    90  		return DeletionStatusDeleting, nil
    91  	}
    92  
    93  	var backupRepo *dpv1alpha1.BackupRepo
    94  	if backup.Status.BackupRepoName != "" {
    95  		backupRepo = &dpv1alpha1.BackupRepo{}
    96  		if err = d.Client.Get(d.Ctx, client.ObjectKey{Name: backup.Status.BackupRepoName}, backupRepo); err != nil {
    97  			if apierrors.IsNotFound(err) {
    98  				return DeletionStatusSucceeded, nil
    99  			}
   100  			return DeletionStatusUnknown, err
   101  		}
   102  	}
   103  
   104  	// if backupRepo is nil (likely because it's a legacy backup object), check the backup PVC
   105  	var legacyPVCName string
   106  	if backupRepo == nil {
   107  		legacyPVCName = backup.Status.PersistentVolumeClaimName
   108  		if legacyPVCName == "" {
   109  			d.Log.Info("skip deleting backup files because PersistentVolumeClaimName is empty",
   110  				"backup", backup.Name)
   111  			return DeletionStatusSucceeded, nil
   112  		}
   113  
   114  		// check if the backup PVC exists, if not, skip to delete backup files
   115  		pvcKey := client.ObjectKey{Namespace: backup.Namespace, Name: legacyPVCName}
   116  		if err = d.Client.Get(d.Ctx, pvcKey, &corev1.PersistentVolumeClaim{}); err != nil {
   117  			if apierrors.IsNotFound(err) {
   118  				return DeletionStatusSucceeded, nil
   119  			}
   120  			return DeletionStatusUnknown, err
   121  		}
   122  	}
   123  
   124  	backupFilePath := backup.Status.Path
   125  	if backupFilePath == "" || !strings.Contains(backupFilePath, backup.Name) {
   126  		// For compatibility: the FilePath field is changing from time to time,
   127  		// and it may not contain the backup name as a path component if the Backup object
   128  		// was created in a previous version. In this case, it's dangerous to execute
   129  		// the deletion command. For example, files belongs to other Backups can be deleted as well.
   130  		d.Log.Info("skip deleting backup files because backup file path is invalid",
   131  			"backupFilePath", backupFilePath, "backup", backup.Name)
   132  		return DeletionStatusSucceeded, nil
   133  	}
   134  	return DeletionStatusDeleting, d.createDeleteBackupFilesJob(jobKey, backup, backupRepo, legacyPVCName, backup.Status.Path)
   135  }
   136  
   137  func (d *Deleter) createDeleteBackupFilesJob(
   138  	jobKey types.NamespacedName,
   139  	backup *dpv1alpha1.Backup,
   140  	backupRepo *dpv1alpha1.BackupRepo,
   141  	legacyPVCName string,
   142  	backupFilePath string) error {
   143  	// make sure the path has a leading slash
   144  	if !strings.HasPrefix(backupFilePath, "/") {
   145  		backupFilePath = "/" + backupFilePath
   146  	}
   147  
   148  	// this script first deletes the directory where the backup is located (including files
   149  	// in the directory), and then traverses up the path level by level to clean up empty directories.
   150  	deleteScript := fmt.Sprintf(`
   151  set -x
   152  export PATH="$PATH:$%s";
   153  targetPath="%s";
   154  
   155  echo "removing backup files in ${targetPath}";
   156  datasafed rm -r "${targetPath}";
   157  
   158  curr="${targetPath}";
   159  while true; do
   160  	parent=$(dirname "${curr}");
   161  	if [ "${parent}" == "/" ]; then
   162  		echo "reach to root, done";
   163  		break;
   164  	fi;
   165  	result=$(datasafed list "${parent}");
   166  	if [ -z "$result" ]; then
   167  		echo "${parent} is empty, removing it...";
   168  		datasafed rmdir "${parent}";
   169  	else
   170  		echo "${parent} is not empty, done";
   171  		break;
   172  	fi;
   173  	curr="${parent}";
   174  done
   175  	`, dptypes.DPDatasafedBinPath, backupFilePath)
   176  
   177  	runAsUser := int64(0)
   178  	container := corev1.Container{
   179  		Name:            backup.Name,
   180  		Command:         []string{"sh", "-c"},
   181  		Args:            []string{deleteScript},
   182  		Image:           viper.GetString(constant.KBToolsImage),
   183  		ImagePullPolicy: corev1.PullPolicy(viper.GetString(constant.KBImagePullPolicy)),
   184  		SecurityContext: &corev1.SecurityContext{
   185  			AllowPrivilegeEscalation: boolptr.False(),
   186  			RunAsUser:                &runAsUser,
   187  		},
   188  	}
   189  	ctrlutil.InjectZeroResourcesLimitsIfEmpty(&container)
   190  
   191  	// build pod
   192  	podSpec := corev1.PodSpec{
   193  		Containers:    []corev1.Container{container},
   194  		RestartPolicy: corev1.RestartPolicyNever,
   195  	}
   196  	if err := utils.AddTolerations(&podSpec); err != nil {
   197  		return err
   198  	}
   199  	if backupRepo != nil {
   200  		utils.InjectDatasafed(&podSpec, backupRepo, RepoVolumeMountPath, backupFilePath)
   201  	} else {
   202  		utils.InjectDatasafedWithPVC(&podSpec, legacyPVCName, RepoVolumeMountPath, backupFilePath)
   203  	}
   204  
   205  	// build job
   206  	job := &batchv1.Job{
   207  		ObjectMeta: metav1.ObjectMeta{
   208  			Namespace: jobKey.Namespace,
   209  			Name:      jobKey.Name,
   210  		},
   211  		Spec: batchv1.JobSpec{
   212  			Template: corev1.PodTemplateSpec{
   213  				ObjectMeta: metav1.ObjectMeta{
   214  					Namespace: jobKey.Namespace,
   215  					Name:      jobKey.Name,
   216  				},
   217  				Spec: podSpec,
   218  			},
   219  			BackoffLimit: &dptypes.DefaultBackOffLimit,
   220  		},
   221  	}
   222  	if err := utils.SetControllerReference(backup, job, d.Scheme); err != nil {
   223  		return err
   224  	}
   225  	d.Log.V(1).Info("create a job to delete backup files", "job", job)
   226  	return client.IgnoreAlreadyExists(d.Client.Create(d.Ctx, job))
   227  }
   228  
   229  func (d *Deleter) DeleteVolumeSnapshots(backup *dpv1alpha1.Backup) error {
   230  	// initialize volume snapshot client that is compatible with both v1beta1 and v1
   231  	vsCli := &ctrlutil.VolumeSnapshotCompatClient{
   232  		Client: d.Client,
   233  		Ctx:    d.Ctx,
   234  	}
   235  
   236  	snaps := &vsv1.VolumeSnapshotList{}
   237  	if err := vsCli.List(snaps, client.InNamespace(backup.Namespace),
   238  		client.MatchingLabels(map[string]string{
   239  			dptypes.BackupNameLabelKey: backup.Name,
   240  		})); err != nil {
   241  		return client.IgnoreNotFound(err)
   242  	}
   243  
   244  	deleteVolumeSnapshot := func(vs *vsv1.VolumeSnapshot) error {
   245  		if controllerutil.ContainsFinalizer(vs, dptypes.DataProtectionFinalizerName) {
   246  			patch := vs.DeepCopy()
   247  			controllerutil.RemoveFinalizer(vs, dptypes.DataProtectionFinalizerName)
   248  			if err := vsCli.Patch(vs, patch); err != nil {
   249  				return err
   250  			}
   251  		}
   252  		if !vs.DeletionTimestamp.IsZero() {
   253  			return nil
   254  		}
   255  		d.Log.V(1).Info("delete volume snapshot", "volume snapshot", vs)
   256  		if err := vsCli.Delete(vs); err != nil {
   257  			return err
   258  		}
   259  		return nil
   260  	}
   261  
   262  	for i := range snaps.Items {
   263  		if err := deleteVolumeSnapshot(&snaps.Items[i]); err != nil {
   264  			return err
   265  		}
   266  	}
   267  	return nil
   268  }
   269  
   270  func BuildDeleteBackupFilesJobKey(backup *dpv1alpha1.Backup) client.ObjectKey {
   271  	jobName := fmt.Sprintf("%s-%s%s", backup.UID[:8], deleteBackupFilesJobNamePrefix, backup.Name)
   272  	if len(jobName) > 63 {
   273  		jobName = jobName[:63]
   274  	}
   275  	return client.ObjectKey{Namespace: backup.Namespace, Name: jobName}
   276  }