From 13c1b4220a4f69c5edc7e4f7cbf3413d823d27c4 Mon Sep 17 00:00:00 2001 From: Jean-Marc ANDRE Date: Sun, 13 Dec 2020 20:44:42 +0100 Subject: [PATCH] Keep last backup policy. Delete the others --- src/go.mod | 2 +- src/pkg/backup/root.go | 14 ++ src/pkg/backupsession/root.go | 14 ++ .../controllers/backupsession_controller.go | 124 +++++++++++++++++- 4 files changed, 152 insertions(+), 2 deletions(-) diff --git a/src/go.mod b/src/go.mod index d56f4a3..de85e67 100644 --- a/src/go.mod +++ b/src/go.mod @@ -3,7 +3,7 @@ module github.com/desmo999r/formolcli go 1.14 require ( - github.com/desmo999r/formol v0.6.0 + github.com/desmo999r/formol v0.6.2-0.20201212121852-aa511e03435a github.com/go-logr/logr v0.1.0 github.com/go-logr/zapr v0.1.0 github.com/mitchellh/go-homedir v1.1.0 diff --git a/src/pkg/backup/root.go b/src/pkg/backup/root.go index 3043730..6f32955 100644 --- a/src/pkg/backup/root.go +++ b/src/pkg/backup/root.go @@ -109,3 +109,17 @@ func BackupDeployment(prefix string, paths []string, c chan []byte) (error) { return nil } + +func DeleteSnapshot(prefix string, snapshotId string) error { + log := logger.WithValues("delete-snapshot", snapshotId) + newrepo := repository + if prefix != "" { + newrepo = repository + "/" + prefix + } + cmd := exec.Command(resticExec, "forget", "-r", newrepo, snapshotId) + if err := cmd.Run(); err != nil { + log.Error(err, "unable to delete the snapshot") + return err + } + return nil +} diff --git a/src/pkg/backupsession/root.go b/src/pkg/backupsession/root.go index 667a97c..51b953c 100644 --- a/src/pkg/backupsession/root.go +++ b/src/pkg/backupsession/root.go @@ -49,6 +49,20 @@ func init() { } func DeleteBackupSession(name string, namespace string) error { + log := logger.WithName("CreateBackupSession") + log.V(0).Info("CreateBackupSession called") + backupSession := &formolv1alpha1.BackupSession{} + if err := cl.Get(context.TODO(), client.ObjectKey{ + Namespace: namespace, + Name: name, + }, backupSession); err != nil { + log.Error(err, "unable to get backupsession", "backupsession", name) + return err + } + if err := cl.Delete(context.TODO(), backupSession); err != nil { + log.Error(err, "unable to delete backupsession", "backupsession", name) + return err + } return nil } diff --git a/src/pkg/controllers/backupsession_controller.go b/src/pkg/controllers/backupsession_controller.go index 6c741da..0ce1bbc 100644 --- a/src/pkg/controllers/backupsession_controller.go +++ b/src/pkg/controllers/backupsession_controller.go @@ -2,26 +2,31 @@ package controllers import ( "time" + "sort" "encoding/json" "context" "github.com/go-logr/logr" "k8s.io/apimachinery/pkg/runtime" ctrl "sigs.k8s.io/controller-runtime" "sigs.k8s.io/controller-runtime/pkg/client" + "sigs.k8s.io/controller-runtime/pkg/predicate" "sigs.k8s.io/controller-runtime/pkg/log/zap" "os" "path/filepath" formolv1alpha1 "github.com/desmo999r/formol/api/v1alpha1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/fields" "k8s.io/client-go/kubernetes" "k8s.io/client-go/rest" "k8s.io/client-go/tools/clientcmd" "github.com/desmo999r/formolcli/pkg/backup" + formolutils "github.com/desmo999r/formol/pkg/utils" ) var ( deploymentName = "" + sessionState = ".metadata.state" ) func init() { @@ -85,6 +90,31 @@ func (r *BackupSessionReconciler) Reconcile(req ctrl.Request) (ctrl.Result, erro log.Error(err, "unable to get backupsession") return ctrl.Result{}, client.IgnoreNotFound(err) } + finalizerName := "finalizer.backupsession.formol.desmojim.fr" + + if backupSession.ObjectMeta.DeletionTimestamp.IsZero() { + if !formolutils.ContainsString(backupSession.ObjectMeta.Finalizers, finalizerName) { + backupSession.ObjectMeta.Finalizers = append(backupSession.ObjectMeta.Finalizers, finalizerName) + if err := r.Update(context.Background(), backupSession); err != nil { + log.Error(err, "unable to append finalizer") + return ctrl.Result{}, err + } + } + } else { + log.V(0).Info("backupsession being deleted", "backupsession", backupSession.Name) + if formolutils.ContainsString(backupSession.ObjectMeta.Finalizers, finalizerName) { + if err := r.deleteExternalResources(backupSession); err != nil { + return ctrl.Result{}, err + } + } + backupSession.ObjectMeta.Finalizers = formolutils.RemoveString(backupSession.ObjectMeta.Finalizers, finalizerName) + if err := r.Update(context.Background(), backupSession); err != nil { + log.Error(err, "unable to remove finalizer") + return ctrl.Result{}, err + } + // We have been deleted. Return here + return ctrl.Result{}, nil + } log.V(1).Info("backupSession.Namespace", "namespace", backupSession.Namespace) log.V(1).Info("backupSession.Spec.Ref.Name", "name", backupSession.Spec.Ref.Name) @@ -101,7 +131,7 @@ func (r *BackupSessionReconciler) Reconcile(req ctrl.Request) (ctrl.Result, erro return ctrl.Result{}, client.IgnoreNotFound(err) } - log.V(1).Info("Found BackupConfiguration", "BackupConfiguration", backupConf) + log.V(1).Info("Found BackupConfiguration", "BackupConfiguration", backupConf.Name) // Found the BackupConfiguration. if backupConf.Spec.Target.Name != deploymentName { @@ -114,6 +144,7 @@ func (r *BackupSessionReconciler) Reconcile(req ctrl.Request) (ctrl.Result, erro log.Error(err, "unable to update status", "backupsession", backupSession) return ctrl.Result{}, err } + // Preparing for backup c := make(chan []byte) go func(){ @@ -138,6 +169,7 @@ func (r *BackupSessionReconciler) Reconcile(req ctrl.Request) (ctrl.Result, erro log.Error(err, "unable to update status") } }() + // do the backup switch backupConf.Spec.Target.Kind { case "Deployment": backupSession.Status.StartTime = &metav1.Time {Time: time.Now()} @@ -147,12 +179,102 @@ func (r *BackupSessionReconciler) Reconcile(req ctrl.Request) (ctrl.Result, erro } result = formolv1alpha1.Success } + + // cleanup old backups + backupSessionList := &formolv1alpha1.BackupSessionList{} + if err := r.List(ctx, backupSessionList, client.InNamespace(backupConf.Namespace), client.MatchingFieldsSelector{fields.SelectorFromSet(fields.Set{sessionState: "Success"})}); err != nil { + return ctrl.Result{}, nil + } + if len(backupSessionList.Items) < 2 { + // Not enough backupSession to proceed + return ctrl.Result{}, nil + } + + 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 + if lastBackups.Counter > 0 { + log.V(1).Info("Keep backup", "last", session.Status.StartTime) + lastBackups.Counter-- + deleteSession = false + } + if dailyBackups.Counter > 0 { + if session.Status.StartTime.Time.YearDay() != dailyBackups.Last.YearDay() { + log.V(1).Info("Keep backup", "daily", session.Status.StartTime) + dailyBackups.Counter-- + dailyBackups.Last = session.Status.StartTime.Time + deleteSession = false + } + } + if weeklyBackups.Counter > 0 { + if session.Status.StartTime.Time.Weekday().String() == "Sunday" && session.Status.StartTime.Time.YearDay() != weeklyBackups.Last.YearDay() { + log.V(1).Info("Keep backup", "weekly", session.Status.StartTime) + weeklyBackups.Counter-- + weeklyBackups.Last = session.Status.StartTime.Time + deleteSession = false + } + } + if monthlyBackups.Counter > 0 { + if session.Status.StartTime.Time.Day() == 1 && session.Status.StartTime.Time.Month() != monthlyBackups.Last.Month() { + log.V(1).Info("Keep backup", "monthly", session.Status.StartTime) + monthlyBackups.Counter-- + monthlyBackups.Last = session.Status.StartTime.Time + deleteSession = false + } + } + if yearlyBackups.Counter > 0 { + if session.Status.StartTime.Time.YearDay() == 1 && session.Status.StartTime.Time.Year() != yearlyBackups.Last.Year() { + log.V(1).Info("Keep backup", "yearly", session.Status.StartTime) + yearlyBackups.Counter-- + yearlyBackups.Last = session.Status.StartTime.Time + deleteSession = false + } + } + if deleteSession { + log.V(1).Info("Delete session", "delete", session.Status.StartTime) + if err := r.Delete(ctx, &session); err != nil { + log.Error(err, "unable to delete backupsession", "session", session.Name) + // we don't return anything, we keep going + } + } + } return ctrl.Result{}, nil } +func (r *BackupSessionReconciler) deleteExternalResources(backupSession *formolv1alpha1.BackupSession) error { + if err := backup.DeleteSnapshot("", backupSession.Status.SnapshotId); err != nil { + return err + } + return nil +} + func (r *BackupSessionReconciler) SetupWithManager(mgr ctrl.Manager) error { + if err := mgr.GetFieldIndexer().IndexField(context.TODO(), &formolv1alpha1.BackupSession{}, sessionState, func(rawObj runtime.Object) []string { + session := rawObj.(*formolv1alpha1.BackupSession) + return []string{string(session.Status.BackupSessionState)} + }); err != nil { + return err + } return ctrl.NewControllerManagedBy(mgr). For(&formolv1alpha1.BackupSession{}). + WithEventFilter(predicate.GenerationChangedPredicate{}). // Don't reconcile when status gets updated Complete(r) }