Compare commits

..

10 Commits

31 changed files with 1618 additions and 1134 deletions

1
.gitignore vendored
View File

@ -20,6 +20,7 @@ PROJECT
bin/ bin/
config/* config/*
!config/crd !config/crd
!config/default
config/crd/bases config/crd/bases
!config/samples !config/samples
test/restic test/restic

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

@ -17,23 +17,16 @@ limitations under the License.
package v1alpha1 package v1alpha1
import ( import (
corev1 "k8s.io/api/core/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
) )
// 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 corev1.ObjectReference `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 +34,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

@ -7,14 +7,26 @@ import (
type SessionState string type SessionState string
const ( const (
New SessionState = "New" New SessionState = "New"
Running SessionState = "Running" Init SessionState = "Initializing"
Success SessionState = "Success" Running SessionState = "Running"
Failure SessionState = "Failure" Waiting SessionState = "Waiting"
Deleted SessionState = "Deleted" Finalize SessionState = "Finalizing"
TARGET_NAME string = "TARGET_NAME" Success SessionState = "Success"
RESTORESESSION_NAMESPACE string = "RESTORESESSION_NAMESPACE" Failure SessionState = "Failure"
RESTORESESSION_NAME string = "RESTORESESSION_NAME" Deleted SessionState = "Deleted"
// Environment variables used by the sidecar container
RESTORE_ANNOTATION = "restore"
// the name of the sidecar container
SIDECARCONTAINER_NAME string = "formol"
// Used by both the backupsession and restoresession controllers to identified the target deployment
TARGET_NAME string = "TARGET_NAME"
// Used by restoresession controller
RESTORESESSION_NAMESPACE string = "RESTORESESSION_NAMESPACE"
RESTORESESSION_NAME string = "RESTORESESSION_NAME"
// Used by the backupsession controller
POD_NAME string = "POD_NAME"
POD_NAMESPACE string = "POD_NAMESPACE"
) )
type TargetStatus struct { type TargetStatus struct {
@ -28,4 +40,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

@ -17,25 +17,30 @@ limitations under the License.
package v1alpha1 package v1alpha1
import ( import (
corev1 "k8s.io/api/core/v1"
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! type BackupSessionRef struct {
// NOTE: json tags are required. Any new fields you add must have json tags for the fields to be serialized. // +optional
Ref corev1.ObjectReference `json:"ref,omitempty"`
// +optional
Spec BackupSessionSpec `json:"spec,omitempty"`
// +optional
Status BackupSessionStatus `json:"status,omitempty"`
}
// RestoreSessionSpec defines the desired state of RestoreSession // RestoreSessionSpec defines the desired state of RestoreSession
type RestoreSessionSpec struct { type RestoreSessionSpec struct {
// INSERT ADDITIONAL SPEC FIELDS - desired state of cluster BackupSessionRef `json:"backupSession"`
// Important: Run "make" to regenerate code after modifying this file //Ref string `json:"backupSessionRef"`
// +optional
BackupSessionRef metav1.ObjectMeta `json:"backupSessionRef"` //Targets []TargetStatus `json:"target,omitempty"`
} }
// RestoreSessionStatus defines the observed state of RestoreSession // RestoreSessionStatus defines the observed state of RestoreSession
type RestoreSessionStatus struct { type RestoreSessionStatus struct {
// INSERT ADDITIONAL STATUS FIELD - define observed state of cluster
// Important: Run "make" to regenerate code after modifying this file
ObservedGeneration int64 `json:"observedGeneration,omitempty"`
// +optional // +optional
SessionState `json:"state,omitempty"` SessionState `json:"state,omitempty"`
// +optional // +optional

View File

@ -207,6 +207,24 @@ func (in *BackupSessionList) DeepCopyObject() runtime.Object {
return nil return nil
} }
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *BackupSessionRef) DeepCopyInto(out *BackupSessionRef) {
*out = *in
out.Ref = in.Ref
out.Spec = in.Spec
in.Status.DeepCopyInto(&out.Status)
}
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new BackupSessionRef.
func (in *BackupSessionRef) DeepCopy() *BackupSessionRef {
if in == nil {
return nil
}
out := new(BackupSessionRef)
in.DeepCopyInto(out)
return out
}
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. // 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
@ -342,21 +360,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
@ -573,6 +576,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 +596,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

@ -15,16 +15,16 @@ patchesStrategicMerge:
#- 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,17 +1,20 @@
# 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==
service: service:
namespace: system namespace: system
name: webhook-service name: webhook-service
path: /convert path: /convert

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,9 +18,9 @@ bases:
- ../manager - ../manager
# [WEBHOOK] To enable webhook, uncomment all the sections with [WEBHOOK] prefix including the one in # [WEBHOOK] To enable webhook, uncomment all the sections with [WEBHOOK] prefix including the one in
# crd/kustomization.yaml # crd/kustomization.yaml
- ../webhook #- ../webhook
# [CERTMANAGER] To enable cert-manager, uncomment all sections with 'CERTMANAGER'. 'WEBHOOK' components are required. # [CERTMANAGER] To enable cert-manager, uncomment all sections with 'CERTMANAGER'. 'WEBHOOK' components are required.
- ../certmanager #- ../certmanager
# [PROMETHEUS] To enable prometheus monitor, uncomment all sections with 'PROMETHEUS'. # [PROMETHEUS] To enable prometheus monitor, uncomment all sections with 'PROMETHEUS'.
#- ../prometheus #- ../prometheus
@ -32,39 +32,39 @@ patchesStrategicMerge:
# [WEBHOOK] To enable webhook, uncomment all the sections with [WEBHOOK] prefix including the one in # [WEBHOOK] To enable webhook, uncomment all the sections with [WEBHOOK] prefix including the one in
# crd/kustomization.yaml # crd/kustomization.yaml
- manager_webhook_patch.yaml #- manager_webhook_patch.yaml
# [CERTMANAGER] To enable cert-manager, uncomment all sections with 'CERTMANAGER'. # [CERTMANAGER] To enable cert-manager, uncomment all sections with 'CERTMANAGER'.
# Uncomment 'CERTMANAGER' sections in crd/kustomization.yaml to enable the CA injection in the admission webhooks. # Uncomment 'CERTMANAGER' sections in crd/kustomization.yaml to enable the CA injection in the admission webhooks.
# 'CERTMANAGER' needs to be enabled to use ca injection # 'CERTMANAGER' needs to be enabled to use ca injection
- webhookcainjection_patch.yaml #- webhookcainjection_patch.yaml
# the following config is for teaching kustomize how to do var substitution # the following config is for teaching kustomize how to do var substitution
vars: vars:
# [CERTMANAGER] To enable cert-manager, uncomment all sections with 'CERTMANAGER' prefix. # [CERTMANAGER] To enable cert-manager, uncomment all sections with 'CERTMANAGER' prefix.
- name: CERTIFICATE_NAMESPACE # namespace of the certificate CR #- name: CERTIFICATE_NAMESPACE # namespace of the certificate CR
objref: # objref:
kind: Certificate # kind: Certificate
group: cert-manager.io # group: cert-manager.io
version: v1alpha2 # version: v1alpha2
name: serving-cert # this name should match the one in certificate.yaml # name: serving-cert # this name should match the one in certificate.yaml
fieldref: # fieldref:
fieldpath: metadata.namespace # fieldpath: metadata.namespace
- name: CERTIFICATE_NAME #- name: CERTIFICATE_NAME
objref: # objref:
kind: Certificate # kind: Certificate
group: cert-manager.io # group: cert-manager.io
version: v1alpha2 # version: v1alpha2
name: serving-cert # this name should match the one in certificate.yaml # name: serving-cert # this name should match the one in certificate.yaml
- name: SERVICE_NAMESPACE # namespace of the service #- name: SERVICE_NAMESPACE # namespace of the service
objref: # objref:
kind: Service # kind: Service
version: v1 # version: v1
name: webhook-service # name: webhook-service
fieldref: # fieldref:
fieldpath: metadata.namespace # fieldpath: metadata.namespace
- name: SERVICE_NAME #- name: SERVICE_NAME
objref: # objref:
kind: Service # kind: Service
version: v1 # version: v1
name: webhook-service # name: webhook-service

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,263 +57,108 @@ func (r *BackupConfigurationReconciler) getDeployment(namespace string, name str
// +kubebuilder:rbac:groups=batch,resources=cronjobs,verbs=get;list;watch;create;update;patch;delete // +kubebuilder:rbac:groups=batch,resources=cronjobs,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) var changed bool
if err != nil { ctx := context.Background()
return err log := r.Log.WithValues("backupconfiguration", req.NamespacedName)
} //time.Sleep(300 * time.Millisecond)
restorecontainers := []corev1.Container{}
for _, container := range deployment.Spec.Template.Spec.Containers { log.V(1).Info("Enter Reconcile with req", "req", req, "reconciler", r)
if container.Name == "backup" {
continue backupConf := &formolv1alpha1.BackupConfiguration{}
} if err := r.Get(ctx, req.NamespacedName, backupConf); err != nil {
restorecontainers = append(restorecontainers, container) return ctrl.Result{}, client.IgnoreNotFound(err)
}
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{} _ = formolrbac.DeleteFormolRBAC(r.Client, "default", backupConf.Namespace)
if err := r.Get(context.Background(), client.ObjectKey{ _ = formolrbac.DeleteBackupSessionCreatorRBAC(r.Client, backupConf.Namespace)
Namespace: backupConf.Namespace, cronjob := &kbatch_beta1.CronJob{}
Name: backupConf.Spec.Repository, if err := r.Get(context.Background(), client.ObjectKey{
}, repo); err != nil {
log.Error(err, "unable to get Repo from BackupConfiguration")
return err
}
sidecar.Env = append(sidecar.Env, formolutils.ConfigureResticEnvVar(backupConf, repo)...)
for _, volumemount := range target.VolumeMounts {
log.V(1).Info("mounts", "volumemount", volumemount)
volumemount.ReadOnly = true
sidecar.VolumeMounts = append(sidecar.VolumeMounts, *volumemount.DeepCopy())
}
selector, err := metav1.LabelSelectorAsMap(deployment.Spec.Selector)
if err != nil {
log.Error(err, "unable to get LableSelector for deployment", "label", deployment.Spec.Selector)
return nil
}
log.V(1).Info("getting pods matching label", "label", selector)
pods := &corev1.PodList{}
err = r.List(context.Background(), pods, client.InNamespace(backupConf.Namespace), client.MatchingLabels(selector))
if err != nil {
log.Error(err, "unable to get deployment pods")
return nil
}
replicasToDelete := []appsv1.ReplicaSet{}
log.V(1).Info("got that list of pods", "pods", len(pods.Items))
for _, pod := range pods.Items {
log.V(1).Info("checking pod", "pod", pod)
for _, podRef := range pod.OwnerReferences {
rs := &appsv1.ReplicaSet{}
if err := r.Get(context.Background(), client.ObjectKey{
Name: podRef.Name,
Namespace: pod.Namespace,
}, rs); err != nil {
log.Error(err, "unable to get replicaset", "replicaset", podRef.Name)
return nil
}
log.V(1).Info("got a replicaset", "rs", rs.Name)
for _, rsRef := range rs.OwnerReferences {
if rsRef.Kind == deployment.Kind && rsRef.Name == deployment.Name {
log.V(0).Info("Adding pod to the list of pods to be restarted", "pod", pod.Name)
replicasToDelete = append(replicasToDelete, *rs)
}
}
}
}
deployment.Spec.Template.Spec.Containers = append(deployment.Spec.Template.Spec.Containers, sidecar)
deployment.Spec.Template.Spec.ShareProcessNamespace = func() *bool { b := true; return &b }()
if err := formolrbac.CreateFormolRBAC(r.Client, deployment.Spec.Template.Spec.ServiceAccountName, deployment.Namespace); err != nil {
log.Error(err, "unable to create backupsessionlistener RBAC")
return nil
}
log.V(0).Info("Adding a sicar container")
if err := r.Update(context.Background(), deployment); err != nil {
log.Error(err, "unable to update the Deployment")
return err
}
for _, replica := range replicasToDelete {
if err := r.Delete(context.TODO(), &replica); err != nil {
log.Error(err, "unable to delete replica", "replica", replica.Name)
return nil
}
}
return nil
}
func (r *BackupConfigurationReconciler) deleteCronJob(backupConf *formolv1alpha1.BackupConfiguration) error {
log := r.Log.WithValues("deleteCronJob", backupConf.Name)
_ = formolrbac.DeleteFormolRBAC(r.Client, "default", backupConf.Namespace)
_ = formolrbac.DeleteBackupSessionCreatorRBAC(r.Client, backupConf.Namespace)
cronjob := &kbatch_beta1.CronJob{}
if err := r.Get(context.Background(), client.ObjectKey{
Namespace: backupConf.Namespace,
Name: "backup-" + backupConf.Name,
}, cronjob); err == nil {
log.V(0).Info("Deleting cronjob", "cronjob", cronjob.Name)
return r.Delete(context.TODO(), cronjob)
} else {
return err
}
}
func (r *BackupConfigurationReconciler) addCronJob(backupConf *formolv1alpha1.BackupConfiguration) error {
log := r.Log.WithValues("addCronJob", backupConf.Name)
if err := formolrbac.CreateFormolRBAC(r.Client, "default", backupConf.Namespace); err != nil {
log.Error(err, "unable to create backupsessionlistener RBAC")
return nil
}
if err := formolrbac.CreateBackupSessionCreatorRBAC(r.Client, backupConf.Namespace); err != nil {
log.Error(err, "unable to create backupsession-creator RBAC")
return nil
}
cronjob := &kbatch_beta1.CronJob{}
if err := r.Get(context.Background(), client.ObjectKey{
Namespace: backupConf.Namespace,
Name: "backup-" + backupConf.Name,
}, cronjob); err == nil {
log.V(0).Info("there is already a cronjob")
var changed bool
if backupConf.Spec.Schedule != cronjob.Spec.Schedule {
log.V(0).Info("cronjob schedule has changed", "old schedule", cronjob.Spec.Schedule, "new schedule", backupConf.Spec.Schedule)
cronjob.Spec.Schedule = backupConf.Spec.Schedule
changed = true
}
if backupConf.Spec.Suspend != cronjob.Spec.Suspend {
log.V(0).Info("cronjob suspend has changed", "before", cronjob.Spec.Suspend, "new", backupConf.Spec.Suspend)
cronjob.Spec.Suspend = backupConf.Spec.Suspend
changed = true
}
if changed == true {
if err := r.Update(context.TODO(), cronjob); err != nil {
log.Error(err, "unable to update cronjob definition")
return err
}
}
return nil
} else if errors.IsNotFound(err) == false {
log.Error(err, "something went wrong")
return err
}
cronjob = &kbatch_beta1.CronJob{
ObjectMeta: metav1.ObjectMeta{
Name: "backup-" + backupConf.Name,
Namespace: backupConf.Namespace, Namespace: backupConf.Namespace,
}, Name: "backup-" + backupConf.Name,
Spec: kbatch_beta1.CronJobSpec{ }, cronjob); err == nil {
Suspend: backupConf.Spec.Suspend, log.V(0).Info("Deleting cronjob", "cronjob", cronjob.Name)
Schedule: backupConf.Spec.Schedule, return r.Delete(context.TODO(), cronjob)
JobTemplate: kbatch_beta1.JobTemplateSpec{ } else {
Spec: batchv1.JobSpec{ return err
Template: corev1.PodTemplateSpec{ }
Spec: corev1.PodSpec{ }
RestartPolicy: corev1.RestartPolicyOnFailure,
ServiceAccountName: "backupsession-creator", addCronJob := func() error {
Containers: []corev1.Container{ if err := formolrbac.CreateFormolRBAC(r.Client, "default", backupConf.Namespace); err != nil {
corev1.Container{ log.Error(err, "unable to create backupsessionlistener RBAC")
Name: "job-createbackupsession-" + backupConf.Name, return nil
Image: "desmo999r/formolcli:latest", }
Args: []string{
"backupsession", if err := formolrbac.CreateBackupSessionCreatorRBAC(r.Client, backupConf.Namespace); err != nil {
"create", log.Error(err, "unable to create backupsession-creator RBAC")
"--namespace", return nil
backupConf.Namespace, }
"--name",
backupConf.Name, cronjob := &kbatch_beta1.CronJob{}
if err := r.Get(context.Background(), client.ObjectKey{
Namespace: backupConf.Namespace,
Name: "backup-" + backupConf.Name,
}, cronjob); err == nil {
log.V(0).Info("there is already a cronjob")
if backupConf.Spec.Schedule != cronjob.Spec.Schedule {
log.V(0).Info("cronjob schedule has changed", "old schedule", cronjob.Spec.Schedule, "new schedule", backupConf.Spec.Schedule)
cronjob.Spec.Schedule = backupConf.Spec.Schedule
changed = true
}
if backupConf.Spec.Suspend != nil && backupConf.Spec.Suspend != cronjob.Spec.Suspend {
log.V(0).Info("cronjob suspend has changed", "before", cronjob.Spec.Suspend, "new", backupConf.Spec.Suspend)
cronjob.Spec.Suspend = backupConf.Spec.Suspend
changed = true
}
if changed == true {
if err := r.Update(context.TODO(), cronjob); err != nil {
log.Error(err, "unable to update cronjob definition")
return err
}
}
return nil
} else if errors.IsNotFound(err) == false {
log.Error(err, "something went wrong")
return err
}
cronjob = &kbatch_beta1.CronJob{
ObjectMeta: metav1.ObjectMeta{
Name: "backup-" + backupConf.Name,
Namespace: backupConf.Namespace,
},
Spec: kbatch_beta1.CronJobSpec{
Suspend: backupConf.Spec.Suspend,
Schedule: backupConf.Spec.Schedule,
JobTemplate: kbatch_beta1.JobTemplateSpec{
Spec: batchv1.JobSpec{
Template: corev1.PodTemplateSpec{
Spec: corev1.PodSpec{
RestartPolicy: corev1.RestartPolicyOnFailure,
ServiceAccountName: "backupsession-creator",
Containers: []corev1.Container{
corev1.Container{
Name: "job-createbackupsession-" + backupConf.Name,
Image: "desmo999r/formolcli:latest",
Args: []string{
"backupsession",
"create",
"--namespace",
backupConf.Namespace,
"--name",
backupConf.Name,
},
}, },
}, },
}, },
@ -331,102 +166,199 @@ func (r *BackupConfigurationReconciler) addCronJob(backupConf *formolv1alpha1.Ba
}, },
}, },
}, },
}, }
if err := ctrl.SetControllerReference(backupConf, cronjob, r.Scheme); err != nil {
log.Error(err, "unable to set controller on job", "cronjob", cronjob, "backupconf", backupConf)
return err
}
log.V(0).Info("creating the cronjob")
if err := r.Create(context.Background(), cronjob); err != nil {
log.Error(err, "unable to create the cronjob", "cronjob", cronjob)
return err
} else {
changed = true
return nil
}
} }
if err := ctrl.SetControllerReference(backupConf, cronjob, r.Scheme); err != nil {
log.Error(err, "unable to set controller on job", "cronjob", cronjob, "backupconf", backupConf) deleteSidecarContainer := func(target formolv1alpha1.Target) error {
return err deployment, err := getDeployment(backupConf.Namespace, target.Name)
if err != nil {
return err
}
restorecontainers := []corev1.Container{}
for _, container := range deployment.Spec.Template.Spec.Containers {
if container.Name == formolv1alpha1.SIDECARCONTAINER_NAME {
continue
}
restorecontainers = append(restorecontainers, container)
}
deployment.Spec.Template.Spec.Containers = restorecontainers
if err := r.Update(context.Background(), deployment); err != nil {
return err
}
if err := formolrbac.DeleteFormolRBAC(r.Client, deployment.Spec.Template.Spec.ServiceAccountName, deployment.Namespace); err != nil {
return err
}
return nil
} }
log.V(0).Info("creating the cronjob")
if err := r.Create(context.Background(), cronjob); err != nil { addSidecarContainer := func(target formolv1alpha1.Target) error {
log.Error(err, "unable to create the cronjob", "cronjob", cronjob) deployment, err := getDeployment(backupConf.Namespace, target.Name)
return err if err != nil {
log.Error(err, "unable to get Deployment")
return err
}
log.V(1).Info("got deployment", "Deployment", deployment)
for _, container := range deployment.Spec.Template.Spec.Containers {
if container.Name == formolv1alpha1.SIDECARCONTAINER_NAME {
log.V(0).Info("There is already a backup sidecar container. Skipping", "container", container)
return nil
}
}
sidecar := corev1.Container{
Name: formolv1alpha1.SIDECARCONTAINER_NAME,
// TODO: Put the image in the BackupConfiguration YAML file
Image: "desmo999r/formolcli:latest",
Args: []string{"backupsession", "server"},
//Image: "busybox",
//Command: []string{
// "sh",
// "-c",
// "sleep 3600; echo done",
//},
Env: []corev1.EnvVar{
corev1.EnvVar{
Name: formolv1alpha1.POD_NAME,
ValueFrom: &corev1.EnvVarSource{
FieldRef: &corev1.ObjectFieldSelector{
FieldPath: "metadata.name",
},
},
},
corev1.EnvVar{
Name: formolv1alpha1.POD_NAMESPACE,
ValueFrom: &corev1.EnvVarSource{
FieldRef: &corev1.ObjectFieldSelector{
FieldPath: "metadata.namespace",
},
},
},
corev1.EnvVar{
Name: formolv1alpha1.TARGET_NAME,
Value: target.Name,
},
},
VolumeMounts: []corev1.VolumeMount{},
}
// Gather information from the repo
repo := &formolv1alpha1.Repo{}
if err := r.Get(context.Background(), client.ObjectKey{
Namespace: backupConf.Namespace,
Name: backupConf.Spec.Repository,
}, repo); err != nil {
log.Error(err, "unable to get Repo from BackupConfiguration")
return err
}
sidecar.Env = append(sidecar.Env, formolutils.ConfigureResticEnvVar(backupConf, repo)...)
for _, volumemount := range target.VolumeMounts {
log.V(1).Info("mounts", "volumemount", volumemount)
volumemount.ReadOnly = true
sidecar.VolumeMounts = append(sidecar.VolumeMounts, *volumemount.DeepCopy())
}
deployment.Spec.Template.Spec.Containers = append(deployment.Spec.Template.Spec.Containers, sidecar)
deployment.Spec.Template.Spec.ShareProcessNamespace = func() *bool { b := true; return &b }()
if err := formolrbac.CreateFormolRBAC(r.Client, deployment.Spec.Template.Spec.ServiceAccountName, deployment.Namespace); err != nil {
log.Error(err, "unable to create backupsessionlistener RBAC")
return nil
}
log.V(0).Info("Adding a sicar container")
if err := r.Update(context.Background(), deployment); err != nil {
log.Error(err, "unable to update the Deployment")
return err
} else {
changed = true
return nil
}
} }
return nil
}
func (r *BackupConfigurationReconciler) Reconcile(req ctrl.Request) (ctrl.Result, error) { deleteExternalResources := func() error {
ctx := context.Background() for _, target := range backupConf.Spec.Targets {
log := r.Log.WithValues("backupconfiguration", req.NamespacedName) switch target.Kind {
time.Sleep(300 * time.Millisecond) case formolv1alpha1.SidecarKind:
_ = deleteSidecarContainer(target)
log.V(1).Info("Enter Reconcile with req", "req", req) }
}
backupConf := &formolv1alpha1.BackupConfiguration{} // TODO: remove the hardcoded "default"
if err := r.Get(ctx, req.NamespacedName, backupConf); err != nil { _ = deleteCronJob()
return ctrl.Result{}, client.IgnoreNotFound(err) 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 { log.Error(err, "unable to remove finalizer")
log.Error(err, "unable to remove finalizer") return ctrl.Result{}, err
return ctrl.Result{}, err }
} }
// We have been deleted. Return here // We have been deleted. Return here
log.V(0).Info("backupconf deleted", "backupconf", backupConf.Name) log.V(0).Info("backupconf deleted", "backupconf", backupConf.Name)
return ctrl.Result{}, nil return ctrl.Result{}, nil
} }
if err := r.addCronJob(backupConf); err != nil { // Add finalizer
return ctrl.Result{}, nil if !formolutils.ContainsString(backupConf.ObjectMeta.Finalizers, finalizerName) {
} backupConf.ObjectMeta.Finalizers = append(backupConf.ObjectMeta.Finalizers, finalizerName)
backupConf.Status.ActiveCronJob = true err := r.Update(context.Background(), backupConf)
if err != nil {
for _, target := range backupConf.Spec.Targets { log.Error(err, "unable to append finalizer")
switch target.Kind {
case "Deployment":
if err := r.addSidecarContainer(backupConf, target); err != nil {
return ctrl.Result{}, client.IgnoreNotFound(err)
}
backupConf.Status.ActiveSidecar = true
case "PersistentVolumeClaim":
log.V(0).Info("TODO backup PVC")
return ctrl.Result{}, nil
} }
}
backupConf.Status.Suspended = false
log.V(1).Info("updating backupconf")
if err := r.Status().Update(ctx, backupConf); err != nil {
log.Error(err, "unable to update backupconf", "backupconf", backupConf)
return ctrl.Result{}, err return ctrl.Result{}, err
} }
return ctrl.Result{}, nil if err := addCronJob(); err != nil {
} return ctrl.Result{}, nil
} else {
backupConf.Status.ActiveCronJob = true
}
func (r *BackupConfigurationReconciler) deleteExternalResources(backupConf *formolv1alpha1.BackupConfiguration) error {
for _, target := range backupConf.Spec.Targets { for _, target := range backupConf.Spec.Targets {
switch target.Kind { switch target.Kind {
case "Deployment": case formolv1alpha1.SidecarKind:
_ = r.deleteSidecarContainer(backupConf, target) if err := addSidecarContainer(target); err != nil {
return ctrl.Result{}, client.IgnoreNotFound(err)
} else {
backupConf.Status.ActiveSidecar = true
}
} }
} }
// TODO: remove the hardcoded "default"
_ = r.deleteCronJob(backupConf) //backupConf.Status.Suspended = false
return nil if changed == true {
log.V(1).Info("updating backupconf")
if err := r.Status().Update(ctx, backupConf); err != nil {
log.Error(err, "unable to update backupconf", "backupconf", backupConf)
return ctrl.Result{}, err
}
}
return ctrl.Result{}, 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

@ -46,214 +46,8 @@ const (
// BackupSessionReconciler reconciles a BackupSession object // BackupSessionReconciler reconciles a BackupSession object
type BackupSessionReconciler struct { 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 {
log := r.Log.WithValues("backupsession-statusupdate", r.BackupSession)
ctx := context.Background()
r.BackupConf = &formolv1alpha1.BackupConfiguration{}
if err := r.Get(ctx, client.ObjectKey{
Namespace: r.BackupSession.Namespace,
Name: r.BackupSession.Spec.Ref.Name}, r.BackupConf); err != nil {
log.Error(err, "unable to get backupConfiguration")
return client.IgnoreNotFound(err)
}
// start the next task
startNextTask := func() (*formolv1alpha1.TargetStatus, error) {
nextTarget := len(r.BackupSession.Status.Targets)
if nextTarget < len(r.BackupConf.Spec.Targets) {
target := r.BackupConf.Spec.Targets[nextTarget]
targetStatus := formolv1alpha1.TargetStatus{
Name: target.Name,
Kind: target.Kind,
SessionState: formolv1alpha1.New,
StartTime: &metav1.Time{Time: time.Now()},
}
r.BackupSession.Status.Targets = append(r.BackupSession.Status.Targets, targetStatus)
switch target.Kind {
case "Task":
if err := r.CreateBackupJob(target); err != nil {
log.V(0).Info("unable to create task", "task", target)
targetStatus.SessionState = formolv1alpha1.Failure
return nil, err
}
}
return &targetStatus, nil
} else {
return nil, nil
}
}
// Test the backupsession backupstate to decide what to do
switch r.BackupSession.Status.SessionState {
case formolv1alpha1.New:
// Brand new backupsession; start the first task
r.BackupSession.Status.SessionState = formolv1alpha1.Running
targetStatus, err := startNextTask()
if err != nil {
return err
}
log.V(0).Info("New backup. Start the first task", "task", targetStatus)
if err := r.Status().Update(ctx, r.BackupSession); err != nil {
log.Error(err, "unable to update BackupSession status")
return err
}
case formolv1alpha1.Running:
// Backup ongoing. Check the status of the last task to decide what to do
currentTargetStatus := r.BackupSession.Status.Targets[len(r.BackupSession.Status.Targets)-1]
switch currentTargetStatus.SessionState {
case formolv1alpha1.Failure:
// The last task failed. We mark the backupsession as failed and we stop here.
log.V(0).Info("last backup task failed. Stop here", "targetStatus", currentTargetStatus)
r.BackupSession.Status.SessionState = formolv1alpha1.Failure
log.V(1).Info("New BackupSession status", "status", r.BackupSession.Status.SessionState)
if err := r.Status().Update(ctx, r.BackupSession); err != nil {
log.Error(err, "unable to update BackupSession status")
return err
}
case formolv1alpha1.Running:
// The current task is still running. Nothing to do
log.V(0).Info("task is still running", "targetStatus", currentTargetStatus)
case formolv1alpha1.Success:
// The last task successed. Let's try to start the next one
log.V(0).Info("last task was a success. start a new one", "currentTargetStatus", currentTargetStatus, "targetStatus", currentTargetStatus)
targetStatus, err := startNextTask()
if err != nil {
return err
}
if targetStatus == nil {
// No more task to start. The backup is a success
r.BackupSession.Status.SessionState = formolv1alpha1.Success
log.V(0).Info("Backup is successful. Let's try to do some cleanup")
backupSessionList := &formolv1alpha1.BackupSessionList{}
if err := r.List(ctx, backupSessionList, client.InNamespace(r.BackupConf.Namespace), client.MatchingFieldsSelector{Selector: fields.SelectorFromSet(fields.Set{sessionState: "Success"})}); err != nil {
log.Error(err, "unable to get backupsessionlist")
return nil
}
if len(backupSessionList.Items) < 2 {
// Not enough backupSession to proceed
log.V(1).Info("Not enough successful backup jobs")
break
}
sort.Slice(backupSessionList.Items, func(i, j int) bool {
return backupSessionList.Items[i].Status.StartTime.Time.Unix() > backupSessionList.Items[j].Status.StartTime.Time.Unix()
})
type KeepBackup struct {
Counter int32
Last time.Time
}
var lastBackups, dailyBackups, weeklyBackups, monthlyBackups, yearlyBackups KeepBackup
lastBackups.Counter = r.BackupConf.Spec.Keep.Last
dailyBackups.Counter = r.BackupConf.Spec.Keep.Daily
weeklyBackups.Counter = r.BackupConf.Spec.Keep.Weekly
monthlyBackups.Counter = r.BackupConf.Spec.Keep.Monthly
yearlyBackups.Counter = r.BackupConf.Spec.Keep.Yearly
for _, session := range backupSessionList.Items {
if session.Spec.Ref.Name != r.BackupConf.Name {
continue
}
deleteSession := true
keep := []string{}
if lastBackups.Counter > 0 {
log.V(1).Info("Keep backup", "last", session.Status.StartTime)
lastBackups.Counter--
keep = append(keep, "last")
deleteSession = false
}
if dailyBackups.Counter > 0 {
if session.Status.StartTime.Time.YearDay() != dailyBackups.Last.YearDay() {
log.V(1).Info("Keep backup", "daily", session.Status.StartTime)
dailyBackups.Counter--
dailyBackups.Last = session.Status.StartTime.Time
keep = append(keep, "daily")
deleteSession = false
}
}
if weeklyBackups.Counter > 0 {
if session.Status.StartTime.Time.Weekday().String() == "Sunday" && session.Status.StartTime.Time.YearDay() != weeklyBackups.Last.YearDay() {
log.V(1).Info("Keep backup", "weekly", session.Status.StartTime)
weeklyBackups.Counter--
weeklyBackups.Last = session.Status.StartTime.Time
keep = append(keep, "weekly")
deleteSession = false
}
}
if monthlyBackups.Counter > 0 {
if session.Status.StartTime.Time.Day() == 1 && session.Status.StartTime.Time.Month() != monthlyBackups.Last.Month() {
log.V(1).Info("Keep backup", "monthly", session.Status.StartTime)
monthlyBackups.Counter--
monthlyBackups.Last = session.Status.StartTime.Time
keep = append(keep, "monthly")
deleteSession = false
}
}
if yearlyBackups.Counter > 0 {
if session.Status.StartTime.Time.YearDay() == 1 && session.Status.StartTime.Time.Year() != yearlyBackups.Last.Year() {
log.V(1).Info("Keep backup", "yearly", session.Status.StartTime)
yearlyBackups.Counter--
yearlyBackups.Last = session.Status.StartTime.Time
keep = append(keep, "yearly")
deleteSession = false
}
}
if deleteSession {
log.V(1).Info("Delete session", "delete", session.Status.StartTime)
if err := r.Delete(ctx, &session); err != nil {
log.Error(err, "unable to delete backupsession", "session", session.Name)
// we don't return anything, we keep going
}
} else {
session.Status.Keep = strings.Join(keep, ",") // + " " + time.Now().Format("2006 Jan 02 15:04:05 -0700 MST")
if err := r.Status().Update(ctx, &session); err != nil {
log.Error(err, "unable to update session status", "session", session)
}
}
}
}
log.V(1).Info("New BackupSession status", "status", r.BackupSession.Status.SessionState)
if err := r.Status().Update(ctx, r.BackupSession); err != nil {
log.Error(err, "unable to update BackupSession status")
return err
}
}
case formolv1alpha1.Deleted:
for _, target := range r.BackupSession.Status.Targets {
if target.SessionState != formolv1alpha1.Deleted {
log.V(1).Info("snaphot has not been deleted. won't delete the backupsession", "target", target)
return nil
}
}
log.V(1).Info("all the snapshots have been deleted. deleting the backupsession")
controllerutil.RemoveFinalizer(r.BackupSession, finalizerName)
if err := r.Update(ctx, r.BackupSession); err != nil {
log.Error(err, "unable to remove finalizer")
return err
}
}
return nil
}
func (r *BackupSessionReconciler) IsBackupOngoing() bool {
log := r.Log.WithName("IsBackupOngoing")
ctx := context.Background()
backupSessionList := &formolv1alpha1.BackupSessionList{}
if err := r.List(ctx, backupSessionList, client.InNamespace(r.BackupConf.Namespace), client.MatchingFieldsSelector{Selector: fields.SelectorFromSet(fields.Set{sessionState: "Running"})}); err != nil {
log.Error(err, "unable to get backupsessionlist")
return true
}
if len(backupSessionList.Items) > 0 {
return true
} else {
return false
}
} }
// +kubebuilder:rbac:groups=formol.desmojim.fr,resources=backupsessions,verbs=get;list;watch;create;update;patch;delete // +kubebuilder:rbac:groups=formol.desmojim.fr,resources=backupsessions,verbs=get;list;watch;create;update;patch;delete
@ -265,55 +59,394 @@ func (r *BackupSessionReconciler) Reconcile(req ctrl.Request) (ctrl.Result, erro
log := r.Log.WithValues("backupsession", req.NamespacedName) log := r.Log.WithValues("backupsession", req.NamespacedName)
ctx := context.Background() ctx := context.Background()
// your logic here backupSession := &formolv1alpha1.BackupSession{}
//time.Sleep(300 * time.Millisecond) if err := r.Get(ctx, req.NamespacedName, backupSession); err != nil {
r.BackupSession = &formolv1alpha1.BackupSession{}
if err := r.Get(ctx, req.NamespacedName, r.BackupSession); err != nil {
log.Error(err, "unable to get backupsession") log.Error(err, "unable to get backupsession")
return ctrl.Result{}, client.IgnoreNotFound(err) return ctrl.Result{}, client.IgnoreNotFound(err)
} }
log.V(0).Info("backupSession", "backupSession.ObjectMeta", r.BackupSession.ObjectMeta, "backupSession.Status", r.BackupSession.Status) backupConf := &formolv1alpha1.BackupConfiguration{}
if r.BackupSession.Status.ObservedGeneration == r.BackupSession.ObjectMeta.Generation { if err := r.Get(ctx, client.ObjectKey{
// status update Namespace: backupSession.Namespace,
log.V(0).Info("status update") Name: backupSession.Spec.Ref.Name,
return ctrl.Result{}, r.StatusUpdate() }, backupConf); err != nil {
log.Error(err, "unable to get backupConfiguration")
return ctrl.Result{}, client.IgnoreNotFound(err)
} }
if r.IsBackupOngoing() {
// There is already a backup ongoing. We don't do anything and we reschedule // helper functions
log.V(0).Info("there is an ongoing backup. let's reschedule this operation") // is there a backup operation ongoing
return ctrl.Result{RequeueAfter: 30 * time.Second}, nil isBackupOngoing := func() bool {
} else if r.BackupSession.ObjectMeta.DeletionTimestamp.IsZero() { backupSessionList := &formolv1alpha1.BackupSessionList{}
// Check if the finalizer has been registered if err := r.List(ctx, backupSessionList, client.InNamespace(backupConf.Namespace), client.MatchingFieldsSelector{Selector: fields.SelectorFromSet(fields.Set{sessionState: "Running"})}); err != nil {
if !controllerutil.ContainsFinalizer(r.BackupSession, finalizerName) { log.Error(err, "unable to get backupsessionlist")
controllerutil.AddFinalizer(r.BackupSession, finalizerName) return true
// We update the BackupSession to add the finalizer
// Reconcile will be called again
// return now
err := r.Update(ctx, r.BackupSession)
if err != nil {
log.Error(err, "unable to add finalizer")
}
return ctrl.Result{}, err
} }
// All signals are green return len(backupSessionList.Items) > 0
// We start the backup process }
r.BackupSession.Status.ObservedGeneration = r.BackupSession.ObjectMeta.Generation
r.BackupSession.Status.SessionState = formolv1alpha1.New // delete session specific backup resources
r.BackupSession.Status.StartTime = &metav1.Time{Time: time.Now()} deleteExternalResources := func() error {
if err := r.Status().Update(ctx, r.BackupSession); err != nil { log := r.Log.WithValues("deleteExternalResources", backupSession.Name)
log.Error(err, "unable to update backupSession") // Gather information from the repo
return ctrl.Result{}, err repo := &formolv1alpha1.Repo{}
if err := r.Get(ctx, client.ObjectKey{
Namespace: backupConf.Namespace,
Name: backupConf.Spec.Repository,
}, repo); err != nil {
log.Error(err, "unable to get Repo from BackupConfiguration")
return err
}
env := formolutils.ConfigureResticEnvVar(backupConf, repo)
// container that will delete the restic snapshot(s) matching the backupsession
deleteSnapshots := []corev1.Container{}
for _, target := range backupSession.Status.Targets {
if target.SessionState == formolv1alpha1.Success {
deleteSnapshots = append(deleteSnapshots, corev1.Container{
Name: target.Name,
Image: "desmo999r/formolcli:latest",
Args: []string{"snapshot", "delete", "--snapshot-id", target.SnapshotId},
Env: env,
})
}
}
// create a job to delete the restic snapshot(s) with the backupsession name tag
if len(deleteSnapshots) > 0 {
jobTtl := JOBTTL
job := &batchv1.Job{
ObjectMeta: metav1.ObjectMeta{
GenerateName: fmt.Sprintf("delete-%s-", backupSession.Name),
Namespace: backupSession.Namespace,
},
Spec: batchv1.JobSpec{
TTLSecondsAfterFinished: &jobTtl,
Template: corev1.PodTemplateSpec{
Spec: corev1.PodSpec{
InitContainers: []corev1.Container{},
Containers: deleteSnapshots,
RestartPolicy: corev1.RestartPolicyOnFailure,
},
},
},
}
log.V(0).Info("creating a job to delete restic snapshots")
if err := r.Create(ctx, job); err != nil {
log.Error(err, "unable to delete job", "job", job)
return err
}
}
return nil
}
// create a backup job
createBackupJob := func(target formolv1alpha1.Target) error {
log := r.Log.WithValues("createbackupjob", target.Name)
ctx := context.Background()
backupSessionEnv := []corev1.EnvVar{
corev1.EnvVar{
Name: "TARGET_NAME",
Value: target.Name,
},
corev1.EnvVar{
Name: "BACKUPSESSION_NAME",
Value: backupSession.Name,
},
corev1.EnvVar{
Name: "BACKUPSESSION_NAMESPACE",
Value: backupSession.Namespace,
},
} }
} else { output := corev1.VolumeMount{
log.V(0).Info("backupsession being deleted", "backupsession", r.BackupSession.Name) Name: "output",
if controllerutil.ContainsFinalizer(r.BackupSession, finalizerName) { MountPath: "/output",
if err := r.deleteExternalResources(); err != nil { }
restic := corev1.Container{
Name: "restic",
Image: "desmo999r/formolcli:latest",
Args: []string{"volume", "backup", "--tag", backupSession.Name, "--path", "/output"},
VolumeMounts: []corev1.VolumeMount{output},
Env: backupSessionEnv,
}
log.V(1).Info("creating a tagged backup job", "container", restic)
// Gather information from the repo
repo := &formolv1alpha1.Repo{}
if err := r.Get(ctx, client.ObjectKey{
Namespace: backupConf.Namespace,
Name: backupConf.Spec.Repository,
}, repo); err != nil {
log.Error(err, "unable to get Repo from BackupConfiguration")
return err
}
// S3 backing storage
restic.Env = append(restic.Env, formolutils.ConfigureResticEnvVar(backupConf, repo)...)
jobTtl := JOBTTL
job := &batchv1.Job{
ObjectMeta: metav1.ObjectMeta{
GenerateName: fmt.Sprintf("%s-%s-", backupSession.Name, target.Name),
Namespace: backupConf.Namespace,
},
Spec: batchv1.JobSpec{
TTLSecondsAfterFinished: &jobTtl,
Template: corev1.PodTemplateSpec{
Spec: corev1.PodSpec{
InitContainers: []corev1.Container{},
Containers: []corev1.Container{restic},
Volumes: []corev1.Volume{
corev1.Volume{Name: "output"},
},
RestartPolicy: corev1.RestartPolicyOnFailure,
},
},
},
}
for _, step := range target.Steps {
function := &formolv1alpha1.Function{}
if err := r.Get(ctx, client.ObjectKey{
Namespace: backupConf.Namespace,
Name: step.Name,
}, function); err != nil {
log.Error(err, "unable to get function", "Function", step)
return err
}
function.Spec.Name = function.Name
function.Spec.Env = append(step.Env, backupSessionEnv...)
function.Spec.VolumeMounts = append(function.Spec.VolumeMounts, output)
job.Spec.Template.Spec.InitContainers = append(job.Spec.Template.Spec.InitContainers, function.Spec)
}
if err := ctrl.SetControllerReference(backupConf, job, r.Scheme); err != nil {
log.Error(err, "unable to set controller on job", "job", job, "backupconf", backupConf)
return err
}
log.V(0).Info("creating a backup job", "target", target)
if err := r.Create(ctx, job); err != nil {
log.Error(err, "unable to create job", "job", job)
return err
}
return nil
}
// start the next task
startNextTask := func() (*formolv1alpha1.TargetStatus, error) {
nextTarget := len(backupSession.Status.Targets)
if nextTarget < len(backupConf.Spec.Targets) {
target := backupConf.Spec.Targets[nextTarget]
targetStatus := formolv1alpha1.TargetStatus{
Name: target.Name,
Kind: target.Kind,
SessionState: formolv1alpha1.New,
StartTime: &metav1.Time{Time: time.Now()},
Try: 1,
}
backupSession.Status.Targets = append(backupSession.Status.Targets, targetStatus)
switch target.Kind {
case formolv1alpha1.JobKind:
if err := createBackupJob(target); err != nil {
log.V(0).Info("unable to create task", "task", target)
targetStatus.SessionState = formolv1alpha1.Failure
return nil, err
}
}
return &targetStatus, nil
} else {
return nil, nil
}
}
// cleanup existing backupsessions
cleanupSessions := func() {
backupSessionList := &formolv1alpha1.BackupSessionList{}
if err := r.List(ctx, backupSessionList, client.InNamespace(backupConf.Namespace), client.MatchingFieldsSelector{Selector: fields.SelectorFromSet(fields.Set{sessionState: string(formolv1alpha1.Success)})}); err != nil {
log.Error(err, "unable to get backupsessionlist")
return
}
if len(backupSessionList.Items) < 2 {
// Not enough backupSession to proceed
log.V(1).Info("Not enough successful backup jobs")
return
}
sort.Slice(backupSessionList.Items, func(i, j int) bool {
return backupSessionList.Items[i].Status.StartTime.Time.Unix() > backupSessionList.Items[j].Status.StartTime.Time.Unix()
})
type KeepBackup struct {
Counter int32
Last time.Time
}
var lastBackups, dailyBackups, weeklyBackups, monthlyBackups, yearlyBackups KeepBackup
lastBackups.Counter = backupConf.Spec.Keep.Last
dailyBackups.Counter = backupConf.Spec.Keep.Daily
weeklyBackups.Counter = backupConf.Spec.Keep.Weekly
monthlyBackups.Counter = backupConf.Spec.Keep.Monthly
yearlyBackups.Counter = backupConf.Spec.Keep.Yearly
for _, session := range backupSessionList.Items {
if session.Spec.Ref.Name != backupConf.Name {
continue
}
deleteSession := true
keep := []string{}
if lastBackups.Counter > 0 {
log.V(1).Info("Keep backup", "last", session.Status.StartTime)
lastBackups.Counter--
keep = append(keep, "last")
deleteSession = false
}
if dailyBackups.Counter > 0 {
if session.Status.StartTime.Time.YearDay() != dailyBackups.Last.YearDay() {
log.V(1).Info("Keep backup", "daily", session.Status.StartTime)
dailyBackups.Counter--
dailyBackups.Last = session.Status.StartTime.Time
keep = append(keep, "daily")
deleteSession = false
}
}
if weeklyBackups.Counter > 0 {
if session.Status.StartTime.Time.Weekday().String() == "Sunday" && session.Status.StartTime.Time.YearDay() != weeklyBackups.Last.YearDay() {
log.V(1).Info("Keep backup", "weekly", session.Status.StartTime)
weeklyBackups.Counter--
weeklyBackups.Last = session.Status.StartTime.Time
keep = append(keep, "weekly")
deleteSession = false
}
}
if monthlyBackups.Counter > 0 {
if session.Status.StartTime.Time.Day() == 1 && session.Status.StartTime.Time.Month() != monthlyBackups.Last.Month() {
log.V(1).Info("Keep backup", "monthly", session.Status.StartTime)
monthlyBackups.Counter--
monthlyBackups.Last = session.Status.StartTime.Time
keep = append(keep, "monthly")
deleteSession = false
}
}
if yearlyBackups.Counter > 0 {
if session.Status.StartTime.Time.YearDay() == 1 && session.Status.StartTime.Time.Year() != yearlyBackups.Last.Year() {
log.V(1).Info("Keep backup", "yearly", session.Status.StartTime)
yearlyBackups.Counter--
yearlyBackups.Last = session.Status.StartTime.Time
keep = append(keep, "yearly")
deleteSession = false
}
}
if deleteSession {
log.V(1).Info("Delete session", "delete", session.Status.StartTime)
if err := r.Delete(ctx, &session); err != nil {
log.Error(err, "unable to delete backupsession", "session", session.Name)
// we don't return anything, we keep going
}
} else {
session.Status.Keep = strings.Join(keep, ",") // + " " + time.Now().Format("2006 Jan 02 15:04:05 -0700 MST")
if err := r.Status().Update(ctx, &session); err != nil {
log.Error(err, "unable to update session status", "session", session)
}
}
}
}
// end helper functions
log.V(0).Info("backupSession", "backupSession.ObjectMeta", backupSession.ObjectMeta, "backupSession.Status", backupSession.Status)
if backupSession.ObjectMeta.DeletionTimestamp.IsZero() {
switch backupSession.Status.SessionState {
case formolv1alpha1.New:
// Check if the finalizer has been registered
if !controllerutil.ContainsFinalizer(backupSession, finalizerName) {
controllerutil.AddFinalizer(backupSession, finalizerName)
// We update the BackupSession to add the finalizer
// Reconcile will be called again
// return now
err := r.Update(ctx, backupSession)
if err != nil {
log.Error(err, "unable to add finalizer")
}
return ctrl.Result{}, err
}
// Brand new backupsession
if isBackupOngoing() {
log.V(0).Info("There is an ongoing backup. Let's reschedule this operation")
return ctrl.Result{RequeueAfter: 30 * time.Second}, nil
}
// start the first task
backupSession.Status.SessionState = formolv1alpha1.Running
targetStatus, err := startNextTask()
if err != nil {
return ctrl.Result{}, err
}
log.V(0).Info("New backup. Start the first task", "task", targetStatus)
if err := r.Status().Update(ctx, backupSession); err != nil {
log.Error(err, "unable to update BackupSession status")
return ctrl.Result{}, err
}
case formolv1alpha1.Running:
// Backup ongoing. Check the status of the last task to decide what to do
currentTargetStatus := &backupSession.Status.Targets[len(backupSession.Status.Targets)-1]
switch currentTargetStatus.SessionState {
case formolv1alpha1.Running:
// The current task is still running. Nothing to do
log.V(0).Info("task is still running", "targetStatus", currentTargetStatus)
case formolv1alpha1.Success:
// The last task succeed. Let's try to start the next one
targetStatus, err := startNextTask()
log.V(0).Info("last task was a success. start a new one", "currentTargetStatus", currentTargetStatus, "targetStatus", targetStatus)
if err != nil {
return ctrl.Result{}, err
}
if targetStatus == nil {
// No more task to start. The backup is a success
backupSession.Status.SessionState = formolv1alpha1.Success
log.V(0).Info("Backup is successful. Let's try to do some cleanup")
cleanupSessions()
}
if err := r.Status().Update(ctx, backupSession); err != nil {
log.Error(err, "unable to update BackupSession status")
return ctrl.Result{}, err
}
case formolv1alpha1.Failure:
// last task failed. Try to run it again
currentTarget := backupConf.Spec.Targets[len(backupSession.Status.Targets)-1]
if currentTargetStatus.Try < currentTarget.Retry {
log.V(0).Info("last task was a failure. try again", "currentTargetStatus", currentTargetStatus)
currentTargetStatus.Try++
currentTargetStatus.SessionState = formolv1alpha1.New
currentTargetStatus.StartTime = &metav1.Time{Time: time.Now()}
switch currentTarget.Kind {
case formolv1alpha1.JobKind:
if err := createBackupJob(currentTarget); err != nil {
log.V(0).Info("unable to create task", "task", currentTarget)
currentTargetStatus.SessionState = formolv1alpha1.Failure
return ctrl.Result{}, err
}
}
} else {
log.V(0).Info("task failed again and for the last time", "currentTargetStatus", currentTargetStatus)
backupSession.Status.SessionState = formolv1alpha1.Failure
}
if err := r.Status().Update(ctx, backupSession); err != nil {
log.Error(err, "unable to update BackupSession status")
return ctrl.Result{}, err
}
}
case formolv1alpha1.Success:
// Should never go there
case formolv1alpha1.Failure:
// The backup failed
case "":
// BackupSession has just been created
backupSession.Status.SessionState = formolv1alpha1.New
backupSession.Status.StartTime = &metav1.Time{Time: time.Now()}
if err := r.Status().Update(ctx, backupSession); err != nil {
log.Error(err, "unable to update backupSession")
return ctrl.Result{}, err return ctrl.Result{}, err
} }
} }
controllerutil.RemoveFinalizer(r.BackupSession, finalizerName) } else {
if err := r.Update(ctx, r.BackupSession); err != nil { log.V(0).Info("backupsession being deleted", "backupsession", backupSession.Name)
if controllerutil.ContainsFinalizer(backupSession, finalizerName) {
if err := deleteExternalResources(); err != nil {
return ctrl.Result{}, err
}
}
controllerutil.RemoveFinalizer(backupSession, finalizerName)
if err := r.Update(ctx, backupSession); err != nil {
log.Error(err, "unable to remove finalizer") 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,147 @@
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"
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: corev1.ObjectReference{
Name: 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

@ -26,6 +26,7 @@ import (
appsv1 "k8s.io/api/apps/v1" appsv1 "k8s.io/api/apps/v1"
batchv1 "k8s.io/api/batch/v1" batchv1 "k8s.io/api/batch/v1"
corev1 "k8s.io/api/core/v1" corev1 "k8s.io/api/core/v1"
"k8s.io/apimachinery/pkg/api/errors"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/runtime" "k8s.io/apimachinery/pkg/runtime"
ctrl "sigs.k8s.io/controller-runtime" ctrl "sigs.k8s.io/controller-runtime"
@ -43,224 +44,270 @@ const (
// RestoreSessionReconciler reconciles a RestoreSession object // RestoreSessionReconciler reconciles a RestoreSession object
type RestoreSessionReconciler struct { 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()
restoreSessionEnv := []corev1.EnvVar{ log := r.Log.WithValues("restoresession", req.NamespacedName)
corev1.EnvVar{
Name: "TARGET_NAME", // Get the RestoreSession
Value: target.Name, restoreSession := &formolv1alpha1.RestoreSession{}
}, if err := r.Get(ctx, req.NamespacedName, restoreSession); err != nil {
corev1.EnvVar{ log.Error(err, "unable to get restoresession")
Name: "RESTORESESSION_NAME", return ctrl.Result{}, client.IgnoreNotFound(err)
Value: r.RestoreSession.Name, }
}, log.V(1).Info("got restoresession", "restoreSession", restoreSession)
corev1.EnvVar{ // Get the BackupSession the RestoreSession references
Name: "RESTORESESSION_NAMESPACE", backupSession := &formolv1alpha1.BackupSession{}
Value: r.RestoreSession.Namespace, if err := r.Get(ctx, client.ObjectKey{
}, Namespace: restoreSession.Namespace,
Name: restoreSession.Spec.BackupSessionRef.Ref.Name,
}, backupSession); err != nil {
if errors.IsNotFound(err) {
backupSession = &formolv1alpha1.BackupSession{
Spec: restoreSession.Spec.BackupSessionRef.Spec,
Status: restoreSession.Spec.BackupSessionRef.Status,
}
log.V(1).Info("generated backupsession", "backupsession", backupSession)
} else {
log.Error(err, "unable to get backupsession", "restoresession", restoreSession.Spec)
return ctrl.Result{}, client.IgnoreNotFound(err)
}
}
// Get the BackupConfiguration linked to the BackupSession
backupConf := &formolv1alpha1.BackupConfiguration{}
if err := r.Get(ctx, client.ObjectKey{
Namespace: backupSession.Spec.Ref.Namespace,
Name: backupSession.Spec.Ref.Name,
}, backupConf); err != nil {
log.Error(err, "unable to get backupConfiguration", "name", backupSession.Spec.Ref, "namespace", backupSession.Namespace)
return ctrl.Result{}, client.IgnoreNotFound(err)
} }
output := corev1.VolumeMount{ // Helper functions
Name: "output", createRestoreJob := func(target formolv1alpha1.Target) error {
MountPath: "/output", restoreSessionEnv := []corev1.EnvVar{
} corev1.EnvVar{
for _, targetStatus := range r.BackupSession.Status.Targets { Name: "TARGET_NAME",
if targetStatus.Name == target.Name { Value: target.Name,
snapshotId := targetStatus.SnapshotId },
restic := corev1.Container{ corev1.EnvVar{
Name: "restic", Name: "RESTORESESSION_NAME",
Image: "desmo999r/formolcli:latest", Value: restoreSession.Name,
Args: []string{"volume", "restore", "--snapshot-id", snapshotId}, },
VolumeMounts: []corev1.VolumeMount{output}, corev1.EnvVar{
Env: restoreSessionEnv, Name: "RESTORESESSION_NAMESPACE",
} Value: restoreSession.Namespace,
finalizer := corev1.Container{ },
Name: "finalizer", }
Image: "desmo999r/formolcli:latest",
Args: []string{"target", "finalize"}, output := corev1.VolumeMount{
VolumeMounts: []corev1.VolumeMount{output}, Name: "output",
Env: restoreSessionEnv, MountPath: "/output",
} }
repo := &formolv1alpha1.Repo{} for _, targetStatus := range backupSession.Status.Targets {
if err := r.Get(ctx, client.ObjectKey{ if targetStatus.Name == target.Name {
Namespace: r.BackupConf.Namespace, snapshotId := targetStatus.SnapshotId
Name: r.BackupConf.Spec.Repository, restic := corev1.Container{
}, repo); err != nil { Name: "restic",
log.Error(err, "unable to get Repo from BackupConfiguration") Image: "desmo999r/formolcli:latest",
return err Args: []string{"volume", "restore", "--snapshot-id", snapshotId},
} VolumeMounts: []corev1.VolumeMount{output},
// S3 backing storage Env: restoreSessionEnv,
var ttl int32 = 300 }
restic.Env = append(restic.Env, formolutils.ConfigureResticEnvVar(r.BackupConf, repo)...) finalizer := corev1.Container{
job := &batchv1.Job{ Name: "finalizer",
ObjectMeta: metav1.ObjectMeta{ Image: "desmo999r/formolcli:latest",
GenerateName: fmt.Sprintf("%s-%s-", r.RestoreSession.Name, target.Name), Args: []string{"target", "finalize"},
Namespace: r.RestoreSession.Namespace, VolumeMounts: []corev1.VolumeMount{output},
}, Env: restoreSessionEnv,
Spec: batchv1.JobSpec{ }
TTLSecondsAfterFinished: &ttl, repo := &formolv1alpha1.Repo{}
Template: corev1.PodTemplateSpec{
Spec: corev1.PodSpec{
InitContainers: []corev1.Container{restic},
Containers: []corev1.Container{finalizer},
Volumes: []corev1.Volume{
corev1.Volume{Name: "output"},
},
RestartPolicy: corev1.RestartPolicyOnFailure,
},
},
},
}
for _, step := range target.Steps {
function := &formolv1alpha1.Function{}
if err := r.Get(ctx, client.ObjectKey{ if err := r.Get(ctx, client.ObjectKey{
Namespace: r.RestoreSession.Namespace, Namespace: backupConf.Namespace,
Name: strings.Replace(step.Name, "backup", "restore", 1)}, function); err != nil { Name: backupConf.Spec.Repository,
log.Error(err, "unable to get function", "function", step) }, repo); err != nil {
log.Error(err, "unable to get Repo from BackupConfiguration")
return err
}
// S3 backing storage
var ttl int32 = 300
restic.Env = append(restic.Env, formolutils.ConfigureResticEnvVar(backupConf, repo)...)
job := &batchv1.Job{
ObjectMeta: metav1.ObjectMeta{
GenerateName: fmt.Sprintf("%s-%s-", restoreSession.Name, target.Name),
Namespace: restoreSession.Namespace,
},
Spec: batchv1.JobSpec{
TTLSecondsAfterFinished: &ttl,
Template: corev1.PodTemplateSpec{
Spec: corev1.PodSpec{
InitContainers: []corev1.Container{restic},
Containers: []corev1.Container{finalizer},
Volumes: []corev1.Volume{
corev1.Volume{Name: "output"},
},
RestartPolicy: corev1.RestartPolicyOnFailure,
},
},
},
}
for _, step := range target.Steps {
function := &formolv1alpha1.Function{}
// get the backup function
if err := r.Get(ctx, client.ObjectKey{
Namespace: restoreSession.Namespace,
Name: step.Name,
}, function); err != nil {
log.Error(err, "unable to get backup function", "name", step.Name)
return err
}
var restoreName string
if function.Annotations["restoreFunction"] != "" {
restoreName = function.Annotations["restoreFunction"]
} else {
restoreName = strings.Replace(step.Name, "backup", "restore", 1)
}
if err := r.Get(ctx, client.ObjectKey{
Namespace: restoreSession.Namespace,
Name: restoreName,
}, function); err != nil {
log.Error(err, "unable to get function", "function", step)
return err
}
function.Spec.Name = function.Name
function.Spec.Env = append(step.Env, restoreSessionEnv...)
function.Spec.VolumeMounts = append(function.Spec.VolumeMounts, output)
job.Spec.Template.Spec.InitContainers = append(job.Spec.Template.Spec.InitContainers, function.Spec)
}
if err := ctrl.SetControllerReference(restoreSession, job, r.Scheme); err != nil {
log.Error(err, "unable to set controller on job", "job", job, "restoresession", restoreSession)
return err
}
log.V(0).Info("creating a restore job", "target", target.Name)
if err := r.Create(ctx, job); err != nil {
log.Error(err, "unable to create job", "job", job)
return err return err
} }
function.Spec.Env = append(step.Env, restoreSessionEnv...)
function.Spec.VolumeMounts = append(function.Spec.VolumeMounts, output)
job.Spec.Template.Spec.InitContainers = append(job.Spec.Template.Spec.InitContainers, function.Spec)
}
if err := ctrl.SetControllerReference(r.RestoreSession, job, r.Scheme); err != nil {
log.Error(err, "unable to set controller on job", "job", job, "restoresession", r.RestoreSession)
return err
}
log.V(0).Info("creating a restore job", "target", target.Name)
if err := r.Create(ctx, job); err != nil {
log.Error(err, "unable to create job", "job", job)
return err
} }
} }
return nil
} }
return nil
}
func (r *RestoreSessionReconciler) DeleteRestoreInitContainer(target formolv1alpha1.Target) error { deleteRestoreInitContainer := func(target formolv1alpha1.Target) error {
log := r.Log.WithValues("createrestoreinitcontainer", target.Name) deployment := &appsv1.Deployment{}
ctx := context.Background() if err := r.Get(context.Background(), client.ObjectKey{
deployment := &appsv1.Deployment{} Namespace: backupConf.Namespace,
if err := r.Get(context.Background(), client.ObjectKey{ Name: target.Name,
Namespace: r.BackupConf.Namespace, }, deployment); err != nil {
Name: target.Name, log.Error(err, "unable to get deployment")
}, deployment); err != nil { return err
log.Error(err, "unable to get deployment")
return err
}
log.V(1).Info("got deployment", "namespace", deployment.Namespace, "name", deployment.Name)
newInitContainers := []corev1.Container{}
for _, initContainer := range deployment.Spec.Template.Spec.InitContainers {
if initContainer.Name == RESTORESESSION {
log.V(0).Info("Found our restoresession container. Removing it from the list of init containers", "container", initContainer)
} else {
newInitContainers = append(newInitContainers, initContainer)
} }
} log.V(1).Info("got deployment", "namespace", deployment.Namespace, "name", deployment.Name)
deployment.Spec.Template.Spec.InitContainers = newInitContainers newInitContainers := []corev1.Container{}
if err := r.Update(ctx, deployment); err != nil { for _, initContainer := range deployment.Spec.Template.Spec.InitContainers {
log.Error(err, "unable to update deployment") if initContainer.Name == RESTORESESSION {
return err log.V(0).Info("Found our restoresession container. Removing it from the list of init containers", "container", initContainer)
} } else {
return nil newInitContainers = append(newInitContainers, initContainer)
} }
func (r *RestoreSessionReconciler) CreateRestoreInitContainer(target formolv1alpha1.Target) error {
log := r.Log.WithValues("createrestoreinitcontainer", target.Name)
ctx := context.Background()
deployment := &appsv1.Deployment{}
if err := r.Get(context.Background(), client.ObjectKey{
Namespace: r.RestoreSession.Namespace,
Name: target.Name,
}, deployment); err != nil {
log.Error(err, "unable to get deployment")
return err
}
log.V(1).Info("got deployment", "namespace", deployment.Namespace, "name", deployment.Name)
for _, initContainer := range deployment.Spec.Template.Spec.InitContainers {
if initContainer.Name == RESTORESESSION {
log.V(0).Info("there is already a restoresession initcontainer", "deployment", deployment.Spec.Template.Spec.InitContainers)
return nil
} }
} deployment.Spec.Template.Spec.InitContainers = newInitContainers
var snapshotId string if err := r.Update(ctx, deployment); err != nil {
for _, targetStatus := range r.BackupSession.Status.Targets { log.Error(err, "unable to update deployment")
if targetStatus.Name == target.Name && targetStatus.Kind == target.Kind { return err
snapshotId = targetStatus.SnapshotId
} }
} return nil
restoreSessionEnv := []corev1.EnvVar{
corev1.EnvVar{
Name: formolv1alpha1.TARGET_NAME,
Value: target.Name,
},
corev1.EnvVar{
Name: formolv1alpha1.RESTORESESSION_NAME,
Value: r.RestoreSession.Name,
},
corev1.EnvVar{
Name: formolv1alpha1.RESTORESESSION_NAMESPACE,
Value: r.RestoreSession.Namespace,
},
}
initContainer := corev1.Container{
Name: RESTORESESSION,
Image: formolutils.FORMOLCLI,
Args: []string{"volume", "restore", "--snapshot-id", snapshotId},
VolumeMounts: target.VolumeMounts,
Env: restoreSessionEnv,
}
repo := &formolv1alpha1.Repo{}
if err := r.Get(ctx, client.ObjectKey{
Namespace: r.BackupConf.Namespace,
Name: r.BackupConf.Spec.Repository,
}, repo); err != nil {
log.Error(err, "unable to get Repo from BackupConfiguration")
return err
}
// S3 backing storage
initContainer.Env = append(initContainer.Env, formolutils.ConfigureResticEnvVar(r.BackupConf, repo)...)
deployment.Spec.Template.Spec.InitContainers = append([]corev1.Container{initContainer},
deployment.Spec.Template.Spec.InitContainers...)
if err := r.Update(ctx, deployment); err != nil {
log.Error(err, "unable to update deployment")
return err
} }
return nil createRestoreInitContainer := func(target formolv1alpha1.Target) error {
} deployment := &appsv1.Deployment{}
if err := r.Get(context.Background(), client.ObjectKey{
Namespace: restoreSession.Namespace,
Name: target.Name,
}, deployment); err != nil {
log.Error(err, "unable to get deployment")
return err
}
log.V(1).Info("got deployment", "namespace", deployment.Namespace, "name", deployment.Name)
for _, initContainer := range deployment.Spec.Template.Spec.InitContainers {
if initContainer.Name == RESTORESESSION {
log.V(0).Info("there is already a restoresession initcontainer", "deployment", deployment.Spec.Template.Spec.InitContainers)
return nil
}
}
for _, targetStatus := range backupSession.Status.Targets {
if targetStatus.Name == target.Name && targetStatus.Kind == target.Kind {
snapshotId := targetStatus.SnapshotId
restoreSessionEnv := []corev1.EnvVar{
corev1.EnvVar{
Name: formolv1alpha1.TARGET_NAME,
Value: target.Name,
},
corev1.EnvVar{
Name: formolv1alpha1.RESTORESESSION_NAME,
Value: restoreSession.Name,
},
corev1.EnvVar{
Name: formolv1alpha1.RESTORESESSION_NAMESPACE,
Value: restoreSession.Namespace,
},
}
initContainer := corev1.Container{
Name: RESTORESESSION,
Image: formolutils.FORMOLCLI,
Args: []string{"volume", "restore", "--snapshot-id", snapshotId},
VolumeMounts: target.VolumeMounts,
Env: restoreSessionEnv,
}
repo := &formolv1alpha1.Repo{}
if err := r.Get(ctx, client.ObjectKey{
Namespace: backupConf.Namespace,
Name: backupConf.Spec.Repository,
}, repo); err != nil {
log.Error(err, "unable to get Repo from BackupConfiguration")
return err
}
// S3 backing storage
initContainer.Env = append(initContainer.Env, formolutils.ConfigureResticEnvVar(backupConf, repo)...)
deployment.Spec.Template.Spec.InitContainers = append([]corev1.Container{initContainer},
deployment.Spec.Template.Spec.InitContainers...)
if err := r.Update(ctx, deployment); err != nil {
log.Error(err, "unable to update deployment")
return err
}
return nil
}
}
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 +318,97 @@ 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.Waiting:
target := backupConf.Spec.Targets[len(restoreSession.Status.Targets)-1]
if target.Kind == formolv1alpha1.SidecarKind {
deployment := &appsv1.Deployment{}
if err := r.Get(context.Background(), client.ObjectKey{
Namespace: restoreSession.Namespace,
Name: target.Name,
}, deployment); err != nil {
log.Error(err, "unable to get deployment")
return ctrl.Result{}, err
}
if deployment.Status.ReadyReplicas == *deployment.Spec.Replicas {
log.V(0).Info("The deployment is ready. We can resume the backup")
currentTargetStatus.SessionState = formolv1alpha1.Finalize
if err := r.Status().Update(ctx, restoreSession); err != nil {
log.Error(err, "unable to update restoresession")
return ctrl.Result{}, err
}
} else {
log.V(0).Info("Waiting for the sidecar to come back")
return ctrl.Result{RequeueAfter: 10 * time.Second}, nil
}
} else {
log.V(0).Info("not a SidecarKind. Ignoring Waiting")
}
case formolv1alpha1.Success: 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, 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 "":
// Restore session has just been created
restoreSession.Status.SessionState = formolv1alpha1.New
restoreSession.Status.StartTime = &metav1.Time{Time: time.Now()}
if err := r.Status().Update(ctx, restoreSession); err != nil {
log.Error(err, "unable to update restoreSession")
return ctrl.Result{}, err
}
} }
return nil return ctrl.Result{}, nil
}
// +kubebuilder:rbac:groups=formol.desmojim.fr,resources=restoresessions,verbs=get;list;watch;create;update;patch;delete
// +kubebuilder:rbac:groups=formol.desmojim.fr,resources=restoresessions/status,verbs=get;update;patch
func (r *RestoreSessionReconciler) Reconcile(req ctrl.Request) (ctrl.Result, error) {
time.Sleep(500 * time.Millisecond)
ctx := context.Background()
log := r.Log.WithValues("restoresession", req.NamespacedName)
r.RestoreSession = &formolv1alpha1.RestoreSession{}
if err := r.Get(ctx, req.NamespacedName, r.RestoreSession); err != nil {
log.Error(err, "unable to get restoresession")
return ctrl.Result{}, client.IgnoreNotFound(err)
}
r.BackupSession = &formolv1alpha1.BackupSession{}
if err := r.Get(ctx, client.ObjectKey{
Namespace: r.RestoreSession.Spec.BackupSessionRef.Namespace,
Name: r.RestoreSession.Spec.BackupSessionRef.Name}, r.BackupSession); err != nil {
log.Error(err, "unable to get backupsession")
return ctrl.Result{}, client.IgnoreNotFound(err)
}
r.BackupConf = &formolv1alpha1.BackupConfiguration{}
if err := r.Get(ctx, client.ObjectKey{
Namespace: r.BackupSession.Namespace,
Name: r.BackupSession.Spec.Ref.Name}, r.BackupConf); err != nil {
log.Error(err, "unable to get backupConfiguration")
return ctrl.Result{}, client.IgnoreNotFound(err)
}
if r.RestoreSession.Status.ObservedGeneration == r.RestoreSession.ObjectMeta.Generation {
// status update
log.V(0).Info("status update")
return ctrl.Result{}, r.StatusUpdate()
}
r.RestoreSession.Status.ObservedGeneration = r.RestoreSession.ObjectMeta.Generation
r.RestoreSession.Status.SessionState = formolv1alpha1.New
r.RestoreSession.Status.StartTime = &metav1.Time{Time: time.Now()}
reschedule := ctrl.Result{RequeueAfter: 5 * time.Second}
if err := r.Status().Update(ctx, r.RestoreSession); err != nil {
log.Error(err, "unable to update restoresession")
return ctrl.Result{}, err
}
return reschedule, nil
} }
func (r *RestoreSessionReconciler) SetupWithManager(mgr ctrl.Manager) error { func (r *RestoreSessionReconciler) SetupWithManager(mgr ctrl.Manager) error {

View File

@ -0,0 +1,95 @@
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 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{
BackupSessionRef: formolv1alpha1.BackupSessionRef{
Ref: corev1.ObjectReference{
Name: 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

@ -18,7 +18,6 @@ package controllers
import ( import (
"context" "context"
"fmt"
"path/filepath" "path/filepath"
"testing" "testing"
"time" "time"
@ -44,12 +43,18 @@ 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"
TestNamespace = "test-namespace" TestFunc = "test-norestore-func"
RepoName = "test-repo" TestRestoreFuncName = "test-restore-func"
DeploymentName = "test-deployment" TestNamespace = "test-namespace"
timeout = time.Second * 10 TestRepoName = "test-repo"
interval = time.Millisecond * 250 TestDeploymentName = "test-deployment"
TestBackupConfName = "test-backupconf"
TestBackupSessionName = "test-backupsession"
TestDataVolume = "data"
TestDataMountPath = "/data"
timeout = time.Second * 10
interval = time.Millisecond * 250
) )
var cfg *rest.Config var cfg *rest.Config
@ -64,7 +69,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 +87,11 @@ var (
Image: "test-image", Image: "test-image",
}, },
}, },
Volumes: []corev1.Volume{
corev1.Volume{
Name: TestDataVolume,
},
},
}, },
}, },
}, },
@ -105,7 +115,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 +130,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 +161,69 @@ 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: corev1.ObjectReference{
Name: TestBackupConfName,
Namespace: TestNamespace,
},
},
}
) )
func TestAPIs(t *testing.T) { func TestAPIs(t *testing.T) {
@ -168,6 +263,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,11 +291,36 @@ 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() {
By("tearing down the test environment") By("tearing down the test environment")
err := testEnv.Stop() err := testEnv.Stop()
Expect(err).ToNot(HaveOccurred()) Expect(err).ToNot(HaveOccurred())
fmt.Println("coucou")
}) })

24
main.go
View File

@ -84,16 +84,6 @@ func main() {
setupLog.Error(err, "unable to create controller", "controller", "BackupSession") setupLog.Error(err, "unable to create controller", "controller", "BackupSession")
os.Exit(1) os.Exit(1)
} }
if os.Getenv("ENABLE_WEBHOOKS") != "false" {
if err = (&formolv1alpha1.BackupSession{}).SetupWebhookWithManager(mgr); err != nil {
setupLog.Error(err, "unable to create webhook", "webhook", "BackupSession")
os.Exit(1)
}
if err = (&formolv1alpha1.BackupConfiguration{}).SetupWebhookWithManager(mgr); err != nil {
setupLog.Error(err, "unable to create webhook", "webhook", "BackupConfiguration")
os.Exit(1)
}
}
if err = (&controllers.RestoreSessionReconciler{ if err = (&controllers.RestoreSessionReconciler{
Client: mgr.GetClient(), Client: mgr.GetClient(),
Log: ctrl.Log.WithName("controllers").WithName("RestoreSession"), Log: ctrl.Log.WithName("controllers").WithName("RestoreSession"),
@ -102,6 +92,20 @@ 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 os.Getenv("ENABLE_WEBHOOKS") != "false" {
// if err = (&formolv1alpha1.BackupSession{}).SetupWebhookWithManager(mgr); err != nil {
// setupLog.Error(err, "unable to create webhook", "webhook", "BackupSession")
// os.Exit(1)
// }
// if err = (&formolv1alpha1.BackupConfiguration{}).SetupWebhookWithManager(mgr); err != nil {
// setupLog.Error(err, "unable to create webhook", "webhook", "BackupConfiguration")
// 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

@ -75,6 +75,16 @@ spec:
--- ---
apiVersion: formol.desmojim.fr/v1alpha1 apiVersion: formol.desmojim.fr/v1alpha1
kind: Function kind: Function
metadata:
name: restore-pg
namespace: demo
spec:
name: restore-pg
image: desmo999r/formolcli:latest
args: ["postgres", "restore", "--hostname", $(PGHOST), "--database", $(PGDATABASE), "--username", $(PGUSER), "--password", $(PGPASSWD), "--file", "/output/backup-pg.sql"]
---
apiVersion: formol.desmojim.fr/v1alpha1
kind: Function
metadata: metadata:
name: backup-pg name: backup-pg
namespace: demo namespace: demo
@ -82,3 +92,21 @@ spec:
name: backup-pg name: backup-pg
image: desmo999r/formolcli:latest image: desmo999r/formolcli:latest
args: ["postgres", "backup", "--hostname", $(PGHOST), "--database", $(PGDATABASE), "--username", $(PGUSER), "--password", $(PGPASSWD), "--file", "/output/backup-pg.sql"] args: ["postgres", "backup", "--hostname", $(PGHOST), "--database", $(PGDATABASE), "--username", $(PGUSER), "--password", $(PGPASSWD), "--file", "/output/backup-pg.sql"]
---
apiVersion: formol.desmojim.fr/v1alpha1
kind: Function
metadata:
name: maintenance-off
namespace: demo
spec:
name: maintenance-off
command: ["/bin/bash", "-c", "echo $(date +%Y/%m/%d-%H:%M:%S) maintenance-off >> /data/logs.txt"]
---
apiVersion: formol.desmojim.fr/v1alpha1
kind: Function
metadata:
name: maintenance-on
namespace: demo
spec:
name: maintenance-on
command: ["/bin/bash", "-c", "echo $(date +%Y/%m/%d-%H:%M:%S) maintenance-on >> /data/logs.txt"]

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,19 +8,22 @@ 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
steps:
- name: maintenance-on
- name: maintenance-off
finalize: true
volumeMounts: volumeMounts:
- name: demo-data - name: demo-data
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

View File

@ -0,0 +1,11 @@
apiVersion: formol.desmojim.fr/v1alpha1
kind: RestoreSession
metadata:
namespace: demo
name: restore-demo
spec:
backupSession:
ref:
name: backupsession-backup-demo-1619904678
namespace: demo

1
test/README Normal file
View File

@ -0,0 +1 @@
NAMESPACE=demo; for i in $(kubectl -n $NAMESPACE get bs | awk 'NR>1 { print $1 }'); do kubectl -n $NAMESPACE get bs -o json $i | sed '/finalizers/,+2d' | curl -vvv -X PUT -H 'Content-type: application/json' -d @- http://127.0.0.1:8001/apis/formol.desmojim.fr/v1alpha1/namespaces/$NAMESPACE/backupsessions/$i; kubectl -n $NAMESPACE delete bs $i; done