diff --git a/controllers/backupsession_controller.go b/controllers/backupsession_controller.go index 6bd3210..f2460cc 100644 --- a/controllers/backupsession_controller.go +++ b/controllers/backupsession_controller.go @@ -57,8 +57,21 @@ func (r *BackupSessionReconciler) Reconcile(ctx context.Context, req ctrl.Reques } return ctrl.Result{}, err } + // we might need the BackupConfiguration is the BackupSession + // is being deleted + backupConf := formolv1alpha1.BackupConfiguration{} + if err := r.Get(ctx, client.ObjectKey{ + Namespace: backupSession.Spec.Ref.Namespace, + Name: backupSession.Spec.Ref.Name, + }, &backupConf); err != nil { + r.Log.Error(err, "unable to get BackupConfiguration") + return ctrl.Result{}, err + } if !backupSession.ObjectMeta.DeletionTimestamp.IsZero() { r.Log.V(0).Info("BackupSession is being deleted") + if err := r.deleteSnapshots(backupSession, backupConf); err != nil { + r.Log.Error(err, "unable to delete the BackupSession snapshots") + } if controllerutil.ContainsFinalizer(&backupSession, finalizerName) { controllerutil.RemoveFinalizer(&backupSession, finalizerName) err := r.Update(ctx, &backupSession) @@ -76,14 +89,6 @@ func (r *BackupSessionReconciler) Reconcile(ctx context.Context, req ctrl.Reques } return ctrl.Result{}, err } - backupConf := formolv1alpha1.BackupConfiguration{} - if err := r.Get(ctx, client.ObjectKey{ - Namespace: backupSession.Spec.Ref.Namespace, - Name: backupSession.Spec.Ref.Name, - }, &backupConf); err != nil { - r.Log.Error(err, "unable to get BackupConfiguration") - return ctrl.Result{}, err - } var newSessionState formolv1alpha1.SessionState switch backupSession.Status.SessionState { @@ -116,6 +121,7 @@ func (r *BackupSessionReconciler) Reconcile(ctx context.Context, req ctrl.Reques newSessionState = formolv1alpha1.Success } case formolv1alpha1.Success: + r.cleanupSessions(backupConf) r.Log.V(0).Info("Backup was a success") case formolv1alpha1.Failure: diff --git a/controllers/backupsession_controller_helper.go b/controllers/backupsession_controller_helper.go new file mode 100644 index 0000000..052743c --- /dev/null +++ b/controllers/backupsession_controller_helper.go @@ -0,0 +1,146 @@ +package controllers + +import ( + formolv1alpha1 "github.com/desmo999r/formol/api/v1alpha1" + batchv1 "k8s.io/api/batch/v1" + corev1 "k8s.io/api/core/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/fields" + "sigs.k8s.io/controller-runtime/pkg/client" + "sort" + "strings" + "time" +) + +const ( + JOBTTL int32 = 7200 +) + +func (r *BackupSessionReconciler) cleanupSessions(backupConf formolv1alpha1.BackupConfiguration) { + backupSessionList := formolv1alpha1.BackupSessionList{} + if err := r.List(r.Context, &backupSessionList, client.InNamespace(backupConf.Namespace), client.MatchingFieldsSelector{Selector: fields.SelectorFromSet(fields.Set{sessionState: string(formolv1alpha1.Success)})}); err != nil { + r.Log.Error(err, "unable to get backupsessionlist") + return + } + if len(backupSessionList.Items) < 2 { + // Not enough backupSession to proceed + r.Log.V(1).Info("Not enough successful backup jobs") + return + } + + sort.Slice(backupSessionList.Items, func(i, j int) bool { + return backupSessionList.Items[i].Status.StartTime.Time.Unix() > backupSessionList.Items[j].Status.StartTime.Time.Unix() + }) + + type KeepBackup struct { + Counter int32 + Last time.Time + } + + var lastBackups, dailyBackups, weeklyBackups, monthlyBackups, yearlyBackups KeepBackup + lastBackups.Counter = backupConf.Spec.Keep.Last + dailyBackups.Counter = backupConf.Spec.Keep.Daily + weeklyBackups.Counter = backupConf.Spec.Keep.Weekly + monthlyBackups.Counter = backupConf.Spec.Keep.Monthly + yearlyBackups.Counter = backupConf.Spec.Keep.Yearly + for _, session := range backupSessionList.Items { + if session.Spec.Ref.Name != backupConf.Name { + continue + } + deleteSession := true + keep := []string{} + if lastBackups.Counter > 0 { + r.Log.V(1).Info("Keep backup", "last", session.Status.StartTime) + lastBackups.Counter-- + keep = append(keep, "last") + deleteSession = false + } + if dailyBackups.Counter > 0 { + if session.Status.StartTime.Time.YearDay() != dailyBackups.Last.YearDay() { + r.Log.V(1).Info("Keep backup", "daily", session.Status.StartTime) + dailyBackups.Counter-- + dailyBackups.Last = session.Status.StartTime.Time + keep = append(keep, "daily") + deleteSession = false + } + } + if weeklyBackups.Counter > 0 { + if session.Status.StartTime.Time.Weekday().String() == "Sunday" && session.Status.StartTime.Time.YearDay() != weeklyBackups.Last.YearDay() { + r.Log.V(1).Info("Keep backup", "weekly", session.Status.StartTime) + weeklyBackups.Counter-- + weeklyBackups.Last = session.Status.StartTime.Time + keep = append(keep, "weekly") + deleteSession = false + } + } + if monthlyBackups.Counter > 0 { + if session.Status.StartTime.Time.Day() == 1 && session.Status.StartTime.Time.Month() != monthlyBackups.Last.Month() { + r.Log.V(1).Info("Keep backup", "monthly", session.Status.StartTime) + monthlyBackups.Counter-- + monthlyBackups.Last = session.Status.StartTime.Time + keep = append(keep, "monthly") + deleteSession = false + } + } + if yearlyBackups.Counter > 0 { + if session.Status.StartTime.Time.YearDay() == 1 && session.Status.StartTime.Time.Year() != yearlyBackups.Last.Year() { + r.Log.V(1).Info("Keep backup", "yearly", session.Status.StartTime) + yearlyBackups.Counter-- + yearlyBackups.Last = session.Status.StartTime.Time + keep = append(keep, "yearly") + deleteSession = false + } + } + if deleteSession { + r.Log.V(1).Info("Delete session", "delete", session.Status.StartTime) + if err := r.Delete(r.Context, &session); err != nil { + r.Log.Error(err, "unable to delete backupsession", "session", session.Name) + // we don't return anything, we keep going + } + } else { + session.Status.Keep = strings.Join(keep, ",") // + " " + time.Now().Format("2006 Jan 02 15:04:05 -0700 MST") + if err := r.Status().Update(r.Context, &session); err != nil { + r.Log.Error(err, "unable to update session status", "session", session) + } + } + } +} + +func (r *BackupSessionReconciler) deleteSnapshots(backupSession formolv1alpha1.BackupSession, backupConf formolv1alpha1.BackupConfiguration) error { + snapshots := []corev1.Container{} + for _, target := range backupSession.Status.Targets { + if target.SnapshotId != "" { + snapshots = append(snapshots, corev1.Container{ + Name: target.TargetName, + Image: backupConf.Spec.Image, + Args: []string{"snapshot", "delete", "--namespace", backupConf.Namespace, "--name", backupConf.Name, "--snapshot-id", target.SnapshotId}, + }) + } + } + if len(snapshots) > 0 { + job := batchv1.Job{ + ObjectMeta: metav1.ObjectMeta{ + GenerateName: "delete-" + backupSession.Name + "-", + Namespace: backupSession.Namespace, + }, + Spec: batchv1.JobSpec{ + TTLSecondsAfterFinished: func() *int32 { ttl := JOBTTL; return &ttl }(), + Template: corev1.PodTemplateSpec{ + Spec: corev1.PodSpec{ + // The snapshot deletions have to be sequential + // otherwise the repository will be locked by restic and it won't work. + InitContainers: snapshots[1:], + Containers: []corev1.Container{snapshots[0]}, + RestartPolicy: corev1.RestartPolicyOnFailure, + }, + }, + }, + } + r.Log.V(0).Info("creating a job to delete the BackupSession restic snapshots", "backupSession", backupSession) + if err := r.Create(r.Context, &job); err != nil { + r.Log.Error(err, "unable to create the job") + return err + } + } + return nil +} diff --git a/test/02-backupconf.yaml b/test/02-backupconf.yaml index 8d5150c..90965b1 100644 --- a/test/02-backupconf.yaml +++ b/test/02-backupconf.yaml @@ -104,7 +104,7 @@ spec: repository: repo-minio schedule: "15 * * * *" keep: - last: 5 + last: 2 daily: 2 weekly: 2 monthly: 6