From 6f95be9c7375ad805c9ffb0998703569650b35c2 Mon Sep 17 00:00:00 2001 From: Jean-Marc ANDRE Date: Mon, 30 Nov 2020 16:55:49 +0100 Subject: [PATCH] Creates dummy backup sidecar --- api/v1alpha1/backupconfiguration_types.go | 10 ++ api/v1alpha1/backupsession_types.go | 3 +- api/v1alpha1/repo_types.go | 75 ++++++++++ controllers/backupconfiguration_controller.go | 101 +++++++++++++- controllers/backupsession_controller.go | 132 ++---------------- 5 files changed, 199 insertions(+), 122 deletions(-) create mode 100644 api/v1alpha1/repo_types.go diff --git a/api/v1alpha1/backupconfiguration_types.go b/api/v1alpha1/backupconfiguration_types.go index c5ce82e..c6a77a1 100644 --- a/api/v1alpha1/backupconfiguration_types.go +++ b/api/v1alpha1/backupconfiguration_types.go @@ -17,6 +17,7 @@ limitations under the License. package v1alpha1 import ( + corev1 "k8s.io/api/core/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" ) @@ -28,6 +29,12 @@ type Repository struct { Name string `json:"name"` } +type Target struct { + ApiVersion string `json:"apiVersion"` + Name string `json:"name"` + Kind string `json:"kind"` +} + // BackupConfigurationSpec defines the desired state of BackupConfiguration type BackupConfigurationSpec struct { // INSERT ADDITIONAL SPEC FIELDS - desired state of cluster @@ -37,6 +44,9 @@ type BackupConfigurationSpec struct { Repository `json:"repository"` Task string `json:"task,omitempty"` Schedule string `json:"schedule"` + Target `json:"target"` + // +optional + VolumeMounts []corev1.VolumeMount `json:"volumeMounts,omitempty"` // +optional Suspend *bool `json:"suspend,omitempty"` } diff --git a/api/v1alpha1/backupsession_types.go b/api/v1alpha1/backupsession_types.go index e81f4b0..892f77c 100644 --- a/api/v1alpha1/backupsession_types.go +++ b/api/v1alpha1/backupsession_types.go @@ -24,8 +24,7 @@ import ( // 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"` - Namespace string `json:"namespace"` + Name string `json:"name"` } // BackupSessionSpec defines the desired state of BackupSession diff --git a/api/v1alpha1/repo_types.go b/api/v1alpha1/repo_types.go new file mode 100644 index 0000000..e66dea1 --- /dev/null +++ b/api/v1alpha1/repo_types.go @@ -0,0 +1,75 @@ +/* + + +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 ( + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" +) + +// EDIT THIS FILE! THIS IS SCAFFOLDING FOR YOU TO OWN! +// NOTE: json tags are required. Any new fields you add must have json tags for the fields to be serialized. + +type S3 struct { + Server string `json:"server"` + Bucket string `json:"bucket"` + // +optional + Prefix string `json:"prefix,omitempty"` +} + +type Backend struct { + S3 `json:"s3"` +} + +// RepoSpec defines the desired state of Repo +type RepoSpec struct { + // INSERT ADDITIONAL SPEC FIELDS - desired state of cluster + // Important: Run "make" to regenerate code after modifying this file + + // Foo is an example field of Repo. Edit Repo_types.go to remove/update + Backend `json:"backend"` + RepositorySecrets string `json:"repositorySecrets"` +} + +// RepoStatus defines the observed state of Repo +type RepoStatus struct { + // INSERT ADDITIONAL STATUS FIELD - define observed state of cluster + // Important: Run "make" to regenerate code after modifying this file +} + +// +kubebuilder:object:root=true + +// Repo is the Schema for the repoes API +type Repo struct { + metav1.TypeMeta `json:",inline"` + metav1.ObjectMeta `json:"metadata,omitempty"` + + Spec RepoSpec `json:"spec,omitempty"` + Status RepoStatus `json:"status,omitempty"` +} + +// +kubebuilder:object:root=true + +// RepoList contains a list of Repo +type RepoList struct { + metav1.TypeMeta `json:",inline"` + metav1.ListMeta `json:"metadata,omitempty"` + Items []Repo `json:"items"` +} + +func init() { + SchemeBuilder.Register(&Repo{}, &RepoList{}) +} diff --git a/controllers/backupconfiguration_controller.go b/controllers/backupconfiguration_controller.go index 78fd58e..bd5778f 100644 --- a/controllers/backupconfiguration_controller.go +++ b/controllers/backupconfiguration_controller.go @@ -20,7 +20,10 @@ import ( "context" "github.com/go-logr/logr" + appsv1 "k8s.io/api/apps/v1" kbatch_beta1 "k8s.io/api/batch/v1beta1" + corev1 "k8s.io/api/core/v1" + // metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/runtime" ctrl "sigs.k8s.io/controller-runtime" "sigs.k8s.io/controller-runtime/pkg/client" @@ -38,6 +41,89 @@ type BackupConfigurationReconciler struct { // +kubebuilder:rbac:groups=formol.desmojim.fr,resources=backupconfigurations,verbs=get;list;watch;create;update;patch;delete // +kubebuilder:rbac:groups=formol.desmojim.fr,resources=backupconfigurations/status,verbs=get;update;patch +func (r *BackupConfigurationReconciler) addSidecarContainer(backupConf *formolv1alpha1.BackupConfiguration) error { + log := r.Log.WithValues("Repository", backupConf.Spec.Repository.Name) + repo := &formolv1alpha1.Repo{} + sidecar := corev1.Container{ + Name: "backup", + Image: "busybox", + Command: []string{"sh", "-c", "echo Toto; sleep 3600"}, + Env: []corev1.EnvVar{ + corev1.EnvVar{ + Name: "POD_NAME", + ValueFrom: &corev1.EnvVarSource{ + FieldRef: &corev1.ObjectFieldSelector{ + FieldPath: "metadata.name", + }, + }, + }, + }, + VolumeMounts: []corev1.VolumeMount{}, + } + + // Gather information from the repo + if err := r.Get(context.Background(), client.ObjectKey{ + Namespace: "backup", + Name: backupConf.Spec.Repository.Name, + }, repo); err != nil { + log.Error(err, "unable to get Repo from BackupConfiguration") + return err + } + // S3 backing storage + if (formolv1alpha1.S3{}) != repo.Spec.Backend.S3 { + url := "s3:http://" + repo.Spec.Backend.S3.Server + "/" + repo.Spec.Backend.S3.Bucket + "/" + backupConf.Spec.Target.Name + sidecar.Env = append(sidecar.Env, corev1.EnvVar{ + Name: "RESTIC_REPOSITORY", + Value: url, + }) + for _, key := range []string{ + "AWS_ACCESS_KEY_ID", + "AWS_SECRET_ACCESS_KEY", + "RESTIC_PASSWORD", + } { + sidecar.Env = append(sidecar.Env, corev1.EnvVar{ + Name: key, + ValueFrom: &corev1.EnvVarSource{ + SecretKeyRef: &corev1.SecretKeySelector{ + LocalObjectReference: corev1.LocalObjectReference{ + Name: repo.Spec.RepositorySecrets, + }, + Key: key, + }, + }, + }) + } + } + + log.WithValues("Deployment", backupConf.Spec.Target.Name) + deployment := &appsv1.Deployment{} + if err := r.Get(context.Background(), client.ObjectKey{ + Namespace: backupConf.Namespace, + Name: backupConf.Spec.Target.Name, + }, deployment); err != nil { + log.Error(err, "unable to fetch Deployment") + return client.IgnoreNotFound(err) + } + 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 + } + } + for _, volumemount := range backupConf.Spec.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) + log.V(0).Info("Adding a sicar container") + if err := r.Update(context.Background(), deployment); err != nil { + log.Error(err, "unable to update the Deployment") + return err + } + return nil +} + func (r *BackupConfigurationReconciler) Reconcile(req ctrl.Request) (ctrl.Result, error) { ctx := context.Background() log := r.Log.WithValues("backupconfiguration", req.NamespacedName) @@ -45,18 +131,27 @@ func (r *BackupConfigurationReconciler) Reconcile(req ctrl.Request) (ctrl.Result log.V(1).Info("Enter Reconcile with req", "req", req) // your logic here - var backupConf formolv1alpha1.BackupConfiguration - if err := r.Get(ctx, req.NamespacedName, &backupConf); err != nil { + backupConf := &formolv1alpha1.BackupConfiguration{} + if err := r.Get(ctx, req.NamespacedName, backupConf); err != nil { log.Error(err, "unable to fetch BackupConfiguration") return ctrl.Result{}, client.IgnoreNotFound(err) } + switch backupConf.Spec.Target.Kind { + case "Deployment": + if err := r.addSidecarContainer(backupConf); err != nil { + return ctrl.Result{}, nil + } + case "PersistentVolumeClaim": + log.V(0).Info("TODO backup PVC") + return ctrl.Result{}, nil + } + if backupConf.Spec.Suspend != nil && *backupConf.Spec.Suspend == true { log.V(0).Info("We are suspended return and wait for the next event") // TODO Suspend the CronJob return ctrl.Result{}, nil } - backupConf.Status.Suspended = backupConf.Spec.Suspend return ctrl.Result{}, nil } diff --git a/controllers/backupsession_controller.go b/controllers/backupsession_controller.go index 7459810..0f819b9 100644 --- a/controllers/backupsession_controller.go +++ b/controllers/backupsession_controller.go @@ -18,8 +18,6 @@ package controllers import ( "context" - "fmt" - "time" "github.com/go-logr/logr" "k8s.io/apimachinery/pkg/runtime" @@ -27,9 +25,6 @@ import ( "sigs.k8s.io/controller-runtime/pkg/client" formolv1alpha1 "github.com/desmo999r/formol/api/v1alpha1" - batchv1 "k8s.io/api/batch/v1" - corev1 "k8s.io/api/core/v1" - metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" ) // BackupSessionReconciler reconciles a BackupSession object @@ -43,140 +38,43 @@ type BackupSessionReconciler struct { // +kubebuilder:rbac:groups=formol.desmojim.fr,resources=backupsessions/status,verbs=get;update;patch func (r *BackupSessionReconciler) Reconcile(req ctrl.Request) (ctrl.Result, error) { - ctx := context.Background() log := r.Log.WithValues("backupsession", req.NamespacedName) + ctx := context.Background() // your logic here - var backupSession formolv1alpha1.BackupSession - if err := r.Get(ctx, req.NamespacedName, &backupSession); err != nil { + backupSession := &formolv1alpha1.BackupSession{} + if err := r.Get(ctx, req.NamespacedName, backupSession); err != nil { log.Error(err, "unable to get backupsession") return ctrl.Result{}, client.IgnoreNotFound(err) } - var backupConf formolv1alpha1.BackupConfiguration + log.V(1).Info("backupSession.Namespace", "namespace", backupSession.Namespace) + log.V(1).Info("backupSession.Spec.Ref.Name", "name", backupSession.Spec.Ref.Name) + backupConf := &formolv1alpha1.BackupConfiguration{} if err := r.Get(ctx, client.ObjectKey{ - Namespace: backupSession.Spec.Ref.Namespace, - Name: backupSession.Spec.Ref.Name}, &backupConf); err != nil { + Namespace: backupSession.Namespace, + Name: backupSession.Spec.Ref.Name}, backupConf); err != nil { log.Error(err, "unable to get backupConfiguration") return ctrl.Result{}, client.IgnoreNotFound(err) } log.V(1).Info("Found BackupConfiguration", "BackupConfiguration", backupConf) - var childJobs batchv1.JobList - if err := r.List(ctx, &childJobs, client.InNamespace(req.Namespace), client.MatchingFields{jobOwnerKey: backupSession.Spec.Ref.Name}); err != nil { - log.Error(err, "unable to list child jobs") - return ctrl.Result{}, err - } - var activeJobs []*batchv1.Job - var successfulJobs []*batchv1.Job - var failedJobs []*batchv1.Job - - isJobFinished := func(job *batchv1.Job) (bool, batchv1.JobConditionType) { - for _, c := range job.Status.Conditions { - if (c.Type == batchv1.JobComplete || c.Type == batchv1.JobFailed) && c.Status == corev1.ConditionTrue { - return true, c.Type - } - } - return false, "" - } - - for i, job := range childJobs.Items { - _, finishedType := isJobFinished(&job) - switch finishedType { - case "": // running - activeJobs = append(activeJobs, &childJobs.Items[i]) - case batchv1.JobFailed: - failedJobs = append(failedJobs, &childJobs.Items[i]) - case batchv1.JobComplete: - successfulJobs = append(successfulJobs, &childJobs.Items[i]) - } - } - - if len(activeJobs) > 0 { - log.V(0).Info("A backup job is already running. Skipping") + // Found the BackupConfiguration. + switch backupConf.Spec.Target.Kind { + case "PersistentVolumeClaim": + return r.CreateJob() + default: return ctrl.Result{}, nil } +} - constructJobForBackupConfiguration := func(backupConf formolv1alpha1.BackupConfiguration) (*batchv1.Job, error) { - name := fmt.Sprintf("%s-%d", backupConf.Name, time.Now().Unix()) - log.V(1).Info("constructing a new Job", "name", name) - task := &formolv1alpha1.Task{} - if err := r.Get(ctx, client.ObjectKey{ - Namespace: "backup", - Name: backupConf.Spec.Task, - }, task); err != nil { - log.Error(err, "unable to get Task from BackupConfiguration") - return nil, err - } - log.V(1).Info("found task", "task", task.Name) - containers := []corev1.Container{} - for _, step := range task.Spec.Steps { - function := &formolv1alpha1.Function{} - if err := r.Get(ctx, client.ObjectKey{ - Namespace: "backup", - Name: step.Name, - }, function); err != nil { - log.Error(err, "unable to get Function") - return nil, err - } - log.V(1).Info("found function", "function", function.Name) - containers = append(containers, *function.Spec.DeepCopy()) - } - job := &batchv1.Job{ - ObjectMeta: metav1.ObjectMeta{ - Labels: make(map[string]string), - Annotations: make(map[string]string), - Name: name, - Namespace: backupConf.Namespace, - }, - Spec: batchv1.JobSpec{ - Template: corev1.PodTemplateSpec{ - Spec: corev1.PodSpec{ - Containers: containers, - RestartPolicy: corev1.RestartPolicyOnFailure, - }, - }, - }, - } - return job, nil - - } - job, err := constructJobForBackupConfiguration(backupConf) - if err != nil { - log.Error(err, "unable to construct job") - return ctrl.Result{}, nil - } - - if err := r.Create(ctx, job); err != nil { - log.Error(err, "unable to create job") - return ctrl.Result{}, err - } - +func (r *BackupSessionReconciler) CreateJob() (ctrl.Result, error) { return ctrl.Result{}, nil } -var ( - jobOwnerKey = ".metadata.controller" - apiGVStr = formolv1alpha1.GroupVersion.String() -) - func (r *BackupSessionReconciler) SetupWithManager(mgr ctrl.Manager) error { - if err := mgr.GetFieldIndexer().IndexField(&batchv1.Job{}, jobOwnerKey, func(rawObj runtime.Object) []string { - job := rawObj.(*batchv1.Job) - owner := metav1.GetControllerOf(job) - if owner == nil { - return nil - } - if owner.APIVersion != apiGVStr || owner.Kind != "BackupSession" { - return nil - } - return []string{owner.Name} - }); err != nil { - return err - } return ctrl.NewControllerManagedBy(mgr). For(&formolv1alpha1.BackupSession{}). - Owns(&batchv1.Job{}). Complete(r) }