From 4eb795e535db6febea28a846dd407b2c39eb7c63 Mon Sep 17 00:00:00 2001 From: ShravaniVangur Date: Thu, 27 Mar 2025 22:35:29 +0530 Subject: [PATCH] e2e: test for PVC with volumeBindingMode on helm installation Test PVC binding with WaitForFirstConsumer in Helm installation. Signed-off-by: ShravaniVangur --- e2e/cephfs.go | 17 ++++++++++++++ e2e/cephfs_helper.go | 51 +++++++++++++++++++++++++++++++++++++---- e2e/pvc.go | 9 ++++++++ e2e/utils.go | 54 ++++++++++++++++++++++++++++++++++++++++++++ 4 files changed, 126 insertions(+), 5 deletions(-) diff --git a/e2e/cephfs.go b/e2e/cephfs.go index 14d0a967a..b7b1ffd94 100644 --- a/e2e/cephfs.go +++ b/e2e/cephfs.go @@ -331,6 +331,23 @@ var _ = Describe(cephfsType, func() { }) } + By("verify PVC and App Binding with volumeBindingMode:WaitForFirstConsumer", func() { + err := createCephfsStorageClassWaitForFirstConsumer(f.ClientSet, f, true, nil) + if err != nil { + framework.Failf("failed to create CephFS storageclass: %v", err) + } + + err = validatePVCAndAppWaitForFirstConsumer(pvcPath, appPath, f) + if err != nil { + framework.Failf("failed to validate CephFS pvc and application binding: %v", err) + } + + err = deleteResource(cephFSExamplePath + "storageclass.yaml") + if err != nil { + framework.Failf("failed to delete CephFS storageclass: %v", err) + } + }) + By("verify mountOptions support", func() { err := createCephfsStorageClass(f.ClientSet, f, true, nil) if err != nil { diff --git a/e2e/cephfs_helper.go b/e2e/cephfs_helper.go index 1fa61cb46..8515fb981 100644 --- a/e2e/cephfs_helper.go +++ b/e2e/cephfs_helper.go @@ -26,6 +26,7 @@ import ( snapapi "github.com/kubernetes-csi/external-snapshotter/client/v8/apis/volumesnapshot/v1" v1 "k8s.io/api/core/v1" + scv1 "k8s.io/api/storage/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/util/wait" "k8s.io/client-go/kubernetes" @@ -62,11 +63,49 @@ func createCephfsStorageClass( params map[string]string, ) error { scPath := fmt.Sprintf("%s/%s", cephFSExamplePath, "storageclass.yaml") - sc, err := getStorageClass(scPath) + scValue, err := getStorageClass(scPath) if err != nil { return err } + sc := &scValue + + err = updateStorageClassParameters(&scValue, params, enablePool, f) + if err != nil { + return err + } + + return createStorageClass(c, sc) +} + +func createCephfsStorageClassWaitForFirstConsumer(c kubernetes.Interface, f *framework.Framework, + enablePool bool, + params map[string]string) error { + scPath := fmt.Sprintf("%s/%s", cephFSExamplePath, "storageclass.yaml") + scValue, err := getStorageClass(scPath) + if err != nil { + return err + } + + sc := &scValue + + err = updateStorageClassParameters(&scValue, params, enablePool, f) + if err != nil { + return err + } + + // Set the volume binding mode to WaitForFirstConsumer + value := scv1.VolumeBindingWaitForFirstConsumer + sc.VolumeBindingMode = &value + + return createStorageClass(c, sc) +} + +func updateStorageClassParameters(sc *scv1.StorageClass, params map[string]string, enablePool bool, f *framework.Framework) error { + if sc == nil { + return fmt.Errorf("StorageClass is nil") + } + sc.Parameters["fsName"] = fileSystemName sc.Parameters["csi.storage.k8s.io/provisioner-secret-namespace"] = cephCSINamespace sc.Parameters["csi.storage.k8s.io/provisioner-secret-name"] = cephFSProvisionerSecretName @@ -93,18 +132,20 @@ func createCephfsStorageClass( // fetch and set fsID from the cluster if not set in params if _, found := params["clusterID"]; !found { - var fsID string - fsID, err = getClusterID(f) + fsID, err := getClusterID(f) if err != nil { return fmt.Errorf("failed to get clusterID: %w", err) } sc.Parameters["clusterID"] = fsID } - timeout := time.Duration(deployTimeout) * time.Minute + return nil +} +func createStorageClass(c kubernetes.Interface, sc *scv1.StorageClass) error { + timeout := time.Duration(deployTimeout) * time.Minute return wait.PollUntilContextTimeout(context.TODO(), poll, timeout, true, func(ctx context.Context) (bool, error) { - _, err = c.StorageV1().StorageClasses().Create(ctx, &sc, metav1.CreateOptions{}) + _, err := c.StorageV1().StorageClasses().Create(ctx, sc, metav1.CreateOptions{}) if err != nil { framework.Logf("error creating StorageClass %q: %v", sc.Name, err) if isRetryableAPIError(err) { diff --git a/e2e/pvc.go b/e2e/pvc.go index 932d66b33..277359794 100644 --- a/e2e/pvc.go +++ b/e2e/pvc.go @@ -130,6 +130,15 @@ func createPVCAndPV(c kubernetes.Interface, pvc *v1.PersistentVolumeClaim, pv *v return err } +func createPVC(c kubernetes.Interface, pvc *v1.PersistentVolumeClaim) error { + _, err := c.CoreV1().PersistentVolumeClaims(pvc.Namespace).Create(context.TODO(), pvc, metav1.CreateOptions{}) + if err != nil { + return fmt.Errorf("failed to create pvc: %w", err) + } + + return err +} + func deletePVCAndPV(c kubernetes.Interface, pvc *v1.PersistentVolumeClaim, pv *v1.PersistentVolume, t int) error { ctx := context.TODO() err := c.CoreV1().PersistentVolumeClaims(pvc.Namespace).Delete(ctx, pvc.Name, metav1.DeleteOptions{}) diff --git a/e2e/utils.go b/e2e/utils.go index 04cc03f2b..7c423b17e 100644 --- a/e2e/utils.go +++ b/e2e/utils.go @@ -508,6 +508,60 @@ func validatePVCAndAppBinding(pvcPath, appPath string, f *framework.Framework) e return err } +// validatePVCAndAppWaitForFirstConsumer creates a Pending PVC, starts an app, and verifies it to become Bound. +func validatePVCAndAppWaitForFirstConsumer(pvcPath, appPath string, f *framework.Framework) error { + pvc, err := loadPVC(pvcPath) + if err != nil { + return err + } + pvc.Namespace = f.UniqueName + + app, err := loadApp(appPath) + if err != nil { + return err + } + app.Namespace = f.UniqueName + + err = createPVC(f.ClientSet, pvc) + if err != nil { + return err + } + + err = waitForPVCPhase(f.ClientSet, pvc.Namespace, pvc.Name, v1.ClaimPending, 30*time.Second) + if err != nil { + return fmt.Errorf("PVC did not stay in Pending state as expected: %w", err) + } + + err = createApp(f.ClientSet, app, deployTimeout) + if err != nil { + return err + } + + err = waitForPVCPhase(f.ClientSet, pvc.Namespace, pvc.Name, v1.ClaimBound, 30*time.Second) + if err != nil { + return fmt.Errorf("PVC did not reach Bound state after creating the pod: %w", err) + } + + err = deletePVCAndApp("", f, pvc, app) + + return err +} + +func waitForPVCPhase(c kubernetes.Interface, namespace, name string, phase v1.PersistentVolumeClaimPhase, timeout time.Duration) error { + return wait.PollUntilContextTimeout(context.TODO(), poll, timeout, true, func(ctx context.Context) (bool, error) { + pvc, err := c.CoreV1().PersistentVolumeClaims(namespace).Get(context.TODO(), name, metav1.GetOptions{}) + if err != nil { + return false, fmt.Errorf("error fetching PVC %q: %w", name, err) + } + if pvc.Status.Phase == phase { + return true, nil + } + framework.Logf("Waiting for PVC %q to reach phase %q, current phase: %q", name, phase, pvc.Status.Phase) + + return false, nil + }) +} + func getMountType(selector, mountPath string, f *framework.Framework) (string, error) { opt := metav1.ListOptions{ LabelSelector: selector,