k8s.io/kubernetes@v1.31.0-alpha.0.0.20240520171757-56147500dadc/pkg/scheduler/framework/plugins/volumebinding/binder_test.go (about) 1 /* 2 Copyright 2017 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 volumebinding 18 19 import ( 20 "context" 21 "fmt" 22 "os" 23 "reflect" 24 "sort" 25 "testing" 26 "time" 27 28 "github.com/google/go-cmp/cmp" 29 30 v1 "k8s.io/api/core/v1" 31 storagev1 "k8s.io/api/storage/v1" 32 "k8s.io/apimachinery/pkg/api/resource" 33 metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 34 "k8s.io/apimachinery/pkg/types" 35 "k8s.io/apimachinery/pkg/util/sets" 36 "k8s.io/apimachinery/pkg/util/wait" 37 "k8s.io/apimachinery/pkg/watch" 38 "k8s.io/client-go/informers" 39 coreinformers "k8s.io/client-go/informers/core/v1" 40 storageinformers "k8s.io/client-go/informers/storage/v1" 41 clientset "k8s.io/client-go/kubernetes" 42 "k8s.io/client-go/kubernetes/fake" 43 k8stesting "k8s.io/client-go/testing" 44 "k8s.io/component-helpers/storage/volume" 45 "k8s.io/klog/v2" 46 "k8s.io/klog/v2/ktesting" 47 _ "k8s.io/klog/v2/ktesting/init" 48 "k8s.io/kubernetes/pkg/controller" 49 pvtesting "k8s.io/kubernetes/pkg/controller/volume/persistentvolume/testing" 50 "k8s.io/kubernetes/pkg/scheduler/util/assumecache" 51 ) 52 53 var ( 54 provisioner = "test-provisioner" 55 56 // PVCs for manual binding 57 // TODO: clean up all of these 58 unboundPVC = makeTestPVC("unbound-pvc", "1G", "", pvcUnbound, "", "1", &waitClass) 59 unboundPVC2 = makeTestPVC("unbound-pvc2", "5G", "", pvcUnbound, "", "1", &waitClass) 60 preboundPVC = makeTestPVC("prebound-pvc", "1G", "", pvcPrebound, "pv-node1a", "1", &waitClass) 61 preboundPVCNode1a = makeTestPVC("unbound-pvc", "1G", "", pvcPrebound, "pv-node1a", "1", &waitClass) 62 boundPVC = makeTestPVC("bound-pvc", "1G", "", pvcBound, "pv-bound", "1", &waitClass) 63 boundPVCNode1a = makeTestPVC("unbound-pvc", "1G", "", pvcBound, "pv-node1a", "1", &waitClass) 64 immediateUnboundPVC = makeTestPVC("immediate-unbound-pvc", "1G", "", pvcUnbound, "", "1", &immediateClass) 65 immediateBoundPVC = makeTestPVC("immediate-bound-pvc", "1G", "", pvcBound, "pv-bound-immediate", "1", &immediateClass) 66 localPreboundPVC1a = makeTestPVC("local-prebound-pvc-1a", "1G", "", pvcPrebound, "local-pv-node1a", "1", &waitClass) 67 localPreboundPVC1b = makeTestPVC("local-prebound-pvc-1b", "1G", "", pvcPrebound, "local-pv-node1b", "1", &waitClass) 68 localPreboundPVC2a = makeTestPVC("local-prebound-pvc-2a", "1G", "", pvcPrebound, "local-pv-node2a", "1", &waitClass) 69 70 // PVCs for dynamic provisioning 71 provisionedPVC = makeTestPVC("provisioned-pvc", "1Gi", "", pvcUnbound, "", "1", &waitClassWithProvisioner) 72 provisionedPVC2 = makeTestPVC("provisioned-pvc2", "1Gi", "", pvcUnbound, "", "1", &waitClassWithProvisioner) 73 provisionedPVCHigherVersion = makeTestPVC("provisioned-pvc2", "1Gi", "", pvcUnbound, "", "2", &waitClassWithProvisioner) 74 provisionedPVCBound = makeTestPVC("provisioned-pvc", "1Gi", "", pvcBound, "pv-bound", "1", &waitClassWithProvisioner) 75 noProvisionerPVC = makeTestPVC("no-provisioner-pvc", "1Gi", "", pvcUnbound, "", "1", &waitClass) 76 topoMismatchPVC = makeTestPVC("topo-mismatch-pvc", "1Gi", "", pvcUnbound, "", "1", &topoMismatchClass) 77 78 selectedNodePVC = makeTestPVC("provisioned-pvc", "1Gi", nodeLabelValue, pvcSelectedNode, "", "1", &waitClassWithProvisioner) 79 80 // PVCs for CSI migration 81 boundMigrationPVC = makeTestPVC("pvc-migration-bound", "1G", "", pvcBound, "pv-migration-bound", "1", &waitClass) 82 provMigrationPVCBound = makeTestPVC("pvc-migration-provisioned", "1Gi", "", pvcBound, "pv-migration-bound", "1", &waitClassWithProvisioner) 83 84 // PVCs and PV for GenericEphemeralVolume 85 conflictingGenericPVC = makeGenericEphemeralPVC("test-volume", false /* not owned*/) 86 correctGenericPVC = makeGenericEphemeralPVC("test-volume", true /* owned */) 87 pvBoundGeneric = makeTestPV("pv-bound", "node1", "1G", "1", correctGenericPVC, waitClass) 88 89 // PVs for manual binding 90 pvNode1a = makeTestPV("pv-node1a", "node1", "5G", "1", nil, waitClass) 91 pvNode1b = makeTestPV("pv-node1b", "node1", "10G", "1", nil, waitClass) 92 pvNode1c = makeTestPV("pv-node1b", "node1", "5G", "1", nil, waitClass) 93 pvNode2 = makeTestPV("pv-node2", "node2", "1G", "1", nil, waitClass) 94 pvBound = makeTestPV("pv-bound", "node1", "1G", "1", boundPVC, waitClass) 95 pvNode1aBound = makeTestPV("pv-node1a", "node1", "5G", "1", unboundPVC, waitClass) 96 pvNode1bBound = makeTestPV("pv-node1b", "node1", "10G", "1", unboundPVC2, waitClass) 97 pvNode1bBoundHigherVersion = makeTestPV("pv-node1b", "node1", "10G", "2", unboundPVC2, waitClass) 98 pvBoundImmediate = makeTestPV("pv-bound-immediate", "node1", "1G", "1", immediateBoundPVC, immediateClass) 99 pvBoundImmediateNode2 = makeTestPV("pv-bound-immediate", "node2", "1G", "1", immediateBoundPVC, immediateClass) 100 localPVNode1a = makeLocalPV("local-pv-node1a", "node1", "5G", "1", nil, waitClass) 101 localPVNode1b = makeLocalPV("local-pv-node1b", "node1", "10G", "1", nil, waitClass) 102 localPVNode2a = makeLocalPV("local-pv-node2a", "node2", "5G", "1", nil, waitClass) 103 104 // PVs for CSI migration 105 migrationPVBound = makeTestPVForCSIMigration(zone1Labels, boundMigrationPVC, true) 106 migrationPVBoundToUnbound = makeTestPVForCSIMigration(zone1Labels, unboundPVC, true) 107 nonmigrationPVBoundToUnbound = makeTestPVForCSIMigration(zone1Labels, unboundPVC, false) 108 109 // storage class names 110 waitClass = "waitClass" 111 immediateClass = "immediateClass" 112 waitClassWithProvisioner = "waitClassWithProvisioner" 113 topoMismatchClass = "topoMismatchClass" 114 115 // nodes objects 116 node1 = makeNode("node1").withLabel(nodeLabelKey, "node1").Node 117 node2 = makeNode("node2").withLabel(nodeLabelKey, "node2").Node 118 node1NoLabels = makeNode("node1").Node 119 node1Zone1 = makeNode("node1").withLabel("topology.gke.io/zone", "us-east-1").Node 120 node1Zone2 = makeNode("node1").withLabel("topology.gke.io/zone", "us-east-2").Node 121 122 // csiNode objects 123 csiNode1Migrated = makeCSINode("node1", "kubernetes.io/gce-pd") 124 csiNode1NotMigrated = makeCSINode("node1", "") 125 126 // node topology 127 nodeLabelKey = "nodeKey" 128 nodeLabelValue = "node1" 129 130 // node topology for CSI migration 131 zone1Labels = map[string]string{v1.LabelFailureDomainBetaZone: "us-east-1", v1.LabelFailureDomainBetaRegion: "us-east-1a"} 132 ) 133 134 type testEnv struct { 135 client clientset.Interface 136 reactor *pvtesting.VolumeReactor 137 binder SchedulerVolumeBinder 138 internalBinder *volumeBinder 139 internalPodInformer coreinformers.PodInformer 140 internalNodeInformer coreinformers.NodeInformer 141 internalCSINodeInformer storageinformers.CSINodeInformer 142 143 // For CSIStorageCapacity feature testing: 144 internalCSIDriverInformer storageinformers.CSIDriverInformer 145 internalCSIStorageCapacityInformer storageinformers.CSIStorageCapacityInformer 146 } 147 148 func newTestBinder(t *testing.T, ctx context.Context) *testEnv { 149 client := &fake.Clientset{} 150 logger := klog.FromContext(ctx) 151 reactor := pvtesting.NewVolumeReactor(ctx, client, nil, nil, nil) 152 // TODO refactor all tests to use real watch mechanism, see #72327 153 client.AddWatchReactor("*", func(action k8stesting.Action) (handled bool, ret watch.Interface, err error) { 154 gvr := action.GetResource() 155 ns := action.GetNamespace() 156 watch, err := reactor.Watch(gvr, ns) 157 if err != nil { 158 return false, nil, err 159 } 160 return true, watch, nil 161 }) 162 informerFactory := informers.NewSharedInformerFactory(client, controller.NoResyncPeriodFunc()) 163 164 podInformer := informerFactory.Core().V1().Pods() 165 nodeInformer := informerFactory.Core().V1().Nodes() 166 csiNodeInformer := informerFactory.Storage().V1().CSINodes() 167 pvcInformer := informerFactory.Core().V1().PersistentVolumeClaims() 168 classInformer := informerFactory.Storage().V1().StorageClasses() 169 csiDriverInformer := informerFactory.Storage().V1().CSIDrivers() 170 csiStorageCapacityInformer := informerFactory.Storage().V1().CSIStorageCapacities() 171 capacityCheck := CapacityCheck{ 172 CSIDriverInformer: csiDriverInformer, 173 CSIStorageCapacityInformer: csiStorageCapacityInformer, 174 } 175 binder := NewVolumeBinder( 176 logger, 177 client, 178 podInformer, 179 nodeInformer, 180 csiNodeInformer, 181 pvcInformer, 182 informerFactory.Core().V1().PersistentVolumes(), 183 classInformer, 184 capacityCheck, 185 10*time.Second) 186 187 // Wait for informers cache sync 188 informerFactory.Start(ctx.Done()) 189 for v, synced := range informerFactory.WaitForCacheSync(ctx.Done()) { 190 if !synced { 191 logger.Error(nil, "Error syncing informer", "informer", v) 192 os.Exit(1) 193 } 194 } 195 196 // Add storageclasses 197 waitMode := storagev1.VolumeBindingWaitForFirstConsumer 198 immediateMode := storagev1.VolumeBindingImmediate 199 classes := []*storagev1.StorageClass{ 200 { 201 ObjectMeta: metav1.ObjectMeta{ 202 Name: waitClassWithProvisioner, 203 }, 204 VolumeBindingMode: &waitMode, 205 Provisioner: provisioner, 206 AllowedTopologies: []v1.TopologySelectorTerm{ 207 { 208 MatchLabelExpressions: []v1.TopologySelectorLabelRequirement{ 209 { 210 Key: nodeLabelKey, 211 Values: []string{nodeLabelValue, "reference-value"}, 212 }, 213 }, 214 }, 215 }, 216 }, 217 { 218 ObjectMeta: metav1.ObjectMeta{ 219 Name: immediateClass, 220 }, 221 VolumeBindingMode: &immediateMode, 222 }, 223 { 224 ObjectMeta: metav1.ObjectMeta{ 225 Name: waitClass, 226 }, 227 VolumeBindingMode: &waitMode, 228 Provisioner: "kubernetes.io/no-provisioner", 229 }, 230 { 231 ObjectMeta: metav1.ObjectMeta{ 232 Name: topoMismatchClass, 233 }, 234 VolumeBindingMode: &waitMode, 235 Provisioner: provisioner, 236 AllowedTopologies: []v1.TopologySelectorTerm{ 237 { 238 MatchLabelExpressions: []v1.TopologySelectorLabelRequirement{ 239 { 240 Key: nodeLabelKey, 241 Values: []string{"reference-value"}, 242 }, 243 }, 244 }, 245 }, 246 }, 247 } 248 for _, class := range classes { 249 if err := classInformer.Informer().GetIndexer().Add(class); err != nil { 250 t.Fatalf("Failed to add storage class to internal cache: %v", err) 251 } 252 } 253 254 // Get internal types 255 internalBinder, ok := binder.(*volumeBinder) 256 if !ok { 257 t.Fatalf("Failed to convert to internal binder") 258 } 259 260 return &testEnv{ 261 client: client, 262 reactor: reactor, 263 binder: binder, 264 internalBinder: internalBinder, 265 internalPodInformer: podInformer, 266 internalNodeInformer: nodeInformer, 267 internalCSINodeInformer: csiNodeInformer, 268 269 internalCSIDriverInformer: csiDriverInformer, 270 internalCSIStorageCapacityInformer: csiStorageCapacityInformer, 271 } 272 } 273 274 func (env *testEnv) initNodes(cachedNodes []*v1.Node) { 275 nodeInformer := env.internalNodeInformer.Informer() 276 for _, node := range cachedNodes { 277 nodeInformer.GetIndexer().Add(node) 278 } 279 } 280 281 func (env *testEnv) initCSINodes(cachedCSINodes []*storagev1.CSINode) { 282 csiNodeInformer := env.internalCSINodeInformer.Informer() 283 for _, csiNode := range cachedCSINodes { 284 csiNodeInformer.GetIndexer().Add(csiNode) 285 } 286 } 287 288 func (env *testEnv) addCSIDriver(csiDriver *storagev1.CSIDriver) { 289 csiDriverInformer := env.internalCSIDriverInformer.Informer() 290 csiDriverInformer.GetIndexer().Add(csiDriver) 291 } 292 293 func (env *testEnv) addCSIStorageCapacities(capacities []*storagev1.CSIStorageCapacity) { 294 csiStorageCapacityInformer := env.internalCSIStorageCapacityInformer.Informer() 295 for _, capacity := range capacities { 296 csiStorageCapacityInformer.GetIndexer().Add(capacity) 297 } 298 } 299 300 func (env *testEnv) initClaims(cachedPVCs []*v1.PersistentVolumeClaim, apiPVCs []*v1.PersistentVolumeClaim) { 301 for _, pvc := range cachedPVCs { 302 assumecache.AddTestObject(env.internalBinder.pvcCache.AssumeCache, pvc) 303 if apiPVCs == nil { 304 env.reactor.AddClaim(pvc) 305 } 306 } 307 for _, pvc := range apiPVCs { 308 env.reactor.AddClaim(pvc) 309 } 310 } 311 312 func (env *testEnv) initVolumes(cachedPVs []*v1.PersistentVolume, apiPVs []*v1.PersistentVolume) { 313 for _, pv := range cachedPVs { 314 assumecache.AddTestObject(env.internalBinder.pvCache.AssumeCache, pv) 315 if apiPVs == nil { 316 env.reactor.AddVolume(pv) 317 } 318 } 319 for _, pv := range apiPVs { 320 env.reactor.AddVolume(pv) 321 } 322 323 } 324 325 func (env *testEnv) updateVolumes(ctx context.Context, pvs []*v1.PersistentVolume) error { 326 for i, pv := range pvs { 327 newPv, err := env.client.CoreV1().PersistentVolumes().Update(ctx, pv, metav1.UpdateOptions{}) 328 if err != nil { 329 return err 330 } 331 pvs[i] = newPv 332 } 333 return wait.PollUntilContextTimeout(ctx, 100*time.Millisecond, 3*time.Second, false, func(ctx context.Context) (bool, error) { 334 for _, pv := range pvs { 335 obj, err := env.internalBinder.pvCache.GetAPIObj(pv.Name) 336 if obj == nil || err != nil { 337 return false, nil 338 } 339 pvInCache, ok := obj.(*v1.PersistentVolume) 340 if !ok { 341 return false, fmt.Errorf("PV %s invalid object", pvInCache.Name) 342 } 343 if versioner.CompareResourceVersion(pvInCache, pv) != 0 { 344 return false, nil 345 } 346 } 347 return true, nil 348 }) 349 } 350 351 func (env *testEnv) updateClaims(ctx context.Context, pvcs []*v1.PersistentVolumeClaim) error { 352 for i, pvc := range pvcs { 353 newPvc, err := env.client.CoreV1().PersistentVolumeClaims(pvc.Namespace).Update(ctx, pvc, metav1.UpdateOptions{}) 354 if err != nil { 355 return err 356 } 357 pvcs[i] = newPvc 358 } 359 return wait.PollUntilContextTimeout(ctx, 100*time.Millisecond, 3*time.Second, false, func(ctx context.Context) (bool, error) { 360 for _, pvc := range pvcs { 361 obj, err := env.internalBinder.pvcCache.GetAPIObj(getPVCName(pvc)) 362 if obj == nil || err != nil { 363 return false, nil 364 } 365 pvcInCache, ok := obj.(*v1.PersistentVolumeClaim) 366 if !ok { 367 return false, fmt.Errorf("PVC %s invalid object", pvcInCache.Name) 368 } 369 if versioner.CompareResourceVersion(pvcInCache, pvc) != 0 { 370 return false, nil 371 } 372 } 373 return true, nil 374 }) 375 } 376 377 func (env *testEnv) deleteVolumes(pvs []*v1.PersistentVolume) { 378 for _, pv := range pvs { 379 assumecache.DeleteTestObject(env.internalBinder.pvCache.AssumeCache, pv) 380 } 381 } 382 383 func (env *testEnv) deleteClaims(pvcs []*v1.PersistentVolumeClaim) { 384 for _, pvc := range pvcs { 385 assumecache.DeleteTestObject(env.internalBinder.pvcCache.AssumeCache, pvc) 386 } 387 } 388 389 func (env *testEnv) assumeVolumes(t *testing.T, node string, pod *v1.Pod, bindings []*BindingInfo, provisionings []*v1.PersistentVolumeClaim) { 390 pvCache := env.internalBinder.pvCache 391 for _, binding := range bindings { 392 if err := pvCache.Assume(binding.pv); err != nil { 393 t.Fatalf("error: %v", err) 394 } 395 } 396 397 pvcCache := env.internalBinder.pvcCache 398 for _, pvc := range provisionings { 399 if err := pvcCache.Assume(pvc); err != nil { 400 t.Fatalf("error: %v", err) 401 } 402 } 403 } 404 405 func (env *testEnv) validatePodCache(t *testing.T, node string, pod *v1.Pod, podVolumes *PodVolumes, expectedBindings []*BindingInfo, expectedProvisionings []*v1.PersistentVolumeClaim) { 406 var ( 407 bindings []*BindingInfo 408 provisionedClaims []*v1.PersistentVolumeClaim 409 ) 410 if podVolumes != nil { 411 bindings = podVolumes.StaticBindings 412 provisionedClaims = podVolumes.DynamicProvisions 413 } 414 if aLen, eLen := len(bindings), len(expectedBindings); aLen != eLen { 415 t.Errorf("expected %v bindings, got %v", eLen, aLen) 416 } else if expectedBindings == nil && bindings != nil { 417 // nil and empty are different 418 t.Error("expected nil bindings, got empty") 419 } else if expectedBindings != nil && bindings == nil { 420 // nil and empty are different 421 t.Error("expected empty bindings, got nil") 422 } else { 423 for i := 0; i < aLen; i++ { 424 // Validate PV 425 if diff := cmp.Diff(expectedBindings[i].pv, bindings[i].pv); diff != "" { 426 t.Errorf("binding.pv doesn't match (-want, +got):\n%s", diff) 427 } 428 429 // Validate PVC 430 if diff := cmp.Diff(expectedBindings[i].pvc, bindings[i].pvc); diff != "" { 431 t.Errorf("binding.pvc doesn't match (-want, +got):\n%s", diff) 432 } 433 } 434 } 435 436 if aLen, eLen := len(provisionedClaims), len(expectedProvisionings); aLen != eLen { 437 t.Errorf("expected %v provisioned claims, got %v", eLen, aLen) 438 } else if expectedProvisionings == nil && provisionedClaims != nil { 439 // nil and empty are different 440 t.Error("expected nil provisionings, got empty") 441 } else if expectedProvisionings != nil && provisionedClaims == nil { 442 // nil and empty are different 443 t.Error("expected empty provisionings, got nil") 444 } else { 445 for i := 0; i < aLen; i++ { 446 if diff := cmp.Diff(expectedProvisionings[i], provisionedClaims[i]); diff != "" { 447 t.Errorf("provisioned claims doesn't match (-want, +got):\n%s", diff) 448 } 449 } 450 } 451 } 452 453 func (env *testEnv) validateAssume(t *testing.T, pod *v1.Pod, bindings []*BindingInfo, provisionings []*v1.PersistentVolumeClaim) { 454 // Check pv cache 455 pvCache := env.internalBinder.pvCache 456 for _, b := range bindings { 457 pv, err := pvCache.GetPV(b.pv.Name) 458 if err != nil { 459 t.Errorf("GetPV %q returned error: %v", b.pv.Name, err) 460 continue 461 } 462 if pv.Spec.ClaimRef == nil { 463 t.Errorf("PV %q ClaimRef is nil", b.pv.Name) 464 continue 465 } 466 if pv.Spec.ClaimRef.Name != b.pvc.Name { 467 t.Errorf("expected PV.ClaimRef.Name %q, got %q", b.pvc.Name, pv.Spec.ClaimRef.Name) 468 } 469 if pv.Spec.ClaimRef.Namespace != b.pvc.Namespace { 470 t.Errorf("expected PV.ClaimRef.Namespace %q, got %q", b.pvc.Namespace, pv.Spec.ClaimRef.Namespace) 471 } 472 } 473 474 // Check pvc cache 475 pvcCache := env.internalBinder.pvcCache 476 for _, p := range provisionings { 477 pvcKey := getPVCName(p) 478 pvc, err := pvcCache.GetPVC(pvcKey) 479 if err != nil { 480 t.Errorf("GetPVC %q returned error: %v", pvcKey, err) 481 continue 482 } 483 if pvc.Annotations[volume.AnnSelectedNode] != nodeLabelValue { 484 t.Errorf("expected volume.AnnSelectedNode of pvc %q to be %q, but got %q", pvcKey, nodeLabelValue, pvc.Annotations[volume.AnnSelectedNode]) 485 } 486 } 487 } 488 489 func (env *testEnv) validateCacheRestored(t *testing.T, pod *v1.Pod, bindings []*BindingInfo, provisionings []*v1.PersistentVolumeClaim) { 490 // All PVs have been unmodified in cache 491 pvCache := env.internalBinder.pvCache 492 for _, b := range bindings { 493 pv, _ := pvCache.GetPV(b.pv.Name) 494 apiPV, _ := pvCache.GetAPIPV(b.pv.Name) 495 // PV could be nil if it's missing from cache 496 if pv != nil && pv != apiPV { 497 t.Errorf("PV %q was modified in cache", b.pv.Name) 498 } 499 } 500 501 // Check pvc cache 502 pvcCache := env.internalBinder.pvcCache 503 for _, p := range provisionings { 504 pvcKey := getPVCName(p) 505 pvc, err := pvcCache.GetPVC(pvcKey) 506 if err != nil { 507 t.Errorf("GetPVC %q returned error: %v", pvcKey, err) 508 continue 509 } 510 if pvc.Annotations[volume.AnnSelectedNode] != "" { 511 t.Errorf("expected volume.AnnSelectedNode of pvc %q empty, but got %q", pvcKey, pvc.Annotations[volume.AnnSelectedNode]) 512 } 513 } 514 } 515 516 func (env *testEnv) validateBind( 517 t *testing.T, 518 pod *v1.Pod, 519 expectedPVs []*v1.PersistentVolume, 520 expectedAPIPVs []*v1.PersistentVolume) { 521 522 // Check pv cache 523 pvCache := env.internalBinder.pvCache 524 for _, pv := range expectedPVs { 525 cachedPV, err := pvCache.GetPV(pv.Name) 526 if err != nil { 527 t.Errorf("GetPV %q returned error: %v", pv.Name, err) 528 } 529 // Cache may be overridden by API object with higher version, compare but ignore resource version. 530 newCachedPV := cachedPV.DeepCopy() 531 newCachedPV.ResourceVersion = pv.ResourceVersion 532 if diff := cmp.Diff(pv, newCachedPV); diff != "" { 533 t.Errorf("cached PV check failed (-want, +got):\n%s", diff) 534 } 535 } 536 537 // Check reactor for API updates 538 if err := env.reactor.CheckVolumes(expectedAPIPVs); err != nil { 539 t.Errorf("API reactor validation failed: %v", err) 540 } 541 } 542 543 func (env *testEnv) validateProvision( 544 t *testing.T, 545 pod *v1.Pod, 546 expectedPVCs []*v1.PersistentVolumeClaim, 547 expectedAPIPVCs []*v1.PersistentVolumeClaim) { 548 549 // Check pvc cache 550 pvcCache := env.internalBinder.pvcCache 551 for _, pvc := range expectedPVCs { 552 cachedPVC, err := pvcCache.GetPVC(getPVCName(pvc)) 553 if err != nil { 554 t.Errorf("GetPVC %q returned error: %v", getPVCName(pvc), err) 555 } 556 // Cache may be overridden by API object with higher version, compare but ignore resource version. 557 newCachedPVC := cachedPVC.DeepCopy() 558 newCachedPVC.ResourceVersion = pvc.ResourceVersion 559 if diff := cmp.Diff(pvc, newCachedPVC); diff != "" { 560 t.Errorf("cached PVC check failed (-want, +got):\n%s", diff) 561 } 562 } 563 564 // Check reactor for API updates 565 if err := env.reactor.CheckClaims(expectedAPIPVCs); err != nil { 566 t.Errorf("API reactor validation failed: %v", err) 567 } 568 } 569 570 const ( 571 pvcUnbound = iota 572 pvcPrebound 573 pvcBound 574 pvcSelectedNode 575 ) 576 577 func makeGenericEphemeralPVC(volumeName string, owned bool) *v1.PersistentVolumeClaim { 578 pod := makePod("test-pod"). 579 withNamespace("testns"). 580 withNodeName("node1"). 581 withGenericEphemeralVolume("").Pod 582 583 pvc := makeTestPVC(pod.Name+"-"+volumeName, "1G", "", pvcBound, "pv-bound", "1", &immediateClass) 584 if owned { 585 controller := true 586 pvc.OwnerReferences = []metav1.OwnerReference{ 587 { 588 Name: pod.Name, 589 UID: pod.UID, 590 Controller: &controller, 591 }, 592 } 593 } 594 return pvc 595 } 596 597 func makeTestPVC(name, size, node string, pvcBoundState int, pvName, resourceVersion string, className *string) *v1.PersistentVolumeClaim { 598 fs := v1.PersistentVolumeFilesystem 599 pvc := &v1.PersistentVolumeClaim{ 600 TypeMeta: metav1.TypeMeta{ 601 Kind: "PersistentVolumeClaim", 602 APIVersion: "v1", 603 }, 604 ObjectMeta: metav1.ObjectMeta{ 605 Name: name, 606 Namespace: "testns", 607 UID: types.UID("pvc-uid"), 608 ResourceVersion: resourceVersion, 609 }, 610 Spec: v1.PersistentVolumeClaimSpec{ 611 Resources: v1.VolumeResourceRequirements{ 612 Requests: v1.ResourceList{ 613 v1.ResourceName(v1.ResourceStorage): resource.MustParse(size), 614 }, 615 }, 616 StorageClassName: className, 617 VolumeMode: &fs, 618 }, 619 } 620 621 switch pvcBoundState { 622 case pvcSelectedNode: 623 metav1.SetMetaDataAnnotation(&pvc.ObjectMeta, volume.AnnSelectedNode, node) 624 // don't fallthrough 625 case pvcBound: 626 metav1.SetMetaDataAnnotation(&pvc.ObjectMeta, volume.AnnBindCompleted, "yes") 627 fallthrough 628 case pvcPrebound: 629 pvc.Spec.VolumeName = pvName 630 } 631 return pvc 632 } 633 634 func makeTestPV(name, node, capacity, version string, boundToPVC *v1.PersistentVolumeClaim, className string) *v1.PersistentVolume { 635 fs := v1.PersistentVolumeFilesystem 636 pv := &v1.PersistentVolume{ 637 ObjectMeta: metav1.ObjectMeta{ 638 Name: name, 639 ResourceVersion: version, 640 }, 641 Spec: v1.PersistentVolumeSpec{ 642 Capacity: v1.ResourceList{ 643 v1.ResourceName(v1.ResourceStorage): resource.MustParse(capacity), 644 }, 645 StorageClassName: className, 646 VolumeMode: &fs, 647 }, 648 Status: v1.PersistentVolumeStatus{ 649 Phase: v1.VolumeAvailable, 650 }, 651 } 652 if node != "" { 653 pv.Spec.NodeAffinity = &v1.VolumeNodeAffinity{ 654 Required: &v1.NodeSelector{ 655 NodeSelectorTerms: []v1.NodeSelectorTerm{ 656 { 657 MatchExpressions: []v1.NodeSelectorRequirement{ 658 { 659 Key: nodeLabelKey, 660 Operator: v1.NodeSelectorOpIn, 661 Values: []string{node}, 662 }, 663 }, 664 }, 665 }, 666 }, 667 } 668 } 669 670 if boundToPVC != nil { 671 pv.Spec.ClaimRef = &v1.ObjectReference{ 672 Kind: boundToPVC.Kind, 673 APIVersion: boundToPVC.APIVersion, 674 ResourceVersion: boundToPVC.ResourceVersion, 675 Name: boundToPVC.Name, 676 Namespace: boundToPVC.Namespace, 677 UID: boundToPVC.UID, 678 } 679 metav1.SetMetaDataAnnotation(&pv.ObjectMeta, volume.AnnBoundByController, "yes") 680 } 681 682 return pv 683 } 684 685 func makeTestPVForCSIMigration(labels map[string]string, pvc *v1.PersistentVolumeClaim, migrationEnabled bool) *v1.PersistentVolume { 686 pv := makeTestPV("pv-migration-bound", "node1", "1G", "1", pvc, waitClass) 687 pv.Spec.NodeAffinity = nil // Will be written by the CSI translation lib 688 pv.ObjectMeta.Labels = labels 689 // GCEPersistentDisk is used when migration is enabled, as its featuregate is locked to GA. 690 // RBD is used for the nonmigrated case, as its featuregate is still alpha. When RBD migration goes GA, 691 // a different nonmigrated plugin should be used instead. If there are no other plugins, then the 692 // nonmigrated test case is no longer relevant and can be removed. 693 if migrationEnabled { 694 pv.Spec.PersistentVolumeSource = v1.PersistentVolumeSource{ 695 GCEPersistentDisk: &v1.GCEPersistentDiskVolumeSource{ 696 PDName: "test-disk", 697 FSType: "ext4", 698 Partition: 0, 699 ReadOnly: false, 700 }, 701 } 702 } else { 703 pv.Spec.PersistentVolumeSource = v1.PersistentVolumeSource{ 704 RBD: &v1.RBDPersistentVolumeSource{ 705 RBDImage: "test-disk", 706 }, 707 } 708 } 709 return pv 710 } 711 712 func makeLocalPV(name, node, capacity, version string, boundToPVC *v1.PersistentVolumeClaim, className string) *v1.PersistentVolume { 713 pv := makeTestPV(name, node, capacity, version, boundToPVC, className) 714 pv.Spec.NodeAffinity.Required.NodeSelectorTerms[0].MatchExpressions[0].Key = v1.LabelHostname 715 return pv 716 } 717 718 func pvcSetSelectedNode(pvc *v1.PersistentVolumeClaim, node string) *v1.PersistentVolumeClaim { 719 newPVC := pvc.DeepCopy() 720 metav1.SetMetaDataAnnotation(&newPVC.ObjectMeta, volume.AnnSelectedNode, node) 721 return newPVC 722 } 723 724 func pvcSetEmptyAnnotations(pvc *v1.PersistentVolumeClaim) *v1.PersistentVolumeClaim { 725 newPVC := pvc.DeepCopy() 726 newPVC.Annotations = map[string]string{} 727 return newPVC 728 } 729 730 func pvRemoveClaimUID(pv *v1.PersistentVolume) *v1.PersistentVolume { 731 newPV := pv.DeepCopy() 732 newPV.Spec.ClaimRef.UID = "" 733 return newPV 734 } 735 736 func makeCSINode(name, migratedPlugin string) *storagev1.CSINode { 737 return &storagev1.CSINode{ 738 ObjectMeta: metav1.ObjectMeta{ 739 Name: name, 740 Annotations: map[string]string{ 741 v1.MigratedPluginsAnnotationKey: migratedPlugin, 742 }, 743 }, 744 } 745 } 746 747 func makeCSIDriver(name string, storageCapacity bool) *storagev1.CSIDriver { 748 return &storagev1.CSIDriver{ 749 ObjectMeta: metav1.ObjectMeta{ 750 Name: name, 751 }, 752 Spec: storagev1.CSIDriverSpec{ 753 StorageCapacity: &storageCapacity, 754 }, 755 } 756 } 757 758 func makeCapacity(name, storageClassName string, node *v1.Node, capacityStr, maximumVolumeSizeStr string) *storagev1.CSIStorageCapacity { 759 c := &storagev1.CSIStorageCapacity{ 760 ObjectMeta: metav1.ObjectMeta{ 761 Name: name, 762 }, 763 StorageClassName: storageClassName, 764 NodeTopology: &metav1.LabelSelector{}, 765 } 766 if node != nil { 767 c.NodeTopology.MatchLabels = map[string]string{nodeLabelKey: node.Labels[nodeLabelKey]} 768 } 769 if capacityStr != "" { 770 capacityQuantity := resource.MustParse(capacityStr) 771 c.Capacity = &capacityQuantity 772 } 773 if maximumVolumeSizeStr != "" { 774 maximumVolumeSizeQuantity := resource.MustParse(maximumVolumeSizeStr) 775 c.MaximumVolumeSize = &maximumVolumeSizeQuantity 776 } 777 return c 778 } 779 780 func makeBinding(pvc *v1.PersistentVolumeClaim, pv *v1.PersistentVolume) *BindingInfo { 781 return &BindingInfo{pvc: pvc.DeepCopy(), pv: pv.DeepCopy()} 782 } 783 784 func addProvisionAnn(pvc *v1.PersistentVolumeClaim) *v1.PersistentVolumeClaim { 785 res := pvc.DeepCopy() 786 // Add provision related annotations 787 metav1.SetMetaDataAnnotation(&res.ObjectMeta, volume.AnnSelectedNode, nodeLabelValue) 788 789 return res 790 } 791 792 // reasonNames pretty-prints a list of reasons with variable names in 793 // case of a test failure because that is easier to read than the full 794 // strings. 795 func reasonNames(reasons ConflictReasons) string { 796 var varNames []string 797 for _, reason := range reasons { 798 switch reason { 799 case ErrReasonBindConflict: 800 varNames = append(varNames, "ErrReasonBindConflict") 801 case ErrReasonNodeConflict: 802 varNames = append(varNames, "ErrReasonNodeConflict") 803 case ErrReasonNotEnoughSpace: 804 varNames = append(varNames, "ErrReasonNotEnoughSpace") 805 default: 806 varNames = append(varNames, string(reason)) 807 } 808 } 809 return fmt.Sprintf("%v", varNames) 810 } 811 812 func checkReasons(t *testing.T, actual, expected ConflictReasons) { 813 equal := len(actual) == len(expected) 814 sort.Sort(actual) 815 sort.Sort(expected) 816 if equal { 817 for i, reason := range actual { 818 if reason != expected[i] { 819 equal = false 820 break 821 } 822 } 823 } 824 if !equal { 825 t.Errorf("expected failure reasons %s, got %s", reasonNames(expected), reasonNames(actual)) 826 } 827 } 828 829 // findPodVolumes gets and finds volumes for given pod and node 830 func findPodVolumes(logger klog.Logger, binder SchedulerVolumeBinder, pod *v1.Pod, node *v1.Node) (*PodVolumes, ConflictReasons, error) { 831 podVolumeClaims, err := binder.GetPodVolumeClaims(logger, pod) 832 if err != nil { 833 return nil, nil, err 834 } 835 if len(podVolumeClaims.unboundClaimsImmediate) > 0 { 836 return nil, nil, fmt.Errorf("pod has unbound immediate PersistentVolumeClaims") 837 } 838 return binder.FindPodVolumes(logger, pod, podVolumeClaims, node) 839 } 840 841 func TestFindPodVolumesWithoutProvisioning(t *testing.T) { 842 t.Parallel() 843 844 type scenarioType struct { 845 // Inputs 846 pvs []*v1.PersistentVolume 847 podPVCs []*v1.PersistentVolumeClaim 848 // If nil, use pod PVCs 849 cachePVCs []*v1.PersistentVolumeClaim 850 // If nil, makePod with podPVCs 851 pod *v1.Pod 852 853 // Expected podBindingCache fields 854 expectedBindings []*BindingInfo 855 856 // Expected return values 857 reasons ConflictReasons 858 shouldFail bool 859 } 860 scenarios := map[string]scenarioType{ 861 "no-volumes": { 862 pod: makePod("test-pod"). 863 withNamespace("testns"). 864 withNodeName("node1").Pod, 865 }, 866 "no-pvcs": { 867 pod: makePod("test-pod"). 868 withNamespace("testns"). 869 withNodeName("node1"). 870 withEmptyDirVolume().Pod, 871 }, 872 "pvc-not-found": { 873 cachePVCs: []*v1.PersistentVolumeClaim{}, 874 podPVCs: []*v1.PersistentVolumeClaim{boundPVC}, 875 shouldFail: true, 876 }, 877 "bound-pvc": { 878 podPVCs: []*v1.PersistentVolumeClaim{boundPVC}, 879 pvs: []*v1.PersistentVolume{pvBound}, 880 }, 881 "bound-pvc,pv-not-exists": { 882 podPVCs: []*v1.PersistentVolumeClaim{boundPVC}, 883 shouldFail: false, 884 reasons: ConflictReasons{ErrReasonPVNotExist}, 885 }, 886 "prebound-pvc": { 887 podPVCs: []*v1.PersistentVolumeClaim{preboundPVC}, 888 pvs: []*v1.PersistentVolume{pvNode1aBound}, 889 shouldFail: true, 890 }, 891 "unbound-pvc,pv-same-node": { 892 podPVCs: []*v1.PersistentVolumeClaim{unboundPVC}, 893 pvs: []*v1.PersistentVolume{pvNode2, pvNode1a, pvNode1b}, 894 expectedBindings: []*BindingInfo{makeBinding(unboundPVC, pvNode1a)}, 895 }, 896 "unbound-pvc,pv-different-node": { 897 podPVCs: []*v1.PersistentVolumeClaim{unboundPVC}, 898 pvs: []*v1.PersistentVolume{pvNode2}, 899 reasons: ConflictReasons{ErrReasonBindConflict}, 900 }, 901 "two-unbound-pvcs": { 902 podPVCs: []*v1.PersistentVolumeClaim{unboundPVC, unboundPVC2}, 903 pvs: []*v1.PersistentVolume{pvNode1a, pvNode1b}, 904 expectedBindings: []*BindingInfo{makeBinding(unboundPVC, pvNode1a), makeBinding(unboundPVC2, pvNode1b)}, 905 }, 906 "two-unbound-pvcs,order-by-size": { 907 podPVCs: []*v1.PersistentVolumeClaim{unboundPVC2, unboundPVC}, 908 pvs: []*v1.PersistentVolume{pvNode1a, pvNode1b}, 909 expectedBindings: []*BindingInfo{makeBinding(unboundPVC, pvNode1a), makeBinding(unboundPVC2, pvNode1b)}, 910 }, 911 "two-unbound-pvcs,partial-match": { 912 podPVCs: []*v1.PersistentVolumeClaim{unboundPVC, unboundPVC2}, 913 pvs: []*v1.PersistentVolume{pvNode1a}, 914 expectedBindings: []*BindingInfo{makeBinding(unboundPVC, pvNode1a)}, 915 reasons: ConflictReasons{ErrReasonBindConflict}, 916 }, 917 "one-bound,one-unbound": { 918 podPVCs: []*v1.PersistentVolumeClaim{unboundPVC, boundPVC}, 919 pvs: []*v1.PersistentVolume{pvBound, pvNode1a}, 920 expectedBindings: []*BindingInfo{makeBinding(unboundPVC, pvNode1a)}, 921 }, 922 "one-bound,one-unbound,no-match": { 923 podPVCs: []*v1.PersistentVolumeClaim{unboundPVC, boundPVC}, 924 pvs: []*v1.PersistentVolume{pvBound, pvNode2}, 925 reasons: ConflictReasons{ErrReasonBindConflict}, 926 }, 927 "one-prebound,one-unbound": { 928 podPVCs: []*v1.PersistentVolumeClaim{unboundPVC, preboundPVC}, 929 pvs: []*v1.PersistentVolume{pvNode1a, pvNode1b}, 930 shouldFail: true, 931 }, 932 "immediate-bound-pvc": { 933 podPVCs: []*v1.PersistentVolumeClaim{immediateBoundPVC}, 934 pvs: []*v1.PersistentVolume{pvBoundImmediate}, 935 }, 936 "immediate-bound-pvc-wrong-node": { 937 podPVCs: []*v1.PersistentVolumeClaim{immediateBoundPVC}, 938 pvs: []*v1.PersistentVolume{pvBoundImmediateNode2}, 939 reasons: ConflictReasons{ErrReasonNodeConflict}, 940 }, 941 "immediate-unbound-pvc": { 942 podPVCs: []*v1.PersistentVolumeClaim{immediateUnboundPVC}, 943 shouldFail: true, 944 }, 945 "immediate-unbound-pvc,delayed-mode-bound": { 946 podPVCs: []*v1.PersistentVolumeClaim{immediateUnboundPVC, boundPVC}, 947 pvs: []*v1.PersistentVolume{pvBound}, 948 shouldFail: true, 949 }, 950 "immediate-unbound-pvc,delayed-mode-unbound": { 951 podPVCs: []*v1.PersistentVolumeClaim{immediateUnboundPVC, unboundPVC}, 952 shouldFail: true, 953 }, 954 "generic-ephemeral,no-pvc": { 955 pod: makePod("test-pod"). 956 withNamespace("testns"). 957 withNodeName("node1"). 958 withGenericEphemeralVolume("no-such-pvc").Pod, 959 shouldFail: true, 960 }, 961 "generic-ephemeral,with-pvc": { 962 pod: makePod("test-pod"). 963 withNamespace("testns"). 964 withNodeName("node1"). 965 withGenericEphemeralVolume("test-volume").Pod, 966 cachePVCs: []*v1.PersistentVolumeClaim{correctGenericPVC}, 967 pvs: []*v1.PersistentVolume{pvBoundGeneric}, 968 }, 969 "generic-ephemeral,wrong-pvc": { 970 pod: makePod("test-pod"). 971 withNamespace("testns"). 972 withNodeName("node1"). 973 withGenericEphemeralVolume("test-volume").Pod, 974 cachePVCs: []*v1.PersistentVolumeClaim{conflictingGenericPVC}, 975 pvs: []*v1.PersistentVolume{pvBoundGeneric}, 976 shouldFail: true, 977 }, 978 } 979 980 testNode := &v1.Node{ 981 ObjectMeta: metav1.ObjectMeta{ 982 Name: "node1", 983 Labels: map[string]string{ 984 nodeLabelKey: "node1", 985 }, 986 }, 987 } 988 989 run := func(t *testing.T, scenario scenarioType, csiDriver *storagev1.CSIDriver) { 990 logger, ctx := ktesting.NewTestContext(t) 991 ctx, cancel := context.WithCancel(ctx) 992 defer cancel() 993 994 // Setup 995 testEnv := newTestBinder(t, ctx) 996 testEnv.initVolumes(scenario.pvs, scenario.pvs) 997 if csiDriver != nil { 998 testEnv.addCSIDriver(csiDriver) 999 } 1000 1001 // a. Init pvc cache 1002 if scenario.cachePVCs == nil { 1003 scenario.cachePVCs = scenario.podPVCs 1004 } 1005 testEnv.initClaims(scenario.cachePVCs, scenario.cachePVCs) 1006 1007 // b. Generate pod with given claims 1008 if scenario.pod == nil { 1009 scenario.pod = makePod("test-pod"). 1010 withNamespace("testns"). 1011 withNodeName("node1"). 1012 withPVCSVolume(scenario.podPVCs).Pod 1013 } 1014 1015 // Execute 1016 podVolumes, reasons, err := findPodVolumes(logger, testEnv.binder, scenario.pod, testNode) 1017 1018 // Validate 1019 if !scenario.shouldFail && err != nil { 1020 t.Errorf("returned error: %v", err) 1021 } 1022 if scenario.shouldFail && err == nil { 1023 t.Error("returned success but expected error") 1024 } 1025 checkReasons(t, reasons, scenario.reasons) 1026 testEnv.validatePodCache(t, testNode.Name, scenario.pod, podVolumes, scenario.expectedBindings, nil) 1027 } 1028 1029 for description, csiDriver := range map[string]*storagev1.CSIDriver{ 1030 "no CSIDriver": nil, 1031 "CSIDriver with capacity tracking": makeCSIDriver(provisioner, true), 1032 "CSIDriver without capacity tracking": makeCSIDriver(provisioner, false), 1033 } { 1034 t.Run(description, func(t *testing.T) { 1035 for name, scenario := range scenarios { 1036 t.Run(name, func(t *testing.T) { run(t, scenario, csiDriver) }) 1037 } 1038 }) 1039 } 1040 } 1041 1042 func TestFindPodVolumesWithProvisioning(t *testing.T) { 1043 t.Parallel() 1044 1045 type scenarioType struct { 1046 // Inputs 1047 pvs []*v1.PersistentVolume 1048 podPVCs []*v1.PersistentVolumeClaim 1049 // If nil, use pod PVCs 1050 cachePVCs []*v1.PersistentVolumeClaim 1051 // If nil, makePod with podPVCs 1052 pod *v1.Pod 1053 1054 // Expected podBindingCache fields 1055 expectedBindings []*BindingInfo 1056 expectedProvisions []*v1.PersistentVolumeClaim 1057 1058 // Expected return values 1059 reasons ConflictReasons 1060 shouldFail bool 1061 needsCapacity bool 1062 } 1063 scenarios := map[string]scenarioType{ 1064 "one-provisioned": { 1065 podPVCs: []*v1.PersistentVolumeClaim{provisionedPVC}, 1066 expectedProvisions: []*v1.PersistentVolumeClaim{provisionedPVC}, 1067 needsCapacity: true, 1068 }, 1069 "two-unbound-pvcs,one-matched,one-provisioned": { 1070 podPVCs: []*v1.PersistentVolumeClaim{unboundPVC, provisionedPVC}, 1071 pvs: []*v1.PersistentVolume{pvNode1a}, 1072 expectedBindings: []*BindingInfo{makeBinding(unboundPVC, pvNode1a)}, 1073 expectedProvisions: []*v1.PersistentVolumeClaim{provisionedPVC}, 1074 needsCapacity: true, 1075 }, 1076 "one-bound,one-provisioned": { 1077 podPVCs: []*v1.PersistentVolumeClaim{boundPVC, provisionedPVC}, 1078 pvs: []*v1.PersistentVolume{pvBound}, 1079 expectedProvisions: []*v1.PersistentVolumeClaim{provisionedPVC}, 1080 needsCapacity: true, 1081 }, 1082 "one-binding,one-selected-node": { 1083 podPVCs: []*v1.PersistentVolumeClaim{boundPVC, selectedNodePVC}, 1084 pvs: []*v1.PersistentVolume{pvBound}, 1085 expectedProvisions: []*v1.PersistentVolumeClaim{selectedNodePVC}, 1086 needsCapacity: true, 1087 }, 1088 "immediate-unbound-pvc": { 1089 podPVCs: []*v1.PersistentVolumeClaim{immediateUnboundPVC}, 1090 shouldFail: true, 1091 }, 1092 "one-immediate-bound,one-provisioned": { 1093 podPVCs: []*v1.PersistentVolumeClaim{immediateBoundPVC, provisionedPVC}, 1094 pvs: []*v1.PersistentVolume{pvBoundImmediate}, 1095 expectedProvisions: []*v1.PersistentVolumeClaim{provisionedPVC}, 1096 needsCapacity: true, 1097 }, 1098 "invalid-provisioner": { 1099 podPVCs: []*v1.PersistentVolumeClaim{noProvisionerPVC}, 1100 reasons: ConflictReasons{ErrReasonBindConflict}, 1101 }, 1102 "volume-topology-unsatisfied": { 1103 podPVCs: []*v1.PersistentVolumeClaim{topoMismatchPVC}, 1104 reasons: ConflictReasons{ErrReasonBindConflict}, 1105 }, 1106 } 1107 1108 testNode := &v1.Node{ 1109 ObjectMeta: metav1.ObjectMeta{ 1110 Name: "node1", 1111 Labels: map[string]string{ 1112 nodeLabelKey: "node1", 1113 }, 1114 }, 1115 } 1116 1117 run := func(t *testing.T, scenario scenarioType, csiDriver *storagev1.CSIDriver) { 1118 logger, ctx := ktesting.NewTestContext(t) 1119 ctx, cancel := context.WithCancel(ctx) 1120 defer cancel() 1121 1122 // Setup 1123 testEnv := newTestBinder(t, ctx) 1124 testEnv.initVolumes(scenario.pvs, scenario.pvs) 1125 if csiDriver != nil { 1126 testEnv.addCSIDriver(csiDriver) 1127 } 1128 1129 // a. Init pvc cache 1130 if scenario.cachePVCs == nil { 1131 scenario.cachePVCs = scenario.podPVCs 1132 } 1133 testEnv.initClaims(scenario.cachePVCs, scenario.cachePVCs) 1134 1135 // b. Generate pod with given claims 1136 if scenario.pod == nil { 1137 scenario.pod = makePod("test-pod"). 1138 withNamespace("testns"). 1139 withNodeName("node1"). 1140 withPVCSVolume(scenario.podPVCs).Pod 1141 } 1142 1143 // Execute 1144 podVolumes, reasons, err := findPodVolumes(logger, testEnv.binder, scenario.pod, testNode) 1145 1146 // Validate 1147 if !scenario.shouldFail && err != nil { 1148 t.Errorf("returned error: %v", err) 1149 } 1150 if scenario.shouldFail && err == nil { 1151 t.Error("returned success but expected error") 1152 } 1153 expectedReasons := scenario.reasons 1154 expectedProvisions := scenario.expectedProvisions 1155 if scenario.needsCapacity && 1156 csiDriver != nil && csiDriver.Spec.StorageCapacity != nil && *csiDriver.Spec.StorageCapacity { 1157 // Without CSIStorageCapacity objects, provisioning is blocked. 1158 expectedReasons = append(expectedReasons, ErrReasonNotEnoughSpace) 1159 expectedProvisions = nil 1160 } 1161 checkReasons(t, reasons, expectedReasons) 1162 testEnv.validatePodCache(t, testNode.Name, scenario.pod, podVolumes, scenario.expectedBindings, expectedProvisions) 1163 } 1164 1165 for description, csiDriver := range map[string]*storagev1.CSIDriver{ 1166 "no CSIDriver": nil, 1167 "CSIDriver with capacity tracking": makeCSIDriver(provisioner, true), 1168 "CSIDriver without capacity tracking": makeCSIDriver(provisioner, false), 1169 } { 1170 t.Run(description, func(t *testing.T) { 1171 for name, scenario := range scenarios { 1172 t.Run(name, func(t *testing.T) { run(t, scenario, csiDriver) }) 1173 } 1174 }) 1175 } 1176 } 1177 1178 // TestFindPodVolumesWithCSIMigration aims to test the node affinity check procedure that's 1179 // done in FindPodVolumes. In order to reach this code path, the given PVCs must be bound to a PV. 1180 func TestFindPodVolumesWithCSIMigration(t *testing.T) { 1181 type scenarioType struct { 1182 // Inputs 1183 pvs []*v1.PersistentVolume 1184 podPVCs []*v1.PersistentVolumeClaim 1185 // If nil, use pod PVCs 1186 cachePVCs []*v1.PersistentVolumeClaim 1187 // If nil, makePod with podPVCs 1188 pod *v1.Pod 1189 1190 // Setup 1191 initNodes []*v1.Node 1192 initCSINodes []*storagev1.CSINode 1193 1194 // Expected return values 1195 reasons ConflictReasons 1196 shouldFail bool 1197 } 1198 scenarios := map[string]scenarioType{ 1199 "pvc-bound": { 1200 podPVCs: []*v1.PersistentVolumeClaim{boundMigrationPVC}, 1201 pvs: []*v1.PersistentVolume{migrationPVBound}, 1202 initNodes: []*v1.Node{node1Zone1}, 1203 initCSINodes: []*storagev1.CSINode{csiNode1Migrated}, 1204 }, 1205 "pvc-bound,csinode-not-migrated": { 1206 podPVCs: []*v1.PersistentVolumeClaim{boundMigrationPVC}, 1207 pvs: []*v1.PersistentVolume{migrationPVBound}, 1208 initNodes: []*v1.Node{node1Zone1}, 1209 initCSINodes: []*storagev1.CSINode{csiNode1NotMigrated}, 1210 }, 1211 "pvc-bound,missing-csinode": { 1212 podPVCs: []*v1.PersistentVolumeClaim{boundMigrationPVC}, 1213 pvs: []*v1.PersistentVolume{migrationPVBound}, 1214 initNodes: []*v1.Node{node1Zone1}, 1215 }, 1216 "pvc-bound,node-different-zone": { 1217 podPVCs: []*v1.PersistentVolumeClaim{boundMigrationPVC}, 1218 pvs: []*v1.PersistentVolume{migrationPVBound}, 1219 initNodes: []*v1.Node{node1Zone2}, 1220 initCSINodes: []*storagev1.CSINode{csiNode1Migrated}, 1221 reasons: ConflictReasons{ErrReasonNodeConflict}, 1222 }, 1223 } 1224 1225 run := func(t *testing.T, scenario scenarioType) { 1226 logger, ctx := ktesting.NewTestContext(t) 1227 ctx, cancel := context.WithCancel(ctx) 1228 defer cancel() 1229 1230 // Setup 1231 testEnv := newTestBinder(t, ctx) 1232 testEnv.initVolumes(scenario.pvs, scenario.pvs) 1233 1234 var node *v1.Node 1235 if len(scenario.initNodes) > 0 { 1236 testEnv.initNodes(scenario.initNodes) 1237 node = scenario.initNodes[0] 1238 } else { 1239 node = node1 1240 } 1241 1242 if len(scenario.initCSINodes) > 0 { 1243 testEnv.initCSINodes(scenario.initCSINodes) 1244 } 1245 1246 // a. Init pvc cache 1247 if scenario.cachePVCs == nil { 1248 scenario.cachePVCs = scenario.podPVCs 1249 } 1250 testEnv.initClaims(scenario.cachePVCs, scenario.cachePVCs) 1251 1252 // b. Generate pod with given claims 1253 if scenario.pod == nil { 1254 scenario.pod = makePod("test-pod"). 1255 withNamespace("testns"). 1256 withNodeName("node1"). 1257 withPVCSVolume(scenario.podPVCs).Pod 1258 } 1259 1260 // Execute 1261 _, reasons, err := findPodVolumes(logger, testEnv.binder, scenario.pod, node) 1262 1263 // Validate 1264 if !scenario.shouldFail && err != nil { 1265 t.Errorf("returned error: %v", err) 1266 } 1267 if scenario.shouldFail && err == nil { 1268 t.Error("returned success but expected error") 1269 } 1270 checkReasons(t, reasons, scenario.reasons) 1271 } 1272 1273 for name, scenario := range scenarios { 1274 t.Run(name, func(t *testing.T) { run(t, scenario) }) 1275 } 1276 } 1277 1278 func TestAssumePodVolumes(t *testing.T) { 1279 type scenarioType struct { 1280 // Inputs 1281 podPVCs []*v1.PersistentVolumeClaim 1282 pvs []*v1.PersistentVolume 1283 bindings []*BindingInfo 1284 provisionedPVCs []*v1.PersistentVolumeClaim 1285 1286 // Expected return values 1287 shouldFail bool 1288 expectedAllBound bool 1289 1290 expectedBindings []*BindingInfo 1291 expectedProvisionings []*v1.PersistentVolumeClaim 1292 } 1293 scenarios := map[string]scenarioType{ 1294 "all-bound": { 1295 podPVCs: []*v1.PersistentVolumeClaim{boundPVC}, 1296 pvs: []*v1.PersistentVolume{pvBound}, 1297 expectedAllBound: true, 1298 }, 1299 "one-binding": { 1300 podPVCs: []*v1.PersistentVolumeClaim{unboundPVC}, 1301 bindings: []*BindingInfo{makeBinding(unboundPVC, pvNode1a)}, 1302 pvs: []*v1.PersistentVolume{pvNode1a}, 1303 expectedBindings: []*BindingInfo{makeBinding(unboundPVC, pvNode1aBound)}, 1304 expectedProvisionings: []*v1.PersistentVolumeClaim{}, 1305 }, 1306 "two-bindings": { 1307 podPVCs: []*v1.PersistentVolumeClaim{unboundPVC, unboundPVC2}, 1308 bindings: []*BindingInfo{makeBinding(unboundPVC, pvNode1a), makeBinding(unboundPVC2, pvNode1b)}, 1309 pvs: []*v1.PersistentVolume{pvNode1a, pvNode1b}, 1310 expectedBindings: []*BindingInfo{makeBinding(unboundPVC, pvNode1aBound), makeBinding(unboundPVC2, pvNode1bBound)}, 1311 expectedProvisionings: []*v1.PersistentVolumeClaim{}, 1312 }, 1313 "pv-already-bound": { 1314 podPVCs: []*v1.PersistentVolumeClaim{unboundPVC}, 1315 bindings: []*BindingInfo{makeBinding(unboundPVC, pvNode1aBound)}, 1316 pvs: []*v1.PersistentVolume{pvNode1aBound}, 1317 expectedBindings: []*BindingInfo{makeBinding(unboundPVC, pvNode1aBound)}, 1318 expectedProvisionings: []*v1.PersistentVolumeClaim{}, 1319 }, 1320 "tmpupdate-failed": { 1321 podPVCs: []*v1.PersistentVolumeClaim{unboundPVC}, 1322 bindings: []*BindingInfo{makeBinding(unboundPVC, pvNode1a), makeBinding(unboundPVC2, pvNode1b)}, 1323 pvs: []*v1.PersistentVolume{pvNode1a}, 1324 shouldFail: true, 1325 }, 1326 "one-binding, one-pvc-provisioned": { 1327 podPVCs: []*v1.PersistentVolumeClaim{unboundPVC, provisionedPVC}, 1328 bindings: []*BindingInfo{makeBinding(unboundPVC, pvNode1a)}, 1329 pvs: []*v1.PersistentVolume{pvNode1a}, 1330 provisionedPVCs: []*v1.PersistentVolumeClaim{provisionedPVC}, 1331 expectedBindings: []*BindingInfo{makeBinding(unboundPVC, pvNode1aBound)}, 1332 expectedProvisionings: []*v1.PersistentVolumeClaim{selectedNodePVC}, 1333 }, 1334 "one-binding, one-provision-tmpupdate-failed": { 1335 podPVCs: []*v1.PersistentVolumeClaim{unboundPVC, provisionedPVCHigherVersion}, 1336 bindings: []*BindingInfo{makeBinding(unboundPVC, pvNode1a)}, 1337 pvs: []*v1.PersistentVolume{pvNode1a}, 1338 provisionedPVCs: []*v1.PersistentVolumeClaim{provisionedPVC2}, 1339 shouldFail: true, 1340 }, 1341 } 1342 1343 run := func(t *testing.T, scenario scenarioType) { 1344 logger, ctx := ktesting.NewTestContext(t) 1345 ctx, cancel := context.WithCancel(ctx) 1346 defer cancel() 1347 1348 // Setup 1349 testEnv := newTestBinder(t, ctx) 1350 testEnv.initClaims(scenario.podPVCs, scenario.podPVCs) 1351 pod := makePod("test-pod"). 1352 withNamespace("testns"). 1353 withNodeName("node1"). 1354 withPVCSVolume(scenario.podPVCs).Pod 1355 podVolumes := &PodVolumes{ 1356 StaticBindings: scenario.bindings, 1357 DynamicProvisions: scenario.provisionedPVCs, 1358 } 1359 testEnv.initVolumes(scenario.pvs, scenario.pvs) 1360 1361 // Execute 1362 allBound, err := testEnv.binder.AssumePodVolumes(logger, pod, "node1", podVolumes) 1363 1364 // Validate 1365 if !scenario.shouldFail && err != nil { 1366 t.Errorf("returned error: %v", err) 1367 } 1368 if scenario.shouldFail && err == nil { 1369 t.Error("returned success but expected error") 1370 } 1371 if scenario.expectedAllBound != allBound { 1372 t.Errorf("returned unexpected allBound: %v", allBound) 1373 } 1374 if scenario.expectedBindings == nil { 1375 scenario.expectedBindings = scenario.bindings 1376 } 1377 if scenario.expectedProvisionings == nil { 1378 scenario.expectedProvisionings = scenario.provisionedPVCs 1379 } 1380 if scenario.shouldFail { 1381 testEnv.validateCacheRestored(t, pod, scenario.bindings, scenario.provisionedPVCs) 1382 } else { 1383 testEnv.validateAssume(t, pod, scenario.expectedBindings, scenario.expectedProvisionings) 1384 } 1385 testEnv.validatePodCache(t, pod.Spec.NodeName, pod, podVolumes, scenario.expectedBindings, scenario.expectedProvisionings) 1386 } 1387 1388 for name, scenario := range scenarios { 1389 t.Run(name, func(t *testing.T) { run(t, scenario) }) 1390 } 1391 } 1392 1393 func TestRevertAssumedPodVolumes(t *testing.T) { 1394 logger, ctx := ktesting.NewTestContext(t) 1395 ctx, cancel := context.WithCancel(ctx) 1396 defer cancel() 1397 1398 podPVCs := []*v1.PersistentVolumeClaim{unboundPVC, provisionedPVC} 1399 bindings := []*BindingInfo{makeBinding(unboundPVC, pvNode1a)} 1400 pvs := []*v1.PersistentVolume{pvNode1a} 1401 provisionedPVCs := []*v1.PersistentVolumeClaim{provisionedPVC} 1402 expectedBindings := []*BindingInfo{makeBinding(unboundPVC, pvNode1aBound)} 1403 expectedProvisionings := []*v1.PersistentVolumeClaim{selectedNodePVC} 1404 1405 // Setup 1406 testEnv := newTestBinder(t, ctx) 1407 testEnv.initClaims(podPVCs, podPVCs) 1408 pod := makePod("test-pod"). 1409 withNamespace("testns"). 1410 withNodeName("node1"). 1411 withPVCSVolume(podPVCs).Pod 1412 podVolumes := &PodVolumes{ 1413 StaticBindings: bindings, 1414 DynamicProvisions: provisionedPVCs, 1415 } 1416 testEnv.initVolumes(pvs, pvs) 1417 1418 allbound, err := testEnv.binder.AssumePodVolumes(logger, pod, "node1", podVolumes) 1419 if allbound || err != nil { 1420 t.Errorf("No volumes are assumed") 1421 } 1422 testEnv.validateAssume(t, pod, expectedBindings, expectedProvisionings) 1423 1424 testEnv.binder.RevertAssumedPodVolumes(podVolumes) 1425 testEnv.validateCacheRestored(t, pod, bindings, provisionedPVCs) 1426 } 1427 1428 func TestBindAPIUpdate(t *testing.T) { 1429 type scenarioType struct { 1430 // Inputs 1431 bindings []*BindingInfo 1432 cachedPVs []*v1.PersistentVolume 1433 // if nil, use cachedPVs 1434 apiPVs []*v1.PersistentVolume 1435 1436 provisionedPVCs []*v1.PersistentVolumeClaim 1437 cachedPVCs []*v1.PersistentVolumeClaim 1438 // if nil, use cachedPVCs 1439 apiPVCs []*v1.PersistentVolumeClaim 1440 1441 // Expected return values 1442 shouldFail bool 1443 expectedPVs []*v1.PersistentVolume 1444 // if nil, use expectedPVs 1445 expectedAPIPVs []*v1.PersistentVolume 1446 1447 expectedPVCs []*v1.PersistentVolumeClaim 1448 // if nil, use expectedPVCs 1449 expectedAPIPVCs []*v1.PersistentVolumeClaim 1450 } 1451 scenarios := map[string]scenarioType{ 1452 "nothing-to-bind-nil": { 1453 shouldFail: true, 1454 }, 1455 "nothing-to-bind-bindings-nil": { 1456 provisionedPVCs: []*v1.PersistentVolumeClaim{}, 1457 shouldFail: true, 1458 }, 1459 "nothing-to-bind-provisionings-nil": { 1460 bindings: []*BindingInfo{}, 1461 shouldFail: true, 1462 }, 1463 "nothing-to-bind-empty": { 1464 bindings: []*BindingInfo{}, 1465 provisionedPVCs: []*v1.PersistentVolumeClaim{}, 1466 }, 1467 "one-binding": { 1468 bindings: []*BindingInfo{makeBinding(unboundPVC, pvNode1aBound)}, 1469 cachedPVs: []*v1.PersistentVolume{pvNode1a}, 1470 expectedPVs: []*v1.PersistentVolume{pvNode1aBound}, 1471 provisionedPVCs: []*v1.PersistentVolumeClaim{}, 1472 }, 1473 "two-bindings": { 1474 bindings: []*BindingInfo{makeBinding(unboundPVC, pvNode1aBound), makeBinding(unboundPVC2, pvNode1bBound)}, 1475 cachedPVs: []*v1.PersistentVolume{pvNode1a, pvNode1b}, 1476 expectedPVs: []*v1.PersistentVolume{pvNode1aBound, pvNode1bBound}, 1477 provisionedPVCs: []*v1.PersistentVolumeClaim{}, 1478 }, 1479 "api-already-updated": { 1480 bindings: []*BindingInfo{makeBinding(unboundPVC, pvNode1aBound)}, 1481 cachedPVs: []*v1.PersistentVolume{pvNode1aBound}, 1482 expectedPVs: []*v1.PersistentVolume{pvNode1aBound}, 1483 provisionedPVCs: []*v1.PersistentVolumeClaim{}, 1484 }, 1485 "api-update-failed": { 1486 bindings: []*BindingInfo{makeBinding(unboundPVC, pvNode1aBound), makeBinding(unboundPVC2, pvNode1bBound)}, 1487 cachedPVs: []*v1.PersistentVolume{pvNode1a, pvNode1b}, 1488 apiPVs: []*v1.PersistentVolume{pvNode1a, pvNode1bBoundHigherVersion}, 1489 expectedPVs: []*v1.PersistentVolume{pvNode1aBound, pvNode1b}, 1490 expectedAPIPVs: []*v1.PersistentVolume{pvNode1aBound, pvNode1bBoundHigherVersion}, 1491 provisionedPVCs: []*v1.PersistentVolumeClaim{}, 1492 shouldFail: true, 1493 }, 1494 "one-provisioned-pvc": { 1495 bindings: []*BindingInfo{}, 1496 provisionedPVCs: []*v1.PersistentVolumeClaim{addProvisionAnn(provisionedPVC)}, 1497 cachedPVCs: []*v1.PersistentVolumeClaim{provisionedPVC}, 1498 expectedPVCs: []*v1.PersistentVolumeClaim{addProvisionAnn(provisionedPVC)}, 1499 }, 1500 "provision-api-update-failed": { 1501 bindings: []*BindingInfo{}, 1502 provisionedPVCs: []*v1.PersistentVolumeClaim{addProvisionAnn(provisionedPVC), addProvisionAnn(provisionedPVC2)}, 1503 cachedPVCs: []*v1.PersistentVolumeClaim{provisionedPVC, provisionedPVC2}, 1504 apiPVCs: []*v1.PersistentVolumeClaim{provisionedPVC, provisionedPVCHigherVersion}, 1505 expectedPVCs: []*v1.PersistentVolumeClaim{addProvisionAnn(provisionedPVC), provisionedPVC2}, 1506 expectedAPIPVCs: []*v1.PersistentVolumeClaim{addProvisionAnn(provisionedPVC), provisionedPVCHigherVersion}, 1507 shouldFail: true, 1508 }, 1509 "binding-succeed, provision-api-update-failed": { 1510 bindings: []*BindingInfo{makeBinding(unboundPVC, pvNode1aBound)}, 1511 cachedPVs: []*v1.PersistentVolume{pvNode1a}, 1512 expectedPVs: []*v1.PersistentVolume{pvNode1aBound}, 1513 provisionedPVCs: []*v1.PersistentVolumeClaim{addProvisionAnn(provisionedPVC), addProvisionAnn(provisionedPVC2)}, 1514 cachedPVCs: []*v1.PersistentVolumeClaim{provisionedPVC, provisionedPVC2}, 1515 apiPVCs: []*v1.PersistentVolumeClaim{provisionedPVC, provisionedPVCHigherVersion}, 1516 expectedPVCs: []*v1.PersistentVolumeClaim{addProvisionAnn(provisionedPVC), provisionedPVC2}, 1517 expectedAPIPVCs: []*v1.PersistentVolumeClaim{addProvisionAnn(provisionedPVC), provisionedPVCHigherVersion}, 1518 shouldFail: true, 1519 }, 1520 } 1521 1522 run := func(t *testing.T, scenario scenarioType) { 1523 _, ctx := ktesting.NewTestContext(t) 1524 ctx, cancel := context.WithCancel(ctx) 1525 defer cancel() 1526 1527 // Setup 1528 testEnv := newTestBinder(t, ctx) 1529 pod := makePod("test-pod"). 1530 withNamespace("testns"). 1531 withNodeName("node1").Pod 1532 if scenario.apiPVs == nil { 1533 scenario.apiPVs = scenario.cachedPVs 1534 } 1535 if scenario.apiPVCs == nil { 1536 scenario.apiPVCs = scenario.cachedPVCs 1537 } 1538 testEnv.initVolumes(scenario.cachedPVs, scenario.apiPVs) 1539 testEnv.initClaims(scenario.cachedPVCs, scenario.apiPVCs) 1540 testEnv.assumeVolumes(t, "node1", pod, scenario.bindings, scenario.provisionedPVCs) 1541 1542 // Execute 1543 err := testEnv.internalBinder.bindAPIUpdate(ctx, pod, scenario.bindings, scenario.provisionedPVCs) 1544 1545 // Validate 1546 if !scenario.shouldFail && err != nil { 1547 t.Errorf("returned error: %v", err) 1548 } 1549 if scenario.shouldFail && err == nil { 1550 t.Error("returned success but expected error") 1551 } 1552 if scenario.expectedAPIPVs == nil { 1553 scenario.expectedAPIPVs = scenario.expectedPVs 1554 } 1555 if scenario.expectedAPIPVCs == nil { 1556 scenario.expectedAPIPVCs = scenario.expectedPVCs 1557 } 1558 testEnv.validateBind(t, pod, scenario.expectedPVs, scenario.expectedAPIPVs) 1559 testEnv.validateProvision(t, pod, scenario.expectedPVCs, scenario.expectedAPIPVCs) 1560 } 1561 1562 for name, scenario := range scenarios { 1563 t.Run(name, func(t *testing.T) { run(t, scenario) }) 1564 } 1565 } 1566 1567 func TestCheckBindings(t *testing.T) { 1568 t.Parallel() 1569 1570 type scenarioType struct { 1571 // Inputs 1572 initPVs []*v1.PersistentVolume 1573 initPVCs []*v1.PersistentVolumeClaim 1574 1575 bindings []*BindingInfo 1576 provisionedPVCs []*v1.PersistentVolumeClaim 1577 1578 // api updates before checking 1579 apiPVs []*v1.PersistentVolume 1580 apiPVCs []*v1.PersistentVolumeClaim 1581 1582 // delete objects before checking 1583 deletePVs bool 1584 deletePVCs bool 1585 1586 // Expected return values 1587 shouldFail bool 1588 expectedBound bool 1589 } 1590 scenarios := map[string]scenarioType{ 1591 "nothing-to-bind-nil": { 1592 shouldFail: true, 1593 }, 1594 "nothing-to-bind-bindings-nil": { 1595 provisionedPVCs: []*v1.PersistentVolumeClaim{}, 1596 shouldFail: true, 1597 }, 1598 "nothing-to-bind-provisionings-nil": { 1599 bindings: []*BindingInfo{}, 1600 shouldFail: true, 1601 }, 1602 "nothing-to-bind": { 1603 bindings: []*BindingInfo{}, 1604 provisionedPVCs: []*v1.PersistentVolumeClaim{}, 1605 expectedBound: true, 1606 }, 1607 "binding-bound": { 1608 bindings: []*BindingInfo{makeBinding(unboundPVC, pvNode1aBound)}, 1609 provisionedPVCs: []*v1.PersistentVolumeClaim{}, 1610 initPVs: []*v1.PersistentVolume{pvNode1aBound}, 1611 initPVCs: []*v1.PersistentVolumeClaim{boundPVCNode1a}, 1612 expectedBound: true, 1613 }, 1614 "binding-prebound": { 1615 bindings: []*BindingInfo{makeBinding(unboundPVC, pvNode1aBound)}, 1616 provisionedPVCs: []*v1.PersistentVolumeClaim{}, 1617 initPVs: []*v1.PersistentVolume{pvNode1aBound}, 1618 initPVCs: []*v1.PersistentVolumeClaim{preboundPVCNode1a}, 1619 }, 1620 "binding-unbound": { 1621 bindings: []*BindingInfo{makeBinding(unboundPVC, pvNode1aBound)}, 1622 provisionedPVCs: []*v1.PersistentVolumeClaim{}, 1623 initPVs: []*v1.PersistentVolume{pvNode1aBound}, 1624 initPVCs: []*v1.PersistentVolumeClaim{unboundPVC}, 1625 }, 1626 "binding-pvc-not-exists": { 1627 bindings: []*BindingInfo{makeBinding(unboundPVC, pvNode1aBound)}, 1628 provisionedPVCs: []*v1.PersistentVolumeClaim{}, 1629 initPVs: []*v1.PersistentVolume{pvNode1aBound}, 1630 shouldFail: true, 1631 }, 1632 "binding-pv-not-exists": { 1633 bindings: []*BindingInfo{makeBinding(unboundPVC, pvNode1aBound)}, 1634 provisionedPVCs: []*v1.PersistentVolumeClaim{}, 1635 initPVs: []*v1.PersistentVolume{pvNode1aBound}, 1636 initPVCs: []*v1.PersistentVolumeClaim{boundPVCNode1a}, 1637 deletePVs: true, 1638 shouldFail: true, 1639 }, 1640 "binding-claimref-nil": { 1641 bindings: []*BindingInfo{makeBinding(unboundPVC, pvNode1aBound)}, 1642 provisionedPVCs: []*v1.PersistentVolumeClaim{}, 1643 initPVs: []*v1.PersistentVolume{pvNode1a}, 1644 initPVCs: []*v1.PersistentVolumeClaim{boundPVCNode1a}, 1645 apiPVs: []*v1.PersistentVolume{pvNode1a}, 1646 apiPVCs: []*v1.PersistentVolumeClaim{boundPVCNode1a}, 1647 shouldFail: true, 1648 }, 1649 "binding-claimref-uid-empty": { 1650 bindings: []*BindingInfo{makeBinding(unboundPVC, pvNode1aBound)}, 1651 provisionedPVCs: []*v1.PersistentVolumeClaim{}, 1652 initPVs: []*v1.PersistentVolume{pvNode1aBound}, 1653 initPVCs: []*v1.PersistentVolumeClaim{boundPVCNode1a}, 1654 apiPVs: []*v1.PersistentVolume{pvRemoveClaimUID(pvNode1aBound)}, 1655 apiPVCs: []*v1.PersistentVolumeClaim{boundPVCNode1a}, 1656 shouldFail: true, 1657 }, 1658 "binding-one-bound,one-unbound": { 1659 bindings: []*BindingInfo{makeBinding(unboundPVC, pvNode1aBound), makeBinding(unboundPVC2, pvNode1bBound)}, 1660 provisionedPVCs: []*v1.PersistentVolumeClaim{}, 1661 initPVs: []*v1.PersistentVolume{pvNode1aBound, pvNode1bBound}, 1662 initPVCs: []*v1.PersistentVolumeClaim{boundPVCNode1a, unboundPVC2}, 1663 }, 1664 "provisioning-pvc-bound": { 1665 bindings: []*BindingInfo{}, 1666 provisionedPVCs: []*v1.PersistentVolumeClaim{addProvisionAnn(provisionedPVC)}, 1667 initPVs: []*v1.PersistentVolume{pvBound}, 1668 initPVCs: []*v1.PersistentVolumeClaim{provisionedPVCBound}, 1669 apiPVCs: []*v1.PersistentVolumeClaim{addProvisionAnn(provisionedPVCBound)}, 1670 expectedBound: true, 1671 }, 1672 "provisioning-pvc-unbound": { 1673 bindings: []*BindingInfo{}, 1674 provisionedPVCs: []*v1.PersistentVolumeClaim{addProvisionAnn(provisionedPVC)}, 1675 initPVCs: []*v1.PersistentVolumeClaim{addProvisionAnn(provisionedPVC)}, 1676 }, 1677 "provisioning-pvc-not-exists": { 1678 bindings: []*BindingInfo{}, 1679 provisionedPVCs: []*v1.PersistentVolumeClaim{addProvisionAnn(provisionedPVC)}, 1680 initPVCs: []*v1.PersistentVolumeClaim{provisionedPVC}, 1681 deletePVCs: true, 1682 shouldFail: true, 1683 }, 1684 "provisioning-pvc-annotations-nil": { 1685 bindings: []*BindingInfo{}, 1686 provisionedPVCs: []*v1.PersistentVolumeClaim{addProvisionAnn(provisionedPVC)}, 1687 initPVCs: []*v1.PersistentVolumeClaim{provisionedPVC}, 1688 apiPVCs: []*v1.PersistentVolumeClaim{provisionedPVC}, 1689 shouldFail: true, 1690 }, 1691 "provisioning-pvc-selected-node-dropped": { 1692 bindings: []*BindingInfo{}, 1693 provisionedPVCs: []*v1.PersistentVolumeClaim{addProvisionAnn(provisionedPVC)}, 1694 initPVCs: []*v1.PersistentVolumeClaim{provisionedPVC}, 1695 apiPVCs: []*v1.PersistentVolumeClaim{pvcSetEmptyAnnotations(provisionedPVC)}, 1696 shouldFail: true, 1697 }, 1698 "provisioning-pvc-selected-node-wrong-node": { 1699 initPVCs: []*v1.PersistentVolumeClaim{provisionedPVC}, 1700 bindings: []*BindingInfo{}, 1701 provisionedPVCs: []*v1.PersistentVolumeClaim{addProvisionAnn(provisionedPVC)}, 1702 apiPVCs: []*v1.PersistentVolumeClaim{pvcSetSelectedNode(provisionedPVC, "wrong-node")}, 1703 shouldFail: true, 1704 }, 1705 "binding-bound-provisioning-unbound": { 1706 bindings: []*BindingInfo{makeBinding(unboundPVC, pvNode1aBound)}, 1707 provisionedPVCs: []*v1.PersistentVolumeClaim{addProvisionAnn(provisionedPVC)}, 1708 initPVs: []*v1.PersistentVolume{pvNode1aBound}, 1709 initPVCs: []*v1.PersistentVolumeClaim{boundPVCNode1a, addProvisionAnn(provisionedPVC)}, 1710 }, 1711 "tolerate-provisioning-pvc-bound-pv-not-found": { 1712 initPVs: []*v1.PersistentVolume{pvNode1a}, 1713 initPVCs: []*v1.PersistentVolumeClaim{provisionedPVC}, 1714 bindings: []*BindingInfo{}, 1715 provisionedPVCs: []*v1.PersistentVolumeClaim{addProvisionAnn(provisionedPVC)}, 1716 apiPVCs: []*v1.PersistentVolumeClaim{addProvisionAnn(provisionedPVCBound)}, 1717 deletePVs: true, 1718 }, 1719 } 1720 1721 run := func(t *testing.T, scenario scenarioType) { 1722 logger, ctx := ktesting.NewTestContext(t) 1723 ctx, cancel := context.WithCancel(ctx) 1724 defer cancel() 1725 // Setup 1726 pod := makePod("test-pod"). 1727 withNamespace("testns"). 1728 withNodeName("node1").Pod 1729 testEnv := newTestBinder(t, ctx) 1730 testEnv.internalPodInformer.Informer().GetIndexer().Add(pod) 1731 testEnv.initNodes([]*v1.Node{node1}) 1732 testEnv.initVolumes(scenario.initPVs, nil) 1733 testEnv.initClaims(scenario.initPVCs, nil) 1734 testEnv.assumeVolumes(t, "node1", pod, scenario.bindings, scenario.provisionedPVCs) 1735 1736 // Before execute 1737 if scenario.deletePVs { 1738 testEnv.deleteVolumes(scenario.initPVs) 1739 } else { 1740 if err := testEnv.updateVolumes(ctx, scenario.apiPVs); err != nil { 1741 t.Errorf("Failed to update PVs: %v", err) 1742 } 1743 } 1744 if scenario.deletePVCs { 1745 testEnv.deleteClaims(scenario.initPVCs) 1746 } else { 1747 if err := testEnv.updateClaims(ctx, scenario.apiPVCs); err != nil { 1748 t.Errorf("Failed to update PVCs: %v", err) 1749 } 1750 } 1751 1752 // Execute 1753 allBound, err := testEnv.internalBinder.checkBindings(logger, pod, scenario.bindings, scenario.provisionedPVCs) 1754 1755 // Validate 1756 if !scenario.shouldFail && err != nil { 1757 t.Errorf("returned error: %v", err) 1758 } 1759 if scenario.shouldFail && err == nil { 1760 t.Error("returned success but expected error") 1761 } 1762 if scenario.expectedBound != allBound { 1763 t.Errorf("returned bound %v", allBound) 1764 } 1765 } 1766 1767 for name, scenario := range scenarios { 1768 t.Run(name, func(t *testing.T) { run(t, scenario) }) 1769 } 1770 } 1771 1772 func TestCheckBindingsWithCSIMigration(t *testing.T) { 1773 t.Parallel() 1774 1775 type scenarioType struct { 1776 // Inputs 1777 initPVs []*v1.PersistentVolume 1778 initPVCs []*v1.PersistentVolumeClaim 1779 initNodes []*v1.Node 1780 initCSINodes []*storagev1.CSINode 1781 1782 bindings []*BindingInfo 1783 provisionedPVCs []*v1.PersistentVolumeClaim 1784 1785 // API updates before checking 1786 apiPVs []*v1.PersistentVolume 1787 apiPVCs []*v1.PersistentVolumeClaim 1788 1789 // Expected return values 1790 shouldFail bool 1791 expectedBound bool 1792 } 1793 scenarios := map[string]scenarioType{ 1794 "provisioning-pvc-bound": { 1795 bindings: []*BindingInfo{}, 1796 provisionedPVCs: []*v1.PersistentVolumeClaim{addProvisionAnn(provMigrationPVCBound)}, 1797 initPVs: []*v1.PersistentVolume{migrationPVBound}, 1798 initPVCs: []*v1.PersistentVolumeClaim{provMigrationPVCBound}, 1799 initNodes: []*v1.Node{node1Zone1}, 1800 initCSINodes: []*storagev1.CSINode{csiNode1Migrated}, 1801 apiPVCs: []*v1.PersistentVolumeClaim{addProvisionAnn(provMigrationPVCBound)}, 1802 expectedBound: true, 1803 }, 1804 "binding-node-pv-same-zone": { 1805 bindings: []*BindingInfo{makeBinding(unboundPVC, migrationPVBoundToUnbound)}, 1806 provisionedPVCs: []*v1.PersistentVolumeClaim{}, 1807 initPVs: []*v1.PersistentVolume{migrationPVBoundToUnbound}, 1808 initPVCs: []*v1.PersistentVolumeClaim{unboundPVC}, 1809 initNodes: []*v1.Node{node1Zone1}, 1810 initCSINodes: []*storagev1.CSINode{csiNode1Migrated}, 1811 }, 1812 "binding-without-csinode": { 1813 bindings: []*BindingInfo{makeBinding(unboundPVC, migrationPVBoundToUnbound)}, 1814 provisionedPVCs: []*v1.PersistentVolumeClaim{}, 1815 initPVs: []*v1.PersistentVolume{migrationPVBoundToUnbound}, 1816 initPVCs: []*v1.PersistentVolumeClaim{unboundPVC}, 1817 initNodes: []*v1.Node{node1Zone1}, 1818 initCSINodes: []*storagev1.CSINode{}, 1819 }, 1820 "binding-non-migrated-plugin": { 1821 bindings: []*BindingInfo{makeBinding(unboundPVC, migrationPVBoundToUnbound)}, 1822 provisionedPVCs: []*v1.PersistentVolumeClaim{}, 1823 initPVs: []*v1.PersistentVolume{migrationPVBoundToUnbound}, 1824 initPVCs: []*v1.PersistentVolumeClaim{unboundPVC}, 1825 initNodes: []*v1.Node{node1Zone1}, 1826 initCSINodes: []*storagev1.CSINode{csiNode1NotMigrated}, 1827 }, 1828 "binding-node-pv-in-different-zones": { 1829 bindings: []*BindingInfo{makeBinding(unboundPVC, migrationPVBoundToUnbound)}, 1830 provisionedPVCs: []*v1.PersistentVolumeClaim{}, 1831 initPVs: []*v1.PersistentVolume{migrationPVBoundToUnbound}, 1832 initPVCs: []*v1.PersistentVolumeClaim{unboundPVC}, 1833 initNodes: []*v1.Node{node1Zone2}, 1834 initCSINodes: []*storagev1.CSINode{csiNode1Migrated}, 1835 shouldFail: true, 1836 }, 1837 "binding-node-pv-different-zones-migration-off": { 1838 bindings: []*BindingInfo{makeBinding(unboundPVC, nonmigrationPVBoundToUnbound)}, 1839 provisionedPVCs: []*v1.PersistentVolumeClaim{}, 1840 initPVs: []*v1.PersistentVolume{nonmigrationPVBoundToUnbound}, 1841 initPVCs: []*v1.PersistentVolumeClaim{unboundPVC}, 1842 initNodes: []*v1.Node{node1Zone2}, 1843 initCSINodes: []*storagev1.CSINode{csiNode1Migrated}, 1844 }, 1845 } 1846 1847 run := func(t *testing.T, scenario scenarioType) { 1848 logger, ctx := ktesting.NewTestContext(t) 1849 ctx, cancel := context.WithCancel(ctx) 1850 defer cancel() 1851 1852 // Setup 1853 pod := makePod("test-pod"). 1854 withNamespace("testns"). 1855 withNodeName("node1").Pod 1856 testEnv := newTestBinder(t, ctx) 1857 testEnv.internalPodInformer.Informer().GetIndexer().Add(pod) 1858 testEnv.initNodes(scenario.initNodes) 1859 testEnv.initCSINodes(scenario.initCSINodes) 1860 testEnv.initVolumes(scenario.initPVs, nil) 1861 testEnv.initClaims(scenario.initPVCs, nil) 1862 testEnv.assumeVolumes(t, "node1", pod, scenario.bindings, scenario.provisionedPVCs) 1863 1864 // Before execute 1865 if err := testEnv.updateVolumes(ctx, scenario.apiPVs); err != nil { 1866 t.Errorf("Failed to update PVs: %v", err) 1867 } 1868 if err := testEnv.updateClaims(ctx, scenario.apiPVCs); err != nil { 1869 t.Errorf("Failed to update PVCs: %v", err) 1870 } 1871 1872 // Execute 1873 allBound, err := testEnv.internalBinder.checkBindings(logger, pod, scenario.bindings, scenario.provisionedPVCs) 1874 1875 // Validate 1876 if !scenario.shouldFail && err != nil { 1877 t.Errorf("returned error: %v", err) 1878 } 1879 if scenario.shouldFail && err == nil { 1880 t.Error("returned success but expected error") 1881 } 1882 if scenario.expectedBound != allBound { 1883 t.Errorf("returned bound %v", allBound) 1884 } 1885 } 1886 1887 for name, scenario := range scenarios { 1888 t.Run(name, func(t *testing.T) { run(t, scenario) }) 1889 } 1890 } 1891 1892 func TestBindPodVolumes(t *testing.T) { 1893 t.Parallel() 1894 1895 type scenarioType struct { 1896 // Inputs 1897 bindingsNil bool // Pass in nil bindings slice 1898 1899 nodes []*v1.Node 1900 1901 // before assume 1902 initPVs []*v1.PersistentVolume 1903 initPVCs []*v1.PersistentVolumeClaim 1904 1905 // assume PV & PVC with these binding results 1906 binding *BindingInfo 1907 claimToProvision *v1.PersistentVolumeClaim 1908 1909 // API updates after assume before bind 1910 apiPV *v1.PersistentVolume 1911 apiPVC *v1.PersistentVolumeClaim 1912 1913 // This function runs with a delay of 5 seconds 1914 delayFunc func(t *testing.T, ctx context.Context, testEnv *testEnv, pod *v1.Pod, pvs []*v1.PersistentVolume, pvcs []*v1.PersistentVolumeClaim) 1915 1916 // Expected return values 1917 shouldFail bool 1918 } 1919 scenarios := map[string]scenarioType{ 1920 "nothing-to-bind-nil": { 1921 bindingsNil: true, 1922 shouldFail: true, 1923 }, 1924 "nothing-to-bind-empty": {}, 1925 "already-bound": { 1926 binding: makeBinding(unboundPVC, pvNode1aBound), 1927 initPVs: []*v1.PersistentVolume{pvNode1aBound}, 1928 initPVCs: []*v1.PersistentVolumeClaim{boundPVCNode1a}, 1929 }, 1930 "binding-static-pv-succeeds-after-time": { 1931 initPVs: []*v1.PersistentVolume{pvNode1a}, 1932 initPVCs: []*v1.PersistentVolumeClaim{unboundPVC}, 1933 binding: makeBinding(unboundPVC, pvNode1aBound), 1934 shouldFail: false, // Will succeed after PVC is fully bound to this PV by pv controller. 1935 delayFunc: func(t *testing.T, ctx context.Context, testEnv *testEnv, pod *v1.Pod, pvs []*v1.PersistentVolume, pvcs []*v1.PersistentVolumeClaim) { 1936 pvc := pvcs[0] 1937 pv := pvs[0] 1938 // Update PVC to be fully bound to PV 1939 newPVC := pvc.DeepCopy() 1940 newPVC.Spec.VolumeName = pv.Name 1941 metav1.SetMetaDataAnnotation(&newPVC.ObjectMeta, volume.AnnBindCompleted, "yes") 1942 if _, err := testEnv.client.CoreV1().PersistentVolumeClaims(newPVC.Namespace).Update(ctx, newPVC, metav1.UpdateOptions{}); err != nil { 1943 t.Errorf("failed to update PVC %q: %v", newPVC.Name, err) 1944 } 1945 }, 1946 }, 1947 "binding-dynamic-pv-succeeds-after-time": { 1948 claimToProvision: pvcSetSelectedNode(provisionedPVC, "node1"), 1949 initPVCs: []*v1.PersistentVolumeClaim{provisionedPVC}, 1950 delayFunc: func(t *testing.T, ctx context.Context, testEnv *testEnv, pod *v1.Pod, pvs []*v1.PersistentVolume, pvcs []*v1.PersistentVolumeClaim) { 1951 pvc := pvcs[0] 1952 // Update PVC to be fully bound to PV 1953 newPVC, err := testEnv.client.CoreV1().PersistentVolumeClaims(pvc.Namespace).Get(ctx, pvc.Name, metav1.GetOptions{}) 1954 if err != nil { 1955 t.Errorf("failed to get PVC %q: %v", pvc.Name, err) 1956 return 1957 } 1958 dynamicPV := makeTestPV("dynamic-pv", "node1", "1G", "1", newPVC, waitClass) 1959 dynamicPV, err = testEnv.client.CoreV1().PersistentVolumes().Create(ctx, dynamicPV, metav1.CreateOptions{}) 1960 if err != nil { 1961 t.Errorf("failed to create PV %q: %v", dynamicPV.Name, err) 1962 return 1963 } 1964 newPVC.Spec.VolumeName = dynamicPV.Name 1965 metav1.SetMetaDataAnnotation(&newPVC.ObjectMeta, volume.AnnBindCompleted, "yes") 1966 if _, err := testEnv.client.CoreV1().PersistentVolumeClaims(newPVC.Namespace).Update(ctx, newPVC, metav1.UpdateOptions{}); err != nil { 1967 t.Errorf("failed to update PVC %q: %v", newPVC.Name, err) 1968 } 1969 }, 1970 }, 1971 "bound-by-pv-controller-before-bind": { 1972 initPVs: []*v1.PersistentVolume{pvNode1a}, 1973 initPVCs: []*v1.PersistentVolumeClaim{unboundPVC}, 1974 binding: makeBinding(unboundPVC, pvNode1aBound), 1975 apiPV: pvNode1aBound, 1976 apiPVC: boundPVCNode1a, 1977 shouldFail: true, // bindAPIUpdate will fail because API conflict 1978 }, 1979 "pod-deleted-after-time": { 1980 binding: makeBinding(unboundPVC, pvNode1aBound), 1981 initPVs: []*v1.PersistentVolume{pvNode1a}, 1982 initPVCs: []*v1.PersistentVolumeClaim{unboundPVC}, 1983 delayFunc: func(t *testing.T, ctx context.Context, testEnv *testEnv, pod *v1.Pod, pvs []*v1.PersistentVolume, pvcs []*v1.PersistentVolumeClaim) { 1984 testEnv.client.CoreV1().Pods(pod.Namespace).Delete(ctx, pod.Name, metav1.DeleteOptions{}) 1985 }, 1986 shouldFail: true, 1987 }, 1988 "binding-times-out": { 1989 binding: makeBinding(unboundPVC, pvNode1aBound), 1990 initPVs: []*v1.PersistentVolume{pvNode1a}, 1991 initPVCs: []*v1.PersistentVolumeClaim{unboundPVC}, 1992 shouldFail: true, 1993 }, 1994 "binding-fails": { 1995 binding: makeBinding(unboundPVC2, pvNode1bBound), 1996 initPVs: []*v1.PersistentVolume{pvNode1b}, 1997 initPVCs: []*v1.PersistentVolumeClaim{unboundPVC2}, 1998 shouldFail: true, 1999 }, 2000 "check-fails": { 2001 binding: makeBinding(unboundPVC, pvNode1aBound), 2002 initPVs: []*v1.PersistentVolume{pvNode1a}, 2003 initPVCs: []*v1.PersistentVolumeClaim{unboundPVC}, 2004 delayFunc: func(t *testing.T, ctx context.Context, testEnv *testEnv, pod *v1.Pod, pvs []*v1.PersistentVolume, pvcs []*v1.PersistentVolumeClaim) { 2005 pvc := pvcs[0] 2006 // Delete PVC will fail check 2007 if err := testEnv.client.CoreV1().PersistentVolumeClaims(pvc.Namespace).Delete(ctx, pvc.Name, metav1.DeleteOptions{}); err != nil { 2008 t.Errorf("failed to delete PVC %q: %v", pvc.Name, err) 2009 } 2010 }, 2011 shouldFail: true, 2012 }, 2013 "node-affinity-fails": { 2014 binding: makeBinding(unboundPVC, pvNode1aBound), 2015 initPVs: []*v1.PersistentVolume{pvNode1aBound}, 2016 initPVCs: []*v1.PersistentVolumeClaim{boundPVCNode1a}, 2017 nodes: []*v1.Node{node1NoLabels}, 2018 shouldFail: true, 2019 }, 2020 "node-affinity-fails-dynamic-provisioning": { 2021 initPVs: []*v1.PersistentVolume{pvNode1a, pvNode2}, 2022 initPVCs: []*v1.PersistentVolumeClaim{selectedNodePVC}, 2023 claimToProvision: selectedNodePVC, 2024 nodes: []*v1.Node{node1, node2}, 2025 delayFunc: func(t *testing.T, ctx context.Context, testEnv *testEnv, pod *v1.Pod, pvs []*v1.PersistentVolume, pvcs []*v1.PersistentVolumeClaim) { 2026 // Update PVC to be fully bound to a PV with a different node 2027 newPVC := pvcs[0].DeepCopy() 2028 newPVC.Spec.VolumeName = pvNode2.Name 2029 metav1.SetMetaDataAnnotation(&newPVC.ObjectMeta, volume.AnnBindCompleted, "yes") 2030 if _, err := testEnv.client.CoreV1().PersistentVolumeClaims(newPVC.Namespace).Update(ctx, newPVC, metav1.UpdateOptions{}); err != nil { 2031 t.Errorf("failed to update PVC %q: %v", newPVC.Name, err) 2032 } 2033 }, 2034 shouldFail: true, 2035 }, 2036 } 2037 2038 run := func(t *testing.T, scenario scenarioType) { 2039 logger, ctx := ktesting.NewTestContext(t) 2040 ctx, cancel := context.WithCancel(ctx) 2041 defer cancel() 2042 // Setup 2043 pod := makePod("test-pod"). 2044 withNamespace("testns"). 2045 withNodeName("node1").Pod 2046 testEnv := newTestBinder(t, ctx) 2047 testEnv.internalPodInformer.Informer().GetIndexer().Add(pod) 2048 if scenario.nodes == nil { 2049 scenario.nodes = []*v1.Node{node1} 2050 } 2051 bindings := []*BindingInfo{} 2052 claimsToProvision := []*v1.PersistentVolumeClaim{} 2053 if !scenario.bindingsNil { 2054 if scenario.binding != nil { 2055 bindings = []*BindingInfo{scenario.binding} 2056 } 2057 if scenario.claimToProvision != nil { 2058 claimsToProvision = []*v1.PersistentVolumeClaim{scenario.claimToProvision} 2059 } 2060 testEnv.initNodes(scenario.nodes) 2061 testEnv.initVolumes(scenario.initPVs, scenario.initPVs) 2062 testEnv.initClaims(scenario.initPVCs, scenario.initPVCs) 2063 testEnv.assumeVolumes(t, "node1", pod, bindings, claimsToProvision) 2064 } 2065 2066 // Before Execute 2067 if scenario.apiPV != nil { 2068 _, err := testEnv.client.CoreV1().PersistentVolumes().Update(ctx, scenario.apiPV, metav1.UpdateOptions{}) 2069 if err != nil { 2070 t.Fatalf("failed to update PV %q", scenario.apiPV.Name) 2071 } 2072 } 2073 if scenario.apiPVC != nil { 2074 _, err := testEnv.client.CoreV1().PersistentVolumeClaims(scenario.apiPVC.Namespace).Update(ctx, scenario.apiPVC, metav1.UpdateOptions{}) 2075 if err != nil { 2076 t.Fatalf("failed to update PVC %q", getPVCName(scenario.apiPVC)) 2077 } 2078 } 2079 2080 if scenario.delayFunc != nil { 2081 go func(scenario scenarioType) { 2082 time.Sleep(5 * time.Second) 2083 // Sleep a while to run after bindAPIUpdate in BindPodVolumes 2084 logger.V(5).Info("Running delay function") 2085 scenario.delayFunc(t, ctx, testEnv, pod, scenario.initPVs, scenario.initPVCs) 2086 }(scenario) 2087 } 2088 2089 // Execute 2090 podVolumes := &PodVolumes{ 2091 StaticBindings: bindings, 2092 DynamicProvisions: claimsToProvision, 2093 } 2094 err := testEnv.binder.BindPodVolumes(ctx, pod, podVolumes) 2095 2096 // Validate 2097 if !scenario.shouldFail && err != nil { 2098 t.Errorf("returned error: %v", err) 2099 } 2100 if scenario.shouldFail && err == nil { 2101 t.Error("returned success but expected error") 2102 } 2103 } 2104 2105 for name, scenario := range scenarios { 2106 scenario := scenario 2107 t.Run(name, func(t *testing.T) { 2108 t.Parallel() 2109 run(t, scenario) 2110 }) 2111 } 2112 } 2113 2114 func TestFindAssumeVolumes(t *testing.T) { 2115 // Test case 2116 podPVCs := []*v1.PersistentVolumeClaim{unboundPVC} 2117 pvs := []*v1.PersistentVolume{pvNode2, pvNode1a, pvNode1c} 2118 2119 // Setup 2120 logger, ctx := ktesting.NewTestContext(t) 2121 ctx, cancel := context.WithCancel(ctx) 2122 defer cancel() 2123 testEnv := newTestBinder(t, ctx) 2124 testEnv.initVolumes(pvs, pvs) 2125 testEnv.initClaims(podPVCs, podPVCs) 2126 pod := makePod("test-pod"). 2127 withNamespace("testns"). 2128 withNodeName("node1"). 2129 withPVCSVolume(podPVCs).Pod 2130 2131 testNode := &v1.Node{ 2132 ObjectMeta: metav1.ObjectMeta{ 2133 Name: "node1", 2134 Labels: map[string]string{ 2135 nodeLabelKey: "node1", 2136 }, 2137 }, 2138 } 2139 2140 // Execute 2141 // 1. Find matching PVs 2142 podVolumes, reasons, err := findPodVolumes(logger, testEnv.binder, pod, testNode) 2143 if err != nil { 2144 t.Errorf("Test failed: FindPodVolumes returned error: %v", err) 2145 } 2146 if len(reasons) > 0 { 2147 t.Errorf("Test failed: couldn't find PVs for all PVCs: %v", reasons) 2148 } 2149 expectedBindings := podVolumes.StaticBindings 2150 2151 // 2. Assume matches 2152 allBound, err := testEnv.binder.AssumePodVolumes(logger, pod, testNode.Name, podVolumes) 2153 if err != nil { 2154 t.Errorf("Test failed: AssumePodVolumes returned error: %v", err) 2155 } 2156 if allBound { 2157 t.Errorf("Test failed: detected unbound volumes as bound") 2158 } 2159 testEnv.validateAssume(t, pod, expectedBindings, nil) 2160 2161 // After assume, claimref should be set on pv 2162 expectedBindings = podVolumes.StaticBindings 2163 2164 // 3. Find matching PVs again 2165 // This should always return the original chosen pv 2166 // Run this many times in case sorting returns different orders for the two PVs. 2167 for i := 0; i < 50; i++ { 2168 podVolumes, reasons, err := findPodVolumes(logger, testEnv.binder, pod, testNode) 2169 if err != nil { 2170 t.Errorf("Test failed: FindPodVolumes returned error: %v", err) 2171 } 2172 if len(reasons) > 0 { 2173 t.Errorf("Test failed: couldn't find PVs for all PVCs: %v", reasons) 2174 } 2175 testEnv.validatePodCache(t, testNode.Name, pod, podVolumes, expectedBindings, nil) 2176 } 2177 } 2178 2179 // TestCapacity covers different scenarios involving CSIStorageCapacity objects. 2180 // Scenarios without those are covered by TestFindPodVolumesWithProvisioning. 2181 func TestCapacity(t *testing.T) { 2182 type scenarioType struct { 2183 // Inputs 2184 pvcs []*v1.PersistentVolumeClaim 2185 capacities []*storagev1.CSIStorageCapacity 2186 2187 // Expected return values 2188 reasons ConflictReasons 2189 shouldFail bool 2190 } 2191 scenarios := map[string]scenarioType{ 2192 "network-attached": { 2193 pvcs: []*v1.PersistentVolumeClaim{provisionedPVC}, 2194 capacities: []*storagev1.CSIStorageCapacity{ 2195 makeCapacity("net", waitClassWithProvisioner, nil, "1Gi", ""), 2196 }, 2197 }, 2198 "local-storage": { 2199 pvcs: []*v1.PersistentVolumeClaim{provisionedPVC}, 2200 capacities: []*storagev1.CSIStorageCapacity{ 2201 makeCapacity("net", waitClassWithProvisioner, node1, "1Gi", ""), 2202 }, 2203 }, 2204 "multiple": { 2205 pvcs: []*v1.PersistentVolumeClaim{provisionedPVC}, 2206 capacities: []*storagev1.CSIStorageCapacity{ 2207 makeCapacity("net", waitClassWithProvisioner, nil, "1Gi", ""), 2208 makeCapacity("net", waitClassWithProvisioner, node2, "1Gi", ""), 2209 makeCapacity("net", waitClassWithProvisioner, node1, "1Gi", ""), 2210 }, 2211 }, 2212 "no-storage": { 2213 pvcs: []*v1.PersistentVolumeClaim{provisionedPVC}, 2214 reasons: ConflictReasons{ErrReasonNotEnoughSpace}, 2215 }, 2216 "wrong-node": { 2217 pvcs: []*v1.PersistentVolumeClaim{provisionedPVC}, 2218 capacities: []*storagev1.CSIStorageCapacity{ 2219 makeCapacity("net", waitClassWithProvisioner, node2, "1Gi", ""), 2220 }, 2221 reasons: ConflictReasons{ErrReasonNotEnoughSpace}, 2222 }, 2223 "wrong-storage-class": { 2224 pvcs: []*v1.PersistentVolumeClaim{provisionedPVC}, 2225 capacities: []*storagev1.CSIStorageCapacity{ 2226 makeCapacity("net", waitClass, node1, "1Gi", ""), 2227 }, 2228 reasons: ConflictReasons{ErrReasonNotEnoughSpace}, 2229 }, 2230 "insufficient-storage": { 2231 pvcs: []*v1.PersistentVolumeClaim{provisionedPVC}, 2232 capacities: []*storagev1.CSIStorageCapacity{ 2233 makeCapacity("net", waitClassWithProvisioner, node1, "1Mi", ""), 2234 }, 2235 reasons: ConflictReasons{ErrReasonNotEnoughSpace}, 2236 }, 2237 "insufficient-volume-size": { 2238 pvcs: []*v1.PersistentVolumeClaim{provisionedPVC}, 2239 capacities: []*storagev1.CSIStorageCapacity{ 2240 makeCapacity("net", waitClassWithProvisioner, node1, "1Gi", "1Mi"), 2241 }, 2242 reasons: ConflictReasons{ErrReasonNotEnoughSpace}, 2243 }, 2244 "zero-storage": { 2245 pvcs: []*v1.PersistentVolumeClaim{provisionedPVC}, 2246 capacities: []*storagev1.CSIStorageCapacity{ 2247 makeCapacity("net", waitClassWithProvisioner, node1, "0Mi", ""), 2248 }, 2249 reasons: ConflictReasons{ErrReasonNotEnoughSpace}, 2250 }, 2251 "zero-volume-size": { 2252 pvcs: []*v1.PersistentVolumeClaim{provisionedPVC}, 2253 capacities: []*storagev1.CSIStorageCapacity{ 2254 makeCapacity("net", waitClassWithProvisioner, node1, "", "0Mi"), 2255 }, 2256 reasons: ConflictReasons{ErrReasonNotEnoughSpace}, 2257 }, 2258 "nil-storage": { 2259 pvcs: []*v1.PersistentVolumeClaim{provisionedPVC}, 2260 capacities: []*storagev1.CSIStorageCapacity{ 2261 makeCapacity("net", waitClassWithProvisioner, node1, "", ""), 2262 }, 2263 reasons: ConflictReasons{ErrReasonNotEnoughSpace}, 2264 }, 2265 } 2266 2267 testNode := &v1.Node{ 2268 ObjectMeta: metav1.ObjectMeta{ 2269 Name: "node1", 2270 Labels: map[string]string{ 2271 nodeLabelKey: "node1", 2272 }, 2273 }, 2274 } 2275 2276 run := func(t *testing.T, scenario scenarioType, optIn bool) { 2277 logger, ctx := ktesting.NewTestContext(t) 2278 ctx, cancel := context.WithCancel(ctx) 2279 defer cancel() 2280 2281 // Setup: the driver has the feature enabled, but the scheduler might not. 2282 testEnv := newTestBinder(t, ctx) 2283 testEnv.addCSIDriver(makeCSIDriver(provisioner, optIn)) 2284 testEnv.addCSIStorageCapacities(scenario.capacities) 2285 2286 // a. Init pvc cache 2287 testEnv.initClaims(scenario.pvcs, scenario.pvcs) 2288 2289 // b. Generate pod with given claims 2290 pod := makePod("test-pod"). 2291 withNamespace("testns"). 2292 withNodeName("node1"). 2293 withPVCSVolume(scenario.pvcs).Pod 2294 2295 // Execute 2296 podVolumes, reasons, err := findPodVolumes(logger, testEnv.binder, pod, testNode) 2297 2298 // Validate 2299 shouldFail := scenario.shouldFail 2300 expectedReasons := scenario.reasons 2301 if !optIn { 2302 shouldFail = false 2303 expectedReasons = nil 2304 } 2305 if !shouldFail && err != nil { 2306 t.Errorf("returned error: %v", err) 2307 } 2308 if shouldFail && err == nil { 2309 t.Error("returned success but expected error") 2310 } 2311 checkReasons(t, reasons, expectedReasons) 2312 provisions := scenario.pvcs 2313 if len(reasons) > 0 { 2314 provisions = nil 2315 } 2316 testEnv.validatePodCache(t, pod.Spec.NodeName, pod, podVolumes, nil, provisions) 2317 } 2318 2319 yesNo := []bool{true, false} 2320 for _, optIn := range yesNo { 2321 name := fmt.Sprintf("CSIDriver.StorageCapacity=%v", optIn) 2322 t.Run(name, func(t *testing.T) { 2323 for name, scenario := range scenarios { 2324 t.Run(name, func(t *testing.T) { run(t, scenario, optIn) }) 2325 } 2326 }) 2327 } 2328 } 2329 2330 func TestGetEligibleNodes(t *testing.T) { 2331 type scenarioType struct { 2332 // Inputs 2333 pvcs []*v1.PersistentVolumeClaim 2334 pvs []*v1.PersistentVolume 2335 nodes []*v1.Node 2336 2337 // Expected return values 2338 eligibleNodes sets.Set[string] 2339 } 2340 2341 scenarios := map[string]scenarioType{ 2342 "no-bound-claims": {}, 2343 "no-nodes-found": { 2344 pvcs: []*v1.PersistentVolumeClaim{ 2345 preboundPVC, 2346 preboundPVCNode1a, 2347 }, 2348 }, 2349 "pv-not-found": { 2350 pvcs: []*v1.PersistentVolumeClaim{ 2351 preboundPVC, 2352 preboundPVCNode1a, 2353 }, 2354 nodes: []*v1.Node{ 2355 node1, 2356 }, 2357 }, 2358 "node-affinity-mismatch": { 2359 pvcs: []*v1.PersistentVolumeClaim{ 2360 preboundPVC, 2361 preboundPVCNode1a, 2362 }, 2363 pvs: []*v1.PersistentVolume{ 2364 pvNode1a, 2365 }, 2366 nodes: []*v1.Node{ 2367 node1, 2368 node2, 2369 }, 2370 }, 2371 "local-pv-with-node-affinity": { 2372 pvcs: []*v1.PersistentVolumeClaim{ 2373 localPreboundPVC1a, 2374 localPreboundPVC1b, 2375 }, 2376 pvs: []*v1.PersistentVolume{ 2377 localPVNode1a, 2378 localPVNode1b, 2379 }, 2380 nodes: []*v1.Node{ 2381 node1, 2382 node2, 2383 }, 2384 eligibleNodes: sets.New("node1"), 2385 }, 2386 "multi-local-pv-with-different-nodes": { 2387 pvcs: []*v1.PersistentVolumeClaim{ 2388 localPreboundPVC1a, 2389 localPreboundPVC1b, 2390 localPreboundPVC2a, 2391 }, 2392 pvs: []*v1.PersistentVolume{ 2393 localPVNode1a, 2394 localPVNode1b, 2395 localPVNode2a, 2396 }, 2397 nodes: []*v1.Node{ 2398 node1, 2399 node2, 2400 }, 2401 eligibleNodes: sets.New[string](), 2402 }, 2403 "local-and-non-local-pv": { 2404 pvcs: []*v1.PersistentVolumeClaim{ 2405 localPreboundPVC1a, 2406 localPreboundPVC1b, 2407 preboundPVC, 2408 immediateBoundPVC, 2409 }, 2410 pvs: []*v1.PersistentVolume{ 2411 localPVNode1a, 2412 localPVNode1b, 2413 pvNode1a, 2414 pvBoundImmediate, 2415 pvBoundImmediateNode2, 2416 }, 2417 nodes: []*v1.Node{ 2418 node1, 2419 node2, 2420 }, 2421 eligibleNodes: sets.New("node1"), 2422 }, 2423 } 2424 2425 run := func(t *testing.T, scenario scenarioType) { 2426 logger, ctx := ktesting.NewTestContext(t) 2427 ctx, cancel := context.WithCancel(ctx) 2428 defer cancel() 2429 2430 // Setup 2431 testEnv := newTestBinder(t, ctx) 2432 testEnv.initVolumes(scenario.pvs, scenario.pvs) 2433 2434 testEnv.initNodes(scenario.nodes) 2435 testEnv.initClaims(scenario.pvcs, scenario.pvcs) 2436 2437 // Execute 2438 eligibleNodes := testEnv.binder.GetEligibleNodes(logger, scenario.pvcs) 2439 2440 // Validate 2441 if reflect.DeepEqual(scenario.eligibleNodes, eligibleNodes) { 2442 fmt.Println("foo") 2443 } 2444 2445 if compDiff := cmp.Diff(scenario.eligibleNodes, eligibleNodes, cmp.Comparer(func(a, b sets.Set[string]) bool { 2446 return reflect.DeepEqual(a, b) 2447 })); compDiff != "" { 2448 t.Errorf("Unexpected eligible nodes (-want +got):\n%s", compDiff) 2449 } 2450 } 2451 2452 for name, scenario := range scenarios { 2453 t.Run(name, func(t *testing.T) { run(t, scenario) }) 2454 } 2455 }