Refactored since reconciler are not reentrant

This commit is contained in:
jandre 2021-04-17 16:21:26 +02:00
parent 393465300a
commit 2901f9aa1a
26 changed files with 1428 additions and 1072 deletions

View File

@ -2,7 +2,8 @@
# Image URL to use all building/pushing image targets # Image URL to use all building/pushing image targets
IMG ?= desmo999r/formolcontroller:latest IMG ?= desmo999r/formolcontroller:latest
# Produce CRDs that work back to Kubernetes 1.11 (no version conversion) # 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) # Get the currently used golang install path (in GOPATH/bin, unless GOBIN is set)
ifeq (,$(shell go env GOBIN)) ifeq (,$(shell go env GOBIN))

View File

@ -21,10 +21,18 @@ import (
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
) )
const (
SidecarKind string = "Sidecar"
JobKind string = "Job"
BackupVolumes string = "Volumes"
)
type Step struct { type Step struct {
Name string `json:"name"` Name string `json:"name"`
Namespace string `json:"namespace"` // +optional
Env []corev1.EnvVar `json:"env"` Env []corev1.EnvVar `json:"env,omitempty"`
// +optional
Finalize *bool `json:"finalize,omitempty"`
} }
type Hook struct { type Hook struct {
@ -34,14 +42,10 @@ type Hook struct {
} }
type Target struct { type Target struct {
// +kubebuilder:validation:Enum=Deployment;Task // +kubebuilder:validation:Enum=Sidecar;Job
Kind string `json:"kind"` Kind string `json:"kind"`
Name string `json:"name"` Name string `json:"name"`
// +optional // +optional
BeforeBackup []Hook `json:"beforeBackup,omitempty"`
// +optional
AfterBackup []Hook `json:"afterBackup,omitempty"`
// +optional
ApiVersion string `json:"apiVersion,omitempty"` ApiVersion string `json:"apiVersion,omitempty"`
// +optional // +optional
VolumeMounts []corev1.VolumeMount `json:"volumeMounts,omitempty"` VolumeMounts []corev1.VolumeMount `json:"volumeMounts,omitempty"`
@ -50,6 +54,8 @@ type Target struct {
// +optional // +optional
// +kubebuilder:validation:MinItems=1 // +kubebuilder:validation:MinItems=1
Steps []Step `json:"steps,omitempty"` Steps []Step `json:"steps,omitempty"`
// +kubebuilder:default:=2
Retry int `json:"retry,omitempty"`
} }
type Keep struct { type Keep struct {

View File

@ -23,17 +23,9 @@ import (
// EDIT THIS FILE! THIS IS SCAFFOLDING FOR YOU TO OWN! // 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. // 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 // BackupSessionSpec defines the desired state of BackupSession
type BackupSessionSpec struct { type BackupSessionSpec struct {
// INSERT ADDITIONAL SPEC FIELDS - desired state of cluster Ref string `json:"ref"`
// 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"`
} }
// BackupSessionStatus defines the observed state of BackupSession // BackupSessionStatus defines the observed state of BackupSession
@ -41,8 +33,6 @@ type BackupSessionStatus struct {
// INSERT ADDITIONAL STATUS FIELD - define observed state of cluster // INSERT ADDITIONAL STATUS FIELD - define observed state of cluster
// Important: Run "make" to regenerate code after modifying this file // Important: Run "make" to regenerate code after modifying this file
// +optional // +optional
ObservedGeneration int64 `json:"observedGeneration,omitempty"`
// +optional
SessionState `json:"state,omitempty"` SessionState `json:"state,omitempty"`
// +optional // +optional
StartTime *metav1.Time `json:"startTime,omitempty"` StartTime *metav1.Time `json:"startTime,omitempty"`

View File

@ -8,13 +8,23 @@ type SessionState string
const ( const (
New SessionState = "New" New SessionState = "New"
Init SessionState = "Initializing"
Running SessionState = "Running" Running SessionState = "Running"
Finalize SessionState = "Finalizing"
Success SessionState = "Success" Success SessionState = "Success"
Failure SessionState = "Failure" Failure SessionState = "Failure"
Deleted SessionState = "Deleted" 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" TARGET_NAME string = "TARGET_NAME"
// Used by restoresession controller
RESTORESESSION_NAMESPACE string = "RESTORESESSION_NAMESPACE" RESTORESESSION_NAMESPACE string = "RESTORESESSION_NAMESPACE"
RESTORESESSION_NAME string = "RESTORESESSION_NAME" RESTORESESSION_NAME string = "RESTORESESSION_NAME"
// Used by the backupsession controller
POD_NAME string = "POD_NAME"
POD_NAMESPACE string = "POD_NAMESPACE"
) )
type TargetStatus struct { type TargetStatus struct {
@ -28,4 +38,6 @@ type TargetStatus struct {
StartTime *metav1.Time `json:"startTime,omitempty"` StartTime *metav1.Time `json:"startTime,omitempty"`
// +optional // +optional
Duration *metav1.Duration `json:"duration,omitempty"` Duration *metav1.Duration `json:"duration,omitempty"`
// +optional
Try int `json:"try,omitemmpty"`
} }

View File

@ -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
}

View File

@ -18,6 +18,7 @@ package v1alpha1
import ( import (
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
//"k8s.io/apimachinery/pkg/types"
) )
// EDIT THIS FILE! THIS IS SCAFFOLDING FOR YOU TO OWN! // 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 // INSERT ADDITIONAL SPEC FIELDS - desired state of cluster
// Important: Run "make" to regenerate code after modifying this file // 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 // RestoreSessionStatus defines the observed state of RestoreSession

View File

@ -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. // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *BackupSessionSpec) DeepCopyInto(out *BackupSessionSpec) { func (in *BackupSessionSpec) DeepCopyInto(out *BackupSessionSpec) {
*out = *in *out = *in
out.Ref = in.Ref
} }
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new BackupSessionSpec. // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new BackupSessionSpec.
@ -342,21 +341,6 @@ func (in *Keep) DeepCopy() *Keep {
return out 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. // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *Repo) DeepCopyInto(out *Repo) { func (in *Repo) DeepCopyInto(out *Repo) {
*out = *in *out = *in
@ -452,7 +436,7 @@ func (in *RestoreSession) DeepCopyInto(out *RestoreSession) {
*out = *in *out = *in
out.TypeMeta = in.TypeMeta out.TypeMeta = in.TypeMeta
in.ObjectMeta.DeepCopyInto(&out.ObjectMeta) in.ObjectMeta.DeepCopyInto(&out.ObjectMeta)
in.Spec.DeepCopyInto(&out.Spec) out.Spec = in.Spec
in.Status.DeepCopyInto(&out.Status) 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. // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *RestoreSessionSpec) DeepCopyInto(out *RestoreSessionSpec) { func (in *RestoreSessionSpec) DeepCopyInto(out *RestoreSessionSpec) {
*out = *in *out = *in
in.BackupSessionRef.DeepCopyInto(&out.BackupSessionRef)
} }
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new RestoreSessionSpec. // 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]) (*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. // 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. // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *Target) DeepCopyInto(out *Target) { func (in *Target) DeepCopyInto(out *Target) {
*out = *in *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 { if in.VolumeMounts != nil {
in, out := &in.VolumeMounts, &out.VolumeMounts in, out := &in.VolumeMounts, &out.VolumeMounts
*out = make([]v1.VolumeMount, len(*in)) *out = make([]v1.VolumeMount, len(*in))

View File

@ -14,17 +14,17 @@ patchesStrategicMerge:
# patches here are for enabling the conversion webhook for each CRD # patches here are for enabling the conversion webhook for each CRD
#- patches/webhook_in_tasks.yaml #- patches/webhook_in_tasks.yaml
#- patches/webhook_in_functions.yaml #- patches/webhook_in_functions.yaml
#- patches/webhook_in_backupconfigurations.yaml - patches/webhook_in_backupconfigurations.yaml
- patches/webhook_in_backupsessions.yaml #- patches/webhook_in_backupsessions.yaml
#- patches/webhook_in_repoes.yaml #- patches/webhook_in_repoes.yaml
#- patches/webhook_in_restoresessions.yaml #- patches/webhook_in_restoresessions.yaml
# +kubebuilder:scaffold:crdkustomizewebhookpatch # +kubebuilder:scaffold:crdkustomizewebhookpatch
# [CERTMANAGER] To enable webhook, uncomment all the sections with [CERTMANAGER] prefix. # [CERTMANAGER] To enable webhook, uncomment all the sections with [CERTMANAGER] prefix.
# patches here are for enabling the CA injection for each CRD # patches here are for enabling the CA injection for each CRD
- patches/cainjection_in_functions.yaml #- patches/cainjection_in_functions.yaml
- patches/cainjection_in_backupconfigurations.yaml #- patches/cainjection_in_backupconfigurations.yaml
- patches/cainjection_in_backupsessions.yaml #- patches/cainjection_in_backupsessions.yaml
#- patches/cainjection_in_repoes.yaml #- patches/cainjection_in_repoes.yaml
#- patches/cainjection_in_restoresessions.yaml #- patches/cainjection_in_restoresessions.yaml
# +kubebuilder:scaffold:crdkustomizecainjectionpatch # +kubebuilder:scaffold:crdkustomizecainjectionpatch

View File

@ -1,6 +1,6 @@
# The following patch adds a directive for certmanager to inject CA into the CRD # The following patch adds a directive for certmanager to inject CA into the CRD
# CRD conversion requires k8s 1.13 or later. # CRD conversion requires k8s 1.13 or later.
apiVersion: apiextensions.k8s.io/v1beta1 apiVersion: apiextensions.k8s.io/v1
kind: CustomResourceDefinition kind: CustomResourceDefinition
metadata: metadata:
annotations: annotations:

View File

@ -1,6 +1,6 @@
# The following patch adds a directive for certmanager to inject CA into the CRD # The following patch adds a directive for certmanager to inject CA into the CRD
# CRD conversion requires k8s 1.13 or later. # CRD conversion requires k8s 1.13 or later.
apiVersion: apiextensions.k8s.io/v1beta1 apiVersion: apiextensions.k8s.io/v1
kind: CustomResourceDefinition kind: CustomResourceDefinition
metadata: metadata:
annotations: annotations:

View File

@ -1,6 +1,6 @@
# The following patch adds a directive for certmanager to inject CA into the CRD # The following patch adds a directive for certmanager to inject CA into the CRD
# CRD conversion requires k8s 1.13 or later. # CRD conversion requires k8s 1.13 or later.
apiVersion: apiextensions.k8s.io/v1beta1 apiVersion: apiextensions.k8s.io/v1
kind: CustomResourceDefinition kind: CustomResourceDefinition
metadata: metadata:
annotations: annotations:

View File

@ -1,6 +1,6 @@
# The following patch adds a directive for certmanager to inject CA into the CRD # The following patch adds a directive for certmanager to inject CA into the CRD
# CRD conversion requires k8s 1.13 or later. # CRD conversion requires k8s 1.13 or later.
apiVersion: apiextensions.k8s.io/v1beta1 apiVersion: apiextensions.k8s.io/v1
kind: CustomResourceDefinition kind: CustomResourceDefinition
metadata: metadata:
annotations: annotations:

View File

@ -1,13 +1,16 @@
# The following patch enables conversion webhook for CRD # The following patch enables conversion webhook for CRD
# CRD conversion requires k8s 1.13 or later. # CRD conversion requires k8s 1.13 or later.
apiVersion: apiextensions.k8s.io/v1beta1 apiVersion: apiextensions.k8s.io/v1
kind: CustomResourceDefinition kind: CustomResourceDefinition
metadata: metadata:
name: backupconfigurations.formol.desmojim.fr name: backupconfigurations.formol.desmojim.fr
spec: spec:
preserveUnknownFields: false
conversion: conversion:
strategy: Webhook 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, # 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) # but we're going to set it later using the cert-manager (or potentially a patch if not using cert-manager)
caBundle: Cg== caBundle: Cg==

View File

@ -1,6 +1,6 @@
# The following patch enables conversion webhook for CRD # The following patch enables conversion webhook for CRD
# CRD conversion requires k8s 1.13 or later. # CRD conversion requires k8s 1.13 or later.
apiVersion: apiextensions.k8s.io/v1beta1 apiVersion: apiextensions.k8s.io/v1
kind: CustomResourceDefinition kind: CustomResourceDefinition
metadata: metadata:
name: backupsessions.formol.desmojim.fr name: backupsessions.formol.desmojim.fr

View File

@ -1,6 +1,6 @@
# The following patch enables conversion webhook for CRD # The following patch enables conversion webhook for CRD
# CRD conversion requires k8s 1.13 or later. # CRD conversion requires k8s 1.13 or later.
apiVersion: apiextensions.k8s.io/v1beta1 apiVersion: apiextensions.k8s.io/v1
kind: CustomResourceDefinition kind: CustomResourceDefinition
metadata: metadata:
name: functions.formol.desmojim.fr name: functions.formol.desmojim.fr

View File

@ -1,6 +1,6 @@
# The following patch enables conversion webhook for CRD # The following patch enables conversion webhook for CRD
# CRD conversion requires k8s 1.13 or later. # CRD conversion requires k8s 1.13 or later.
apiVersion: apiextensions.k8s.io/v1beta1 apiVersion: apiextensions.k8s.io/v1
kind: CustomResourceDefinition kind: CustomResourceDefinition
metadata: metadata:
name: restoresessions.formol.desmojim.fr.desmojim.fr name: restoresessions.formol.desmojim.fr.desmojim.fr

View File

@ -18,7 +18,7 @@ package controllers
import ( import (
"context" "context"
"time" //"time"
formolrbac "github.com/desmo999r/formol/pkg/rbac" formolrbac "github.com/desmo999r/formol/pkg/rbac"
formolutils "github.com/desmo999r/formol/pkg/utils" formolutils "github.com/desmo999r/formol/pkg/utils"
@ -33,7 +33,7 @@ import (
ctrl "sigs.k8s.io/controller-runtime" ctrl "sigs.k8s.io/controller-runtime"
"sigs.k8s.io/controller-runtime/pkg/client" "sigs.k8s.io/controller-runtime/pkg/client"
"sigs.k8s.io/controller-runtime/pkg/controller" "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" formolv1alpha1 "github.com/desmo999r/formol/api/v1alpha1"
) )
@ -45,16 +45,6 @@ type BackupConfigurationReconciler struct {
Scheme *runtime.Scheme 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=formol.desmojim.fr,resources=*,verbs=*
// +kubebuilder:rbac:groups=apps,resources=deployments,verbs=get;list;watch;create;update;patch;delete // +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 // +kubebuilder:rbac:groups=apps,resources=replicasets,verbs=get;list;watch;create;update;patch;delete
@ -67,183 +57,28 @@ 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,verbs=get;list;watch;create;update;patch;delete
// +kubebuilder:rbac:groups=batch,resources=cronjobs/status,verbs=get // +kubebuilder:rbac:groups=batch,resources=cronjobs/status,verbs=get
func (r *BackupConfigurationReconciler) deleteSidecarContainer(backupConf *formolv1alpha1.BackupConfiguration, target formolv1alpha1.Target) error { func (r *BackupConfigurationReconciler) Reconcile(req ctrl.Request) (ctrl.Result, error) {
deployment, err := r.getDeployment(backupConf.Namespace, target.Name) ctx := context.Background()
if err != nil { log := r.Log.WithValues("backupconfiguration", req.NamespacedName)
return err //time.Sleep(300 * time.Millisecond)
}
restorecontainers := []corev1.Container{} log.V(1).Info("Enter Reconcile with req", "req", req, "reconciler", r)
for _, container := range deployment.Spec.Template.Spec.Containers {
if container.Name == "backup" { backupConf := &formolv1alpha1.BackupConfiguration{}
continue if err := r.Get(ctx, req.NamespacedName, backupConf); err != nil {
} return ctrl.Result{}, client.IgnoreNotFound(err)
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)
}
}
}
} }
for _, replica := range replicasToDelete { getDeployment := func(namespace string, name string) (*appsv1.Deployment, error) {
if err := r.Delete(context.TODO(), &replica); err != nil { deployment := &appsv1.Deployment{}
return nil err := r.Get(context.Background(), client.ObjectKey{
} Namespace: namespace,
} Name: name,
return nil }, deployment)
} return deployment, err
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{},
} }
// Gather information from the repo deleteCronJob := func() error {
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.DeleteFormolRBAC(r.Client, "default", backupConf.Namespace)
_ = formolrbac.DeleteBackupSessionCreatorRBAC(r.Client, backupConf.Namespace) _ = formolrbac.DeleteBackupSessionCreatorRBAC(r.Client, backupConf.Namespace)
cronjob := &kbatch_beta1.CronJob{} cronjob := &kbatch_beta1.CronJob{}
@ -256,10 +91,9 @@ func (r *BackupConfigurationReconciler) deleteCronJob(backupConf *formolv1alpha1
} else { } else {
return err return err
} }
} }
func (r *BackupConfigurationReconciler) addCronJob(backupConf *formolv1alpha1.BackupConfiguration) error {
log := r.Log.WithValues("addCronJob", backupConf.Name)
addCronJob := func() error {
if err := formolrbac.CreateFormolRBAC(r.Client, "default", backupConf.Namespace); err != nil { if err := formolrbac.CreateFormolRBAC(r.Client, "default", backupConf.Namespace); err != nil {
log.Error(err, "unable to create backupsessionlistener RBAC") log.Error(err, "unable to create backupsessionlistener RBAC")
return nil return nil
@ -343,34 +177,129 @@ func (r *BackupConfigurationReconciler) addCronJob(backupConf *formolv1alpha1.Ba
return err return err
} }
return nil return nil
} }
func (r *BackupConfigurationReconciler) Reconcile(req ctrl.Request) (ctrl.Result, error) { deleteSidecarContainer := func(target formolv1alpha1.Target) error {
ctx := context.Background() deployment, err := getDeployment(backupConf.Namespace, target.Name)
log := r.Log.WithValues("backupconfiguration", req.NamespacedName) if err != nil {
time.Sleep(300 * time.Millisecond) 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(1).Info("Enter Reconcile with req", "req", req) 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{},
}
backupConf := &formolv1alpha1.BackupConfiguration{} // Gather information from the repo
if err := r.Get(ctx, req.NamespacedName, backupConf); err != nil { repo := &formolv1alpha1.Repo{}
return ctrl.Result{}, client.IgnoreNotFound(err) 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
}
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" finalizerName := "finalizer.backupconfiguration.formol.desmojim.fr"
if backupConf.ObjectMeta.DeletionTimestamp.IsZero() { 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 {
log.V(0).Info("backupconf being deleted", "backupconf", backupConf.Name) log.V(0).Info("backupconf being deleted", "backupconf", backupConf.Name)
if formolutils.ContainsString(backupConf.ObjectMeta.Finalizers, finalizerName) { if formolutils.ContainsString(backupConf.ObjectMeta.Finalizers, finalizerName) {
_ = r.deleteExternalResources(backupConf) _ = deleteExternalResources()
} }
backupConf.ObjectMeta.Finalizers = formolutils.RemoveString(backupConf.ObjectMeta.Finalizers, finalizerName) backupConf.ObjectMeta.Finalizers = formolutils.RemoveString(backupConf.ObjectMeta.Finalizers, finalizerName)
if err := r.Update(context.Background(), backupConf); err != nil { 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 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 return ctrl.Result{}, nil
} }
backupConf.Status.ActiveCronJob = true backupConf.Status.ActiveCronJob = true
for _, target := range backupConf.Spec.Targets { for _, target := range backupConf.Spec.Targets {
switch target.Kind { switch target.Kind {
case "Deployment": case formolv1alpha1.SidecarKind:
if err := r.addSidecarContainer(backupConf, target); err != nil { if err := addSidecarContainer(target); err != nil {
return ctrl.Result{}, client.IgnoreNotFound(err) return ctrl.Result{}, client.IgnoreNotFound(err)
} }
backupConf.Status.ActiveSidecar = true 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 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 { func (r *BackupConfigurationReconciler) SetupWithManager(mgr ctrl.Manager) error {
return ctrl.NewControllerManagedBy(mgr). return ctrl.NewControllerManagedBy(mgr).
For(&formolv1alpha1.BackupConfiguration{}). For(&formolv1alpha1.BackupConfiguration{}).
WithOptions(controller.Options{MaxConcurrentReconciles: 3}). 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(&formolv1alpha1.BackupSession{}).
Owns(&kbatch_beta1.CronJob{}). Owns(&kbatch_beta1.CronJob{}).
Complete(r) Complete(r)

View File

@ -5,7 +5,7 @@ import (
//"k8s.io/apimachinery/pkg/types" //"k8s.io/apimachinery/pkg/types"
//"reflect" //"reflect"
//"fmt" //"fmt"
//"time" "time"
. "github.com/onsi/ginkgo" . "github.com/onsi/ginkgo"
. "github.com/onsi/gomega" . "github.com/onsi/gomega"
@ -21,11 +21,11 @@ import (
var _ = Describe("Testing BackupConf controller", func() { var _ = Describe("Testing BackupConf controller", func() {
const ( const (
BackupConfName = "test-backupconf" BCBackupConfName = "test-backupconf-controller"
) )
var ( var (
key = types.NamespacedName{ key = types.NamespacedName{
Name: BackupConfName, Name: BCBackupConfName,
Namespace: TestNamespace, Namespace: TestNamespace,
} }
ctx = context.Background() ctx = context.Background()
@ -35,24 +35,32 @@ var _ = Describe("Testing BackupConf controller", func() {
BeforeEach(func() { BeforeEach(func() {
backupConf = &formolv1alpha1.BackupConfiguration{ backupConf = &formolv1alpha1.BackupConfiguration{
ObjectMeta: metav1.ObjectMeta{ ObjectMeta: metav1.ObjectMeta{
Name: BackupConfName, Name: BCBackupConfName,
Namespace: TestNamespace, Namespace: TestNamespace,
}, },
Spec: formolv1alpha1.BackupConfigurationSpec{ Spec: formolv1alpha1.BackupConfigurationSpec{
Repository: RepoName, Repository: TestRepoName,
Schedule: "1 * * * *", Schedule: "1 * * * *",
Targets: []formolv1alpha1.Target{ Targets: []formolv1alpha1.Target{
formolv1alpha1.Target{ formolv1alpha1.Target{
Kind: "Deployment", Kind: formolv1alpha1.SidecarKind,
Name: DeploymentName, Name: TestDeploymentName,
VolumeMounts: []corev1.VolumeMount{
corev1.VolumeMount{
Name: TestDataVolume,
MountPath: TestDataMountPath,
},
},
Paths: []string{
TestDataMountPath,
},
}, },
formolv1alpha1.Target{ formolv1alpha1.Target{
Kind: "Task", Kind: formolv1alpha1.JobKind,
Name: BackupFuncName, Name: TestBackupFuncName,
Steps: []formolv1alpha1.Step{ Steps: []formolv1alpha1.Step{
formolv1alpha1.Step{ formolv1alpha1.Step{
Name: BackupFuncName, Name: TestBackupFuncName,
Namespace: TestNamespace,
Env: []corev1.EnvVar{ Env: []corev1.EnvVar{
corev1.EnvVar{ corev1.EnvVar{
Name: "foo", 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() { JustBeforeEach(func() {
Eventually(func() error { Eventually(func() error {
return k8sClient.Create(ctx, backupConf) return k8sClient.Create(ctx, backupConf)
@ -85,18 +93,16 @@ var _ = Describe("Testing BackupConf controller", func() {
return true return true
}, timeout, interval).Should(BeTrue()) }, timeout, interval).Should(BeTrue())
Expect(realBackupConf.Spec.Schedule).Should(Equal("1 * * * *")) Expect(realBackupConf.Spec.Schedule).Should(Equal("1 * * * *"))
Expect(realBackupConf.Spec.Targets[0].Retry).Should(Equal(2))
}) })
It("Should also create a CronJob", func() { It("Should also create a CronJob", func() {
cronJob := &batchv1beta1.CronJob{} cronJob := &batchv1beta1.CronJob{}
Eventually(func() bool { Eventually(func() bool {
err := k8sClient.Get(ctx, types.NamespacedName{ err := k8sClient.Get(ctx, types.NamespacedName{
Name: "backup-" + BackupConfName, Name: "backup-" + BCBackupConfName,
Namespace: TestNamespace, Namespace: TestNamespace,
}, cronJob) }, cronJob)
if err != nil { return err == nil
return false
}
return true
}, timeout, interval).Should(BeTrue()) }, timeout, interval).Should(BeTrue())
Expect(cronJob.Spec.Schedule).Should(Equal("1 * * * *")) Expect(cronJob.Spec.Schedule).Should(Equal("1 * * * *"))
}) })
@ -104,7 +110,7 @@ var _ = Describe("Testing BackupConf controller", func() {
realDeployment := &appsv1.Deployment{} realDeployment := &appsv1.Deployment{}
Eventually(func() (int, error) { Eventually(func() (int, error) {
err := k8sClient.Get(ctx, types.NamespacedName{ err := k8sClient.Get(ctx, types.NamespacedName{
Name: DeploymentName, Name: TestDeploymentName,
Namespace: TestNamespace, Namespace: TestNamespace,
}, realDeployment) }, realDeployment)
if err != nil { if err != nil {
@ -115,6 +121,7 @@ var _ = Describe("Testing BackupConf controller", func() {
}) })
It("Should also update the CronJob", func() { It("Should also update the CronJob", func() {
realBackupConf := &formolv1alpha1.BackupConfiguration{} realBackupConf := &formolv1alpha1.BackupConfiguration{}
time.Sleep(300 * time.Millisecond)
Eventually(func() bool { Eventually(func() bool {
err := k8sClient.Get(ctx, key, realBackupConf) err := k8sClient.Get(ctx, key, realBackupConf)
if err != nil { if err != nil {
@ -129,7 +136,7 @@ var _ = Describe("Testing BackupConf controller", func() {
cronJob := &batchv1beta1.CronJob{} cronJob := &batchv1beta1.CronJob{}
Eventually(func() (string, error) { Eventually(func() (string, error) {
err := k8sClient.Get(ctx, types.NamespacedName{ err := k8sClient.Get(ctx, types.NamespacedName{
Name: "backup-" + BackupConfName, Name: "backup-" + BCBackupConfName,
Namespace: TestNamespace, Namespace: TestNamespace,
}, cronJob) }, cronJob)
if err != nil { if err != nil {
@ -139,7 +146,7 @@ var _ = Describe("Testing BackupConf controller", func() {
}, timeout, interval).Should(Equal("1 0 * * *")) }, timeout, interval).Should(Equal("1 0 * * *"))
Eventually(func() (bool, error) { Eventually(func() (bool, error) {
err := k8sClient.Get(ctx, types.NamespacedName{ err := k8sClient.Get(ctx, types.NamespacedName{
Name: "backup-" + BackupConfName, Name: "backup-" + BCBackupConfName,
Namespace: TestNamespace, Namespace: TestNamespace,
}, cronJob) }, cronJob)
if err != nil { if err != nil {
@ -160,7 +167,7 @@ var _ = Describe("Testing BackupConf controller", func() {
realDeployment := &appsv1.Deployment{} realDeployment := &appsv1.Deployment{}
Eventually(func() (int, error) { Eventually(func() (int, error) {
err := k8sClient.Get(ctx, types.NamespacedName{ err := k8sClient.Get(ctx, types.NamespacedName{
Name: DeploymentName, Name: TestDeploymentName,
Namespace: TestNamespace, Namespace: TestNamespace,
}, realDeployment) }, realDeployment)
if err != nil { if err != nil {

View File

@ -48,36 +48,199 @@ type BackupSessionReconciler struct {
client.Client client.Client
Log logr.Logger Log logr.Logger
Scheme *runtime.Scheme Scheme *runtime.Scheme
BackupSession *formolv1alpha1.BackupSession
BackupConf *formolv1alpha1.BackupConfiguration
} }
func (r *BackupSessionReconciler) StatusUpdate() error { // +kubebuilder:rbac:groups=formol.desmojim.fr,resources=backupsessions,verbs=get;list;watch;create;update;patch;delete
log := r.Log.WithValues("backupsession-statusupdate", r.BackupSession) // +kubebuilder:rbac:groups=formol.desmojim.fr,resources=backupsessions/status,verbs=get;update;patch;create;delete
// +kubebuilder:rbac:groups=formol.desmojim.fr,resources=functions,verbs=get;list;watch
// +kubebuilder:rbac:groups=batch,resources=jobs,verbs=get;list;create;update;patch;delete;watch
func (r *BackupSessionReconciler) Reconcile(req ctrl.Request) (ctrl.Result, error) {
log := r.Log.WithValues("backupsession", req.NamespacedName)
ctx := context.Background() ctx := context.Background()
r.BackupConf = &formolv1alpha1.BackupConfiguration{}
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)
}
backupConf := &formolv1alpha1.BackupConfiguration{}
if err := r.Get(ctx, client.ObjectKey{ if err := r.Get(ctx, client.ObjectKey{
Namespace: r.BackupSession.Namespace, Namespace: backupSession.Namespace,
Name: r.BackupSession.Spec.Ref.Name}, r.BackupConf); err != nil { Name: backupSession.Spec.Ref,
}, backupConf); err != nil {
log.Error(err, "unable to get backupConfiguration") log.Error(err, "unable to get backupConfiguration")
return client.IgnoreNotFound(err) return ctrl.Result{}, client.IgnoreNotFound(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
}
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,
},
}
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 // start the next task
startNextTask := func() (*formolv1alpha1.TargetStatus, error) { startNextTask := func() (*formolv1alpha1.TargetStatus, error) {
nextTarget := len(r.BackupSession.Status.Targets) nextTarget := len(backupSession.Status.Targets)
if nextTarget < len(r.BackupConf.Spec.Targets) { if nextTarget < len(backupConf.Spec.Targets) {
target := r.BackupConf.Spec.Targets[nextTarget] target := backupConf.Spec.Targets[nextTarget]
targetStatus := formolv1alpha1.TargetStatus{ targetStatus := formolv1alpha1.TargetStatus{
Name: target.Name, Name: target.Name,
Kind: target.Kind, Kind: target.Kind,
SessionState: formolv1alpha1.New, SessionState: formolv1alpha1.New,
StartTime: &metav1.Time{Time: time.Now()}, StartTime: &metav1.Time{Time: time.Now()},
Try: 1,
} }
r.BackupSession.Status.Targets = append(r.BackupSession.Status.Targets, targetStatus) backupSession.Status.Targets = append(backupSession.Status.Targets, targetStatus)
switch target.Kind { switch target.Kind {
case "Task": case formolv1alpha1.JobKind:
if err := r.CreateBackupJob(target); err != nil { if err := createBackupJob(target); err != nil {
log.V(0).Info("unable to create task", "task", target) log.V(0).Info("unable to create task", "task", target)
targetStatus.SessionState = formolv1alpha1.Failure targetStatus.SessionState = formolv1alpha1.Failure
return nil, err return nil, err
@ -88,56 +251,18 @@ func (r *BackupSessionReconciler) StatusUpdate() error {
return nil, nil return nil, nil
} }
} }
// Test the backupsession backupstate to decide what to do
switch r.BackupSession.Status.SessionState { // cleanup existing backupsessions
case formolv1alpha1.New: cleanupSessions := func() {
// 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{} 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 { 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") log.Error(err, "unable to get backupsessionlist")
return nil return
} }
if len(backupSessionList.Items) < 2 { if len(backupSessionList.Items) < 2 {
// Not enough backupSession to proceed // Not enough backupSession to proceed
log.V(1).Info("Not enough successful backup jobs") log.V(1).Info("Not enough successful backup jobs")
break return
} }
sort.Slice(backupSessionList.Items, func(i, j int) bool { sort.Slice(backupSessionList.Items, func(i, j int) bool {
@ -150,13 +275,13 @@ func (r *BackupSessionReconciler) StatusUpdate() error {
} }
var lastBackups, dailyBackups, weeklyBackups, monthlyBackups, yearlyBackups KeepBackup var lastBackups, dailyBackups, weeklyBackups, monthlyBackups, yearlyBackups KeepBackup
lastBackups.Counter = r.BackupConf.Spec.Keep.Last lastBackups.Counter = backupConf.Spec.Keep.Last
dailyBackups.Counter = r.BackupConf.Spec.Keep.Daily dailyBackups.Counter = backupConf.Spec.Keep.Daily
weeklyBackups.Counter = r.BackupConf.Spec.Keep.Weekly weeklyBackups.Counter = backupConf.Spec.Keep.Weekly
monthlyBackups.Counter = r.BackupConf.Spec.Keep.Monthly monthlyBackups.Counter = backupConf.Spec.Keep.Monthly
yearlyBackups.Counter = r.BackupConf.Spec.Keep.Yearly yearlyBackups.Counter = backupConf.Spec.Keep.Yearly
for _, session := range backupSessionList.Items { for _, session := range backupSessionList.Items {
if session.Spec.Ref.Name != r.BackupConf.Name { if session.Spec.Ref != backupConf.Name {
continue continue
} }
deleteSession := true deleteSession := true
@ -217,103 +342,111 @@ func (r *BackupSessionReconciler) StatusUpdate() error {
} }
} }
} }
log.V(1).Info("New BackupSession status", "status", r.BackupSession.Status.SessionState) // end helper functions
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.V(0).Info("backupSession", "backupSession.ObjectMeta", backupSession.ObjectMeta, "backupSession.Status", backupSession.Status)
log := r.Log.WithName("IsBackupOngoing") if backupSession.ObjectMeta.DeletionTimestamp.IsZero() {
ctx := context.Background() switch backupSession.Status.SessionState {
case formolv1alpha1.New:
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
}
}
// +kubebuilder:rbac:groups=formol.desmojim.fr,resources=backupsessions,verbs=get;list;watch;create;update;patch;delete
// +kubebuilder:rbac:groups=formol.desmojim.fr,resources=backupsessions/status,verbs=get;update;patch;create;delete
// +kubebuilder:rbac:groups=formol.desmojim.fr,resources=functions,verbs=get;list;watch
// +kubebuilder:rbac:groups=batch,resources=jobs,verbs=get;list;create;update;patch;delete;watch
func (r *BackupSessionReconciler) Reconcile(req ctrl.Request) (ctrl.Result, error) {
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 {
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()
}
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 // Check if the finalizer has been registered
if !controllerutil.ContainsFinalizer(r.BackupSession, finalizerName) { if !controllerutil.ContainsFinalizer(backupSession, finalizerName) {
controllerutil.AddFinalizer(r.BackupSession, finalizerName) controllerutil.AddFinalizer(backupSession, finalizerName)
// We update the BackupSession to add the finalizer // We update the BackupSession to add the finalizer
// Reconcile will be called again // Reconcile will be called again
// return now // return now
err := r.Update(ctx, r.BackupSession) err := r.Update(ctx, backupSession)
if err != nil { if err != nil {
log.Error(err, "unable to add finalizer") log.Error(err, "unable to add finalizer")
} }
return ctrl.Result{}, err return ctrl.Result{}, err
} }
// All signals are green // Brand new backupsession
// We start the backup process if isBackupOngoing() {
r.BackupSession.Status.ObservedGeneration = r.BackupSession.ObjectMeta.Generation log.V(0).Info("There is an ongoing backup. Let's reschedule this operation")
r.BackupSession.Status.SessionState = formolv1alpha1.New return ctrl.Result{RequeueAfter: 30 * time.Second}, nil
r.BackupSession.Status.StartTime = &metav1.Time{Time: time.Now()} }
if err := r.Status().Update(ctx, r.BackupSession); err != 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") log.Error(err, "unable to update backupSession")
return ctrl.Result{}, err return ctrl.Result{}, err
} }
}
} else { } else {
log.V(0).Info("backupsession being deleted", "backupsession", r.BackupSession.Name) log.V(0).Info("backupsession being deleted", "backupsession", backupSession.Name)
if controllerutil.ContainsFinalizer(r.BackupSession, finalizerName) { if controllerutil.ContainsFinalizer(backupSession, finalizerName) {
if err := r.deleteExternalResources(); err != nil { if err := deleteExternalResources(); err != nil {
return ctrl.Result{}, err return ctrl.Result{}, err
} }
} }
controllerutil.RemoveFinalizer(r.BackupSession, finalizerName) controllerutil.RemoveFinalizer(backupSession, finalizerName)
if err := r.Update(ctx, r.BackupSession); err != nil { if err := r.Update(ctx, backupSession); err != nil {
log.Error(err, "unable to remove finalizer") log.Error(err, "unable to remove finalizer")
return ctrl.Result{}, err return ctrl.Result{}, err
} }
@ -323,144 +456,6 @@ func (r *BackupSessionReconciler) Reconcile(req ctrl.Request) (ctrl.Result, erro
return ctrl.Result{}, nil 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 { func (r *BackupSessionReconciler) SetupWithManager(mgr ctrl.Manager) error {
if err := mgr.GetFieldIndexer().IndexField(context.TODO(), &formolv1alpha1.BackupSession{}, sessionState, func(rawObj runtime.Object) []string { if err := mgr.GetFieldIndexer().IndexField(context.TODO(), &formolv1alpha1.BackupSession{}, sessionState, func(rawObj runtime.Object) []string {
session := rawObj.(*formolv1alpha1.BackupSession) session := rawObj.(*formolv1alpha1.BackupSession)

View File

@ -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() {
})
})
})

View File

@ -45,14 +45,40 @@ type RestoreSessionReconciler struct {
client.Client client.Client
Log logr.Logger Log logr.Logger
Scheme *runtime.Scheme Scheme *runtime.Scheme
RestoreSession *formolv1alpha1.RestoreSession
BackupSession *formolv1alpha1.BackupSession
BackupConf *formolv1alpha1.BackupConfiguration
} }
func (r *RestoreSessionReconciler) CreateRestoreJob(target formolv1alpha1.Target) error { // +kubebuilder:rbac:groups=formol.desmojim.fr,resources=restoresessions,verbs=get;list;watch;create;update;patch;delete
log := r.Log.WithValues("createrestorejob", target.Name) // +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() ctx := context.Background()
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)
}
// Helper functions
createRestoreJob := func(target formolv1alpha1.Target) error {
restoreSessionEnv := []corev1.EnvVar{ restoreSessionEnv := []corev1.EnvVar{
corev1.EnvVar{ corev1.EnvVar{
Name: "TARGET_NAME", Name: "TARGET_NAME",
@ -60,11 +86,11 @@ func (r *RestoreSessionReconciler) CreateRestoreJob(target formolv1alpha1.Target
}, },
corev1.EnvVar{ corev1.EnvVar{
Name: "RESTORESESSION_NAME", Name: "RESTORESESSION_NAME",
Value: r.RestoreSession.Name, Value: restoreSession.Name,
}, },
corev1.EnvVar{ corev1.EnvVar{
Name: "RESTORESESSION_NAMESPACE", Name: "RESTORESESSION_NAMESPACE",
Value: r.RestoreSession.Namespace, Value: restoreSession.Namespace,
}, },
} }
@ -72,7 +98,7 @@ func (r *RestoreSessionReconciler) CreateRestoreJob(target formolv1alpha1.Target
Name: "output", Name: "output",
MountPath: "/output", MountPath: "/output",
} }
for _, targetStatus := range r.BackupSession.Status.Targets { for _, targetStatus := range backupSession.Status.Targets {
if targetStatus.Name == target.Name { if targetStatus.Name == target.Name {
snapshotId := targetStatus.SnapshotId snapshotId := targetStatus.SnapshotId
restic := corev1.Container{ restic := corev1.Container{
@ -91,19 +117,19 @@ func (r *RestoreSessionReconciler) CreateRestoreJob(target formolv1alpha1.Target
} }
repo := &formolv1alpha1.Repo{} repo := &formolv1alpha1.Repo{}
if err := r.Get(ctx, client.ObjectKey{ if err := r.Get(ctx, client.ObjectKey{
Namespace: r.BackupConf.Namespace, Namespace: backupConf.Namespace,
Name: r.BackupConf.Spec.Repository, Name: backupConf.Spec.Repository,
}, repo); err != nil { }, repo); err != nil {
log.Error(err, "unable to get Repo from BackupConfiguration") log.Error(err, "unable to get Repo from BackupConfiguration")
return err return err
} }
// S3 backing storage // S3 backing storage
var ttl int32 = 300 var ttl int32 = 300
restic.Env = append(restic.Env, formolutils.ConfigureResticEnvVar(r.BackupConf, repo)...) restic.Env = append(restic.Env, formolutils.ConfigureResticEnvVar(backupConf, repo)...)
job := &batchv1.Job{ job := &batchv1.Job{
ObjectMeta: metav1.ObjectMeta{ ObjectMeta: metav1.ObjectMeta{
GenerateName: fmt.Sprintf("%s-%s-", r.RestoreSession.Name, target.Name), GenerateName: fmt.Sprintf("%s-%s-", restoreSession.Name, target.Name),
Namespace: r.RestoreSession.Namespace, Namespace: restoreSession.Namespace,
}, },
Spec: batchv1.JobSpec{ Spec: batchv1.JobSpec{
TTLSecondsAfterFinished: &ttl, TTLSecondsAfterFinished: &ttl,
@ -121,18 +147,34 @@ func (r *RestoreSessionReconciler) CreateRestoreJob(target formolv1alpha1.Target
} }
for _, step := range target.Steps { for _, step := range target.Steps {
function := &formolv1alpha1.Function{} function := &formolv1alpha1.Function{}
// get the backup function
if err := r.Get(ctx, client.ObjectKey{ if err := r.Get(ctx, client.ObjectKey{
Namespace: r.RestoreSession.Namespace, Namespace: restoreSession.Namespace,
Name: strings.Replace(step.Name, "backup", "restore", 1)}, function); err != nil { 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) log.Error(err, "unable to get function", "function", step)
return err return err
} }
function.Spec.Name = function.Name
function.Spec.Env = append(step.Env, restoreSessionEnv...) function.Spec.Env = append(step.Env, restoreSessionEnv...)
function.Spec.VolumeMounts = append(function.Spec.VolumeMounts, output) function.Spec.VolumeMounts = append(function.Spec.VolumeMounts, output)
job.Spec.Template.Spec.InitContainers = append(job.Spec.Template.Spec.InitContainers, function.Spec) job.Spec.Template.Spec.InitContainers = append(job.Spec.Template.Spec.InitContainers, function.Spec)
} }
if err := ctrl.SetControllerReference(r.RestoreSession, job, r.Scheme); err != nil { if err := ctrl.SetControllerReference(restoreSession, job, r.Scheme); err != nil {
log.Error(err, "unable to set controller on job", "job", job, "restoresession", r.RestoreSession) log.Error(err, "unable to set controller on job", "job", job, "restoresession", restoreSession)
return err return err
} }
log.V(0).Info("creating a restore job", "target", target.Name) log.V(0).Info("creating a restore job", "target", target.Name)
@ -143,14 +185,12 @@ func (r *RestoreSessionReconciler) CreateRestoreJob(target formolv1alpha1.Target
} }
} }
return nil return nil
} }
func (r *RestoreSessionReconciler) DeleteRestoreInitContainer(target formolv1alpha1.Target) error { deleteRestoreInitContainer := func(target formolv1alpha1.Target) error {
log := r.Log.WithValues("createrestoreinitcontainer", target.Name)
ctx := context.Background()
deployment := &appsv1.Deployment{} deployment := &appsv1.Deployment{}
if err := r.Get(context.Background(), client.ObjectKey{ if err := r.Get(context.Background(), client.ObjectKey{
Namespace: r.BackupConf.Namespace, Namespace: backupConf.Namespace,
Name: target.Name, Name: target.Name,
}, deployment); err != nil { }, deployment); err != nil {
log.Error(err, "unable to get deployment") log.Error(err, "unable to get deployment")
@ -171,14 +211,12 @@ func (r *RestoreSessionReconciler) DeleteRestoreInitContainer(target formolv1alp
return err return err
} }
return nil return nil
} }
func (r *RestoreSessionReconciler) CreateRestoreInitContainer(target formolv1alpha1.Target) error { createRestoreInitContainer := func(target formolv1alpha1.Target) error {
log := r.Log.WithValues("createrestoreinitcontainer", target.Name)
ctx := context.Background()
deployment := &appsv1.Deployment{} deployment := &appsv1.Deployment{}
if err := r.Get(context.Background(), client.ObjectKey{ if err := r.Get(context.Background(), client.ObjectKey{
Namespace: r.RestoreSession.Namespace, Namespace: restoreSession.Namespace,
Name: target.Name, Name: target.Name,
}, deployment); err != nil { }, deployment); err != nil {
log.Error(err, "unable to get deployment") log.Error(err, "unable to get deployment")
@ -192,7 +230,7 @@ func (r *RestoreSessionReconciler) CreateRestoreInitContainer(target formolv1alp
} }
} }
var snapshotId string var snapshotId string
for _, targetStatus := range r.BackupSession.Status.Targets { for _, targetStatus := range backupSession.Status.Targets {
if targetStatus.Name == target.Name && targetStatus.Kind == target.Kind { if targetStatus.Name == target.Name && targetStatus.Kind == target.Kind {
snapshotId = targetStatus.SnapshotId snapshotId = targetStatus.SnapshotId
} }
@ -204,11 +242,11 @@ func (r *RestoreSessionReconciler) CreateRestoreInitContainer(target formolv1alp
}, },
corev1.EnvVar{ corev1.EnvVar{
Name: formolv1alpha1.RESTORESESSION_NAME, Name: formolv1alpha1.RESTORESESSION_NAME,
Value: r.RestoreSession.Name, Value: restoreSession.Name,
}, },
corev1.EnvVar{ corev1.EnvVar{
Name: formolv1alpha1.RESTORESESSION_NAMESPACE, Name: formolv1alpha1.RESTORESESSION_NAMESPACE,
Value: r.RestoreSession.Namespace, Value: restoreSession.Namespace,
}, },
} }
initContainer := corev1.Container{ initContainer := corev1.Container{
@ -220,14 +258,14 @@ func (r *RestoreSessionReconciler) CreateRestoreInitContainer(target formolv1alp
} }
repo := &formolv1alpha1.Repo{} repo := &formolv1alpha1.Repo{}
if err := r.Get(ctx, client.ObjectKey{ if err := r.Get(ctx, client.ObjectKey{
Namespace: r.BackupConf.Namespace, Namespace: backupConf.Namespace,
Name: r.BackupConf.Spec.Repository, Name: backupConf.Spec.Repository,
}, repo); err != nil { }, repo); err != nil {
log.Error(err, "unable to get Repo from BackupConfiguration") log.Error(err, "unable to get Repo from BackupConfiguration")
return err return err
} }
// S3 backing storage // S3 backing storage
initContainer.Env = append(initContainer.Env, formolutils.ConfigureResticEnvVar(r.BackupConf, repo)...) initContainer.Env = append(initContainer.Env, formolutils.ConfigureResticEnvVar(backupConf, repo)...)
deployment.Spec.Template.Spec.InitContainers = append([]corev1.Container{initContainer}, deployment.Spec.Template.Spec.InitContainers = append([]corev1.Container{initContainer},
deployment.Spec.Template.Spec.InitContainers...) deployment.Spec.Template.Spec.InitContainers...)
if err := r.Update(ctx, deployment); err != nil { if err := r.Update(ctx, deployment); err != nil {
@ -236,31 +274,28 @@ func (r *RestoreSessionReconciler) CreateRestoreInitContainer(target formolv1alp
} }
return nil return nil
} }
func (r *RestoreSessionReconciler) StatusUpdate() error {
log := r.Log.WithValues("statusupdate", r.RestoreSession.Name)
ctx := context.Background()
startNextTask := func() (*formolv1alpha1.TargetStatus, error) { startNextTask := func() (*formolv1alpha1.TargetStatus, error) {
nextTarget := len(r.RestoreSession.Status.Targets) nextTarget := len(restoreSession.Status.Targets)
if nextTarget < len(r.BackupConf.Spec.Targets) { if nextTarget < len(backupConf.Spec.Targets) {
target := r.BackupConf.Spec.Targets[nextTarget] target := backupConf.Spec.Targets[nextTarget]
targetStatus := formolv1alpha1.TargetStatus{ targetStatus := formolv1alpha1.TargetStatus{
Name: target.Name, Name: target.Name,
Kind: target.Kind, Kind: target.Kind,
SessionState: formolv1alpha1.New, SessionState: formolv1alpha1.New,
StartTime: &metav1.Time{Time: time.Now()}, 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 { switch target.Kind {
case "Deployment": case formolv1alpha1.SidecarKind:
if err := r.CreateRestoreInitContainer(target); err != nil { if err := createRestoreInitContainer(target); err != nil {
log.V(0).Info("unable to create restore init container", "task", target) log.V(0).Info("unable to create restore init container", "task", target)
targetStatus.SessionState = formolv1alpha1.Failure targetStatus.SessionState = formolv1alpha1.Failure
return nil, err return nil, err
} }
case "Task": case formolv1alpha1.JobKind:
if err := r.CreateRestoreJob(target); err != nil { if err := createRestoreJob(target); err != nil {
log.V(0).Info("unable to create restore job", "task", target) log.V(0).Info("unable to create restore job", "task", target)
targetStatus.SessionState = formolv1alpha1.Failure targetStatus.SessionState = formolv1alpha1.Failure
return nil, err return nil, err
@ -271,110 +306,71 @@ func (r *RestoreSessionReconciler) StatusUpdate() error {
return nil, nil return nil, nil
} }
} }
endTask := func() error { 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 { switch target.Kind {
case "Deployment": case formolv1alpha1.SidecarKind:
if err := r.DeleteRestoreInitContainer(target); err != nil { if err := deleteRestoreInitContainer(target); err != nil {
log.Error(err, "unable to delete restore init container") log.Error(err, "unable to delete restore init container")
return err return err
} }
} }
return nil return nil
} }
switch r.RestoreSession.Status.SessionState {
switch restoreSession.Status.SessionState {
case formolv1alpha1.New: case formolv1alpha1.New:
r.RestoreSession.Status.SessionState = formolv1alpha1.Running restoreSession.Status.SessionState = formolv1alpha1.Running
targetStatus, err := startNextTask() if targetStatus, err := startNextTask(); err != nil {
if err != nil { log.Error(err, "unable to start next restore task")
return err return ctrl.Result{}, err
} } else {
log.V(0).Info("New restore. Start the first task", "task", targetStatus.Name) log.V(0).Info("New restore. Start the first task", "task", targetStatus.Name)
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") log.Error(err, "unable to update restoresession")
return err return ctrl.Result{}, err
}
} }
case formolv1alpha1.Running: 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 { switch currentTargetStatus.SessionState {
case formolv1alpha1.Failure: case formolv1alpha1.Failure:
log.V(0).Info("last restore task failed. Stop here", "target", currentTargetStatus.Name) log.V(0).Info("last restore task failed. Stop here", "target", currentTargetStatus.Name)
r.RestoreSession.Status.SessionState = formolv1alpha1.Failure restoreSession.Status.SessionState = formolv1alpha1.Failure
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") log.Error(err, "unable to update restoresession")
return err return ctrl.Result{}, err
} }
case formolv1alpha1.Running: case formolv1alpha1.Running:
log.V(0).Info("task is still running", "target", currentTargetStatus.Name) log.V(0).Info("task is still running", "target", currentTargetStatus.Name)
return nil return ctrl.Result{}, nil
case formolv1alpha1.Success: case formolv1alpha1.Success:
_ = endTask() _ = endTask()
log.V(0).Info("last task was a success. start a new one", "target", currentTargetStatus) log.V(0).Info("last task was a success. start a new one", "target", currentTargetStatus)
targetStatus, err := startNextTask() targetStatus, err := startNextTask()
if err != nil { if err != nil {
return err return ctrl.Result{}, err
} }
if targetStatus == nil { if targetStatus == nil {
// No more task to start. The restore is over // No more task to start. The restore is over
r.RestoreSession.Status.SessionState = formolv1alpha1.Success restoreSession.Status.SessionState = formolv1alpha1.Success
if err := r.Status().Update(ctx, r.RestoreSession); err != nil {
log.Error(err, "unable to update restoresession")
return err
} }
} if err := r.Status().Update(ctx, restoreSession); err != nil {
if err := r.Status().Update(ctx, r.RestoreSession); err != nil {
log.Error(err, "unable to update restoresession")
return 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") log.Error(err, "unable to update restoresession")
return ctrl.Result{}, err return ctrl.Result{}, err
} }
}
return reschedule, nil 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 ctrl.Result{}, nil
} }
func (r *RestoreSessionReconciler) SetupWithManager(mgr ctrl.Manager) error { func (r *RestoreSessionReconciler) SetupWithManager(mgr ctrl.Manager) error {

View File

@ -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))
})
})
})

View File

@ -44,10 +44,16 @@ import (
// These tests use Ginkgo (BDD-style Go testing framework). Refer to // These tests use Ginkgo (BDD-style Go testing framework). Refer to
// http://onsi.github.io/ginkgo/ to learn more about Ginkgo. // http://onsi.github.io/ginkgo/ to learn more about Ginkgo.
const ( const (
BackupFuncName = "test-backup-func" TestBackupFuncName = "test-backup-func"
TestFunc = "test-norestore-func"
TestRestoreFuncName = "test-restore-func"
TestNamespace = "test-namespace" TestNamespace = "test-namespace"
RepoName = "test-repo" TestRepoName = "test-repo"
DeploymentName = "test-deployment" TestDeploymentName = "test-deployment"
TestBackupConfName = "test-backupconf"
TestBackupSessionName = "test-backupsession"
TestDataVolume = "data"
TestDataMountPath = "/data"
timeout = time.Second * 10 timeout = time.Second * 10
interval = time.Millisecond * 250 interval = time.Millisecond * 250
) )
@ -64,7 +70,7 @@ var (
} }
deployment = &appsv1.Deployment{ deployment = &appsv1.Deployment{
ObjectMeta: metav1.ObjectMeta{ ObjectMeta: metav1.ObjectMeta{
Name: DeploymentName, Name: TestDeploymentName,
Namespace: TestNamespace, Namespace: TestNamespace,
}, },
Spec: appsv1.DeploymentSpec{ Spec: appsv1.DeploymentSpec{
@ -82,6 +88,11 @@ var (
Image: "test-image", Image: "test-image",
}, },
}, },
Volumes: []corev1.Volume{
corev1.Volume{
Name: TestDataVolume,
},
},
}, },
}, },
}, },
@ -105,7 +116,7 @@ var (
} }
repo = &formolv1alpha1.Repo{ repo = &formolv1alpha1.Repo{
ObjectMeta: metav1.ObjectMeta{ ObjectMeta: metav1.ObjectMeta{
Name: RepoName, Name: TestRepoName,
Namespace: TestNamespace, Namespace: TestNamespace,
}, },
Spec: formolv1alpha1.RepoSpec{ Spec: formolv1alpha1.RepoSpec{
@ -120,7 +131,29 @@ var (
} }
function = &formolv1alpha1.Function{ function = &formolv1alpha1.Function{
ObjectMeta: metav1.ObjectMeta{ 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, Namespace: TestNamespace,
}, },
Spec: corev1.Container{ Spec: corev1.Container{
@ -129,6 +162,66 @@ var (
Args: []string{"a", "set", "of", "args"}, 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) { func TestAPIs(t *testing.T) {
@ -168,6 +261,20 @@ var _ = BeforeSuite(func() {
}).SetupWithManager(k8sManager) }).SetupWithManager(k8sManager)
Expect(err).ToNot(HaveOccurred()) 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() { go func() {
err = k8sManager.Start(ctrl.SetupSignalHandler()) err = k8sManager.Start(ctrl.SetupSignalHandler())
Expect(err).ToNot(HaveOccurred()) Expect(err).ToNot(HaveOccurred())
@ -182,6 +289,32 @@ var _ = BeforeSuite(func() {
Expect(k8sClient.Create(ctx, repo)).Should(Succeed()) Expect(k8sClient.Create(ctx, repo)).Should(Succeed())
Expect(k8sClient.Create(ctx, deployment)).Should(Succeed()) Expect(k8sClient.Create(ctx, deployment)).Should(Succeed())
Expect(k8sClient.Create(ctx, function)).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) }, 60)
var _ = AfterSuite(func() { var _ = AfterSuite(func() {

View File

@ -102,6 +102,10 @@ func main() {
setupLog.Error(err, "unable to create controller", "controller", "RestoreSession") setupLog.Error(err, "unable to create controller", "controller", "RestoreSession")
os.Exit(1) 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 // +kubebuilder:scaffold:builder
setupLog.Info("starting manager") setupLog.Info("starting manager")

View File

@ -8,6 +8,8 @@ metadata:
app: nginx app: nginx
spec: spec:
replicas: 1 replicas: 1
strategy:
type: Recreate
selector: selector:
matchLabels: matchLabels:
app: nginx app: nginx

View File

@ -8,7 +8,7 @@ spec:
repository: repo-minio repository: repo-minio
schedule: "15 * * * *" schedule: "15 * * * *"
targets: targets:
- kind: Deployment - kind: Sidecar
apiVersion: v1 apiVersion: v1
name: nginx-deployment name: nginx-deployment
volumeMounts: volumeMounts:
@ -16,11 +16,10 @@ spec:
mountPath: /data mountPath: /data
paths: paths:
- /data - /data
- kind: Task - kind: Job
name: backup-pg name: backup-pg
steps: steps:
- name: backup-pg - name: backup-pg
namespace: demo
env: env:
- name: PGHOST - name: PGHOST
value: postgres value: postgres