diff --git a/api/v1beta1/questdbsnapshot_types.go b/api/v1beta1/questdbsnapshot_types.go index 7f42776..cf07803 100644 --- a/api/v1beta1/questdbsnapshot_types.go +++ b/api/v1beta1/questdbsnapshot_types.go @@ -41,9 +41,9 @@ const ( // QuestDBSnapshotSpec defines the desired state of QuestDBSnapshot type QuestDBSnapshotSpec struct { - QuestDBName string `json:"questdbName"` - VolumeSnapshotClassName string `json:"volumeSnapshotClassName"` - JobBackoffLimit int32 `json:"jobBackoffLimit,omitempty"` + QuestDBName string `json:"questdbName"` + VolumeSnapshotClassName *string `json:"volumeSnapshotClassName,omitempty"` + JobBackoffLimit int32 `json:"jobBackoffLimit,omitempty"` } // QuestDBSnapshotStatus defines the observed state of QuestDBSnapshot diff --git a/api/v1beta1/questdbsnapshot_webhook.go b/api/v1beta1/questdbsnapshot_webhook.go index 893a495..52e8f5f 100644 --- a/api/v1beta1/questdbsnapshot_webhook.go +++ b/api/v1beta1/questdbsnapshot_webhook.go @@ -18,6 +18,7 @@ package v1beta1 import ( "errors" + "reflect" "k8s.io/apimachinery/pkg/runtime" ctrl "sigs.k8s.io/controller-runtime" @@ -61,14 +62,14 @@ func validateSnapshotCreate(spec QuestDBSnapshotSpec) error { return errors.New("QuestDBName is required") } - if spec.VolumeSnapshotClassName == "" { - return errors.New("VolumeSnapshotClassName is required") - } - if spec.JobBackoffLimit <= 0 { return errors.New("JobBackoffLimit must be greater than 0") } + if spec.VolumeSnapshotClassName != nil && *spec.VolumeSnapshotClassName == "" { + return errors.New("VolumeSnapshotClassName must not be empty") + } + return nil } @@ -77,7 +78,7 @@ func validateSnapshotUpdate(oldSpec, newSpec QuestDBSnapshotSpec) error { return errors.New("QuestDBName is immutable") } - if newSpec.VolumeSnapshotClassName != oldSpec.VolumeSnapshotClassName { + if !reflect.DeepEqual(newSpec.VolumeSnapshotClassName, oldSpec.VolumeSnapshotClassName) { return errors.New("VolumeSnapshotClassName is immutable") } diff --git a/api/v1beta1/questdbsnapshot_webhook_test.go b/api/v1beta1/questdbsnapshot_webhook_test.go index 1a34a95..594d2ce 100644 --- a/api/v1beta1/questdbsnapshot_webhook_test.go +++ b/api/v1beta1/questdbsnapshot_webhook_test.go @@ -7,6 +7,7 @@ import ( . "github.com/onsi/ginkgo/v2" . "github.com/onsi/gomega" "k8s.io/apimachinery/pkg/api/resource" + "k8s.io/utils/pointer" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" ) @@ -39,7 +40,7 @@ var _ = Describe("QuestDBSnapshot Webhook", func() { }, Spec: QuestDBSnapshotSpec{ QuestDBName: q.Name, - VolumeSnapshotClassName: "csi-hostpath-snapclass", + VolumeSnapshotClassName: pointer.String("csi-hostpath-snapclass"), }, } }) @@ -56,10 +57,15 @@ var _ = Describe("QuestDBSnapshot Webhook", func() { }) It("should reject empty volume snapshot class names", func() { - snap.Spec.VolumeSnapshotClassName = "" + snap.Spec.VolumeSnapshotClassName = pointer.String("") Expect(k8sClient.Create(ctx, snap)).ToNot(Succeed()) }) + It("should accept nil volume snapshot class names", func() { + snap.Spec.VolumeSnapshotClassName = nil + Expect(k8sClient.Create(ctx, snap)).To(Succeed()) + }) + }) Context("When validating QuestDBSnapshot Updates", func() { @@ -72,7 +78,14 @@ var _ = Describe("QuestDBSnapshot Webhook", func() { It("should reject updates to volume snapshot class names", func() { Expect(k8sClient.Create(ctx, snap)).To(Succeed()) - snap.Spec.VolumeSnapshotClassName = "foo" + snap.Spec.VolumeSnapshotClassName = pointer.String("foo") + Expect(k8sClient.Update(ctx, snap)).ToNot(Succeed()) + }) + + It("should reject nil-to-value changes to volume snapshot class names", func() { + snap.Spec.VolumeSnapshotClassName = nil + Expect(k8sClient.Create(ctx, snap)).To(Succeed()) + snap.Spec.VolumeSnapshotClassName = pointer.String("foo") Expect(k8sClient.Update(ctx, snap)).ToNot(Succeed()) }) @@ -81,6 +94,13 @@ var _ = Describe("QuestDBSnapshot Webhook", func() { snap.Spec.JobBackoffLimit = 500 Expect(k8sClient.Update(ctx, snap)).ToNot(Succeed()) }) + + It("should accept updates if nothing has changed and volume snapshot class name is nil", func() { + snap.Spec.VolumeSnapshotClassName = nil + Expect(k8sClient.Create(ctx, snap)).To(Succeed()) + Expect(k8sClient.Update(ctx, snap)).To(Succeed()) + }) + }) }) diff --git a/api/v1beta1/questdbsnapshotschedule_webhook_test.go b/api/v1beta1/questdbsnapshotschedule_webhook_test.go index 809d36d..e7559e4 100644 --- a/api/v1beta1/questdbsnapshotschedule_webhook_test.go +++ b/api/v1beta1/questdbsnapshotschedule_webhook_test.go @@ -7,6 +7,7 @@ import ( . "github.com/onsi/ginkgo/v2" . "github.com/onsi/gomega" "k8s.io/apimachinery/pkg/api/resource" + "k8s.io/utils/pointer" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" ) @@ -40,7 +41,7 @@ var _ = Describe("QuestDBSnapshotSchedule Webhook", func() { Spec: QuestDBSnapshotScheduleSpec{ Snapshot: QuestDBSnapshotSpec{ QuestDBName: q.Name, - VolumeSnapshotClassName: "csi-hostpath-snapclass", + VolumeSnapshotClassName: pointer.String("csi-hostpath-snapclass"), }, Schedule: "*/1 * * * *", }, @@ -65,7 +66,7 @@ var _ = Describe("QuestDBSnapshotSchedule Webhook", func() { }) It("should reject empty volume snapshot class names", func() { - sched.Spec.Snapshot.VolumeSnapshotClassName = "" + sched.Spec.Snapshot.VolumeSnapshotClassName = pointer.String("") Expect(k8sClient.Create(ctx, sched)).ToNot(Succeed()) }) @@ -87,7 +88,7 @@ var _ = Describe("QuestDBSnapshotSchedule Webhook", func() { It("should reject updates to volume snapshot class names", func() { Expect(k8sClient.Create(ctx, sched)).To(Succeed()) - sched.Spec.Snapshot.VolumeSnapshotClassName = "foo" + sched.Spec.Snapshot.VolumeSnapshotClassName = pointer.String("foo") Expect(k8sClient.Update(ctx, sched)).ToNot(Succeed()) }) diff --git a/api/v1beta1/zz_generated.deepcopy.go b/api/v1beta1/zz_generated.deepcopy.go index 357f600..b40de11 100644 --- a/api/v1beta1/zz_generated.deepcopy.go +++ b/api/v1beta1/zz_generated.deepcopy.go @@ -165,7 +165,7 @@ func (in *QuestDBSnapshot) DeepCopyInto(out *QuestDBSnapshot) { *out = *in out.TypeMeta = in.TypeMeta in.ObjectMeta.DeepCopyInto(&out.ObjectMeta) - out.Spec = in.Spec + in.Spec.DeepCopyInto(&out.Spec) in.Status.DeepCopyInto(&out.Status) } @@ -224,7 +224,7 @@ func (in *QuestDBSnapshotSchedule) DeepCopyInto(out *QuestDBSnapshotSchedule) { *out = *in out.TypeMeta = in.TypeMeta in.ObjectMeta.DeepCopyInto(&out.ObjectMeta) - out.Spec = in.Spec + in.Spec.DeepCopyInto(&out.Spec) in.Status.DeepCopyInto(&out.Status) } @@ -281,7 +281,7 @@ func (in *QuestDBSnapshotScheduleList) DeepCopyObject() runtime.Object { // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *QuestDBSnapshotScheduleSpec) DeepCopyInto(out *QuestDBSnapshotScheduleSpec) { *out = *in - out.Snapshot = in.Snapshot + in.Snapshot.DeepCopyInto(&out.Snapshot) } // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new QuestDBSnapshotScheduleSpec. @@ -313,6 +313,11 @@ func (in *QuestDBSnapshotScheduleStatus) DeepCopy() *QuestDBSnapshotScheduleStat // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *QuestDBSnapshotSpec) DeepCopyInto(out *QuestDBSnapshotSpec) { *out = *in + if in.VolumeSnapshotClassName != nil { + in, out := &in.VolumeSnapshotClassName, &out.VolumeSnapshotClassName + *out = new(string) + **out = **in + } } // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new QuestDBSnapshotSpec. diff --git a/config/crd/bases/crd.questdb.io_questdbsnapshots.yaml b/config/crd/bases/crd.questdb.io_questdbsnapshots.yaml index 2a2b37a..4d1bf7b 100644 --- a/config/crd/bases/crd.questdb.io_questdbsnapshots.yaml +++ b/config/crd/bases/crd.questdb.io_questdbsnapshots.yaml @@ -54,7 +54,6 @@ spec: type: string required: - questdbName - - volumeSnapshotClassName type: object status: description: QuestDBSnapshotStatus defines the observed state of QuestDBSnapshot diff --git a/config/crd/bases/crd.questdb.io_questdbsnapshotschedules.yaml b/config/crd/bases/crd.questdb.io_questdbsnapshotschedules.yaml index 3aad2f9..7707c01 100644 --- a/config/crd/bases/crd.questdb.io_questdbsnapshotschedules.yaml +++ b/config/crd/bases/crd.questdb.io_questdbsnapshotschedules.yaml @@ -64,7 +64,6 @@ spec: type: string required: - questdbName - - volumeSnapshotClassName type: object required: - retention diff --git a/config/manager/kustomization.yaml b/config/manager/kustomization.yaml index 7d1334c..d4e1ee7 100644 --- a/config/manager/kustomization.yaml +++ b/config/manager/kustomization.yaml @@ -5,4 +5,4 @@ kind: Kustomization images: - name: controller newName: questdb/questdb-operator - newTag: 0.0.1 + newTag: 0.1.0 diff --git a/internal/controller/questdbsnapshot_controller.go b/internal/controller/questdbsnapshot_controller.go index 518e3b4..e08bfc4 100644 --- a/internal/controller/questdbsnapshot_controller.go +++ b/internal/controller/questdbsnapshot_controller.go @@ -69,15 +69,8 @@ func (r *QuestDBSnapshotReconciler) Reconcile(ctx context.Context, req ctrl.Requ return ctrl.Result{}, client.IgnoreNotFound(err) } - // Get status of any related secrets. These will be used later to add pgwire credentials - // to the pre- and post-snapshot jobs. - s, err := secrets.GetSecrets(ctx, r.Client, client.ObjectKeyFromObject(snap)) - if err != nil { - return ctrl.Result{}, err - } - // Handle finalizer. This will ensure that the "SNAPSHOT COMPLETE;" is run before the snapshot is deleted - if finalizerResult, err := r.handleFinalizer(ctx, snap, s); err != nil { + if finalizerResult, err := r.handleFinalizer(ctx, snap); err != nil { return finalizerResult, err } @@ -99,11 +92,11 @@ func (r *QuestDBSnapshotReconciler) Reconcile(ctx context.Context, req ctrl.Requ case "": return r.handlePhaseEmpty(ctx, snap) case crdv1beta1.SnapshotPending: - return r.handlePhasePending(ctx, snap, s) + return r.handlePhasePending(ctx, snap) case crdv1beta1.SnapshotRunning: return r.handlePhaseRunning(ctx, snap) case crdv1beta1.SnapshotFinalizing: - return r.handlePhaseFinalizing(ctx, snap, s) + return r.handlePhaseFinalizing(ctx, snap) case crdv1beta1.SnapshotFailed: return r.handlePhaseFailed(ctx, snap) case crdv1beta1.SnapshotSucceeded: @@ -122,21 +115,28 @@ func (r *QuestDBSnapshotReconciler) SetupWithManager(mgr ctrl.Manager) error { Complete(r) } -func (r *QuestDBSnapshotReconciler) buildPreSnapshotJob(snap *crdv1beta1.QuestDBSnapshot, s secrets.QuestDBSecrets) (batchv1.Job, error) { - return r.buildGenericSnapshotJob(snap, s, "pre-snapshot", "SNAPSHOT PREPARE;") +func (r *QuestDBSnapshotReconciler) buildPreSnapshotJob(ctx context.Context, snap *crdv1beta1.QuestDBSnapshot) (batchv1.Job, error) { + return r.buildGenericSnapshotJob(ctx, snap, "pre-snapshot", "SNAPSHOT PREPARE;") } -func (r *QuestDBSnapshotReconciler) buildPostSnapshotJob(snap *crdv1beta1.QuestDBSnapshot, s secrets.QuestDBSecrets) (batchv1.Job, error) { - return r.buildGenericSnapshotJob(snap, s, "post-snapshot", "SNAPSHOT COMPLETE;") +func (r *QuestDBSnapshotReconciler) buildPostSnapshotJob(ctx context.Context, snap *crdv1beta1.QuestDBSnapshot) (batchv1.Job, error) { + return r.buildGenericSnapshotJob(ctx, snap, "post-snapshot", "SNAPSHOT COMPLETE;") } -func (r *QuestDBSnapshotReconciler) buildGenericSnapshotJob(snap *crdv1beta1.QuestDBSnapshot, s secrets.QuestDBSecrets, nameSuffix, command string) (batchv1.Job, error) { +func (r *QuestDBSnapshotReconciler) buildGenericSnapshotJob(ctx context.Context, snap *crdv1beta1.QuestDBSnapshot, nameSuffix, command string) (batchv1.Job, error) { var ( err error user = "admin" password = "quest" ) + // Get status of any related secrets. These will be used later to add pgwire credentials + // to the pre- and post-snapshot jobs. + s, err := secrets.GetSecrets(ctx, r.Client, client.ObjectKeyFromObject(snap)) + if err != nil { + return batchv1.Job{}, err + } + if s.PsqlSecret != nil { if val, found := s.PsqlSecret.Data["QDB_PSQL_USER"]; found { user = string(val) @@ -218,20 +218,17 @@ func (r *QuestDBSnapshotReconciler) buildVolumeSnapshot(snap *crdv1beta1.QuestDB Source: volumesnapshotv1.VolumeSnapshotSource{ PersistentVolumeClaimName: pointer.String(snap.Spec.QuestDBName), }, + VolumeSnapshotClassName: snap.Spec.VolumeSnapshotClassName, }, } - if snap.Spec.VolumeSnapshotClassName != "" { - volSnap.Spec.VolumeSnapshotClassName = pointer.String(snap.Spec.VolumeSnapshotClassName) - } - err = ctrl.SetControllerReference(snap, &volSnap, r.Scheme) return volSnap, err } // handleFinalizer is guaranteed to run before any other reconciliation logic -func (r *QuestDBSnapshotReconciler) handleFinalizer(ctx context.Context, snap *crdv1beta1.QuestDBSnapshot, s secrets.QuestDBSecrets) (ctrl.Result, error) { +func (r *QuestDBSnapshotReconciler) handleFinalizer(ctx context.Context, snap *crdv1beta1.QuestDBSnapshot) (ctrl.Result, error) { var err error if snap.DeletionTimestamp.IsZero() { @@ -269,7 +266,8 @@ func (r *QuestDBSnapshotReconciler) handleFinalizer(ctx context.Context, snap *c return ctrl.Result{}, err } r.Recorder.Eventf(snap, v1.EventTypeNormal, "SnapshotFinalizing", "Running 'SNAPSHOT COMPLETE;' for snapshot %s", snap.Name) - return r.handlePhaseFinalizing(ctx, snap, s) + + return r.handlePhaseFinalizing(ctx, snap) } // If the job is active, we need to wait until it is not @@ -359,7 +357,7 @@ func (r *QuestDBSnapshotReconciler) handlePhaseEmpty(ctx context.Context, snap * return ctrl.Result{}, nil } -func (r *QuestDBSnapshotReconciler) handlePhasePending(ctx context.Context, snap *crdv1beta1.QuestDBSnapshot, s secrets.QuestDBSecrets) (ctrl.Result, error) { +func (r *QuestDBSnapshotReconciler) handlePhasePending(ctx context.Context, snap *crdv1beta1.QuestDBSnapshot) (ctrl.Result, error) { // Add the snapshot protection finalizer to the questdb err := retry.RetryOnConflict(retry.DefaultBackoff, func() error { @@ -367,6 +365,12 @@ func (r *QuestDBSnapshotReconciler) handlePhasePending(ctx context.Context, snap questdb := &crdv1beta1.QuestDB{} err := r.Get(ctx, client.ObjectKey{Name: snap.Spec.QuestDBName, Namespace: snap.Namespace}, questdb) if err != nil { + // Fail the snapshot if the questdb is not found + if apierrors.IsNotFound(err) { + snap.Status.Phase = crdv1beta1.SnapshotFailed + err = r.Status().Update(ctx, snap) + r.Recorder.Eventf(snap, v1.EventTypeWarning, "SnapshotFailed", "QuestDB %s not found", snap.Spec.QuestDBName) + } return err } if !controllerutil.ContainsFinalizer(questdb, crdv1beta1.QuestDBSnapshotProtectionFinalizer) { @@ -380,7 +384,7 @@ func (r *QuestDBSnapshotReconciler) handlePhasePending(ctx context.Context, snap } // Create the pre-snapshot job - job, err := r.buildPreSnapshotJob(snap, s) + job, err := r.buildPreSnapshotJob(ctx, snap) if err != nil { return ctrl.Result{}, err } @@ -414,6 +418,21 @@ func (r *QuestDBSnapshotReconciler) handlePhasePending(ctx context.Context, snap } func (r *QuestDBSnapshotReconciler) handlePhaseRunning(ctx context.Context, snap *crdv1beta1.QuestDBSnapshot) (ctrl.Result, error) { + // Check that the volume snapshot class exists + if snap.Spec.VolumeSnapshotClassName != nil { + volSnapClass := &volumesnapshotv1.VolumeSnapshotClass{} + err := r.Get(ctx, client.ObjectKey{Name: *snap.Spec.VolumeSnapshotClassName}, volSnapClass) + if err != nil { + // If the volume snapshot class does not exist, fail the snapshot + if apierrors.IsNotFound(err) { + snap.Status.Phase = crdv1beta1.SnapshotFailed + err = r.Status().Update(ctx, snap) + r.Recorder.Eventf(snap, v1.EventTypeWarning, "SnapshotFailed", "VolumeSnapshotClass %s not found", *snap.Spec.VolumeSnapshotClassName) + } + return ctrl.Result{}, err + } + } + // Create the volume snapshot volumeSnap, err := r.buildVolumeSnapshot(snap) if err != nil { @@ -443,10 +462,10 @@ func (r *QuestDBSnapshotReconciler) handlePhaseRunning(ctx context.Context, snap } -func (r *QuestDBSnapshotReconciler) handlePhaseFinalizing(ctx context.Context, snap *crdv1beta1.QuestDBSnapshot, s secrets.QuestDBSecrets) (ctrl.Result, error) { +func (r *QuestDBSnapshotReconciler) handlePhaseFinalizing(ctx context.Context, snap *crdv1beta1.QuestDBSnapshot) (ctrl.Result, error) { // Create the pre-snapshot job - job, err := r.buildPostSnapshotJob(snap, s) + job, err := r.buildPostSnapshotJob(ctx, snap) if err != nil { return ctrl.Result{}, err } diff --git a/internal/controller/questdbsnapshot_controller_test.go b/internal/controller/questdbsnapshot_controller_test.go index b31b9bd..4715f24 100644 --- a/internal/controller/questdbsnapshot_controller_test.go +++ b/internal/controller/questdbsnapshot_controller_test.go @@ -19,7 +19,7 @@ import ( var _ = Describe("QuestDBSnapshot Controller", func() { var ( - timeout = time.Second * 2 + timeout = time.Second * 20000 consistencyTimeout = time.Millisecond * 600 interval = time.Millisecond * 100 ) @@ -584,4 +584,63 @@ var _ = Describe("QuestDBSnapshot Controller", func() { }, timeout, interval).Should(Succeed()) }) + Context("Validation tests", func() { + It("Should fail the snapshot immediately if the QuestDB does not exist", func() { + // QuestDB is not actually created, but namespace is + q := testutils.BuildMockQuestDB(ctx, k8sClient) + snap := testutils.BuildAndCreateMockQuestDBSnapshot(ctx, k8sClient, q) + Eventually(func(g Gomega) { + g.Expect(k8sClient.Get(ctx, client.ObjectKey{Name: snap.Name, Namespace: snap.Namespace}, snap)).To(Succeed()) + g.Expect(snap.Status.Phase).To(Equal(crdv1beta1.SnapshotFailed)) + }, consistencyTimeout, interval).Should(Succeed()) + }) + + It("Should fail the snapshot in the running phase if the VolumeSnapshot class does not exist", func() { + q := testutils.BuildAndCreateMockQuestDB(ctx, k8sClient) + // QuestDBSnapshot is not actually created so we can update the VolumeSnapshotClassName to something that does not exist + snap := testutils.BuildMockQuestDBSnapshot(ctx, k8sClient, q) + snap.Spec.VolumeSnapshotClassName = pointer.String("non-existent-class") + // Now create the QuestDBSnapshot + Expect(k8sClient.Create(ctx, snap)).To(Succeed()) + + preSnapJob := &batchv1.Job{} + By("Checking if a pre-snapshot job is created") + Eventually(func() error { + return k8sClient.Get(ctx, client.ObjectKey{ + Name: fmt.Sprintf("%s-pre-snapshot", snap.Name), + Namespace: snap.Namespace, + }, preSnapJob) + }, timeout, interval).Should(Succeed()) + + By("Checking if the phase is set to SnapshotPending") + Eventually(func() crdv1beta1.QuestDBSnapshotPhase { + k8sClient.Get(ctx, client.ObjectKey{ + Name: snap.Name, + Namespace: snap.Namespace, + }, snap) + return snap.Status.Phase + }, timeout, interval).Should(Equal(crdv1beta1.SnapshotPending)) + + By("Manually completing the pre-snapshot job") + Eventually(func(g Gomega) { + g.Expect(k8sClient.Get(ctx, client.ObjectKey{ + Name: preSnapJob.Name, + Namespace: preSnapJob.Namespace, + }, preSnapJob)).To(Succeed()) + preSnapJob.Status.Succeeded = 1 + Expect(k8sClient.Status().Update(ctx, preSnapJob)).To(Succeed()) + }, timeout, interval).Should(Succeed()) + + By("Checking if the phase is set to SnapshotFailed") + Eventually(func(g Gomega) { + g.Expect(k8sClient.Get(ctx, client.ObjectKey{ + Name: snap.Name, + Namespace: snap.Namespace, + }, snap)).Should(Succeed()) + + g.Expect(snap.Status.Phase).Should(Equal(crdv1beta1.SnapshotFailed)) + }, timeout, interval).Should(Succeed()) + }) + }) + }) diff --git a/internal/controller/questdbsnapshotschedule_controller_test.go b/internal/controller/questdbsnapshotschedule_controller_test.go index eeae70a..5c01c6b 100644 --- a/internal/controller/questdbsnapshotschedule_controller_test.go +++ b/internal/controller/questdbsnapshotschedule_controller_test.go @@ -11,6 +11,7 @@ import ( metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/client-go/kubernetes/scheme" "k8s.io/client-go/tools/record" + "k8s.io/utils/pointer" ctrl "sigs.k8s.io/controller-runtime" "sigs.k8s.io/controller-runtime/pkg/client" ) @@ -54,7 +55,7 @@ var _ = Describe("QuestDBSnapshotSchedule Controller", func() { Spec: crdv1beta1.QuestDBSnapshotScheduleSpec{ Snapshot: crdv1beta1.QuestDBSnapshotSpec{ QuestDBName: q.Name, - VolumeSnapshotClassName: "csi-hostpath-snapclass", + VolumeSnapshotClassName: pointer.String("csi-hostpath-snapclass"), }, Schedule: "*/1 * * * *", }, diff --git a/internal/controller/suite_test.go b/internal/controller/suite_test.go index 03740e6..03def1e 100644 --- a/internal/controller/suite_test.go +++ b/internal/controller/suite_test.go @@ -24,6 +24,7 @@ import ( . "github.com/onsi/ginkgo/v2" . "github.com/onsi/gomega" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/client-go/kubernetes/scheme" "k8s.io/client-go/rest" "sigs.k8s.io/controller-runtime/pkg/client" @@ -35,7 +36,10 @@ import ( volumesnapshotv1 "github.com/kubernetes-csi/external-snapshotter/client/v6/apis/volumesnapshot/v1" crdv1beta1 "github.com/questdb/questdb-operator/api/v1beta1" + //+kubebuilder:scaffold:imports + + testutils "github.com/questdb/questdb-operator/tests/utils" ) // These tests use Ginkgo (BDD-style Go testing framework). Refer to @@ -109,6 +113,16 @@ var _ = BeforeSuite(func() { // a new QuestDBSnapshotScheduleReconciler with a mock time source and call // Reconcile directly. + // Create a mock volumesnapshotclass + snapClass := &volumesnapshotv1.VolumeSnapshotClass{ + ObjectMeta: metav1.ObjectMeta{ + Name: testutils.SnapshotClassName, + }, + Driver: testutils.StorageClassName, + DeletionPolicy: volumesnapshotv1.VolumeSnapshotContentDelete, + } + Expect(k8sClient.Create(ctx, snapClass)).To(Succeed()) + // Start the manager go func() { defer GinkgoRecover() diff --git a/tests/utils/testutils.go b/tests/utils/testutils.go index 0259288..de3a8e9 100644 --- a/tests/utils/testutils.go +++ b/tests/utils/testutils.go @@ -63,9 +63,8 @@ func BuildAndCreateMockQuestDB(ctx context.Context, c client.Client) *crdv1beta1 return q } -func BuildAndCreateMockQuestDBSnapshot(ctx context.Context, c client.Client, q *crdv1beta1.QuestDB) *crdv1beta1.QuestDBSnapshot { - By("Creating a QuestDBSnapshot") - snap := &crdv1beta1.QuestDBSnapshot{ +func BuildMockQuestDBSnapshot(ctx context.Context, c client.Client, q *crdv1beta1.QuestDB) *crdv1beta1.QuestDBSnapshot { + return &crdv1beta1.QuestDBSnapshot{ ObjectMeta: metav1.ObjectMeta{ Name: fmt.Sprintf("%s-%s", q.Name, time.Now().Format("20060102150405")), Namespace: q.Namespace, @@ -73,13 +72,16 @@ func BuildAndCreateMockQuestDBSnapshot(ctx context.Context, c client.Client, q * }, Spec: crdv1beta1.QuestDBSnapshotSpec{ QuestDBName: q.Name, - VolumeSnapshotClassName: SnapshotClassName, + VolumeSnapshotClassName: pointer.String(SnapshotClassName), JobBackoffLimit: 5, }, } +} +func BuildAndCreateMockQuestDBSnapshot(ctx context.Context, c client.Client, q *crdv1beta1.QuestDB) *crdv1beta1.QuestDBSnapshot { + By("Creating a QuestDBSnapshot") + snap := BuildMockQuestDBSnapshot(ctx, c, q) Expect(c.Create(ctx, snap)).To(Succeed()) - return snap } @@ -95,7 +97,7 @@ func BuildAndCreateMockVolumeSnapshot(ctx context.Context, c client.Client, snap Source: volumesnapshotv1.VolumeSnapshotSource{ PersistentVolumeClaimName: pointer.String(snap.Spec.QuestDBName), }, - VolumeSnapshotClassName: pointer.String(snap.Spec.VolumeSnapshotClassName), + VolumeSnapshotClassName: snap.Spec.VolumeSnapshotClassName, }, } @@ -116,7 +118,7 @@ func BuildAndCreateMockQuestDBSnapshotSchedule(ctx context.Context, c client.Cli Spec: crdv1beta1.QuestDBSnapshotScheduleSpec{ Snapshot: crdv1beta1.QuestDBSnapshotSpec{ QuestDBName: q.Name, - VolumeSnapshotClassName: SnapshotClassName, + VolumeSnapshotClassName: pointer.String(SnapshotClassName), }, Schedule: "*/1 * * * *", },