From 2901f9aa1afa617a241cb797e23eb3455e87cb4f Mon Sep 17 00:00:00 2001 From: Jean-Marc Andre Date: Sat, 17 Apr 2021 16:21:26 +0200 Subject: [PATCH] Refactored since reconciler are not reentrant --- Makefile | 3 +- api/v1alpha1/backupconfiguration_types.go | 22 +- api/v1alpha1/backupsession_types.go | 12 +- api/v1alpha1/common.go | 28 +- api/v1alpha1/function_webhook.go | 76 ++ api/v1alpha1/restoresession_types.go | 3 +- api/v1alpha1/zz_generated.deepcopy.go | 38 +- config/crd/kustomization.yaml | 10 +- .../cainjection_in_backupconfigurations.yaml | 2 +- .../cainjection_in_backupsessions.yaml | 2 +- .../crd/patches/cainjection_in_functions.yaml | 2 +- .../cainjection_in_restoresessions.yaml | 2 +- .../webhook_in_backupconfigurations.yaml | 17 +- .../patches/webhook_in_backupsessions.yaml | 2 +- config/crd/patches/webhook_in_functions.yaml | 2 +- .../patches/webhook_in_restoresessions.yaml | 2 +- controllers/backupconfiguration_controller.go | 549 ++++++------- .../backupconfiguration_controller_test.go | 49 +- controllers/backupsession_controller.go | 765 +++++++++--------- controllers/backupsession_controller_test.go | 144 ++++ controllers/restoresession_controller.go | 518 ++++++------ controllers/restoresession_controller_test.go | 90 +++ controllers/suite_test.go | 151 +++- main.go | 4 + test/01-deployment.yaml | 2 + test/02-backupconf.yaml | 5 +- 26 files changed, 1428 insertions(+), 1072 deletions(-) create mode 100644 api/v1alpha1/function_webhook.go create mode 100644 controllers/backupsession_controller_test.go create mode 100644 controllers/restoresession_controller_test.go diff --git a/Makefile b/Makefile index 8eaf741..5cb3c63 100644 --- a/Makefile +++ b/Makefile @@ -2,7 +2,8 @@ # Image URL to use all building/pushing image targets IMG ?= desmo999r/formolcontroller:latest # Produce CRDs that work back to Kubernetes 1.11 (no version conversion) -CRD_OPTIONS ?= "crd:trivialVersions=true" +#CRD_OPTIONS ?= "crd:trivialVersions=true" +CRD_OPTIONS ?= "crd:trivialVersions=true,crdVersions=v1" # Get the currently used golang install path (in GOPATH/bin, unless GOBIN is set) ifeq (,$(shell go env GOBIN)) diff --git a/api/v1alpha1/backupconfiguration_types.go b/api/v1alpha1/backupconfiguration_types.go index 8ceedfe..2d3e6bb 100644 --- a/api/v1alpha1/backupconfiguration_types.go +++ b/api/v1alpha1/backupconfiguration_types.go @@ -21,10 +21,18 @@ import ( metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" ) +const ( + SidecarKind string = "Sidecar" + JobKind string = "Job" + BackupVolumes string = "Volumes" +) + type Step struct { - Name string `json:"name"` - Namespace string `json:"namespace"` - Env []corev1.EnvVar `json:"env"` + Name string `json:"name"` + // +optional + Env []corev1.EnvVar `json:"env,omitempty"` + // +optional + Finalize *bool `json:"finalize,omitempty"` } type Hook struct { @@ -34,14 +42,10 @@ type Hook struct { } type Target struct { - // +kubebuilder:validation:Enum=Deployment;Task + // +kubebuilder:validation:Enum=Sidecar;Job Kind string `json:"kind"` Name string `json:"name"` // +optional - BeforeBackup []Hook `json:"beforeBackup,omitempty"` - // +optional - AfterBackup []Hook `json:"afterBackup,omitempty"` - // +optional ApiVersion string `json:"apiVersion,omitempty"` // +optional VolumeMounts []corev1.VolumeMount `json:"volumeMounts,omitempty"` @@ -50,6 +54,8 @@ type Target struct { // +optional // +kubebuilder:validation:MinItems=1 Steps []Step `json:"steps,omitempty"` + // +kubebuilder:default:=2 + Retry int `json:"retry,omitempty"` } type Keep struct { diff --git a/api/v1alpha1/backupsession_types.go b/api/v1alpha1/backupsession_types.go index 4c2a38c..a2a4a9f 100644 --- a/api/v1alpha1/backupsession_types.go +++ b/api/v1alpha1/backupsession_types.go @@ -23,17 +23,9 @@ import ( // 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 Ref struct { - Name string `json:"name"` -} - // BackupSessionSpec defines the desired state of BackupSession type BackupSessionSpec struct { - // INSERT ADDITIONAL SPEC FIELDS - desired state of cluster - // Important: Run "make" to regenerate code after modifying this file - - // Foo is an example field of BackupSession. Edit BackupSession_types.go to remove/update - Ref `json:"ref"` + Ref string `json:"ref"` } // BackupSessionStatus defines the observed state of BackupSession @@ -41,8 +33,6 @@ type BackupSessionStatus struct { // INSERT ADDITIONAL STATUS FIELD - define observed state of cluster // Important: Run "make" to regenerate code after modifying this file // +optional - ObservedGeneration int64 `json:"observedGeneration,omitempty"` - // +optional SessionState `json:"state,omitempty"` // +optional StartTime *metav1.Time `json:"startTime,omitempty"` diff --git a/api/v1alpha1/common.go b/api/v1alpha1/common.go index 993f6a4..e9ea1da 100644 --- a/api/v1alpha1/common.go +++ b/api/v1alpha1/common.go @@ -7,14 +7,24 @@ import ( type SessionState string const ( - New SessionState = "New" - Running SessionState = "Running" - Success SessionState = "Success" - Failure SessionState = "Failure" - Deleted SessionState = "Deleted" - TARGET_NAME string = "TARGET_NAME" - RESTORESESSION_NAMESPACE string = "RESTORESESSION_NAMESPACE" - RESTORESESSION_NAME string = "RESTORESESSION_NAME" + New SessionState = "New" + Init SessionState = "Initializing" + Running SessionState = "Running" + Finalize SessionState = "Finalizing" + Success SessionState = "Success" + Failure SessionState = "Failure" + Deleted SessionState = "Deleted" + // Environment variables used by the sidecar container + // the name of the sidecar container + SIDECARCONTAINER_NAME string = "formol" + // Used by both the backupsession and restoresession controllers to identified the target deployment + TARGET_NAME string = "TARGET_NAME" + // Used by restoresession controller + RESTORESESSION_NAMESPACE string = "RESTORESESSION_NAMESPACE" + RESTORESESSION_NAME string = "RESTORESESSION_NAME" + // Used by the backupsession controller + POD_NAME string = "POD_NAME" + POD_NAMESPACE string = "POD_NAMESPACE" ) type TargetStatus struct { @@ -28,4 +38,6 @@ type TargetStatus struct { StartTime *metav1.Time `json:"startTime,omitempty"` // +optional Duration *metav1.Duration `json:"duration,omitempty"` + // +optional + Try int `json:"try,omitemmpty"` } diff --git a/api/v1alpha1/function_webhook.go b/api/v1alpha1/function_webhook.go new file mode 100644 index 0000000..d1019d9 --- /dev/null +++ b/api/v1alpha1/function_webhook.go @@ -0,0 +1,76 @@ +/* + + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package v1alpha1 + +import ( + "k8s.io/apimachinery/pkg/runtime" + ctrl "sigs.k8s.io/controller-runtime" + logf "sigs.k8s.io/controller-runtime/pkg/log" + "sigs.k8s.io/controller-runtime/pkg/webhook" +) + +// log is for logging in this package. +var functionlog = logf.Log.WithName("function-resource") + +func (r *Function) SetupWebhookWithManager(mgr ctrl.Manager) error { + return ctrl.NewWebhookManagedBy(mgr). + For(r). + Complete() +} + +// EDIT THIS FILE! THIS IS SCAFFOLDING FOR YOU TO OWN! + +// +kubebuilder:webhook:path=/mutate-formol-desmojim-fr-v1alpha1-function,mutating=true,failurePolicy=fail,groups=formol.desmojim.fr,resources=functions,verbs=create;update,versions=v1alpha1,name=mfunction.kb.io + +var _ webhook.Defaulter = &Function{} + +// Default implements webhook.Defaulter so a webhook will be registered for the type +func (r *Function) Default() { + functionlog.Info("default", "name", r.Name) + + // TODO(user): fill in your defaulting logic. + r.Spec.Name = r.Name +} + +// TODO(user): change verbs to "verbs=create;update;delete" if you want to enable deletion validation. +// +kubebuilder:webhook:verbs=create;update,path=/validate-formol-desmojim-fr-v1alpha1-function,mutating=false,failurePolicy=fail,groups=formol.desmojim.fr,resources=functions,versions=v1alpha1,name=vfunction.kb.io + +var _ webhook.Validator = &Function{} + +// ValidateCreate implements webhook.Validator so a webhook will be registered for the type +func (r *Function) ValidateCreate() error { + functionlog.Info("validate create", "name", r.Name) + + // TODO(user): fill in your validation logic upon object creation. + return nil +} + +// ValidateUpdate implements webhook.Validator so a webhook will be registered for the type +func (r *Function) ValidateUpdate(old runtime.Object) error { + functionlog.Info("validate update", "name", r.Name) + + // TODO(user): fill in your validation logic upon object update. + return nil +} + +// ValidateDelete implements webhook.Validator so a webhook will be registered for the type +func (r *Function) ValidateDelete() error { + functionlog.Info("validate delete", "name", r.Name) + + // TODO(user): fill in your validation logic upon object deletion. + return nil +} diff --git a/api/v1alpha1/restoresession_types.go b/api/v1alpha1/restoresession_types.go index 95b9df4..6cbbd31 100644 --- a/api/v1alpha1/restoresession_types.go +++ b/api/v1alpha1/restoresession_types.go @@ -18,6 +18,7 @@ package v1alpha1 import ( metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + //"k8s.io/apimachinery/pkg/types" ) // EDIT THIS FILE! THIS IS SCAFFOLDING FOR YOU TO OWN! @@ -28,7 +29,7 @@ type RestoreSessionSpec struct { // INSERT ADDITIONAL SPEC FIELDS - desired state of cluster // Important: Run "make" to regenerate code after modifying this file - BackupSessionRef metav1.ObjectMeta `json:"backupSessionRef"` + Ref string `json:"backupSessionRef"` } // RestoreSessionStatus defines the observed state of RestoreSession diff --git a/api/v1alpha1/zz_generated.deepcopy.go b/api/v1alpha1/zz_generated.deepcopy.go index 406c13d..37e1b26 100644 --- a/api/v1alpha1/zz_generated.deepcopy.go +++ b/api/v1alpha1/zz_generated.deepcopy.go @@ -210,7 +210,6 @@ func (in *BackupSessionList) DeepCopyObject() runtime.Object { // 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. @@ -342,21 +341,6 @@ func (in *Keep) DeepCopy() *Keep { return out } -// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. -func (in *Ref) DeepCopyInto(out *Ref) { - *out = *in -} - -// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new Ref. -func (in *Ref) DeepCopy() *Ref { - if in == nil { - return nil - } - out := new(Ref) - in.DeepCopyInto(out) - return out -} - // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *Repo) DeepCopyInto(out *Repo) { *out = *in @@ -452,7 +436,7 @@ func (in *RestoreSession) DeepCopyInto(out *RestoreSession) { *out = *in out.TypeMeta = in.TypeMeta in.ObjectMeta.DeepCopyInto(&out.ObjectMeta) - in.Spec.DeepCopyInto(&out.Spec) + out.Spec = in.Spec in.Status.DeepCopyInto(&out.Status) } @@ -509,7 +493,6 @@ 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. @@ -573,6 +556,11 @@ func (in *Step) DeepCopyInto(out *Step) { (*in)[i].DeepCopyInto(&(*out)[i]) } } + if in.Finalize != nil { + in, out := &in.Finalize, &out.Finalize + *out = new(bool) + **out = **in + } } // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new Step. @@ -588,20 +576,6 @@ func (in *Step) DeepCopy() *Step { // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *Target) DeepCopyInto(out *Target) { *out = *in - if in.BeforeBackup != nil { - in, out := &in.BeforeBackup, &out.BeforeBackup - *out = make([]Hook, len(*in)) - for i := range *in { - (*in)[i].DeepCopyInto(&(*out)[i]) - } - } - if in.AfterBackup != nil { - in, out := &in.AfterBackup, &out.AfterBackup - *out = make([]Hook, len(*in)) - for i := range *in { - (*in)[i].DeepCopyInto(&(*out)[i]) - } - } if in.VolumeMounts != nil { in, out := &in.VolumeMounts, &out.VolumeMounts *out = make([]v1.VolumeMount, len(*in)) diff --git a/config/crd/kustomization.yaml b/config/crd/kustomization.yaml index 3557d7e..6fdb777 100644 --- a/config/crd/kustomization.yaml +++ b/config/crd/kustomization.yaml @@ -14,17 +14,17 @@ patchesStrategicMerge: # patches here are for enabling the conversion webhook for each CRD #- patches/webhook_in_tasks.yaml #- patches/webhook_in_functions.yaml -#- patches/webhook_in_backupconfigurations.yaml -- patches/webhook_in_backupsessions.yaml +- patches/webhook_in_backupconfigurations.yaml +#- patches/webhook_in_backupsessions.yaml #- patches/webhook_in_repoes.yaml #- patches/webhook_in_restoresessions.yaml # +kubebuilder:scaffold:crdkustomizewebhookpatch # [CERTMANAGER] To enable webhook, uncomment all the sections with [CERTMANAGER] prefix. # patches here are for enabling the CA injection for each CRD -- patches/cainjection_in_functions.yaml -- patches/cainjection_in_backupconfigurations.yaml -- patches/cainjection_in_backupsessions.yaml +#- patches/cainjection_in_functions.yaml +#- patches/cainjection_in_backupconfigurations.yaml +#- patches/cainjection_in_backupsessions.yaml #- patches/cainjection_in_repoes.yaml #- patches/cainjection_in_restoresessions.yaml # +kubebuilder:scaffold:crdkustomizecainjectionpatch diff --git a/config/crd/patches/cainjection_in_backupconfigurations.yaml b/config/crd/patches/cainjection_in_backupconfigurations.yaml index 9260d66..ba16473 100644 --- a/config/crd/patches/cainjection_in_backupconfigurations.yaml +++ b/config/crd/patches/cainjection_in_backupconfigurations.yaml @@ -1,6 +1,6 @@ # The following patch adds a directive for certmanager to inject CA into the CRD # CRD conversion requires k8s 1.13 or later. -apiVersion: apiextensions.k8s.io/v1beta1 +apiVersion: apiextensions.k8s.io/v1 kind: CustomResourceDefinition metadata: annotations: diff --git a/config/crd/patches/cainjection_in_backupsessions.yaml b/config/crd/patches/cainjection_in_backupsessions.yaml index c2d5996..f395951 100644 --- a/config/crd/patches/cainjection_in_backupsessions.yaml +++ b/config/crd/patches/cainjection_in_backupsessions.yaml @@ -1,6 +1,6 @@ # The following patch adds a directive for certmanager to inject CA into the CRD # CRD conversion requires k8s 1.13 or later. -apiVersion: apiextensions.k8s.io/v1beta1 +apiVersion: apiextensions.k8s.io/v1 kind: CustomResourceDefinition metadata: annotations: diff --git a/config/crd/patches/cainjection_in_functions.yaml b/config/crd/patches/cainjection_in_functions.yaml index 4788af8..c8c1091 100644 --- a/config/crd/patches/cainjection_in_functions.yaml +++ b/config/crd/patches/cainjection_in_functions.yaml @@ -1,6 +1,6 @@ # The following patch adds a directive for certmanager to inject CA into the CRD # CRD conversion requires k8s 1.13 or later. -apiVersion: apiextensions.k8s.io/v1beta1 +apiVersion: apiextensions.k8s.io/v1 kind: CustomResourceDefinition metadata: annotations: diff --git a/config/crd/patches/cainjection_in_restoresessions.yaml b/config/crd/patches/cainjection_in_restoresessions.yaml index aa30a5c..cfed67d 100644 --- a/config/crd/patches/cainjection_in_restoresessions.yaml +++ b/config/crd/patches/cainjection_in_restoresessions.yaml @@ -1,6 +1,6 @@ # The following patch adds a directive for certmanager to inject CA into the CRD # CRD conversion requires k8s 1.13 or later. -apiVersion: apiextensions.k8s.io/v1beta1 +apiVersion: apiextensions.k8s.io/v1 kind: CustomResourceDefinition metadata: annotations: diff --git a/config/crd/patches/webhook_in_backupconfigurations.yaml b/config/crd/patches/webhook_in_backupconfigurations.yaml index d09d44a..e08ff07 100644 --- a/config/crd/patches/webhook_in_backupconfigurations.yaml +++ b/config/crd/patches/webhook_in_backupconfigurations.yaml @@ -1,17 +1,20 @@ # The following patch enables conversion webhook for CRD # CRD conversion requires k8s 1.13 or later. -apiVersion: apiextensions.k8s.io/v1beta1 +apiVersion: apiextensions.k8s.io/v1 kind: CustomResourceDefinition metadata: name: backupconfigurations.formol.desmojim.fr spec: + preserveUnknownFields: false conversion: strategy: Webhook - webhookClientConfig: + webhook: + conversionReviewVersions: ["v1", "v1beta1", "v1alpha1"] + clientConfig: # this is "\n" used as a placeholder, otherwise it will be rejected by the apiserver for being blank, # but we're going to set it later using the cert-manager (or potentially a patch if not using cert-manager) - caBundle: Cg== - service: - namespace: system - name: webhook-service - path: /convert + caBundle: Cg== + service: + namespace: system + name: webhook-service + path: /convert diff --git a/config/crd/patches/webhook_in_backupsessions.yaml b/config/crd/patches/webhook_in_backupsessions.yaml index 5be984f..7ae00b1 100644 --- a/config/crd/patches/webhook_in_backupsessions.yaml +++ b/config/crd/patches/webhook_in_backupsessions.yaml @@ -1,6 +1,6 @@ # The following patch enables conversion webhook for CRD # CRD conversion requires k8s 1.13 or later. -apiVersion: apiextensions.k8s.io/v1beta1 +apiVersion: apiextensions.k8s.io/v1 kind: CustomResourceDefinition metadata: name: backupsessions.formol.desmojim.fr diff --git a/config/crd/patches/webhook_in_functions.yaml b/config/crd/patches/webhook_in_functions.yaml index 6f09264..e969e6f 100644 --- a/config/crd/patches/webhook_in_functions.yaml +++ b/config/crd/patches/webhook_in_functions.yaml @@ -1,6 +1,6 @@ # The following patch enables conversion webhook for CRD # CRD conversion requires k8s 1.13 or later. -apiVersion: apiextensions.k8s.io/v1beta1 +apiVersion: apiextensions.k8s.io/v1 kind: CustomResourceDefinition metadata: name: functions.formol.desmojim.fr diff --git a/config/crd/patches/webhook_in_restoresessions.yaml b/config/crd/patches/webhook_in_restoresessions.yaml index c412117..1dc3e58 100644 --- a/config/crd/patches/webhook_in_restoresessions.yaml +++ b/config/crd/patches/webhook_in_restoresessions.yaml @@ -1,6 +1,6 @@ # The following patch enables conversion webhook for CRD # CRD conversion requires k8s 1.13 or later. -apiVersion: apiextensions.k8s.io/v1beta1 +apiVersion: apiextensions.k8s.io/v1 kind: CustomResourceDefinition metadata: name: restoresessions.formol.desmojim.fr.desmojim.fr diff --git a/controllers/backupconfiguration_controller.go b/controllers/backupconfiguration_controller.go index cc7f45b..ca9ab46 100644 --- a/controllers/backupconfiguration_controller.go +++ b/controllers/backupconfiguration_controller.go @@ -18,7 +18,7 @@ package controllers import ( "context" - "time" + //"time" formolrbac "github.com/desmo999r/formol/pkg/rbac" formolutils "github.com/desmo999r/formol/pkg/utils" @@ -33,7 +33,7 @@ import ( ctrl "sigs.k8s.io/controller-runtime" "sigs.k8s.io/controller-runtime/pkg/client" "sigs.k8s.io/controller-runtime/pkg/controller" - "sigs.k8s.io/controller-runtime/pkg/predicate" + //"sigs.k8s.io/controller-runtime/pkg/predicate" formolv1alpha1 "github.com/desmo999r/formol/api/v1alpha1" ) @@ -45,16 +45,6 @@ type BackupConfigurationReconciler struct { Scheme *runtime.Scheme } -func (r *BackupConfigurationReconciler) getDeployment(namespace string, name string) (*appsv1.Deployment, error) { - - deployment := &appsv1.Deployment{} - err := r.Get(context.Background(), client.ObjectKey{ - Namespace: namespace, - Name: name, - }, deployment) - return deployment, err -} - // +kubebuilder:rbac:groups=formol.desmojim.fr,resources=*,verbs=* // +kubebuilder:rbac:groups=apps,resources=deployments,verbs=get;list;watch;create;update;patch;delete // +kubebuilder:rbac:groups=apps,resources=replicasets,verbs=get;list;watch;create;update;patch;delete @@ -67,263 +57,108 @@ func (r *BackupConfigurationReconciler) getDeployment(namespace string, name str // +kubebuilder:rbac:groups=batch,resources=cronjobs,verbs=get;list;watch;create;update;patch;delete // +kubebuilder:rbac:groups=batch,resources=cronjobs/status,verbs=get -func (r *BackupConfigurationReconciler) deleteSidecarContainer(backupConf *formolv1alpha1.BackupConfiguration, target formolv1alpha1.Target) error { - deployment, err := r.getDeployment(backupConf.Namespace, target.Name) - if err != nil { - return err - } - restorecontainers := []corev1.Container{} - for _, container := range deployment.Spec.Template.Spec.Containers { - if container.Name == "backup" { - continue - } - restorecontainers = append(restorecontainers, container) - } - deployment.Spec.Template.Spec.Containers = restorecontainers - if err := r.Update(context.Background(), deployment); err != nil { - return err - } - if err := formolrbac.DeleteFormolRBAC(r.Client, deployment.Spec.Template.Spec.ServiceAccountName, deployment.Namespace); err != nil { - return err - } - selector, err := metav1.LabelSelectorAsMap(deployment.Spec.Selector) - if err != nil { - return nil - } - pods := &corev1.PodList{} - err = r.List(context.Background(), pods, client.MatchingLabels(selector)) - if err != nil { - return nil - } - replicasToDelete := []appsv1.ReplicaSet{} - for _, pod := range pods.Items { - for _, podRef := range pod.OwnerReferences { - rs := &appsv1.ReplicaSet{} - if err := r.Get(context.Background(), client.ObjectKey{ - Name: podRef.Name, - Namespace: pod.Namespace, - }, rs); err != nil { - return nil - } - for _, rsRef := range rs.OwnerReferences { - if rsRef.Kind == deployment.Kind && rsRef.Name == deployment.Name { - replicasToDelete = append(replicasToDelete, *rs) - } - } - } +func (r *BackupConfigurationReconciler) Reconcile(req ctrl.Request) (ctrl.Result, error) { + ctx := context.Background() + log := r.Log.WithValues("backupconfiguration", req.NamespacedName) + //time.Sleep(300 * time.Millisecond) + + log.V(1).Info("Enter Reconcile with req", "req", req, "reconciler", r) + + backupConf := &formolv1alpha1.BackupConfiguration{} + if err := r.Get(ctx, req.NamespacedName, backupConf); err != nil { + return ctrl.Result{}, client.IgnoreNotFound(err) } - for _, replica := range replicasToDelete { - if err := r.Delete(context.TODO(), &replica); err != nil { - return nil - } - } - return nil -} - -func (r *BackupConfigurationReconciler) addSidecarContainer(backupConf *formolv1alpha1.BackupConfiguration, target formolv1alpha1.Target) error { - log := r.Log.WithValues("backupconf", backupConf.Name) - deployment, err := r.getDeployment(backupConf.Namespace, target.Name) - if err != nil { - log.Error(err, "unable to get Deployment") - return err - } - log.V(1).Info("got deployment", "Deployment", deployment) - for _, container := range deployment.Spec.Template.Spec.Containers { - if container.Name == "backup" { - log.V(0).Info("There is already a backup sidecar container. Skipping", "container", container) - return nil - } - } - sidecar := corev1.Container{ - Name: "backup", - Image: "desmo999r/formolcli:latest", - Args: []string{"backupsession", "server"}, - //Image: "busybox", - //Command: []string{ - // "sh", - // "-c", - // "sleep 3600; echo done", - //}, - Env: []corev1.EnvVar{ - corev1.EnvVar{ - Name: "POD_NAME", - ValueFrom: &corev1.EnvVarSource{ - FieldRef: &corev1.ObjectFieldSelector{ - FieldPath: "metadata.name", - }, - }, - }, - corev1.EnvVar{ - Name: "POD_NAMESPACE", - ValueFrom: &corev1.EnvVarSource{ - FieldRef: &corev1.ObjectFieldSelector{ - FieldPath: "metadata.namespace", - }, - }, - }, - corev1.EnvVar{ - Name: "POD_DEPLOYMENT", - Value: target.Name, - }, - }, - VolumeMounts: []corev1.VolumeMount{}, + getDeployment := func(namespace string, name string) (*appsv1.Deployment, error) { + deployment := &appsv1.Deployment{} + err := r.Get(context.Background(), client.ObjectKey{ + Namespace: namespace, + Name: name, + }, deployment) + return deployment, err } - // Gather information from the repo - repo := &formolv1alpha1.Repo{} - if err := r.Get(context.Background(), client.ObjectKey{ - Namespace: backupConf.Namespace, - Name: backupConf.Spec.Repository, - }, repo); err != nil { - log.Error(err, "unable to get Repo from BackupConfiguration") - return err - } - sidecar.Env = append(sidecar.Env, formolutils.ConfigureResticEnvVar(backupConf, repo)...) - - for _, volumemount := range target.VolumeMounts { - log.V(1).Info("mounts", "volumemount", volumemount) - volumemount.ReadOnly = true - sidecar.VolumeMounts = append(sidecar.VolumeMounts, *volumemount.DeepCopy()) - } - selector, err := metav1.LabelSelectorAsMap(deployment.Spec.Selector) - if err != nil { - log.Error(err, "unable to get LableSelector for deployment", "label", deployment.Spec.Selector) - return nil - } - log.V(1).Info("getting pods matching label", "label", selector) - pods := &corev1.PodList{} - err = r.List(context.Background(), pods, client.InNamespace(backupConf.Namespace), client.MatchingLabels(selector)) - if err != nil { - log.Error(err, "unable to get deployment pods") - return nil - } - replicasToDelete := []appsv1.ReplicaSet{} - log.V(1).Info("got that list of pods", "pods", len(pods.Items)) - for _, pod := range pods.Items { - log.V(1).Info("checking pod", "pod", pod) - for _, podRef := range pod.OwnerReferences { - rs := &appsv1.ReplicaSet{} - if err := r.Get(context.Background(), client.ObjectKey{ - Name: podRef.Name, - Namespace: pod.Namespace, - }, rs); err != nil { - log.Error(err, "unable to get replicaset", "replicaset", podRef.Name) - return nil - } - log.V(1).Info("got a replicaset", "rs", rs.Name) - for _, rsRef := range rs.OwnerReferences { - if rsRef.Kind == deployment.Kind && rsRef.Name == deployment.Name { - log.V(0).Info("Adding pod to the list of pods to be restarted", "pod", pod.Name) - replicasToDelete = append(replicasToDelete, *rs) - } - } - } - } - deployment.Spec.Template.Spec.Containers = append(deployment.Spec.Template.Spec.Containers, sidecar) - deployment.Spec.Template.Spec.ShareProcessNamespace = func() *bool { b := true; return &b }() - - if err := formolrbac.CreateFormolRBAC(r.Client, deployment.Spec.Template.Spec.ServiceAccountName, deployment.Namespace); err != nil { - log.Error(err, "unable to create backupsessionlistener RBAC") - return nil - } - - log.V(0).Info("Adding a sicar container") - if err := r.Update(context.Background(), deployment); err != nil { - log.Error(err, "unable to update the Deployment") - return err - } - for _, replica := range replicasToDelete { - if err := r.Delete(context.TODO(), &replica); err != nil { - log.Error(err, "unable to delete replica", "replica", replica.Name) - return nil - } - } - return nil -} - -func (r *BackupConfigurationReconciler) deleteCronJob(backupConf *formolv1alpha1.BackupConfiguration) error { - log := r.Log.WithValues("deleteCronJob", backupConf.Name) - _ = formolrbac.DeleteFormolRBAC(r.Client, "default", backupConf.Namespace) - _ = formolrbac.DeleteBackupSessionCreatorRBAC(r.Client, backupConf.Namespace) - cronjob := &kbatch_beta1.CronJob{} - if err := r.Get(context.Background(), client.ObjectKey{ - Namespace: backupConf.Namespace, - Name: "backup-" + backupConf.Name, - }, cronjob); err == nil { - log.V(0).Info("Deleting cronjob", "cronjob", cronjob.Name) - return r.Delete(context.TODO(), cronjob) - } else { - return err - } -} -func (r *BackupConfigurationReconciler) addCronJob(backupConf *formolv1alpha1.BackupConfiguration) error { - log := r.Log.WithValues("addCronJob", backupConf.Name) - - if err := formolrbac.CreateFormolRBAC(r.Client, "default", backupConf.Namespace); err != nil { - log.Error(err, "unable to create backupsessionlistener RBAC") - return nil - } - - if err := formolrbac.CreateBackupSessionCreatorRBAC(r.Client, backupConf.Namespace); err != nil { - log.Error(err, "unable to create backupsession-creator RBAC") - return nil - } - - cronjob := &kbatch_beta1.CronJob{} - if err := r.Get(context.Background(), client.ObjectKey{ - Namespace: backupConf.Namespace, - Name: "backup-" + backupConf.Name, - }, cronjob); err == nil { - log.V(0).Info("there is already a cronjob") - var changed bool - if backupConf.Spec.Schedule != cronjob.Spec.Schedule { - log.V(0).Info("cronjob schedule has changed", "old schedule", cronjob.Spec.Schedule, "new schedule", backupConf.Spec.Schedule) - cronjob.Spec.Schedule = backupConf.Spec.Schedule - changed = true - } - if backupConf.Spec.Suspend != cronjob.Spec.Suspend { - log.V(0).Info("cronjob suspend has changed", "before", cronjob.Spec.Suspend, "new", backupConf.Spec.Suspend) - cronjob.Spec.Suspend = backupConf.Spec.Suspend - changed = true - } - if changed == true { - if err := r.Update(context.TODO(), cronjob); err != nil { - log.Error(err, "unable to update cronjob definition") - return err - } - } - return nil - } else if errors.IsNotFound(err) == false { - log.Error(err, "something went wrong") - return err - } - - cronjob = &kbatch_beta1.CronJob{ - ObjectMeta: metav1.ObjectMeta{ - Name: "backup-" + backupConf.Name, + deleteCronJob := func() error { + _ = formolrbac.DeleteFormolRBAC(r.Client, "default", backupConf.Namespace) + _ = formolrbac.DeleteBackupSessionCreatorRBAC(r.Client, backupConf.Namespace) + cronjob := &kbatch_beta1.CronJob{} + if err := r.Get(context.Background(), client.ObjectKey{ Namespace: backupConf.Namespace, - }, - Spec: kbatch_beta1.CronJobSpec{ - Suspend: backupConf.Spec.Suspend, - Schedule: backupConf.Spec.Schedule, - JobTemplate: kbatch_beta1.JobTemplateSpec{ - Spec: batchv1.JobSpec{ - Template: corev1.PodTemplateSpec{ - Spec: corev1.PodSpec{ - RestartPolicy: corev1.RestartPolicyOnFailure, - ServiceAccountName: "backupsession-creator", - Containers: []corev1.Container{ - corev1.Container{ - Name: "job-createbackupsession-" + backupConf.Name, - Image: "desmo999r/formolcli:latest", - Args: []string{ - "backupsession", - "create", - "--namespace", - backupConf.Namespace, - "--name", - backupConf.Name, + Name: "backup-" + backupConf.Name, + }, cronjob); err == nil { + log.V(0).Info("Deleting cronjob", "cronjob", cronjob.Name) + return r.Delete(context.TODO(), cronjob) + } else { + return err + } + } + + addCronJob := func() error { + if err := formolrbac.CreateFormolRBAC(r.Client, "default", backupConf.Namespace); err != nil { + log.Error(err, "unable to create backupsessionlistener RBAC") + return nil + } + + if err := formolrbac.CreateBackupSessionCreatorRBAC(r.Client, backupConf.Namespace); err != nil { + log.Error(err, "unable to create backupsession-creator RBAC") + return nil + } + + cronjob := &kbatch_beta1.CronJob{} + if err := r.Get(context.Background(), client.ObjectKey{ + Namespace: backupConf.Namespace, + Name: "backup-" + backupConf.Name, + }, cronjob); err == nil { + log.V(0).Info("there is already a cronjob") + var changed bool + if backupConf.Spec.Schedule != cronjob.Spec.Schedule { + log.V(0).Info("cronjob schedule has changed", "old schedule", cronjob.Spec.Schedule, "new schedule", backupConf.Spec.Schedule) + cronjob.Spec.Schedule = backupConf.Spec.Schedule + changed = true + } + if backupConf.Spec.Suspend != cronjob.Spec.Suspend { + log.V(0).Info("cronjob suspend has changed", "before", cronjob.Spec.Suspend, "new", backupConf.Spec.Suspend) + cronjob.Spec.Suspend = backupConf.Spec.Suspend + changed = true + } + if changed == true { + if err := r.Update(context.TODO(), cronjob); err != nil { + log.Error(err, "unable to update cronjob definition") + return err + } + } + return nil + } else if errors.IsNotFound(err) == false { + log.Error(err, "something went wrong") + return err + } + + cronjob = &kbatch_beta1.CronJob{ + ObjectMeta: metav1.ObjectMeta{ + Name: "backup-" + backupConf.Name, + Namespace: backupConf.Namespace, + }, + Spec: kbatch_beta1.CronJobSpec{ + Suspend: backupConf.Spec.Suspend, + Schedule: backupConf.Spec.Schedule, + JobTemplate: kbatch_beta1.JobTemplateSpec{ + Spec: batchv1.JobSpec{ + Template: corev1.PodTemplateSpec{ + Spec: corev1.PodSpec{ + RestartPolicy: corev1.RestartPolicyOnFailure, + ServiceAccountName: "backupsession-creator", + Containers: []corev1.Container{ + corev1.Container{ + Name: "job-createbackupsession-" + backupConf.Name, + Image: "desmo999r/formolcli:latest", + Args: []string{ + "backupsession", + "create", + "--namespace", + backupConf.Namespace, + "--name", + backupConf.Name, + }, }, }, }, @@ -331,46 +166,140 @@ func (r *BackupConfigurationReconciler) addCronJob(backupConf *formolv1alpha1.Ba }, }, }, - }, + } + if err := ctrl.SetControllerReference(backupConf, cronjob, r.Scheme); err != nil { + log.Error(err, "unable to set controller on job", "cronjob", cronjob, "backupconf", backupConf) + return err + } + log.V(0).Info("creating the cronjob") + if err := r.Create(context.Background(), cronjob); err != nil { + log.Error(err, "unable to create the cronjob", "cronjob", cronjob) + return err + } + return nil } - if err := ctrl.SetControllerReference(backupConf, cronjob, r.Scheme); err != nil { - log.Error(err, "unable to set controller on job", "cronjob", cronjob, "backupconf", backupConf) - return err + + deleteSidecarContainer := func(target formolv1alpha1.Target) error { + deployment, err := getDeployment(backupConf.Namespace, target.Name) + if err != nil { + return err + } + restorecontainers := []corev1.Container{} + for _, container := range deployment.Spec.Template.Spec.Containers { + if container.Name == formolv1alpha1.SIDECARCONTAINER_NAME { + continue + } + restorecontainers = append(restorecontainers, container) + } + deployment.Spec.Template.Spec.Containers = restorecontainers + if err := r.Update(context.Background(), deployment); err != nil { + return err + } + if err := formolrbac.DeleteFormolRBAC(r.Client, deployment.Spec.Template.Spec.ServiceAccountName, deployment.Namespace); err != nil { + return err + } + return nil } - log.V(0).Info("creating the cronjob") - if err := r.Create(context.Background(), cronjob); err != nil { - log.Error(err, "unable to create the cronjob", "cronjob", cronjob) - return err + + addSidecarContainer := func(target formolv1alpha1.Target) error { + deployment, err := getDeployment(backupConf.Namespace, target.Name) + if err != nil { + log.Error(err, "unable to get Deployment") + return err + } + log.V(1).Info("got deployment", "Deployment", deployment) + for _, container := range deployment.Spec.Template.Spec.Containers { + if container.Name == formolv1alpha1.SIDECARCONTAINER_NAME { + log.V(0).Info("There is already a backup sidecar container. Skipping", "container", container) + return nil + } + } + sidecar := corev1.Container{ + Name: formolv1alpha1.SIDECARCONTAINER_NAME, + // TODO: Put the image in the BackupConfiguration YAML file + Image: "desmo999r/formolcli:latest", + Args: []string{"backupsession", "server"}, + //Image: "busybox", + //Command: []string{ + // "sh", + // "-c", + // "sleep 3600; echo done", + //}, + Env: []corev1.EnvVar{ + corev1.EnvVar{ + Name: formolv1alpha1.POD_NAME, + ValueFrom: &corev1.EnvVarSource{ + FieldRef: &corev1.ObjectFieldSelector{ + FieldPath: "metadata.name", + }, + }, + }, + corev1.EnvVar{ + Name: formolv1alpha1.POD_NAMESPACE, + ValueFrom: &corev1.EnvVarSource{ + FieldRef: &corev1.ObjectFieldSelector{ + FieldPath: "metadata.namespace", + }, + }, + }, + corev1.EnvVar{ + Name: formolv1alpha1.TARGET_NAME, + Value: target.Name, + }, + }, + VolumeMounts: []corev1.VolumeMount{}, + } + + // Gather information from the repo + repo := &formolv1alpha1.Repo{} + if err := r.Get(context.Background(), client.ObjectKey{ + Namespace: backupConf.Namespace, + Name: backupConf.Spec.Repository, + }, repo); err != nil { + log.Error(err, "unable to get Repo from BackupConfiguration") + return err + } + sidecar.Env = append(sidecar.Env, formolutils.ConfigureResticEnvVar(backupConf, repo)...) + + for _, volumemount := range target.VolumeMounts { + log.V(1).Info("mounts", "volumemount", volumemount) + volumemount.ReadOnly = true + sidecar.VolumeMounts = append(sidecar.VolumeMounts, *volumemount.DeepCopy()) + } + deployment.Spec.Template.Spec.Containers = append(deployment.Spec.Template.Spec.Containers, sidecar) + deployment.Spec.Template.Spec.ShareProcessNamespace = func() *bool { b := true; return &b }() + + if err := formolrbac.CreateFormolRBAC(r.Client, deployment.Spec.Template.Spec.ServiceAccountName, deployment.Namespace); err != nil { + log.Error(err, "unable to create backupsessionlistener RBAC") + return nil + } + + log.V(0).Info("Adding a sicar container") + if err := r.Update(context.Background(), deployment); err != nil { + log.Error(err, "unable to update the Deployment") + return err + } + return nil } - return nil -} -func (r *BackupConfigurationReconciler) Reconcile(req ctrl.Request) (ctrl.Result, error) { - ctx := context.Background() - log := r.Log.WithValues("backupconfiguration", req.NamespacedName) - time.Sleep(300 * time.Millisecond) - - log.V(1).Info("Enter Reconcile with req", "req", req) - - backupConf := &formolv1alpha1.BackupConfiguration{} - if err := r.Get(ctx, req.NamespacedName, backupConf); err != nil { - return ctrl.Result{}, client.IgnoreNotFound(err) + deleteExternalResources := func() error { + for _, target := range backupConf.Spec.Targets { + switch target.Kind { + case formolv1alpha1.SidecarKind: + _ = deleteSidecarContainer(target) + } + } + // TODO: remove the hardcoded "default" + _ = deleteCronJob() + return nil } finalizerName := "finalizer.backupconfiguration.formol.desmojim.fr" - if backupConf.ObjectMeta.DeletionTimestamp.IsZero() { - if !formolutils.ContainsString(backupConf.ObjectMeta.Finalizers, finalizerName) { - backupConf.ObjectMeta.Finalizers = append(backupConf.ObjectMeta.Finalizers, finalizerName) - if err := r.Update(context.Background(), backupConf); err != nil { - log.Error(err, "unable to append finalizer") - return ctrl.Result{}, err - } - } - } else { + if !backupConf.ObjectMeta.DeletionTimestamp.IsZero() { log.V(0).Info("backupconf being deleted", "backupconf", backupConf.Name) if formolutils.ContainsString(backupConf.ObjectMeta.Finalizers, finalizerName) { - _ = r.deleteExternalResources(backupConf) + _ = deleteExternalResources() } backupConf.ObjectMeta.Finalizers = formolutils.RemoveString(backupConf.ObjectMeta.Finalizers, finalizerName) if err := r.Update(context.Background(), backupConf); err != nil { @@ -382,21 +311,27 @@ func (r *BackupConfigurationReconciler) Reconcile(req ctrl.Request) (ctrl.Result return ctrl.Result{}, nil } - if err := r.addCronJob(backupConf); err != nil { + if !formolutils.ContainsString(backupConf.ObjectMeta.Finalizers, finalizerName) { + backupConf.ObjectMeta.Finalizers = append(backupConf.ObjectMeta.Finalizers, finalizerName) + err := r.Update(context.Background(), backupConf) + if err != nil { + log.Error(err, "unable to append finalizer") + } + return ctrl.Result{}, err + } + + if err := addCronJob(); err != nil { return ctrl.Result{}, nil } backupConf.Status.ActiveCronJob = true for _, target := range backupConf.Spec.Targets { switch target.Kind { - case "Deployment": - if err := r.addSidecarContainer(backupConf, target); err != nil { + case formolv1alpha1.SidecarKind: + if err := addSidecarContainer(target); err != nil { return ctrl.Result{}, client.IgnoreNotFound(err) } backupConf.Status.ActiveSidecar = true - case "PersistentVolumeClaim": - log.V(0).Info("TODO backup PVC") - return ctrl.Result{}, nil } } @@ -410,23 +345,11 @@ func (r *BackupConfigurationReconciler) Reconcile(req ctrl.Request) (ctrl.Result return ctrl.Result{}, nil } -func (r *BackupConfigurationReconciler) deleteExternalResources(backupConf *formolv1alpha1.BackupConfiguration) error { - for _, target := range backupConf.Spec.Targets { - switch target.Kind { - case "Deployment": - _ = r.deleteSidecarContainer(backupConf, target) - } - } - // TODO: remove the hardcoded "default" - _ = r.deleteCronJob(backupConf) - return nil -} - func (r *BackupConfigurationReconciler) SetupWithManager(mgr ctrl.Manager) error { return ctrl.NewControllerManagedBy(mgr). For(&formolv1alpha1.BackupConfiguration{}). WithOptions(controller.Options{MaxConcurrentReconciles: 3}). - WithEventFilter(predicate.GenerationChangedPredicate{}). // Don't reconcile when status gets updated + //WithEventFilter(predicate.GenerationChangedPredicate{}). // Don't reconcile when status gets updated //Owns(&formolv1alpha1.BackupSession{}). Owns(&kbatch_beta1.CronJob{}). Complete(r) diff --git a/controllers/backupconfiguration_controller_test.go b/controllers/backupconfiguration_controller_test.go index be5f24f..bc6da33 100644 --- a/controllers/backupconfiguration_controller_test.go +++ b/controllers/backupconfiguration_controller_test.go @@ -5,7 +5,7 @@ import ( //"k8s.io/apimachinery/pkg/types" //"reflect" //"fmt" - //"time" + "time" . "github.com/onsi/ginkgo" . "github.com/onsi/gomega" @@ -21,11 +21,11 @@ import ( var _ = Describe("Testing BackupConf controller", func() { const ( - BackupConfName = "test-backupconf" + BCBackupConfName = "test-backupconf-controller" ) var ( key = types.NamespacedName{ - Name: BackupConfName, + Name: BCBackupConfName, Namespace: TestNamespace, } ctx = context.Background() @@ -35,24 +35,32 @@ var _ = Describe("Testing BackupConf controller", func() { BeforeEach(func() { backupConf = &formolv1alpha1.BackupConfiguration{ ObjectMeta: metav1.ObjectMeta{ - Name: BackupConfName, + Name: BCBackupConfName, Namespace: TestNamespace, }, Spec: formolv1alpha1.BackupConfigurationSpec{ - Repository: RepoName, + Repository: TestRepoName, Schedule: "1 * * * *", Targets: []formolv1alpha1.Target{ formolv1alpha1.Target{ - Kind: "Deployment", - Name: DeploymentName, + Kind: formolv1alpha1.SidecarKind, + Name: TestDeploymentName, + VolumeMounts: []corev1.VolumeMount{ + corev1.VolumeMount{ + Name: TestDataVolume, + MountPath: TestDataMountPath, + }, + }, + Paths: []string{ + TestDataMountPath, + }, }, formolv1alpha1.Target{ - Kind: "Task", - Name: BackupFuncName, + Kind: formolv1alpha1.JobKind, + Name: TestBackupFuncName, Steps: []formolv1alpha1.Step{ formolv1alpha1.Step{ - Name: BackupFuncName, - Namespace: TestNamespace, + Name: TestBackupFuncName, Env: []corev1.EnvVar{ corev1.EnvVar{ Name: "foo", @@ -66,7 +74,7 @@ var _ = Describe("Testing BackupConf controller", func() { }, } }) - Context("There is a backupconf", func() { + Context("Creating a backupconf", func() { JustBeforeEach(func() { Eventually(func() error { return k8sClient.Create(ctx, backupConf) @@ -85,18 +93,16 @@ var _ = Describe("Testing BackupConf controller", func() { return true }, timeout, interval).Should(BeTrue()) Expect(realBackupConf.Spec.Schedule).Should(Equal("1 * * * *")) + Expect(realBackupConf.Spec.Targets[0].Retry).Should(Equal(2)) }) It("Should also create a CronJob", func() { cronJob := &batchv1beta1.CronJob{} Eventually(func() bool { err := k8sClient.Get(ctx, types.NamespacedName{ - Name: "backup-" + BackupConfName, + Name: "backup-" + BCBackupConfName, Namespace: TestNamespace, }, cronJob) - if err != nil { - return false - } - return true + return err == nil }, timeout, interval).Should(BeTrue()) Expect(cronJob.Spec.Schedule).Should(Equal("1 * * * *")) }) @@ -104,7 +110,7 @@ var _ = Describe("Testing BackupConf controller", func() { realDeployment := &appsv1.Deployment{} Eventually(func() (int, error) { err := k8sClient.Get(ctx, types.NamespacedName{ - Name: DeploymentName, + Name: TestDeploymentName, Namespace: TestNamespace, }, realDeployment) if err != nil { @@ -115,6 +121,7 @@ var _ = Describe("Testing BackupConf controller", func() { }) It("Should also update the CronJob", func() { realBackupConf := &formolv1alpha1.BackupConfiguration{} + time.Sleep(300 * time.Millisecond) Eventually(func() bool { err := k8sClient.Get(ctx, key, realBackupConf) if err != nil { @@ -129,7 +136,7 @@ var _ = Describe("Testing BackupConf controller", func() { cronJob := &batchv1beta1.CronJob{} Eventually(func() (string, error) { err := k8sClient.Get(ctx, types.NamespacedName{ - Name: "backup-" + BackupConfName, + Name: "backup-" + BCBackupConfName, Namespace: TestNamespace, }, cronJob) if err != nil { @@ -139,7 +146,7 @@ var _ = Describe("Testing BackupConf controller", func() { }, timeout, interval).Should(Equal("1 0 * * *")) Eventually(func() (bool, error) { err := k8sClient.Get(ctx, types.NamespacedName{ - Name: "backup-" + BackupConfName, + Name: "backup-" + BCBackupConfName, Namespace: TestNamespace, }, cronJob) if err != nil { @@ -160,7 +167,7 @@ var _ = Describe("Testing BackupConf controller", func() { realDeployment := &appsv1.Deployment{} Eventually(func() (int, error) { err := k8sClient.Get(ctx, types.NamespacedName{ - Name: DeploymentName, + Name: TestDeploymentName, Namespace: TestNamespace, }, realDeployment) if err != nil { diff --git a/controllers/backupsession_controller.go b/controllers/backupsession_controller.go index 3188cc9..6f28f86 100644 --- a/controllers/backupsession_controller.go +++ b/controllers/backupsession_controller.go @@ -46,214 +46,8 @@ const ( // BackupSessionReconciler reconciles a BackupSession object type BackupSessionReconciler struct { client.Client - Log logr.Logger - Scheme *runtime.Scheme - BackupSession *formolv1alpha1.BackupSession - BackupConf *formolv1alpha1.BackupConfiguration -} - -func (r *BackupSessionReconciler) StatusUpdate() error { - log := r.Log.WithValues("backupsession-statusupdate", r.BackupSession) - ctx := context.Background() - r.BackupConf = &formolv1alpha1.BackupConfiguration{} - if err := r.Get(ctx, client.ObjectKey{ - Namespace: r.BackupSession.Namespace, - Name: r.BackupSession.Spec.Ref.Name}, r.BackupConf); err != nil { - log.Error(err, "unable to get backupConfiguration") - return client.IgnoreNotFound(err) - } - - // start the next task - startNextTask := func() (*formolv1alpha1.TargetStatus, error) { - nextTarget := len(r.BackupSession.Status.Targets) - if nextTarget < len(r.BackupConf.Spec.Targets) { - target := r.BackupConf.Spec.Targets[nextTarget] - targetStatus := formolv1alpha1.TargetStatus{ - Name: target.Name, - Kind: target.Kind, - SessionState: formolv1alpha1.New, - StartTime: &metav1.Time{Time: time.Now()}, - } - r.BackupSession.Status.Targets = append(r.BackupSession.Status.Targets, targetStatus) - switch target.Kind { - case "Task": - if err := r.CreateBackupJob(target); err != nil { - log.V(0).Info("unable to create task", "task", target) - targetStatus.SessionState = formolv1alpha1.Failure - return nil, err - } - } - return &targetStatus, nil - } else { - return nil, nil - } - } - // Test the backupsession backupstate to decide what to do - switch r.BackupSession.Status.SessionState { - case formolv1alpha1.New: - // Brand new backupsession; start the first task - r.BackupSession.Status.SessionState = formolv1alpha1.Running - targetStatus, err := startNextTask() - if err != nil { - return err - } - log.V(0).Info("New backup. Start the first task", "task", targetStatus) - if err := r.Status().Update(ctx, r.BackupSession); err != nil { - log.Error(err, "unable to update BackupSession status") - return err - } - case formolv1alpha1.Running: - // Backup ongoing. Check the status of the last task to decide what to do - currentTargetStatus := r.BackupSession.Status.Targets[len(r.BackupSession.Status.Targets)-1] - switch currentTargetStatus.SessionState { - case formolv1alpha1.Failure: - // The last task failed. We mark the backupsession as failed and we stop here. - log.V(0).Info("last backup task failed. Stop here", "targetStatus", currentTargetStatus) - r.BackupSession.Status.SessionState = formolv1alpha1.Failure - log.V(1).Info("New BackupSession status", "status", r.BackupSession.Status.SessionState) - if err := r.Status().Update(ctx, r.BackupSession); err != nil { - log.Error(err, "unable to update BackupSession status") - return err - } - case formolv1alpha1.Running: - // The current task is still running. Nothing to do - log.V(0).Info("task is still running", "targetStatus", currentTargetStatus) - case formolv1alpha1.Success: - // The last task successed. Let's try to start the next one - log.V(0).Info("last task was a success. start a new one", "currentTargetStatus", currentTargetStatus, "targetStatus", currentTargetStatus) - targetStatus, err := startNextTask() - if err != nil { - return err - } - if targetStatus == nil { - // No more task to start. The backup is a success - r.BackupSession.Status.SessionState = formolv1alpha1.Success - log.V(0).Info("Backup is successful. Let's try to do some cleanup") - backupSessionList := &formolv1alpha1.BackupSessionList{} - if err := r.List(ctx, backupSessionList, client.InNamespace(r.BackupConf.Namespace), client.MatchingFieldsSelector{Selector: fields.SelectorFromSet(fields.Set{sessionState: "Success"})}); err != nil { - log.Error(err, "unable to get backupsessionlist") - return nil - } - if len(backupSessionList.Items) < 2 { - // Not enough backupSession to proceed - log.V(1).Info("Not enough successful backup jobs") - break - } - - 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 = r.BackupConf.Spec.Keep.Last - dailyBackups.Counter = r.BackupConf.Spec.Keep.Daily - weeklyBackups.Counter = r.BackupConf.Spec.Keep.Weekly - monthlyBackups.Counter = r.BackupConf.Spec.Keep.Monthly - yearlyBackups.Counter = r.BackupConf.Spec.Keep.Yearly - for _, session := range backupSessionList.Items { - if session.Spec.Ref.Name != r.BackupConf.Name { - continue - } - deleteSession := true - keep := []string{} - if lastBackups.Counter > 0 { - 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() { - 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() { - 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() { - 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() { - 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 { - 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 - } - } else { - session.Status.Keep = strings.Join(keep, ",") // + " " + time.Now().Format("2006 Jan 02 15:04:05 -0700 MST") - if err := r.Status().Update(ctx, &session); err != nil { - log.Error(err, "unable to update session status", "session", session) - } - } - } - } - log.V(1).Info("New BackupSession status", "status", r.BackupSession.Status.SessionState) - if err := r.Status().Update(ctx, r.BackupSession); err != nil { - log.Error(err, "unable to update BackupSession status") - return err - } - } - case formolv1alpha1.Deleted: - for _, target := range r.BackupSession.Status.Targets { - if target.SessionState != formolv1alpha1.Deleted { - log.V(1).Info("snaphot has not been deleted. won't delete the backupsession", "target", target) - return nil - } - } - log.V(1).Info("all the snapshots have been deleted. deleting the backupsession") - controllerutil.RemoveFinalizer(r.BackupSession, finalizerName) - if err := r.Update(ctx, r.BackupSession); err != nil { - log.Error(err, "unable to remove finalizer") - return err - } - } - return nil -} - -func (r *BackupSessionReconciler) IsBackupOngoing() bool { - log := r.Log.WithName("IsBackupOngoing") - ctx := context.Background() - - backupSessionList := &formolv1alpha1.BackupSessionList{} - if err := r.List(ctx, backupSessionList, client.InNamespace(r.BackupConf.Namespace), client.MatchingFieldsSelector{Selector: fields.SelectorFromSet(fields.Set{sessionState: "Running"})}); err != nil { - log.Error(err, "unable to get backupsessionlist") - return true - } - if len(backupSessionList.Items) > 0 { - return true - } else { - return false - } + Log logr.Logger + Scheme *runtime.Scheme } // +kubebuilder:rbac:groups=formol.desmojim.fr,resources=backupsessions,verbs=get;list;watch;create;update;patch;delete @@ -265,55 +59,394 @@ func (r *BackupSessionReconciler) Reconcile(req ctrl.Request) (ctrl.Result, erro log := r.Log.WithValues("backupsession", req.NamespacedName) ctx := context.Background() - // your logic here - //time.Sleep(300 * time.Millisecond) - r.BackupSession = &formolv1alpha1.BackupSession{} - if err := r.Get(ctx, req.NamespacedName, r.BackupSession); err != nil { + backupSession := &formolv1alpha1.BackupSession{} + if err := r.Get(ctx, req.NamespacedName, backupSession); err != nil { log.Error(err, "unable to get backupsession") return ctrl.Result{}, client.IgnoreNotFound(err) } - log.V(0).Info("backupSession", "backupSession.ObjectMeta", r.BackupSession.ObjectMeta, "backupSession.Status", r.BackupSession.Status) - if r.BackupSession.Status.ObservedGeneration == r.BackupSession.ObjectMeta.Generation { - // status update - log.V(0).Info("status update") - return ctrl.Result{}, r.StatusUpdate() + 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") + return ctrl.Result{}, client.IgnoreNotFound(err) } - if r.IsBackupOngoing() { - // There is already a backup ongoing. We don't do anything and we reschedule - log.V(0).Info("there is an ongoing backup. let's reschedule this operation") - return ctrl.Result{RequeueAfter: 30 * time.Second}, nil - } else if r.BackupSession.ObjectMeta.DeletionTimestamp.IsZero() { - // Check if the finalizer has been registered - if !controllerutil.ContainsFinalizer(r.BackupSession, finalizerName) { - controllerutil.AddFinalizer(r.BackupSession, finalizerName) - // We update the BackupSession to add the finalizer - // Reconcile will be called again - // return now - err := r.Update(ctx, r.BackupSession) - if err != nil { - log.Error(err, "unable to add finalizer") - } - return ctrl.Result{}, err + + // helper functions + // is there a backup operation ongoing + isBackupOngoing := func() bool { + backupSessionList := &formolv1alpha1.BackupSessionList{} + if err := r.List(ctx, backupSessionList, client.InNamespace(backupConf.Namespace), client.MatchingFieldsSelector{Selector: fields.SelectorFromSet(fields.Set{sessionState: "Running"})}); err != nil { + log.Error(err, "unable to get backupsessionlist") + return true } - // All signals are green - // We start the backup process - r.BackupSession.Status.ObservedGeneration = r.BackupSession.ObjectMeta.Generation - r.BackupSession.Status.SessionState = formolv1alpha1.New - r.BackupSession.Status.StartTime = &metav1.Time{Time: time.Now()} - if err := r.Status().Update(ctx, r.BackupSession); err != nil { - log.Error(err, "unable to update backupSession") - return ctrl.Result{}, err + return len(backupSessionList.Items) > 0 + } + + // delete session specific backup resources + deleteExternalResources := func() error { + log := r.Log.WithValues("deleteExternalResources", backupSession.Name) + // Gather information from the repo + 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 + } + env := formolutils.ConfigureResticEnvVar(backupConf, repo) + // container that will delete the restic snapshot(s) matching the backupsession + deleteSnapshots := []corev1.Container{} + for _, target := range backupSession.Status.Targets { + if target.SessionState == formolv1alpha1.Success { + deleteSnapshots = append(deleteSnapshots, corev1.Container{ + Name: target.Name, + Image: "desmo999r/formolcli:latest", + Args: []string{"snapshot", "delete", "--snapshot-id", target.SnapshotId}, + Env: env, + }) + } + } + // create a job to delete the restic snapshot(s) with the backupsession name tag + if len(deleteSnapshots) > 0 { + jobTtl := JOBTTL + job := &batchv1.Job{ + ObjectMeta: metav1.ObjectMeta{ + GenerateName: fmt.Sprintf("delete-%s-", backupSession.Name), + Namespace: backupSession.Namespace, + }, + Spec: batchv1.JobSpec{ + TTLSecondsAfterFinished: &jobTtl, + Template: corev1.PodTemplateSpec{ + Spec: corev1.PodSpec{ + InitContainers: []corev1.Container{}, + Containers: deleteSnapshots, + RestartPolicy: corev1.RestartPolicyOnFailure, + }, + }, + }, + } + log.V(0).Info("creating a job to delete restic snapshots") + if err := r.Create(ctx, job); err != nil { + log.Error(err, "unable to delete job", "job", job) + return err + } + } + return nil + } + + // create a backup job + createBackupJob := func(target formolv1alpha1.Target) error { + log := r.Log.WithValues("createbackupjob", target.Name) + ctx := context.Background() + backupSessionEnv := []corev1.EnvVar{ + corev1.EnvVar{ + Name: "TARGET_NAME", + Value: target.Name, + }, + corev1.EnvVar{ + Name: "BACKUPSESSION_NAME", + Value: backupSession.Name, + }, + corev1.EnvVar{ + Name: "BACKUPSESSION_NAMESPACE", + Value: backupSession.Namespace, + }, } - } else { - log.V(0).Info("backupsession being deleted", "backupsession", r.BackupSession.Name) - if controllerutil.ContainsFinalizer(r.BackupSession, finalizerName) { - if err := r.deleteExternalResources(); err != nil { + output := corev1.VolumeMount{ + Name: "output", + MountPath: "/output", + } + restic := corev1.Container{ + Name: "restic", + Image: "desmo999r/formolcli:latest", + Args: []string{"volume", "backup", "--tag", backupSession.Name, "--path", "/output"}, + VolumeMounts: []corev1.VolumeMount{output}, + Env: backupSessionEnv, + } + log.V(1).Info("creating a tagged backup job", "container", restic) + // Gather information from the repo + 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 + restic.Env = append(restic.Env, formolutils.ConfigureResticEnvVar(backupConf, repo)...) + jobTtl := JOBTTL + job := &batchv1.Job{ + ObjectMeta: metav1.ObjectMeta{ + GenerateName: fmt.Sprintf("%s-%s-", backupSession.Name, target.Name), + Namespace: backupConf.Namespace, + }, + Spec: batchv1.JobSpec{ + TTLSecondsAfterFinished: &jobTtl, + Template: corev1.PodTemplateSpec{ + Spec: corev1.PodSpec{ + InitContainers: []corev1.Container{}, + Containers: []corev1.Container{restic}, + Volumes: []corev1.Volume{ + corev1.Volume{Name: "output"}, + }, + RestartPolicy: corev1.RestartPolicyOnFailure, + }, + }, + }, + } + for _, step := range target.Steps { + function := &formolv1alpha1.Function{} + if err := r.Get(ctx, client.ObjectKey{ + Namespace: backupConf.Namespace, + Name: step.Name, + }, function); err != nil { + log.Error(err, "unable to get function", "Function", step) + return err + } + function.Spec.Name = function.Name + function.Spec.Env = append(step.Env, backupSessionEnv...) + function.Spec.VolumeMounts = append(function.Spec.VolumeMounts, output) + job.Spec.Template.Spec.InitContainers = append(job.Spec.Template.Spec.InitContainers, function.Spec) + } + if err := ctrl.SetControllerReference(backupConf, job, r.Scheme); err != nil { + log.Error(err, "unable to set controller on job", "job", job, "backupconf", backupConf) + return err + } + log.V(0).Info("creating a backup job", "target", target) + if err := r.Create(ctx, job); err != nil { + log.Error(err, "unable to create job", "job", job) + return err + } + return nil + } + + // start the next task + startNextTask := func() (*formolv1alpha1.TargetStatus, error) { + nextTarget := len(backupSession.Status.Targets) + if nextTarget < len(backupConf.Spec.Targets) { + target := backupConf.Spec.Targets[nextTarget] + targetStatus := formolv1alpha1.TargetStatus{ + Name: target.Name, + Kind: target.Kind, + SessionState: formolv1alpha1.New, + StartTime: &metav1.Time{Time: time.Now()}, + Try: 1, + } + backupSession.Status.Targets = append(backupSession.Status.Targets, targetStatus) + switch target.Kind { + case formolv1alpha1.JobKind: + if err := createBackupJob(target); err != nil { + log.V(0).Info("unable to create task", "task", target) + targetStatus.SessionState = formolv1alpha1.Failure + return nil, err + } + } + return &targetStatus, nil + } else { + return nil, nil + } + } + + // cleanup existing backupsessions + cleanupSessions := func() { + backupSessionList := &formolv1alpha1.BackupSessionList{} + if err := r.List(ctx, backupSessionList, client.InNamespace(backupConf.Namespace), client.MatchingFieldsSelector{Selector: fields.SelectorFromSet(fields.Set{sessionState: string(formolv1alpha1.Success)})}); err != nil { + log.Error(err, "unable to get backupsessionlist") + return + } + if len(backupSessionList.Items) < 2 { + // Not enough backupSession to proceed + 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 != backupConf.Name { + continue + } + deleteSession := true + keep := []string{} + if lastBackups.Counter > 0 { + 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() { + 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() { + 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() { + 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() { + 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 { + 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 + } + } else { + session.Status.Keep = strings.Join(keep, ",") // + " " + time.Now().Format("2006 Jan 02 15:04:05 -0700 MST") + if err := r.Status().Update(ctx, &session); err != nil { + log.Error(err, "unable to update session status", "session", session) + } + } + } + } + // end helper functions + + log.V(0).Info("backupSession", "backupSession.ObjectMeta", backupSession.ObjectMeta, "backupSession.Status", backupSession.Status) + if backupSession.ObjectMeta.DeletionTimestamp.IsZero() { + switch backupSession.Status.SessionState { + case formolv1alpha1.New: + // Check if the finalizer has been registered + if !controllerutil.ContainsFinalizer(backupSession, finalizerName) { + controllerutil.AddFinalizer(backupSession, finalizerName) + // We update the BackupSession to add the finalizer + // Reconcile will be called again + // return now + err := r.Update(ctx, backupSession) + if err != nil { + log.Error(err, "unable to add finalizer") + } + return ctrl.Result{}, err + } + // Brand new backupsession + if isBackupOngoing() { + log.V(0).Info("There is an ongoing backup. Let's reschedule this operation") + return ctrl.Result{RequeueAfter: 30 * time.Second}, nil + } + // start the first task + backupSession.Status.SessionState = formolv1alpha1.Running + targetStatus, err := startNextTask() + if err != nil { + return ctrl.Result{}, err + } + log.V(0).Info("New backup. Start the first task", "task", targetStatus) + if err := r.Status().Update(ctx, backupSession); err != nil { + log.Error(err, "unable to update BackupSession status") + return ctrl.Result{}, err + } + case formolv1alpha1.Running: + // Backup ongoing. Check the status of the last task to decide what to do + currentTargetStatus := &backupSession.Status.Targets[len(backupSession.Status.Targets)-1] + switch currentTargetStatus.SessionState { + case formolv1alpha1.Running: + // The current task is still running. Nothing to do + log.V(0).Info("task is still running", "targetStatus", currentTargetStatus) + case formolv1alpha1.Success: + // The last task succeed. Let's try to start the next one + targetStatus, err := startNextTask() + log.V(0).Info("last task was a success. start a new one", "currentTargetStatus", currentTargetStatus, "targetStatus", targetStatus) + if err != nil { + return ctrl.Result{}, err + } + if targetStatus == nil { + // No more task to start. The backup is a success + backupSession.Status.SessionState = formolv1alpha1.Success + log.V(0).Info("Backup is successful. Let's try to do some cleanup") + cleanupSessions() + } + if err := r.Status().Update(ctx, backupSession); err != nil { + log.Error(err, "unable to update BackupSession status") + return ctrl.Result{}, err + } + case formolv1alpha1.Failure: + // last task failed. Try to run it again + currentTarget := backupConf.Spec.Targets[len(backupSession.Status.Targets)-1] + if currentTargetStatus.Try < currentTarget.Retry { + log.V(0).Info("last task was a failure. try again", "currentTargetStatus", currentTargetStatus) + currentTargetStatus.Try++ + currentTargetStatus.SessionState = formolv1alpha1.New + currentTargetStatus.StartTime = &metav1.Time{Time: time.Now()} + switch currentTarget.Kind { + case formolv1alpha1.JobKind: + if err := createBackupJob(currentTarget); err != nil { + log.V(0).Info("unable to create task", "task", currentTarget) + currentTargetStatus.SessionState = formolv1alpha1.Failure + return ctrl.Result{}, err + } + } + } else { + log.V(0).Info("task failed again and for the last time", "currentTargetStatus", currentTargetStatus) + backupSession.Status.SessionState = formolv1alpha1.Failure + } + if err := r.Status().Update(ctx, backupSession); err != nil { + log.Error(err, "unable to update BackupSession status") + return ctrl.Result{}, err + } + } + case formolv1alpha1.Success: + // Should never go there + case formolv1alpha1.Failure: + // The backup failed + case "": + // BackupSession has just been created + backupSession.Status.SessionState = formolv1alpha1.New + backupSession.Status.StartTime = &metav1.Time{Time: time.Now()} + if err := r.Status().Update(ctx, backupSession); err != nil { + log.Error(err, "unable to update backupSession") return ctrl.Result{}, err } } - controllerutil.RemoveFinalizer(r.BackupSession, finalizerName) - if err := r.Update(ctx, r.BackupSession); err != nil { + } else { + log.V(0).Info("backupsession being deleted", "backupsession", backupSession.Name) + if controllerutil.ContainsFinalizer(backupSession, finalizerName) { + if err := deleteExternalResources(); err != nil { + return ctrl.Result{}, err + } + } + controllerutil.RemoveFinalizer(backupSession, finalizerName) + if err := r.Update(ctx, backupSession); err != nil { log.Error(err, "unable to remove finalizer") return ctrl.Result{}, err } @@ -323,144 +456,6 @@ func (r *BackupSessionReconciler) Reconcile(req ctrl.Request) (ctrl.Result, erro return ctrl.Result{}, nil } -func (r *BackupSessionReconciler) CreateBackupJob(target formolv1alpha1.Target) error { - log := r.Log.WithValues("createbackupjob", target.Name) - ctx := context.Background() - backupSessionEnv := []corev1.EnvVar{ - corev1.EnvVar{ - Name: "TARGET_NAME", - Value: target.Name, - }, - corev1.EnvVar{ - Name: "BACKUPSESSION_NAME", - Value: r.BackupSession.Name, - }, - corev1.EnvVar{ - Name: "BACKUPSESSION_NAMESPACE", - Value: r.BackupSession.Namespace, - }, - } - - output := corev1.VolumeMount{ - Name: "output", - MountPath: "/output", - } - restic := corev1.Container{ - Name: "restic", - Image: "desmo999r/formolcli:latest", - Args: []string{"volume", "backup", "--tag", r.BackupSession.Name, "--path", "/output"}, - VolumeMounts: []corev1.VolumeMount{output}, - Env: backupSessionEnv, - } - log.V(1).Info("creating a tagged backup job", "container", restic) - // Gather information from the repo - repo := &formolv1alpha1.Repo{} - if err := r.Get(ctx, client.ObjectKey{ - Namespace: r.BackupConf.Namespace, - Name: r.BackupConf.Spec.Repository, - }, repo); err != nil { - log.Error(err, "unable to get Repo from BackupConfiguration") - return err - } - // S3 backing storage - restic.Env = append(restic.Env, formolutils.ConfigureResticEnvVar(r.BackupConf, repo)...) - jobTtl := JOBTTL - job := &batchv1.Job{ - ObjectMeta: metav1.ObjectMeta{ - GenerateName: fmt.Sprintf("%s-%s-", r.BackupSession.Name, target.Name), - Namespace: r.BackupConf.Namespace, - }, - Spec: batchv1.JobSpec{ - TTLSecondsAfterFinished: &jobTtl, - Template: corev1.PodTemplateSpec{ - Spec: corev1.PodSpec{ - InitContainers: []corev1.Container{}, - Containers: []corev1.Container{restic}, - Volumes: []corev1.Volume{ - corev1.Volume{Name: "output"}, - }, - RestartPolicy: corev1.RestartPolicyOnFailure, - }, - }, - }, - } - for _, step := range target.Steps { - function := &formolv1alpha1.Function{} - if err := r.Get(ctx, client.ObjectKey{ - Namespace: step.Namespace, - Name: step.Name}, function); err != nil { - log.Error(err, "unable to get function", "Function", step) - return err - } - function.Spec.Env = append(step.Env, backupSessionEnv...) - function.Spec.VolumeMounts = append(function.Spec.VolumeMounts, output) - job.Spec.Template.Spec.InitContainers = append(job.Spec.Template.Spec.InitContainers, function.Spec) - } - if err := ctrl.SetControllerReference(r.BackupConf, job, r.Scheme); err != nil { - log.Error(err, "unable to set controller on job", "job", job, "backupconf", r.BackupConf) - return err - } - log.V(0).Info("creating a backup job", "target", target) - if err := r.Create(ctx, job); err != nil { - log.Error(err, "unable to create job", "job", job) - return err - } - return nil -} - -func (r *BackupSessionReconciler) deleteExternalResources() error { - ctx := context.Background() - log := r.Log.WithValues("deleteExternalResources", r.BackupSession.Name) - // Gather information from the repo - repo := &formolv1alpha1.Repo{} - if err := r.Get(ctx, client.ObjectKey{ - Namespace: r.BackupConf.Namespace, - Name: r.BackupConf.Spec.Repository, - }, repo); err != nil { - log.Error(err, "unable to get Repo from BackupConfiguration") - return err - } - env := formolutils.ConfigureResticEnvVar(r.BackupConf, repo) - // container that will delete the restic snapshot(s) matching the backupsession - deleteSnapshots := []corev1.Container{} - for _, target := range r.BackupSession.Status.Targets { - if target.SessionState == formolv1alpha1.Success { - deleteSnapshots = append(deleteSnapshots, corev1.Container{ - Name: target.Name, - Image: "desmo999r/formolcli:latest", - Args: []string{"snapshot", "delete", "--snapshot-id", target.SnapshotId}, - Env: env, - }) - } - } - // create a job to delete the restic snapshot(s) with the backupsession name tag - if len(deleteSnapshots) > 0 { - jobTtl := JOBTTL - job := &batchv1.Job{ - ObjectMeta: metav1.ObjectMeta{ - GenerateName: fmt.Sprintf("delete-%s-", r.BackupSession.Name), - Namespace: r.BackupSession.Namespace, - }, - Spec: batchv1.JobSpec{ - TTLSecondsAfterFinished: &jobTtl, - Template: corev1.PodTemplateSpec{ - Spec: corev1.PodSpec{ - InitContainers: []corev1.Container{}, - Containers: deleteSnapshots, - RestartPolicy: corev1.RestartPolicyOnFailure, - }, - }, - }, - } - log.V(0).Info("creating a job to delete restic snapshots") - if err := r.Create(ctx, job); err != nil { - log.Error(err, "unable to delete job", "job", job) - 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) diff --git a/controllers/backupsession_controller_test.go b/controllers/backupsession_controller_test.go new file mode 100644 index 0000000..f26402a --- /dev/null +++ b/controllers/backupsession_controller_test.go @@ -0,0 +1,144 @@ +package controllers + +import ( + "context" + 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" +) + +var _ = Describe("Testing BackupSession controller", func() { + const ( + BSBackupSessionName = "test-backupsession-controller" + ) + var ( + ctx = context.Background() + key = types.NamespacedName{ + Name: BSBackupSessionName, + Namespace: TestNamespace, + } + backupSession = &formolv1alpha1.BackupSession{} + ) + BeforeEach(func() { + backupSession = &formolv1alpha1.BackupSession{ + ObjectMeta: metav1.ObjectMeta{ + Name: BSBackupSessionName, + Namespace: TestNamespace, + }, + Spec: formolv1alpha1.BackupSessionSpec{ + Ref: TestBackupConfName, + }, + } + }) + Context("Creating a backupsession", func() { + JustBeforeEach(func() { + Eventually(func() error { + return k8sClient.Create(ctx, backupSession) + }, timeout, interval).Should(Succeed()) + realBackupSession := &formolv1alpha1.BackupSession{} + Eventually(func() error { + err := k8sClient.Get(ctx, key, realBackupSession) + return err + }, timeout, interval).Should(Succeed()) + Eventually(func() formolv1alpha1.SessionState { + if err := k8sClient.Get(ctx, key, realBackupSession); err != nil { + return "" + } else { + return realBackupSession.Status.SessionState + } + }, timeout, interval).Should(Equal(formolv1alpha1.Running)) + }) + AfterEach(func() { + Expect(k8sClient.Delete(ctx, backupSession)).Should(Succeed()) + }) + + It("Should have a new task", func() { + realBackupSession := &formolv1alpha1.BackupSession{} + _ = k8sClient.Get(ctx, key, realBackupSession) + Expect(realBackupSession.Status.Targets[0].Name).Should(Equal(TestDeploymentName)) + Expect(realBackupSession.Status.Targets[0].SessionState).Should(Equal(formolv1alpha1.New)) + Expect(realBackupSession.Status.Targets[0].Kind).Should(Equal(formolv1alpha1.SidecarKind)) + Expect(realBackupSession.Status.Targets[0].Try).Should(Equal(1)) + }) + + It("Should move to the next task when the first one is a success", func() { + realBackupSession := &formolv1alpha1.BackupSession{} + Expect(k8sClient.Get(ctx, key, realBackupSession)).Should(Succeed()) + realBackupSession.Status.Targets[0].SessionState = formolv1alpha1.Success + Expect(k8sClient.Status().Update(ctx, realBackupSession)).Should(Succeed()) + Eventually(func() int { + _ = k8sClient.Get(ctx, key, realBackupSession) + return len(realBackupSession.Status.Targets) + }, timeout, interval).Should(Equal(2)) + Expect(k8sClient.Get(ctx, key, realBackupSession)).Should(Succeed()) + Expect(realBackupSession.Status.Targets[1].Name).Should(Equal(TestBackupFuncName)) + Expect(realBackupSession.Status.Targets[1].SessionState).Should(Equal(formolv1alpha1.New)) + Expect(realBackupSession.Status.Targets[1].Kind).Should(Equal(formolv1alpha1.JobKind)) + }) + + It("Should be a success when the last task is a success", func() { + realBackupSession := &formolv1alpha1.BackupSession{} + Expect(k8sClient.Get(ctx, key, realBackupSession)).Should(Succeed()) + realBackupSession.Status.Targets[0].SessionState = formolv1alpha1.Success + Expect(k8sClient.Status().Update(ctx, realBackupSession)).Should(Succeed()) + Eventually(func() int { + _ = k8sClient.Get(ctx, key, realBackupSession) + return len(realBackupSession.Status.Targets) + }, timeout, interval).Should(Equal(2)) + Expect(k8sClient.Get(ctx, key, realBackupSession)).Should(Succeed()) + realBackupSession.Status.Targets[1].SessionState = formolv1alpha1.Success + Expect(k8sClient.Status().Update(ctx, realBackupSession)).Should(Succeed()) + Expect(k8sClient.Get(ctx, key, realBackupSession)).Should(Succeed()) + Eventually(func() formolv1alpha1.SessionState { + _ = k8sClient.Get(ctx, key, realBackupSession) + return realBackupSession.Status.SessionState + }, timeout, interval).Should(Equal(formolv1alpha1.Success)) + }) + + It("Should retry when the task is a failure", func() { + realBackupSession := &formolv1alpha1.BackupSession{} + Expect(k8sClient.Get(ctx, key, realBackupSession)).Should(Succeed()) + realBackupSession.Status.Targets[0].SessionState = formolv1alpha1.Success + Expect(k8sClient.Status().Update(ctx, realBackupSession)).Should(Succeed()) + Eventually(func() int { + _ = k8sClient.Get(ctx, key, realBackupSession) + return len(realBackupSession.Status.Targets) + }, timeout, interval).Should(Equal(2)) + Expect(k8sClient.Get(ctx, key, realBackupSession)).Should(Succeed()) + realBackupSession.Status.Targets[1].SessionState = formolv1alpha1.Failure + Expect(k8sClient.Status().Update(ctx, realBackupSession)).Should(Succeed()) + Eventually(func() int { + _ = k8sClient.Get(ctx, key, realBackupSession) + return realBackupSession.Status.Targets[1].Try + }, timeout, interval).Should(Equal(2)) + Expect(k8sClient.Get(ctx, key, realBackupSession)).Should(Succeed()) + Expect(realBackupSession.Status.Targets[1].SessionState).Should(Equal(formolv1alpha1.New)) + realBackupSession.Status.Targets[1].SessionState = formolv1alpha1.Failure + Expect(k8sClient.Status().Update(ctx, realBackupSession)).Should(Succeed()) + Eventually(func() formolv1alpha1.SessionState { + _ = k8sClient.Get(ctx, key, realBackupSession) + return realBackupSession.Status.SessionState + }, timeout, interval).Should(Equal(formolv1alpha1.Failure)) + }) + + It("should create a backup job", func() { + }) + }) + Context("When other BackupSession exist", func() { + const ( + bs1Name = "test-backupsession-controller1" + bs2Name = "test-backupsession-controller2" + bs3Name = "test-backupsession-controller3" + ) + var () + BeforeEach(func() { + }) + JustBeforeEach(func() { + }) + It("Should clean up old sessions", func() { + }) + }) +}) diff --git a/controllers/restoresession_controller.go b/controllers/restoresession_controller.go index 8f5648f..042f352 100644 --- a/controllers/restoresession_controller.go +++ b/controllers/restoresession_controller.go @@ -43,224 +43,259 @@ const ( // RestoreSessionReconciler reconciles a RestoreSession object type RestoreSessionReconciler struct { client.Client - Log logr.Logger - Scheme *runtime.Scheme - RestoreSession *formolv1alpha1.RestoreSession - BackupSession *formolv1alpha1.BackupSession - BackupConf *formolv1alpha1.BackupConfiguration + Log logr.Logger + Scheme *runtime.Scheme } -func (r *RestoreSessionReconciler) CreateRestoreJob(target formolv1alpha1.Target) error { - log := r.Log.WithValues("createrestorejob", target.Name) +// +kubebuilder:rbac:groups=formol.desmojim.fr,resources=restoresessions,verbs=get;list;watch;create;update;patch;delete +// +kubebuilder:rbac:groups=formol.desmojim.fr,resources=restoresessions/status,verbs=get;update;patch + +func (r *RestoreSessionReconciler) Reconcile(req ctrl.Request) (ctrl.Result, error) { ctx := context.Background() - restoreSessionEnv := []corev1.EnvVar{ - corev1.EnvVar{ - Name: "TARGET_NAME", - Value: target.Name, - }, - corev1.EnvVar{ - Name: "RESTORESESSION_NAME", - Value: r.RestoreSession.Name, - }, - corev1.EnvVar{ - Name: "RESTORESESSION_NAMESPACE", - Value: r.RestoreSession.Namespace, - }, + log := r.Log.WithValues("restoresession", req.NamespacedName) + + // Get the RestoreSession + restoreSession := &formolv1alpha1.RestoreSession{} + if err := r.Get(ctx, req.NamespacedName, restoreSession); err != nil { + log.Error(err, "unable to get restoresession") + return ctrl.Result{}, client.IgnoreNotFound(err) + } + // 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) + } + // 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") + return ctrl.Result{}, client.IgnoreNotFound(err) } - output := corev1.VolumeMount{ - Name: "output", - MountPath: "/output", - } - for _, targetStatus := range r.BackupSession.Status.Targets { - if targetStatus.Name == target.Name { - snapshotId := targetStatus.SnapshotId - restic := corev1.Container{ - Name: "restic", - Image: "desmo999r/formolcli:latest", - Args: []string{"volume", "restore", "--snapshot-id", snapshotId}, - VolumeMounts: []corev1.VolumeMount{output}, - Env: restoreSessionEnv, - } - finalizer := corev1.Container{ - Name: "finalizer", - Image: "desmo999r/formolcli:latest", - Args: []string{"target", "finalize"}, - VolumeMounts: []corev1.VolumeMount{output}, - Env: restoreSessionEnv, - } - repo := &formolv1alpha1.Repo{} - if err := r.Get(ctx, client.ObjectKey{ - Namespace: r.BackupConf.Namespace, - Name: r.BackupConf.Spec.Repository, - }, repo); err != nil { - log.Error(err, "unable to get Repo from BackupConfiguration") - return err - } - // S3 backing storage - var ttl int32 = 300 - restic.Env = append(restic.Env, formolutils.ConfigureResticEnvVar(r.BackupConf, repo)...) - job := &batchv1.Job{ - ObjectMeta: metav1.ObjectMeta{ - GenerateName: fmt.Sprintf("%s-%s-", r.RestoreSession.Name, target.Name), - Namespace: r.RestoreSession.Namespace, - }, - Spec: batchv1.JobSpec{ - TTLSecondsAfterFinished: &ttl, - Template: corev1.PodTemplateSpec{ - Spec: corev1.PodSpec{ - InitContainers: []corev1.Container{restic}, - Containers: []corev1.Container{finalizer}, - Volumes: []corev1.Volume{ - corev1.Volume{Name: "output"}, - }, - RestartPolicy: corev1.RestartPolicyOnFailure, - }, - }, - }, - } - for _, step := range target.Steps { - function := &formolv1alpha1.Function{} + // Helper functions + createRestoreJob := func(target formolv1alpha1.Target) error { + restoreSessionEnv := []corev1.EnvVar{ + corev1.EnvVar{ + Name: "TARGET_NAME", + Value: target.Name, + }, + corev1.EnvVar{ + Name: "RESTORESESSION_NAME", + Value: restoreSession.Name, + }, + corev1.EnvVar{ + Name: "RESTORESESSION_NAMESPACE", + Value: restoreSession.Namespace, + }, + } + + output := corev1.VolumeMount{ + Name: "output", + MountPath: "/output", + } + for _, targetStatus := range backupSession.Status.Targets { + if targetStatus.Name == target.Name { + snapshotId := targetStatus.SnapshotId + restic := corev1.Container{ + Name: "restic", + Image: "desmo999r/formolcli:latest", + Args: []string{"volume", "restore", "--snapshot-id", snapshotId}, + VolumeMounts: []corev1.VolumeMount{output}, + Env: restoreSessionEnv, + } + finalizer := corev1.Container{ + Name: "finalizer", + Image: "desmo999r/formolcli:latest", + Args: []string{"target", "finalize"}, + VolumeMounts: []corev1.VolumeMount{output}, + Env: restoreSessionEnv, + } + repo := &formolv1alpha1.Repo{} if err := r.Get(ctx, client.ObjectKey{ - Namespace: r.RestoreSession.Namespace, - Name: strings.Replace(step.Name, "backup", "restore", 1)}, function); err != nil { - log.Error(err, "unable to get function", "function", step) + Namespace: backupConf.Namespace, + Name: backupConf.Spec.Repository, + }, repo); err != nil { + log.Error(err, "unable to get Repo from BackupConfiguration") + return err + } + // S3 backing storage + var ttl int32 = 300 + restic.Env = append(restic.Env, formolutils.ConfigureResticEnvVar(backupConf, repo)...) + job := &batchv1.Job{ + ObjectMeta: metav1.ObjectMeta{ + GenerateName: fmt.Sprintf("%s-%s-", restoreSession.Name, target.Name), + Namespace: restoreSession.Namespace, + }, + Spec: batchv1.JobSpec{ + TTLSecondsAfterFinished: &ttl, + Template: corev1.PodTemplateSpec{ + Spec: corev1.PodSpec{ + InitContainers: []corev1.Container{restic}, + Containers: []corev1.Container{finalizer}, + Volumes: []corev1.Volume{ + corev1.Volume{Name: "output"}, + }, + RestartPolicy: corev1.RestartPolicyOnFailure, + }, + }, + }, + } + for _, step := range target.Steps { + function := &formolv1alpha1.Function{} + // get the backup function + if err := r.Get(ctx, client.ObjectKey{ + Namespace: restoreSession.Namespace, + Name: step.Name, + }, function); err != nil { + log.Error(err, "unable to get backup function", "name", step.Name) + return err + } + var restoreName string + if function.Annotations["restoreFunction"] != "" { + restoreName = function.Annotations["restoreFunction"] + } else { + restoreName = strings.Replace(step.Name, "backup", "restore", 1) + } + if err := r.Get(ctx, client.ObjectKey{ + Namespace: restoreSession.Namespace, + Name: restoreName, + }, function); err != nil { + log.Error(err, "unable to get function", "function", step) + return err + } + function.Spec.Name = function.Name + function.Spec.Env = append(step.Env, restoreSessionEnv...) + function.Spec.VolumeMounts = append(function.Spec.VolumeMounts, output) + job.Spec.Template.Spec.InitContainers = append(job.Spec.Template.Spec.InitContainers, function.Spec) + } + if err := ctrl.SetControllerReference(restoreSession, job, r.Scheme); err != nil { + log.Error(err, "unable to set controller on job", "job", job, "restoresession", restoreSession) + return err + } + log.V(0).Info("creating a restore job", "target", target.Name) + if err := r.Create(ctx, job); err != nil { + log.Error(err, "unable to create job", "job", job) return err } - function.Spec.Env = append(step.Env, restoreSessionEnv...) - function.Spec.VolumeMounts = append(function.Spec.VolumeMounts, output) - job.Spec.Template.Spec.InitContainers = append(job.Spec.Template.Spec.InitContainers, function.Spec) - } - if err := ctrl.SetControllerReference(r.RestoreSession, job, r.Scheme); err != nil { - log.Error(err, "unable to set controller on job", "job", job, "restoresession", r.RestoreSession) - return err - } - log.V(0).Info("creating a restore job", "target", target.Name) - if err := r.Create(ctx, job); err != nil { - log.Error(err, "unable to create job", "job", job) - return err } } + return nil } - return nil -} -func (r *RestoreSessionReconciler) DeleteRestoreInitContainer(target formolv1alpha1.Target) error { - log := r.Log.WithValues("createrestoreinitcontainer", target.Name) - ctx := context.Background() - deployment := &appsv1.Deployment{} - if err := r.Get(context.Background(), client.ObjectKey{ - Namespace: r.BackupConf.Namespace, - Name: target.Name, - }, deployment); err != nil { - log.Error(err, "unable to get deployment") - return err - } - log.V(1).Info("got deployment", "namespace", deployment.Namespace, "name", deployment.Name) - newInitContainers := []corev1.Container{} - for _, initContainer := range deployment.Spec.Template.Spec.InitContainers { - if initContainer.Name == RESTORESESSION { - log.V(0).Info("Found our restoresession container. Removing it from the list of init containers", "container", initContainer) - } else { - newInitContainers = append(newInitContainers, initContainer) + deleteRestoreInitContainer := func(target formolv1alpha1.Target) error { + deployment := &appsv1.Deployment{} + if err := r.Get(context.Background(), client.ObjectKey{ + Namespace: backupConf.Namespace, + Name: target.Name, + }, deployment); err != nil { + log.Error(err, "unable to get deployment") + return err } - } - deployment.Spec.Template.Spec.InitContainers = newInitContainers - if err := r.Update(ctx, deployment); err != nil { - log.Error(err, "unable to update deployment") - return err - } - return nil -} - -func (r *RestoreSessionReconciler) CreateRestoreInitContainer(target formolv1alpha1.Target) error { - log := r.Log.WithValues("createrestoreinitcontainer", target.Name) - ctx := context.Background() - deployment := &appsv1.Deployment{} - if err := r.Get(context.Background(), client.ObjectKey{ - Namespace: r.RestoreSession.Namespace, - Name: target.Name, - }, deployment); err != nil { - log.Error(err, "unable to get deployment") - return err - } - log.V(1).Info("got deployment", "namespace", deployment.Namespace, "name", deployment.Name) - for _, initContainer := range deployment.Spec.Template.Spec.InitContainers { - if initContainer.Name == RESTORESESSION { - log.V(0).Info("there is already a restoresession initcontainer", "deployment", deployment.Spec.Template.Spec.InitContainers) - return nil + log.V(1).Info("got deployment", "namespace", deployment.Namespace, "name", deployment.Name) + newInitContainers := []corev1.Container{} + for _, initContainer := range deployment.Spec.Template.Spec.InitContainers { + if initContainer.Name == RESTORESESSION { + log.V(0).Info("Found our restoresession container. Removing it from the list of init containers", "container", initContainer) + } else { + newInitContainers = append(newInitContainers, initContainer) + } } - } - var snapshotId string - for _, targetStatus := range r.BackupSession.Status.Targets { - if targetStatus.Name == target.Name && targetStatus.Kind == target.Kind { - snapshotId = targetStatus.SnapshotId + deployment.Spec.Template.Spec.InitContainers = newInitContainers + if err := r.Update(ctx, deployment); err != nil { + log.Error(err, "unable to update deployment") + return err } - } - restoreSessionEnv := []corev1.EnvVar{ - corev1.EnvVar{ - Name: formolv1alpha1.TARGET_NAME, - Value: target.Name, - }, - corev1.EnvVar{ - Name: formolv1alpha1.RESTORESESSION_NAME, - Value: r.RestoreSession.Name, - }, - corev1.EnvVar{ - Name: formolv1alpha1.RESTORESESSION_NAMESPACE, - Value: r.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: r.BackupConf.Namespace, - Name: r.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(r.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 } - return nil -} + createRestoreInitContainer := func(target formolv1alpha1.Target) error { + 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 err + } + log.V(1).Info("got deployment", "namespace", deployment.Namespace, "name", deployment.Name) + for _, initContainer := range deployment.Spec.Template.Spec.InitContainers { + if initContainer.Name == RESTORESESSION { + log.V(0).Info("there is already a restoresession initcontainer", "deployment", deployment.Spec.Template.Spec.InitContainers) + return nil + } + } + var snapshotId string + for _, targetStatus := range backupSession.Status.Targets { + if targetStatus.Name == target.Name && targetStatus.Kind == target.Kind { + 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 + } -func (r *RestoreSessionReconciler) StatusUpdate() error { - log := r.Log.WithValues("statusupdate", r.RestoreSession.Name) - ctx := context.Background() startNextTask := func() (*formolv1alpha1.TargetStatus, error) { - nextTarget := len(r.RestoreSession.Status.Targets) - if nextTarget < len(r.BackupConf.Spec.Targets) { - target := r.BackupConf.Spec.Targets[nextTarget] + nextTarget := len(restoreSession.Status.Targets) + if nextTarget < len(backupConf.Spec.Targets) { + target := backupConf.Spec.Targets[nextTarget] targetStatus := formolv1alpha1.TargetStatus{ Name: target.Name, Kind: target.Kind, SessionState: formolv1alpha1.New, StartTime: &metav1.Time{Time: time.Now()}, } - r.RestoreSession.Status.Targets = append(r.RestoreSession.Status.Targets, targetStatus) + restoreSession.Status.Targets = append(restoreSession.Status.Targets, targetStatus) switch target.Kind { - case "Deployment": - if err := r.CreateRestoreInitContainer(target); err != nil { + case formolv1alpha1.SidecarKind: + if err := createRestoreInitContainer(target); err != nil { log.V(0).Info("unable to create restore init container", "task", target) targetStatus.SessionState = formolv1alpha1.Failure return nil, err } - case "Task": - if err := r.CreateRestoreJob(target); err != nil { + case formolv1alpha1.JobKind: + if err := createRestoreJob(target); err != nil { log.V(0).Info("unable to create restore job", "task", target) targetStatus.SessionState = formolv1alpha1.Failure return nil, err @@ -271,110 +306,71 @@ func (r *RestoreSessionReconciler) StatusUpdate() error { return nil, nil } } + endTask := func() error { - target := r.BackupConf.Spec.Targets[len(r.RestoreSession.Status.Targets)-1] + target := backupConf.Spec.Targets[len(restoreSession.Status.Targets)-1] switch target.Kind { - case "Deployment": - if err := r.DeleteRestoreInitContainer(target); err != nil { + case formolv1alpha1.SidecarKind: + if err := deleteRestoreInitContainer(target); err != nil { log.Error(err, "unable to delete restore init container") return err } } return nil } - switch r.RestoreSession.Status.SessionState { + + switch restoreSession.Status.SessionState { case formolv1alpha1.New: - r.RestoreSession.Status.SessionState = formolv1alpha1.Running - targetStatus, err := startNextTask() - if err != nil { - return err - } - log.V(0).Info("New restore. Start the first task", "task", targetStatus.Name) - if err := r.Status().Update(ctx, r.RestoreSession); err != nil { - log.Error(err, "unable to update restoresession") - return err + restoreSession.Status.SessionState = formolv1alpha1.Running + if targetStatus, err := startNextTask(); err != nil { + log.Error(err, "unable to start next restore task") + return ctrl.Result{}, err + } else { + log.V(0).Info("New restore. Start the first task", "task", targetStatus.Name) + if err := r.Status().Update(ctx, restoreSession); err != nil { + log.Error(err, "unable to update restoresession") + return ctrl.Result{}, err + } } case formolv1alpha1.Running: - currentTargetStatus := r.RestoreSession.Status.Targets[len(r.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) - r.RestoreSession.Status.SessionState = formolv1alpha1.Failure - if err := r.Status().Update(ctx, r.RestoreSession); err != nil { + restoreSession.Status.SessionState = formolv1alpha1.Failure + if err := r.Status().Update(ctx, restoreSession); err != nil { log.Error(err, "unable to update restoresession") - return err + return ctrl.Result{}, err } case formolv1alpha1.Running: log.V(0).Info("task is still running", "target", currentTargetStatus.Name) - return nil + return ctrl.Result{}, nil case formolv1alpha1.Success: _ = endTask() log.V(0).Info("last task was a success. start a new one", "target", currentTargetStatus) targetStatus, err := startNextTask() if err != nil { - return err + return ctrl.Result{}, err } if targetStatus == nil { // No more task to start. The restore is over - r.RestoreSession.Status.SessionState = formolv1alpha1.Success - if err := r.Status().Update(ctx, r.RestoreSession); err != nil { - log.Error(err, "unable to update restoresession") - return err - } + restoreSession.Status.SessionState = formolv1alpha1.Success } - if err := r.Status().Update(ctx, r.RestoreSession); err != nil { + if err := r.Status().Update(ctx, restoreSession); err != nil { log.Error(err, "unable to update restoresession") - return err + return ctrl.Result{}, err } } + case "": + // Restore session has just been created + restoreSession.Status.SessionState = formolv1alpha1.New + restoreSession.Status.StartTime = &metav1.Time{Time: time.Now()} + if err := r.Status().Update(ctx, restoreSession); err != nil { + log.Error(err, "unable to update restoreSession") + return ctrl.Result{}, err + } } - return nil -} - -// +kubebuilder:rbac:groups=formol.desmojim.fr,resources=restoresessions,verbs=get;list;watch;create;update;patch;delete -// +kubebuilder:rbac:groups=formol.desmojim.fr,resources=restoresessions/status,verbs=get;update;patch - -func (r *RestoreSessionReconciler) Reconcile(req ctrl.Request) (ctrl.Result, error) { - time.Sleep(500 * time.Millisecond) - ctx := context.Background() - log := r.Log.WithValues("restoresession", req.NamespacedName) - - r.RestoreSession = &formolv1alpha1.RestoreSession{} - if err := r.Get(ctx, req.NamespacedName, r.RestoreSession); err != nil { - log.Error(err, "unable to get restoresession") - return ctrl.Result{}, client.IgnoreNotFound(err) - } - r.BackupSession = &formolv1alpha1.BackupSession{} - if err := r.Get(ctx, client.ObjectKey{ - Namespace: r.RestoreSession.Spec.BackupSessionRef.Namespace, - Name: r.RestoreSession.Spec.BackupSessionRef.Name}, r.BackupSession); err != nil { - log.Error(err, "unable to get backupsession") - return ctrl.Result{}, client.IgnoreNotFound(err) - } - r.BackupConf = &formolv1alpha1.BackupConfiguration{} - if err := r.Get(ctx, client.ObjectKey{ - Namespace: r.BackupSession.Namespace, - Name: r.BackupSession.Spec.Ref.Name}, r.BackupConf); err != nil { - log.Error(err, "unable to get backupConfiguration") - return ctrl.Result{}, client.IgnoreNotFound(err) - } - - if r.RestoreSession.Status.ObservedGeneration == r.RestoreSession.ObjectMeta.Generation { - // status update - log.V(0).Info("status update") - return ctrl.Result{}, r.StatusUpdate() - } - r.RestoreSession.Status.ObservedGeneration = r.RestoreSession.ObjectMeta.Generation - r.RestoreSession.Status.SessionState = formolv1alpha1.New - r.RestoreSession.Status.StartTime = &metav1.Time{Time: time.Now()} - reschedule := ctrl.Result{RequeueAfter: 5 * time.Second} - - if err := r.Status().Update(ctx, r.RestoreSession); err != nil { - log.Error(err, "unable to update restoresession") - return ctrl.Result{}, err - } - - return reschedule, nil + return ctrl.Result{}, nil } func (r *RestoreSessionReconciler) SetupWithManager(mgr ctrl.Manager) error { diff --git a/controllers/restoresession_controller_test.go b/controllers/restoresession_controller_test.go new file mode 100644 index 0000000..0d073d4 --- /dev/null +++ b/controllers/restoresession_controller_test.go @@ -0,0 +1,90 @@ +package controllers + +import ( + "context" + formolv1alpha1 "github.com/desmo999r/formol/api/v1alpha1" + . "github.com/onsi/ginkgo" + . "github.com/onsi/gomega" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/types" +) + +var _ = Describe("Testing RestoreSession controller", func() { + const ( + RSRestoreSessionName = "test-restoresession-controller" + ) + var ( + ctx = context.Background() + key = types.NamespacedName{ + Name: RSRestoreSessionName, + Namespace: TestNamespace, + } + restoreSession = &formolv1alpha1.RestoreSession{} + ) + BeforeEach(func() { + restoreSession = &formolv1alpha1.RestoreSession{ + ObjectMeta: metav1.ObjectMeta{ + Name: RSRestoreSessionName, + Namespace: TestNamespace, + }, + Spec: formolv1alpha1.RestoreSessionSpec{ + Ref: TestBackupSessionName, + }, + } + }) + Context("Creating a RestoreSession", func() { + JustBeforeEach(func() { + Eventually(func() error { + return k8sClient.Create(ctx, restoreSession) + }, timeout, interval).Should(Succeed()) + realRestoreSession := &formolv1alpha1.RestoreSession{} + Eventually(func() error { + return k8sClient.Get(ctx, key, realRestoreSession) + }, timeout, interval).Should(Succeed()) + Eventually(func() formolv1alpha1.SessionState { + _ = k8sClient.Get(ctx, key, realRestoreSession) + return realRestoreSession.Status.SessionState + }, timeout, interval).Should(Equal(formolv1alpha1.Running)) + }) + AfterEach(func() { + Expect(k8sClient.Delete(ctx, restoreSession)).Should(Succeed()) + }) + It("Should have a new task and should fail if the task fails", func() { + restoreSession := &formolv1alpha1.RestoreSession{} + Expect(k8sClient.Get(ctx, key, restoreSession)).Should(Succeed()) + Expect(len(restoreSession.Status.Targets)).Should(Equal(1)) + Expect(restoreSession.Status.Targets[0].SessionState).Should(Equal(formolv1alpha1.New)) + restoreSession.Status.Targets[0].SessionState = formolv1alpha1.Running + Expect(k8sClient.Status().Update(ctx, restoreSession)).Should(Succeed()) + Expect(k8sClient.Get(ctx, key, restoreSession)).Should(Succeed()) + Eventually(func() formolv1alpha1.SessionState { + _ = k8sClient.Get(ctx, key, restoreSession) + return restoreSession.Status.Targets[0].SessionState + }, timeout, interval).Should(Equal(formolv1alpha1.Running)) + restoreSession.Status.Targets[0].SessionState = formolv1alpha1.Failure + Expect(k8sClient.Status().Update(ctx, restoreSession)).Should(Succeed()) + Expect(k8sClient.Get(ctx, key, restoreSession)).Should(Succeed()) + Eventually(func() formolv1alpha1.SessionState { + _ = k8sClient.Get(ctx, key, restoreSession) + return restoreSession.Status.SessionState + }, timeout, interval).Should(Equal(formolv1alpha1.Failure)) + }) + It("Should move to the new task if the first one is a success and be a success if all the tasks succeed", func() { + restoreSession := &formolv1alpha1.RestoreSession{} + Expect(k8sClient.Get(ctx, key, restoreSession)).Should(Succeed()) + Expect(len(restoreSession.Status.Targets)).Should(Equal(1)) + restoreSession.Status.Targets[0].SessionState = formolv1alpha1.Success + Expect(k8sClient.Status().Update(ctx, restoreSession)).Should(Succeed()) + Eventually(func() int { + _ = k8sClient.Get(ctx, key, restoreSession) + return len(restoreSession.Status.Targets) + }, timeout, interval).Should(Equal(2)) + restoreSession.Status.Targets[1].SessionState = formolv1alpha1.Success + Expect(k8sClient.Status().Update(ctx, restoreSession)).Should(Succeed()) + Eventually(func() formolv1alpha1.SessionState { + _ = k8sClient.Get(ctx, key, restoreSession) + return restoreSession.Status.SessionState + }, timeout, interval).Should(Equal(formolv1alpha1.Success)) + }) + }) +}) diff --git a/controllers/suite_test.go b/controllers/suite_test.go index 520f310..0370d57 100644 --- a/controllers/suite_test.go +++ b/controllers/suite_test.go @@ -44,12 +44,18 @@ import ( // These tests use Ginkgo (BDD-style Go testing framework). Refer to // http://onsi.github.io/ginkgo/ to learn more about Ginkgo. const ( - BackupFuncName = "test-backup-func" - TestNamespace = "test-namespace" - RepoName = "test-repo" - DeploymentName = "test-deployment" - timeout = time.Second * 10 - interval = time.Millisecond * 250 + TestBackupFuncName = "test-backup-func" + TestFunc = "test-norestore-func" + TestRestoreFuncName = "test-restore-func" + TestNamespace = "test-namespace" + TestRepoName = "test-repo" + TestDeploymentName = "test-deployment" + TestBackupConfName = "test-backupconf" + TestBackupSessionName = "test-backupsession" + TestDataVolume = "data" + TestDataMountPath = "/data" + timeout = time.Second * 10 + interval = time.Millisecond * 250 ) var cfg *rest.Config @@ -64,7 +70,7 @@ var ( } deployment = &appsv1.Deployment{ ObjectMeta: metav1.ObjectMeta{ - Name: DeploymentName, + Name: TestDeploymentName, Namespace: TestNamespace, }, Spec: appsv1.DeploymentSpec{ @@ -82,6 +88,11 @@ var ( Image: "test-image", }, }, + Volumes: []corev1.Volume{ + corev1.Volume{ + Name: TestDataVolume, + }, + }, }, }, }, @@ -105,7 +116,7 @@ var ( } repo = &formolv1alpha1.Repo{ ObjectMeta: metav1.ObjectMeta{ - Name: RepoName, + Name: TestRepoName, Namespace: TestNamespace, }, Spec: formolv1alpha1.RepoSpec{ @@ -120,7 +131,29 @@ var ( } function = &formolv1alpha1.Function{ ObjectMeta: metav1.ObjectMeta{ - Name: BackupFuncName, + Name: TestFunc, + Namespace: TestNamespace, + }, + Spec: corev1.Container{ + Name: "norestore-func", + Image: "myimage", + Args: []string{"a", "set", "of", "args"}, + }, + } + backupFunc = &formolv1alpha1.Function{ + ObjectMeta: metav1.ObjectMeta{ + Name: TestRestoreFuncName, + Namespace: TestNamespace, + }, + Spec: corev1.Container{ + Name: "restore-func", + Image: "myimage", + Args: []string{"a", "set", "of", "args"}, + }, + } + restoreFunc = &formolv1alpha1.Function{ + ObjectMeta: metav1.ObjectMeta{ + Name: TestBackupFuncName, Namespace: TestNamespace, }, Spec: corev1.Container{ @@ -129,6 +162,66 @@ var ( Args: []string{"a", "set", "of", "args"}, }, } + testBackupConf = &formolv1alpha1.BackupConfiguration{ + ObjectMeta: metav1.ObjectMeta{ + Name: TestBackupConfName, + Namespace: TestNamespace, + }, + Spec: formolv1alpha1.BackupConfigurationSpec{ + Repository: TestRepoName, + Schedule: "1 * * * *", + Keep: formolv1alpha1.Keep{ + Last: 2, + }, + Targets: []formolv1alpha1.Target{ + formolv1alpha1.Target{ + Kind: formolv1alpha1.SidecarKind, + Name: TestDeploymentName, + Steps: []formolv1alpha1.Step{ + formolv1alpha1.Step{ + Name: TestFunc, + }, + }, + Paths: []string{ + TestDataMountPath, + }, + VolumeMounts: []corev1.VolumeMount{ + corev1.VolumeMount{ + Name: TestDataVolume, + MountPath: TestDataMountPath, + }, + }, + }, + formolv1alpha1.Target{ + Kind: formolv1alpha1.JobKind, + Name: TestBackupFuncName, + Steps: []formolv1alpha1.Step{ + formolv1alpha1.Step{ + Name: TestFunc, + }, + formolv1alpha1.Step{ + Name: TestBackupFuncName, + Env: []corev1.EnvVar{ + corev1.EnvVar{ + Name: "foo", + Value: "bar", + }, + }, + }, + }, + }, + }, + }, + } + testBackupSession = &formolv1alpha1.BackupSession{ + ObjectMeta: metav1.ObjectMeta{ + Name: TestBackupSessionName, + Namespace: TestNamespace, + }, + Spec: formolv1alpha1.BackupSessionSpec{ + Ref: TestBackupConfName, + }, + } ) func TestAPIs(t *testing.T) { @@ -168,6 +261,20 @@ var _ = BeforeSuite(func() { }).SetupWithManager(k8sManager) Expect(err).ToNot(HaveOccurred()) + err = (&BackupSessionReconciler{ + Client: k8sManager.GetClient(), + Scheme: k8sManager.GetScheme(), + Log: ctrl.Log.WithName("controllers").WithName("BackupSession"), + }).SetupWithManager(k8sManager) + Expect(err).ToNot(HaveOccurred()) + + err = (&RestoreSessionReconciler{ + Client: k8sManager.GetClient(), + Scheme: k8sManager.GetScheme(), + Log: ctrl.Log.WithName("controllers").WithName("RestoreSession"), + }).SetupWithManager(k8sManager) + Expect(err).ToNot(HaveOccurred()) + go func() { err = k8sManager.Start(ctrl.SetupSignalHandler()) Expect(err).ToNot(HaveOccurred()) @@ -182,6 +289,32 @@ var _ = BeforeSuite(func() { Expect(k8sClient.Create(ctx, repo)).Should(Succeed()) Expect(k8sClient.Create(ctx, deployment)).Should(Succeed()) Expect(k8sClient.Create(ctx, function)).Should(Succeed()) + Expect(k8sClient.Create(ctx, backupFunc)).Should(Succeed()) + Expect(k8sClient.Create(ctx, restoreFunc)).Should(Succeed()) + Expect(k8sClient.Create(ctx, testBackupConf)).Should(Succeed()) + Expect(k8sClient.Create(ctx, testBackupSession)).Should(Succeed()) + Eventually(func() error { + return k8sClient.Get(ctx, client.ObjectKey{ + Name: TestBackupSessionName, + Namespace: TestNamespace, + }, testBackupSession) + }, timeout, interval).Should(Succeed()) + testBackupSession.Status.SessionState = formolv1alpha1.Success + testBackupSession.Status.Targets = []formolv1alpha1.TargetStatus{ + formolv1alpha1.TargetStatus{ + Name: TestDeploymentName, + Kind: formolv1alpha1.SidecarKind, + SessionState: formolv1alpha1.Success, + SnapshotId: "12345abcdef", + }, + formolv1alpha1.TargetStatus{ + Name: TestBackupFuncName, + Kind: formolv1alpha1.JobKind, + SessionState: formolv1alpha1.Success, + SnapshotId: "67890ghijk", + }, + } + Expect(k8sClient.Status().Update(ctx, testBackupSession)).Should(Succeed()) }, 60) var _ = AfterSuite(func() { diff --git a/main.go b/main.go index 575cc90..d7cb1af 100644 --- a/main.go +++ b/main.go @@ -102,6 +102,10 @@ func main() { setupLog.Error(err, "unable to create controller", "controller", "RestoreSession") os.Exit(1) } + if err = (&formoldesmojimfrv1alpha1.Function{}).SetupWebhookWithManager(mgr); err != nil { + setupLog.Error(err, "unable to create webhook", "webhook", "Function") + os.Exit(1) + } // +kubebuilder:scaffold:builder setupLog.Info("starting manager") diff --git a/test/01-deployment.yaml b/test/01-deployment.yaml index d3daac7..eff5dd1 100644 --- a/test/01-deployment.yaml +++ b/test/01-deployment.yaml @@ -8,6 +8,8 @@ metadata: app: nginx spec: replicas: 1 + strategy: + type: Recreate selector: matchLabels: app: nginx diff --git a/test/02-backupconf.yaml b/test/02-backupconf.yaml index 6b1659d..caaa90c 100644 --- a/test/02-backupconf.yaml +++ b/test/02-backupconf.yaml @@ -8,7 +8,7 @@ spec: repository: repo-minio schedule: "15 * * * *" targets: - - kind: Deployment + - kind: Sidecar apiVersion: v1 name: nginx-deployment volumeMounts: @@ -16,11 +16,10 @@ spec: mountPath: /data paths: - /data - - kind: Task + - kind: Job name: backup-pg steps: - name: backup-pg - namespace: demo env: - name: PGHOST value: postgres