diff --git a/api/v1alpha1/backupsession_types.go b/api/v1alpha1/backupsession_types.go index 171aa7e..4c2a38c 100644 --- a/api/v1alpha1/backupsession_types.go +++ b/api/v1alpha1/backupsession_types.go @@ -23,16 +23,6 @@ import ( // EDIT THIS FILE! THIS IS SCAFFOLDING FOR YOU TO OWN! // NOTE: json tags are required. Any new fields you add must have json tags for the fields to be serialized. -type BackupState string - -const ( - New BackupState = "New" - Running BackupState = "Running" - Success BackupState = "Success" - Failure BackupState = "Failure" - Deleted BackupState = "Deleted" -) - type Ref struct { Name string `json:"name"` } @@ -46,19 +36,6 @@ type BackupSessionSpec struct { Ref `json:"ref"` } -type TargetStatus struct { - Name string `json:"name"` - Kind string `json:"kind"` - // +optional - BackupState `json:"state,omitempty"` - // +optional - SnapshotId string `json:"snapshotId,omitempty"` - // +optional - StartTime *metav1.Time `json:"startTime,omitempty"` - // +optional - Duration *metav1.Duration `json:"duration,omitempty"` -} - // BackupSessionStatus defines the observed state of BackupSession type BackupSessionStatus struct { // INSERT ADDITIONAL STATUS FIELD - define observed state of cluster @@ -66,7 +43,7 @@ type BackupSessionStatus struct { // +optional ObservedGeneration int64 `json:"observedGeneration,omitempty"` // +optional - BackupState `json:"state,omitempty"` + SessionState `json:"state,omitempty"` // +optional StartTime *metav1.Time `json:"startTime,omitempty"` // +optional diff --git a/api/v1alpha1/restoresession_types.go b/api/v1alpha1/restoresession_types.go new file mode 100644 index 0000000..95b9df4 --- /dev/null +++ b/api/v1alpha1/restoresession_types.go @@ -0,0 +1,71 @@ +/* + + +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. + +// RestoreSessionSpec defines the desired state of RestoreSession +type RestoreSessionSpec struct { + // INSERT ADDITIONAL SPEC FIELDS - desired state of cluster + // Important: Run "make" to regenerate code after modifying this file + + BackupSessionRef metav1.ObjectMeta `json:"backupSessionRef"` +} + +// RestoreSessionStatus defines the observed state of RestoreSession +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 + SessionState `json:"state,omitempty"` + // +optional + StartTime *metav1.Time `json:"startTime,omitempty"` + // +optional + Targets []TargetStatus `json:"target,omitempty"` +} + +// +kubebuilder:object:root=true +// +kubebuilder:resource:shortName="rs" +// +kubebuilder:subresource:status + +// RestoreSession is the Schema for the restoresessions API +type RestoreSession struct { + metav1.TypeMeta `json:",inline"` + metav1.ObjectMeta `json:"metadata,omitempty"` + + Spec RestoreSessionSpec `json:"spec,omitempty"` + Status RestoreSessionStatus `json:"status,omitempty"` +} + +// +kubebuilder:object:root=true + +// RestoreSessionList contains a list of RestoreSession +type RestoreSessionList struct { + metav1.TypeMeta `json:",inline"` + metav1.ListMeta `json:"metadata,omitempty"` + Items []RestoreSession `json:"items"` +} + +func init() { + SchemeBuilder.Register(&RestoreSession{}, &RestoreSessionList{}) +} diff --git a/api/v1alpha1/zz_generated.deepcopy.go b/api/v1alpha1/zz_generated.deepcopy.go index f33fd4c..83175c0 100644 --- a/api/v1alpha1/zz_generated.deepcopy.go +++ b/api/v1alpha1/zz_generated.deepcopy.go @@ -458,6 +458,107 @@ func (in *Repository) DeepCopy() *Repository { return out } +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *RestoreSession) DeepCopyInto(out *RestoreSession) { + *out = *in + out.TypeMeta = in.TypeMeta + in.ObjectMeta.DeepCopyInto(&out.ObjectMeta) + in.Spec.DeepCopyInto(&out.Spec) + in.Status.DeepCopyInto(&out.Status) +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new RestoreSession. +func (in *RestoreSession) DeepCopy() *RestoreSession { + if in == nil { + return nil + } + out := new(RestoreSession) + in.DeepCopyInto(out) + return out +} + +// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. +func (in *RestoreSession) DeepCopyObject() runtime.Object { + if c := in.DeepCopy(); c != nil { + return c + } + return nil +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *RestoreSessionList) DeepCopyInto(out *RestoreSessionList) { + *out = *in + out.TypeMeta = in.TypeMeta + in.ListMeta.DeepCopyInto(&out.ListMeta) + if in.Items != nil { + in, out := &in.Items, &out.Items + *out = make([]RestoreSession, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new RestoreSessionList. +func (in *RestoreSessionList) DeepCopy() *RestoreSessionList { + if in == nil { + return nil + } + out := new(RestoreSessionList) + in.DeepCopyInto(out) + return out +} + +// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. +func (in *RestoreSessionList) DeepCopyObject() runtime.Object { + if c := in.DeepCopy(); c != nil { + return c + } + return nil +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *RestoreSessionSpec) DeepCopyInto(out *RestoreSessionSpec) { + *out = *in + in.BackupSessionRef.DeepCopyInto(&out.BackupSessionRef) +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new RestoreSessionSpec. +func (in *RestoreSessionSpec) DeepCopy() *RestoreSessionSpec { + if in == nil { + return nil + } + out := new(RestoreSessionSpec) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *RestoreSessionStatus) DeepCopyInto(out *RestoreSessionStatus) { + *out = *in + if in.StartTime != nil { + in, out := &in.StartTime, &out.StartTime + *out = (*in).DeepCopy() + } + if in.Targets != nil { + in, out := &in.Targets, &out.Targets + *out = make([]TargetStatus, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new RestoreSessionStatus. +func (in *RestoreSessionStatus) DeepCopy() *RestoreSessionStatus { + if in == nil { + return nil + } + out := new(RestoreSessionStatus) + in.DeepCopyInto(out) + return out +} + // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *S3) DeepCopyInto(out *S3) { *out = *in diff --git a/config/crd/kustomization.yaml b/config/crd/kustomization.yaml index 0b4701d..3557d7e 100644 --- a/config/crd/kustomization.yaml +++ b/config/crd/kustomization.yaml @@ -6,6 +6,7 @@ resources: - bases/formol.desmojim.fr_backupconfigurations.yaml - bases/formol.desmojim.fr_backupsessions.yaml - bases/formol.desmojim.fr_repoes.yaml +- bases/formol.desmojim.fr_restoresessions.yaml # +kubebuilder:scaffold:crdkustomizeresource patchesStrategicMerge: @@ -16,6 +17,7 @@ patchesStrategicMerge: #- patches/webhook_in_backupconfigurations.yaml - patches/webhook_in_backupsessions.yaml #- patches/webhook_in_repoes.yaml +#- patches/webhook_in_restoresessions.yaml # +kubebuilder:scaffold:crdkustomizewebhookpatch # [CERTMANAGER] To enable webhook, uncomment all the sections with [CERTMANAGER] prefix. @@ -24,6 +26,7 @@ patchesStrategicMerge: - patches/cainjection_in_backupconfigurations.yaml - patches/cainjection_in_backupsessions.yaml #- patches/cainjection_in_repoes.yaml +#- patches/cainjection_in_restoresessions.yaml # +kubebuilder:scaffold:crdkustomizecainjectionpatch # the following config is for teaching kustomize how to do kustomization for CRDs. diff --git a/config/crd/patches/cainjection_in_restoresessions.yaml b/config/crd/patches/cainjection_in_restoresessions.yaml new file mode 100644 index 0000000..aa30a5c --- /dev/null +++ b/config/crd/patches/cainjection_in_restoresessions.yaml @@ -0,0 +1,8 @@ +# The following patch adds a directive for certmanager to inject CA into the CRD +# CRD conversion requires k8s 1.13 or later. +apiVersion: apiextensions.k8s.io/v1beta1 +kind: CustomResourceDefinition +metadata: + annotations: + cert-manager.io/inject-ca-from: $(CERTIFICATE_NAMESPACE)/$(CERTIFICATE_NAME) + name: restoresessions.formol.desmojim.fr.desmojim.fr diff --git a/config/crd/patches/webhook_in_restoresessions.yaml b/config/crd/patches/webhook_in_restoresessions.yaml new file mode 100644 index 0000000..c412117 --- /dev/null +++ b/config/crd/patches/webhook_in_restoresessions.yaml @@ -0,0 +1,17 @@ +# The following patch enables conversion webhook for CRD +# CRD conversion requires k8s 1.13 or later. +apiVersion: apiextensions.k8s.io/v1beta1 +kind: CustomResourceDefinition +metadata: + name: restoresessions.formol.desmojim.fr.desmojim.fr +spec: + conversion: + strategy: Webhook + webhookClientConfig: + # this is "\n" used as a placeholder, otherwise it will be rejected by the apiserver for being blank, + # but we're going to set it later using the cert-manager (or potentially a patch if not using cert-manager) + caBundle: Cg== + service: + namespace: system + name: webhook-service + path: /convert diff --git a/config/samples/formol.desmojim.fr_v1alpha1_restoresession.yaml b/config/samples/formol.desmojim.fr_v1alpha1_restoresession.yaml new file mode 100644 index 0000000..f6782a5 --- /dev/null +++ b/config/samples/formol.desmojim.fr_v1alpha1_restoresession.yaml @@ -0,0 +1,7 @@ +apiVersion: formol.desmojim.fr.desmojim.fr/v1alpha1 +kind: RestoreSession +metadata: + name: restoresession-sample +spec: + # Add fields here + foo: bar diff --git a/controllers/restoresession_controller.go b/controllers/restoresession_controller.go new file mode 100644 index 0000000..9153195 --- /dev/null +++ b/controllers/restoresession_controller.go @@ -0,0 +1,140 @@ +/* + + +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 controllers + +import ( + "context" + "time" + + "github.com/go-logr/logr" + batchv1 "k8s.io/api/batch/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" + + formolv1alpha1 "github.com/desmo999r/formol/api/v1alpha1" +) + +// RestoreSessionReconciler reconciles a RestoreSession object +type RestoreSessionReconciler struct { + client.Client + Log logr.Logger + Scheme *runtime.Scheme + RestoreSession *formolv1alpha1.RestoreSession + BackupSession *formolv1alpha1.BackupSession + BackupConf *formolv1alpha1.BackupConfiguration +} + +func (r *RestoreSessionReconciler) CreateRestoreJob(target formolv1alpha1.Target) error { + return nil +} + +func (r *RestoreSessionReconciler) StatusUpdate() error { + log := r.Log.WithValues("statusupdate", r.RestoreSession.Name) + ctx := context.Background() + startNextTask := func() (*formolv1alpha1.TargetStatus, error) { + nextTarget := len(r.RestoreSession.Status.Targets) + if nextTarget < len(r.BackupConf.Spec.Targets) { + target := r.BackupConf.Spec.Targets[len(r.BackupConf.Spec.Targets)-1-nextTarget] + targetStatus := formolv1alpha1.TargetStatus{ + Name: target.Name, + Kind: target.Kind, + SessionState: formolv1alpha1.New, + } + r.RestoreSession.Status.Targets = append(r.RestoreSession.Status.Targets, targetStatus) + switch target.Kind { + case "Task": + if err := r.CreateRestoreJob(target); err != nil { + log.V(0).Info("unable to create restore task", "task", target) + targetStatus.SessionState = formolv1alpha1.Failure + return nil, err + } + } + return &targetStatus, nil + } else { + return nil, nil + } + } + var ret error + switch r.RestoreSession.Status.SessionState { + case formolv1alpha1.New: + r.RestoreSession.Status.SessionState = formolv1alpha1.Running + targetStatus, err := startNextTask() + if err != nil { + return err + } + log.V(0).Info("New restore. Start the first task", "task", targetStatus.Name) + } + if ret = r.Status().Update(ctx, r.RestoreSession); ret != nil { + log.Error(ret, "unable to update restoresession") + } + return ret +} + +// +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(100 * 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) + } + 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} + + 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 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 { + return ctrl.NewControllerManagedBy(mgr). + For(&formolv1alpha1.RestoreSession{}). + Owns(&batchv1.Job{}). + Complete(r) +} diff --git a/controllers/suite_test.go b/controllers/suite_test.go index 866de4f..66edd1d 100644 --- a/controllers/suite_test.go +++ b/controllers/suite_test.go @@ -30,6 +30,7 @@ import ( logf "sigs.k8s.io/controller-runtime/pkg/log" "sigs.k8s.io/controller-runtime/pkg/log/zap" + formoldesmojimfrv1alpha1 "github.com/desmo999r/formol/api/v1alpha1" formolv1alpha1 "github.com/desmo999r/formol/api/v1alpha1" // +kubebuilder:scaffold:imports ) @@ -68,6 +69,9 @@ var _ = BeforeSuite(func(done Done) { err = formolv1alpha1.AddToScheme(scheme.Scheme) Expect(err).NotTo(HaveOccurred()) + err = formoldesmojimfrv1alpha1.AddToScheme(scheme.Scheme) + Expect(err).NotTo(HaveOccurred()) + // +kubebuilder:scaffold:scheme k8sClient, err = client.New(cfg, client.Options{Scheme: scheme.Scheme}) diff --git a/main.go b/main.go index a907994..1b64d58 100644 --- a/main.go +++ b/main.go @@ -26,6 +26,7 @@ import ( ctrl "sigs.k8s.io/controller-runtime" "sigs.k8s.io/controller-runtime/pkg/log/zap" + formoldesmojimfrv1alpha1 "github.com/desmo999r/formol/api/v1alpha1" formolv1alpha1 "github.com/desmo999r/formol/api/v1alpha1" "github.com/desmo999r/formol/controllers" // +kubebuilder:scaffold:imports @@ -40,6 +41,7 @@ func init() { _ = clientgoscheme.AddToScheme(scheme) _ = formolv1alpha1.AddToScheme(scheme) + _ = formoldesmojimfrv1alpha1.AddToScheme(scheme) // +kubebuilder:scaffold:scheme } @@ -88,6 +90,14 @@ func main() { os.Exit(1) } } + if err = (&controllers.RestoreSessionReconciler{ + Client: mgr.GetClient(), + Log: ctrl.Log.WithName("controllers").WithName("RestoreSession"), + Scheme: mgr.GetScheme(), + }).SetupWithManager(mgr); err != nil { + setupLog.Error(err, "unable to create controller", "controller", "RestoreSession") + os.Exit(1) + } // +kubebuilder:scaffold:builder setupLog.Info("starting manager")