diff --git a/api/v1alpha1/backupsession_types.go b/api/v1alpha1/backupsession_types.go index 866b94e..8e6d2f1 100644 --- a/api/v1alpha1/backupsession_types.go +++ b/api/v1alpha1/backupsession_types.go @@ -17,6 +17,7 @@ limitations under the License. package v1alpha1 import ( + corev1 "k8s.io/api/core/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" ) @@ -25,7 +26,7 @@ import ( // BackupSessionSpec defines the desired state of BackupSession type BackupSessionSpec struct { - Ref string `json:"ref"` + Ref corev1.ObjectReference `json:"ref"` } // BackupSessionStatus defines the observed state of BackupSession @@ -45,7 +46,7 @@ type BackupSessionStatus struct { // +kubebuilder:object:root=true // +kubebuilder:resource:shortName="bs" // +kubebuilder:subresource:status -// +kubebuilder:printcolumn:name="Ref",type=string,JSONPath=`.spec.ref` +// +kubebuilder:printcolumn:name="Ref",type=string,JSONPath=`.spec.ref.name` // +kubebuilder:printcolumn:name="State",type=string,JSONPath=`.status.state` // +kubebuilder:printcolumn:name="Started",type=string,format=date-time,JSONPath=`.status.startTime` // +kubebuilder:printcolumn:name="Keep",type=string,JSONPath=`.status.keep` diff --git a/api/v1alpha1/common.go b/api/v1alpha1/common.go index 87fe36e..dc49db0 100644 --- a/api/v1alpha1/common.go +++ b/api/v1alpha1/common.go @@ -10,6 +10,7 @@ const ( New SessionState = "New" Init SessionState = "Initializing" Running SessionState = "Running" + Waiting SessionState = "Waiting" Finalize SessionState = "Finalizing" Success SessionState = "Success" Failure SessionState = "Failure" diff --git a/api/v1alpha1/restoresession_types.go b/api/v1alpha1/restoresession_types.go index 6cbbd31..00aa941 100644 --- a/api/v1alpha1/restoresession_types.go +++ b/api/v1alpha1/restoresession_types.go @@ -17,26 +17,30 @@ limitations under the License. package v1alpha1 import ( + corev1 "k8s.io/api/core/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" //"k8s.io/apimachinery/pkg/types" ) -// EDIT THIS FILE! THIS IS SCAFFOLDING FOR YOU TO OWN! -// NOTE: json tags are required. Any new fields you add must have json tags for the fields to be serialized. +type BackupSessionRef struct { + // +optional + Ref corev1.ObjectReference `json:"ref,omitempty"` + // +optional + Spec BackupSessionSpec `json:"spec,omitempty"` + // +optional + Status BackupSessionStatus `json:"status,omitempty"` +} // RestoreSessionSpec defines the desired state of RestoreSession type RestoreSessionSpec struct { - // INSERT ADDITIONAL SPEC FIELDS - desired state of cluster - // Important: Run "make" to regenerate code after modifying this file - - Ref string `json:"backupSessionRef"` + BackupSessionRef `json:"backupSession"` + //Ref string `json:"backupSessionRef"` + // +optional + //Targets []TargetStatus `json:"target,omitempty"` } // RestoreSessionStatus defines the observed state of RestoreSession type RestoreSessionStatus struct { - // INSERT ADDITIONAL STATUS FIELD - define observed state of cluster - // Important: Run "make" to regenerate code after modifying this file - ObservedGeneration int64 `json:"observedGeneration,omitempty"` // +optional SessionState `json:"state,omitempty"` // +optional diff --git a/api/v1alpha1/zz_generated.deepcopy.go b/api/v1alpha1/zz_generated.deepcopy.go index 37e1b26..743bb90 100644 --- a/api/v1alpha1/zz_generated.deepcopy.go +++ b/api/v1alpha1/zz_generated.deepcopy.go @@ -207,9 +207,28 @@ func (in *BackupSessionList) DeepCopyObject() runtime.Object { return nil } +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *BackupSessionRef) DeepCopyInto(out *BackupSessionRef) { + *out = *in + out.Ref = in.Ref + out.Spec = in.Spec + in.Status.DeepCopyInto(&out.Status) +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new BackupSessionRef. +func (in *BackupSessionRef) DeepCopy() *BackupSessionRef { + if in == nil { + return nil + } + out := new(BackupSessionRef) + in.DeepCopyInto(out) + return out +} + // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *BackupSessionSpec) DeepCopyInto(out *BackupSessionSpec) { *out = *in + out.Ref = in.Ref } // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new BackupSessionSpec. @@ -436,7 +455,7 @@ func (in *RestoreSession) DeepCopyInto(out *RestoreSession) { *out = *in out.TypeMeta = in.TypeMeta in.ObjectMeta.DeepCopyInto(&out.ObjectMeta) - out.Spec = in.Spec + in.Spec.DeepCopyInto(&out.Spec) in.Status.DeepCopyInto(&out.Status) } @@ -493,6 +512,7 @@ func (in *RestoreSessionList) DeepCopyObject() runtime.Object { // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *RestoreSessionSpec) DeepCopyInto(out *RestoreSessionSpec) { *out = *in + in.BackupSessionRef.DeepCopyInto(&out.BackupSessionRef) } // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new RestoreSessionSpec. diff --git a/controllers/backupsession_controller.go b/controllers/backupsession_controller.go index 6f28f86..94e39a1 100644 --- a/controllers/backupsession_controller.go +++ b/controllers/backupsession_controller.go @@ -67,7 +67,7 @@ func (r *BackupSessionReconciler) Reconcile(req ctrl.Request) (ctrl.Result, erro backupConf := &formolv1alpha1.BackupConfiguration{} if err := r.Get(ctx, client.ObjectKey{ Namespace: backupSession.Namespace, - Name: backupSession.Spec.Ref, + Name: backupSession.Spec.Ref.Name, }, backupConf); err != nil { log.Error(err, "unable to get backupConfiguration") return ctrl.Result{}, client.IgnoreNotFound(err) @@ -281,7 +281,7 @@ func (r *BackupSessionReconciler) Reconcile(req ctrl.Request) (ctrl.Result, erro monthlyBackups.Counter = backupConf.Spec.Keep.Monthly yearlyBackups.Counter = backupConf.Spec.Keep.Yearly for _, session := range backupSessionList.Items { - if session.Spec.Ref != backupConf.Name { + if session.Spec.Ref.Name != backupConf.Name { continue } deleteSession := true diff --git a/controllers/backupsession_controller_test.go b/controllers/backupsession_controller_test.go index f26402a..b283522 100644 --- a/controllers/backupsession_controller_test.go +++ b/controllers/backupsession_controller_test.go @@ -6,6 +6,7 @@ import ( . "github.com/onsi/ginkgo" . "github.com/onsi/gomega" //corev1 "k8s.io/api/core/v1" + corev1 "k8s.io/api/core/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/types" ) @@ -29,7 +30,9 @@ var _ = Describe("Testing BackupSession controller", func() { Namespace: TestNamespace, }, Spec: formolv1alpha1.BackupSessionSpec{ - Ref: TestBackupConfName, + Ref: corev1.ObjectReference{ + Name: TestBackupConfName, + }, }, } }) diff --git a/controllers/restoresession_controller.go b/controllers/restoresession_controller.go index 042f352..440c8c7 100644 --- a/controllers/restoresession_controller.go +++ b/controllers/restoresession_controller.go @@ -26,6 +26,7 @@ import ( appsv1 "k8s.io/api/apps/v1" batchv1 "k8s.io/api/batch/v1" corev1 "k8s.io/api/core/v1" + "k8s.io/apimachinery/pkg/api/errors" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/runtime" ctrl "sigs.k8s.io/controller-runtime" @@ -60,20 +61,31 @@ func (r *RestoreSessionReconciler) Reconcile(req ctrl.Request) (ctrl.Result, err log.Error(err, "unable to get restoresession") return ctrl.Result{}, client.IgnoreNotFound(err) } + log.V(1).Info("got restoresession", "restoreSession", restoreSession) // Get the BackupSession the RestoreSession references backupSession := &formolv1alpha1.BackupSession{} if err := r.Get(ctx, client.ObjectKey{ Namespace: restoreSession.Namespace, - Name: restoreSession.Spec.Ref}, backupSession); err != nil { - log.Error(err, "unable to get backupsession", "restoresession", restoreSession.Spec) - return ctrl.Result{}, client.IgnoreNotFound(err) + Name: restoreSession.Spec.BackupSessionRef.Ref.Name, + }, backupSession); err != nil { + if errors.IsNotFound(err) { + backupSession = &formolv1alpha1.BackupSession{ + Spec: restoreSession.Spec.BackupSessionRef.Spec, + Status: restoreSession.Spec.BackupSessionRef.Status, + } + log.V(1).Info("generated backupsession", "backupsession", backupSession) + } else { + log.Error(err, "unable to get backupsession", "restoresession", restoreSession.Spec) + return ctrl.Result{}, client.IgnoreNotFound(err) + } } // Get the BackupConfiguration linked to the BackupSession backupConf := &formolv1alpha1.BackupConfiguration{} if err := r.Get(ctx, client.ObjectKey{ - Namespace: backupSession.Namespace, - Name: backupSession.Spec.Ref}, backupConf); err != nil { - log.Error(err, "unable to get backupConfiguration") + Namespace: backupSession.Spec.Ref.Namespace, + Name: backupSession.Spec.Ref.Name, + }, backupConf); err != nil { + log.Error(err, "unable to get backupConfiguration", "name", backupSession.Spec.Ref, "namespace", backupSession.Namespace) return ctrl.Result{}, client.IgnoreNotFound(err) } @@ -229,50 +241,50 @@ func (r *RestoreSessionReconciler) Reconcile(req ctrl.Request) (ctrl.Result, err return nil } } - var snapshotId string for _, targetStatus := range backupSession.Status.Targets { if targetStatus.Name == target.Name && targetStatus.Kind == target.Kind { - snapshotId = targetStatus.SnapshotId + snapshotId := targetStatus.SnapshotId + restoreSessionEnv := []corev1.EnvVar{ + corev1.EnvVar{ + Name: formolv1alpha1.TARGET_NAME, + Value: target.Name, + }, + corev1.EnvVar{ + Name: formolv1alpha1.RESTORESESSION_NAME, + Value: restoreSession.Name, + }, + corev1.EnvVar{ + Name: formolv1alpha1.RESTORESESSION_NAMESPACE, + Value: restoreSession.Namespace, + }, + } + initContainer := corev1.Container{ + Name: RESTORESESSION, + Image: formolutils.FORMOLCLI, + Args: []string{"volume", "restore", "--snapshot-id", snapshotId}, + VolumeMounts: target.VolumeMounts, + Env: restoreSessionEnv, + } + repo := &formolv1alpha1.Repo{} + if err := r.Get(ctx, client.ObjectKey{ + Namespace: backupConf.Namespace, + Name: backupConf.Spec.Repository, + }, repo); err != nil { + log.Error(err, "unable to get Repo from BackupConfiguration") + return err + } + // S3 backing storage + initContainer.Env = append(initContainer.Env, formolutils.ConfigureResticEnvVar(backupConf, repo)...) + deployment.Spec.Template.Spec.InitContainers = append([]corev1.Container{initContainer}, + deployment.Spec.Template.Spec.InitContainers...) + if err := r.Update(ctx, deployment); err != nil { + log.Error(err, "unable to update deployment") + return err + } + + return nil } } - restoreSessionEnv := []corev1.EnvVar{ - corev1.EnvVar{ - Name: formolv1alpha1.TARGET_NAME, - Value: target.Name, - }, - corev1.EnvVar{ - Name: formolv1alpha1.RESTORESESSION_NAME, - Value: restoreSession.Name, - }, - corev1.EnvVar{ - Name: formolv1alpha1.RESTORESESSION_NAMESPACE, - Value: restoreSession.Namespace, - }, - } - initContainer := corev1.Container{ - Name: RESTORESESSION, - Image: formolutils.FORMOLCLI, - Args: []string{"volume", "restore", "--snapshot-id", snapshotId}, - VolumeMounts: target.VolumeMounts, - Env: restoreSessionEnv, - } - repo := &formolv1alpha1.Repo{} - if err := r.Get(ctx, client.ObjectKey{ - Namespace: backupConf.Namespace, - Name: backupConf.Spec.Repository, - }, repo); err != nil { - log.Error(err, "unable to get Repo from BackupConfiguration") - return err - } - // S3 backing storage - initContainer.Env = append(initContainer.Env, formolutils.ConfigureResticEnvVar(backupConf, repo)...) - deployment.Spec.Template.Spec.InitContainers = append([]corev1.Container{initContainer}, - deployment.Spec.Template.Spec.InitContainers...) - if err := r.Update(ctx, deployment); err != nil { - log.Error(err, "unable to update deployment") - return err - } - return nil } @@ -333,7 +345,7 @@ func (r *RestoreSessionReconciler) Reconcile(req ctrl.Request) (ctrl.Result, err } } case formolv1alpha1.Running: - currentTargetStatus := restoreSession.Status.Targets[len(restoreSession.Status.Targets)-1] + currentTargetStatus := &restoreSession.Status.Targets[len(restoreSession.Status.Targets)-1] switch currentTargetStatus.SessionState { case formolv1alpha1.Failure: log.V(0).Info("last restore task failed. Stop here", "target", currentTargetStatus.Name) @@ -345,6 +357,32 @@ func (r *RestoreSessionReconciler) Reconcile(req ctrl.Request) (ctrl.Result, err case formolv1alpha1.Running: log.V(0).Info("task is still running", "target", currentTargetStatus.Name) return ctrl.Result{}, nil + case formolv1alpha1.Waiting: + target := backupConf.Spec.Targets[len(restoreSession.Status.Targets)-1] + if target.Kind == formolv1alpha1.SidecarKind { + deployment := &appsv1.Deployment{} + if err := r.Get(context.Background(), client.ObjectKey{ + Namespace: restoreSession.Namespace, + Name: target.Name, + }, deployment); err != nil { + log.Error(err, "unable to get deployment") + return ctrl.Result{}, err + } + + if deployment.Status.ReadyReplicas == *deployment.Spec.Replicas { + log.V(0).Info("The deployment is ready. We can resume the backup") + currentTargetStatus.SessionState = formolv1alpha1.Finalize + if err := r.Status().Update(ctx, restoreSession); err != nil { + log.Error(err, "unable to update restoresession") + return ctrl.Result{}, err + } + } else { + log.V(0).Info("Waiting for the sidecar to come back") + return ctrl.Result{RequeueAfter: 10 * time.Second}, nil + } + } else { + log.V(0).Info("not a SidecarKind. Ignoring Waiting") + } case formolv1alpha1.Success: _ = endTask() log.V(0).Info("last task was a success. start a new one", "target", currentTargetStatus) diff --git a/controllers/restoresession_controller_test.go b/controllers/restoresession_controller_test.go index 0d073d4..2a3750c 100644 --- a/controllers/restoresession_controller_test.go +++ b/controllers/restoresession_controller_test.go @@ -5,6 +5,7 @@ import ( formolv1alpha1 "github.com/desmo999r/formol/api/v1alpha1" . "github.com/onsi/ginkgo" . "github.com/onsi/gomega" + corev1 "k8s.io/api/core/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/types" ) @@ -28,7 +29,11 @@ var _ = Describe("Testing RestoreSession controller", func() { Namespace: TestNamespace, }, Spec: formolv1alpha1.RestoreSessionSpec{ - Ref: TestBackupSessionName, + BackupSessionRef: formolv1alpha1.BackupSessionRef{ + Ref: corev1.ObjectReference{ + Name: TestBackupSessionName, + }, + }, }, } }) diff --git a/controllers/suite_test.go b/controllers/suite_test.go index 4edb984..0acdce0 100644 --- a/controllers/suite_test.go +++ b/controllers/suite_test.go @@ -218,7 +218,10 @@ var ( Namespace: TestNamespace, }, Spec: formolv1alpha1.BackupSessionSpec{ - Ref: TestBackupConfName, + Ref: corev1.ObjectReference{ + Name: TestBackupConfName, + Namespace: TestNamespace, + }, }, } ) diff --git a/test/00-setup.yaml b/test/00-setup.yaml index 2e88742..5aff4be 100644 --- a/test/00-setup.yaml +++ b/test/00-setup.yaml @@ -75,6 +75,16 @@ spec: --- apiVersion: formol.desmojim.fr/v1alpha1 kind: Function +metadata: + name: restore-pg + namespace: demo +spec: + name: restore-pg + image: desmo999r/formolcli:latest + args: ["postgres", "restore", "--hostname", $(PGHOST), "--database", $(PGDATABASE), "--username", $(PGUSER), "--password", $(PGPASSWD), "--file", "/output/backup-pg.sql"] +--- +apiVersion: formol.desmojim.fr/v1alpha1 +kind: Function metadata: name: backup-pg namespace: demo @@ -82,3 +92,21 @@ spec: name: backup-pg image: desmo999r/formolcli:latest args: ["postgres", "backup", "--hostname", $(PGHOST), "--database", $(PGDATABASE), "--username", $(PGUSER), "--password", $(PGPASSWD), "--file", "/output/backup-pg.sql"] +--- +apiVersion: formol.desmojim.fr/v1alpha1 +kind: Function +metadata: + name: maintenance-off + namespace: demo +spec: + name: maintenance-off + command: ["/bin/bash", "-c", "echo $(date +%Y/%m/%d-%H:%M:%S) maintenance-off >> /data/logs.txt"] +--- +apiVersion: formol.desmojim.fr/v1alpha1 +kind: Function +metadata: + name: maintenance-on + namespace: demo +spec: + name: maintenance-on + command: ["/bin/bash", "-c", "echo $(date +%Y/%m/%d-%H:%M:%S) maintenance-on >> /data/logs.txt"] diff --git a/test/02-backupconf.yaml b/test/02-backupconf.yaml index caaa90c..00b8cda 100644 --- a/test/02-backupconf.yaml +++ b/test/02-backupconf.yaml @@ -11,6 +11,10 @@ spec: - kind: Sidecar apiVersion: v1 name: nginx-deployment + steps: + - name: maintenance-on + - name: maintenance-off + finalize: true volumeMounts: - name: demo-data mountPath: /data