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 }