k8s.io/kubernetes@v1.31.0-alpha.0.0.20240520171757-56147500dadc/test/integration/volume/attach_detach_test.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 volume 18 19 import ( 20 "context" 21 "fmt" 22 "testing" 23 "time" 24 25 v1 "k8s.io/api/core/v1" 26 "k8s.io/apimachinery/pkg/api/resource" 27 metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 28 "k8s.io/apimachinery/pkg/util/wait" 29 clientgoinformers "k8s.io/client-go/informers" 30 clientset "k8s.io/client-go/kubernetes" 31 restclient "k8s.io/client-go/rest" 32 "k8s.io/client-go/tools/cache" 33 kubeapiservertesting "k8s.io/kubernetes/cmd/kube-apiserver/app/testing" 34 "k8s.io/kubernetes/pkg/controller/volume/attachdetach" 35 volumecache "k8s.io/kubernetes/pkg/controller/volume/attachdetach/cache" 36 "k8s.io/kubernetes/pkg/controller/volume/persistentvolume" 37 persistentvolumeoptions "k8s.io/kubernetes/pkg/controller/volume/persistentvolume/options" 38 "k8s.io/kubernetes/pkg/volume" 39 volumetest "k8s.io/kubernetes/pkg/volume/testing" 40 "k8s.io/kubernetes/pkg/volume/util" 41 "k8s.io/kubernetes/test/integration/framework" 42 "k8s.io/kubernetes/test/utils/ktesting" 43 ) 44 45 func fakePodWithVol(namespace string) *v1.Pod { 46 fakePod := &v1.Pod{ 47 ObjectMeta: metav1.ObjectMeta{ 48 Namespace: namespace, 49 Name: "fakepod", 50 }, 51 Spec: v1.PodSpec{ 52 Containers: []v1.Container{ 53 { 54 Name: "fake-container", 55 Image: "nginx", 56 VolumeMounts: []v1.VolumeMount{ 57 { 58 Name: "fake-mount", 59 MountPath: "/var/www/html", 60 }, 61 }, 62 }, 63 }, 64 Volumes: []v1.Volume{ 65 { 66 Name: "fake-mount", 67 VolumeSource: v1.VolumeSource{ 68 HostPath: &v1.HostPathVolumeSource{ 69 Path: "/var/www/html", 70 }, 71 }, 72 }, 73 }, 74 NodeName: "node-sandbox", 75 }, 76 } 77 return fakePod 78 } 79 80 func fakePodWithPVC(name, pvcName, namespace string) (*v1.Pod, *v1.PersistentVolumeClaim) { 81 fakePod := &v1.Pod{ 82 ObjectMeta: metav1.ObjectMeta{ 83 Namespace: namespace, 84 Name: name, 85 }, 86 Spec: v1.PodSpec{ 87 Containers: []v1.Container{ 88 { 89 Name: "fake-container", 90 Image: "nginx", 91 VolumeMounts: []v1.VolumeMount{ 92 { 93 Name: "fake-mount", 94 MountPath: "/var/www/html", 95 }, 96 }, 97 }, 98 }, 99 Volumes: []v1.Volume{ 100 { 101 Name: "fake-mount", 102 VolumeSource: v1.VolumeSource{ 103 PersistentVolumeClaim: &v1.PersistentVolumeClaimVolumeSource{ 104 ClaimName: pvcName, 105 }, 106 }, 107 }, 108 }, 109 NodeName: "node-sandbox", 110 }, 111 } 112 class := "fake-sc" 113 fakePVC := &v1.PersistentVolumeClaim{ 114 ObjectMeta: metav1.ObjectMeta{ 115 Namespace: namespace, 116 Name: pvcName, 117 }, 118 Spec: v1.PersistentVolumeClaimSpec{ 119 AccessModes: []v1.PersistentVolumeAccessMode{ 120 v1.ReadWriteOnce, 121 }, 122 Resources: v1.VolumeResourceRequirements{ 123 Requests: v1.ResourceList{ 124 v1.ResourceName(v1.ResourceStorage): resource.MustParse("5Gi"), 125 }, 126 }, 127 StorageClassName: &class, 128 }, 129 } 130 return fakePod, fakePVC 131 } 132 133 var defaultTimerConfig = attachdetach.TimerConfig{ 134 ReconcilerLoopPeriod: 100 * time.Millisecond, 135 ReconcilerMaxWaitForUnmountDuration: 6 * time.Second, 136 DesiredStateOfWorldPopulatorLoopSleepPeriod: 1 * time.Second, 137 DesiredStateOfWorldPopulatorListPodsRetryDuration: 3 * time.Second, 138 } 139 140 // Via integration test we can verify that if pod delete 141 // event is somehow missed by AttachDetach controller - it still 142 // gets cleaned up by Desired State of World populator. 143 func TestPodDeletionWithDswp(t *testing.T) { 144 // Disable ServiceAccount admission plugin as we don't have serviceaccount controller running. 145 server := kubeapiservertesting.StartTestServerOrDie(t, nil, []string{"--disable-admission-plugins=ServiceAccount"}, framework.SharedEtcd()) 146 defer server.TearDownFn() 147 148 namespaceName := "test-pod-deletion" 149 node := &v1.Node{ 150 ObjectMeta: metav1.ObjectMeta{ 151 Name: "node-sandbox", 152 Annotations: map[string]string{ 153 util.ControllerManagedAttachAnnotation: "true", 154 }, 155 }, 156 } 157 158 tCtx := ktesting.Init(t) 159 defer tCtx.Cancel("test has completed") 160 testClient, ctrl, pvCtrl, informers := createAdClients(tCtx, t, server, defaultSyncPeriod, defaultTimerConfig) 161 162 ns := framework.CreateNamespaceOrDie(testClient, namespaceName, t) 163 defer framework.DeleteNamespaceOrDie(testClient, ns, t) 164 165 pod := fakePodWithVol(namespaceName) 166 167 if _, err := testClient.CoreV1().Nodes().Create(tCtx, node, metav1.CreateOptions{}); err != nil { 168 t.Fatalf("Failed to created node : %v", err) 169 } 170 171 // start controller loop 172 go informers.Core().V1().Nodes().Informer().Run(tCtx.Done()) 173 if _, err := testClient.CoreV1().Pods(ns.Name).Create(tCtx, pod, metav1.CreateOptions{}); err != nil { 174 t.Errorf("Failed to create pod : %v", err) 175 } 176 177 podInformer := informers.Core().V1().Pods().Informer() 178 go podInformer.Run(tCtx.Done()) 179 180 go informers.Core().V1().PersistentVolumeClaims().Informer().Run(tCtx.Done()) 181 go informers.Core().V1().PersistentVolumes().Informer().Run(tCtx.Done()) 182 go informers.Storage().V1().VolumeAttachments().Informer().Run(tCtx.Done()) 183 initCSIObjects(tCtx.Done(), informers) 184 go ctrl.Run(tCtx) 185 // Run pvCtrl to avoid leaking goroutines started during its creation. 186 go pvCtrl.Run(tCtx) 187 188 waitToObservePods(t, podInformer, 1) 189 podKey, err := cache.MetaNamespaceKeyFunc(pod) 190 if err != nil { 191 t.Fatalf("MetaNamespaceKeyFunc failed with : %v", err) 192 } 193 194 podInformerObj, _, err := podInformer.GetStore().GetByKey(podKey) 195 196 if err != nil { 197 t.Fatalf("Pod not found in Pod Informer cache : %v", err) 198 } 199 200 waitForPodsInDSWP(t, ctrl.GetDesiredStateOfWorld()) 201 // let's stop pod events from getting triggered 202 err = podInformer.GetStore().Delete(podInformerObj) 203 if err != nil { 204 t.Fatalf("Error deleting pod : %v", err) 205 } 206 207 waitToObservePods(t, podInformer, 0) 208 // the populator loop turns every 1 minute 209 waitForPodFuncInDSWP(t, ctrl.GetDesiredStateOfWorld(), 80*time.Second, "expected 0 pods in dsw after pod delete", 0) 210 } 211 212 func initCSIObjects(stopCh <-chan struct{}, informers clientgoinformers.SharedInformerFactory) { 213 go informers.Storage().V1().CSINodes().Informer().Run(stopCh) 214 go informers.Storage().V1().CSIDrivers().Informer().Run(stopCh) 215 } 216 217 func TestPodUpdateWithWithADC(t *testing.T) { 218 // Disable ServiceAccount admission plugin as we don't have serviceaccount controller running. 219 server := kubeapiservertesting.StartTestServerOrDie(t, nil, []string{"--disable-admission-plugins=ServiceAccount"}, framework.SharedEtcd()) 220 defer server.TearDownFn() 221 namespaceName := "test-pod-update" 222 223 node := &v1.Node{ 224 ObjectMeta: metav1.ObjectMeta{ 225 Name: "node-sandbox", 226 Annotations: map[string]string{ 227 util.ControllerManagedAttachAnnotation: "true", 228 }, 229 }, 230 } 231 232 tCtx := ktesting.Init(t) 233 defer tCtx.Cancel("test has completed") 234 testClient, ctrl, pvCtrl, informers := createAdClients(tCtx, t, server, defaultSyncPeriod, defaultTimerConfig) 235 236 ns := framework.CreateNamespaceOrDie(testClient, namespaceName, t) 237 defer framework.DeleteNamespaceOrDie(testClient, ns, t) 238 239 pod := fakePodWithVol(namespaceName) 240 podStopCh := make(chan struct{}) 241 defer close(podStopCh) 242 243 if _, err := testClient.CoreV1().Nodes().Create(context.TODO(), node, metav1.CreateOptions{}); err != nil { 244 t.Fatalf("Failed to created node : %v", err) 245 } 246 247 go informers.Core().V1().Nodes().Informer().Run(podStopCh) 248 249 if _, err := testClient.CoreV1().Pods(ns.Name).Create(context.TODO(), pod, metav1.CreateOptions{}); err != nil { 250 t.Errorf("Failed to create pod : %v", err) 251 } 252 253 podInformer := informers.Core().V1().Pods().Informer() 254 go podInformer.Run(podStopCh) 255 256 // start controller loop 257 go informers.Core().V1().PersistentVolumeClaims().Informer().Run(tCtx.Done()) 258 go informers.Core().V1().PersistentVolumes().Informer().Run(tCtx.Done()) 259 go informers.Storage().V1().VolumeAttachments().Informer().Run(tCtx.Done()) 260 initCSIObjects(tCtx.Done(), informers) 261 go ctrl.Run(tCtx) 262 // Run pvCtrl to avoid leaking goroutines started during its creation. 263 go pvCtrl.Run(tCtx) 264 265 waitToObservePods(t, podInformer, 1) 266 podKey, err := cache.MetaNamespaceKeyFunc(pod) 267 if err != nil { 268 t.Fatalf("MetaNamespaceKeyFunc failed with : %v", err) 269 } 270 271 _, _, err = podInformer.GetStore().GetByKey(podKey) 272 273 if err != nil { 274 t.Fatalf("Pod not found in Pod Informer cache : %v", err) 275 } 276 277 waitForPodsInDSWP(t, ctrl.GetDesiredStateOfWorld()) 278 279 pod.Status.Phase = v1.PodSucceeded 280 281 if _, err := testClient.CoreV1().Pods(ns.Name).UpdateStatus(context.TODO(), pod, metav1.UpdateOptions{}); err != nil { 282 t.Errorf("Failed to update pod : %v", err) 283 } 284 285 waitForPodFuncInDSWP(t, ctrl.GetDesiredStateOfWorld(), 20*time.Second, "expected 0 pods in dsw after pod completion", 0) 286 } 287 288 // wait for the podInformer to observe the pods. Call this function before 289 // running the RC manager to prevent the rc manager from creating new pods 290 // rather than adopting the existing ones. 291 func waitToObservePods(t *testing.T, podInformer cache.SharedIndexInformer, podNum int) { 292 if err := wait.Poll(100*time.Millisecond, 60*time.Second, func() (bool, error) { 293 objects := podInformer.GetIndexer().List() 294 if len(objects) == podNum { 295 return true, nil 296 } 297 return false, nil 298 }); err != nil { 299 t.Fatal(err) 300 } 301 } 302 303 // wait for pods to be observed in desired state of world 304 func waitForPodsInDSWP(t *testing.T, dswp volumecache.DesiredStateOfWorld) { 305 if err := wait.Poll(time.Millisecond*500, wait.ForeverTestTimeout, func() (bool, error) { 306 pods := dswp.GetPodToAdd() 307 if len(pods) > 0 { 308 return true, nil 309 } 310 return false, nil 311 }); err != nil { 312 t.Fatalf("Pod not added to desired state of world : %v", err) 313 } 314 } 315 316 // wait for pods to be observed in desired state of world 317 func waitForPodFuncInDSWP(t *testing.T, dswp volumecache.DesiredStateOfWorld, checkTimeout time.Duration, failMessage string, podCount int) { 318 if err := wait.Poll(time.Millisecond*500, checkTimeout, func() (bool, error) { 319 pods := dswp.GetPodToAdd() 320 if len(pods) == podCount { 321 return true, nil 322 } 323 return false, nil 324 }); err != nil { 325 t.Fatalf("%s but got error %v", failMessage, err) 326 } 327 } 328 329 func createAdClients(ctx context.Context, t *testing.T, server *kubeapiservertesting.TestServer, syncPeriod time.Duration, timers attachdetach.TimerConfig) (*clientset.Clientset, attachdetach.AttachDetachController, *persistentvolume.PersistentVolumeController, clientgoinformers.SharedInformerFactory) { 330 config := restclient.CopyConfig(server.ClientConfig) 331 config.QPS = 1000000 332 config.Burst = 1000000 333 resyncPeriod := 12 * time.Hour 334 testClient := clientset.NewForConfigOrDie(server.ClientConfig) 335 336 host := volumetest.NewFakeVolumeHost(t, "/tmp/fake", nil, nil) 337 plugin := &volumetest.FakeVolumePlugin{ 338 PluginName: provisionerPluginName, 339 Host: host, 340 Config: volume.VolumeConfig{}, 341 LastProvisionerOptions: volume.VolumeOptions{}, 342 NewAttacherCallCount: 0, 343 NewDetacherCallCount: 0, 344 Mounters: nil, 345 Unmounters: nil, 346 Attachers: nil, 347 Detachers: nil, 348 } 349 plugins := []volume.VolumePlugin{plugin} 350 informers := clientgoinformers.NewSharedInformerFactory(testClient, resyncPeriod) 351 ctrl, err := attachdetach.NewAttachDetachController( 352 ctx, 353 testClient, 354 informers.Core().V1().Pods(), 355 informers.Core().V1().Nodes(), 356 informers.Core().V1().PersistentVolumeClaims(), 357 informers.Core().V1().PersistentVolumes(), 358 informers.Storage().V1().CSINodes(), 359 informers.Storage().V1().CSIDrivers(), 360 informers.Storage().V1().VolumeAttachments(), 361 plugins, 362 nil, /* prober */ 363 false, 364 5*time.Second, 365 false, 366 timers, 367 ) 368 369 if err != nil { 370 t.Fatalf("Error creating AttachDetach : %v", err) 371 } 372 373 // create pv controller 374 controllerOptions := persistentvolumeoptions.NewPersistentVolumeControllerOptions() 375 params := persistentvolume.ControllerParameters{ 376 KubeClient: testClient, 377 SyncPeriod: controllerOptions.PVClaimBinderSyncPeriod, 378 VolumePlugins: plugins, 379 VolumeInformer: informers.Core().V1().PersistentVolumes(), 380 ClaimInformer: informers.Core().V1().PersistentVolumeClaims(), 381 ClassInformer: informers.Storage().V1().StorageClasses(), 382 PodInformer: informers.Core().V1().Pods(), 383 NodeInformer: informers.Core().V1().Nodes(), 384 EnableDynamicProvisioning: false, 385 } 386 pvCtrl, err := persistentvolume.NewController(ctx, params) 387 if err != nil { 388 t.Fatalf("Failed to create PV controller: %v", err) 389 } 390 return testClient, ctrl, pvCtrl, informers 391 } 392 393 // Via integration test we can verify that if pod add 394 // event is somehow missed by AttachDetach controller - it still 395 // gets added by Desired State of World populator. 396 func TestPodAddedByDswp(t *testing.T) { 397 // Disable ServiceAccount admission plugin as we don't have serviceaccount controller running. 398 server := kubeapiservertesting.StartTestServerOrDie(t, nil, []string{"--disable-admission-plugins=ServiceAccount"}, framework.SharedEtcd()) 399 defer server.TearDownFn() 400 namespaceName := "test-pod-deletion" 401 402 node := &v1.Node{ 403 ObjectMeta: metav1.ObjectMeta{ 404 Name: "node-sandbox", 405 Annotations: map[string]string{ 406 util.ControllerManagedAttachAnnotation: "true", 407 }, 408 }, 409 } 410 411 tCtx := ktesting.Init(t) 412 defer tCtx.Cancel("test has completed") 413 testClient, ctrl, pvCtrl, informers := createAdClients(tCtx, t, server, defaultSyncPeriod, defaultTimerConfig) 414 415 ns := framework.CreateNamespaceOrDie(testClient, namespaceName, t) 416 defer framework.DeleteNamespaceOrDie(testClient, ns, t) 417 418 pod := fakePodWithVol(namespaceName) 419 podStopCh := make(chan struct{}) 420 421 if _, err := testClient.CoreV1().Nodes().Create(tCtx, node, metav1.CreateOptions{}); err != nil { 422 t.Fatalf("Failed to created node : %v", err) 423 } 424 425 go informers.Core().V1().Nodes().Informer().Run(podStopCh) 426 427 if _, err := testClient.CoreV1().Pods(ns.Name).Create(tCtx, pod, metav1.CreateOptions{}); err != nil { 428 t.Errorf("Failed to create pod : %v", err) 429 } 430 431 podInformer := informers.Core().V1().Pods().Informer() 432 go podInformer.Run(podStopCh) 433 434 // start controller loop 435 go informers.Core().V1().PersistentVolumeClaims().Informer().Run(tCtx.Done()) 436 go informers.Core().V1().PersistentVolumes().Informer().Run(tCtx.Done()) 437 go informers.Storage().V1().VolumeAttachments().Informer().Run(tCtx.Done()) 438 initCSIObjects(tCtx.Done(), informers) 439 go ctrl.Run(tCtx) 440 // Run pvCtrl to avoid leaking goroutines started during its creation. 441 go pvCtrl.Run(tCtx) 442 443 waitToObservePods(t, podInformer, 1) 444 podKey, err := cache.MetaNamespaceKeyFunc(pod) 445 if err != nil { 446 t.Fatalf("MetaNamespaceKeyFunc failed with : %v", err) 447 } 448 449 _, _, err = podInformer.GetStore().GetByKey(podKey) 450 451 if err != nil { 452 t.Fatalf("Pod not found in Pod Informer cache : %v", err) 453 } 454 455 waitForPodsInDSWP(t, ctrl.GetDesiredStateOfWorld()) 456 457 // let's stop pod events from getting triggered 458 close(podStopCh) 459 podNew := pod.DeepCopy() 460 newPodName := "newFakepod" 461 podNew.SetName(newPodName) 462 err = podInformer.GetStore().Add(podNew) 463 if err != nil { 464 t.Fatalf("Error adding pod : %v", err) 465 } 466 467 waitToObservePods(t, podInformer, 2) 468 469 // the findAndAddActivePods loop turns every 3 minute 470 waitForPodFuncInDSWP(t, ctrl.GetDesiredStateOfWorld(), 200*time.Second, "expected 2 pods in dsw after pod addition", 2) 471 } 472 473 func TestPVCBoundWithADC(t *testing.T) { 474 // Disable ServiceAccount admission plugin as we don't have serviceaccount controller running. 475 server := kubeapiservertesting.StartTestServerOrDie(t, nil, []string{"--disable-admission-plugins=ServiceAccount"}, framework.SharedEtcd()) 476 defer server.TearDownFn() 477 478 tCtx := ktesting.Init(t) 479 defer tCtx.Cancel("test has completed") 480 481 namespaceName := "test-pod-deletion" 482 483 testClient, ctrl, pvCtrl, informers := createAdClients(tCtx, t, server, defaultSyncPeriod, attachdetach.TimerConfig{ 484 ReconcilerLoopPeriod: 100 * time.Millisecond, 485 ReconcilerMaxWaitForUnmountDuration: 6 * time.Second, 486 DesiredStateOfWorldPopulatorLoopSleepPeriod: 24 * time.Hour, 487 // Use high duration to disable DesiredStateOfWorldPopulator.findAndAddActivePods loop in test. 488 DesiredStateOfWorldPopulatorListPodsRetryDuration: 24 * time.Hour, 489 }) 490 491 ns := framework.CreateNamespaceOrDie(testClient, namespaceName, t) 492 defer framework.DeleteNamespaceOrDie(testClient, ns, t) 493 494 node := &v1.Node{ 495 ObjectMeta: metav1.ObjectMeta{ 496 Name: "node-sandbox", 497 Annotations: map[string]string{ 498 util.ControllerManagedAttachAnnotation: "true", 499 }, 500 }, 501 } 502 if _, err := testClient.CoreV1().Nodes().Create(context.TODO(), node, metav1.CreateOptions{}); err != nil { 503 t.Fatalf("Failed to created node : %v", err) 504 } 505 506 // pods with pvc not bound 507 pvcs := []*v1.PersistentVolumeClaim{} 508 for i := 0; i < 3; i++ { 509 pod, pvc := fakePodWithPVC(fmt.Sprintf("fakepod-pvcnotbound-%d", i), fmt.Sprintf("fakepvc-%d", i), namespaceName) 510 if _, err := testClient.CoreV1().Pods(pod.Namespace).Create(context.TODO(), pod, metav1.CreateOptions{}); err != nil { 511 t.Errorf("Failed to create pod : %v", err) 512 } 513 if _, err := testClient.CoreV1().PersistentVolumeClaims(pvc.Namespace).Create(context.TODO(), pvc, metav1.CreateOptions{}); err != nil { 514 t.Errorf("Failed to create pvc : %v", err) 515 } 516 pvcs = append(pvcs, pvc) 517 } 518 // pod with no pvc 519 podNew := fakePodWithVol(namespaceName) 520 podNew.SetName("fakepod") 521 if _, err := testClient.CoreV1().Pods(podNew.Namespace).Create(context.TODO(), podNew, metav1.CreateOptions{}); err != nil { 522 t.Errorf("Failed to create pod : %v", err) 523 } 524 525 // start controller loop 526 informers.Start(tCtx.Done()) 527 informers.WaitForCacheSync(tCtx.Done()) 528 initCSIObjects(tCtx.Done(), informers) 529 go ctrl.Run(tCtx) 530 go pvCtrl.Run(tCtx) 531 532 waitToObservePods(t, informers.Core().V1().Pods().Informer(), 4) 533 // Give attachdetach controller enough time to populate pods into DSWP. 534 time.Sleep(10 * time.Second) 535 waitForPodFuncInDSWP(t, ctrl.GetDesiredStateOfWorld(), 60*time.Second, "expected 1 pod in dsw", 1) 536 for _, pvc := range pvcs { 537 createPVForPVC(t, testClient, pvc) 538 } 539 waitForPodFuncInDSWP(t, ctrl.GetDesiredStateOfWorld(), 60*time.Second, "expected 4 pods in dsw after PVCs are bound", 4) 540 } 541 542 // Create PV for PVC, pv controller will bind them together. 543 func createPVForPVC(t *testing.T, testClient *clientset.Clientset, pvc *v1.PersistentVolumeClaim) { 544 pv := &v1.PersistentVolume{ 545 ObjectMeta: metav1.ObjectMeta{ 546 Name: fmt.Sprintf("fakepv-%s", pvc.Name), 547 }, 548 Spec: v1.PersistentVolumeSpec{ 549 Capacity: pvc.Spec.Resources.Requests, 550 AccessModes: pvc.Spec.AccessModes, 551 PersistentVolumeSource: v1.PersistentVolumeSource{ 552 HostPath: &v1.HostPathVolumeSource{ 553 Path: "/var/www/html", 554 }, 555 }, 556 ClaimRef: &v1.ObjectReference{Name: pvc.Name, Namespace: pvc.Namespace}, 557 StorageClassName: *pvc.Spec.StorageClassName, 558 }, 559 } 560 if _, err := testClient.CoreV1().PersistentVolumes().Create(context.TODO(), pv, metav1.CreateOptions{}); err != nil { 561 t.Errorf("Failed to create pv : %v", err) 562 } 563 }