From 1dac5f6e138a9e765b0631bebc285eb0206b82f6 Mon Sep 17 00:00:00 2001 From: Mateus Oliveira Date: Tue, 9 Apr 2024 10:01:56 -0300 Subject: [PATCH 1/8] feat: Add Non Admin Restore controller Signed-off-by: Mateus Oliveira --- PROJECT | 9 ++ api/v1alpha1/nonadminrestore_types.go | 64 +++++++++++++ api/v1alpha1/zz_generated.deepcopy.go | 89 +++++++++++++++++++ cmd/main.go | 8 ++ config/crd/kustomization.yaml | 3 + config/rbac/nonadminrestore_editor_role.yaml | 31 +++++++ config/rbac/nonadminrestore_viewer_role.yaml | 27 ++++++ config/samples/kustomization.yaml | 1 + .../samples/nac_v1alpha1_nonadminrestore.yaml | 12 +++ docs/architecture.md | 6 ++ .../controller/nonadminrestore_controller.go | 62 +++++++++++++ .../nonadminrestore_controller_test.go | 84 +++++++++++++++++ internal/controller/suite_test.go | 3 + 13 files changed, 399 insertions(+) create mode 100644 api/v1alpha1/nonadminrestore_types.go create mode 100644 config/rbac/nonadminrestore_editor_role.yaml create mode 100644 config/rbac/nonadminrestore_viewer_role.yaml create mode 100644 config/samples/nac_v1alpha1_nonadminrestore.yaml create mode 100644 internal/controller/nonadminrestore_controller.go create mode 100644 internal/controller/nonadminrestore_controller_test.go diff --git a/PROJECT b/PROJECT index b10e3ba..3f6ef52 100644 --- a/PROJECT +++ b/PROJECT @@ -17,4 +17,13 @@ resources: kind: NonAdminBackup path: github.com/migtools/oadp-non-admin/api/v1alpha1 version: v1alpha1 +- api: + crdVersion: v1 + namespaced: true + controller: true + domain: oadp.openshift.io + group: nac + kind: NonAdminRestore + path: github.com/migtools/oadp-non-admin/api/v1alpha1 + version: v1alpha1 version: "3" diff --git a/api/v1alpha1/nonadminrestore_types.go b/api/v1alpha1/nonadminrestore_types.go new file mode 100644 index 0000000..3542775 --- /dev/null +++ b/api/v1alpha1/nonadminrestore_types.go @@ -0,0 +1,64 @@ +/* +Copyright 2024. + +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. + +// NonAdminRestoreSpec defines the desired state of NonAdminRestore +type NonAdminRestoreSpec 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 NonAdminRestore. Edit nonadminrestore_types.go to remove/update + Foo string `json:"foo,omitempty"` +} + +// NonAdminRestoreStatus defines the observed state of NonAdminRestore +type NonAdminRestoreStatus struct { + // INSERT ADDITIONAL STATUS FIELD - define observed state of cluster + // Important: Run "make" to regenerate code after modifying this file +} + +//+kubebuilder:object:root=true +//+kubebuilder:subresource:status + +// NonAdminRestore is the Schema for the nonadminrestores API +type NonAdminRestore struct { + metav1.TypeMeta `json:",inline"` + metav1.ObjectMeta `json:"metadata,omitempty"` + + Spec NonAdminRestoreSpec `json:"spec,omitempty"` + Status NonAdminRestoreStatus `json:"status,omitempty"` +} + +//+kubebuilder:object:root=true + +// NonAdminRestoreList contains a list of NonAdminRestore +type NonAdminRestoreList struct { + metav1.TypeMeta `json:",inline"` + metav1.ListMeta `json:"metadata,omitempty"` + Items []NonAdminRestore `json:"items"` +} + +func init() { + SchemeBuilder.Register(&NonAdminRestore{}, &NonAdminRestoreList{}) +} diff --git a/api/v1alpha1/zz_generated.deepcopy.go b/api/v1alpha1/zz_generated.deepcopy.go index 9ac916b..ac08481 100644 --- a/api/v1alpha1/zz_generated.deepcopy.go +++ b/api/v1alpha1/zz_generated.deepcopy.go @@ -131,3 +131,92 @@ func (in *NonAdminBackupStatus) DeepCopy() *NonAdminBackupStatus { in.DeepCopyInto(out) return out } + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *NonAdminRestore) DeepCopyInto(out *NonAdminRestore) { + *out = *in + out.TypeMeta = in.TypeMeta + in.ObjectMeta.DeepCopyInto(&out.ObjectMeta) + out.Spec = in.Spec + out.Status = in.Status +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new NonAdminRestore. +func (in *NonAdminRestore) DeepCopy() *NonAdminRestore { + if in == nil { + return nil + } + out := new(NonAdminRestore) + in.DeepCopyInto(out) + return out +} + +// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. +func (in *NonAdminRestore) 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 *NonAdminRestoreList) DeepCopyInto(out *NonAdminRestoreList) { + *out = *in + out.TypeMeta = in.TypeMeta + in.ListMeta.DeepCopyInto(&out.ListMeta) + if in.Items != nil { + in, out := &in.Items, &out.Items + *out = make([]NonAdminRestore, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new NonAdminRestoreList. +func (in *NonAdminRestoreList) DeepCopy() *NonAdminRestoreList { + if in == nil { + return nil + } + out := new(NonAdminRestoreList) + in.DeepCopyInto(out) + return out +} + +// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. +func (in *NonAdminRestoreList) 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 *NonAdminRestoreSpec) DeepCopyInto(out *NonAdminRestoreSpec) { + *out = *in +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new NonAdminRestoreSpec. +func (in *NonAdminRestoreSpec) DeepCopy() *NonAdminRestoreSpec { + if in == nil { + return nil + } + out := new(NonAdminRestoreSpec) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *NonAdminRestoreStatus) DeepCopyInto(out *NonAdminRestoreStatus) { + *out = *in +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new NonAdminRestoreStatus. +func (in *NonAdminRestoreStatus) DeepCopy() *NonAdminRestoreStatus { + if in == nil { + return nil + } + out := new(NonAdminRestoreStatus) + in.DeepCopyInto(out) + return out +} diff --git a/cmd/main.go b/cmd/main.go index 8d6be27..bf3e9d0 100644 --- a/cmd/main.go +++ b/cmd/main.go @@ -27,6 +27,7 @@ import ( "k8s.io/apimachinery/pkg/runtime" utilruntime "k8s.io/apimachinery/pkg/util/runtime" clientgoscheme "k8s.io/client-go/kubernetes/scheme" + // Import all Kubernetes client auth plugins (e.g. Azure, GCP, OIDC, etc.) // to ensure that exec-entrypoint and run can make use of them. _ "k8s.io/client-go/plugin/pkg/client/auth" @@ -138,6 +139,13 @@ func main() { setupLog.Error(err, "unable to create controller", "controller", "NonAdminBackup") os.Exit(1) } + if err = (&controller.NonAdminRestoreReconciler{ + Client: mgr.GetClient(), + Scheme: mgr.GetScheme(), + }).SetupWithManager(mgr); err != nil { + setupLog.Error(err, "unable to create controller", "controller", "NonAdminRestore") + os.Exit(1) + } // +kubebuilder:scaffold:builder if err := mgr.AddHealthzCheck("healthz", healthz.Ping); err != nil { diff --git a/config/crd/kustomization.yaml b/config/crd/kustomization.yaml index 2665502..a0ae6c7 100644 --- a/config/crd/kustomization.yaml +++ b/config/crd/kustomization.yaml @@ -3,17 +3,20 @@ # It should be run by config/default resources: - bases/nac.oadp.openshift.io_nonadminbackups.yaml +- bases/nac.oadp.openshift.io_nonadminrestores.yaml #+kubebuilder:scaffold:crdkustomizeresource patches: # [WEBHOOK] To enable webhook, uncomment all the sections with [WEBHOOK] prefix. # patches here are for enabling the conversion webhook for each CRD #- path: patches/webhook_in_nonadminbackups.yaml +#- path: patches/webhook_in_nonadminrestores.yaml #+kubebuilder:scaffold:crdkustomizewebhookpatch # [CERTMANAGER] To enable cert-manager, uncomment all the sections with [CERTMANAGER] prefix. # patches here are for enabling the CA injection for each CRD #- path: patches/cainjection_in_nonadminbackups.yaml +#- path: patches/cainjection_in_nonadminrestores.yaml #+kubebuilder:scaffold:crdkustomizecainjectionpatch # [WEBHOOK] To enable webhook, uncomment the following section diff --git a/config/rbac/nonadminrestore_editor_role.yaml b/config/rbac/nonadminrestore_editor_role.yaml new file mode 100644 index 0000000..9390a97 --- /dev/null +++ b/config/rbac/nonadminrestore_editor_role.yaml @@ -0,0 +1,31 @@ +# permissions for end users to edit nonadminrestores. +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRole +metadata: + labels: + app.kubernetes.io/name: clusterrole + app.kubernetes.io/instance: nonadminrestore-editor-role + app.kubernetes.io/component: rbac + app.kubernetes.io/created-by: oadp-nac + app.kubernetes.io/part-of: oadp-nac + app.kubernetes.io/managed-by: kustomize + name: nonadminrestore-editor-role +rules: +- apiGroups: + - nac.oadp.openshift.io + resources: + - nonadminrestores + verbs: + - create + - delete + - get + - list + - patch + - update + - watch +- apiGroups: + - nac.oadp.openshift.io + resources: + - nonadminrestores/status + verbs: + - get diff --git a/config/rbac/nonadminrestore_viewer_role.yaml b/config/rbac/nonadminrestore_viewer_role.yaml new file mode 100644 index 0000000..55b2b6c --- /dev/null +++ b/config/rbac/nonadminrestore_viewer_role.yaml @@ -0,0 +1,27 @@ +# permissions for end users to view nonadminrestores. +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRole +metadata: + labels: + app.kubernetes.io/name: clusterrole + app.kubernetes.io/instance: nonadminrestore-viewer-role + app.kubernetes.io/component: rbac + app.kubernetes.io/created-by: oadp-nac + app.kubernetes.io/part-of: oadp-nac + app.kubernetes.io/managed-by: kustomize + name: nonadminrestore-viewer-role +rules: +- apiGroups: + - nac.oadp.openshift.io + resources: + - nonadminrestores + verbs: + - get + - list + - watch +- apiGroups: + - nac.oadp.openshift.io + resources: + - nonadminrestores/status + verbs: + - get diff --git a/config/samples/kustomization.yaml b/config/samples/kustomization.yaml index 7b31120..f5148bf 100644 --- a/config/samples/kustomization.yaml +++ b/config/samples/kustomization.yaml @@ -1,4 +1,5 @@ ## Append samples of your project ## resources: - nac_v1alpha1_nonadminbackup.yaml +- nac_v1alpha1_nonadminrestore.yaml #+kubebuilder:scaffold:manifestskustomizesamples diff --git a/config/samples/nac_v1alpha1_nonadminrestore.yaml b/config/samples/nac_v1alpha1_nonadminrestore.yaml new file mode 100644 index 0000000..5a103d6 --- /dev/null +++ b/config/samples/nac_v1alpha1_nonadminrestore.yaml @@ -0,0 +1,12 @@ +apiVersion: nac.oadp.openshift.io/v1alpha1 +kind: NonAdminRestore +metadata: + labels: + app.kubernetes.io/name: nonadminrestore + app.kubernetes.io/instance: nonadminrestore-sample + app.kubernetes.io/part-of: oadp-nac + app.kubernetes.io/managed-by: kustomize + app.kubernetes.io/created-by: oadp-nac + name: nonadminrestore-sample +spec: + # TODO(user): Add fields here diff --git a/docs/architecture.md b/docs/architecture.md index 2a8bfc3..b4a7fb1 100644 --- a/docs/architecture.md +++ b/docs/architecture.md @@ -33,6 +33,12 @@ kubebuilder create api \ --version v1alpha1 \ --kind NonAdminBackup \ --resource --controller +kubebuilder create api \ + --plugins go.kubebuilder.io/v4 \ + --group nac \ + --version v1alpha1 \ + --kind NonAdminRestore \ + --resource --controller make manifests ``` > **NOTE:** The information about plugin and project version, as well as project name, repo and domain, is stored in [PROJECT](../PROJECT) file diff --git a/internal/controller/nonadminrestore_controller.go b/internal/controller/nonadminrestore_controller.go new file mode 100644 index 0000000..979faa3 --- /dev/null +++ b/internal/controller/nonadminrestore_controller.go @@ -0,0 +1,62 @@ +/* +Copyright 2024. + +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 controller + +import ( + "context" + + "k8s.io/apimachinery/pkg/runtime" + ctrl "sigs.k8s.io/controller-runtime" + "sigs.k8s.io/controller-runtime/pkg/client" + "sigs.k8s.io/controller-runtime/pkg/log" + + nacv1alpha1 "github.com/migtools/oadp-non-admin/api/v1alpha1" +) + +// NonAdminRestoreReconciler reconciles a NonAdminRestore object +type NonAdminRestoreReconciler struct { + client.Client + Scheme *runtime.Scheme +} + +//+kubebuilder:rbac:groups=nac.oadp.openshift.io,resources=nonadminrestores,verbs=get;list;watch;create;update;patch;delete +//+kubebuilder:rbac:groups=nac.oadp.openshift.io,resources=nonadminrestores/status,verbs=get;update;patch +//+kubebuilder:rbac:groups=nac.oadp.openshift.io,resources=nonadminrestores/finalizers,verbs=update + +// Reconcile is part of the main kubernetes reconciliation loop which aims to +// move the current state of the cluster closer to the desired state. +// TODO(user): Modify the Reconcile function to compare the state specified by +// the NonAdminRestore object against the actual cluster state, and then +// perform operations to make the cluster state reflect the state specified by +// the user. +// +// For more details, check Reconcile and its Result here: +// - https://pkg.go.dev/sigs.k8s.io/controller-runtime@v0.17.0/pkg/reconcile +func (r *NonAdminRestoreReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) { + _ = log.FromContext(ctx) + + // TODO(user): your logic here + + return ctrl.Result{}, nil +} + +// SetupWithManager sets up the controller with the Manager. +func (r *NonAdminRestoreReconciler) SetupWithManager(mgr ctrl.Manager) error { + return ctrl.NewControllerManagedBy(mgr). + For(&nacv1alpha1.NonAdminRestore{}). + Complete(r) +} diff --git a/internal/controller/nonadminrestore_controller_test.go b/internal/controller/nonadminrestore_controller_test.go new file mode 100644 index 0000000..7037554 --- /dev/null +++ b/internal/controller/nonadminrestore_controller_test.go @@ -0,0 +1,84 @@ +/* +Copyright 2024. + +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 controller + +import ( + "context" + + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" + "k8s.io/apimachinery/pkg/api/errors" + "k8s.io/apimachinery/pkg/types" + "sigs.k8s.io/controller-runtime/pkg/reconcile" + + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + + nacv1alpha1 "github.com/migtools/oadp-non-admin/api/v1alpha1" +) + +var _ = Describe("NonAdminRestore Controller", func() { + Context("When reconciling a resource", func() { + const resourceName = "test-resource" + + ctx := context.Background() + + typeNamespacedName := types.NamespacedName{ + Name: resourceName, + Namespace: "default", // TODO(user):Modify as needed + } + nonadminrestore := &nacv1alpha1.NonAdminRestore{} + + BeforeEach(func() { + By("creating the custom resource for the Kind NonAdminRestore") + err := k8sClient.Get(ctx, typeNamespacedName, nonadminrestore) + if err != nil && errors.IsNotFound(err) { + resource := &nacv1alpha1.NonAdminRestore{ + ObjectMeta: metav1.ObjectMeta{ + Name: resourceName, + Namespace: "default", + }, + // TODO(user): Specify other spec details if needed. + } + Expect(k8sClient.Create(ctx, resource)).To(Succeed()) + } + }) + + AfterEach(func() { + // TODO(user): Cleanup logic after each test, like removing the resource instance. + resource := &nacv1alpha1.NonAdminRestore{} + err := k8sClient.Get(ctx, typeNamespacedName, resource) + Expect(err).NotTo(HaveOccurred()) + + By("Cleanup the specific resource instance NonAdminRestore") + Expect(k8sClient.Delete(ctx, resource)).To(Succeed()) + }) + It("should successfully reconcile the resource", func() { + By("Reconciling the created resource") + controllerReconciler := &NonAdminRestoreReconciler{ + Client: k8sClient, + Scheme: k8sClient.Scheme(), + } + + _, err := controllerReconciler.Reconcile(ctx, reconcile.Request{ + NamespacedName: typeNamespacedName, + }) + Expect(err).NotTo(HaveOccurred()) + // TODO(user): Add more specific assertions depending on your controller's reconciliation logic. + // Example: If you expect a certain status condition after reconciliation, verify it here. + }) + }) +}) diff --git a/internal/controller/suite_test.go b/internal/controller/suite_test.go index e1510ba..ff5f04a 100644 --- a/internal/controller/suite_test.go +++ b/internal/controller/suite_test.go @@ -73,6 +73,9 @@ var _ = ginkgov2.BeforeSuite(func() { err = nacv1alpha1.AddToScheme(scheme.Scheme) gomega.Expect(err).NotTo(gomega.HaveOccurred()) + err = nacv1alpha1.AddToScheme(scheme.Scheme) + Expect(err).NotTo(HaveOccurred()) + // +kubebuilder:scaffold:scheme k8sClient, err = client.New(cfg, client.Options{Scheme: scheme.Scheme}) From 344be35badecafb0bdd4e52d5fcadc2ccef99b4a Mon Sep 17 00:00:00 2001 From: Mateus Oliveira Date: Tue, 9 Apr 2024 10:05:05 -0300 Subject: [PATCH 2/8] fixup! feat: Add Non Admin Restore controller Signed-off-by: Mateus Oliveira --- ...ac.oadp.openshift.io_nonadminrestores.yaml | 54 +++++++++++++++++++ config/rbac/role.yaml | 26 +++++++++ 2 files changed, 80 insertions(+) create mode 100644 config/crd/bases/nac.oadp.openshift.io_nonadminrestores.yaml diff --git a/config/crd/bases/nac.oadp.openshift.io_nonadminrestores.yaml b/config/crd/bases/nac.oadp.openshift.io_nonadminrestores.yaml new file mode 100644 index 0000000..5a8af11 --- /dev/null +++ b/config/crd/bases/nac.oadp.openshift.io_nonadminrestores.yaml @@ -0,0 +1,54 @@ +--- +apiVersion: apiextensions.k8s.io/v1 +kind: CustomResourceDefinition +metadata: + annotations: + controller-gen.kubebuilder.io/version: v0.14.0 + name: nonadminrestores.nac.oadp.openshift.io +spec: + group: nac.oadp.openshift.io + names: + kind: NonAdminRestore + listKind: NonAdminRestoreList + plural: nonadminrestores + singular: nonadminrestore + scope: Namespaced + versions: + - name: v1alpha1 + schema: + openAPIV3Schema: + description: NonAdminRestore is the Schema for the nonadminrestores API + properties: + apiVersion: + description: |- + APIVersion defines the versioned schema of this representation of an object. + Servers should convert recognized schemas to the latest internal value, and + may reject unrecognized values. + More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources + type: string + kind: + description: |- + Kind is a string value representing the REST resource this object represents. + Servers may infer this from the endpoint the client submits requests to. + Cannot be updated. + In CamelCase. + More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds + type: string + metadata: + type: object + spec: + description: NonAdminRestoreSpec defines the desired state of NonAdminRestore + properties: + foo: + description: Foo is an example field of NonAdminRestore. Edit nonadminrestore_types.go + to remove/update + type: string + type: object + status: + description: NonAdminRestoreStatus defines the observed state of NonAdminRestore + type: object + type: object + served: true + storage: true + subresources: + status: {} diff --git a/config/rbac/role.yaml b/config/rbac/role.yaml index 3b3efad..f013316 100644 --- a/config/rbac/role.yaml +++ b/config/rbac/role.yaml @@ -30,6 +30,32 @@ rules: - get - patch - update +- apiGroups: + - nac.oadp.openshift.io + resources: + - nonadminrestores + verbs: + - create + - delete + - get + - list + - patch + - update + - watch +- apiGroups: + - nac.oadp.openshift.io + resources: + - nonadminrestores/finalizers + verbs: + - update +- apiGroups: + - nac.oadp.openshift.io + resources: + - nonadminrestores/status + verbs: + - get + - patch + - update - apiGroups: - velero.io resources: From 3067eb302cde93bf12fb14c51479b2cf50b06f52 Mon Sep 17 00:00:00 2001 From: Mateus Oliveira Date: Tue, 9 Apr 2024 10:34:36 -0300 Subject: [PATCH 3/8] fixup! feat: Add Non Admin Restore controller Signed-off-by: Mateus Oliveira --- api/v1alpha1/nonadminrestore_types.go | 9 ++- api/v1alpha1/zz_generated.deepcopy.go | 9 ++- cmd/main.go | 1 - ...ac.oadp.openshift.io_nonadminrestores.yaml | 70 +++++++++++++++++++ .../controller/nonadminrestore_controller.go | 8 +-- .../nonadminrestore_controller_test.go | 31 ++++---- internal/controller/suite_test.go | 2 +- 7 files changed, 102 insertions(+), 28 deletions(-) diff --git a/api/v1alpha1/nonadminrestore_types.go b/api/v1alpha1/nonadminrestore_types.go index 3542775..a9935dc 100644 --- a/api/v1alpha1/nonadminrestore_types.go +++ b/api/v1alpha1/nonadminrestore_types.go @@ -34,12 +34,11 @@ type NonAdminRestoreSpec struct { // NonAdminRestoreStatus defines the observed state of NonAdminRestore type NonAdminRestoreStatus struct { - // INSERT ADDITIONAL STATUS FIELD - define observed state of cluster - // Important: Run "make" to regenerate code after modifying this file + Conditions []metav1.Condition `json:"conditions,omitempty"` } -//+kubebuilder:object:root=true -//+kubebuilder:subresource:status +// +kubebuilder:object:root=true +// +kubebuilder:subresource:status // NonAdminRestore is the Schema for the nonadminrestores API type NonAdminRestore struct { @@ -50,7 +49,7 @@ type NonAdminRestore struct { Status NonAdminRestoreStatus `json:"status,omitempty"` } -//+kubebuilder:object:root=true +// +kubebuilder:object:root=true // NonAdminRestoreList contains a list of NonAdminRestore type NonAdminRestoreList struct { diff --git a/api/v1alpha1/zz_generated.deepcopy.go b/api/v1alpha1/zz_generated.deepcopy.go index ac08481..19fd898 100644 --- a/api/v1alpha1/zz_generated.deepcopy.go +++ b/api/v1alpha1/zz_generated.deepcopy.go @@ -138,7 +138,7 @@ func (in *NonAdminRestore) DeepCopyInto(out *NonAdminRestore) { out.TypeMeta = in.TypeMeta in.ObjectMeta.DeepCopyInto(&out.ObjectMeta) out.Spec = in.Spec - out.Status = in.Status + in.Status.DeepCopyInto(&out.Status) } // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new NonAdminRestore. @@ -209,6 +209,13 @@ func (in *NonAdminRestoreSpec) DeepCopy() *NonAdminRestoreSpec { // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *NonAdminRestoreStatus) DeepCopyInto(out *NonAdminRestoreStatus) { *out = *in + if in.Conditions != nil { + in, out := &in.Conditions, &out.Conditions + *out = make([]metav1.Condition, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } } // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new NonAdminRestoreStatus. diff --git a/cmd/main.go b/cmd/main.go index bf3e9d0..8dec830 100644 --- a/cmd/main.go +++ b/cmd/main.go @@ -27,7 +27,6 @@ import ( "k8s.io/apimachinery/pkg/runtime" utilruntime "k8s.io/apimachinery/pkg/util/runtime" clientgoscheme "k8s.io/client-go/kubernetes/scheme" - // Import all Kubernetes client auth plugins (e.g. Azure, GCP, OIDC, etc.) // to ensure that exec-entrypoint and run can make use of them. _ "k8s.io/client-go/plugin/pkg/client/auth" diff --git a/config/crd/bases/nac.oadp.openshift.io_nonadminrestores.yaml b/config/crd/bases/nac.oadp.openshift.io_nonadminrestores.yaml index 5a8af11..dabd110 100644 --- a/config/crd/bases/nac.oadp.openshift.io_nonadminrestores.yaml +++ b/config/crd/bases/nac.oadp.openshift.io_nonadminrestores.yaml @@ -46,6 +46,76 @@ spec: type: object status: description: NonAdminRestoreStatus defines the observed state of NonAdminRestore + properties: + conditions: + items: + description: "Condition contains details for one aspect of the current + state of this API Resource.\n---\nThis struct is intended for + direct use as an array at the field path .status.conditions. For + example,\n\n\n\ttype FooStatus struct{\n\t // Represents the + observations of a foo's current state.\n\t // Known .status.conditions.type + are: \"Available\", \"Progressing\", and \"Degraded\"\n\t // + +patchMergeKey=type\n\t // +patchStrategy=merge\n\t // +listType=map\n\t + \ // +listMapKey=type\n\t Conditions []metav1.Condition `json:\"conditions,omitempty\" + patchStrategy:\"merge\" patchMergeKey:\"type\" protobuf:\"bytes,1,rep,name=conditions\"`\n\n\n\t + \ // other fields\n\t}" + properties: + lastTransitionTime: + description: |- + lastTransitionTime is the last time the condition transitioned from one status to another. + This should be when the underlying condition changed. If that is not known, then using the time when the API field changed is acceptable. + format: date-time + type: string + message: + description: |- + message is a human readable message indicating details about the transition. + This may be an empty string. + maxLength: 32768 + type: string + observedGeneration: + description: |- + observedGeneration represents the .metadata.generation that the condition was set based upon. + For instance, if .metadata.generation is currently 12, but the .status.conditions[x].observedGeneration is 9, the condition is out of date + with respect to the current state of the instance. + format: int64 + minimum: 0 + type: integer + reason: + description: |- + reason contains a programmatic identifier indicating the reason for the condition's last transition. + Producers of specific condition types may define expected values and meanings for this field, + and whether the values are considered a guaranteed API. + The value should be a CamelCase string. + This field may not be empty. + maxLength: 1024 + minLength: 1 + pattern: ^[A-Za-z]([A-Za-z0-9_,:]*[A-Za-z0-9_])?$ + type: string + status: + description: status of the condition, one of True, False, Unknown. + enum: + - "True" + - "False" + - Unknown + type: string + type: + description: |- + type of condition in CamelCase or in foo.example.com/CamelCase. + --- + Many .condition.type values are consistent across resources like Available, but because arbitrary conditions can be + useful (see .node.status.conditions), the ability to deconflict is important. + The regex it matches is (dns1123SubdomainFmt/)?(qualifiedNameFmt) + maxLength: 316 + pattern: ^([a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*/)?(([A-Za-z0-9][-A-Za-z0-9_.]*)?[A-Za-z0-9])$ + type: string + required: + - lastTransitionTime + - message + - reason + - status + - type + type: object + type: array type: object type: object served: true diff --git a/internal/controller/nonadminrestore_controller.go b/internal/controller/nonadminrestore_controller.go index 979faa3..964c774 100644 --- a/internal/controller/nonadminrestore_controller.go +++ b/internal/controller/nonadminrestore_controller.go @@ -33,9 +33,9 @@ type NonAdminRestoreReconciler struct { Scheme *runtime.Scheme } -//+kubebuilder:rbac:groups=nac.oadp.openshift.io,resources=nonadminrestores,verbs=get;list;watch;create;update;patch;delete -//+kubebuilder:rbac:groups=nac.oadp.openshift.io,resources=nonadminrestores/status,verbs=get;update;patch -//+kubebuilder:rbac:groups=nac.oadp.openshift.io,resources=nonadminrestores/finalizers,verbs=update +// +kubebuilder:rbac:groups=nac.oadp.openshift.io,resources=nonadminrestores,verbs=get;list;watch;create;update;patch;delete +// +kubebuilder:rbac:groups=nac.oadp.openshift.io,resources=nonadminrestores/status,verbs=get;update;patch +// +kubebuilder:rbac:groups=nac.oadp.openshift.io,resources=nonadminrestores/finalizers,verbs=update // Reconcile is part of the main kubernetes reconciliation loop which aims to // move the current state of the cluster closer to the desired state. @@ -46,7 +46,7 @@ type NonAdminRestoreReconciler struct { // // For more details, check Reconcile and its Result here: // - https://pkg.go.dev/sigs.k8s.io/controller-runtime@v0.17.0/pkg/reconcile -func (r *NonAdminRestoreReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) { +func (*NonAdminRestoreReconciler) Reconcile(ctx context.Context, _ ctrl.Request) (ctrl.Result, error) { _ = log.FromContext(ctx) // TODO(user): your logic here diff --git a/internal/controller/nonadminrestore_controller_test.go b/internal/controller/nonadminrestore_controller_test.go index 7037554..5d6f76e 100644 --- a/internal/controller/nonadminrestore_controller_test.go +++ b/internal/controller/nonadminrestore_controller_test.go @@ -19,19 +19,18 @@ package controller import ( "context" - . "github.com/onsi/ginkgo/v2" - . "github.com/onsi/gomega" + "github.com/onsi/ginkgo/v2" + "github.com/onsi/gomega" "k8s.io/apimachinery/pkg/api/errors" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/types" "sigs.k8s.io/controller-runtime/pkg/reconcile" - metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" - nacv1alpha1 "github.com/migtools/oadp-non-admin/api/v1alpha1" ) -var _ = Describe("NonAdminRestore Controller", func() { - Context("When reconciling a resource", func() { +var _ = ginkgo.Describe("NonAdminRestore Controller", func() { + ginkgo.Context("When reconciling a resource", func() { const resourceName = "test-resource" ctx := context.Background() @@ -42,8 +41,8 @@ var _ = Describe("NonAdminRestore Controller", func() { } nonadminrestore := &nacv1alpha1.NonAdminRestore{} - BeforeEach(func() { - By("creating the custom resource for the Kind NonAdminRestore") + ginkgo.BeforeEach(func() { + ginkgo.By("creating the custom resource for the Kind NonAdminRestore") err := k8sClient.Get(ctx, typeNamespacedName, nonadminrestore) if err != nil && errors.IsNotFound(err) { resource := &nacv1alpha1.NonAdminRestore{ @@ -53,21 +52,21 @@ var _ = Describe("NonAdminRestore Controller", func() { }, // TODO(user): Specify other spec details if needed. } - Expect(k8sClient.Create(ctx, resource)).To(Succeed()) + gomega.Expect(k8sClient.Create(ctx, resource)).To(gomega.Succeed()) } }) - AfterEach(func() { + ginkgo.AfterEach(func() { // TODO(user): Cleanup logic after each test, like removing the resource instance. resource := &nacv1alpha1.NonAdminRestore{} err := k8sClient.Get(ctx, typeNamespacedName, resource) - Expect(err).NotTo(HaveOccurred()) + gomega.Expect(err).NotTo(gomega.HaveOccurred()) - By("Cleanup the specific resource instance NonAdminRestore") - Expect(k8sClient.Delete(ctx, resource)).To(Succeed()) + ginkgo.By("Cleanup the specific resource instance NonAdminRestore") + gomega.Expect(k8sClient.Delete(ctx, resource)).To(gomega.Succeed()) }) - It("should successfully reconcile the resource", func() { - By("Reconciling the created resource") + ginkgo.It("should successfully reconcile the resource", func() { + ginkgo.By("Reconciling the created resource") controllerReconciler := &NonAdminRestoreReconciler{ Client: k8sClient, Scheme: k8sClient.Scheme(), @@ -76,7 +75,7 @@ var _ = Describe("NonAdminRestore Controller", func() { _, err := controllerReconciler.Reconcile(ctx, reconcile.Request{ NamespacedName: typeNamespacedName, }) - Expect(err).NotTo(HaveOccurred()) + gomega.Expect(err).NotTo(gomega.HaveOccurred()) // TODO(user): Add more specific assertions depending on your controller's reconciliation logic. // Example: If you expect a certain status condition after reconciliation, verify it here. }) diff --git a/internal/controller/suite_test.go b/internal/controller/suite_test.go index ff5f04a..c200c59 100644 --- a/internal/controller/suite_test.go +++ b/internal/controller/suite_test.go @@ -74,7 +74,7 @@ var _ = ginkgov2.BeforeSuite(func() { gomega.Expect(err).NotTo(gomega.HaveOccurred()) err = nacv1alpha1.AddToScheme(scheme.Scheme) - Expect(err).NotTo(HaveOccurred()) + gomega.Expect(err).NotTo(gomega.HaveOccurred()) // +kubebuilder:scaffold:scheme From cdb939082f8c5931fa480012cf2af526aa7df815 Mon Sep 17 00:00:00 2001 From: Mateus Oliveira Date: Tue, 9 Apr 2024 15:33:22 -0300 Subject: [PATCH 4/8] fixup! feat: Add Non Admin Restore controller Signed-off-by: Mateus Oliveira --- config/samples/nac_v1alpha1_nonadminrestore.yaml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/config/samples/nac_v1alpha1_nonadminrestore.yaml b/config/samples/nac_v1alpha1_nonadminrestore.yaml index 5a103d6..80c51d5 100644 --- a/config/samples/nac_v1alpha1_nonadminrestore.yaml +++ b/config/samples/nac_v1alpha1_nonadminrestore.yaml @@ -4,9 +4,9 @@ metadata: labels: app.kubernetes.io/name: nonadminrestore app.kubernetes.io/instance: nonadminrestore-sample - app.kubernetes.io/part-of: oadp-nac + app.kubernetes.io/part-of: oadp-operator app.kubernetes.io/managed-by: kustomize - app.kubernetes.io/created-by: oadp-nac + app.kubernetes.io/created-by: oadp-operator name: nonadminrestore-sample spec: # TODO(user): Add fields here From e0e01ed9ac80f2ad2a7b8e326d04ca6f39c30f6f Mon Sep 17 00:00:00 2001 From: Mateus Oliveira Date: Mon, 29 Apr 2024 12:52:22 -0300 Subject: [PATCH 5/8] fixup! feat: Add Non Admin Restore controller Signed-off-by: Mateus Oliveira --- Makefile | 1 + README.md | 25 +- api/v1alpha1/nonadminrestore_types.go | 21 +- api/v1alpha1/zz_generated.deepcopy.go | 7 +- cmd/main.go | 3 +- ...ac.oadp.openshift.io_nonadminrestores.yaml | 406 +++++++++++++++++- config/rbac/role.yaml | 11 + .../samples/nac_v1alpha1_nonadminrestore.yaml | 3 +- docs/non_admin_user.md | 58 ++- go.mod | 2 +- hack/samples/restores/common.yaml | 23 + .../controller/nonadminrestore_controller.go | 78 +++- .../nonadminrestore_controller_test.go | 212 +++++++-- internal/controller/suite_test.go | 2 +- 14 files changed, 759 insertions(+), 93 deletions(-) create mode 100644 hack/samples/restores/common.yaml diff --git a/Makefile b/Makefile index 443fa00..7b51ccc 100644 --- a/Makefile +++ b/Makefile @@ -48,6 +48,7 @@ help: ## Display this help. .PHONY: manifests manifests: controller-gen ## Generate WebhookConfiguration, ClusterRole and CustomResourceDefinition objects. $(CONTROLLER_GEN) rbac:roleName=non-admin-controller-role crd webhook paths="./..." output:crd:artifacts:config=config/crd/bases + sed -i 's/Velero backup/NonAdminBackup/' ./config/crd/bases/nac.oadp.openshift.io_nonadminrestores.yaml .PHONY: generate generate: controller-gen ## Generate code containing DeepCopy, DeepCopyInto, and DeepCopyObject method implementations. diff --git a/README.md b/README.md index e98f34d..5c5fe0d 100644 --- a/README.md +++ b/README.md @@ -38,6 +38,8 @@ To use NAC functionality: ``` Check the application was successful deployed by accessing its route. + + Create and update items in application UI, to later check if application was successfully restored. - create NonAdminBackup For example, use one of the sample NonAdminBackup available in `hack/samples/backups/` folder, by running @@ -47,7 +49,28 @@ To use NAC functionality: | oc create -f - ``` - - TODO NonAdminRestore + - delete sample application + + For example, delete one of the sample applications available in `hack/samples/apps/` folder, by running + ```sh + oc process -f ./hack/samples/apps/ \ + -p NAMESPACE= \ + | oc delete -f - + ``` + + Check that application was successful deleted by accessing its route. + - create NonAdminRestore + + For example, use one of the sample NonAdminRestore available in `hack/samples/restores/` folder, by running + ```sh + oc process -f ./hack/samples/restores/ \ + -p NAMESPACE= \ + -p NAME= \ + | oc create -f - + ``` + + + After NonAdminRestore completes, check if the application was successful restored by accessing its route and seeing its items in application UI. ## Contributing diff --git a/api/v1alpha1/nonadminrestore_types.go b/api/v1alpha1/nonadminrestore_types.go index a9935dc..73662b0 100644 --- a/api/v1alpha1/nonadminrestore_types.go +++ b/api/v1alpha1/nonadminrestore_types.go @@ -17,23 +17,30 @@ limitations under the License. package v1alpha1 import ( + velerov1api "github.com/vmware-tanzu/velero/pkg/apis/velero/v1" 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. - // NonAdminRestoreSpec defines the desired state of NonAdminRestore type NonAdminRestoreSpec struct { - // INSERT ADDITIONAL SPEC FIELDS - desired state of cluster - // Important: Run "make" to regenerate code after modifying this file + // Specification for a Velero restore. + // +kubebuilder:validation:Required + RestoreSpec *velerov1api.RestoreSpec `json:"restoreSpec,omitempty"` + // TODO add test that NAR can not be created without restoreSpec or restoreSpec.backupName + // TODO need to investigate restoreSpec.namespaceMapping, depends on how NAC tracks the namespace access per user - // Foo is an example field of NonAdminRestore. Edit nonadminrestore_types.go to remove/update - Foo string `json:"foo,omitempty"` + // TODO NonAdminRestore log level, by default TODO. + // +optional + // +kubebuilder:validation:Enum=trace;debug;info;warning;error;fatal;panic + LogLevel string `json:"logLevel,omitempty"` + // TODO ALSO ADD TEST FOR DIFFERENT LOG LEVELS } // NonAdminRestoreStatus defines the observed state of NonAdminRestore type NonAdminRestoreStatus struct { + // TODO https://github.com/migtools/oadp-non-admin/pull/23 + // TODO https://github.com/migtools/oadp-non-admin/pull/13 + Conditions []metav1.Condition `json:"conditions,omitempty"` } diff --git a/api/v1alpha1/zz_generated.deepcopy.go b/api/v1alpha1/zz_generated.deepcopy.go index 19fd898..edaec21 100644 --- a/api/v1alpha1/zz_generated.deepcopy.go +++ b/api/v1alpha1/zz_generated.deepcopy.go @@ -137,7 +137,7 @@ func (in *NonAdminRestore) DeepCopyInto(out *NonAdminRestore) { *out = *in out.TypeMeta = in.TypeMeta in.ObjectMeta.DeepCopyInto(&out.ObjectMeta) - out.Spec = in.Spec + in.Spec.DeepCopyInto(&out.Spec) in.Status.DeepCopyInto(&out.Status) } @@ -194,6 +194,11 @@ func (in *NonAdminRestoreList) DeepCopyObject() runtime.Object { // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *NonAdminRestoreSpec) DeepCopyInto(out *NonAdminRestoreSpec) { *out = *in + if in.RestoreSpec != nil { + in, out := &in.RestoreSpec, &out.RestoreSpec + *out = new(v1.RestoreSpec) + (*in).DeepCopyInto(*out) + } } // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new NonAdminRestoreSpec. diff --git a/cmd/main.go b/cmd/main.go index 8dec830..d047639 100644 --- a/cmd/main.go +++ b/cmd/main.go @@ -98,7 +98,8 @@ func main() { TLSOpts: tlsOpts, }) - if len(constant.OadpNamespace) == 0 { + // TODO create get function in common :question: + if len(os.Getenv(constant.NamespaceEnvVar)) == 0 { setupLog.Error(fmt.Errorf("%v environment variable is empty", constant.NamespaceEnvVar), "environment variable must be set") os.Exit(1) } diff --git a/config/crd/bases/nac.oadp.openshift.io_nonadminrestores.yaml b/config/crd/bases/nac.oadp.openshift.io_nonadminrestores.yaml index dabd110..434252b 100644 --- a/config/crd/bases/nac.oadp.openshift.io_nonadminrestores.yaml +++ b/config/crd/bases/nac.oadp.openshift.io_nonadminrestores.yaml @@ -39,10 +39,410 @@ spec: spec: description: NonAdminRestoreSpec defines the desired state of NonAdminRestore properties: - foo: - description: Foo is an example field of NonAdminRestore. Edit nonadminrestore_types.go - to remove/update + logLevel: + description: TODO NonAdminRestore log level, by default TODO. + enum: + - trace + - debug + - info + - warning + - error + - fatal + - panic type: string + restoreSpec: + description: Specification for a Velero restore. + properties: + backupName: + description: |- + BackupName is the unique name of the NonAdminBackup to restore + from. + type: string + excludedNamespaces: + description: |- + ExcludedNamespaces contains a list of namespaces that are not + included in the restore. + items: + type: string + nullable: true + type: array + excludedResources: + description: |- + ExcludedResources is a slice of resource names that are not + included in the restore. + items: + type: string + nullable: true + type: array + existingResourcePolicy: + description: ExistingResourcePolicy specifies the restore behavior + for the Kubernetes resource to be restored + nullable: true + type: string + hooks: + description: Hooks represent custom behaviors that should be executed + during or post restore. + properties: + resources: + items: + description: |- + RestoreResourceHookSpec defines one or more RestoreResrouceHooks that should be executed based on + the rules defined for namespaces, resources, and label selector. + properties: + excludedNamespaces: + description: ExcludedNamespaces specifies the namespaces + to which this hook spec does not apply. + items: + type: string + nullable: true + type: array + excludedResources: + description: ExcludedResources specifies the resources + to which this hook spec does not apply. + items: + type: string + nullable: true + type: array + includedNamespaces: + description: |- + IncludedNamespaces specifies the namespaces to which this hook spec applies. If empty, it applies + to all namespaces. + items: + type: string + nullable: true + type: array + includedResources: + description: |- + IncludedResources specifies the resources to which this hook spec applies. If empty, it applies + to all resources. + items: + type: string + nullable: true + type: array + labelSelector: + description: LabelSelector, if specified, filters the + resources to which this hook spec applies. + nullable: true + properties: + matchExpressions: + description: matchExpressions is a list of label + selector requirements. The requirements are ANDed. + items: + description: |- + A label selector requirement is a selector that contains values, a key, and an operator that + relates the key and values. + properties: + key: + description: key is the label key that the + selector applies to. + type: string + operator: + description: |- + operator represents a key's relationship to a set of values. + Valid operators are In, NotIn, Exists and DoesNotExist. + type: string + values: + description: |- + values is an array of string values. If the operator is In or NotIn, + the values array must be non-empty. If the operator is Exists or DoesNotExist, + the values array must be empty. This array is replaced during a strategic + merge patch. + items: + type: string + type: array + required: + - key + - operator + type: object + type: array + matchLabels: + additionalProperties: + type: string + description: |- + matchLabels is a map of {key,value} pairs. A single {key,value} in the matchLabels + map is equivalent to an element of matchExpressions, whose key field is "key", the + operator is "In", and the values array contains only "value". The requirements are ANDed. + type: object + type: object + x-kubernetes-map-type: atomic + name: + description: Name is the name of this hook. + type: string + postHooks: + description: PostHooks is a list of RestoreResourceHooks + to execute during and after restoring a resource. + items: + description: RestoreResourceHook defines a restore + hook for a resource. + properties: + exec: + description: Exec defines an exec restore hook. + properties: + command: + description: Command is the command and arguments + to execute from within a container after + a pod has been restored. + items: + type: string + minItems: 1 + type: array + container: + description: |- + Container is the container in the pod where the command should be executed. If not specified, + the pod's first container is used. + type: string + execTimeout: + description: |- + ExecTimeout defines the maximum amount of time Velero should wait for the hook to complete before + considering the execution a failure. + type: string + onError: + description: OnError specifies how Velero + should behave if it encounters an error + executing this hook. + enum: + - Continue + - Fail + type: string + waitTimeout: + description: |- + WaitTimeout defines the maximum amount of time Velero should wait for the container to be Ready + before attempting to run the command. + type: string + required: + - command + type: object + init: + description: Init defines an init restore hook. + properties: + initContainers: + description: InitContainers is list of init + containers to be added to a pod during its + restore. + items: + type: object + x-kubernetes-preserve-unknown-fields: true + type: array + x-kubernetes-preserve-unknown-fields: true + timeout: + description: Timeout defines the maximum amount + of time Velero should wait for the initContainers + to complete. + type: string + type: object + type: object + type: array + required: + - name + type: object + type: array + type: object + includeClusterResources: + description: |- + IncludeClusterResources specifies whether cluster-scoped resources + should be included for consideration in the restore. If null, defaults + to true. + nullable: true + type: boolean + includedNamespaces: + description: |- + IncludedNamespaces is a slice of namespace names to include objects + from. If empty, all namespaces are included. + items: + type: string + nullable: true + type: array + includedResources: + description: |- + IncludedResources is a slice of resource names to include + in the restore. If empty, all resources in the backup are included. + items: + type: string + nullable: true + type: array + itemOperationTimeout: + description: |- + ItemOperationTimeout specifies the time used to wait for RestoreItemAction operations + The default value is 1 hour. + type: string + labelSelector: + description: |- + LabelSelector is a metav1.LabelSelector to filter with + when restoring individual objects from the backup. If empty + or nil, all objects are included. Optional. + nullable: true + properties: + matchExpressions: + description: matchExpressions is a list of label selector + requirements. The requirements are ANDed. + items: + description: |- + A label selector requirement is a selector that contains values, a key, and an operator that + relates the key and values. + properties: + key: + description: key is the label key that the selector + applies to. + type: string + operator: + description: |- + operator represents a key's relationship to a set of values. + Valid operators are In, NotIn, Exists and DoesNotExist. + type: string + values: + description: |- + values is an array of string values. If the operator is In or NotIn, + the values array must be non-empty. If the operator is Exists or DoesNotExist, + the values array must be empty. This array is replaced during a strategic + merge patch. + items: + type: string + type: array + required: + - key + - operator + type: object + type: array + matchLabels: + additionalProperties: + type: string + description: |- + matchLabels is a map of {key,value} pairs. A single {key,value} in the matchLabels + map is equivalent to an element of matchExpressions, whose key field is "key", the + operator is "In", and the values array contains only "value". The requirements are ANDed. + type: object + type: object + x-kubernetes-map-type: atomic + namespaceMapping: + additionalProperties: + type: string + description: |- + NamespaceMapping is a map of source namespace names + to target namespace names to restore into. Any source + namespaces not included in the map will be restored into + namespaces of the same name. + type: object + orLabelSelectors: + description: |- + OrLabelSelectors is list of metav1.LabelSelector to filter with + when restoring individual objects from the backup. If multiple provided + they will be joined by the OR operator. LabelSelector as well as + OrLabelSelectors cannot co-exist in restore request, only one of them + can be used + items: + description: |- + A label selector is a label query over a set of resources. The result of matchLabels and + matchExpressions are ANDed. An empty label selector matches all objects. A null + label selector matches no objects. + properties: + matchExpressions: + description: matchExpressions is a list of label selector + requirements. The requirements are ANDed. + items: + description: |- + A label selector requirement is a selector that contains values, a key, and an operator that + relates the key and values. + properties: + key: + description: key is the label key that the selector + applies to. + type: string + operator: + description: |- + operator represents a key's relationship to a set of values. + Valid operators are In, NotIn, Exists and DoesNotExist. + type: string + values: + description: |- + values is an array of string values. If the operator is In or NotIn, + the values array must be non-empty. If the operator is Exists or DoesNotExist, + the values array must be empty. This array is replaced during a strategic + merge patch. + items: + type: string + type: array + required: + - key + - operator + type: object + type: array + matchLabels: + additionalProperties: + type: string + description: |- + matchLabels is a map of {key,value} pairs. A single {key,value} in the matchLabels + map is equivalent to an element of matchExpressions, whose key field is "key", the + operator is "In", and the values array contains only "value". The requirements are ANDed. + type: object + type: object + x-kubernetes-map-type: atomic + nullable: true + type: array + preserveNodePorts: + description: PreserveNodePorts specifies whether to restore old + nodePorts from backup. + nullable: true + type: boolean + resourceModifier: + description: ResourceModifier specifies the reference to JSON + resource patches that should be applied to resources before + restoration. + nullable: true + properties: + apiGroup: + description: |- + APIGroup is the group for the resource being referenced. + If APIGroup is not specified, the specified Kind must be in the core API group. + For any other third-party types, APIGroup is required. + type: string + kind: + description: Kind is the type of resource being referenced + type: string + name: + description: Name is the name of resource being referenced + type: string + required: + - kind + - name + type: object + x-kubernetes-map-type: atomic + restorePVs: + description: |- + RestorePVs specifies whether to restore all included + PVs from snapshot + nullable: true + type: boolean + restoreStatus: + description: |- + RestoreStatus specifies which resources we should restore the status + field. If nil, no objects are included. Optional. + nullable: true + properties: + excludedResources: + description: ExcludedResources specifies the resources to + which will not restore the status. + items: + type: string + nullable: true + type: array + includedResources: + description: |- + IncludedResources specifies the resources to which will restore the status. + If empty, it applies to all resources. + items: + type: string + nullable: true + type: array + type: object + scheduleName: + description: |- + ScheduleName is the unique name of the Velero schedule to restore + from. If specified, and BackupName is empty, Velero will restore + from the most recent successful backup created from this schedule. + type: string + required: + - backupName + type: object type: object status: description: NonAdminRestoreStatus defines the observed state of NonAdminRestore diff --git a/config/rbac/role.yaml b/config/rbac/role.yaml index f013316..58ffd37 100644 --- a/config/rbac/role.yaml +++ b/config/rbac/role.yaml @@ -67,3 +67,14 @@ rules: - patch - update - watch +- apiGroups: + - velero.io + resources: + - restores + verbs: + - create + - get + - list + - patch + - update + - watch diff --git a/config/samples/nac_v1alpha1_nonadminrestore.yaml b/config/samples/nac_v1alpha1_nonadminrestore.yaml index 80c51d5..c8e2e2f 100644 --- a/config/samples/nac_v1alpha1_nonadminrestore.yaml +++ b/config/samples/nac_v1alpha1_nonadminrestore.yaml @@ -9,4 +9,5 @@ metadata: app.kubernetes.io/created-by: oadp-operator name: nonadminrestore-sample spec: - # TODO(user): Add fields here + restoreSpec: + backupName: nonadminbackup-sample diff --git a/docs/non_admin_user.md b/docs/non_admin_user.md index 4902541..182b5c0 100644 --- a/docs/non_admin_user.md +++ b/docs/non_admin_user.md @@ -40,28 +40,48 @@ Choose one of the authentication method sections to follow. ``` - Ensure non admin user have appropriate permissions in its namespace, i.e., non admin user have editor roles for the following objects - `nonadminbackups.nac.oadp.openshift.io` + - `nonadminrestores.nac.oadp.openshift.io` For example ```yaml - # config/rbac/nonadminbackup_editor_role.yaml - - apiGroups: - - nac.oadp.openshift.io - resources: - - nonadminbackups - verbs: - - create - - delete - - get - - list - - patch - - update - - watch - - apiGroups: - - nac.oadp.openshift.io - resources: - - nonadminbackups/status - verbs: - - get + # config/rbac/nonadminbackup_editor_role.yaml + - apiGroups: + - nac.oadp.openshift.io + resources: + - nonadminbackups + verbs: + - create + - delete + - get + - list + - patch + - update + - watch + - apiGroups: + - nac.oadp.openshift.io + resources: + - nonadminbackups/status + verbs: + - get + # config/rbac/nonadminrestore_editor_role.yaml + - apiGroups: + - nac.oadp.openshift.io + resources: + - nonadminrestores + verbs: + - create + - delete + - get + - list + - patch + - update + - watch + - apiGroups: + - nac.oadp.openshift.io + resources: + - nonadminrestores/status + verbs: + - get ``` For example, make non admin user have `admin` ClusterRole permissions on its namespace ```sh diff --git a/go.mod b/go.mod index 00d9950..7eb4612 100644 --- a/go.mod +++ b/go.mod @@ -8,6 +8,7 @@ require ( github.com/onsi/gomega v1.30.0 github.com/stretchr/testify v1.8.4 github.com/vmware-tanzu/velero v1.12.0 + k8s.io/api v0.29.0 k8s.io/apimachinery v0.29.0 k8s.io/client-go v0.29.0 sigs.k8s.io/controller-runtime v0.17.0 @@ -65,7 +66,6 @@ require ( gopkg.in/inf.v0 v0.9.1 // indirect gopkg.in/yaml.v2 v2.4.0 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect - k8s.io/api v0.29.0 // indirect k8s.io/apiextensions-apiserver v0.29.0 // indirect k8s.io/component-base v0.29.0 // indirect k8s.io/klog/v2 v2.110.1 // indirect diff --git a/hack/samples/restores/common.yaml b/hack/samples/restores/common.yaml new file mode 100644 index 0000000..7b47631 --- /dev/null +++ b/hack/samples/restores/common.yaml @@ -0,0 +1,23 @@ +apiVersion: template.openshift.io/v1 +kind: Template +metadata: + name: sample-nonadminrestore +objects: + - apiVersion: nac.oadp.openshift.io/v1alpha1 + kind: NonAdminRestore + metadata: + name: nonadminrestore-sample-${SUFFIX} + namespace: ${NAMESPACE} + spec: + restoreSpec: + backupName: ${NAME} +parameters: + - description: NonAdminRestore suffix + from: '[a-z0-9]{8}' + generate: expression + name: SUFFIX + - description: NonAdminRestore namespace + name: NAMESPACE + value: mysql-persistent + - description: NonAdminBackup name + name: NAME diff --git a/internal/controller/nonadminrestore_controller.go b/internal/controller/nonadminrestore_controller.go index 964c774..cd21d52 100644 --- a/internal/controller/nonadminrestore_controller.go +++ b/internal/controller/nonadminrestore_controller.go @@ -18,43 +18,95 @@ package controller import ( "context" + "fmt" + "os" + "github.com/go-logr/logr" + velerov1api "github.com/vmware-tanzu/velero/pkg/apis/velero/v1" + "k8s.io/apimachinery/pkg/api/errors" "k8s.io/apimachinery/pkg/runtime" + "k8s.io/apimachinery/pkg/types" ctrl "sigs.k8s.io/controller-runtime" "sigs.k8s.io/controller-runtime/pkg/client" "sigs.k8s.io/controller-runtime/pkg/log" nacv1alpha1 "github.com/migtools/oadp-non-admin/api/v1alpha1" + "github.com/migtools/oadp-non-admin/internal/common/constant" ) // NonAdminRestoreReconciler reconciles a NonAdminRestore object type NonAdminRestoreReconciler struct { client.Client Scheme *runtime.Scheme + Logger logr.Logger } // +kubebuilder:rbac:groups=nac.oadp.openshift.io,resources=nonadminrestores,verbs=get;list;watch;create;update;patch;delete // +kubebuilder:rbac:groups=nac.oadp.openshift.io,resources=nonadminrestores/status,verbs=get;update;patch // +kubebuilder:rbac:groups=nac.oadp.openshift.io,resources=nonadminrestores/finalizers,verbs=update +// +kubebuilder:rbac:groups=velero.io,resources=restores,verbs=get;list;watch;create;update;patch + // Reconcile is part of the main kubernetes reconciliation loop which aims to -// move the current state of the cluster closer to the desired state. -// TODO(user): Modify the Reconcile function to compare the state specified by -// the NonAdminRestore object against the actual cluster state, and then -// perform operations to make the cluster state reflect the state specified by -// the user. -// -// For more details, check Reconcile and its Result here: -// - https://pkg.go.dev/sigs.k8s.io/controller-runtime@v0.17.0/pkg/reconcile -func (*NonAdminRestoreReconciler) Reconcile(ctx context.Context, _ ctrl.Request) (ctrl.Result, error) { - _ = log.FromContext(ctx) - - // TODO(user): your logic here +// move the current state of the NonAdminRestore to the desired state. +func (r *NonAdminRestoreReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) { + r.Logger = log.FromContext(ctx) + logger := r.Logger.WithValues("NonAdminRestore", req.NamespacedName) + + logger.Info("TODO") + + nonAdminRestore := nacv1alpha1.NonAdminRestore{} + err := r.Get(ctx, req.NamespacedName, &nonAdminRestore) + if err != nil { + return ctrl.Result{}, err + } + + err = r.validateSpec(ctx, req, nonAdminRestore.Spec) + if err != nil { + return ctrl.Result{}, err + } + + // TODO try to create Velero Restore return ctrl.Result{}, nil } -// SetupWithManager sets up the controller with the Manager. +// TODO remove functions params +func (r *NonAdminRestoreReconciler) validateSpec(ctx context.Context, req ctrl.Request, objectSpec nacv1alpha1.NonAdminRestoreSpec) error { + if len(objectSpec.RestoreSpec.ScheduleName) > 0 { + return fmt.Errorf("spec.restoreSpec.scheduleName field is not allowed in NonAdminRestore") + } + + // TODO nonAdminBackup respect restricted fields + + nonAdminBackupName := objectSpec.RestoreSpec.BackupName + nonAdminBackup := &nacv1alpha1.NonAdminBackup{} + err := r.Get(ctx, types.NamespacedName{Namespace: req.Namespace, Name: nonAdminBackupName}, nonAdminBackup) + if err != nil { + if errors.IsNotFound(err) { + // TODO add this error message to NonAdminRestore status + return fmt.Errorf("invalid spec.restoreSpec.backupName: NonAdminBackup '%s' does not exist in namespace %s", nonAdminBackupName, req.Namespace) + } + return err + } + // TODO nonAdminBackup has necessary labels (NAB controller job :question:) + // TODO nonAdminBackup is in complete state :question:!!!! + + // TODO create get function in common :question: + oadpNamespace := os.Getenv(constant.NamespaceEnvVar) + + veleroBackupName := nonAdminBackup.Labels["naoSei"] + veleroBackup := &velerov1api.Backup{} + err = r.Get(ctx, types.NamespacedName{Namespace: oadpNamespace, Name: veleroBackupName}, veleroBackup) + if err != nil { + // TODO test error messages, THEY MUST BE INFORMATIVE + return err + } + + return nil +} + +// SetupWithManager sets up the NonAdminRestore controller with the Manager. func (r *NonAdminRestoreReconciler) SetupWithManager(mgr ctrl.Manager) error { return ctrl.NewControllerManagedBy(mgr). For(&nacv1alpha1.NonAdminRestore{}). diff --git a/internal/controller/nonadminrestore_controller_test.go b/internal/controller/nonadminrestore_controller_test.go index 5d6f76e..fb0b087 100644 --- a/internal/controller/nonadminrestore_controller_test.go +++ b/internal/controller/nonadminrestore_controller_test.go @@ -18,66 +18,188 @@ package controller import ( "context" + "os" "github.com/onsi/ginkgo/v2" "github.com/onsi/gomega" - "k8s.io/apimachinery/pkg/api/errors" + "github.com/vmware-tanzu/velero/pkg/apis/velero/v1" + corev1 "k8s.io/api/core/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/types" "sigs.k8s.io/controller-runtime/pkg/reconcile" nacv1alpha1 "github.com/migtools/oadp-non-admin/api/v1alpha1" + "github.com/migtools/oadp-non-admin/internal/common/constant" ) -var _ = ginkgo.Describe("NonAdminRestore Controller", func() { - ginkgo.Context("When reconciling a resource", func() { - const resourceName = "test-resource" +type clusterScenario struct { + namespace string + nonAdminRestore string +} - ctx := context.Background() +type nonAdminRestoreReconcileScenario struct { + restoreSpec *v1.RestoreSpec + namespace string + nonAdminRestore string + errMessage string +} - typeNamespacedName := types.NamespacedName{ - Name: resourceName, - Namespace: "default", // TODO(user):Modify as needed +func createTestNonAdminRestore(name string, namespace string, restoreSpec v1.RestoreSpec) *nacv1alpha1.NonAdminRestore { + return &nacv1alpha1.NonAdminRestore{ + ObjectMeta: metav1.ObjectMeta{ + Name: name, + Namespace: namespace, + }, + Spec: nacv1alpha1.NonAdminRestoreSpec{ + RestoreSpec: &restoreSpec, + }, + } +} + +// TODO this does not work with envtest :question: +var _ = ginkgo.Describe("Test NonAdminRestore in cluster validation", func() { + var ( + ctx = context.Background() + currentTestScenario clusterScenario + updateTestScenario = func(scenario clusterScenario) { + currentTestScenario = scenario + } + ) + + ginkgo.AfterEach(func() { + nonAdminRestore := &nacv1alpha1.NonAdminRestore{} + if k8sClient.Get( + ctx, + types.NamespacedName{ + Name: currentTestScenario.nonAdminRestore, + Namespace: currentTestScenario.namespace, + }, + nonAdminRestore, + ) == nil { + gomega.Expect(k8sClient.Delete(ctx, nonAdminRestore)).To(gomega.Succeed()) } - nonadminrestore := &nacv1alpha1.NonAdminRestore{} - - ginkgo.BeforeEach(func() { - ginkgo.By("creating the custom resource for the Kind NonAdminRestore") - err := k8sClient.Get(ctx, typeNamespacedName, nonadminrestore) - if err != nil && errors.IsNotFound(err) { - resource := &nacv1alpha1.NonAdminRestore{ - ObjectMeta: metav1.ObjectMeta{ - Name: resourceName, - Namespace: "default", - }, - // TODO(user): Specify other spec details if needed. - } - gomega.Expect(k8sClient.Create(ctx, resource)).To(gomega.Succeed()) + + namespace := &corev1.Namespace{ + ObjectMeta: metav1.ObjectMeta{ + Name: currentTestScenario.namespace, + }, + } + gomega.Expect(k8sClient.Delete(ctx, namespace)).To(gomega.Succeed()) + }) + + ginkgo.DescribeTable("Validation is false", + func(scenario clusterScenario) { + updateTestScenario(scenario) + + namespace := &corev1.Namespace{ + ObjectMeta: metav1.ObjectMeta{ + Name: scenario.namespace, + }, } - }) - - ginkgo.AfterEach(func() { - // TODO(user): Cleanup logic after each test, like removing the resource instance. - resource := &nacv1alpha1.NonAdminRestore{} - err := k8sClient.Get(ctx, typeNamespacedName, resource) - gomega.Expect(err).NotTo(gomega.HaveOccurred()) - - ginkgo.By("Cleanup the specific resource instance NonAdminRestore") - gomega.Expect(k8sClient.Delete(ctx, resource)).To(gomega.Succeed()) - }) - ginkgo.It("should successfully reconcile the resource", func() { - ginkgo.By("Reconciling the created resource") - controllerReconciler := &NonAdminRestoreReconciler{ - Client: k8sClient, - Scheme: k8sClient.Scheme(), + gomega.Expect(k8sClient.Create(ctx, namespace)).To(gomega.Succeed()) + + nonAdminRestore := &nacv1alpha1.NonAdminRestore{ + ObjectMeta: metav1.ObjectMeta{ + Name: scenario.nonAdminRestore, + Namespace: scenario.namespace, + }, + // Spec: nacv1alpha1.NonAdminRestoreSpec{}, } + gomega.Expect(k8sClient.Create(ctx, nonAdminRestore)).To(gomega.Not(gomega.Succeed())) + }, + ginkgo.Entry("Should NOT create NonAdminRestore without spec.restoreSpec", clusterScenario{ + namespace: "test-nonadminrestore-cluster-1", + nonAdminRestore: "test-nonadminrestore-cluster-1-cr", + }), + // TODO Should NOT create NonAdminRestore without spec.restoreSpec.backupName + ) +}) + +var _ = ginkgo.Describe("Test NonAdminRestore Reconcile function", func() { + var ( + ctx = context.Background() + currentTestScenario nonAdminRestoreReconcileScenario + updateTestScenario = func(scenario nonAdminRestoreReconcileScenario) { + currentTestScenario = scenario + } + ) - _, err := controllerReconciler.Reconcile(ctx, reconcile.Request{ - NamespacedName: typeNamespacedName, - }) - gomega.Expect(err).NotTo(gomega.HaveOccurred()) - // TODO(user): Add more specific assertions depending on your controller's reconciliation logic. - // Example: If you expect a certain status condition after reconciliation, verify it here. - }) + ginkgo.AfterEach(func() { + gomega.Expect(os.Unsetenv(constant.NamespaceEnvVar)).To(gomega.Succeed()) + + nonAdminRestore := &nacv1alpha1.NonAdminRestore{} + if k8sClient.Get( + ctx, + types.NamespacedName{ + Name: currentTestScenario.nonAdminRestore, + Namespace: currentTestScenario.namespace, + }, + nonAdminRestore, + ) == nil { + gomega.Expect(k8sClient.Delete(ctx, nonAdminRestore)).To(gomega.Succeed()) + } + + namespace := &corev1.Namespace{ + ObjectMeta: metav1.ObjectMeta{ + Name: currentTestScenario.namespace, + }, + } + gomega.Expect(k8sClient.Delete(ctx, namespace)).To(gomega.Succeed()) }) + + ginkgo.DescribeTable("Reconcile is false", + func(scenario nonAdminRestoreReconcileScenario) { + updateTestScenario(scenario) + + namespace := &corev1.Namespace{ + ObjectMeta: metav1.ObjectMeta{ + Name: scenario.namespace, + }, + } + gomega.Expect(k8sClient.Create(ctx, namespace)).To(gomega.Succeed()) + + nonAdminRestore := createTestNonAdminRestore(scenario.nonAdminRestore, scenario.namespace, *scenario.restoreSpec) + gomega.Expect(k8sClient.Create(ctx, nonAdminRestore)).To(gomega.Succeed()) + + gomega.Expect(os.Setenv(constant.NamespaceEnvVar, "envVarValue")).To(gomega.Succeed()) + r := &NonAdminRestoreReconciler{ + Client: k8sClient, + Scheme: testEnv.Scheme, + } + result, err := r.Reconcile( + context.Background(), + reconcile.Request{NamespacedName: types.NamespacedName{ + Namespace: scenario.namespace, + Name: scenario.nonAdminRestore, + }}, + ) + + if len(scenario.errMessage) == 0 { + gomega.Expect(result).To(gomega.Equal(reconcile.Result{Requeue: false, RequeueAfter: 0})) + gomega.Expect(err).To(gomega.Not(gomega.HaveOccurred())) + } else { + gomega.Expect(result).To(gomega.Equal(reconcile.Result{Requeue: false, RequeueAfter: 0})) + gomega.Expect(err).To(gomega.HaveOccurred()) + gomega.Expect(err.Error()).To(gomega.ContainSubstring(scenario.errMessage)) + } + }, + ginkgo.Entry("Should NOT accept scheduleName", nonAdminRestoreReconcileScenario{ + namespace: "test-nonadminrestore-reconcile-1", + nonAdminRestore: "test-nonadminrestore-reconcile-1-cr", + errMessage: "scheduleName", + restoreSpec: &v1.RestoreSpec{ + ScheduleName: "wrong", + }, + }), + ginkgo.Entry("Should NOT accept non existing NonAdminBackup", nonAdminRestoreReconcileScenario{ + namespace: "test-nonadminrestore-reconcile-2", + nonAdminRestore: "test-nonadminrestore-reconcile-2-cr", + errMessage: "backupName", + restoreSpec: &v1.RestoreSpec{ + BackupName: "do-not-exist", + }, + }), + // TODO Should NOT accept NonAdminBackup that is not in complete state :question: + // TODO Should NOT accept non existing related Velero Backup + ) }) diff --git a/internal/controller/suite_test.go b/internal/controller/suite_test.go index c200c59..54bfa6b 100644 --- a/internal/controller/suite_test.go +++ b/internal/controller/suite_test.go @@ -61,7 +61,7 @@ var _ = ginkgov2.BeforeSuite(func() { // Note that you must have the required binaries setup under the bin directory to perform // the tests directly. When we run make test it will be setup and used automatically. BinaryAssetsDirectory: filepath.Join("..", "..", "bin", "k8s", - fmt.Sprintf("1.29.0-%s-%s", runtime.GOOS, runtime.GOARCH)), + fmt.Sprintf("1.28.3-%s-%s", runtime.GOOS, runtime.GOARCH)), } var err error From 49de8b54b3b380ae70ae6be2c10e6f953de23b26 Mon Sep 17 00:00:00 2001 From: Mateus Oliveira Date: Tue, 30 Apr 2024 12:19:51 -0300 Subject: [PATCH 6/8] fixup! feat: Add Non Admin Restore controller Signed-off-by: Mateus Oliveira --- api/v1alpha1/nonadminbackup_types.go | 20 ++-- api/v1alpha1/nonadminrestore_types.go | 10 +- api/v1alpha1/zz_generated.deepcopy.go | 5 + ...nac.oadp.openshift.io_nonadminbackups.yaml | 4 +- ...ac.oadp.openshift.io_nonadminrestores.yaml | 97 +++++++++++++++++++ internal/common/constant/constant.go | 15 ++- internal/common/function/function.go | 11 ++- .../common/types/types.go | 15 +-- .../controller/nonadminbackup_controller.go | 14 +-- .../controller/nonadminrestore_controller.go | 18 ++-- .../nonadminrestore_controller_test.go | 1 - .../predicate/nonadminbackup_predicate.go | 4 +- 12 files changed, 166 insertions(+), 48 deletions(-) rename api/v1alpha1/nonadmincontroller_types.go => internal/common/types/types.go (56%) diff --git a/api/v1alpha1/nonadminbackup_types.go b/api/v1alpha1/nonadminbackup_types.go index ea4657a..73b6966 100644 --- a/api/v1alpha1/nonadminbackup_types.go +++ b/api/v1alpha1/nonadminbackup_types.go @@ -21,17 +21,17 @@ import ( metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" ) -// NonAdminBackupPhase is a simple one high-level summary of the lifecycle of an NonAdminBackup. +// NonAdminPhase is a simple one high-level summary of the lifecycle of a non admin object. // +kubebuilder:validation:Enum=New;BackingOff;Created -type NonAdminBackupPhase string +type NonAdminPhase string const ( - // NonAdminBackupPhaseNew - NonAdminBackup resource was accepted by the OpenShift cluster, but it has not yet been processed by the NonAdminController - NonAdminBackupPhaseNew NonAdminBackupPhase = "New" - // NonAdminBackupPhaseBackingOff - Velero Backup object was not created due to NonAdminBackup error (configuration or similar) - NonAdminBackupPhaseBackingOff NonAdminBackupPhase = "BackingOff" - // NonAdminBackupPhaseCreated - Velero Backup was created. The Phase will not have additional informations about the Backup. - NonAdminBackupPhaseCreated NonAdminBackupPhase = "Created" + // NonAdminPhaseNew - non admin resource was accepted by the OpenShift cluster, but it has not yet been processed by the NonAdminController + NonAdminPhaseNew NonAdminPhase = "New" + // NonAdminPhaseBackingOff - Velero object was not created due to error in non admin object (configuration or similar) + NonAdminPhaseBackingOff NonAdminPhase = "BackingOff" + // NonAdminPhaseCreated - Velero object was created. The Phase will not have additional information about the Velero object. + NonAdminPhaseCreated NonAdminPhase = "Created" ) // NonAdminBackupSpec defines the desired state of NonAdminBackup @@ -60,8 +60,8 @@ type NonAdminBackupStatus struct { // +optional VeleroBackupStatus *velerov1api.BackupStatus `json:"veleroBackupStatus,omitempty"` - Phase NonAdminBackupPhase `json:"phase,omitempty"` - Conditions []metav1.Condition `json:"conditions,omitempty"` + Phase NonAdminPhase `json:"phase,omitempty"` + Conditions []metav1.Condition `json:"conditions,omitempty"` } // +kubebuilder:object:root=true diff --git a/api/v1alpha1/nonadminrestore_types.go b/api/v1alpha1/nonadminrestore_types.go index 73662b0..512ad2a 100644 --- a/api/v1alpha1/nonadminrestore_types.go +++ b/api/v1alpha1/nonadminrestore_types.go @@ -38,9 +38,15 @@ type NonAdminRestoreSpec struct { // NonAdminRestoreStatus defines the observed state of NonAdminRestore type NonAdminRestoreStatus struct { - // TODO https://github.com/migtools/oadp-non-admin/pull/23 - // TODO https://github.com/migtools/oadp-non-admin/pull/13 + // Related Velero Restore name. + // +optional + VeleroRestoreName string `json:"veleroRestoreName,omitempty"` + + // Related Velero Restore status. + // +optional + VeleroRestoreStatus *velerov1api.RestoreStatus `json:"veleroRestoreStatus,omitempty"` + Phase NonAdminPhase `json:"phase,omitempty"` Conditions []metav1.Condition `json:"conditions,omitempty"` } diff --git a/api/v1alpha1/zz_generated.deepcopy.go b/api/v1alpha1/zz_generated.deepcopy.go index edaec21..0d2f898 100644 --- a/api/v1alpha1/zz_generated.deepcopy.go +++ b/api/v1alpha1/zz_generated.deepcopy.go @@ -214,6 +214,11 @@ func (in *NonAdminRestoreSpec) DeepCopy() *NonAdminRestoreSpec { // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *NonAdminRestoreStatus) DeepCopyInto(out *NonAdminRestoreStatus) { *out = *in + if in.VeleroRestoreStatus != nil { + in, out := &in.VeleroRestoreStatus, &out.VeleroRestoreStatus + *out = new(v1.RestoreStatus) + (*in).DeepCopyInto(*out) + } if in.Conditions != nil { in, out := &in.Conditions, &out.Conditions *out = make([]metav1.Condition, len(*in)) diff --git a/config/crd/bases/nac.oadp.openshift.io_nonadminbackups.yaml b/config/crd/bases/nac.oadp.openshift.io_nonadminbackups.yaml index 41dac9e..53e5b39 100644 --- a/config/crd/bases/nac.oadp.openshift.io_nonadminbackups.yaml +++ b/config/crd/bases/nac.oadp.openshift.io_nonadminbackups.yaml @@ -591,8 +591,8 @@ spec: type: object type: array phase: - description: NonAdminBackupPhase is a simple one high-level summary - of the lifecycle of an NonAdminBackup. + description: NonAdminPhase is a simple one high-level summary of the + lifecycle of a non admin object. enum: - New - BackingOff diff --git a/config/crd/bases/nac.oadp.openshift.io_nonadminrestores.yaml b/config/crd/bases/nac.oadp.openshift.io_nonadminrestores.yaml index 434252b..0f6ae9e 100644 --- a/config/crd/bases/nac.oadp.openshift.io_nonadminrestores.yaml +++ b/config/crd/bases/nac.oadp.openshift.io_nonadminrestores.yaml @@ -516,6 +516,103 @@ spec: - type type: object type: array + phase: + description: NonAdminPhase is a simple one high-level summary of the + lifecycle of a non admin object. + enum: + - New + - BackingOff + - Created + type: string + veleroRestoreName: + description: Related Velero Restore name. + type: string + veleroRestoreStatus: + description: Related Velero Restore status. + properties: + completionTimestamp: + description: |- + CompletionTimestamp records the time the restore operation was completed. + Completion time is recorded even on failed restore. + The server's time is used for StartTimestamps + format: date-time + nullable: true + type: string + errors: + description: |- + Errors is a count of all error messages that were generated during + execution of the restore. The actual errors are stored in object storage. + type: integer + failureReason: + description: FailureReason is an error that caused the entire + restore to fail. + type: string + phase: + description: Phase is the current state of the Restore + enum: + - New + - FailedValidation + - InProgress + - WaitingForPluginOperations + - WaitingForPluginOperationsPartiallyFailed + - Completed + - PartiallyFailed + - Failed + type: string + progress: + description: |- + Progress contains information about the restore's execution progress. Note + that this information is best-effort only -- if Velero fails to update it + during a restore for any reason, it may be inaccurate/stale. + nullable: true + properties: + itemsRestored: + description: ItemsRestored is the number of items that have + actually been restored so far + type: integer + totalItems: + description: |- + TotalItems is the total number of items to be restored. This number may change + throughout the execution of the restore due to plugins that return additional related + items to restore + type: integer + type: object + restoreItemOperationsAttempted: + description: |- + RestoreItemOperationsAttempted is the total number of attempted + async RestoreItemAction operations for this restore. + type: integer + restoreItemOperationsCompleted: + description: |- + RestoreItemOperationsCompleted is the total number of successfully completed + async RestoreItemAction operations for this restore. + type: integer + restoreItemOperationsFailed: + description: |- + RestoreItemOperationsFailed is the total number of async + RestoreItemAction operations for this restore which ended with an error. + type: integer + startTimestamp: + description: |- + StartTimestamp records the time the restore operation was started. + The server's time is used for StartTimestamps + format: date-time + nullable: true + type: string + validationErrors: + description: |- + ValidationErrors is a slice of all validation errors (if + applicable) + items: + type: string + nullable: true + type: array + warnings: + description: |- + Warnings is a count of all warning messages that were generated during + execution of the restore. The actual warnings are stored in object storage. + type: integer + type: object type: object type: object served: true diff --git a/internal/common/constant/constant.go b/internal/common/constant/constant.go index c3748d7..72351f8 100644 --- a/internal/common/constant/constant.go +++ b/internal/common/constant/constant.go @@ -17,7 +17,11 @@ limitations under the License. // Package constant contains all common constants used in the project package constant -import "os" +import ( + "os" + + "github.com/migtools/oadp-non-admin/internal/common/types" +) // Common labels for objects manipulated by the Non Admin Controller // Labels should be used to identify the NAC object @@ -37,6 +41,15 @@ const ( NamespaceEnvVar = "WATCH_NAMESPACE" ) +// Predefined conditions for NonAdminBackup. +// One NonAdminBackup object may have multiple conditions. +// It is more granular knowledge of the NonAdminBackup object and represents the +// array of the conditions through which the NonAdminBackup has or has not passed +const ( + NonAdminConditionAccepted types.NonAdminCondition = "Accepted" + NonAdminConditionQueued types.NonAdminCondition = "Queued" +) + // OadpNamespace is the namespace OADP operator is installed var OadpNamespace = os.Getenv(NamespaceEnvVar) diff --git a/internal/common/function/function.go b/internal/common/function/function.go index 250e58b..1a195c0 100644 --- a/internal/common/function/function.go +++ b/internal/common/function/function.go @@ -29,12 +29,13 @@ import ( velerov1api "github.com/vmware-tanzu/velero/pkg/apis/velero/v1" apimeta "k8s.io/apimachinery/pkg/api/meta" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" - "k8s.io/apimachinery/pkg/types" + apitypes "k8s.io/apimachinery/pkg/types" "sigs.k8s.io/controller-runtime/pkg/client" "sigs.k8s.io/controller-runtime/pkg/log" nacv1alpha1 "github.com/migtools/oadp-non-admin/api/v1alpha1" "github.com/migtools/oadp-non-admin/internal/common/constant" + "github.com/migtools/oadp-non-admin/internal/common/types" ) const requiredAnnotationError = "backup does not have the required annotation '%s'" @@ -145,7 +146,7 @@ func GenerateVeleroBackupName(namespace, nabName string) string { } // UpdateNonAdminPhase updates the phase of a NonAdminBackup object with the provided phase. -func UpdateNonAdminPhase(ctx context.Context, r client.Client, logger logr.Logger, nab *nacv1alpha1.NonAdminBackup, phase nacv1alpha1.NonAdminBackupPhase) (bool, error) { +func UpdateNonAdminPhase(ctx context.Context, r client.Client, logger logr.Logger, nab *nacv1alpha1.NonAdminBackup, phase nacv1alpha1.NonAdminPhase) (bool, error) { if nab == nil { return false, errors.New("NonAdminBackup object is nil") } @@ -177,7 +178,7 @@ func UpdateNonAdminPhase(ctx context.Context, r client.Client, logger logr.Logge // based on the provided parameters. It validates the input parameters and ensures // that the condition is set to the desired status only if it differs from the current status. // If the condition is already set to the desired status, no update is performed. -func UpdateNonAdminBackupCondition(ctx context.Context, r client.Client, logger logr.Logger, nab *nacv1alpha1.NonAdminBackup, condition nacv1alpha1.NonAdminCondition, conditionStatus metav1.ConditionStatus, reason string, message string) (bool, error) { +func UpdateNonAdminBackupCondition(ctx context.Context, r client.Client, logger logr.Logger, nab *nacv1alpha1.NonAdminBackup, condition types.NonAdminCondition, conditionStatus metav1.ConditionStatus, reason string, message string) (bool, error) { if nab == nil { return false, errors.New("NonAdminBackup object is nil") } @@ -306,7 +307,7 @@ func GetNonAdminBackupFromVeleroBackup(ctx context.Context, clientInstance clien return nil, fmt.Errorf(requiredAnnotationError, constant.NabOriginNameAnnotation) } - nonAdminBackupKey := types.NamespacedName{ + nonAdminBackupKey := apitypes.NamespacedName{ Namespace: nabOriginNamespace, Name: nabOriginName, } @@ -322,7 +323,7 @@ func GetNonAdminBackupFromVeleroBackup(ctx context.Context, clientInstance clien return nil, fmt.Errorf(requiredAnnotationError, constant.NabOriginUUIDAnnotation) } // Ensure UID matches - if nonAdminBackup.ObjectMeta.UID != types.UID(nabOriginUUID) { + if nonAdminBackup.ObjectMeta.UID != apitypes.UID(nabOriginUUID) { return nil, fmt.Errorf("UID from annotation does not match UID of fetched NonAdminBackup object") } diff --git a/api/v1alpha1/nonadmincontroller_types.go b/internal/common/types/types.go similarity index 56% rename from api/v1alpha1/nonadmincontroller_types.go rename to internal/common/types/types.go index 61c5608..bfdbd43 100644 --- a/api/v1alpha1/nonadmincontroller_types.go +++ b/internal/common/types/types.go @@ -14,17 +14,8 @@ See the License for the specific language governing permissions and limitations under the License. */ -package v1alpha1 +// Package types contains all common types used in the project +package types -// NonAdminCondition are used for more detailed information supporing NonAdminBackupPhase state. -// +kubebuilder:validation:Enum=Accepted;Queued +// NonAdminCondition are used for more detailed information supporting NonAdminBackupPhase state. type NonAdminCondition string - -// Predefined conditions for NonAdminBackup. -// One NonAdminBackup object may have multiple conditions. -// It is more granular knowledge of the NonAdminBackup object and represents the -// array of the conditions through which the NonAdminBackup has or has not passed -const ( - NonAdminConditionAccepted NonAdminCondition = "Accepted" - NonAdminConditionQueued NonAdminCondition = "Queued" -) diff --git a/internal/controller/nonadminbackup_controller.go b/internal/controller/nonadminbackup_controller.go index d8ac1a9..520fe2c 100644 --- a/internal/controller/nonadminbackup_controller.go +++ b/internal/controller/nonadminbackup_controller.go @@ -128,7 +128,7 @@ func (r *NonAdminBackupReconciler) InitNonAdminBackup(ctx context.Context, logrL // Set initial Phase if nab.Status.Phase == constant.EmptyString { // Phase: New - updatedStatus, errUpdate := function.UpdateNonAdminPhase(ctx, r.Client, logger, nab, nacv1alpha1.NonAdminBackupPhaseNew) + updatedStatus, errUpdate := function.UpdateNonAdminPhase(ctx, r.Client, logger, nab, nacv1alpha1.NonAdminPhaseNew) if errUpdate != nil { logger.Error(errUpdate, "Unable to set NonAdminBackup Phase: New", nameField, nab.Name, constant.NameSpaceString, nab.Namespace) @@ -171,7 +171,7 @@ func (r *NonAdminBackupReconciler) ValidateVeleroBackupSpec(ctx context.Context, } logger.Error(err, errMsg) - updatedStatus, errUpdateStatus := function.UpdateNonAdminPhase(ctx, r.Client, logger, nab, nacv1alpha1.NonAdminBackupPhaseBackingOff) + updatedStatus, errUpdateStatus := function.UpdateNonAdminPhase(ctx, r.Client, logger, nab, nacv1alpha1.NonAdminPhaseBackingOff) if errUpdateStatus != nil { logger.Error(errUpdateStatus, "Unable to set NonAdminBackup Phase: BackingOff", nameField, nab.Name, constant.NameSpaceString, nab.Namespace) return true, false, errUpdateStatus @@ -181,7 +181,7 @@ func (r *NonAdminBackupReconciler) ValidateVeleroBackupSpec(ctx context.Context, } // Continue. VeleroBackup looks fine, setting Accepted condition - updatedCondition, errUpdateCondition := function.UpdateNonAdminBackupCondition(ctx, r.Client, logger, nab, nacv1alpha1.NonAdminConditionAccepted, metav1.ConditionFalse, "InvalidBackupSpec", errMsg) + updatedCondition, errUpdateCondition := function.UpdateNonAdminBackupCondition(ctx, r.Client, logger, nab, constant.NonAdminConditionAccepted, metav1.ConditionFalse, "InvalidBackupSpec", errMsg) if errUpdateCondition != nil { logger.Error(errUpdateCondition, "Unable to set BackupAccepted Condition: False", nameField, nab.Name, constant.NameSpaceString, nab.Namespace) @@ -194,7 +194,7 @@ func (r *NonAdminBackupReconciler) ValidateVeleroBackupSpec(ctx context.Context, return true, false, err } - updatedStatus, errUpdateStatus := function.UpdateNonAdminBackupCondition(ctx, r.Client, logger, nab, nacv1alpha1.NonAdminConditionAccepted, metav1.ConditionTrue, "BackupAccepted", "backup accepted") + updatedStatus, errUpdateStatus := function.UpdateNonAdminBackupCondition(ctx, r.Client, logger, nab, constant.NonAdminConditionAccepted, metav1.ConditionTrue, "BackupAccepted", "backup accepted") if errUpdateStatus != nil { logger.Error(errUpdateStatus, "Unable to set BackupAccepted Condition: True", nameField, nab.Name, constant.NameSpaceString, nab.Namespace) return true, false, errUpdateStatus @@ -291,17 +291,17 @@ func (r *NonAdminBackupReconciler) CreateVeleroBackupSpec(ctx context.Context, l } logger.Info("VeleroBackup successfully created", nameField, veleroBackupName) - _, errUpdate := function.UpdateNonAdminPhase(ctx, r.Client, logger, nab, nacv1alpha1.NonAdminBackupPhaseCreated) + _, errUpdate := function.UpdateNonAdminPhase(ctx, r.Client, logger, nab, nacv1alpha1.NonAdminPhaseCreated) if errUpdate != nil { logger.Error(errUpdate, "Unable to set NonAdminBackup Phase: Created", nameField, nab.Name, constant.NameSpaceString, nab.Namespace) return true, false, errUpdate } - _, errUpdate = function.UpdateNonAdminBackupCondition(ctx, r.Client, logger, nab, nacv1alpha1.NonAdminConditionAccepted, metav1.ConditionTrue, "Validated", "Valid Backup config") + _, errUpdate = function.UpdateNonAdminBackupCondition(ctx, r.Client, logger, nab, constant.NonAdminConditionAccepted, metav1.ConditionTrue, "Validated", "Valid Backup config") if errUpdate != nil { logger.Error(errUpdate, "Unable to set BackupAccepted Condition: True", nameField, nab.Name, constant.NameSpaceString, nab.Namespace) return true, false, errUpdate } - _, errUpdate = function.UpdateNonAdminBackupCondition(ctx, r.Client, logger, nab, nacv1alpha1.NonAdminConditionQueued, metav1.ConditionTrue, "BackupScheduled", "Created Velero Backup object") + _, errUpdate = function.UpdateNonAdminBackupCondition(ctx, r.Client, logger, nab, constant.NonAdminConditionQueued, metav1.ConditionTrue, "BackupScheduled", "Created Velero Backup object") if errUpdate != nil { logger.Error(errUpdate, "Unable to set BackupQueued Condition: True", nameField, nab.Name, constant.NameSpaceString, nab.Namespace) return true, false, errUpdate diff --git a/internal/controller/nonadminrestore_controller.go b/internal/controller/nonadminrestore_controller.go index cd21d52..b254c5b 100644 --- a/internal/controller/nonadminrestore_controller.go +++ b/internal/controller/nonadminrestore_controller.go @@ -68,6 +68,8 @@ func (r *NonAdminRestoreReconciler) Reconcile(ctx context.Context, req ctrl.Requ // TODO try to create Velero Restore + // TODO update status of NonAdminRestore as Velero Restore progresses + return ctrl.Result{}, nil } @@ -77,7 +79,7 @@ func (r *NonAdminRestoreReconciler) validateSpec(ctx context.Context, req ctrl.R return fmt.Errorf("spec.restoreSpec.scheduleName field is not allowed in NonAdminRestore") } - // TODO nonAdminBackup respect restricted fields + // TODO nonAdminRestore respect restricted fields nonAdminBackupName := objectSpec.RestoreSpec.BackupName nonAdminBackup := &nacv1alpha1.NonAdminBackup{} @@ -89,18 +91,22 @@ func (r *NonAdminRestoreReconciler) validateSpec(ctx context.Context, req ctrl.R } return err } - // TODO nonAdminBackup has necessary labels (NAB controller job :question:) - // TODO nonAdminBackup is in complete state :question:!!!! + // TODO move this following to another function, it does not check spec // TODO create get function in common :question: oadpNamespace := os.Getenv(constant.NamespaceEnvVar) - veleroBackupName := nonAdminBackup.Labels["naoSei"] + veleroBackupName := nonAdminBackup.Status.VeleroBackupName + if len(veleroBackupName) == 0 { + return fmt.Errorf("NonAdminBackup '%s' does not reference Velero Backup name", nonAdminBackupName) + } veleroBackup := &velerov1api.Backup{} err = r.Get(ctx, types.NamespacedName{Namespace: oadpNamespace, Name: veleroBackupName}, veleroBackup) if err != nil { - // TODO test error messages, THEY MUST BE INFORMATIVE - return err + if errors.IsNotFound(err) { + // TODO add this error message to NonAdminRestore status + return fmt.Errorf("related Velero backup '%s' for NonAdminBackup '%s' does not exist in OADP namespace %s", veleroBackupName, nonAdminBackupName, oadpNamespace) + } } return nil diff --git a/internal/controller/nonadminrestore_controller_test.go b/internal/controller/nonadminrestore_controller_test.go index fb0b087..8d0e4ac 100644 --- a/internal/controller/nonadminrestore_controller_test.go +++ b/internal/controller/nonadminrestore_controller_test.go @@ -199,7 +199,6 @@ var _ = ginkgo.Describe("Test NonAdminRestore Reconcile function", func() { BackupName: "do-not-exist", }, }), - // TODO Should NOT accept NonAdminBackup that is not in complete state :question: // TODO Should NOT accept non existing related Velero Backup ) }) diff --git a/internal/predicate/nonadminbackup_predicate.go b/internal/predicate/nonadminbackup_predicate.go index d3309a3..0c5fd57 100644 --- a/internal/predicate/nonadminbackup_predicate.go +++ b/internal/predicate/nonadminbackup_predicate.go @@ -44,7 +44,7 @@ func (NonAdminBackupPredicate) Create(ctx context.Context, evt event.CreateEvent logger := getNonAdminBackupPredicateLogger(ctx, name, nameSpace) logger.V(1).Info("NonAdminBackupPredicate: Received Create event") if nonAdminBackup, ok := evt.Object.(*nacv1alpha1.NonAdminBackup); ok { - if nonAdminBackup.Status.Phase == constant.EmptyString || nonAdminBackup.Status.Phase == nacv1alpha1.NonAdminBackupPhaseNew { + if nonAdminBackup.Status.Phase == constant.EmptyString || nonAdminBackup.Status.Phase == nacv1alpha1.NonAdminPhaseNew { logger.V(1).Info("NonAdminBackupPredicate: Accepted Create event") return true } @@ -75,7 +75,7 @@ func (NonAdminBackupPredicate) Update(ctx context.Context, evt event.UpdateEvent if oldPhase == constant.EmptyString && newPhase != constant.EmptyString { logger.V(1).Info("NonAdminBsackupPredicate: Accepted Update event - phase change") return true - } else if oldPhase == nacv1alpha1.NonAdminBackupPhaseNew && newPhase == nacv1alpha1.NonAdminBackupPhaseCreated { + } else if oldPhase == nacv1alpha1.NonAdminPhaseNew && newPhase == nacv1alpha1.NonAdminPhaseCreated { logger.V(1).Info("NonAdminBackupPredicate: Accepted Update event - phase created") return true } From 47605cdf3078f6a018b222aa8795bc355f19e6a9 Mon Sep 17 00:00:00 2001 From: Mateus Oliveira Date: Wed, 15 May 2024 13:54:09 -0300 Subject: [PATCH 7/8] fixup! feat: Add Non Admin Restore controller Signed-off-by: Mateus Oliveira --- api/v1alpha1/nonadminrestore_types.go | 1 + config/crd/bases/nac.oadp.openshift.io_nonadminrestores.yaml | 2 ++ 2 files changed, 3 insertions(+) diff --git a/api/v1alpha1/nonadminrestore_types.go b/api/v1alpha1/nonadminrestore_types.go index 512ad2a..47b80be 100644 --- a/api/v1alpha1/nonadminrestore_types.go +++ b/api/v1alpha1/nonadminrestore_types.go @@ -52,6 +52,7 @@ type NonAdminRestoreStatus struct { // +kubebuilder:object:root=true // +kubebuilder:subresource:status +// +kubebuilder:resource:path=nonadminrestores,shortName=nar // NonAdminRestore is the Schema for the nonadminrestores API type NonAdminRestore struct { diff --git a/config/crd/bases/nac.oadp.openshift.io_nonadminrestores.yaml b/config/crd/bases/nac.oadp.openshift.io_nonadminrestores.yaml index 0f6ae9e..6b8f540 100644 --- a/config/crd/bases/nac.oadp.openshift.io_nonadminrestores.yaml +++ b/config/crd/bases/nac.oadp.openshift.io_nonadminrestores.yaml @@ -11,6 +11,8 @@ spec: kind: NonAdminRestore listKind: NonAdminRestoreList plural: nonadminrestores + shortNames: + - nar singular: nonadminrestore scope: Namespaced versions: From ba8553ee40ef4a74836b4c01ea15fc388a3d059a Mon Sep 17 00:00:00 2001 From: Mateus Oliveira Date: Wed, 15 May 2024 17:05:24 -0300 Subject: [PATCH 8/8] fixup! feat: Add Non Admin Restore controller Signed-off-by: Mateus Oliveira --- api/v1alpha1/nonadminrestore_types.go | 4 +- ...ac.oadp.openshift.io_nonadminrestores.yaml | 2 + .../nonadminrestore_controller_test.go | 64 ------------------- 3 files changed, 3 insertions(+), 67 deletions(-) diff --git a/api/v1alpha1/nonadminrestore_types.go b/api/v1alpha1/nonadminrestore_types.go index 47b80be..4a50149 100644 --- a/api/v1alpha1/nonadminrestore_types.go +++ b/api/v1alpha1/nonadminrestore_types.go @@ -24,9 +24,7 @@ import ( // NonAdminRestoreSpec defines the desired state of NonAdminRestore type NonAdminRestoreSpec struct { // Specification for a Velero restore. - // +kubebuilder:validation:Required - RestoreSpec *velerov1api.RestoreSpec `json:"restoreSpec,omitempty"` - // TODO add test that NAR can not be created without restoreSpec or restoreSpec.backupName + RestoreSpec *velerov1api.RestoreSpec `json:"restoreSpec"` // TODO need to investigate restoreSpec.namespaceMapping, depends on how NAC tracks the namespace access per user // TODO NonAdminRestore log level, by default TODO. diff --git a/config/crd/bases/nac.oadp.openshift.io_nonadminrestores.yaml b/config/crd/bases/nac.oadp.openshift.io_nonadminrestores.yaml index 6b8f540..fa420e3 100644 --- a/config/crd/bases/nac.oadp.openshift.io_nonadminrestores.yaml +++ b/config/crd/bases/nac.oadp.openshift.io_nonadminrestores.yaml @@ -445,6 +445,8 @@ spec: required: - backupName type: object + required: + - restoreSpec type: object status: description: NonAdminRestoreStatus defines the observed state of NonAdminRestore diff --git a/internal/controller/nonadminrestore_controller_test.go b/internal/controller/nonadminrestore_controller_test.go index 8d0e4ac..0f7474a 100644 --- a/internal/controller/nonadminrestore_controller_test.go +++ b/internal/controller/nonadminrestore_controller_test.go @@ -32,11 +32,6 @@ import ( "github.com/migtools/oadp-non-admin/internal/common/constant" ) -type clusterScenario struct { - namespace string - nonAdminRestore string -} - type nonAdminRestoreReconcileScenario struct { restoreSpec *v1.RestoreSpec namespace string @@ -56,65 +51,6 @@ func createTestNonAdminRestore(name string, namespace string, restoreSpec v1.Res } } -// TODO this does not work with envtest :question: -var _ = ginkgo.Describe("Test NonAdminRestore in cluster validation", func() { - var ( - ctx = context.Background() - currentTestScenario clusterScenario - updateTestScenario = func(scenario clusterScenario) { - currentTestScenario = scenario - } - ) - - ginkgo.AfterEach(func() { - nonAdminRestore := &nacv1alpha1.NonAdminRestore{} - if k8sClient.Get( - ctx, - types.NamespacedName{ - Name: currentTestScenario.nonAdminRestore, - Namespace: currentTestScenario.namespace, - }, - nonAdminRestore, - ) == nil { - gomega.Expect(k8sClient.Delete(ctx, nonAdminRestore)).To(gomega.Succeed()) - } - - namespace := &corev1.Namespace{ - ObjectMeta: metav1.ObjectMeta{ - Name: currentTestScenario.namespace, - }, - } - gomega.Expect(k8sClient.Delete(ctx, namespace)).To(gomega.Succeed()) - }) - - ginkgo.DescribeTable("Validation is false", - func(scenario clusterScenario) { - updateTestScenario(scenario) - - namespace := &corev1.Namespace{ - ObjectMeta: metav1.ObjectMeta{ - Name: scenario.namespace, - }, - } - gomega.Expect(k8sClient.Create(ctx, namespace)).To(gomega.Succeed()) - - nonAdminRestore := &nacv1alpha1.NonAdminRestore{ - ObjectMeta: metav1.ObjectMeta{ - Name: scenario.nonAdminRestore, - Namespace: scenario.namespace, - }, - // Spec: nacv1alpha1.NonAdminRestoreSpec{}, - } - gomega.Expect(k8sClient.Create(ctx, nonAdminRestore)).To(gomega.Not(gomega.Succeed())) - }, - ginkgo.Entry("Should NOT create NonAdminRestore without spec.restoreSpec", clusterScenario{ - namespace: "test-nonadminrestore-cluster-1", - nonAdminRestore: "test-nonadminrestore-cluster-1-cr", - }), - // TODO Should NOT create NonAdminRestore without spec.restoreSpec.backupName - ) -}) - var _ = ginkgo.Describe("Test NonAdminRestore Reconcile function", func() { var ( ctx = context.Background()