volcano.sh/volcano@v1.9.0/pkg/scheduler/capabilities/volumebinding/volume_binding_test.go (about) 1 /* 2 Copyright 2019 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 "testing" 22 23 "github.com/google/go-cmp/cmp" 24 "github.com/google/go-cmp/cmp/cmpopts" 25 "github.com/stretchr/testify/assert" 26 v1 "k8s.io/api/core/v1" 27 storagev1 "k8s.io/api/storage/v1" 28 "k8s.io/apimachinery/pkg/api/resource" 29 metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 30 "k8s.io/apimachinery/pkg/util/sets" 31 "k8s.io/client-go/informers" 32 "k8s.io/client-go/kubernetes/fake" 33 "k8s.io/klog/v2/ktesting" 34 "k8s.io/kubernetes/pkg/scheduler/apis/config" 35 "k8s.io/kubernetes/pkg/scheduler/framework" 36 "k8s.io/kubernetes/pkg/scheduler/framework/plugins/feature" 37 "k8s.io/kubernetes/pkg/scheduler/framework/runtime" 38 39 "volcano.sh/volcano/cmd/scheduler/app/options" 40 ) 41 42 var ( 43 immediate = storagev1.VolumeBindingImmediate 44 waitForFirstConsumer = storagev1.VolumeBindingWaitForFirstConsumer 45 immediateSC = &storagev1.StorageClass{ 46 ObjectMeta: metav1.ObjectMeta{ 47 Name: "immediate-sc", 48 }, 49 VolumeBindingMode: &immediate, 50 } 51 waitSC = &storagev1.StorageClass{ 52 ObjectMeta: metav1.ObjectMeta{ 53 Name: "wait-sc", 54 }, 55 VolumeBindingMode: &waitForFirstConsumer, 56 } 57 waitHDDSC = &storagev1.StorageClass{ 58 ObjectMeta: metav1.ObjectMeta{ 59 Name: "wait-hdd-sc", 60 }, 61 VolumeBindingMode: &waitForFirstConsumer, 62 } 63 64 defaultShapePoint = []config.UtilizationShapePoint{ 65 { 66 Utilization: 0, 67 Score: 0, 68 }, 69 { 70 Utilization: 100, 71 Score: int32(config.MaxCustomPriorityScore), 72 }, 73 } 74 ) 75 76 func TestVolumeBinding(t *testing.T) { 77 table := []struct { 78 name string 79 pod *v1.Pod 80 nodes []*v1.Node 81 pvcs []*v1.PersistentVolumeClaim 82 pvs []*v1.PersistentVolume 83 fts feature.Features 84 args *config.VolumeBindingArgs 85 wantPreFilterResult *framework.PreFilterResult 86 wantPreFilterStatus *framework.Status 87 wantStateAfterPreFilter *stateData 88 wantFilterStatus []*framework.Status 89 wantScores []int64 90 }{ 91 { 92 name: "pod has not pvcs", 93 pod: makePod("pod-a").Pod, 94 nodes: []*v1.Node{ 95 makeNode("node-a").Node, 96 }, 97 wantPreFilterStatus: framework.NewStatus(framework.Skip), 98 wantFilterStatus: []*framework.Status{ 99 nil, 100 }, 101 wantScores: []int64{ 102 0, 103 }, 104 }, 105 { 106 name: "all bound", 107 pod: makePod("pod-a").withPVCVolume("pvc-a", "").Pod, 108 nodes: []*v1.Node{ 109 makeNode("node-a").Node, 110 }, 111 pvcs: []*v1.PersistentVolumeClaim{ 112 makePVC("pvc-a", waitSC.Name).withBoundPV("pv-a").PersistentVolumeClaim, 113 }, 114 pvs: []*v1.PersistentVolume{ 115 makePV("pv-a", waitSC.Name).withPhase(v1.VolumeAvailable).PersistentVolume, 116 }, 117 wantStateAfterPreFilter: &stateData{ 118 podVolumeClaims: &PodVolumeClaims{ 119 boundClaims: []*v1.PersistentVolumeClaim{ 120 makePVC("pvc-a", waitSC.Name).withBoundPV("pv-a").PersistentVolumeClaim, 121 }, 122 unboundClaimsDelayBinding: []*v1.PersistentVolumeClaim{}, 123 unboundVolumesDelayBinding: map[string][]*v1.PersistentVolume{}, 124 }, 125 podVolumesByNode: map[string]*PodVolumes{}, 126 }, 127 wantFilterStatus: []*framework.Status{ 128 nil, 129 }, 130 wantScores: []int64{ 131 0, 132 }, 133 }, 134 { 135 name: "all bound with local volumes", 136 pod: makePod("pod-a").withPVCVolume("pvc-a", "volume-a").withPVCVolume("pvc-b", "volume-b").Pod, 137 nodes: []*v1.Node{ 138 makeNode("node-a").Node, 139 }, 140 pvcs: []*v1.PersistentVolumeClaim{ 141 makePVC("pvc-a", waitSC.Name).withBoundPV("pv-a").PersistentVolumeClaim, 142 makePVC("pvc-b", waitSC.Name).withBoundPV("pv-b").PersistentVolumeClaim, 143 }, 144 pvs: []*v1.PersistentVolume{ 145 makePV("pv-a", waitSC.Name).withPhase(v1.VolumeBound).withNodeAffinity(map[string][]string{ 146 v1.LabelHostname: {"node-a"}, 147 }).PersistentVolume, 148 makePV("pv-b", waitSC.Name).withPhase(v1.VolumeBound).withNodeAffinity(map[string][]string{ 149 v1.LabelHostname: {"node-a"}, 150 }).PersistentVolume, 151 }, 152 wantPreFilterResult: &framework.PreFilterResult{ 153 NodeNames: sets.New("node-a"), 154 }, 155 wantStateAfterPreFilter: &stateData{ 156 podVolumeClaims: &PodVolumeClaims{ 157 boundClaims: []*v1.PersistentVolumeClaim{ 158 makePVC("pvc-a", waitSC.Name).withBoundPV("pv-a").PersistentVolumeClaim, 159 makePVC("pvc-b", waitSC.Name).withBoundPV("pv-b").PersistentVolumeClaim, 160 }, 161 unboundClaimsDelayBinding: []*v1.PersistentVolumeClaim{}, 162 unboundVolumesDelayBinding: map[string][]*v1.PersistentVolume{}, 163 }, 164 podVolumesByNode: map[string]*PodVolumes{}, 165 }, 166 wantFilterStatus: []*framework.Status{ 167 nil, 168 }, 169 wantScores: []int64{ 170 0, 171 }, 172 }, 173 { 174 name: "PVC does not exist", 175 pod: makePod("pod-a").withPVCVolume("pvc-a", "").Pod, 176 nodes: []*v1.Node{ 177 makeNode("node-a").Node, 178 }, 179 pvcs: []*v1.PersistentVolumeClaim{}, 180 wantPreFilterStatus: framework.NewStatus(framework.UnschedulableAndUnresolvable, `persistentvolumeclaim "pvc-a" not found`), 181 wantFilterStatus: []*framework.Status{ 182 nil, 183 }, 184 wantScores: []int64{ 185 0, 186 }, 187 }, 188 { 189 name: "Part of PVCs do not exist", 190 pod: makePod("pod-a").withPVCVolume("pvc-a", "").withPVCVolume("pvc-b", "").Pod, 191 nodes: []*v1.Node{ 192 makeNode("node-a").Node, 193 }, 194 pvcs: []*v1.PersistentVolumeClaim{ 195 makePVC("pvc-a", waitSC.Name).withBoundPV("pv-a").PersistentVolumeClaim, 196 }, 197 wantPreFilterStatus: framework.NewStatus(framework.UnschedulableAndUnresolvable, `persistentvolumeclaim "pvc-b" not found`), 198 wantFilterStatus: []*framework.Status{ 199 nil, 200 }, 201 wantScores: []int64{ 202 0, 203 }, 204 }, 205 { 206 name: "immediate claims not bound", 207 pod: makePod("pod-a").withPVCVolume("pvc-a", "").Pod, 208 nodes: []*v1.Node{ 209 makeNode("node-a").Node, 210 }, 211 pvcs: []*v1.PersistentVolumeClaim{ 212 makePVC("pvc-a", immediateSC.Name).PersistentVolumeClaim, 213 }, 214 wantPreFilterStatus: framework.NewStatus(framework.UnschedulableAndUnresolvable, "pod has unbound immediate PersistentVolumeClaims"), 215 wantFilterStatus: []*framework.Status{ 216 nil, 217 }, 218 wantScores: []int64{ 219 0, 220 }, 221 }, 222 { 223 name: "unbound claims no matches", 224 pod: makePod("pod-a").withPVCVolume("pvc-a", "").Pod, 225 nodes: []*v1.Node{ 226 makeNode("node-a").Node, 227 }, 228 pvcs: []*v1.PersistentVolumeClaim{ 229 makePVC("pvc-a", waitSC.Name).PersistentVolumeClaim, 230 }, 231 wantStateAfterPreFilter: &stateData{ 232 podVolumeClaims: &PodVolumeClaims{ 233 boundClaims: []*v1.PersistentVolumeClaim{}, 234 unboundClaimsDelayBinding: []*v1.PersistentVolumeClaim{ 235 makePVC("pvc-a", waitSC.Name).PersistentVolumeClaim, 236 }, 237 unboundVolumesDelayBinding: map[string][]*v1.PersistentVolume{waitSC.Name: {}}, 238 }, 239 podVolumesByNode: map[string]*PodVolumes{}, 240 }, 241 wantFilterStatus: []*framework.Status{ 242 framework.NewStatus(framework.UnschedulableAndUnresolvable, string(ErrReasonBindConflict)), 243 }, 244 wantScores: []int64{ 245 0, 246 }, 247 }, 248 { 249 name: "bound and unbound unsatisfied", 250 pod: makePod("pod-a").withPVCVolume("pvc-a", "").withPVCVolume("pvc-b", "").Pod, 251 nodes: []*v1.Node{ 252 makeNode("node-a").withLabel("foo", "barbar").Node, 253 }, 254 pvcs: []*v1.PersistentVolumeClaim{ 255 makePVC("pvc-a", waitSC.Name).withBoundPV("pv-a").PersistentVolumeClaim, 256 makePVC("pvc-b", waitSC.Name).PersistentVolumeClaim, 257 }, 258 pvs: []*v1.PersistentVolume{ 259 makePV("pv-a", waitSC.Name). 260 withPhase(v1.VolumeAvailable). 261 withNodeAffinity(map[string][]string{"foo": {"bar"}}).PersistentVolume, 262 }, 263 wantStateAfterPreFilter: &stateData{ 264 podVolumeClaims: &PodVolumeClaims{ 265 boundClaims: []*v1.PersistentVolumeClaim{ 266 makePVC("pvc-a", waitSC.Name).withBoundPV("pv-a").PersistentVolumeClaim, 267 }, 268 unboundClaimsDelayBinding: []*v1.PersistentVolumeClaim{ 269 makePVC("pvc-b", waitSC.Name).PersistentVolumeClaim, 270 }, 271 unboundVolumesDelayBinding: map[string][]*v1.PersistentVolume{ 272 waitSC.Name: { 273 makePV("pv-a", waitSC.Name). 274 withPhase(v1.VolumeAvailable). 275 withNodeAffinity(map[string][]string{"foo": {"bar"}}).PersistentVolume, 276 }, 277 }, 278 }, 279 podVolumesByNode: map[string]*PodVolumes{}, 280 }, 281 wantFilterStatus: []*framework.Status{ 282 framework.NewStatus(framework.UnschedulableAndUnresolvable, string(ErrReasonNodeConflict), string(ErrReasonBindConflict)), 283 }, 284 wantScores: []int64{ 285 0, 286 }, 287 }, 288 { 289 name: "pvc not found", 290 pod: makePod("pod-a").withPVCVolume("pvc-a", "").Pod, 291 nodes: []*v1.Node{ 292 makeNode("node-a").Node, 293 }, 294 wantPreFilterStatus: framework.NewStatus(framework.UnschedulableAndUnresolvable, `persistentvolumeclaim "pvc-a" not found`), 295 wantFilterStatus: []*framework.Status{ 296 nil, 297 }, 298 wantScores: []int64{ 299 0, 300 }, 301 }, 302 { 303 name: "pv not found", 304 pod: makePod("pod-a").withPVCVolume("pvc-a", "").Pod, 305 nodes: []*v1.Node{ 306 makeNode("node-a").Node, 307 }, 308 pvcs: []*v1.PersistentVolumeClaim{ 309 makePVC("pvc-a", waitSC.Name).withBoundPV("pv-a").PersistentVolumeClaim, 310 }, 311 wantPreFilterStatus: nil, 312 wantStateAfterPreFilter: &stateData{ 313 podVolumeClaims: &PodVolumeClaims{ 314 boundClaims: []*v1.PersistentVolumeClaim{ 315 makePVC("pvc-a", waitSC.Name).withBoundPV("pv-a").PersistentVolumeClaim, 316 }, 317 unboundClaimsDelayBinding: []*v1.PersistentVolumeClaim{}, 318 unboundVolumesDelayBinding: map[string][]*v1.PersistentVolume{}, 319 }, 320 podVolumesByNode: map[string]*PodVolumes{}, 321 }, 322 wantFilterStatus: []*framework.Status{ 323 framework.NewStatus(framework.UnschedulableAndUnresolvable, `node(s) unavailable due to one or more pvc(s) bound to non-existent pv(s)`), 324 }, 325 wantScores: []int64{ 326 0, 327 }, 328 }, 329 { 330 name: "pv not found claim lost", 331 pod: makePod("pod-a").withPVCVolume("pvc-a", "").Pod, 332 nodes: []*v1.Node{ 333 makeNode("node-a").Node, 334 }, 335 pvcs: []*v1.PersistentVolumeClaim{ 336 makePVC("pvc-a", waitSC.Name).withBoundPV("pv-a").withPhase(v1.ClaimLost).PersistentVolumeClaim, 337 }, 338 wantPreFilterStatus: framework.NewStatus(framework.UnschedulableAndUnresolvable, `persistentvolumeclaim "pvc-a" bound to non-existent persistentvolume "pv-a"`), 339 wantFilterStatus: []*framework.Status{ 340 nil, 341 }, 342 wantScores: []int64{ 343 0, 344 }, 345 }, 346 { 347 name: "local volumes with close capacity are preferred", 348 pod: makePod("pod-a").withPVCVolume("pvc-a", "").Pod, 349 nodes: []*v1.Node{ 350 makeNode("node-a").Node, 351 makeNode("node-b").Node, 352 makeNode("node-c").Node, 353 }, 354 pvcs: []*v1.PersistentVolumeClaim{ 355 makePVC("pvc-a", waitSC.Name).withRequestStorage(resource.MustParse("50Gi")).PersistentVolumeClaim, 356 }, 357 pvs: []*v1.PersistentVolume{ 358 makePV("pv-a-0", waitSC.Name). 359 withPhase(v1.VolumeAvailable). 360 withCapacity(resource.MustParse("200Gi")). 361 withNodeAffinity(map[string][]string{v1.LabelHostname: {"node-a"}}).PersistentVolume, 362 makePV("pv-a-1", waitSC.Name). 363 withPhase(v1.VolumeAvailable). 364 withCapacity(resource.MustParse("200Gi")). 365 withNodeAffinity(map[string][]string{v1.LabelHostname: {"node-a"}}).PersistentVolume, 366 makePV("pv-b-0", waitSC.Name). 367 withPhase(v1.VolumeAvailable). 368 withCapacity(resource.MustParse("100Gi")). 369 withNodeAffinity(map[string][]string{v1.LabelHostname: {"node-b"}}).PersistentVolume, 370 makePV("pv-b-1", waitSC.Name). 371 withPhase(v1.VolumeAvailable). 372 withCapacity(resource.MustParse("100Gi")). 373 withNodeAffinity(map[string][]string{v1.LabelHostname: {"node-b"}}).PersistentVolume, 374 }, 375 fts: feature.Features{ 376 EnableVolumeCapacityPriority: true, 377 }, 378 wantPreFilterStatus: nil, 379 wantStateAfterPreFilter: &stateData{ 380 podVolumeClaims: &PodVolumeClaims{ 381 boundClaims: []*v1.PersistentVolumeClaim{}, 382 unboundClaimsDelayBinding: []*v1.PersistentVolumeClaim{ 383 makePVC("pvc-a", waitSC.Name).withRequestStorage(resource.MustParse("50Gi")).PersistentVolumeClaim, 384 }, 385 unboundVolumesDelayBinding: map[string][]*v1.PersistentVolume{ 386 waitSC.Name: { 387 makePV("pv-a-0", waitSC.Name). 388 withPhase(v1.VolumeAvailable). 389 withCapacity(resource.MustParse("200Gi")). 390 withNodeAffinity(map[string][]string{v1.LabelHostname: {"node-a"}}).PersistentVolume, 391 makePV("pv-a-1", waitSC.Name). 392 withPhase(v1.VolumeAvailable). 393 withCapacity(resource.MustParse("200Gi")). 394 withNodeAffinity(map[string][]string{v1.LabelHostname: {"node-a"}}).PersistentVolume, 395 makePV("pv-b-0", waitSC.Name). 396 withPhase(v1.VolumeAvailable). 397 withCapacity(resource.MustParse("100Gi")). 398 withNodeAffinity(map[string][]string{v1.LabelHostname: {"node-b"}}).PersistentVolume, 399 makePV("pv-b-1", waitSC.Name). 400 withPhase(v1.VolumeAvailable). 401 withCapacity(resource.MustParse("100Gi")). 402 withNodeAffinity(map[string][]string{v1.LabelHostname: {"node-b"}}).PersistentVolume, 403 }, 404 }, 405 }, 406 podVolumesByNode: map[string]*PodVolumes{}, 407 }, 408 wantFilterStatus: []*framework.Status{ 409 nil, 410 nil, 411 framework.NewStatus(framework.UnschedulableAndUnresolvable, `node(s) didn't find available persistent volumes to bind`), 412 }, 413 wantScores: []int64{ 414 25, 415 50, 416 0, 417 }, 418 }, 419 { 420 name: "local volumes with close capacity are preferred (multiple pvcs)", 421 pod: makePod("pod-a").withPVCVolume("pvc-0", "").withPVCVolume("pvc-1", "").Pod, 422 nodes: []*v1.Node{ 423 makeNode("node-a").Node, 424 makeNode("node-b").Node, 425 makeNode("node-c").Node, 426 }, 427 pvcs: []*v1.PersistentVolumeClaim{ 428 makePVC("pvc-0", waitSC.Name).withRequestStorage(resource.MustParse("50Gi")).PersistentVolumeClaim, 429 makePVC("pvc-1", waitHDDSC.Name).withRequestStorage(resource.MustParse("100Gi")).PersistentVolumeClaim, 430 }, 431 pvs: []*v1.PersistentVolume{ 432 makePV("pv-a-0", waitSC.Name). 433 withPhase(v1.VolumeAvailable). 434 withCapacity(resource.MustParse("200Gi")). 435 withNodeAffinity(map[string][]string{v1.LabelHostname: {"node-a"}}).PersistentVolume, 436 makePV("pv-a-1", waitSC.Name). 437 withPhase(v1.VolumeAvailable). 438 withCapacity(resource.MustParse("200Gi")). 439 withNodeAffinity(map[string][]string{v1.LabelHostname: {"node-a"}}).PersistentVolume, 440 makePV("pv-a-2", waitHDDSC.Name). 441 withPhase(v1.VolumeAvailable). 442 withCapacity(resource.MustParse("200Gi")). 443 withNodeAffinity(map[string][]string{v1.LabelHostname: {"node-a"}}).PersistentVolume, 444 makePV("pv-a-3", waitHDDSC.Name). 445 withPhase(v1.VolumeAvailable). 446 withCapacity(resource.MustParse("200Gi")). 447 withNodeAffinity(map[string][]string{v1.LabelHostname: {"node-a"}}).PersistentVolume, 448 makePV("pv-b-0", waitSC.Name). 449 withPhase(v1.VolumeAvailable). 450 withCapacity(resource.MustParse("100Gi")). 451 withNodeAffinity(map[string][]string{v1.LabelHostname: {"node-b"}}).PersistentVolume, 452 makePV("pv-b-1", waitSC.Name). 453 withPhase(v1.VolumeAvailable). 454 withCapacity(resource.MustParse("100Gi")). 455 withNodeAffinity(map[string][]string{v1.LabelHostname: {"node-b"}}).PersistentVolume, 456 makePV("pv-b-2", waitHDDSC.Name). 457 withPhase(v1.VolumeAvailable). 458 withCapacity(resource.MustParse("100Gi")). 459 withNodeAffinity(map[string][]string{v1.LabelHostname: {"node-b"}}).PersistentVolume, 460 makePV("pv-b-3", waitHDDSC.Name). 461 withPhase(v1.VolumeAvailable). 462 withCapacity(resource.MustParse("100Gi")). 463 withNodeAffinity(map[string][]string{v1.LabelHostname: {"node-b"}}).PersistentVolume, 464 }, 465 fts: feature.Features{ 466 EnableVolumeCapacityPriority: true, 467 }, 468 wantPreFilterStatus: nil, 469 wantStateAfterPreFilter: &stateData{ 470 podVolumeClaims: &PodVolumeClaims{ 471 boundClaims: []*v1.PersistentVolumeClaim{}, 472 unboundClaimsDelayBinding: []*v1.PersistentVolumeClaim{ 473 makePVC("pvc-0", waitSC.Name).withRequestStorage(resource.MustParse("50Gi")).PersistentVolumeClaim, 474 makePVC("pvc-1", waitHDDSC.Name).withRequestStorage(resource.MustParse("100Gi")).PersistentVolumeClaim, 475 }, 476 unboundVolumesDelayBinding: map[string][]*v1.PersistentVolume{ 477 waitHDDSC.Name: { 478 makePV("pv-a-2", waitHDDSC.Name). 479 withPhase(v1.VolumeAvailable). 480 withCapacity(resource.MustParse("200Gi")). 481 withNodeAffinity(map[string][]string{v1.LabelHostname: {"node-a"}}).PersistentVolume, 482 makePV("pv-a-3", waitHDDSC.Name). 483 withPhase(v1.VolumeAvailable). 484 withCapacity(resource.MustParse("200Gi")). 485 withNodeAffinity(map[string][]string{v1.LabelHostname: {"node-a"}}).PersistentVolume, 486 makePV("pv-b-2", waitHDDSC.Name). 487 withPhase(v1.VolumeAvailable). 488 withCapacity(resource.MustParse("100Gi")). 489 withNodeAffinity(map[string][]string{v1.LabelHostname: {"node-b"}}).PersistentVolume, 490 makePV("pv-b-3", waitHDDSC.Name). 491 withPhase(v1.VolumeAvailable). 492 withCapacity(resource.MustParse("100Gi")). 493 withNodeAffinity(map[string][]string{v1.LabelHostname: {"node-b"}}).PersistentVolume, 494 }, 495 waitSC.Name: { 496 makePV("pv-a-0", waitSC.Name). 497 withPhase(v1.VolumeAvailable). 498 withCapacity(resource.MustParse("200Gi")). 499 withNodeAffinity(map[string][]string{v1.LabelHostname: {"node-a"}}).PersistentVolume, 500 makePV("pv-a-1", waitSC.Name). 501 withPhase(v1.VolumeAvailable). 502 withCapacity(resource.MustParse("200Gi")). 503 withNodeAffinity(map[string][]string{v1.LabelHostname: {"node-a"}}).PersistentVolume, 504 makePV("pv-b-0", waitSC.Name). 505 withPhase(v1.VolumeAvailable). 506 withCapacity(resource.MustParse("100Gi")). 507 withNodeAffinity(map[string][]string{v1.LabelHostname: {"node-b"}}).PersistentVolume, 508 makePV("pv-b-1", waitSC.Name). 509 withPhase(v1.VolumeAvailable). 510 withCapacity(resource.MustParse("100Gi")). 511 withNodeAffinity(map[string][]string{v1.LabelHostname: {"node-b"}}).PersistentVolume, 512 }, 513 }, 514 }, 515 podVolumesByNode: map[string]*PodVolumes{}, 516 }, 517 wantFilterStatus: []*framework.Status{ 518 nil, 519 nil, 520 framework.NewStatus(framework.UnschedulableAndUnresolvable, `node(s) didn't find available persistent volumes to bind`), 521 }, 522 wantScores: []int64{ 523 38, 524 75, 525 0, 526 }, 527 }, 528 { 529 name: "zonal volumes with close capacity are preferred", 530 pod: makePod("pod-a").withPVCVolume("pvc-a", "").Pod, 531 nodes: []*v1.Node{ 532 makeNode("zone-a-node-a"). 533 withLabel("topology.kubernetes.io/region", "region-a"). 534 withLabel("topology.kubernetes.io/zone", "zone-a").Node, 535 makeNode("zone-a-node-b"). 536 withLabel("topology.kubernetes.io/region", "region-a"). 537 withLabel("topology.kubernetes.io/zone", "zone-a").Node, 538 makeNode("zone-b-node-a"). 539 withLabel("topology.kubernetes.io/region", "region-b"). 540 withLabel("topology.kubernetes.io/zone", "zone-b").Node, 541 makeNode("zone-b-node-b"). 542 withLabel("topology.kubernetes.io/region", "region-b"). 543 withLabel("topology.kubernetes.io/zone", "zone-b").Node, 544 makeNode("zone-c-node-a"). 545 withLabel("topology.kubernetes.io/region", "region-c"). 546 withLabel("topology.kubernetes.io/zone", "zone-c").Node, 547 makeNode("zone-c-node-b"). 548 withLabel("topology.kubernetes.io/region", "region-c"). 549 withLabel("topology.kubernetes.io/zone", "zone-c").Node, 550 }, 551 pvcs: []*v1.PersistentVolumeClaim{ 552 makePVC("pvc-a", waitSC.Name).withRequestStorage(resource.MustParse("50Gi")).PersistentVolumeClaim, 553 }, 554 pvs: []*v1.PersistentVolume{ 555 makePV("pv-a-0", waitSC.Name). 556 withPhase(v1.VolumeAvailable). 557 withCapacity(resource.MustParse("200Gi")). 558 withNodeAffinity(map[string][]string{ 559 "topology.kubernetes.io/region": {"region-a"}, 560 "topology.kubernetes.io/zone": {"zone-a"}, 561 }).PersistentVolume, 562 makePV("pv-a-1", waitSC.Name). 563 withPhase(v1.VolumeAvailable). 564 withCapacity(resource.MustParse("200Gi")). 565 withNodeAffinity(map[string][]string{ 566 "topology.kubernetes.io/region": {"region-a"}, 567 "topology.kubernetes.io/zone": {"zone-a"}, 568 }).PersistentVolume, 569 makePV("pv-b-0", waitSC.Name). 570 withPhase(v1.VolumeAvailable). 571 withCapacity(resource.MustParse("100Gi")). 572 withNodeAffinity(map[string][]string{ 573 "topology.kubernetes.io/region": {"region-b"}, 574 "topology.kubernetes.io/zone": {"zone-b"}, 575 }).PersistentVolume, 576 makePV("pv-b-1", waitSC.Name). 577 withPhase(v1.VolumeAvailable). 578 withCapacity(resource.MustParse("100Gi")). 579 withNodeAffinity(map[string][]string{ 580 "topology.kubernetes.io/region": {"region-b"}, 581 "topology.kubernetes.io/zone": {"zone-b"}, 582 }).PersistentVolume, 583 }, 584 fts: feature.Features{ 585 EnableVolumeCapacityPriority: true, 586 }, 587 wantPreFilterStatus: nil, 588 wantStateAfterPreFilter: &stateData{ 589 podVolumeClaims: &PodVolumeClaims{ 590 boundClaims: []*v1.PersistentVolumeClaim{}, 591 unboundClaimsDelayBinding: []*v1.PersistentVolumeClaim{ 592 makePVC("pvc-a", waitSC.Name).withRequestStorage(resource.MustParse("50Gi")).PersistentVolumeClaim, 593 }, 594 unboundVolumesDelayBinding: map[string][]*v1.PersistentVolume{ 595 waitSC.Name: { 596 makePV("pv-a-0", waitSC.Name). 597 withPhase(v1.VolumeAvailable). 598 withCapacity(resource.MustParse("200Gi")). 599 withNodeAffinity(map[string][]string{ 600 "topology.kubernetes.io/region": {"region-a"}, 601 "topology.kubernetes.io/zone": {"zone-a"}, 602 }).PersistentVolume, 603 makePV("pv-a-1", waitSC.Name). 604 withPhase(v1.VolumeAvailable). 605 withCapacity(resource.MustParse("200Gi")). 606 withNodeAffinity(map[string][]string{ 607 "topology.kubernetes.io/region": {"region-a"}, 608 "topology.kubernetes.io/zone": {"zone-a"}, 609 }).PersistentVolume, 610 makePV("pv-b-0", waitSC.Name). 611 withPhase(v1.VolumeAvailable). 612 withCapacity(resource.MustParse("100Gi")). 613 withNodeAffinity(map[string][]string{ 614 "topology.kubernetes.io/region": {"region-b"}, 615 "topology.kubernetes.io/zone": {"zone-b"}, 616 }).PersistentVolume, 617 makePV("pv-b-1", waitSC.Name). 618 withPhase(v1.VolumeAvailable). 619 withCapacity(resource.MustParse("100Gi")). 620 withNodeAffinity(map[string][]string{ 621 "topology.kubernetes.io/region": {"region-b"}, 622 "topology.kubernetes.io/zone": {"zone-b"}, 623 }).PersistentVolume, 624 }, 625 }, 626 }, 627 podVolumesByNode: map[string]*PodVolumes{}, 628 }, 629 wantFilterStatus: []*framework.Status{ 630 nil, 631 nil, 632 nil, 633 nil, 634 framework.NewStatus(framework.UnschedulableAndUnresolvable, `node(s) didn't find available persistent volumes to bind`), 635 framework.NewStatus(framework.UnschedulableAndUnresolvable, `node(s) didn't find available persistent volumes to bind`), 636 }, 637 wantScores: []int64{ 638 25, 639 25, 640 50, 641 50, 642 0, 643 0, 644 }, 645 }, 646 { 647 name: "zonal volumes with close capacity are preferred (custom shape)", 648 pod: makePod("pod-a").withPVCVolume("pvc-a", "").Pod, 649 nodes: []*v1.Node{ 650 makeNode("zone-a-node-a"). 651 withLabel("topology.kubernetes.io/region", "region-a"). 652 withLabel("topology.kubernetes.io/zone", "zone-a").Node, 653 makeNode("zone-a-node-b"). 654 withLabel("topology.kubernetes.io/region", "region-a"). 655 withLabel("topology.kubernetes.io/zone", "zone-a").Node, 656 makeNode("zone-b-node-a"). 657 withLabel("topology.kubernetes.io/region", "region-b"). 658 withLabel("topology.kubernetes.io/zone", "zone-b").Node, 659 makeNode("zone-b-node-b"). 660 withLabel("topology.kubernetes.io/region", "region-b"). 661 withLabel("topology.kubernetes.io/zone", "zone-b").Node, 662 makeNode("zone-c-node-a"). 663 withLabel("topology.kubernetes.io/region", "region-c"). 664 withLabel("topology.kubernetes.io/zone", "zone-c").Node, 665 makeNode("zone-c-node-b"). 666 withLabel("topology.kubernetes.io/region", "region-c"). 667 withLabel("topology.kubernetes.io/zone", "zone-c").Node, 668 }, 669 pvcs: []*v1.PersistentVolumeClaim{ 670 makePVC("pvc-a", waitSC.Name).withRequestStorage(resource.MustParse("50Gi")).PersistentVolumeClaim, 671 }, 672 pvs: []*v1.PersistentVolume{ 673 makePV("pv-a-0", waitSC.Name). 674 withPhase(v1.VolumeAvailable). 675 withCapacity(resource.MustParse("200Gi")). 676 withNodeAffinity(map[string][]string{ 677 "topology.kubernetes.io/region": {"region-a"}, 678 "topology.kubernetes.io/zone": {"zone-a"}, 679 }).PersistentVolume, 680 makePV("pv-a-1", waitSC.Name). 681 withPhase(v1.VolumeAvailable). 682 withCapacity(resource.MustParse("200Gi")). 683 withNodeAffinity(map[string][]string{ 684 "topology.kubernetes.io/region": {"region-a"}, 685 "topology.kubernetes.io/zone": {"zone-a"}, 686 }).PersistentVolume, 687 makePV("pv-b-0", waitSC.Name). 688 withPhase(v1.VolumeAvailable). 689 withCapacity(resource.MustParse("100Gi")). 690 withNodeAffinity(map[string][]string{ 691 "topology.kubernetes.io/region": {"region-b"}, 692 "topology.kubernetes.io/zone": {"zone-b"}, 693 }).PersistentVolume, 694 makePV("pv-b-1", waitSC.Name). 695 withPhase(v1.VolumeAvailable). 696 withCapacity(resource.MustParse("100Gi")). 697 withNodeAffinity(map[string][]string{ 698 "topology.kubernetes.io/region": {"region-b"}, 699 "topology.kubernetes.io/zone": {"zone-b"}, 700 }).PersistentVolume, 701 }, 702 fts: feature.Features{ 703 EnableVolumeCapacityPriority: true, 704 }, 705 args: &config.VolumeBindingArgs{ 706 BindTimeoutSeconds: 300, 707 Shape: []config.UtilizationShapePoint{ 708 { 709 Utilization: 0, 710 Score: 0, 711 }, 712 { 713 Utilization: 50, 714 Score: 3, 715 }, 716 { 717 Utilization: 100, 718 Score: 5, 719 }, 720 }, 721 }, 722 wantPreFilterStatus: nil, 723 wantStateAfterPreFilter: &stateData{ 724 podVolumeClaims: &PodVolumeClaims{ 725 boundClaims: []*v1.PersistentVolumeClaim{}, 726 unboundClaimsDelayBinding: []*v1.PersistentVolumeClaim{ 727 makePVC("pvc-a", waitSC.Name).withRequestStorage(resource.MustParse("50Gi")).PersistentVolumeClaim, 728 }, 729 unboundClaimsImmediate: nil, 730 unboundVolumesDelayBinding: map[string][]*v1.PersistentVolume{ 731 waitSC.Name: { 732 makePV("pv-a-0", waitSC.Name). 733 withPhase(v1.VolumeAvailable). 734 withCapacity(resource.MustParse("200Gi")). 735 withNodeAffinity(map[string][]string{ 736 "topology.kubernetes.io/region": {"region-a"}, 737 "topology.kubernetes.io/zone": {"zone-a"}, 738 }).PersistentVolume, 739 makePV("pv-a-1", waitSC.Name). 740 withPhase(v1.VolumeAvailable). 741 withCapacity(resource.MustParse("200Gi")). 742 withNodeAffinity(map[string][]string{ 743 "topology.kubernetes.io/region": {"region-a"}, 744 "topology.kubernetes.io/zone": {"zone-a"}, 745 }).PersistentVolume, 746 makePV("pv-b-0", waitSC.Name). 747 withPhase(v1.VolumeAvailable). 748 withCapacity(resource.MustParse("100Gi")). 749 withNodeAffinity(map[string][]string{ 750 "topology.kubernetes.io/region": {"region-b"}, 751 "topology.kubernetes.io/zone": {"zone-b"}, 752 }).PersistentVolume, 753 makePV("pv-b-1", waitSC.Name). 754 withPhase(v1.VolumeAvailable). 755 withCapacity(resource.MustParse("100Gi")). 756 withNodeAffinity(map[string][]string{ 757 "topology.kubernetes.io/region": {"region-b"}, 758 "topology.kubernetes.io/zone": {"zone-b"}, 759 }).PersistentVolume, 760 }, 761 }, 762 }, 763 podVolumesByNode: map[string]*PodVolumes{}, 764 }, 765 wantFilterStatus: []*framework.Status{ 766 nil, 767 nil, 768 nil, 769 nil, 770 framework.NewStatus(framework.UnschedulableAndUnresolvable, `node(s) didn't find available persistent volumes to bind`), 771 framework.NewStatus(framework.UnschedulableAndUnresolvable, `node(s) didn't find available persistent volumes to bind`), 772 }, 773 wantScores: []int64{ 774 15, 775 15, 776 30, 777 30, 778 0, 779 0, 780 }, 781 }, 782 } 783 784 options.ServerOpts = &options.ServerOption{ 785 EnableCSIStorage: true, 786 } 787 for _, item := range table { 788 t.Run(item.name, func(t *testing.T) { 789 _, ctx := ktesting.NewTestContext(t) 790 ctx, cancel := context.WithCancel(ctx) 791 defer cancel() 792 client := fake.NewSimpleClientset() 793 informerFactory := informers.NewSharedInformerFactory(client, 0) 794 opts := []runtime.Option{ 795 runtime.WithClientSet(client), 796 runtime.WithInformerFactory(informerFactory), 797 } 798 fh, err := runtime.NewFramework(ctx, nil, nil, opts...) 799 if err != nil { 800 t.Fatal(err) 801 } 802 803 args := item.args 804 if args == nil { 805 // default args if the args is not specified in test cases 806 args = &config.VolumeBindingArgs{ 807 BindTimeoutSeconds: 300, 808 } 809 if item.fts.EnableVolumeCapacityPriority { 810 args.Shape = defaultShapePoint 811 } 812 } 813 814 pl, err := New(ctx, args, fh, item.fts) 815 if err != nil { 816 t.Fatal(err) 817 } 818 819 t.Log("Feed testing data and wait for them to be synced") 820 client.StorageV1().StorageClasses().Create(ctx, immediateSC, metav1.CreateOptions{}) 821 client.StorageV1().StorageClasses().Create(ctx, waitSC, metav1.CreateOptions{}) 822 client.StorageV1().StorageClasses().Create(ctx, waitHDDSC, metav1.CreateOptions{}) 823 for _, node := range item.nodes { 824 client.CoreV1().Nodes().Create(ctx, node, metav1.CreateOptions{}) 825 } 826 for _, pvc := range item.pvcs { 827 client.CoreV1().PersistentVolumeClaims(pvc.Namespace).Create(ctx, pvc, metav1.CreateOptions{}) 828 } 829 for _, pv := range item.pvs { 830 client.CoreV1().PersistentVolumes().Create(ctx, pv, metav1.CreateOptions{}) 831 } 832 833 t.Log("Start informer factory after initialization") 834 informerFactory.Start(ctx.Done()) 835 836 t.Log("Wait for all started informers' cache were synced") 837 informerFactory.WaitForCacheSync(ctx.Done()) 838 839 t.Log("Verify") 840 841 p := pl.(*VolumeBinding) 842 nodeInfos := make([]*framework.NodeInfo, 0) 843 for _, node := range item.nodes { 844 nodeInfo := framework.NewNodeInfo() 845 nodeInfo.SetNode(node) 846 nodeInfos = append(nodeInfos, nodeInfo) 847 } 848 state := framework.NewCycleState() 849 850 t.Logf("Verify: call PreFilter and check status") 851 gotPreFilterResult, gotPreFilterStatus := p.PreFilter(ctx, state, item.pod) 852 assert.Equal(t, item.wantPreFilterStatus, gotPreFilterStatus) 853 assert.Equal(t, item.wantPreFilterResult, gotPreFilterResult) 854 855 if !gotPreFilterStatus.IsSuccess() { 856 // scheduler framework will skip Filter if PreFilter fails 857 return 858 } 859 860 t.Logf("Verify: check state after prefilter phase") 861 got, err := getStateData(state) 862 if err != nil { 863 t.Fatal(err) 864 } 865 stateCmpOpts := []cmp.Option{ 866 cmp.AllowUnexported(stateData{}), 867 cmp.AllowUnexported(PodVolumeClaims{}), 868 cmpopts.IgnoreFields(stateData{}, "Mutex"), 869 cmpopts.SortSlices(func(a *v1.PersistentVolume, b *v1.PersistentVolume) bool { 870 return a.Name < b.Name 871 }), 872 cmpopts.SortSlices(func(a v1.NodeSelectorRequirement, b v1.NodeSelectorRequirement) bool { 873 return a.Key < b.Key 874 }), 875 } 876 if diff := cmp.Diff(item.wantStateAfterPreFilter, got, stateCmpOpts...); diff != "" { 877 t.Errorf("state got after prefilter does not match (-want,+got):\n%s", diff) 878 } 879 880 t.Logf("Verify: call Filter and check status") 881 for i, nodeInfo := range nodeInfos { 882 gotStatus := p.Filter(ctx, state, item.pod, nodeInfo) 883 assert.Equal(t, item.wantFilterStatus[i], gotStatus) 884 } 885 886 t.Logf("Verify: Score") 887 for i, node := range item.nodes { 888 score, status := p.Score(ctx, state, item.pod, node.Name) 889 if !status.IsSuccess() { 890 t.Errorf("Score expects success status, got: %v", status) 891 } 892 if score != item.wantScores[i] { 893 t.Errorf("Score expects score %d for node %q, got: %d", item.wantScores[i], node.Name, score) 894 } 895 } 896 }) 897 } 898 }