k8s.io/kubernetes@v1.29.3/test/e2e/storage/regional_pd.go (about)

     1  /*
     2  Copyright 2016 The Kubernetes Authors.
     3  
     4  Licensed under the Apache License, Version 2.0 (the "License");
     5  you may not use this file except in compliance with the License.
     6  You may obtain a copy of the License at
     7  
     8      http://www.apache.org/licenses/LICENSE-2.0
     9  
    10  Unless required by applicable law or agreed to in writing, software
    11  distributed under the License is distributed on an "AS IS" BASIS,
    12  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    13  See the License for the specific language governing permissions and
    14  limitations under the License.
    15  */
    16  
    17  package storage
    18  
    19  import (
    20  	"context"
    21  
    22  	"github.com/onsi/ginkgo/v2"
    23  	"github.com/onsi/gomega"
    24  
    25  	"fmt"
    26  	"strings"
    27  	"time"
    28  
    29  	"encoding/json"
    30  
    31  	appsv1 "k8s.io/api/apps/v1"
    32  	v1 "k8s.io/api/core/v1"
    33  	storagev1 "k8s.io/api/storage/v1"
    34  	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
    35  	"k8s.io/apimachinery/pkg/labels"
    36  	"k8s.io/apimachinery/pkg/types"
    37  	"k8s.io/apimachinery/pkg/util/sets"
    38  	"k8s.io/apimachinery/pkg/util/strategicpatch"
    39  	"k8s.io/apimachinery/pkg/util/wait"
    40  	clientset "k8s.io/client-go/kubernetes"
    41  	volumehelpers "k8s.io/cloud-provider/volume/helpers"
    42  	podutil "k8s.io/kubernetes/pkg/api/v1/pod"
    43  	"k8s.io/kubernetes/test/e2e/framework"
    44  	e2enode "k8s.io/kubernetes/test/e2e/framework/node"
    45  	e2epod "k8s.io/kubernetes/test/e2e/framework/pod"
    46  	e2epv "k8s.io/kubernetes/test/e2e/framework/pv"
    47  	e2eskipper "k8s.io/kubernetes/test/e2e/framework/skipper"
    48  	"k8s.io/kubernetes/test/e2e/storage/testsuites"
    49  	"k8s.io/kubernetes/test/e2e/storage/utils"
    50  	imageutils "k8s.io/kubernetes/test/utils/image"
    51  	admissionapi "k8s.io/pod-security-admission/api"
    52  )
    53  
    54  const (
    55  	pvDeletionTimeout       = 3 * time.Minute
    56  	statefulSetReadyTimeout = 3 * time.Minute
    57  	taintKeyPrefix          = "zoneTaint_"
    58  	repdMinSize             = "200Gi"
    59  	pvcName                 = "regional-pd-vol"
    60  )
    61  
    62  var _ = utils.SIGDescribe("Regional PD", func() {
    63  	f := framework.NewDefaultFramework("regional-pd")
    64  	f.NamespacePodSecurityLevel = admissionapi.LevelPrivileged
    65  
    66  	// filled in BeforeEach
    67  	var c clientset.Interface
    68  	var ns string
    69  
    70  	ginkgo.BeforeEach(func(ctx context.Context) {
    71  		c = f.ClientSet
    72  		ns = f.Namespace.Name
    73  
    74  		e2eskipper.SkipUnlessProviderIs("gce", "gke")
    75  		e2eskipper.SkipUnlessMultizone(ctx, c)
    76  	})
    77  
    78  	ginkgo.Describe("RegionalPD", func() {
    79  		f.It("should provision storage", f.WithSlow(), func(ctx context.Context) {
    80  			testVolumeProvisioning(ctx, c, f.Timeouts, ns)
    81  		})
    82  
    83  		f.It("should provision storage with delayed binding", f.WithSlow(), func(ctx context.Context) {
    84  			testRegionalDelayedBinding(ctx, c, ns, 1 /* pvcCount */)
    85  			testRegionalDelayedBinding(ctx, c, ns, 3 /* pvcCount */)
    86  		})
    87  
    88  		f.It("should provision storage in the allowedTopologies", f.WithSlow(), func(ctx context.Context) {
    89  			testRegionalAllowedTopologies(ctx, c, ns)
    90  		})
    91  
    92  		f.It("should provision storage in the allowedTopologies with delayed binding", f.WithSlow(), func(ctx context.Context) {
    93  			testRegionalAllowedTopologiesWithDelayedBinding(ctx, c, ns, 1 /* pvcCount */)
    94  			testRegionalAllowedTopologiesWithDelayedBinding(ctx, c, ns, 3 /* pvcCount */)
    95  		})
    96  
    97  		f.It("should failover to a different zone when all nodes in one zone become unreachable", f.WithSlow(), f.WithDisruptive(), func(ctx context.Context) {
    98  			testZonalFailover(ctx, c, ns)
    99  		})
   100  	})
   101  })
   102  
   103  func testVolumeProvisioning(ctx context.Context, c clientset.Interface, t *framework.TimeoutContext, ns string) {
   104  	cloudZones := getTwoRandomZones(ctx, c)
   105  
   106  	// This test checks that dynamic provisioning can provision a volume
   107  	// that can be used to persist data among pods.
   108  	tests := []testsuites.StorageClassTest{
   109  		{
   110  			Name:           "HDD Regional PD on GCE/GKE",
   111  			CloudProviders: []string{"gce", "gke"},
   112  			Provisioner:    "kubernetes.io/gce-pd",
   113  			Timeouts:       framework.NewTimeoutContext(),
   114  			Parameters: map[string]string{
   115  				"type":             "pd-standard",
   116  				"zones":            strings.Join(cloudZones, ","),
   117  				"replication-type": "regional-pd",
   118  			},
   119  			ClaimSize:    repdMinSize,
   120  			ExpectedSize: repdMinSize,
   121  			PvCheck: func(ctx context.Context, claim *v1.PersistentVolumeClaim) {
   122  				volume := testsuites.PVWriteReadSingleNodeCheck(ctx, c, t, claim, e2epod.NodeSelection{})
   123  				gomega.Expect(volume).NotTo(gomega.BeNil())
   124  
   125  				err := checkGCEPD(volume, "pd-standard")
   126  				framework.ExpectNoError(err, "checkGCEPD")
   127  				err = verifyZonesInPV(volume, sets.NewString(cloudZones...), true /* match */)
   128  				framework.ExpectNoError(err, "verifyZonesInPV")
   129  
   130  			},
   131  		},
   132  		{
   133  			Name:           "HDD Regional PD with auto zone selection on GCE/GKE",
   134  			CloudProviders: []string{"gce", "gke"},
   135  			Provisioner:    "kubernetes.io/gce-pd",
   136  			Timeouts:       framework.NewTimeoutContext(),
   137  			Parameters: map[string]string{
   138  				"type":             "pd-standard",
   139  				"replication-type": "regional-pd",
   140  			},
   141  			ClaimSize:    repdMinSize,
   142  			ExpectedSize: repdMinSize,
   143  			PvCheck: func(ctx context.Context, claim *v1.PersistentVolumeClaim) {
   144  				volume := testsuites.PVWriteReadSingleNodeCheck(ctx, c, t, claim, e2epod.NodeSelection{})
   145  				gomega.Expect(volume).NotTo(gomega.BeNil())
   146  
   147  				err := checkGCEPD(volume, "pd-standard")
   148  				framework.ExpectNoError(err, "checkGCEPD")
   149  				zones, err := e2enode.GetClusterZones(ctx, c)
   150  				framework.ExpectNoError(err, "GetClusterZones")
   151  				err = verifyZonesInPV(volume, zones, false /* match */)
   152  				framework.ExpectNoError(err, "verifyZonesInPV")
   153  			},
   154  		},
   155  	}
   156  
   157  	for _, test := range tests {
   158  		test.Client = c
   159  		computedStorageClass := testsuites.SetupStorageClass(ctx, test.Client, newStorageClass(test, ns, "" /* suffix */))
   160  		test.Class = computedStorageClass
   161  		test.Claim = e2epv.MakePersistentVolumeClaim(e2epv.PersistentVolumeClaimConfig{
   162  			ClaimSize:        test.ClaimSize,
   163  			StorageClassName: &(test.Class.Name),
   164  			VolumeMode:       &test.VolumeMode,
   165  		}, ns)
   166  
   167  		test.TestDynamicProvisioning(ctx)
   168  	}
   169  }
   170  
   171  func testZonalFailover(ctx context.Context, c clientset.Interface, ns string) {
   172  	cloudZones := getTwoRandomZones(ctx, c)
   173  	testSpec := testsuites.StorageClassTest{
   174  		Name:           "Regional PD Failover on GCE/GKE",
   175  		CloudProviders: []string{"gce", "gke"},
   176  		Timeouts:       framework.NewTimeoutContext(),
   177  		Provisioner:    "kubernetes.io/gce-pd",
   178  		Parameters: map[string]string{
   179  			"type":             "pd-standard",
   180  			"zones":            strings.Join(cloudZones, ","),
   181  			"replication-type": "regional-pd",
   182  		},
   183  		ClaimSize:    repdMinSize,
   184  		ExpectedSize: repdMinSize,
   185  	}
   186  	class := newStorageClass(testSpec, ns, "" /* suffix */)
   187  	claimTemplate := e2epv.MakePersistentVolumeClaim(e2epv.PersistentVolumeClaimConfig{
   188  		NamePrefix:       pvcName,
   189  		ClaimSize:        testSpec.ClaimSize,
   190  		StorageClassName: &(class.Name),
   191  		VolumeMode:       &testSpec.VolumeMode,
   192  	}, ns)
   193  	statefulSet, service, regionalPDLabels := newStatefulSet(claimTemplate, ns)
   194  
   195  	ginkgo.By("creating a StorageClass " + class.Name)
   196  	_, err := c.StorageV1().StorageClasses().Create(ctx, class, metav1.CreateOptions{})
   197  	framework.ExpectNoError(err)
   198  	defer func() {
   199  		framework.Logf("deleting storage class %s", class.Name)
   200  		framework.ExpectNoError(c.StorageV1().StorageClasses().Delete(ctx, class.Name, metav1.DeleteOptions{}),
   201  			"Error deleting StorageClass %s", class.Name)
   202  	}()
   203  
   204  	ginkgo.By("creating a StatefulSet")
   205  	_, err = c.CoreV1().Services(ns).Create(ctx, service, metav1.CreateOptions{})
   206  	framework.ExpectNoError(err)
   207  	_, err = c.AppsV1().StatefulSets(ns).Create(ctx, statefulSet, metav1.CreateOptions{})
   208  	framework.ExpectNoError(err)
   209  
   210  	ginkgo.DeferCleanup(func(ctx context.Context) {
   211  		framework.Logf("deleting statefulset%q/%q", statefulSet.Namespace, statefulSet.Name)
   212  		// typically this claim has already been deleted
   213  		framework.ExpectNoError(c.AppsV1().StatefulSets(ns).Delete(ctx, statefulSet.Name, metav1.DeleteOptions{}),
   214  			"Error deleting StatefulSet %s", statefulSet.Name)
   215  
   216  		framework.Logf("deleting claims in namespace %s", ns)
   217  		pvc := getPVC(ctx, c, ns, regionalPDLabels)
   218  		framework.ExpectNoError(c.CoreV1().PersistentVolumeClaims(pvc.Namespace).Delete(ctx, pvc.Name, metav1.DeleteOptions{}),
   219  			"Error deleting claim %s.", pvc.Name)
   220  		if pvc.Spec.VolumeName != "" {
   221  			err = e2epv.WaitForPersistentVolumeDeleted(ctx, c, pvc.Spec.VolumeName, framework.Poll, pvDeletionTimeout)
   222  			if err != nil {
   223  				framework.Logf("WARNING: PV %s is not yet deleted, and subsequent tests may be affected.", pvc.Spec.VolumeName)
   224  			}
   225  		}
   226  	})
   227  
   228  	err = waitForStatefulSetReplicasReady(ctx, statefulSet.Name, ns, c, framework.Poll, statefulSetReadyTimeout)
   229  	if err != nil {
   230  		pod := getPod(ctx, c, ns, regionalPDLabels)
   231  		if !podutil.IsPodReadyConditionTrue(pod.Status) {
   232  			framework.Failf("The statefulset pod %s was expected to be ready, instead has the following conditions: %v", pod.Name, pod.Status.Conditions)
   233  		}
   234  		framework.ExpectNoError(err)
   235  	}
   236  
   237  	pvc := getPVC(ctx, c, ns, regionalPDLabels)
   238  
   239  	ginkgo.By("getting zone information from pod")
   240  	pod := getPod(ctx, c, ns, regionalPDLabels)
   241  	nodeName := pod.Spec.NodeName
   242  	node, err := c.CoreV1().Nodes().Get(ctx, nodeName, metav1.GetOptions{})
   243  	framework.ExpectNoError(err)
   244  	podZone := node.Labels[v1.LabelTopologyZone]
   245  
   246  	ginkgo.By("tainting nodes in the zone the pod is scheduled in")
   247  	selector := labels.SelectorFromSet(labels.Set(map[string]string{v1.LabelTopologyZone: podZone}))
   248  	nodesInZone, err := c.CoreV1().Nodes().List(ctx, metav1.ListOptions{LabelSelector: selector.String()})
   249  	framework.ExpectNoError(err)
   250  	addTaint(ctx, c, ns, nodesInZone.Items, podZone)
   251  
   252  	ginkgo.By("deleting StatefulSet pod")
   253  	err = c.CoreV1().Pods(ns).Delete(ctx, pod.Name, metav1.DeleteOptions{})
   254  
   255  	// Verify the pod is scheduled in the other zone.
   256  	ginkgo.By("verifying the pod is scheduled in a different zone.")
   257  	var otherZone string
   258  	if cloudZones[0] == podZone {
   259  		otherZone = cloudZones[1]
   260  	} else {
   261  		otherZone = cloudZones[0]
   262  	}
   263  	waitErr := wait.PollUntilContextTimeout(ctx, framework.Poll, statefulSetReadyTimeout, true, func(ctx context.Context) (bool, error) {
   264  		framework.Logf("Checking whether new pod is scheduled in zone %q", otherZone)
   265  		pod := getPod(ctx, c, ns, regionalPDLabels)
   266  		node, err := c.CoreV1().Nodes().Get(ctx, pod.Spec.NodeName, metav1.GetOptions{})
   267  		if err != nil {
   268  			return false, nil
   269  		}
   270  		newPodZone := node.Labels[v1.LabelTopologyZone]
   271  		return newPodZone == otherZone, nil
   272  	})
   273  	framework.ExpectNoError(waitErr, "Error waiting for pod to be scheduled in a different zone (%q): %v", otherZone, err)
   274  
   275  	err = waitForStatefulSetReplicasReady(ctx, statefulSet.Name, ns, c, 3*time.Second, framework.RestartPodReadyAgainTimeout)
   276  	if err != nil {
   277  		pod := getPod(ctx, c, ns, regionalPDLabels)
   278  		if !podutil.IsPodReadyConditionTrue(pod.Status) {
   279  			framework.Failf("The statefulset pod %s was expected to be ready, instead has the following conditions: %v", pod.Name, pod.Status.Conditions)
   280  		}
   281  		framework.ExpectNoError(err)
   282  	}
   283  
   284  	ginkgo.By("verifying the same PVC is used by the new pod")
   285  	gomega.Expect(getPVC(ctx, c, ns, regionalPDLabels).Name).To(gomega.Equal(pvc.Name), "The same PVC should be used after failover.")
   286  
   287  	ginkgo.By("verifying the container output has 2 lines, indicating the pod has been created twice using the same regional PD.")
   288  	logs, err := e2epod.GetPodLogs(ctx, c, ns, pod.Name, "")
   289  	framework.ExpectNoError(err,
   290  		"Error getting logs from pod %s in namespace %s", pod.Name, ns)
   291  	lineCount := len(strings.Split(strings.TrimSpace(logs), "\n"))
   292  	expectedLineCount := 2
   293  	gomega.Expect(lineCount).To(gomega.Equal(expectedLineCount), "Line count of the written file should be %d.", expectedLineCount)
   294  
   295  }
   296  
   297  func addTaint(ctx context.Context, c clientset.Interface, ns string, nodes []v1.Node, podZone string) {
   298  	for _, node := range nodes {
   299  		oldData, err := json.Marshal(node)
   300  		framework.ExpectNoError(err)
   301  
   302  		node.Spec.Taints = append(node.Spec.Taints, v1.Taint{
   303  			Key:    taintKeyPrefix + ns,
   304  			Value:  podZone,
   305  			Effect: v1.TaintEffectNoSchedule,
   306  		})
   307  
   308  		newData, err := json.Marshal(node)
   309  		framework.ExpectNoError(err)
   310  
   311  		patchBytes, err := strategicpatch.CreateTwoWayMergePatch(oldData, newData, v1.Node{})
   312  		framework.ExpectNoError(err)
   313  
   314  		reversePatchBytes, err := strategicpatch.CreateTwoWayMergePatch(newData, oldData, v1.Node{})
   315  		framework.ExpectNoError(err)
   316  
   317  		_, err = c.CoreV1().Nodes().Patch(ctx, node.Name, types.StrategicMergePatchType, patchBytes, metav1.PatchOptions{})
   318  		framework.ExpectNoError(err)
   319  
   320  		nodeName := node.Name
   321  		ginkgo.DeferCleanup(func(ctx context.Context) {
   322  			framework.Logf("removing taint for node %q", nodeName)
   323  			_, err := c.CoreV1().Nodes().Patch(ctx, nodeName, types.StrategicMergePatchType, reversePatchBytes, metav1.PatchOptions{})
   324  			framework.ExpectNoError(err)
   325  		})
   326  	}
   327  }
   328  
   329  func testRegionalDelayedBinding(ctx context.Context, c clientset.Interface, ns string, pvcCount int) {
   330  	test := testsuites.StorageClassTest{
   331  		Client:      c,
   332  		Name:        "Regional PD storage class with waitForFirstConsumer test on GCE",
   333  		Provisioner: "kubernetes.io/gce-pd",
   334  		Timeouts:    framework.NewTimeoutContext(),
   335  		Parameters: map[string]string{
   336  			"type":             "pd-standard",
   337  			"replication-type": "regional-pd",
   338  		},
   339  		ClaimSize:    repdMinSize,
   340  		DelayBinding: true,
   341  	}
   342  
   343  	suffix := "delayed-regional"
   344  
   345  	test.Class = testsuites.SetupStorageClass(ctx, test.Client, newStorageClass(test, ns, suffix))
   346  	var claims []*v1.PersistentVolumeClaim
   347  	for i := 0; i < pvcCount; i++ {
   348  		claim := e2epv.MakePersistentVolumeClaim(e2epv.PersistentVolumeClaimConfig{
   349  			ClaimSize:        test.ClaimSize,
   350  			StorageClassName: &(test.Class.Name),
   351  			VolumeMode:       &test.VolumeMode,
   352  		}, ns)
   353  		claims = append(claims, claim)
   354  	}
   355  	pvs, node := test.TestBindingWaitForFirstConsumerMultiPVC(ctx, claims, nil /* node selector */, false /* expect unschedulable */)
   356  	if node == nil {
   357  		framework.Failf("unexpected nil node found")
   358  	}
   359  	zone, ok := node.Labels[v1.LabelTopologyZone]
   360  	if !ok {
   361  		framework.Failf("label %s not found on Node", v1.LabelTopologyZone)
   362  	}
   363  	for _, pv := range pvs {
   364  		checkZoneFromLabelAndAffinity(pv, zone, false)
   365  	}
   366  }
   367  
   368  func testRegionalAllowedTopologies(ctx context.Context, c clientset.Interface, ns string) {
   369  	test := testsuites.StorageClassTest{
   370  		Name:        "Regional PD storage class with allowedTopologies test on GCE",
   371  		Provisioner: "kubernetes.io/gce-pd",
   372  		Timeouts:    framework.NewTimeoutContext(),
   373  		Parameters: map[string]string{
   374  			"type":             "pd-standard",
   375  			"replication-type": "regional-pd",
   376  		},
   377  		ClaimSize:    repdMinSize,
   378  		ExpectedSize: repdMinSize,
   379  	}
   380  
   381  	suffix := "topo-regional"
   382  	test.Client = c
   383  	test.Class = testsuites.SetupStorageClass(ctx, test.Client, newStorageClass(test, ns, suffix))
   384  	zones := getTwoRandomZones(ctx, c)
   385  	addAllowedTopologiesToStorageClass(c, test.Class, zones)
   386  	test.Claim = e2epv.MakePersistentVolumeClaim(e2epv.PersistentVolumeClaimConfig{
   387  		NamePrefix:       pvcName,
   388  		ClaimSize:        test.ClaimSize,
   389  		StorageClassName: &(test.Class.Name),
   390  		VolumeMode:       &test.VolumeMode,
   391  	}, ns)
   392  
   393  	pv := test.TestDynamicProvisioning(ctx)
   394  	checkZonesFromLabelAndAffinity(pv, sets.NewString(zones...), true)
   395  }
   396  
   397  func testRegionalAllowedTopologiesWithDelayedBinding(ctx context.Context, c clientset.Interface, ns string, pvcCount int) {
   398  	test := testsuites.StorageClassTest{
   399  		Client:      c,
   400  		Timeouts:    framework.NewTimeoutContext(),
   401  		Name:        "Regional PD storage class with allowedTopologies and waitForFirstConsumer test on GCE",
   402  		Provisioner: "kubernetes.io/gce-pd",
   403  		Parameters: map[string]string{
   404  			"type":             "pd-standard",
   405  			"replication-type": "regional-pd",
   406  		},
   407  		ClaimSize:    repdMinSize,
   408  		DelayBinding: true,
   409  	}
   410  
   411  	suffix := "topo-delayed-regional"
   412  	test.Class = testsuites.SetupStorageClass(ctx, test.Client, newStorageClass(test, ns, suffix))
   413  	topoZones := getTwoRandomZones(ctx, c)
   414  	addAllowedTopologiesToStorageClass(c, test.Class, topoZones)
   415  	var claims []*v1.PersistentVolumeClaim
   416  	for i := 0; i < pvcCount; i++ {
   417  		claim := e2epv.MakePersistentVolumeClaim(e2epv.PersistentVolumeClaimConfig{
   418  			ClaimSize:        test.ClaimSize,
   419  			StorageClassName: &(test.Class.Name),
   420  			VolumeMode:       &test.VolumeMode,
   421  		}, ns)
   422  		claims = append(claims, claim)
   423  	}
   424  	pvs, node := test.TestBindingWaitForFirstConsumerMultiPVC(ctx, claims, nil /* node selector */, false /* expect unschedulable */)
   425  	if node == nil {
   426  		framework.Failf("unexpected nil node found")
   427  	}
   428  	nodeZone, ok := node.Labels[v1.LabelTopologyZone]
   429  	if !ok {
   430  		framework.Failf("label %s not found on Node", v1.LabelTopologyZone)
   431  	}
   432  	zoneFound := false
   433  	for _, zone := range topoZones {
   434  		if zone == nodeZone {
   435  			zoneFound = true
   436  			break
   437  		}
   438  	}
   439  	if !zoneFound {
   440  		framework.Failf("zones specified in AllowedTopologies: %v does not contain zone of node where PV got provisioned: %s", topoZones, nodeZone)
   441  	}
   442  	for _, pv := range pvs {
   443  		checkZonesFromLabelAndAffinity(pv, sets.NewString(topoZones...), true)
   444  	}
   445  }
   446  
   447  func getPVC(ctx context.Context, c clientset.Interface, ns string, pvcLabels map[string]string) *v1.PersistentVolumeClaim {
   448  	selector := labels.Set(pvcLabels).AsSelector()
   449  	options := metav1.ListOptions{LabelSelector: selector.String()}
   450  	pvcList, err := c.CoreV1().PersistentVolumeClaims(ns).List(ctx, options)
   451  	framework.ExpectNoError(err)
   452  	gomega.Expect(pvcList.Items).To(gomega.HaveLen(1), "There should be exactly 1 PVC matched.")
   453  
   454  	return &pvcList.Items[0]
   455  }
   456  
   457  func getPod(ctx context.Context, c clientset.Interface, ns string, podLabels map[string]string) *v1.Pod {
   458  	selector := labels.Set(podLabels).AsSelector()
   459  	options := metav1.ListOptions{LabelSelector: selector.String()}
   460  	podList, err := c.CoreV1().Pods(ns).List(ctx, options)
   461  	framework.ExpectNoError(err)
   462  	gomega.Expect(podList.Items).To(gomega.HaveLen(1), "There should be exactly 1 pod matched.")
   463  
   464  	return &podList.Items[0]
   465  }
   466  
   467  func addAllowedTopologiesToStorageClass(c clientset.Interface, sc *storagev1.StorageClass, zones []string) {
   468  	term := v1.TopologySelectorTerm{
   469  		MatchLabelExpressions: []v1.TopologySelectorLabelRequirement{
   470  			{
   471  				Key:    v1.LabelTopologyZone,
   472  				Values: zones,
   473  			},
   474  		},
   475  	}
   476  	sc.AllowedTopologies = append(sc.AllowedTopologies, term)
   477  }
   478  
   479  // Generates the spec of a StatefulSet with 1 replica that mounts a Regional PD.
   480  func newStatefulSet(claimTemplate *v1.PersistentVolumeClaim, ns string) (sts *appsv1.StatefulSet, svc *v1.Service, labels map[string]string) {
   481  	var replicas int32 = 1
   482  	labels = map[string]string{"app": "regional-pd-workload"}
   483  
   484  	svc = &v1.Service{
   485  		ObjectMeta: metav1.ObjectMeta{
   486  			Name:      "regional-pd-service",
   487  			Namespace: ns,
   488  			Labels:    labels,
   489  		},
   490  		Spec: v1.ServiceSpec{
   491  			Ports: []v1.ServicePort{{
   492  				Port: 80,
   493  				Name: "web",
   494  			}},
   495  			ClusterIP: v1.ClusterIPNone,
   496  			Selector:  labels,
   497  		},
   498  	}
   499  
   500  	sts = &appsv1.StatefulSet{
   501  		ObjectMeta: metav1.ObjectMeta{
   502  			Name:      "regional-pd-sts",
   503  			Namespace: ns,
   504  		},
   505  		Spec: appsv1.StatefulSetSpec{
   506  			Selector: &metav1.LabelSelector{
   507  				MatchLabels: labels,
   508  			},
   509  			ServiceName:          svc.Name,
   510  			Replicas:             &replicas,
   511  			Template:             *newPodTemplate(labels),
   512  			VolumeClaimTemplates: []v1.PersistentVolumeClaim{*claimTemplate},
   513  		},
   514  	}
   515  
   516  	return
   517  }
   518  
   519  func newPodTemplate(labels map[string]string) *v1.PodTemplateSpec {
   520  	return &v1.PodTemplateSpec{
   521  		ObjectMeta: metav1.ObjectMeta{
   522  			Labels: labels,
   523  		},
   524  		Spec: v1.PodSpec{
   525  			Containers: []v1.Container{
   526  				// This container writes its pod name to a file in the Regional PD
   527  				// and prints the entire file to stdout.
   528  				{
   529  					Name:    "busybox",
   530  					Image:   imageutils.GetE2EImage(imageutils.BusyBox),
   531  					Command: []string{"sh", "-c"},
   532  					Args: []string{
   533  						"echo ${POD_NAME} >> /mnt/data/regional-pd/pods.txt;" +
   534  							"cat /mnt/data/regional-pd/pods.txt;" +
   535  							"sleep 3600;",
   536  					},
   537  					Env: []v1.EnvVar{{
   538  						Name: "POD_NAME",
   539  						ValueFrom: &v1.EnvVarSource{
   540  							FieldRef: &v1.ObjectFieldSelector{
   541  								FieldPath: "metadata.name",
   542  							},
   543  						},
   544  					}},
   545  					Ports: []v1.ContainerPort{{
   546  						ContainerPort: 80,
   547  						Name:          "web",
   548  					}},
   549  					VolumeMounts: []v1.VolumeMount{{
   550  						Name:      pvcName,
   551  						MountPath: "/mnt/data/regional-pd",
   552  					}},
   553  				},
   554  			},
   555  		},
   556  	}
   557  }
   558  
   559  func getTwoRandomZones(ctx context.Context, c clientset.Interface) []string {
   560  	zones, err := e2enode.GetClusterZones(ctx, c)
   561  	framework.ExpectNoError(err)
   562  	gomega.Expect(zones.Len()).To(gomega.BeNumerically(">=", 2),
   563  		"The test should only be run in multizone clusters.")
   564  
   565  	zone1, _ := zones.PopAny()
   566  	zone2, _ := zones.PopAny()
   567  	return []string{zone1, zone2}
   568  }
   569  
   570  // If match is true, check if zones in PV exactly match zones given.
   571  // Otherwise, check whether zones in PV is superset of zones given.
   572  func verifyZonesInPV(volume *v1.PersistentVolume, zones sets.String, match bool) error {
   573  	pvZones, err := volumehelpers.LabelZonesToSet(volume.Labels[v1.LabelTopologyZone])
   574  	if err != nil {
   575  		return err
   576  	}
   577  
   578  	if match && zones.Equal(pvZones) || !match && zones.IsSuperset(pvZones) {
   579  		return nil
   580  	}
   581  
   582  	return fmt.Errorf("Zones in StorageClass are %v, but zones in PV are %v", zones, pvZones)
   583  
   584  }
   585  
   586  func checkZoneFromLabelAndAffinity(pv *v1.PersistentVolume, zone string, matchZone bool) {
   587  	checkZonesFromLabelAndAffinity(pv, sets.NewString(zone), matchZone)
   588  }
   589  
   590  // checkZoneLabelAndAffinity checks the LabelTopologyZone label of PV and terms
   591  // with key LabelTopologyZone in PV's node affinity contains zone
   592  // matchZones is used to indicate if zones should match perfectly
   593  func checkZonesFromLabelAndAffinity(pv *v1.PersistentVolume, zones sets.String, matchZones bool) {
   594  	ginkgo.By("checking PV's zone label and node affinity terms match expected zone")
   595  	if pv == nil {
   596  		framework.Failf("nil pv passed")
   597  	}
   598  	pvLabel, ok := pv.Labels[v1.LabelTopologyZone]
   599  	if !ok {
   600  		framework.Failf("label %s not found on PV", v1.LabelTopologyZone)
   601  	}
   602  
   603  	zonesFromLabel, err := volumehelpers.LabelZonesToSet(pvLabel)
   604  	if err != nil {
   605  		framework.Failf("unable to parse zone labels %s: %v", pvLabel, err)
   606  	}
   607  	if matchZones && !zonesFromLabel.Equal(zones) {
   608  		framework.Failf("value[s] of %s label for PV: %v does not match expected zone[s]: %v", v1.LabelTopologyZone, zonesFromLabel, zones)
   609  	}
   610  	if !matchZones && !zonesFromLabel.IsSuperset(zones) {
   611  		framework.Failf("value[s] of %s label for PV: %v does not contain expected zone[s]: %v", v1.LabelTopologyZone, zonesFromLabel, zones)
   612  	}
   613  	if pv.Spec.NodeAffinity == nil {
   614  		framework.Failf("node affinity not found in PV spec %v", pv.Spec)
   615  	}
   616  	if len(pv.Spec.NodeAffinity.Required.NodeSelectorTerms) == 0 {
   617  		framework.Failf("node selector terms not found in PV spec %v", pv.Spec)
   618  	}
   619  
   620  	for _, term := range pv.Spec.NodeAffinity.Required.NodeSelectorTerms {
   621  		keyFound := false
   622  		for _, r := range term.MatchExpressions {
   623  			if r.Key != v1.LabelTopologyZone {
   624  				continue
   625  			}
   626  			keyFound = true
   627  			zonesFromNodeAffinity := sets.NewString(r.Values...)
   628  			if matchZones && !zonesFromNodeAffinity.Equal(zones) {
   629  				framework.Failf("zones from NodeAffinity of PV: %v does not equal expected zone[s]: %v", zonesFromNodeAffinity, zones)
   630  			}
   631  			if !matchZones && !zonesFromNodeAffinity.IsSuperset(zones) {
   632  				framework.Failf("zones from NodeAffinity of PV: %v does not contain expected zone[s]: %v", zonesFromNodeAffinity, zones)
   633  			}
   634  			break
   635  		}
   636  		if !keyFound {
   637  			framework.Failf("label %s not found in term %v", v1.LabelTopologyZone, term)
   638  		}
   639  	}
   640  }
   641  
   642  // waitForStatefulSetReplicasReady waits for all replicas of a StatefulSet to become ready or until timeout occurs, whichever comes first.
   643  func waitForStatefulSetReplicasReady(ctx context.Context, statefulSetName, ns string, c clientset.Interface, Poll, timeout time.Duration) error {
   644  	framework.Logf("Waiting up to %v for StatefulSet %s to have all replicas ready", timeout, statefulSetName)
   645  	for start := time.Now(); time.Since(start) < timeout; time.Sleep(Poll) {
   646  		sts, err := c.AppsV1().StatefulSets(ns).Get(ctx, statefulSetName, metav1.GetOptions{})
   647  		if err != nil {
   648  			framework.Logf("Get StatefulSet %s failed, ignoring for %v: %v", statefulSetName, Poll, err)
   649  			continue
   650  		}
   651  		if sts.Status.ReadyReplicas == *sts.Spec.Replicas {
   652  			framework.Logf("All %d replicas of StatefulSet %s are ready. (%v)", sts.Status.ReadyReplicas, statefulSetName, time.Since(start))
   653  			return nil
   654  		}
   655  		framework.Logf("StatefulSet %s found but there are %d ready replicas and %d total replicas.", statefulSetName, sts.Status.ReadyReplicas, *sts.Spec.Replicas)
   656  	}
   657  	return fmt.Errorf("StatefulSet %s still has unready pods within %v", statefulSetName, timeout)
   658  }