diff --git a/api/v1alpha1/backupconfiguration_types.go b/api/v1alpha1/backupconfiguration_types.go index 9e38cc1..fae0698 100644 --- a/api/v1alpha1/backupconfiguration_types.go +++ b/api/v1alpha1/backupconfiguration_types.go @@ -57,10 +57,13 @@ type BackupConfigurationStatus struct { // INSERT ADDITIONAL STATUS FIELD - define observed state of cluster // Important: Run "make" to regenerate code after modifying this file LastBackupTime *metav1.Time `json:"lastBackupTime,omitempty"` - Suspended *bool `json:"suspended"` + Suspended bool `json:"suspended"` + ActiveCronJob bool `json:"activeCronJob"` + ActiveSidecar bool `json:"activeSidecar"` } // +kubebuilder:object:root=true +// +kubebuilder:subresource:status // BackupConfiguration is the Schema for the backupconfigurations API type BackupConfiguration struct { diff --git a/api/v1alpha1/zz_generated.deepcopy.go b/api/v1alpha1/zz_generated.deepcopy.go index 792d1fb..175d7b0 100644 --- a/api/v1alpha1/zz_generated.deepcopy.go +++ b/api/v1alpha1/zz_generated.deepcopy.go @@ -142,11 +142,6 @@ func (in *BackupConfigurationStatus) DeepCopyInto(out *BackupConfigurationStatus in, out := &in.LastBackupTime, &out.LastBackupTime *out = (*in).DeepCopy() } - if in.Suspended != nil { - in, out := &in.Suspended, &out.Suspended - *out = new(bool) - **out = **in - } } // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new BackupConfigurationStatus. diff --git a/config/samples/formol_v1alpha1_backupconfiguration.yaml b/config/samples/formol_v1alpha1_backupconfiguration.yaml index 408129d..cc33bb6 100644 --- a/config/samples/formol_v1alpha1_backupconfiguration.yaml +++ b/config/samples/formol_v1alpha1_backupconfiguration.yaml @@ -5,7 +5,7 @@ metadata: spec: repository: name: repo-minio - schedule: "*/5 * * * *" + schedule: "*/1 * * * *" target: apiVersion: v1 kind: Deployment diff --git a/controllers/backupconfiguration_controller.go b/controllers/backupconfiguration_controller.go index b0ff251..53560d5 100644 --- a/controllers/backupconfiguration_controller.go +++ b/controllers/backupconfiguration_controller.go @@ -21,9 +21,12 @@ import ( "github.com/go-logr/logr" appsv1 "k8s.io/api/apps/v1" + batchv1 "k8s.io/api/batch/v1" kbatch_beta1 "k8s.io/api/batch/v1beta1" corev1 "k8s.io/api/core/v1" - // metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + // rbacv1 "k8s.io/api/rbac/v1" + "k8s.io/apimachinery/pkg/api/errors" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/runtime" ctrl "sigs.k8s.io/controller-runtime" "sigs.k8s.io/controller-runtime/pkg/client" @@ -42,16 +45,44 @@ type BackupConfigurationReconciler struct { // +kubebuilder:rbac:groups=formol.desmojim.fr,resources=backupconfigurations/status,verbs=get;update;patch // +kubebuilder:rbac:groups=formol.desmojim.fr,resources=repoes,verbs=get;list;watch // +kubebuilder:rbac:groups=apps,resources=deployments,verbs=get;list;watch;create;update;patch;delete +// +kubebuilder:rbac:groups=core,resources=serviceaccounts,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 func (r *BackupConfigurationReconciler) addSidecarContainer(backupConf *formolv1alpha1.BackupConfiguration) error { - log := r.Log.WithValues("Repository", backupConf.Spec.Repository.Name) + log := r.Log.WithValues("backupconf", backupConf.Name) + getDeployment := func() (*appsv1.Deployment, error) { + deployment := &appsv1.Deployment{} + err := r.Get(context.Background(), client.ObjectKey{ + Namespace: backupConf.Namespace, + Name: backupConf.Spec.Target.Name, + }, deployment) + return deployment, err + } + + deployment, err := getDeployment() + if err != nil { + log.Error(err, "unable to get Deployment") + return err + } + log.WithValues("Deployment", backupConf.Spec.Target.Name) + 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 + } + } repo := &formolv1alpha1.Repo{} sidecar := corev1.Container{ Name: "backup", Image: "desmo999r/formolcli:latest", Args: []string{"create", "server"}, + //Image: "busybox", + //Command: []string{ + // "sh", + // "-c", + // "sleep 3600; echo done", + //}, Env: []corev1.EnvVar{ corev1.EnvVar{ Name: "POD_NAME", @@ -75,7 +106,7 @@ func (r *BackupConfigurationReconciler) addSidecarContainer(backupConf *formolv1 // Gather information from the repo if err := r.Get(context.Background(), client.ObjectKey{ - Namespace: "backup", + Namespace: backupConf.Namespace, Name: backupConf.Spec.Repository.Name, }, repo); err != nil { log.Error(err, "unable to get Repo from BackupConfiguration") @@ -107,32 +138,166 @@ func (r *BackupConfigurationReconciler) addSidecarContainer(backupConf *formolv1 } } - 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()) } + 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.MatchingLabels(selector)) + if err != nil { + log.Error(err, "unable to get deployment pods") + return nil + } + podsToDelete := []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) + podsToDelete = append(podsToDelete, *rs) + } + } + } + } 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 } + for _, pod := range podsToDelete { + if err := r.Delete(context.TODO(), &pod); err != nil { + log.Error(err, "unable to delete pod", "pod", pod.Name) + return nil + } + } + deployment, err = getDeployment() + if err != nil { + log.Error(err, "unable to get Deployment") + return err + } + return nil +} + +func (r *BackupConfigurationReconciler) addCronJob(backupConf *formolv1alpha1.BackupConfiguration) error { + log := r.Log.WithName("addCronJob") + + // serviceaccount := &corev1.ServiceAccount{ + // ObjectMeta: metav1.ObjectMeta{ + // Namespace: backupConf.Namespace, + // Name: "backupsession-creator", + // }, + // } + // if err := r.Get(context.Background(), client.ObjectKey{ + // Namespace: backupConf.Namespace, + // Name: "backupsession-creator", + // }, serviceaccount); err != nil && errors.IsNotFound(err) { + // log.V(0).Info("creating service account", "service account", serviceaccount) + // if err = r.Create(context.Background(), serviceaccount); err != nil { + // log.Error(err, "unable to create serviceaccount", "serviceaccount", serviceaccount) + // return nil + // } + // } + // rolebinding := &rbacv1.RoleBinding{ + // ObjectMeta: metav1.ObjectMeta{ + // Namespace: backupConf.Namespace, + // Name: "backupsession-creator-rolebinding", + // }, + // Subjects: []rbacv1.Subject{ + // rbacv1.Subject{ + // Kind: "ServiceAccount", + // Name: "backupsession-creator", + // }, + // }, + // RoleRef: rbacv1.RoleRef{ + // APIGroup: "rbac.authorization.k8s.io", + // Kind: "ClusterRole", + // Name: "backupsession-creator", + // }, + // } + // if err := r.Get(context.Background(), client.ObjectKey{ + // Namespace: backupConf.Namespace, + // Name: "backupsession-creator-rolebinding", + // }, rolebinding); err != nil && errors.IsNotFound(err) { + // log.V(0).Info("creating role binding for service account", "rolebinding", rolebinding, "service account", serviceaccount) + // if err = r.Create(context.Background(), rolebinding); err != nil { + // log.Error(err, "unable to create rolebinding", "rolebinding", rolebinding) + // 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", "cronjob", cronjob, "backupconf", backupConf.Name) + 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{ + 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{ + "create", + "backupsession", + "--namespace", + backupConf.Namespace, + "--name", + backupConf.Name, + }, + }, + }, + }, + }, + }, + }, + }, + } + if err := ctrl.SetControllerReference(backupConf, cronjob, r.Scheme); err != nil { + log.Error(err, "unable to set controller on job", "cronjob", cronjob, "backupconf", backupConf) + return err + } + log.V(0).Info("creating the cronjob") + if err := r.Create(context.Background(), cronjob); err != nil { + log.Error(err, "unable to create the cronjob", "cronjob", cronjob) + return err + } return nil } @@ -149,11 +314,17 @@ func (r *BackupConfigurationReconciler) Reconcile(req ctrl.Request) (ctrl.Result return ctrl.Result{}, client.IgnoreNotFound(err) } + if err := r.addCronJob(backupConf); err != nil { + return ctrl.Result{}, nil + } + backupConf.Status.ActiveCronJob = true + switch backupConf.Spec.Target.Kind { case "Deployment": if err := r.addSidecarContainer(backupConf); err != nil { return ctrl.Result{}, nil } + backupConf.Status.ActiveSidecar = true case "PersistentVolumeClaim": log.V(0).Info("TODO backup PVC") return ctrl.Result{}, nil @@ -165,6 +336,12 @@ func (r *BackupConfigurationReconciler) Reconcile(req ctrl.Request) (ctrl.Result return ctrl.Result{}, nil } + 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 }