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..4a97ea8d --- /dev/null +++ b/api/v1alpha1/snapshot_types.go @@ -0,0 +1,72 @@ +/* +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"` + TimeMachineID string `json:"timemachineid,omitempty"` + Name string `json:"name,omitempty"` + ExpiryDateTimezone string `json:"expirydatetimezone,omitempty"` + ExpireInDays int `json:"expireindays,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"` + DeletionOperationID string `json:"deletionoperationid,omitempty"` + Status string `json:"status,omitempty"` + Id string `json:"id,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..c4cf107c --- /dev/null +++ b/config/crd/bases/ndb.nutanix.com_snapshots.yaml @@ -0,0 +1,70 @@ +--- +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: + expireindays: + type: integer + expirydatetimezone: + type: string + ip: + description: Foo is an example field of Snapshot. Edit snapshot_types.go + to remove/update + type: string + name: + type: string + timemachineid: + type: string + type: object + status: + description: SnapshotStatus defines the observed state of Snapshot + properties: + deletionoperationid: + type: string + id: + type: string + 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/controllers/snapshot_controller.go b/controllers/snapshot_controller.go new file mode 100644 index 00000000..871154f8 --- /dev/null +++ b/controllers/snapshot_controller.go @@ -0,0 +1,117 @@ +/* +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" + + 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" + 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 + recorder record.EventRecorder +} + +//+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 := 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) + } + + log.Info("Snapshot Status: " + util.ToString(snapshot.Status)) + + // Fetch the NDBServer resource from the namespace + ndbServer := &ndbv1alpha1.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. + // 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..1514f2a2 --- /dev/null +++ b/controllers/snapshot_controller_helper.go @@ -0,0 +1,202 @@ +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" + "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" +) + +// 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_conxtroller_helper.handleSync") + + snapshotStatus := snapshot.Status.DeepCopy() + + // Take a snapshot + if snapshotStatus.Status == "" { + // DB Status.Status is empty => Provision a DB + 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) + 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 { + 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.LcmConfig != nil { + 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)) + break + } + } + } + } + } 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) + } + } + + 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: + 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) +} + +// 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(fmt.Sprintf("Snapshot CR is being deleted with id %s", snapshot.Status.Id)) + log.Info(snapshot.ResourceVersion) + 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 { + 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 + } + } + } + } 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/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 { diff --git a/ndb_api/snapshot.go b/ndb_api/snapshot.go new file mode 100644 index 00000000..52f1d043 --- /dev/null +++ b/ndb_api/snapshot.go @@ -0,0 +1,234 @@ +/* +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.GetAllSnapshots") + 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 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) + 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 snapshots (/snapshots) + 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 snapshot") + 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 *SnapshotRequest, timeMachineId string) (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 timeMachineId == "" { + err = errors.New("empty timeMachineId") + log.Error(err, "Received empty timeMachineId in request") + return + } + snapshotEndPoint := "tms/" + 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") + 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, nil) + 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 index a2f1719a..c0888607 100644 --- a/ndb_api/snapshot_request_types.go +++ b/ndb_api/snapshot_request_types.go @@ -10,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/snapshot_response_types.go b/ndb_api/snapshot_response_types.go index aad3efa9..219419ea 100644 --- a/ndb_api/snapshot_response_types.go +++ b/ndb_api/snapshot_response_types.go @@ -1,10 +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"` + 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 { + ExpiryDateTimezone string `json:"expiryDateTimezone"` + ExpireInDays int `json:"expireInDays"` + } `json:"expiryDetails"` } diff --git a/ndb_api/snapshots_helper.go b/ndb_api/snapshots_helper.go new file mode 100644 index 00000000..f7d90949 --- /dev/null +++ b/ndb_api/snapshots_helper.go @@ -0,0 +1,34 @@ +/* +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 ( + ndbv1alpha1 "github.com/nutanix-cloud-native/ndb-operator/api/v1alpha1" +) + +// Returns a request to delete a snapshot instance +func GenerateTakeSnapshotRequest(snapshot *ndbv1alpha1.Snapshot) (req *SnapshotRequest) { + req = &SnapshotRequest{ + Name: snapshot.Spec.Name, + SnapshotLcmConfig: SnapshotLcmConfig{ + SnapshotLCMConfigDetailed: SnapshotLcmConfigDetailed{ + ExpiryDetails: SnapshotExpiryDetails{ + ExpiryDateTimezone: snapshot.Spec.ExpiryDateTimezone, + ExpireInDays: snapshot.Spec.ExpireInDays, + }, + }, + }, + } + return +} 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, }, diff --git a/ndb_client/ndb_client.go b/ndb_client/ndb_client.go index 379384a0..667ce1e2 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,8 +60,8 @@ 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 } req.SetBasicAuth(ndbClient.username, ndbClient.password) @@ -70,13 +71,33 @@ 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") 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 ndbClient.client.Do(req) + return req, nil } diff --git a/test/ndb_api_test.go b/test/ndb_api_test.go index 17308a85..f6670263 100644 --- a/test/ndb_api_test.go +++ b/test/ndb_api_test.go @@ -90,3 +90,158 @@ 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) + + 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, "Id") + 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) + 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, "id") + 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.") + } +}