From 3350e77d63c56d18f2604937ebb88fa072e22784 Mon Sep 17 00:00:00 2001 From: rdhamal Date: Wed, 15 Nov 2023 21:43:24 -0500 Subject: [PATCH 01/17] Added cookies --- ndb_client/ndb_client.go | 3 +++ 1 file changed, 3 insertions(+) diff --git a/ndb_client/ndb_client.go b/ndb_client/ndb_client.go index 379384a0..eb82f769 100644 --- a/ndb_client/ndb_client.go +++ b/ndb_client/ndb_client.go @@ -47,6 +47,7 @@ func NewNDBClient(username, password, url, caCert string, skipVerify bool) *NDBC func (ndbClient *NDBClient) Get(path string) (*http.Response, error) { url := ndbClient.url + "/" + path req, err := http.NewRequest(http.MethodGet, url, nil) + req.Header.Add("Cookie", "eraAuth=eyJhbGciOiJSUzUxMiJ9") if err != nil { // fmt.Println(err) return nil, err @@ -59,6 +60,7 @@ func (ndbClient *NDBClient) Post(path string, body interface{}) (*http.Response, url := ndbClient.url + "/" + path payload, _ := json.Marshal(body) req, err := http.NewRequest(http.MethodPost, url, bytes.NewBuffer(payload)) + req.Header.Add("Cookie", "eraAuth=eyJhbGciOiJSUzUxMiJ9") if err != nil { // fmt.Println(err) return nil, err @@ -72,6 +74,7 @@ func (ndbClient *NDBClient) Delete(path string, body interface{}) (*http.Respons url := ndbClient.url + "/" + path payload, _ := json.Marshal(body) req, err := http.NewRequest(http.MethodDelete, url, bytes.NewBuffer(payload)) + req.Header.Add("Cookie", "eraAuth=eyJhbGciOiJSUzUxMiJ9") if err != nil { // fmt.Println(err) return nil, err From 1466f605511cff3f5718216b50cea9e4558ac5e5 Mon Sep 17 00:00:00 2001 From: rdhamal Date: Sat, 25 Nov 2023 18:55:59 -0500 Subject: [PATCH 02/17] Scaffolding and initial CRD created --- PROJECT | 9 ++ api/v1alpha1/snapshot_types.go | 70 +++++++++++++++ api/v1alpha1/zz_generated.deepcopy.go | 89 +++++++++++++++++++ .../crd/bases/ndb.nutanix.com_snapshots.yaml | 66 ++++++++++++++ config/crd/kustomization.yaml | 3 + .../crd/patches/cainjection_in_snapshots.yaml | 7 ++ config/crd/patches/webhook_in_snapshots.yaml | 16 ++++ config/rbac/role.yaml | 26 ++++++ config/rbac/snapshot_editor_role.yaml | 31 +++++++ config/rbac/snapshot_viewer_role.yaml | 27 ++++++ config/samples/kustomization.yaml | 4 + config/samples/ndb_v1alpha1_snapshot.yaml | 12 +++ controllers/snapshot_controller.go | 62 +++++++++++++ main.go | 7 ++ 14 files changed, 429 insertions(+) create mode 100644 api/v1alpha1/snapshot_types.go create mode 100644 config/crd/bases/ndb.nutanix.com_snapshots.yaml create mode 100644 config/crd/patches/cainjection_in_snapshots.yaml create mode 100644 config/crd/patches/webhook_in_snapshots.yaml create mode 100644 config/rbac/snapshot_editor_role.yaml create mode 100644 config/rbac/snapshot_viewer_role.yaml create mode 100644 config/samples/kustomization.yaml create mode 100644 config/samples/ndb_v1alpha1_snapshot.yaml create mode 100644 controllers/snapshot_controller.go diff --git a/PROJECT b/PROJECT index bf7bd33a..7f43bb58 100644 --- a/PROJECT +++ b/PROJECT @@ -33,4 +33,13 @@ resources: kind: NDBServer path: github.com/nutanix-cloud-native/ndb-operator/api/v1alpha1 version: v1alpha1 +- api: + crdVersion: v1 + namespaced: true + controller: true + domain: nutanix.com + group: ndb + kind: Snapshot + path: github.com/nutanix-cloud-native/ndb-operator/api/v1alpha1 + version: v1alpha1 version: "3" diff --git a/api/v1alpha1/snapshot_types.go b/api/v1alpha1/snapshot_types.go new file mode 100644 index 00000000..5a69ba6c --- /dev/null +++ b/api/v1alpha1/snapshot_types.go @@ -0,0 +1,70 @@ +/* +Copyright 2022-2023 Nutanix, Inc. + +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. + +// SnapshotSpec defines the desired state of Snapshot +type SnapshotSpec 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 Snapshot. Edit snapshot_types.go to remove/update + IP string `json:"ip,omitempty"` + Username string `json:"username,omitempty"` + Password string `json:"password,omitempty"` + TimeMachineID string `json:"timemachineid,omitempty"` + Name string `json:"name,omitempty"` +} + +// SnapshotStatus defines the observed state of Snapshot +type SnapshotStatus struct { + // INSERT ADDITIONAL STATUS FIELD - define observed state of cluster + // Important: Run "make" to regenerate code after modifying this file + OperationID string `json:"operationid,omitempty"` + Status string `json:"status,omitempty"` +} + +//+kubebuilder:object:root=true +//+kubebuilder:subresource:status + +// Snapshot is the Schema for the snapshots API +type Snapshot struct { + metav1.TypeMeta `json:",inline"` + metav1.ObjectMeta `json:"metadata,omitempty"` + + Spec SnapshotSpec `json:"spec,omitempty"` + Status SnapshotStatus `json:"status,omitempty"` +} + +//+kubebuilder:object:root=true + +// SnapshotList contains a list of Snapshot +type SnapshotList struct { + metav1.TypeMeta `json:",inline"` + metav1.ListMeta `json:"metadata,omitempty"` + Items []Snapshot `json:"items"` +} + +func init() { + SchemeBuilder.Register(&Snapshot{}, &SnapshotList{}) +} diff --git a/api/v1alpha1/zz_generated.deepcopy.go b/api/v1alpha1/zz_generated.deepcopy.go index 96e391a9..70019318 100644 --- a/api/v1alpha1/zz_generated.deepcopy.go +++ b/api/v1alpha1/zz_generated.deepcopy.go @@ -364,3 +364,92 @@ func (in *ReconcileCounter) DeepCopy() *ReconcileCounter { in.DeepCopyInto(out) return out } + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *Snapshot) DeepCopyInto(out *Snapshot) { + *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 Snapshot. +func (in *Snapshot) DeepCopy() *Snapshot { + if in == nil { + return nil + } + out := new(Snapshot) + in.DeepCopyInto(out) + return out +} + +// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. +func (in *Snapshot) 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 *SnapshotList) DeepCopyInto(out *SnapshotList) { + *out = *in + out.TypeMeta = in.TypeMeta + in.ListMeta.DeepCopyInto(&out.ListMeta) + if in.Items != nil { + in, out := &in.Items, &out.Items + *out = make([]Snapshot, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new SnapshotList. +func (in *SnapshotList) DeepCopy() *SnapshotList { + if in == nil { + return nil + } + out := new(SnapshotList) + in.DeepCopyInto(out) + return out +} + +// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. +func (in *SnapshotList) 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 *SnapshotSpec) DeepCopyInto(out *SnapshotSpec) { + *out = *in +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new SnapshotSpec. +func (in *SnapshotSpec) DeepCopy() *SnapshotSpec { + if in == nil { + return nil + } + out := new(SnapshotSpec) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *SnapshotStatus) DeepCopyInto(out *SnapshotStatus) { + *out = *in +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new SnapshotStatus. +func (in *SnapshotStatus) DeepCopy() *SnapshotStatus { + if in == nil { + return nil + } + out := new(SnapshotStatus) + in.DeepCopyInto(out) + return out +} diff --git a/config/crd/bases/ndb.nutanix.com_snapshots.yaml b/config/crd/bases/ndb.nutanix.com_snapshots.yaml new file mode 100644 index 00000000..d7c7bb3b --- /dev/null +++ b/config/crd/bases/ndb.nutanix.com_snapshots.yaml @@ -0,0 +1,66 @@ +--- +apiVersion: apiextensions.k8s.io/v1 +kind: CustomResourceDefinition +metadata: + annotations: + controller-gen.kubebuilder.io/version: v0.9.2 + creationTimestamp: null + name: snapshots.ndb.nutanix.com +spec: + group: ndb.nutanix.com + names: + kind: Snapshot + listKind: SnapshotList + plural: snapshots + singular: snapshot + scope: Namespaced + versions: + - name: v1alpha1 + schema: + openAPIV3Schema: + description: Snapshot is the Schema for the snapshots 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: SnapshotSpec defines the desired state of Snapshot + properties: + ip: + description: Foo is an example field of Snapshot. Edit snapshot_types.go + to remove/update + type: string + name: + type: string + password: + type: string + timemachineid: + type: string + username: + type: string + type: object + status: + description: SnapshotStatus defines the observed state of Snapshot + properties: + operationid: + description: 'INSERT ADDITIONAL STATUS FIELD - define observed state + of cluster Important: Run "make" to regenerate code after modifying + this file' + type: string + status: + type: string + type: object + type: object + served: true + storage: true + subresources: + status: {} diff --git a/config/crd/kustomization.yaml b/config/crd/kustomization.yaml index 3f1aa251..e47dce62 100644 --- a/config/crd/kustomization.yaml +++ b/config/crd/kustomization.yaml @@ -4,6 +4,7 @@ resources: - bases/ndb.nutanix.com_databases.yaml - bases/ndb.nutanix.com_ndbservers.yaml +- bases/ndb.nutanix.com_snapshots.yaml #+kubebuilder:scaffold:crdkustomizeresource patchesStrategicMerge: @@ -11,12 +12,14 @@ patchesStrategicMerge: # patches here are for enabling the conversion webhook for each CRD - patches/webhook_in_databases.yaml #- patches/webhook_in_ndbservers.yaml +#- patches/webhook_in_snapshots.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 - patches/cainjection_in_databases.yaml #- patches/cainjection_in_ndbservers.yaml +#- patches/cainjection_in_snapshots.yaml #+kubebuilder:scaffold:crdkustomizecainjectionpatch # the following config is for teaching kustomize how to do kustomization for CRDs. diff --git a/config/crd/patches/cainjection_in_snapshots.yaml b/config/crd/patches/cainjection_in_snapshots.yaml new file mode 100644 index 00000000..933dfbe9 --- /dev/null +++ b/config/crd/patches/cainjection_in_snapshots.yaml @@ -0,0 +1,7 @@ +# The following patch adds a directive for certmanager to inject CA into the CRD +apiVersion: apiextensions.k8s.io/v1 +kind: CustomResourceDefinition +metadata: + annotations: + cert-manager.io/inject-ca-from: $(CERTIFICATE_NAMESPACE)/$(CERTIFICATE_NAME) + name: snapshots.ndb.nutanix.com diff --git a/config/crd/patches/webhook_in_snapshots.yaml b/config/crd/patches/webhook_in_snapshots.yaml new file mode 100644 index 00000000..050019bf --- /dev/null +++ b/config/crd/patches/webhook_in_snapshots.yaml @@ -0,0 +1,16 @@ +# The following patch enables a conversion webhook for the CRD +apiVersion: apiextensions.k8s.io/v1 +kind: CustomResourceDefinition +metadata: + name: snapshots.ndb.nutanix.com +spec: + conversion: + strategy: Webhook + webhook: + clientConfig: + service: + namespace: system + name: webhook-service + path: /convert + conversionReviewVersions: + - v1 diff --git a/config/rbac/role.yaml b/config/rbac/role.yaml index 51cfdecf..27daf1c3 100644 --- a/config/rbac/role.yaml +++ b/config/rbac/role.yaml @@ -96,3 +96,29 @@ rules: - get - patch - update +- apiGroups: + - ndb.nutanix.com + resources: + - snapshots + verbs: + - create + - delete + - get + - list + - patch + - update + - watch +- apiGroups: + - ndb.nutanix.com + resources: + - snapshots/finalizers + verbs: + - update +- apiGroups: + - ndb.nutanix.com + resources: + - snapshots/status + verbs: + - get + - patch + - update diff --git a/config/rbac/snapshot_editor_role.yaml b/config/rbac/snapshot_editor_role.yaml new file mode 100644 index 00000000..e393e115 --- /dev/null +++ b/config/rbac/snapshot_editor_role.yaml @@ -0,0 +1,31 @@ +# permissions for end users to edit snapshots. +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRole +metadata: + labels: + app.kubernetes.io/name: clusterrole + app.kubernetes.io/instance: snapshot-editor-role + app.kubernetes.io/component: rbac + app.kubernetes.io/created-by: ndb-operator + app.kubernetes.io/part-of: ndb-operator + app.kubernetes.io/managed-by: kustomize + name: snapshot-editor-role +rules: +- apiGroups: + - ndb.nutanix.com + resources: + - snapshots + verbs: + - create + - delete + - get + - list + - patch + - update + - watch +- apiGroups: + - ndb.nutanix.com + resources: + - snapshots/status + verbs: + - get diff --git a/config/rbac/snapshot_viewer_role.yaml b/config/rbac/snapshot_viewer_role.yaml new file mode 100644 index 00000000..c9860772 --- /dev/null +++ b/config/rbac/snapshot_viewer_role.yaml @@ -0,0 +1,27 @@ +# permissions for end users to view snapshots. +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRole +metadata: + labels: + app.kubernetes.io/name: clusterrole + app.kubernetes.io/instance: snapshot-viewer-role + app.kubernetes.io/component: rbac + app.kubernetes.io/created-by: ndb-operator + app.kubernetes.io/part-of: ndb-operator + app.kubernetes.io/managed-by: kustomize + name: snapshot-viewer-role +rules: +- apiGroups: + - ndb.nutanix.com + resources: + - snapshots + verbs: + - get + - list + - watch +- apiGroups: + - ndb.nutanix.com + resources: + - snapshots/status + verbs: + - get diff --git a/config/samples/kustomization.yaml b/config/samples/kustomization.yaml new file mode 100644 index 00000000..3263de3e --- /dev/null +++ b/config/samples/kustomization.yaml @@ -0,0 +1,4 @@ +## Append samples you want in your CSV to this file as resources ## +resources: +- ndb_v1alpha1_snapshot.yaml +#+kubebuilder:scaffold:manifestskustomizesamples diff --git a/config/samples/ndb_v1alpha1_snapshot.yaml b/config/samples/ndb_v1alpha1_snapshot.yaml new file mode 100644 index 00000000..8b71d894 --- /dev/null +++ b/config/samples/ndb_v1alpha1_snapshot.yaml @@ -0,0 +1,12 @@ +apiVersion: ndb.nutanix.com/v1alpha1 +kind: Snapshot +metadata: + labels: + app.kubernetes.io/name: snapshot + app.kubernetes.io/instance: snapshot-sample + app.kubernetes.io/part-of: ndb-operator + app.kubernetes.io/managed-by: kustomize + app.kubernetes.io/created-by: ndb-operator + name: snapshot-sample +spec: + # TODO(user): Add fields here diff --git a/controllers/snapshot_controller.go b/controllers/snapshot_controller.go new file mode 100644 index 00000000..2bafb9c0 --- /dev/null +++ b/controllers/snapshot_controller.go @@ -0,0 +1,62 @@ +/* +Copyright 2022-2023 Nutanix, Inc. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package controllers + +import ( + "context" + + "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" + + ndbv1alpha1 "github.com/nutanix-cloud-native/ndb-operator/api/v1alpha1" +) + +// SnapshotReconciler reconciles a Snapshot object +type SnapshotReconciler struct { + client.Client + Scheme *runtime.Scheme +} + +//+kubebuilder:rbac:groups=ndb.nutanix.com,resources=snapshots,verbs=get;list;watch;create;update;patch;delete +//+kubebuilder:rbac:groups=ndb.nutanix.com,resources=snapshots/status,verbs=get;update;patch +//+kubebuilder:rbac:groups=ndb.nutanix.com,resources=snapshots/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 Snapshot 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.14.1/pkg/reconcile +func (r *SnapshotReconciler) 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 *SnapshotReconciler) SetupWithManager(mgr ctrl.Manager) error { + return ctrl.NewControllerManagedBy(mgr). + For(&ndbv1alpha1.Snapshot{}). + Complete(r) +} diff --git a/main.go b/main.go index 24b3daad..be711942 100644 --- a/main.go +++ b/main.go @@ -125,6 +125,13 @@ func main() { setupLog.Error(err, "unable to create controller", "controller", "NDBServer") os.Exit(1) } + if err = (&controllers.SnapshotReconciler{ + Client: mgr.GetClient(), + Scheme: mgr.GetScheme(), + }).SetupWithManager(mgr); err != nil { + setupLog.Error(err, "unable to create controller", "controller", "Snapshot") + os.Exit(1) + } //+kubebuilder:scaffold:builder if err := mgr.AddHealthzCheck("healthz", healthz.Ping); err != nil { From c7cc167ec3dffcda81581071c2dea37376351956 Mon Sep 17 00:00:00 2001 From: rdhamal Date: Sat, 25 Nov 2023 19:51:45 -0500 Subject: [PATCH 03/17] Created a skeleton for snapshot api --- ndb_api/snapshot.go | 197 +++++++++++++++++++++++++++++ ndb_api/snapshot_request_types.go | 21 +++ ndb_api/snapshot_response_types.go | 27 ++++ 3 files changed, 245 insertions(+) create mode 100644 ndb_api/snapshot.go create mode 100644 ndb_api/snapshot_request_types.go create mode 100644 ndb_api/snapshot_response_types.go diff --git a/ndb_api/snapshot.go b/ndb_api/snapshot.go new file mode 100644 index 00000000..d4a1c808 --- /dev/null +++ b/ndb_api/snapshot.go @@ -0,0 +1,197 @@ +/* +Copyright 2022-2023 Nutanix, Inc. + +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 ndb_api + +import ( + "context" + "encoding/json" + "errors" + "fmt" + "io" + "net/http" + + "github.com/nutanix-cloud-native/ndb-operator/ndb_client" + ctrllog "sigs.k8s.io/controller-runtime/pkg/log" +) + +// Fetches all snapshots on the NDB instance and returns a slice of the snapshots +func GetAllSnapshots(ctx context.Context, ndbClient *ndb_client.NDBClient) (snapshots []SnapshotResponse, err error) { + log := ctrllog.FromContext(ctx) + log.Info("Entered ndb_api.GetAllDatabases") + if ndbClient == nil { + err = errors.New("nil reference: received nil reference for ndbClient") + log.Error(err, "Received nil ndbClient reference") + return + } + res, err := ndbClient.Get("snapshots") + if err != nil || res == nil || res.StatusCode != http.StatusOK { + if err == nil { + if res != nil { + err = fmt.Errorf("GET /snapshots responded with %d", res.StatusCode) + } else { + err = fmt.Errorf("GET /snapshots responded with a nil response") + } + } + log.Error(err, "Error occurred fetching all snapshots") + return + } + log.Info("GET /snapshots", "HTTP status code", res.StatusCode) + body, err := io.ReadAll(res.Body) + defer res.Body.Close() + if err != nil { + log.Error(err, "Error occurred reading response.Body in GetAllSnapshots") + return + } + err = json.Unmarshal(body, &snapshots) + if err != nil { + log.Error(err, "Error occurred trying to unmarshal.") + return + } + log.Info("Returning from ndb_api.GetAllSnapshots") + return +} + +// Fetches and returns a snapshot by an Id +func GetSnapshotById(ctx context.Context, ndbClient *ndb_client.NDBClient, id string) (snapshot SnapshotResponse, err error) { + log := ctrllog.FromContext(ctx) + log.Info("Entered ndb_api.GetSnapshotById", "snapshotId", id) + if ndbClient == nil { + err = errors.New("nil reference") + log.Error(err, "Received nil ndbClient reference") + return + } + // Checking if id is empty, this is necessary otherwise the request becomes a call to get all databases (/databases) + if id == "" { + err = fmt.Errorf("snapshot id is empty") + log.Error(err, "no snapshot id provided") + return + } + getSnapshotPath := fmt.Sprintf("snapshots/%s", id) + res, err := ndbClient.Get(getSnapshotPath) + if err != nil || res == nil || res.StatusCode != http.StatusOK { + if err == nil { + if res != nil { + err = fmt.Errorf("GET %s responded with %d", getSnapshotPath, res.StatusCode) + } else { + err = fmt.Errorf("GET %s responded with a nil response", getSnapshotPath) + } + } + log.Error(err, "Error occurred fetching database") + return + } + log.Info(getSnapshotPath, "HTTP status code", res.StatusCode) + body, err := io.ReadAll(res.Body) + defer res.Body.Close() + if err != nil { + log.Error(err, "Error occurred reading response.Body in Get Snapshot by ID") + return + } + err = json.Unmarshal(body, &snapshot) + if err != nil { + log.Error(err, "Error occurred trying to unmarshal.") + return + } + log.Info("Returning from ndb_api.GetSnapshotById") + return +} + +// Takes a snapshot of the database upon request +// Returns the task info summary response for the operation +func TakeSnapshot(ctx context.Context, ndbClient *ndb_client.NDBClient, req *TakeSnapshotRequest) (task TaskInfoSummaryResponse, err error) { + log := ctrllog.FromContext(ctx) + log.Info("Entered ndb_api.TakeSnapshot") + if ndbClient == nil { + err = errors.New("nil reference") + log.Error(err, "Received nil ndbClient reference") + return + } + if req.TimeMachineId == "" { + err = errors.New("empty timeMachineId") + log.Error(err, "Received empty timeMachineId in request") + return + } + snapshotEndPoint := "tms/" + req.TimeMachineId + "/snapshots" + res, err := ndbClient.Post(snapshotEndPoint, req) + if err != nil || res == nil || res.StatusCode != http.StatusOK { + if err == nil { + if res != nil { + err = fmt.Errorf("POST %s responded with %d", snapshotEndPoint, res.StatusCode) + } else { + err = fmt.Errorf("POST %s responded with nil response", snapshotEndPoint) + } + } + log.Error(err, "Error taking snapshot of database") + return + } + log.Info("POST "+snapshotEndPoint, "HTTP status code", res.StatusCode) + body, err := io.ReadAll(res.Body) + defer res.Body.Close() + if err != nil { + log.Error(err, "Error occurred reading response.Body in TakeSnapshot") + return + } + err = json.Unmarshal(body, &task) + if err != nil { + log.Error(err, "Error occurred trying to unmarshal.") + return + } + log.Info("Returning from ndb_api.TakeSnapshot") + return +} + +// Deletes a snapshot given a snapshot id +// Returns the task info summary response for the operation +func DeleteSnapshot(ctx context.Context, ndbClient *ndb_client.NDBClient, id string) (task TaskInfoSummaryResponse, err error) { + log := ctrllog.FromContext(ctx) + log.Info("Entered ndb_api.DeleteSnapshot", "SnapshotId", id) + if ndbClient == nil { + err = errors.New("nil reference") + log.Error(err, "Received nil ndbClient reference") + return + } + if id == "" { + err = fmt.Errorf("id is empty") + log.Error(err, "no snapshot id provided") + return + } + res, err := ndbClient.Delete("snapshots/" + id) + if err != nil || res == nil || res.StatusCode != http.StatusOK { + if err == nil { + if res != nil { + err = fmt.Errorf("DELETE /snapshots/%s responded with %d", id, res.StatusCode) + } else { + err = fmt.Errorf("DELETE /snapshots/%s responded with nil response", id) + } + } + log.Error(err, "Error occurred deleting snapshots") + return + } + log.Info("DELETE /snapshots/"+id, "HTTP status code", res.StatusCode) + body, err := io.ReadAll(res.Body) + defer res.Body.Close() + if err != nil { + log.Error(err, "Error occurred reading response.Body") + return + } + err = json.Unmarshal(body, &task) + if err != nil { + log.Error(err, "Error occurred trying to unmarshal.") + return + } + log.Info("Returning from ndb_api.DeleteSnapshot") + return +} diff --git a/ndb_api/snapshot_request_types.go b/ndb_api/snapshot_request_types.go new file mode 100644 index 00000000..93204f86 --- /dev/null +++ b/ndb_api/snapshot_request_types.go @@ -0,0 +1,21 @@ +/* +Copyright 2022-2023 Nutanix, Inc. + +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 ndb_api + +type TakeSnapshotRequest struct { + TimeMachineId string `json:'timemachine_id'` +} diff --git a/ndb_api/snapshot_response_types.go b/ndb_api/snapshot_response_types.go new file mode 100644 index 00000000..93cfe1a6 --- /dev/null +++ b/ndb_api/snapshot_response_types.go @@ -0,0 +1,27 @@ +/* +Copyright 2022-2023 Nutanix, Inc. + +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 ndb_api + +type SnapshotResponse struct { + Id string `json:"id"` + Name string `json:"name"` + Status string `json:"status"` + Properties []Property `json:"properties"` + TimeMachineId string `json:"timeMachineId"` + SnapshotId string `json:"snapshotId"` + SnapshotTimeStamp string `json:"snapshotTimeStamp"` +} From 2dce4c3f6136b594c9a143feb4668366fa5871f8 Mon Sep 17 00:00:00 2001 From: rdhamal Date: Sat, 25 Nov 2023 19:57:35 -0500 Subject: [PATCH 04/17] Adjustment for incoming merge change --- ndb_api/snapshot.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ndb_api/snapshot.go b/ndb_api/snapshot.go index d4a1c808..808588d4 100644 --- a/ndb_api/snapshot.go +++ b/ndb_api/snapshot.go @@ -111,7 +111,7 @@ func GetSnapshotById(ctx context.Context, ndbClient *ndb_client.NDBClient, id st // Takes a snapshot of the database upon request // Returns the task info summary response for the operation -func TakeSnapshot(ctx context.Context, ndbClient *ndb_client.NDBClient, req *TakeSnapshotRequest) (task TaskInfoSummaryResponse, err error) { +func TakeSnapshot(ctx context.Context, ndbClient *ndb_client.NDBClient, req *SnapshotRequest) (task TaskInfoSummaryResponse, err error) { log := ctrllog.FromContext(ctx) log.Info("Entered ndb_api.TakeSnapshot") if ndbClient == nil { From 0cf73c9525afc84bd454dd80b3fbecbc89e1e244 Mon Sep 17 00:00:00 2001 From: rdhamal Date: Tue, 28 Nov 2023 14:28:56 -0500 Subject: [PATCH 05/17] Changed logging statements --- ndb_api/snapshot.go | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/ndb_api/snapshot.go b/ndb_api/snapshot.go index 808588d4..ec51e930 100644 --- a/ndb_api/snapshot.go +++ b/ndb_api/snapshot.go @@ -31,7 +31,7 @@ import ( // Fetches all snapshots on the NDB instance and returns a slice of the snapshots func GetAllSnapshots(ctx context.Context, ndbClient *ndb_client.NDBClient) (snapshots []SnapshotResponse, err error) { log := ctrllog.FromContext(ctx) - log.Info("Entered ndb_api.GetAllDatabases") + log.Info("Entered ndb_api.GetAllSnapshots") if ndbClient == nil { err = errors.New("nil reference: received nil reference for ndbClient") log.Error(err, "Received nil ndbClient reference") @@ -74,7 +74,7 @@ func GetSnapshotById(ctx context.Context, ndbClient *ndb_client.NDBClient, id st log.Error(err, "Received nil ndbClient reference") return } - // Checking if id is empty, this is necessary otherwise the request becomes a call to get all databases (/databases) + // Checking if id is empty, this is necessary otherwise the request becomes a call to get all snapshots (/snapshots) if id == "" { err = fmt.Errorf("snapshot id is empty") log.Error(err, "no snapshot id provided") @@ -90,7 +90,7 @@ func GetSnapshotById(ctx context.Context, ndbClient *ndb_client.NDBClient, id st err = fmt.Errorf("GET %s responded with a nil response", getSnapshotPath) } } - log.Error(err, "Error occurred fetching database") + log.Error(err, "Error occurred fetching snapshot") return } log.Info(getSnapshotPath, "HTTP status code", res.StatusCode) From 9960d36141e6b3545c4952c81a7b151e6b41245e Mon Sep 17 00:00:00 2001 From: rdhamal Date: Tue, 28 Nov 2023 22:40:01 -0500 Subject: [PATCH 06/17] Implemented controller skeleton --- controllers/snapshot_controller.go | 63 +++++++++++++++++++++-- controllers/snapshot_controller_helper.go | 1 + ndb_api/snapshots_helper.go | 19 +++++++ 3 files changed, 78 insertions(+), 5 deletions(-) create mode 100644 controllers/snapshot_controller_helper.go create mode 100644 ndb_api/snapshots_helper.go diff --git a/controllers/snapshot_controller.go b/controllers/snapshot_controller.go index 2bafb9c0..4f235f51 100644 --- a/controllers/snapshot_controller.go +++ b/controllers/snapshot_controller.go @@ -19,18 +19,25 @@ package controllers import ( "context" + corev1 "k8s.io/api/core/v1" + "k8s.io/apimachinery/pkg/api/errors" "k8s.io/apimachinery/pkg/runtime" + "k8s.io/client-go/tools/record" ctrl "sigs.k8s.io/controller-runtime" "sigs.k8s.io/controller-runtime/pkg/client" - "sigs.k8s.io/controller-runtime/pkg/log" + ctrllog "sigs.k8s.io/controller-runtime/pkg/log" + "sigs.k8s.io/controller-runtime/pkg/predicate" ndbv1alpha1 "github.com/nutanix-cloud-native/ndb-operator/api/v1alpha1" + "github.com/nutanix-cloud-native/ndb-operator/common/util" + "github.com/nutanix-cloud-native/ndb-operator/ndb_client" ) // SnapshotReconciler reconciles a Snapshot object type SnapshotReconciler struct { client.Client - Scheme *runtime.Scheme + Scheme *runtime.Scheme + recorder record.EventRecorder } //+kubebuilder:rbac:groups=ndb.nutanix.com,resources=snapshots,verbs=get;list;watch;create;update;patch;delete @@ -47,16 +54,62 @@ type SnapshotReconciler struct { // For more details, check Reconcile and its Result here: // - https://pkg.go.dev/sigs.k8s.io/controller-runtime@v0.14.1/pkg/reconcile func (r *SnapshotReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) { - _ = log.FromContext(ctx) + log := ctrllog.FromContext(ctx) + log.Info("<==============================Reconcile Started=============================>") + // Fetch the snapshot from the namespace + snapshot := &ndbv1alpha1.Snapshot{} + err := r.Get(ctx, req.NamespacedName, snapshot) + if err != nil { + if errors.IsNotFound(err) { + // Request object not found, could have been deleted after reconcile request. + // Return and don't requeue + log.Info("Snapshot not found. Ignoring since object must be deleted") + return doNotRequeue() + } + // Error reading the object - requeue the request. + log.Error(err, "Failed to get Snapshot") + return requeueOnErr(err) + } - // TODO(user): your logic here + log.Info("Snapshot Status: " + util.ToString(snapshot.Status)) - return ctrl.Result{}, nil + // Fetch the NDBServer resource from the namespace + ndbServer := &ndbv1alpha1.NDBServer{} + err = r.Get(ctx, req.NamespacedName, ndbServer) + if err != nil { + if errors.IsNotFound(err) { + // Request object not found, could have been deleted after reconcile request. + // Return and don't requeue + log.Info("NDBServer resource not found. Ignoring since object must be deleted") + return doNotRequeue() + } + // Error reading the object - requeue the request. + log.Error(err, "Failed to get NDB") + return requeueOnErr(err) + } + + NDBInfo := ndbServer.Spec + username, password, caCert, err := getNDBCredentialsFromSecret(ctx, r.Client, NDBInfo.CredentialSecret, req.Namespace) + if err != nil { + r.recorder.Eventf(snapshot, "Warning", EVENT_INVALID_CREDENTIALS, "Error: %s", err.Error()) + return requeueOnErr(err) + } + if caCert == "" { + log.Info("Ca-cert not found, falling back to host's HTTPs certs.") + } + ndbClient := ndb_client.NewNDBClient(username, password, NDBInfo.Server, caCert, NDBInfo.SkipCertificateVerification) + + return r.handleSync(ctx, snapshot, ndbClient, req, ndbServer) } // SetupWithManager sets up the controller with the Manager. func (r *SnapshotReconciler) SetupWithManager(mgr ctrl.Manager) error { + //Create a new EventRecorder with the provided name + r.recorder = mgr.GetEventRecorderFor("snapshot-controller") return ctrl.NewControllerManagedBy(mgr). For(&ndbv1alpha1.Snapshot{}). + WithEventFilter(predicate.GenerationChangedPredicate{}). + Owns(&corev1.Service{}). + Owns(&corev1.Endpoints{}). Complete(r) } diff --git a/controllers/snapshot_controller_helper.go b/controllers/snapshot_controller_helper.go new file mode 100644 index 00000000..2d329367 --- /dev/null +++ b/controllers/snapshot_controller_helper.go @@ -0,0 +1 @@ +package controllers diff --git a/ndb_api/snapshots_helper.go b/ndb_api/snapshots_helper.go new file mode 100644 index 00000000..d56f20c1 --- /dev/null +++ b/ndb_api/snapshots_helper.go @@ -0,0 +1,19 @@ +/* +Copyright 2022-2023 Nutanix, Inc. +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 ndb_api + +// Returns a request to delete a snapshot instance +func GenerateDeprovisionCloneRequest() (req *CloneDeprovisionRequest) { + +} From 58f303be544dcaa9e8704153c834e30cd277ba22 Mon Sep 17 00:00:00 2001 From: rdhamal Date: Thu, 30 Nov 2023 18:28:27 -0500 Subject: [PATCH 07/17] Changed SnapshotSpec --- api/v1alpha1/snapshot_types.go | 2 - config/samples/ndb_v1alpha1_snapshot.yaml | 4 +- controllers/snapshot_controller_helper.go | 81 +++++++++++++++++++++++ ndb_api/snapshot.go | 2 +- ndb_api/snapshot_request_types.go | 2 +- ndb_api/snapshots_helper.go | 7 +- 6 files changed, 91 insertions(+), 7 deletions(-) diff --git a/api/v1alpha1/snapshot_types.go b/api/v1alpha1/snapshot_types.go index 5a69ba6c..a27c899d 100644 --- a/api/v1alpha1/snapshot_types.go +++ b/api/v1alpha1/snapshot_types.go @@ -30,8 +30,6 @@ type SnapshotSpec struct { // Foo is an example field of Snapshot. Edit snapshot_types.go to remove/update IP string `json:"ip,omitempty"` - Username string `json:"username,omitempty"` - Password string `json:"password,omitempty"` TimeMachineID string `json:"timemachineid,omitempty"` Name string `json:"name,omitempty"` } diff --git a/config/samples/ndb_v1alpha1_snapshot.yaml b/config/samples/ndb_v1alpha1_snapshot.yaml index 8b71d894..cc458831 100644 --- a/config/samples/ndb_v1alpha1_snapshot.yaml +++ b/config/samples/ndb_v1alpha1_snapshot.yaml @@ -9,4 +9,6 @@ metadata: app.kubernetes.io/created-by: ndb-operator name: snapshot-sample spec: - # TODO(user): Add fields here + ip: "ndb-snapshot.free.beeceptor.com" + timemachineid: "123" + name: "test-snapshot" diff --git a/controllers/snapshot_controller_helper.go b/controllers/snapshot_controller_helper.go index 2d329367..7296dfcb 100644 --- a/controllers/snapshot_controller_helper.go +++ b/controllers/snapshot_controller_helper.go @@ -1 +1,82 @@ package controllers + +import ( + "context" + "fmt" + "reflect" + + ndbv1alpha1 "github.com/nutanix-cloud-native/ndb-operator/api/v1alpha1" + "github.com/nutanix-cloud-native/ndb-operator/common" + "github.com/nutanix-cloud-native/ndb-operator/ndb_api" + "github.com/nutanix-cloud-native/ndb-operator/ndb_client" + ctrl "sigs.k8s.io/controller-runtime" + ctrllog "sigs.k8s.io/controller-runtime/pkg/log" +) + +// The handleSync function synchronizes the database CR with the database info object in the +// NDBServer CR (which fetches it from NDB). It handles the transition from EMPTY (initial state) => WAITING => PROVISIONING => RUNNING +// and updates the status accordingly. The update() triggers an implicit requeue of the reconcile request. +func (r *SnapshotReconciler) handleSync(ctx context.Context, snapshot *ndbv1alpha1.Snapshot, ndbClient *ndb_client.NDBClient, req ctrl.Request, ndbServer *ndbv1alpha1.NDBServer) (ctrl.Result, error) { + log := ctrllog.FromContext(ctx) + log.Info("Entered snapshot_controller_helper.handleSync") + + snapshotStatus := snapshot.Status.DeepCopy() + + // Take a snapshot + if snapshotStatus.Status == "" { + // DB Status.Status is empty => Provision a DB + req := ndb_api.GenerateTakeSnapshotRequest(snapshot.Spec.TimeMachineID) + taskResponse, err := ndb_api.TakeSnapshot(ctx, ndbClient, req) + if err != nil { + errStatement := "Failed to create snapshot of database" + log.Error(err, errStatement) + r.recorder.Eventf(snapshot, "Warning", EVENT_NDB_REQUEST_FAILED, "Error: %s. %s", errStatement, err.Error()) + return requeueOnErr(err) + } + log.Info(fmt.Sprintf("Updating Snapshot CR to Status: CREATING, id: %s and creationOperationId: %s", taskResponse.EntityId, taskResponse.OperationId)) + + snapshotStatus.Status = common.DATABASE_CR_STATUS_CREATING + snapshotStatus.OperationID = taskResponse.OperationId + r.recorder.Event(snapshot, "Normal", EVENT_CREATION_STARTED, "Snapshot creation initiated") + } + + isUnderDeletion := !snapshot.ObjectMeta.DeletionTimestamp.IsZero() + if isUnderDeletion { + snapshotStatus.Status = common.DATABASE_CR_STATUS_DELETING + } else if snapshotStatus.Status == common.DATABASE_CR_STATUS_CREATING { + creationOp, err := ndb_api.GetOperationById(ctx, ndbClient, snapshotStatus.OperationID) + if err != nil { + message := fmt.Sprintf("NDB API to fetch operation by id failed. OperationId: %s:, error: %s", creationOp.Id, err.Error()) + r.recorder.Event(snapshot, "Warning", EVENT_NDB_REQUEST_FAILED, message) + } else { + switch ndb_api.GetOperationStatus(creationOp) { + case ndb_api.OPERATION_STATUS_FAILED: + snapshotStatus.Status = common.DATABASE_CR_STATUS_CREATION_ERROR + err = fmt.Errorf("creation operation terminated. status: %s, message: %s, operationId: %s", creationOp.Status, creationOp.Message, creationOp.Id) + log.Error(err, "Database Creation Failed") + r.recorder.Event(snapshot, "Warning", EVENT_CREATION_FAILED, "Take Snapshot operation failed with error: "+err.Error()) + case ndb_api.OPERATION_STATUS_PASSED: + snapshotStatus.Status = common.DATABASE_CR_STATUS_READY + r.recorder.Event(snapshot, "Normal", EVENT_CREATION_COMPLETED, "Take Snapshot operation passed") + default: + // Do nothing, we do not care about other statuses + } + } + } else { + log.Info("Snapshot missing from NDB CR") + snapshotStatus.Status = common.DATABASE_CR_STATUS_NOT_FOUND + } + + if !reflect.DeepEqual(snapshot.Status, *snapshotStatus) { + snapshot.Status = *snapshotStatus + err := r.Status().Update(ctx, snapshot) + if err != nil { + errStatement := "Failed to update status of snapshot custom resource" + log.Error(err, errStatement) + r.recorder.Eventf(snapshot, "Warning", EVENT_CR_STATUS_UPDATE_FAILED, "Error: %s. %s.", err.Error()) + return requeueOnErr(err) + } + } + + return requeueWithTimeout(common.DATABASE_RECONCILE_INTERVAL_SECONDS) +} diff --git a/ndb_api/snapshot.go b/ndb_api/snapshot.go index ec51e930..efcbe293 100644 --- a/ndb_api/snapshot.go +++ b/ndb_api/snapshot.go @@ -168,7 +168,7 @@ func DeleteSnapshot(ctx context.Context, ndbClient *ndb_client.NDBClient, id str log.Error(err, "no snapshot id provided") return } - res, err := ndbClient.Delete("snapshots/" + id) + res, err := ndbClient.Delete("snapshots/"+id, nil) if err != nil || res == nil || res.StatusCode != http.StatusOK { if err == nil { if res != nil { diff --git a/ndb_api/snapshot_request_types.go b/ndb_api/snapshot_request_types.go index db7ea1db..d1559cc7 100644 --- a/ndb_api/snapshot_request_types.go +++ b/ndb_api/snapshot_request_types.go @@ -3,7 +3,7 @@ package ndb_api type SnapshotRequest struct { Name string `json:"name"` SnapshotLcmConfig SnapshotLcmConfig `json:"lcmConfig"` - TimeMachineId string `json:'timemachine_id'` + TimeMachineId string `json:"timemachineId"` } type SnapshotLcmConfig struct { diff --git a/ndb_api/snapshots_helper.go b/ndb_api/snapshots_helper.go index d56f20c1..18f8891b 100644 --- a/ndb_api/snapshots_helper.go +++ b/ndb_api/snapshots_helper.go @@ -14,6 +14,9 @@ limitations under the License. package ndb_api // Returns a request to delete a snapshot instance -func GenerateDeprovisionCloneRequest() (req *CloneDeprovisionRequest) { - +func GenerateTakeSnapshotRequest(timeMachineId string) (req *SnapshotRequest) { + req = &SnapshotRequest{ + TimeMachineId: timeMachineId, + } + return } From 59927a57281020999c4c407b64348df8cf884025 Mon Sep 17 00:00:00 2001 From: rdhamal Date: Thu, 30 Nov 2023 18:32:16 -0500 Subject: [PATCH 08/17] Spec Changes after manifests and generate --- config/crd/bases/ndb.nutanix.com_snapshots.yaml | 4 ---- 1 file changed, 4 deletions(-) diff --git a/config/crd/bases/ndb.nutanix.com_snapshots.yaml b/config/crd/bases/ndb.nutanix.com_snapshots.yaml index d7c7bb3b..a468e730 100644 --- a/config/crd/bases/ndb.nutanix.com_snapshots.yaml +++ b/config/crd/bases/ndb.nutanix.com_snapshots.yaml @@ -41,12 +41,8 @@ spec: type: string name: type: string - password: - type: string timemachineid: type: string - username: - type: string type: object status: description: SnapshotStatus defines the observed state of Snapshot From fb731fa5a0b672463015b1e9427d387edf2c8eb1 Mon Sep 17 00:00:00 2001 From: rdhamal Date: Fri, 1 Dec 2023 14:55:29 -0500 Subject: [PATCH 09/17] Implemented deletion logic --- api/v1alpha1/snapshot_types.go | 9 ++- .../crd/bases/ndb.nutanix.com_snapshots.yaml | 4 + config/samples/ndb_v1alpha1_snapshot.yaml | 14 ---- controllers/snapshot_controller.go | 4 +- controllers/snapshot_controller_helper.go | 80 ++++++++++++++++++- ndb_api/snapshot.go | 6 +- ndb_api/snapshots_helper.go | 16 +++- 7 files changed, 107 insertions(+), 26 deletions(-) delete mode 100644 config/samples/ndb_v1alpha1_snapshot.yaml diff --git a/api/v1alpha1/snapshot_types.go b/api/v1alpha1/snapshot_types.go index a27c899d..abdf26f0 100644 --- a/api/v1alpha1/snapshot_types.go +++ b/api/v1alpha1/snapshot_types.go @@ -29,9 +29,11 @@ type SnapshotSpec struct { // Important: Run "make" to regenerate code after modifying this file // Foo is an example field of Snapshot. Edit snapshot_types.go to remove/update - IP string `json:"ip,omitempty"` - TimeMachineID string `json:"timemachineid,omitempty"` - Name string `json:"name,omitempty"` + IP string `json:"ip,omitempty"` + TimeMachineID string `json:"timemachineid,omitempty"` + Name string `json:"name,omitempty"` + ExpiryDateTimezone string `json:"expirydatetimezone,omitempty"` + ExpireInDays string `json:"expireindays,omitempty"` } // SnapshotStatus defines the observed state of Snapshot @@ -40,6 +42,7 @@ type SnapshotStatus struct { // Important: Run "make" to regenerate code after modifying this file OperationID string `json:"operationid,omitempty"` Status string `json:"status,omitempty"` + Id string `json:"id,omitempty"` } //+kubebuilder:object:root=true diff --git a/config/crd/bases/ndb.nutanix.com_snapshots.yaml b/config/crd/bases/ndb.nutanix.com_snapshots.yaml index a468e730..284dfd45 100644 --- a/config/crd/bases/ndb.nutanix.com_snapshots.yaml +++ b/config/crd/bases/ndb.nutanix.com_snapshots.yaml @@ -35,6 +35,10 @@ spec: spec: description: SnapshotSpec defines the desired state of Snapshot properties: + expireindays: + type: string + expirydatetimezone: + type: string ip: description: Foo is an example field of Snapshot. Edit snapshot_types.go to remove/update diff --git a/config/samples/ndb_v1alpha1_snapshot.yaml b/config/samples/ndb_v1alpha1_snapshot.yaml deleted file mode 100644 index cc458831..00000000 --- a/config/samples/ndb_v1alpha1_snapshot.yaml +++ /dev/null @@ -1,14 +0,0 @@ -apiVersion: ndb.nutanix.com/v1alpha1 -kind: Snapshot -metadata: - labels: - app.kubernetes.io/name: snapshot - app.kubernetes.io/instance: snapshot-sample - app.kubernetes.io/part-of: ndb-operator - app.kubernetes.io/managed-by: kustomize - app.kubernetes.io/created-by: ndb-operator - name: snapshot-sample -spec: - ip: "ndb-snapshot.free.beeceptor.com" - timemachineid: "123" - name: "test-snapshot" diff --git a/controllers/snapshot_controller.go b/controllers/snapshot_controller.go index 4f235f51..871154f8 100644 --- a/controllers/snapshot_controller.go +++ b/controllers/snapshot_controller.go @@ -75,7 +75,9 @@ func (r *SnapshotReconciler) Reconcile(ctx context.Context, req ctrl.Request) (c // Fetch the NDBServer resource from the namespace ndbServer := &ndbv1alpha1.NDBServer{} - err = r.Get(ctx, req.NamespacedName, ndbServer) + ndbNamespacedName := req.NamespacedName + ndbNamespacedName.Name = "ndb" + err = r.Get(ctx, ndbNamespacedName, ndbServer) if err != nil { if errors.IsNotFound(err) { // Request object not found, could have been deleted after reconcile request. diff --git a/controllers/snapshot_controller_helper.go b/controllers/snapshot_controller_helper.go index 7296dfcb..f7767dcb 100644 --- a/controllers/snapshot_controller_helper.go +++ b/controllers/snapshot_controller_helper.go @@ -10,6 +10,7 @@ import ( "github.com/nutanix-cloud-native/ndb-operator/ndb_api" "github.com/nutanix-cloud-native/ndb-operator/ndb_client" ctrl "sigs.k8s.io/controller-runtime" + "sigs.k8s.io/controller-runtime/pkg/controller/controllerutil" ctrllog "sigs.k8s.io/controller-runtime/pkg/log" ) @@ -18,15 +19,15 @@ import ( // and updates the status accordingly. The update() triggers an implicit requeue of the reconcile request. func (r *SnapshotReconciler) handleSync(ctx context.Context, snapshot *ndbv1alpha1.Snapshot, ndbClient *ndb_client.NDBClient, req ctrl.Request, ndbServer *ndbv1alpha1.NDBServer) (ctrl.Result, error) { log := ctrllog.FromContext(ctx) - log.Info("Entered snapshot_controller_helper.handleSync") + log.Info("Entered snapshot_conxtroller_helper.handleSync") snapshotStatus := snapshot.Status.DeepCopy() // Take a snapshot if snapshotStatus.Status == "" { // DB Status.Status is empty => Provision a DB - req := ndb_api.GenerateTakeSnapshotRequest(snapshot.Spec.TimeMachineID) - taskResponse, err := ndb_api.TakeSnapshot(ctx, ndbClient, req) + requestBody := ndb_api.GenerateTakeSnapshotRequest(snapshot) + taskResponse, err := ndb_api.TakeSnapshot(ctx, ndbClient, requestBody, snapshot.Spec.TimeMachineID) if err != nil { errStatement := "Failed to create snapshot of database" log.Error(err, errStatement) @@ -37,6 +38,7 @@ func (r *SnapshotReconciler) handleSync(ctx context.Context, snapshot *ndbv1alph snapshotStatus.Status = common.DATABASE_CR_STATUS_CREATING snapshotStatus.OperationID = taskResponse.OperationId + snapshotStatus.Id = taskResponse.EntityId r.recorder.Event(snapshot, "Normal", EVENT_CREATION_STARTED, "Snapshot creation initiated") } @@ -78,5 +80,77 @@ func (r *SnapshotReconciler) handleSync(ctx context.Context, snapshot *ndbv1alph } } + switch snapshotStatus.Status { + case common.DATABASE_CR_STATUS_READY: + if !isUnderDeletion { + if !controllerutil.ContainsFinalizer(database, common.FINALIZER_INSTANCE) { + return r.addFinalizer(ctx, req, common.FINALIZER_INSTANCE, database) + } + if !controllerutil.ContainsFinalizer(database, common.FINALIZER_DATABASE_SERVER) { + return r.addFinalizer(ctx, req, common.FINALIZER_DATABASE_SERVER, database) + } + } + if databaseStatus.IPAddress != "" { + r.setupConnectivity(ctx, database, req) + } else { + // The database is in "READY" state on NDB, but the API responses sometimes do not have + // an IP address in the response right after reaching the READY state. We only setup connectivity + // once we have a non-empty IP Address. Just logging and raising an event to notify the user. + message := fmt.Sprintf("Empty IP Address for Database %s, will setup connectivity once the IP address is assigned", database.Name) + log.Info(message) + r.recorder.Event(database, "Warning", EVENT_WAITING_FOR_IP_ADDRESS, message) + } + case common.DATABASE_CR_STATUS_DELETING: + snapshotStatus.operationId = "" + return r.handleDelete(ctx, snapshot, ndbClient) + case common.DATABASE_CR_STATUS_NOT_FOUND: + r.recorder.Eventf(snapshot, "Warning", EVENT_EXTERNAL_DELETE, "Error: Resource not found on NDB") + case common.DATABASE_CR_STATUS_CREATION_ERROR: + return doNotRequeue() + default: + // No-Op + } + + return requeueWithTimeout(common.DATABASE_RECONCILE_INTERVAL_SECONDS) +} + +func (r *SnapshotReconciler) handleDelete(ctx context.Context, snapshot *ndbv1alpha1.Snapshot, ndbClient *ndb_client.NDBClient) (ctrl.Result, error) { + log := ctrllog.FromContext(ctx) + log.Info("Snapshot CR is being deleted") + log.Info(snapshot.ResourceVersion) + deleteOperationId := snapshot.Status.OperationID + if deleteOperationId == "" { + deleteOp, err := ndb_api.DeleteSnapshot(ctx, ndbClient, snapshot.Status.Id) + if err != nil { + // Not logging here, already done in the deregister function + return requeueOnErr(err) + } + snapshot.Status.OperationId = deleteOp.OperationId + if err := r.Status().Update(ctx, snapshot); err != nil { + log.Error(err, "An error occurred while updating the CR.") + return requeueOnErr(err) + } + } else { + deleteOp, err := ndb_api.GetOperationById(ctx, ndbClient, deleteOperationId) + if err != nil { + message := fmt.Sprintf("NDB API to fetch operation by id failed. OperationId: %s:, error: %s", &deleteOperationId, err.Error()) + r.recorder.Event(snapshot, "Warning", EVENT_NDB_REQUEST_FAILED, message) + } else { + switch ndb_api.GetOperationStatus(deleteOp) { + case ndb_api.OPERATION_STATUS_FAILED: + err := fmt.Errorf("Delete operation terminated. status: %s, message: %s, operationId: %s", deleteOp.Status, deleteOp.Message, deleteOperationId) + log.Error(err, "Deletion Failed") + r.recorder.Event(snapshot, "Warning", "OPERATION FAILED", "Snapshot deletion operation failed with error: "+err.Error()) + case ndb_api.OPERATION_STATUS_PASSED: + r.recorder.Eventf(snapshot, "Normal", EVENT_DEREGISTRATION_COMPLETED, "Snapshot deleted from NDB.") + if err := r.Update(ctx, snapshot); err != nil { + return requeueOnErr(err) + } + default: + // Do nothing, we do not care about other statuses + } + } + } + // Requeue the request while waiting for the database instance to be deleted from NDB. return requeueWithTimeout(common.DATABASE_RECONCILE_INTERVAL_SECONDS) } diff --git a/ndb_api/snapshot.go b/ndb_api/snapshot.go index efcbe293..87132135 100644 --- a/ndb_api/snapshot.go +++ b/ndb_api/snapshot.go @@ -111,7 +111,7 @@ func GetSnapshotById(ctx context.Context, ndbClient *ndb_client.NDBClient, id st // Takes a snapshot of the database upon request // Returns the task info summary response for the operation -func TakeSnapshot(ctx context.Context, ndbClient *ndb_client.NDBClient, req *SnapshotRequest) (task TaskInfoSummaryResponse, err error) { +func TakeSnapshot(ctx context.Context, ndbClient *ndb_client.NDBClient, req *SnapshotRequest, timeMachineId string) (task TaskInfoSummaryResponse, err error) { log := ctrllog.FromContext(ctx) log.Info("Entered ndb_api.TakeSnapshot") if ndbClient == nil { @@ -119,12 +119,12 @@ func TakeSnapshot(ctx context.Context, ndbClient *ndb_client.NDBClient, req *Sna log.Error(err, "Received nil ndbClient reference") return } - if req.TimeMachineId == "" { + if timeMachineId == "" { err = errors.New("empty timeMachineId") log.Error(err, "Received empty timeMachineId in request") return } - snapshotEndPoint := "tms/" + req.TimeMachineId + "/snapshots" + snapshotEndPoint := "tms/" + timeMachineId + "/snapshots" res, err := ndbClient.Post(snapshotEndPoint, req) if err != nil || res == nil || res.StatusCode != http.StatusOK { if err == nil { diff --git a/ndb_api/snapshots_helper.go b/ndb_api/snapshots_helper.go index 18f8891b..08410c22 100644 --- a/ndb_api/snapshots_helper.go +++ b/ndb_api/snapshots_helper.go @@ -13,10 +13,22 @@ limitations under the License. package ndb_api +import ( + ndbv1alpha1 "github.com/nutanix-cloud-native/ndb-operator/api/v1alpha1" +) + // Returns a request to delete a snapshot instance -func GenerateTakeSnapshotRequest(timeMachineId string) (req *SnapshotRequest) { +func GenerateTakeSnapshotRequest(snapshot *ndbv1alpha1.Snapshot) (req *SnapshotRequest) { req = &SnapshotRequest{ - TimeMachineId: timeMachineId, + Name: snapshot.Spec.Name, + SnapshotLcmConfig: SnapshotLcmConfig{ + SnapshotLCMConfigDetailed: SnapshotLcmConfigDetailed{ + ExpiryDetails: ExpiryDetails{ + ExpiryDateTimezone: snapshot.Spec.ExpiryDateTimezone, + ExpireInDays: snapshot.Spec.ExpireInDays, + }, + }, + }, } return } From c571618bf606da0681211fc1e7badfee9d69159b Mon Sep 17 00:00:00 2001 From: rdhamal Date: Fri, 1 Dec 2023 16:22:51 -0500 Subject: [PATCH 10/17] Snapshot creation implemented --- api/v1alpha1/snapshot_types.go | 2 +- .../crd/bases/ndb.nutanix.com_snapshots.yaml | 4 ++- controllers/snapshot_controller_helper.go | 27 +++---------------- ndb_api/snapshot_request_types.go | 8 ++++-- ndb_api/snapshots_helper.go | 2 +- ndb_api/time_machine.go | 2 +- ndb_api/time_machine_helpers.go | 4 +-- 7 files changed, 18 insertions(+), 31 deletions(-) diff --git a/api/v1alpha1/snapshot_types.go b/api/v1alpha1/snapshot_types.go index abdf26f0..95da0f23 100644 --- a/api/v1alpha1/snapshot_types.go +++ b/api/v1alpha1/snapshot_types.go @@ -33,7 +33,7 @@ type SnapshotSpec struct { TimeMachineID string `json:"timemachineid,omitempty"` Name string `json:"name,omitempty"` ExpiryDateTimezone string `json:"expirydatetimezone,omitempty"` - ExpireInDays string `json:"expireindays,omitempty"` + ExpireInDays int `json:"expireindays,omitempty"` } // SnapshotStatus defines the observed state of Snapshot diff --git a/config/crd/bases/ndb.nutanix.com_snapshots.yaml b/config/crd/bases/ndb.nutanix.com_snapshots.yaml index 284dfd45..db952624 100644 --- a/config/crd/bases/ndb.nutanix.com_snapshots.yaml +++ b/config/crd/bases/ndb.nutanix.com_snapshots.yaml @@ -36,7 +36,7 @@ spec: description: SnapshotSpec defines the desired state of Snapshot properties: expireindays: - type: string + type: integer expirydatetimezone: type: string ip: @@ -51,6 +51,8 @@ spec: status: description: SnapshotStatus defines the observed state of Snapshot properties: + id: + type: string operationid: description: 'INSERT ADDITIONAL STATUS FIELD - define observed state of cluster Important: Run "make" to regenerate code after modifying diff --git a/controllers/snapshot_controller_helper.go b/controllers/snapshot_controller_helper.go index f7767dcb..c90dd5d1 100644 --- a/controllers/snapshot_controller_helper.go +++ b/controllers/snapshot_controller_helper.go @@ -10,7 +10,6 @@ import ( "github.com/nutanix-cloud-native/ndb-operator/ndb_api" "github.com/nutanix-cloud-native/ndb-operator/ndb_client" ctrl "sigs.k8s.io/controller-runtime" - "sigs.k8s.io/controller-runtime/pkg/controller/controllerutil" ctrllog "sigs.k8s.io/controller-runtime/pkg/log" ) @@ -81,27 +80,9 @@ func (r *SnapshotReconciler) handleSync(ctx context.Context, snapshot *ndbv1alph } switch snapshotStatus.Status { - case common.DATABASE_CR_STATUS_READY: - if !isUnderDeletion { - if !controllerutil.ContainsFinalizer(database, common.FINALIZER_INSTANCE) { - return r.addFinalizer(ctx, req, common.FINALIZER_INSTANCE, database) - } - if !controllerutil.ContainsFinalizer(database, common.FINALIZER_DATABASE_SERVER) { - return r.addFinalizer(ctx, req, common.FINALIZER_DATABASE_SERVER, database) - } - } - if databaseStatus.IPAddress != "" { - r.setupConnectivity(ctx, database, req) - } else { - // The database is in "READY" state on NDB, but the API responses sometimes do not have - // an IP address in the response right after reaching the READY state. We only setup connectivity - // once we have a non-empty IP Address. Just logging and raising an event to notify the user. - message := fmt.Sprintf("Empty IP Address for Database %s, will setup connectivity once the IP address is assigned", database.Name) - log.Info(message) - r.recorder.Event(database, "Warning", EVENT_WAITING_FOR_IP_ADDRESS, message) - } + case common.DATABASE_CR_STATUS_DELETING: - snapshotStatus.operationId = "" + snapshotStatus.OperationID = "" return r.handleDelete(ctx, snapshot, ndbClient) case common.DATABASE_CR_STATUS_NOT_FOUND: r.recorder.Eventf(snapshot, "Warning", EVENT_EXTERNAL_DELETE, "Error: Resource not found on NDB") @@ -125,7 +106,7 @@ func (r *SnapshotReconciler) handleDelete(ctx context.Context, snapshot *ndbv1al // Not logging here, already done in the deregister function return requeueOnErr(err) } - snapshot.Status.OperationId = deleteOp.OperationId + snapshot.Status.OperationID = deleteOp.OperationId if err := r.Status().Update(ctx, snapshot); err != nil { log.Error(err, "An error occurred while updating the CR.") return requeueOnErr(err) @@ -133,7 +114,7 @@ func (r *SnapshotReconciler) handleDelete(ctx context.Context, snapshot *ndbv1al } else { deleteOp, err := ndb_api.GetOperationById(ctx, ndbClient, deleteOperationId) if err != nil { - message := fmt.Sprintf("NDB API to fetch operation by id failed. OperationId: %s:, error: %s", &deleteOperationId, err.Error()) + message := fmt.Sprintf("NDB API to fetch operation by id failed. OperationId: %s:, error: %s", deleteOperationId, err.Error()) r.recorder.Event(snapshot, "Warning", EVENT_NDB_REQUEST_FAILED, message) } else { switch ndb_api.GetOperationStatus(deleteOp) { diff --git a/ndb_api/snapshot_request_types.go b/ndb_api/snapshot_request_types.go index d1559cc7..c0888607 100644 --- a/ndb_api/snapshot_request_types.go +++ b/ndb_api/snapshot_request_types.go @@ -3,7 +3,6 @@ package ndb_api type SnapshotRequest struct { Name string `json:"name"` SnapshotLcmConfig SnapshotLcmConfig `json:"lcmConfig"` - TimeMachineId string `json:"timemachineId"` } type SnapshotLcmConfig struct { @@ -11,5 +10,10 @@ type SnapshotLcmConfig struct { } type SnapshotLcmConfigDetailed struct { - ExpiryDetails ExpiryDetails `json:"expiryDetails"` + ExpiryDetails SnapshotExpiryDetails `json:"expiryDetails"` +} + +type SnapshotExpiryDetails struct { + ExpiryDateTimezone string `json:"expiryDateTimezone"` + ExpireInDays int `json:"expireInDays"` } diff --git a/ndb_api/snapshots_helper.go b/ndb_api/snapshots_helper.go index 08410c22..f7d90949 100644 --- a/ndb_api/snapshots_helper.go +++ b/ndb_api/snapshots_helper.go @@ -23,7 +23,7 @@ func GenerateTakeSnapshotRequest(snapshot *ndbv1alpha1.Snapshot) (req *SnapshotR Name: snapshot.Spec.Name, SnapshotLcmConfig: SnapshotLcmConfig{ SnapshotLCMConfigDetailed: SnapshotLcmConfigDetailed{ - ExpiryDetails: ExpiryDetails{ + ExpiryDetails: SnapshotExpiryDetails{ ExpiryDateTimezone: snapshot.Spec.ExpiryDateTimezone, ExpireInDays: snapshot.Spec.ExpireInDays, }, diff --git a/ndb_api/time_machine.go b/ndb_api/time_machine.go index 8e08df4d..53dc0b17 100644 --- a/ndb_api/time_machine.go +++ b/ndb_api/time_machine.go @@ -33,7 +33,7 @@ func CreateSnapshotForTM( tmName string, snapshotName string, expiryDateTimezone string, - ExpireInDays string) (task TaskInfoSummaryResponse, err error) { + ExpireInDays int) (task TaskInfoSummaryResponse, err error) { log := ctrllog.FromContext(ctx) log.Info("Entered ndb_api.CreateSnapshotForTM") diff --git a/ndb_api/time_machine_helpers.go b/ndb_api/time_machine_helpers.go index 07dded46..74d2ca50 100644 --- a/ndb_api/time_machine_helpers.go +++ b/ndb_api/time_machine_helpers.go @@ -1,11 +1,11 @@ package ndb_api -func GenerateSnapshotRequest(name string, expiryDateTimezone string, ExpireInDays string) *SnapshotRequest { +func GenerateSnapshotRequest(name string, expiryDateTimezone string, ExpireInDays int) *SnapshotRequest { return &SnapshotRequest{ Name: name, SnapshotLcmConfig: SnapshotLcmConfig{ SnapshotLCMConfigDetailed: SnapshotLcmConfigDetailed{ - ExpiryDetails: ExpiryDetails{ + ExpiryDetails: SnapshotExpiryDetails{ ExpiryDateTimezone: expiryDateTimezone, ExpireInDays: ExpireInDays, }, From 5e0c549406e0b2b0736d3745da6b6ca138891975 Mon Sep 17 00:00:00 2001 From: rdhamal Date: Fri, 1 Dec 2023 23:17:02 -0500 Subject: [PATCH 11/17] Deletion implemented --- api/v1alpha1/snapshot_types.go | 7 +- .../crd/bases/ndb.nutanix.com_snapshots.yaml | 2 + controllers/snapshot_controller_helper.go | 129 +++++++++++++----- ndb_api/snapshot.go | 39 +++++- ndb_client/ndb_client.go | 22 ++- 5 files changed, 160 insertions(+), 39 deletions(-) diff --git a/api/v1alpha1/snapshot_types.go b/api/v1alpha1/snapshot_types.go index 95da0f23..4a97ea8d 100644 --- a/api/v1alpha1/snapshot_types.go +++ b/api/v1alpha1/snapshot_types.go @@ -40,9 +40,10 @@ type SnapshotSpec struct { type SnapshotStatus struct { // INSERT ADDITIONAL STATUS FIELD - define observed state of cluster // Important: Run "make" to regenerate code after modifying this file - OperationID string `json:"operationid,omitempty"` - Status string `json:"status,omitempty"` - Id string `json:"id,omitempty"` + OperationID string `json:"operationid,omitempty"` + DeletionOperationID string `json:"deletionoperationid,omitempty"` + Status string `json:"status,omitempty"` + Id string `json:"id,omitempty"` } //+kubebuilder:object:root=true diff --git a/config/crd/bases/ndb.nutanix.com_snapshots.yaml b/config/crd/bases/ndb.nutanix.com_snapshots.yaml index db952624..c4cf107c 100644 --- a/config/crd/bases/ndb.nutanix.com_snapshots.yaml +++ b/config/crd/bases/ndb.nutanix.com_snapshots.yaml @@ -51,6 +51,8 @@ spec: status: description: SnapshotStatus defines the observed state of Snapshot properties: + deletionoperationid: + type: string id: type: string operationid: diff --git a/controllers/snapshot_controller_helper.go b/controllers/snapshot_controller_helper.go index c90dd5d1..1475dd57 100644 --- a/controllers/snapshot_controller_helper.go +++ b/controllers/snapshot_controller_helper.go @@ -9,7 +9,9 @@ import ( "github.com/nutanix-cloud-native/ndb-operator/common" "github.com/nutanix-cloud-native/ndb-operator/ndb_api" "github.com/nutanix-cloud-native/ndb-operator/ndb_client" + "k8s.io/apimachinery/pkg/api/errors" ctrl "sigs.k8s.io/controller-runtime" + "sigs.k8s.io/controller-runtime/pkg/controller/controllerutil" ctrllog "sigs.k8s.io/controller-runtime/pkg/log" ) @@ -37,13 +39,27 @@ func (r *SnapshotReconciler) handleSync(ctx context.Context, snapshot *ndbv1alph snapshotStatus.Status = common.DATABASE_CR_STATUS_CREATING snapshotStatus.OperationID = taskResponse.OperationId - snapshotStatus.Id = taskResponse.EntityId r.recorder.Event(snapshot, "Normal", EVENT_CREATION_STARTED, "Snapshot creation initiated") } isUnderDeletion := !snapshot.ObjectMeta.DeletionTimestamp.IsZero() if isUnderDeletion { - snapshotStatus.Status = common.DATABASE_CR_STATUS_DELETING + if snapshotStatus.Status != common.DATABASE_CR_STATUS_DELETING { + snapshots, err := ndb_api.GetAllSnapshots(ctx, ndbClient) + if err != nil { + log.Error(err, "Unable to get snapshots") + r.recorder.Eventf(snapshot, "Warning", EVENT_NDB_REQUEST_FAILED, "Error:", "Unable to get snapshots", err.Error()) + return requeueOnErr(err) + } + for _, snap := range snapshots { + if snap.Name == snapshot.Spec.Name { + snapshotStatus.Id = snap.Id + snapshotStatus.Status = common.DATABASE_CR_STATUS_DELETING + log.Info(fmt.Sprintf("Snap %s with id %s", snap.Name, snap.Id)) + break + } + } + } } else if snapshotStatus.Status == common.DATABASE_CR_STATUS_CREATING { creationOp, err := ndb_api.GetOperationById(ctx, ndbClient, snapshotStatus.OperationID) if err != nil { @@ -80,9 +96,13 @@ func (r *SnapshotReconciler) handleSync(ctx context.Context, snapshot *ndbv1alph } switch snapshotStatus.Status { - + case common.DATABASE_CR_STATUS_READY: + if !isUnderDeletion { + if !controllerutil.ContainsFinalizer(snapshot, common.FINALIZER_INSTANCE) { + return r.addFinalizer(ctx, req, common.FINALIZER_INSTANCE, snapshot) + } + } case common.DATABASE_CR_STATUS_DELETING: - snapshotStatus.OperationID = "" return r.handleDelete(ctx, snapshot, ndbClient) case common.DATABASE_CR_STATUS_NOT_FOUND: r.recorder.Eventf(snapshot, "Warning", EVENT_EXTERNAL_DELETE, "Error: Resource not found on NDB") @@ -95,43 +115,86 @@ func (r *SnapshotReconciler) handleSync(ctx context.Context, snapshot *ndbv1alph return requeueWithTimeout(common.DATABASE_RECONCILE_INTERVAL_SECONDS) } +// handleDelete function handles the deletion of +// +// a. Snapshot +// b. Snapshot Finalizer func (r *SnapshotReconciler) handleDelete(ctx context.Context, snapshot *ndbv1alpha1.Snapshot, ndbClient *ndb_client.NDBClient) (ctrl.Result, error) { log := ctrllog.FromContext(ctx) - log.Info("Snapshot CR is being deleted") + log.Info(fmt.Sprintf("Snapshot CR is being deleted with id %s", snapshot.Status.Id)) log.Info(snapshot.ResourceVersion) - deleteOperationId := snapshot.Status.OperationID - if deleteOperationId == "" { - deleteOp, err := ndb_api.DeleteSnapshot(ctx, ndbClient, snapshot.Status.Id) - if err != nil { - // Not logging here, already done in the deregister function - return requeueOnErr(err) - } - snapshot.Status.OperationID = deleteOp.OperationId - if err := r.Status().Update(ctx, snapshot); err != nil { - log.Error(err, "An error occurred while updating the CR.") - return requeueOnErr(err) - } - } else { - deleteOp, err := ndb_api.GetOperationById(ctx, ndbClient, deleteOperationId) - if err != nil { - message := fmt.Sprintf("NDB API to fetch operation by id failed. OperationId: %s:, error: %s", deleteOperationId, err.Error()) - r.recorder.Event(snapshot, "Warning", EVENT_NDB_REQUEST_FAILED, message) + if controllerutil.ContainsFinalizer(snapshot, common.FINALIZER_INSTANCE) { + // Check if the deregistration operation id (database.Status.DeregistrationOperationId) is empty + // If so, then make a deprovisionDatabase API call to NDB + // else proceed check for the operation completion before removing finalizer. + deletionOperationId := snapshot.Status.DeletionOperationID + if deletionOperationId == "" { + deletionOp, err := ndb_api.DeleteSnapshot(ctx, ndbClient, snapshot.Status.Id) + if err != nil { + // Not logging here, already done in the deregister function + return requeueOnErr(err) + } + snapshot.Status.DeletionOperationID = deletionOp.OperationId + if err := r.Status().Update(ctx, snapshot); err != nil { + log.Error(err, "An error occurred while updating the CR.") + return requeueOnErr(err) + } } else { - switch ndb_api.GetOperationStatus(deleteOp) { - case ndb_api.OPERATION_STATUS_FAILED: - err := fmt.Errorf("Delete operation terminated. status: %s, message: %s, operationId: %s", deleteOp.Status, deleteOp.Message, deleteOperationId) - log.Error(err, "Deletion Failed") - r.recorder.Event(snapshot, "Warning", "OPERATION FAILED", "Snapshot deletion operation failed with error: "+err.Error()) - case ndb_api.OPERATION_STATUS_PASSED: - r.recorder.Eventf(snapshot, "Normal", EVENT_DEREGISTRATION_COMPLETED, "Snapshot deleted from NDB.") - if err := r.Update(ctx, snapshot); err != nil { - return requeueOnErr(err) + deletionOp, err := ndb_api.GetOperationById(ctx, ndbClient, deletionOperationId) + if err != nil { + message := fmt.Sprintf("NDB API to fetch operation by id failed. OperationId: %s:, error: %s", deletionOperationId, err.Error()) + r.recorder.Event(snapshot, "Warning", EVENT_NDB_REQUEST_FAILED, message) + } else { + switch ndb_api.GetOperationStatus(deletionOp) { + case ndb_api.OPERATION_STATUS_FAILED: + err := fmt.Errorf("Deletion operation terminated. status: %s, message: %s, operationId: %s", deletionOp.Status, deletionOp.Message, deletionOperationId) + log.Error(err, "Deletion Failed") + r.recorder.Event(snapshot, "Warning", "OPERATION FAILED", "Snapshot deletion operation failed with error: "+err.Error()) + case ndb_api.OPERATION_STATUS_PASSED: + r.recorder.Eventf(snapshot, "Normal", EVENT_DEREGISTRATION_COMPLETED, "Snapshot deleted from NDB.") + log.Info("Removing Finalizer " + common.FINALIZER_INSTANCE) + controllerutil.RemoveFinalizer(snapshot, common.FINALIZER_INSTANCE) + if err := r.Update(ctx, snapshot); err != nil { + return requeueOnErr(err) + } + log.Info("Removed Finalizer " + common.FINALIZER_INSTANCE) + default: + // Do nothing, we do not care about other statuses } - default: - // Do nothing, we do not care about other statuses } } + } else { + // Finalizer has been removed, no need to requeue + // CR will be deleted. + return doNotRequeue() } // Requeue the request while waiting for the database instance to be deleted from NDB. return requeueWithTimeout(common.DATABASE_RECONCILE_INTERVAL_SECONDS) } + +func (r *SnapshotReconciler) addFinalizer(ctx context.Context, req ctrl.Request, finalizer string, snapshot *ndbv1alpha1.Snapshot) (ctrl.Result, error) { + log := ctrllog.FromContext(ctx) + log.Info("Fetching the most recent version of the Snapshot CR") + err := r.Get(ctx, req.NamespacedName, snapshot) + if err != nil { + if errors.IsNotFound(err) { + // Request object not found, could have been deleted after reconcile request. + // Owned objects are automatically garbage collected. For additional cleanup logic use finalizers. + // Return and don't requeue + log.Info("Snapshot resource not found. Ignoring since object must be deleted") + return doNotRequeue() + } + // Error reading the object - requeue the request. + log.Error(err, "Failed to get Snapshot") + return requeueOnErr(err) + } + log.Info("Snapshot CR fetched. Adding finalizer " + finalizer) + controllerutil.AddFinalizer(snapshot, finalizer) + if err := r.Update(ctx, snapshot); err != nil { + return requeueOnErr(err) + } else { + log.Info("Added finalizer " + finalizer) + } + //Not requeuing as a successful update automatically triggers a reconcile. + return requeueWithTimeout(common.DATABASE_RECONCILE_INTERVAL_SECONDS) +} diff --git a/ndb_api/snapshot.go b/ndb_api/snapshot.go index 87132135..52f1d043 100644 --- a/ndb_api/snapshot.go +++ b/ndb_api/snapshot.go @@ -65,6 +65,43 @@ func GetAllSnapshots(ctx context.Context, ndbClient *ndb_client.NDBClient) (snap return } +// Fetches all snapshots for a timemachineID on the NDB instance and returns a slice of the snapshots +func GetAllSnapshotsByTM(ctx context.Context, ndbClient *ndb_client.NDBClient, timemachineId string) (snapshots []SnapshotResponse, err error) { + log := ctrllog.FromContext(ctx) + log.Info("Entered ndb_api.GetAllSnapshotsByTM") + if ndbClient == nil { + err = errors.New("nil reference: received nil reference for ndbClient") + log.Error(err, "Received nil ndbClient reference") + return + } + res, err := ndbClient.Get("tms/" + timemachineId + "/snapshots") + if err != nil || res == nil || res.StatusCode != http.StatusOK { + if err == nil { + if res != nil { + err = fmt.Errorf("GET /tms/%s/snapshots responded with %d", timemachineId, res.StatusCode) + } else { + err = fmt.Errorf("GET /tms/%s/snapshots responded with a nil response", timemachineId) + } + } + log.Error(err, "Error occurred fetching snapshots by time machine id %s", timemachineId) + return + } + log.Info("GET /tms/", timemachineId, "snapshots", "HTTP status code", res.StatusCode) + body, err := io.ReadAll(res.Body) + defer res.Body.Close() + if err != nil { + log.Error(err, "Error occurred reading response.Body in GetAllSnapshotsByTM") + return + } + err = json.Unmarshal(body, &snapshots) + if err != nil { + log.Error(err, "Error occurred trying to unmarshal.") + return + } + log.Info("Returning from ndb_api.GetAllSnapshotsByTM") + return +} + // Fetches and returns a snapshot by an Id func GetSnapshotById(ctx context.Context, ndbClient *ndb_client.NDBClient, id string) (snapshot SnapshotResponse, err error) { log := ctrllog.FromContext(ctx) @@ -157,7 +194,7 @@ func TakeSnapshot(ctx context.Context, ndbClient *ndb_client.NDBClient, req *Sna // Returns the task info summary response for the operation func DeleteSnapshot(ctx context.Context, ndbClient *ndb_client.NDBClient, id string) (task TaskInfoSummaryResponse, err error) { log := ctrllog.FromContext(ctx) - log.Info("Entered ndb_api.DeleteSnapshot", "SnapshotId", id) + log.Info("Entered ndb_api.DeleteSnapshot") if ndbClient == nil { err = errors.New("nil reference") log.Error(err, "Received nil ndbClient reference") diff --git a/ndb_client/ndb_client.go b/ndb_client/ndb_client.go index eb82f769..667ce1e2 100644 --- a/ndb_client/ndb_client.go +++ b/ndb_client/ndb_client.go @@ -62,7 +62,6 @@ func (ndbClient *NDBClient) Post(path string, body interface{}) (*http.Response, req, err := http.NewRequest(http.MethodPost, url, bytes.NewBuffer(payload)) req.Header.Add("Cookie", "eraAuth=eyJhbGciOiJSUzUxMiJ9") if err != nil { - // fmt.Println(err) return nil, err } req.SetBasicAuth(ndbClient.username, ndbClient.password) @@ -72,6 +71,25 @@ func (ndbClient *NDBClient) Post(path string, body interface{}) (*http.Response, func (ndbClient *NDBClient) Delete(path string, body interface{}) (*http.Response, error) { url := ndbClient.url + "/" + path + req, err := getDeleteHttpRequest(url, body, ndbClient) + if err != nil { + return nil, err + } + return ndbClient.client.Do(req) +} + +func getDeleteHttpRequest(url string, body interface{}, ndbClient *NDBClient) (*http.Request, error) { + if body == nil { + req, err := http.NewRequest(http.MethodDelete, url, nil) + req.Header.Add("Cookie", "eraAuth=eyJhbGciOiJSUzUxMiJ9") + if err != nil { + // fmt.Println(err) + return nil, err + } + req.SetBasicAuth(ndbClient.username, ndbClient.password) + req.Header.Add("Content-Type", "application/json; charset=utf-8") + return req, nil + } payload, _ := json.Marshal(body) req, err := http.NewRequest(http.MethodDelete, url, bytes.NewBuffer(payload)) req.Header.Add("Cookie", "eraAuth=eyJhbGciOiJSUzUxMiJ9") @@ -81,5 +99,5 @@ func (ndbClient *NDBClient) Delete(path string, body interface{}) (*http.Respons } req.SetBasicAuth(ndbClient.username, ndbClient.password) req.Header.Add("Content-Type", "application/json; charset=utf-8") - return ndbClient.client.Do(req) + return req, nil } From f93834ee4cc3a70822ec11d774237ac8fa2099ab Mon Sep 17 00:00:00 2001 From: pnprathima Date: Mon, 4 Dec 2023 10:23:51 -0500 Subject: [PATCH 12/17] Adding test cases --- test/ndb_api_test.go | 140 +++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 140 insertions(+) diff --git a/test/ndb_api_test.go b/test/ndb_api_test.go index 17308a85..b54a0dc1 100644 --- a/test/ndb_api_test.go +++ b/test/ndb_api_test.go @@ -90,3 +90,143 @@ func TestGetAllProfileThrowsErrorWhenClientReturnsNon200(t *testing.T) { t.Error("TestGetAllProfiles should return an error when client responds with non 200 status.") } } + +func TestGetAllSnapshots(t *testing.T) { + //Set + server := GetServerTestHelper(t) + defer server.Close() + ndb_client := ndb_client.NewNDBClient("username", "password", server.URL, "", true) + + //Test + value, _ := ndb_api.GetAllSnapshots(context.Background(), ndb_client) + t.Log(len(value)) + if len(value) == 0 { + t.Error("Could not fetch Snapshot profiles") + } +} + +func TestGetAllSnapshotsThrowsErrorWhenClientReturnsNon200(t *testing.T) { + //Set + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + if !checkAuthTestHelper(r) { + t.Errorf("Invalid Authentication Credentials") + } else { + w.WriteHeader(http.StatusNotFound) + } + })) + defer server.Close() + ndb_client := ndb_client.NewNDBClient("username", "password", server.URL, "", true) + + //Test + _, err := ndb_api.GetAllSnapshots(context.Background(), ndb_client) + if err == nil { + t.Error("GetAllSnapshots should return an error when client responds with non 200 status.") + } +} + +func TestGetSnapshotById(t *testing.T) { + //Set + server := GetServerTestHelper(t) + defer server.Close() + ndb_client := ndb_client.NewNDBClient("username", "password", server.URL, "", true) + + //Test + value, _ := ndb_api.GetSnapshotById(context.Background(), ndb_client, "id") + t.Log(value) + if value.Id != "id" { + t.Error("Could not fetch Snapshot profiles") + } +} + +func TestGetSnapshotByIdThrowsErrorWhenClientReturnsNon200(t *testing.T) { + //Set + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + if !checkAuthTestHelper(r) { + t.Errorf("Invalid Authentication Credentials") + } else { + w.WriteHeader(http.StatusNotFound) + } + })) + defer server.Close() + ndb_client := ndb_client.NewNDBClient("username", "password", server.URL, "", true) + + //Test + _, err := ndb_api.GetSnapshotById(context.Background(), ndb_client, "id") + if err == nil { + t.Error("GetAllSnapshots should return an error when client responds with non 200 status.") + } +} + +func TestTakeSnapshot(t *testing.T) { + //Set + server := GetServerTestHelper(t) + defer server.Close() + ndb_client := ndb_client.NewNDBClient("username", "password", server.URL, "", true) + exp := ndb_api.SnapshotExpiryDetails("timezone", 1) + detailedConfig := ndb_api.SnapshotLcmConfigDetailed(exp) + config := ndb_api.SnapshotLcmConfig(detailedConfig) + request := ndb_api.SnapshotRequest("name", config) + + //Test + value, _ := ndb_api.TakeSnapshot(context.Background(), ndb_client, request) + t.Log(value) + if value.Name != "name" { + t.Error("Could not create Snapshot profiles") + } +} + +func TestTakeSnapshotThrowsErrorWhenClientReturnsNon200(t *testing.T) { + //Set + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + if !checkAuthTestHelper(r) { + t.Errorf("Invalid Authentication Credentials") + } else { + w.WriteHeader(http.StatusNotFound) + } + })) + defer server.Close() + ndb_client := ndb_client.NewNDBClient("username", "password", server.URL, "", true) + exp := ndb_api.SnapshotExpiryDetails("timezone", 1) + detailedConfig := ndb_api.SnapshotLcmConfigDetailed(exp) + config := ndb_api.SnapshotLcmConfig(detailedConfig) + request := ndb_api.SnapshotRequest("name", config) + + //Test + _, err := ndb_api.TakeSnapshot(context.Background(), ndb_client, request) + if err == nil { + t.Error("GetAllSnapshots should return an error when client responds with non 200 status.") + } +} + +func TestDeleteSnapshot(t *testing.T) { + //Set + server := GetServerTestHelper(t) + defer server.Close() + ndb_client := ndb_client.NewNDBClient("username", "password", server.URL, "", true) + + //Test + value, _ := ndb_api.DeleteSnapshot(context.Background(), ndb_client, "id") + t.Log(value) + if value.EntityId != "id" { + t.Error("Could not delete Snapshot profiles") + } +} + +func TestDeleteSnapshotThrowsErrorWhenClientReturnsNon200(t *testing.T) { + //Set + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + if !checkAuthTestHelper(r) { + t.Errorf("Invalid Authentication Credentials") + } else { + w.WriteHeader(http.StatusNotFound) + } + })) + defer server.Close() + ndb_client := ndb_client.NewNDBClient("username", "password", server.URL, "", true) + + //Test + _, err := ndb_api.DeleteSnapshot(context.Background(), ndb_client, "id") + if err == nil { + t.Error("DeleteSnapshot should return an error when client responds with non 200 status.") + } +} From 56998690bb18d97da537a38add304ab789117231 Mon Sep 17 00:00:00 2001 From: Sachin Kanth Date: Mon, 4 Dec 2023 11:36:24 -0500 Subject: [PATCH 13/17] delete logic change --- controllers/snapshot_controller_helper.go | 20 +++- ndb_api/snapshot.go | 2 +- ndb_api/snapshot_response_types.go | 118 ++++++++++++++++++++++ 3 files changed, 134 insertions(+), 6 deletions(-) diff --git a/controllers/snapshot_controller_helper.go b/controllers/snapshot_controller_helper.go index 1475dd57..b5dd23ca 100644 --- a/controllers/snapshot_controller_helper.go +++ b/controllers/snapshot_controller_helper.go @@ -2,6 +2,7 @@ package controllers import ( "context" + "encoding/json" "fmt" "reflect" @@ -52,11 +53,20 @@ func (r *SnapshotReconciler) handleSync(ctx context.Context, snapshot *ndbv1alph return requeueOnErr(err) } for _, snap := range snapshots { - if snap.Name == snapshot.Spec.Name { - snapshotStatus.Id = snap.Id - snapshotStatus.Status = common.DATABASE_CR_STATUS_DELETING - log.Info(fmt.Sprintf("Snap %s with id %s", snap.Name, snap.Id)) - break + if snap.LcmConfig != nil { + var lcmConfig LcmConfig + err = json.Unmarshal(snap.LcmConfig, &lcmConfig) + if err != nil { + log.Error(err, "Unmarshalling error") + r.recorder.Eventf(snapshot, "Warning", EVENT_NDB_REQUEST_FAILED, "Error:", "Unmarshalling error", err.Error()) + return requeueOnErr(err) + } + if snap.Name == snapshot.Spec.Name && lcmConfig.ExpiryDateTimezone == snapshot.ExpiryDateTimezone && lcmConfig.ExpiryInDays == snapshot.ExpiryInDays { + snapshotStatus.Id = snap.Id + snapshotStatus.Status = common.DATABASE_CR_STATUS_DELETING + log.Info(fmt.Sprintf("Snap %s with id %s", snap.Name, snap.Id)) + break + } } } } diff --git a/ndb_api/snapshot.go b/ndb_api/snapshot.go index 52f1d043..8d1a72d4 100644 --- a/ndb_api/snapshot.go +++ b/ndb_api/snapshot.go @@ -29,7 +29,7 @@ import ( ) // Fetches all snapshots on the NDB instance and returns a slice of the snapshots -func GetAllSnapshots(ctx context.Context, ndbClient *ndb_client.NDBClient) (snapshots []SnapshotResponse, err error) { +func GetAllSnapshots(ctx context.Context, ndbClient *ndb_client.NDBClient) (snapshots []AllSnapshotResponse, err error) { log := ctrllog.FromContext(ctx) log.Info("Entered ndb_api.GetAllSnapshots") if ndbClient == nil { diff --git a/ndb_api/snapshot_response_types.go b/ndb_api/snapshot_response_types.go index aad3efa9..edadfa1a 100644 --- a/ndb_api/snapshot_response_types.go +++ b/ndb_api/snapshot_response_types.go @@ -8,3 +8,121 @@ type SnapshotResponse struct { SnapshotUuid string `json:"snapshotUuid"` TimeMachineId string `json:"timeMachineId"` } + +type AllSnapshotResponse struct { + ID string `json:"id"` + Name string `json:"name"` + Description interface{} `json:"description"` + OwnerID string `json:"ownerId"` + DateCreated string `json:"dateCreated"` + DateModified string `json:"dateModified"` + AccessLevel interface{} `json:"accessLevel"` + Properties []interface{} `json:"properties"` + Tags []interface{} `json:"tags"` + SnapshotID string `json:"snapshotId"` + SnapshotUUID string `json:"snapshotUuid"` + NxClusterID string `json:"nxClusterId"` + ProtectionDomainID string `json:"protectionDomainId"` + ParentSnapshotID interface{} `json:"parentSnapshotId"` + TimeMachineID string `json:"timeMachineId"` + DatabaseNodeID string `json:"databaseNodeId"` + AppInfoVersion string `json:"appInfoVersion"` + Status string `json:"status"` + Type string `json:"type"` + ApplicableTypes []string `json:"applicableTypes"` + SnapshotTimeStamp string `json:"snapshotTimeStamp"` + Info struct { + SecureInfo interface{} `json:"secureInfo"` + Info interface{} `json:"info"` + LinkedDatabases []struct { + ID string `json:"id"` + DatabaseName string `json:"databaseName"` + Status string `json:"status"` + Info struct { + Info struct { + CreatedBy string `json:"created_by"` + } `json:"info"` + } `json:"info"` + AppConsistent bool `json:"appConsistent"` + Message interface{} `json:"message"` + Clone bool `json:"clone"` + } `json:"linkedDatabases"` + Databases interface{} `json:"databases"` + DatabaseGroupID interface{} `json:"databaseGroupId"` + MissingDatabases interface{} `json:"missingDatabases"` + ReplicationHistory interface{} `json:"replicationHistory"` + } `json:"info"` + Metadata struct { + SecureInfo interface{} `json:"secureInfo"` + Info interface{} `json:"info"` + DeregisterInfo interface{} `json:"deregisterInfo"` + FromTimeStamp string `json:"fromTimeStamp"` + ToTimeStamp string `json:"toTimeStamp"` + ReplicationRetryCount int `json:"replicationRetryCount"` + LastReplicationRetryTimestamp interface{} `json:"lastReplicationRetryTimestamp"` + LastReplicationRetrySourceSnapshotID interface{} `json:"lastReplicationRetrySourceSnapshotId"` + Async bool `json:"async"` + Standby bool `json:"standby"` + CurationRetryCount int `json:"curationRetryCount"` + OperationsUsingSnapshot []interface{} `json:"operationsUsingSnapshot"` + } `json:"metadata"` + Metric struct { + LastUpdatedTimeInUTC interface{} `json:"lastUpdatedTimeInUTC"` + Storage struct { + LastUpdatedTimeInUTC interface{} `json:"lastUpdatedTimeInUTC"` + ControllerNumIops interface{} `json:"controllerNumIops"` + ControllerAvgIoLatencyUsecs interface{} `json:"controllerAvgIoLatencyUsecs"` + Size int `json:"size"` + AllocatedSize int `json:"allocatedSize"` + UsedSize int `json:"usedSize"` + Unit string `json:"unit"` + } `json:"storage"` + } `json:"metric"` + SoftwareSnapshotID string `json:"softwareSnapshotId"` + SoftwareDatabaseSnapshot bool `json:"softwareDatabaseSnapshot"` + DbServerStorageMetadataVersion int `json:"dbServerStorageMetadataVersion"` + Sanitised bool `json:"sanitised"` + SanitisedFromSnapshotID interface{} `json:"sanitisedFromSnapshotId"` + TimeZone string `json:"timeZone"` + Processed bool `json:"processed"` + DatabaseSnapshot bool `json:"databaseSnapshot"` + FromTimeStamp string `json:"fromTimeStamp"` + ToTimeStamp string `json:"toTimeStamp"` + DbserverID interface{} `json:"dbserverId"` + DbserverName interface{} `json:"dbserverName"` + DbserverIP interface{} `json:"dbserverIp"` + ReplicatedSnapshots interface{} `json:"replicatedSnapshots"` + SoftwareSnapshot interface{} `json:"softwareSnapshot"` + SanitisedSnapshots interface{} `json:"sanitisedSnapshots"` + SnapshotFamily interface{} `json:"snapshotFamily"` + SnapshotTimeStampDate int64 `json:"snapshotTimeStampDate"` + LcmConfig interface{} `json:"lcmConfig"` + SnapshotSize int `json:"snapshotSize"` + ParentSnapshot bool `json:"parentSnapshot"` +} + +type LcmConfig struct { + ExpiryDetails struct { + RemindBeforeInDays int `json:"remindBeforeInDays"` + EffectiveTimestamp string `json:"effectiveTimestamp"` + ExpiryTimestamp string `json:"expiryTimestamp"` + ExpiryDateTimezone string `json:"expiryDateTimezone"` + UserCreated bool `json:"userCreated"` + ExpireInDays int `json:"expireInDays"` + } `json:"expiryDetails"` + RefreshDetails struct { + RefreshInDays int `json:"refreshInDays"` + RefreshInHours int `json:"refreshInHours"` + RefreshInMonths int `json:"refreshInMonths"` + LastRefreshDate string `json:"lastRefreshDate"` + NextRefreshDate string `json:"nextRefreshDate"` + RefreshTime string `json:"refreshTime"` + RefreshDateTimezone string `json:"refreshDateTimezone"` + } `json:"refreshDetails"` + PreDeleteCommand struct { + Command string `json:"command"` + } `json:"preDeleteCommand"` + PostDeleteCommand struct { + Command string `json:"command"` + } `json:"postDeleteCommand"` +} From f794c5152cd6c97d54378a10c15183234e08a3a8 Mon Sep 17 00:00:00 2001 From: Sachin Kanth Date: Mon, 4 Dec 2023 12:15:06 -0500 Subject: [PATCH 14/17] compile issues fixed --- controllers/snapshot_controller_helper.go | 17 ++++++++--------- ndb_api/snapshot_response_types.go | 2 +- 2 files changed, 9 insertions(+), 10 deletions(-) diff --git a/controllers/snapshot_controller_helper.go b/controllers/snapshot_controller_helper.go index b5dd23ca..1ee9e4f6 100644 --- a/controllers/snapshot_controller_helper.go +++ b/controllers/snapshot_controller_helper.go @@ -2,7 +2,6 @@ package controllers import ( "context" - "encoding/json" "fmt" "reflect" @@ -54,17 +53,17 @@ func (r *SnapshotReconciler) handleSync(ctx context.Context, snapshot *ndbv1alph } for _, snap := range snapshots { if snap.LcmConfig != nil { - var lcmConfig LcmConfig - err = json.Unmarshal(snap.LcmConfig, &lcmConfig) - if err != nil { - log.Error(err, "Unmarshalling error") - r.recorder.Eventf(snapshot, "Warning", EVENT_NDB_REQUEST_FAILED, "Error:", "Unmarshalling error", err.Error()) + // var lcmConfig ndb_api.LcmConfigResponse + lcmConfig, ok := snap.LcmConfig.(ndb_api.LcmConfigResponse) + if !ok { + // log.Error("", "Unmarshalling error") + r.recorder.Eventf(snapshot, "Warning", EVENT_NDB_REQUEST_FAILED, "Error:", "Unmarshalling error", "") return requeueOnErr(err) } - if snap.Name == snapshot.Spec.Name && lcmConfig.ExpiryDateTimezone == snapshot.ExpiryDateTimezone && lcmConfig.ExpiryInDays == snapshot.ExpiryInDays { - snapshotStatus.Id = snap.Id + if snap.Name == snapshot.Spec.Name && lcmConfig.ExpiryDetails.ExpiryDateTimezone == snapshot.Spec.ExpiryDateTimezone && lcmConfig.ExpiryDetails.ExpireInDays == snapshot.Spec.ExpireInDays { + snapshotStatus.Id = snap.ID snapshotStatus.Status = common.DATABASE_CR_STATUS_DELETING - log.Info(fmt.Sprintf("Snap %s with id %s", snap.Name, snap.Id)) + log.Info(fmt.Sprintf("Snap %s with id %s", snap.Name, snap.ID)) break } } diff --git a/ndb_api/snapshot_response_types.go b/ndb_api/snapshot_response_types.go index edadfa1a..afefb4fc 100644 --- a/ndb_api/snapshot_response_types.go +++ b/ndb_api/snapshot_response_types.go @@ -101,7 +101,7 @@ type AllSnapshotResponse struct { ParentSnapshot bool `json:"parentSnapshot"` } -type LcmConfig struct { +type LcmConfigResponse struct { ExpiryDetails struct { RemindBeforeInDays int `json:"remindBeforeInDays"` EffectiveTimestamp string `json:"effectiveTimestamp"` From 18c88351fe091cda4dbd52f6a91ad312ee03e5b3 Mon Sep 17 00:00:00 2001 From: pnprathima Date: Mon, 4 Dec 2023 14:55:39 -0500 Subject: [PATCH 15/17] Fix tests --- test/ndb_api_test.go | 35 +++++++++++++++++++++++++---------- 1 file changed, 25 insertions(+), 10 deletions(-) diff --git a/test/ndb_api_test.go b/test/ndb_api_test.go index b54a0dc1..f6670263 100644 --- a/test/ndb_api_test.go +++ b/test/ndb_api_test.go @@ -162,13 +162,21 @@ func TestTakeSnapshot(t *testing.T) { server := GetServerTestHelper(t) defer server.Close() ndb_client := ndb_client.NewNDBClient("username", "password", server.URL, "", true) - exp := ndb_api.SnapshotExpiryDetails("timezone", 1) - detailedConfig := ndb_api.SnapshotLcmConfigDetailed(exp) - config := ndb_api.SnapshotLcmConfig(detailedConfig) - request := ndb_api.SnapshotRequest("name", config) + + request := &ndb_api.SnapshotRequest{ + Name: "name", + SnapshotLcmConfig: ndb_api.SnapshotLcmConfig{ + SnapshotLCMConfigDetailed: ndb_api.SnapshotLcmConfigDetailed{ + ExpiryDetails: ndb_api.SnapshotExpiryDetails{ + ExpiryDateTimezone: "time", + ExpireInDays: 1, + }, + }, + }, + } //Test - value, _ := ndb_api.TakeSnapshot(context.Background(), ndb_client, request) + value, _ := ndb_api.TakeSnapshot(context.Background(), ndb_client, request, "Id") t.Log(value) if value.Name != "name" { t.Error("Could not create Snapshot profiles") @@ -186,13 +194,20 @@ func TestTakeSnapshotThrowsErrorWhenClientReturnsNon200(t *testing.T) { })) defer server.Close() ndb_client := ndb_client.NewNDBClient("username", "password", server.URL, "", true) - exp := ndb_api.SnapshotExpiryDetails("timezone", 1) - detailedConfig := ndb_api.SnapshotLcmConfigDetailed(exp) - config := ndb_api.SnapshotLcmConfig(detailedConfig) - request := ndb_api.SnapshotRequest("name", config) + request := &ndb_api.SnapshotRequest{ + Name: "name", + SnapshotLcmConfig: ndb_api.SnapshotLcmConfig{ + SnapshotLCMConfigDetailed: ndb_api.SnapshotLcmConfigDetailed{ + ExpiryDetails: ndb_api.SnapshotExpiryDetails{ + ExpiryDateTimezone: "time", + ExpireInDays: 1, + }, + }, + }, + } //Test - _, err := ndb_api.TakeSnapshot(context.Background(), ndb_client, request) + _, err := ndb_api.TakeSnapshot(context.Background(), ndb_client, request, "id") if err == nil { t.Error("GetAllSnapshots should return an error when client responds with non 200 status.") } From 8781a864319f6a7936a00b43626128fd44dd20d8 Mon Sep 17 00:00:00 2001 From: Sachin Kanth Date: Mon, 4 Dec 2023 21:05:18 -0500 Subject: [PATCH 16/17] changing model for snapshot --- controllers/snapshot_controller_helper.go | 4 +- ndb_api/snapshot.go | 2 +- ndb_api/snapshot_response_types.go | 47 ++++++++++++----------- 3 files changed, 27 insertions(+), 26 deletions(-) diff --git a/controllers/snapshot_controller_helper.go b/controllers/snapshot_controller_helper.go index 1ee9e4f6..344fdd8f 100644 --- a/controllers/snapshot_controller_helper.go +++ b/controllers/snapshot_controller_helper.go @@ -61,9 +61,9 @@ func (r *SnapshotReconciler) handleSync(ctx context.Context, snapshot *ndbv1alph return requeueOnErr(err) } if snap.Name == snapshot.Spec.Name && lcmConfig.ExpiryDetails.ExpiryDateTimezone == snapshot.Spec.ExpiryDateTimezone && lcmConfig.ExpiryDetails.ExpireInDays == snapshot.Spec.ExpireInDays { - snapshotStatus.Id = snap.ID + snapshotStatus.Id = snap.Id snapshotStatus.Status = common.DATABASE_CR_STATUS_DELETING - log.Info(fmt.Sprintf("Snap %s with id %s", snap.Name, snap.ID)) + log.Info(fmt.Sprintf("Snap %s with id %s", snap.Name, snap.Id)) break } } diff --git a/ndb_api/snapshot.go b/ndb_api/snapshot.go index 8d1a72d4..52f1d043 100644 --- a/ndb_api/snapshot.go +++ b/ndb_api/snapshot.go @@ -29,7 +29,7 @@ import ( ) // Fetches all snapshots on the NDB instance and returns a slice of the snapshots -func GetAllSnapshots(ctx context.Context, ndbClient *ndb_client.NDBClient) (snapshots []AllSnapshotResponse, err error) { +func GetAllSnapshots(ctx context.Context, ndbClient *ndb_client.NDBClient) (snapshots []SnapshotResponse, err error) { log := ctrllog.FromContext(ctx) log.Info("Entered ndb_api.GetAllSnapshots") if ndbClient == nil { diff --git a/ndb_api/snapshot_response_types.go b/ndb_api/snapshot_response_types.go index afefb4fc..6638b916 100644 --- a/ndb_api/snapshot_response_types.go +++ b/ndb_api/snapshot_response_types.go @@ -1,12 +1,13 @@ package ndb_api type SnapshotResponse struct { - Id string `json:"id"` - Name string `json:"name"` - Description string `json:"description"` - SnapshotId string `json:"snapshotId"` - SnapshotUuid string `json:"snapshotUuid"` - TimeMachineId string `json:"timeMachineId"` + Id string `json:"id"` + Name string `json:"name"` + Description string `json:"description"` + SnapshotId string `json:"snapshotId"` + SnapshotUuid string `json:"snapshotUuid"` + TimeMachineId string `json:"timeMachineId"` + LcmConfig interface{} `json:"lcmConfig"` } type AllSnapshotResponse struct { @@ -76,7 +77,7 @@ type AllSnapshotResponse struct { AllocatedSize int `json:"allocatedSize"` UsedSize int `json:"usedSize"` Unit string `json:"unit"` - } `json:"storage"` + } `json:"-"` } `json:"metric"` SoftwareSnapshotID string `json:"softwareSnapshotId"` SoftwareDatabaseSnapshot bool `json:"softwareDatabaseSnapshot"` @@ -103,26 +104,26 @@ type AllSnapshotResponse struct { type LcmConfigResponse struct { ExpiryDetails struct { - RemindBeforeInDays int `json:"remindBeforeInDays"` - EffectiveTimestamp string `json:"effectiveTimestamp"` - ExpiryTimestamp string `json:"expiryTimestamp"` + RemindBeforeInDays int `json:"-"` + EffectiveTimestamp string `json:"-"` + ExpiryTimestamp string `json:"-"` ExpiryDateTimezone string `json:"expiryDateTimezone"` - UserCreated bool `json:"userCreated"` + UserCreated bool `json:"-"` ExpireInDays int `json:"expireInDays"` } `json:"expiryDetails"` RefreshDetails struct { - RefreshInDays int `json:"refreshInDays"` - RefreshInHours int `json:"refreshInHours"` - RefreshInMonths int `json:"refreshInMonths"` - LastRefreshDate string `json:"lastRefreshDate"` - NextRefreshDate string `json:"nextRefreshDate"` - RefreshTime string `json:"refreshTime"` - RefreshDateTimezone string `json:"refreshDateTimezone"` - } `json:"refreshDetails"` + RefreshInDays int `json:"-"` + RefreshInHours int `json:"-"` + RefreshInMonths int `json:"-"` + LastRefreshDate string `json:"-"` + NextRefreshDate string `json:"-"` + RefreshTime string `json:"-"` + RefreshDateTimezone string `json:"-"` + } `json:"-"` PreDeleteCommand struct { - Command string `json:"command"` - } `json:"preDeleteCommand"` + Command string `json:"-"` + } `json:"-"` PostDeleteCommand struct { - Command string `json:"command"` - } `json:"postDeleteCommand"` + Command string `json:"-"` + } `json:"-"` } From fbc2cd642bf0119040b6db44da0f9e6269031586 Mon Sep 17 00:00:00 2001 From: rdhamal Date: Mon, 4 Dec 2023 23:18:03 -0500 Subject: [PATCH 17/17] Changed equality check for deletion --- controllers/snapshot_controller_helper.go | 9 +- ndb_api/snapshot_response_types.go | 125 ++-------------------- 2 files changed, 8 insertions(+), 126 deletions(-) diff --git a/controllers/snapshot_controller_helper.go b/controllers/snapshot_controller_helper.go index 344fdd8f..1514f2a2 100644 --- a/controllers/snapshot_controller_helper.go +++ b/controllers/snapshot_controller_helper.go @@ -53,14 +53,7 @@ func (r *SnapshotReconciler) handleSync(ctx context.Context, snapshot *ndbv1alph } for _, snap := range snapshots { if snap.LcmConfig != nil { - // var lcmConfig ndb_api.LcmConfigResponse - lcmConfig, ok := snap.LcmConfig.(ndb_api.LcmConfigResponse) - if !ok { - // log.Error("", "Unmarshalling error") - r.recorder.Eventf(snapshot, "Warning", EVENT_NDB_REQUEST_FAILED, "Error:", "Unmarshalling error", "") - return requeueOnErr(err) - } - if snap.Name == snapshot.Spec.Name && lcmConfig.ExpiryDetails.ExpiryDateTimezone == snapshot.Spec.ExpiryDateTimezone && lcmConfig.ExpiryDetails.ExpireInDays == snapshot.Spec.ExpireInDays { + if snap.Name == snapshot.Spec.Name && snap.LcmConfig.ExpiryDetails.ExpiryDateTimezone == snapshot.Spec.ExpiryDateTimezone && snap.LcmConfig.ExpiryDetails.ExpireInDays == snapshot.Spec.ExpireInDays { snapshotStatus.Id = snap.Id snapshotStatus.Status = common.DATABASE_CR_STATUS_DELETING log.Info(fmt.Sprintf("Snap %s with id %s", snap.Name, snap.Id)) diff --git a/ndb_api/snapshot_response_types.go b/ndb_api/snapshot_response_types.go index 6638b916..219419ea 100644 --- a/ndb_api/snapshot_response_types.go +++ b/ndb_api/snapshot_response_types.go @@ -1,129 +1,18 @@ package ndb_api type SnapshotResponse struct { - Id string `json:"id"` - Name string `json:"name"` - Description string `json:"description"` - SnapshotId string `json:"snapshotId"` - SnapshotUuid string `json:"snapshotUuid"` - TimeMachineId string `json:"timeMachineId"` - LcmConfig interface{} `json:"lcmConfig"` -} - -type AllSnapshotResponse struct { - ID string `json:"id"` - Name string `json:"name"` - Description interface{} `json:"description"` - OwnerID string `json:"ownerId"` - DateCreated string `json:"dateCreated"` - DateModified string `json:"dateModified"` - AccessLevel interface{} `json:"accessLevel"` - Properties []interface{} `json:"properties"` - Tags []interface{} `json:"tags"` - SnapshotID string `json:"snapshotId"` - SnapshotUUID string `json:"snapshotUuid"` - NxClusterID string `json:"nxClusterId"` - ProtectionDomainID string `json:"protectionDomainId"` - ParentSnapshotID interface{} `json:"parentSnapshotId"` - TimeMachineID string `json:"timeMachineId"` - DatabaseNodeID string `json:"databaseNodeId"` - AppInfoVersion string `json:"appInfoVersion"` - Status string `json:"status"` - Type string `json:"type"` - ApplicableTypes []string `json:"applicableTypes"` - SnapshotTimeStamp string `json:"snapshotTimeStamp"` - Info struct { - SecureInfo interface{} `json:"secureInfo"` - Info interface{} `json:"info"` - LinkedDatabases []struct { - ID string `json:"id"` - DatabaseName string `json:"databaseName"` - Status string `json:"status"` - Info struct { - Info struct { - CreatedBy string `json:"created_by"` - } `json:"info"` - } `json:"info"` - AppConsistent bool `json:"appConsistent"` - Message interface{} `json:"message"` - Clone bool `json:"clone"` - } `json:"linkedDatabases"` - Databases interface{} `json:"databases"` - DatabaseGroupID interface{} `json:"databaseGroupId"` - MissingDatabases interface{} `json:"missingDatabases"` - ReplicationHistory interface{} `json:"replicationHistory"` - } `json:"info"` - Metadata struct { - SecureInfo interface{} `json:"secureInfo"` - Info interface{} `json:"info"` - DeregisterInfo interface{} `json:"deregisterInfo"` - FromTimeStamp string `json:"fromTimeStamp"` - ToTimeStamp string `json:"toTimeStamp"` - ReplicationRetryCount int `json:"replicationRetryCount"` - LastReplicationRetryTimestamp interface{} `json:"lastReplicationRetryTimestamp"` - LastReplicationRetrySourceSnapshotID interface{} `json:"lastReplicationRetrySourceSnapshotId"` - Async bool `json:"async"` - Standby bool `json:"standby"` - CurationRetryCount int `json:"curationRetryCount"` - OperationsUsingSnapshot []interface{} `json:"operationsUsingSnapshot"` - } `json:"metadata"` - Metric struct { - LastUpdatedTimeInUTC interface{} `json:"lastUpdatedTimeInUTC"` - Storage struct { - LastUpdatedTimeInUTC interface{} `json:"lastUpdatedTimeInUTC"` - ControllerNumIops interface{} `json:"controllerNumIops"` - ControllerAvgIoLatencyUsecs interface{} `json:"controllerAvgIoLatencyUsecs"` - Size int `json:"size"` - AllocatedSize int `json:"allocatedSize"` - UsedSize int `json:"usedSize"` - Unit string `json:"unit"` - } `json:"-"` - } `json:"metric"` - SoftwareSnapshotID string `json:"softwareSnapshotId"` - SoftwareDatabaseSnapshot bool `json:"softwareDatabaseSnapshot"` - DbServerStorageMetadataVersion int `json:"dbServerStorageMetadataVersion"` - Sanitised bool `json:"sanitised"` - SanitisedFromSnapshotID interface{} `json:"sanitisedFromSnapshotId"` - TimeZone string `json:"timeZone"` - Processed bool `json:"processed"` - DatabaseSnapshot bool `json:"databaseSnapshot"` - FromTimeStamp string `json:"fromTimeStamp"` - ToTimeStamp string `json:"toTimeStamp"` - DbserverID interface{} `json:"dbserverId"` - DbserverName interface{} `json:"dbserverName"` - DbserverIP interface{} `json:"dbserverIp"` - ReplicatedSnapshots interface{} `json:"replicatedSnapshots"` - SoftwareSnapshot interface{} `json:"softwareSnapshot"` - SanitisedSnapshots interface{} `json:"sanitisedSnapshots"` - SnapshotFamily interface{} `json:"snapshotFamily"` - SnapshotTimeStampDate int64 `json:"snapshotTimeStampDate"` - LcmConfig interface{} `json:"lcmConfig"` - SnapshotSize int `json:"snapshotSize"` - ParentSnapshot bool `json:"parentSnapshot"` + Id string `json:"id"` + Name string `json:"name"` + Description string `json:"description"` + SnapshotId string `json:"snapshotId"` + SnapshotUuid string `json:"snapshotUuid"` + TimeMachineId string `json:"timeMachineId"` + LcmConfig *LcmConfigResponse `json:"lcmConfig,omitempty"` } type LcmConfigResponse struct { ExpiryDetails struct { - RemindBeforeInDays int `json:"-"` - EffectiveTimestamp string `json:"-"` - ExpiryTimestamp string `json:"-"` ExpiryDateTimezone string `json:"expiryDateTimezone"` - UserCreated bool `json:"-"` ExpireInDays int `json:"expireInDays"` } `json:"expiryDetails"` - RefreshDetails struct { - RefreshInDays int `json:"-"` - RefreshInHours int `json:"-"` - RefreshInMonths int `json:"-"` - LastRefreshDate string `json:"-"` - NextRefreshDate string `json:"-"` - RefreshTime string `json:"-"` - RefreshDateTimezone string `json:"-"` - } `json:"-"` - PreDeleteCommand struct { - Command string `json:"-"` - } `json:"-"` - PostDeleteCommand struct { - Command string `json:"-"` - } `json:"-"` }