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 }