k8s.io/kubernetes@v1.29.3/pkg/scheduler/apis/config/validation/validation_pluginargs_test.go (about) 1 /* 2 Copyright 2020 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 validation 18 19 import ( 20 "fmt" 21 "strings" 22 "testing" 23 24 "github.com/google/go-cmp/cmp" 25 "github.com/google/go-cmp/cmp/cmpopts" 26 v1 "k8s.io/api/core/v1" 27 metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 28 "k8s.io/apimachinery/pkg/util/errors" 29 "k8s.io/apimachinery/pkg/util/validation/field" 30 "k8s.io/apiserver/pkg/util/feature" 31 "k8s.io/component-base/featuregate" 32 featuregatetesting "k8s.io/component-base/featuregate/testing" 33 "k8s.io/kubernetes/pkg/features" 34 "k8s.io/kubernetes/pkg/scheduler/apis/config" 35 ) 36 37 var ( 38 ignoreBadValueDetail = cmpopts.IgnoreFields(field.Error{}, "BadValue", "Detail") 39 ) 40 41 func TestValidateDefaultPreemptionArgs(t *testing.T) { 42 cases := map[string]struct { 43 args config.DefaultPreemptionArgs 44 wantErrs field.ErrorList 45 }{ 46 "valid args (default)": { 47 args: config.DefaultPreemptionArgs{ 48 MinCandidateNodesPercentage: 10, 49 MinCandidateNodesAbsolute: 100, 50 }, 51 }, 52 "negative minCandidateNodesPercentage": { 53 args: config.DefaultPreemptionArgs{ 54 MinCandidateNodesPercentage: -1, 55 MinCandidateNodesAbsolute: 100, 56 }, 57 wantErrs: field.ErrorList{ 58 &field.Error{ 59 Type: field.ErrorTypeInvalid, 60 Field: "minCandidateNodesPercentage", 61 }, 62 }, 63 }, 64 "minCandidateNodesPercentage over 100": { 65 args: config.DefaultPreemptionArgs{ 66 MinCandidateNodesPercentage: 900, 67 MinCandidateNodesAbsolute: 100, 68 }, 69 wantErrs: field.ErrorList{ 70 &field.Error{ 71 Type: field.ErrorTypeInvalid, 72 Field: "minCandidateNodesPercentage", 73 }, 74 }, 75 }, 76 "negative minCandidateNodesAbsolute": { 77 args: config.DefaultPreemptionArgs{ 78 MinCandidateNodesPercentage: 20, 79 MinCandidateNodesAbsolute: -1, 80 }, 81 wantErrs: field.ErrorList{ 82 &field.Error{ 83 Type: field.ErrorTypeInvalid, 84 Field: "minCandidateNodesAbsolute", 85 }, 86 }, 87 }, 88 "all zero": { 89 args: config.DefaultPreemptionArgs{ 90 MinCandidateNodesPercentage: 0, 91 MinCandidateNodesAbsolute: 0, 92 }, 93 wantErrs: field.ErrorList{ 94 &field.Error{ 95 Type: field.ErrorTypeInvalid, 96 Field: "minCandidateNodesPercentage", 97 }, &field.Error{ 98 Type: field.ErrorTypeInvalid, 99 Field: "minCandidateNodesAbsolute", 100 }, 101 }, 102 }, 103 "both negative": { 104 args: config.DefaultPreemptionArgs{ 105 MinCandidateNodesPercentage: -1, 106 MinCandidateNodesAbsolute: -1, 107 }, 108 wantErrs: field.ErrorList{ 109 &field.Error{ 110 Type: field.ErrorTypeInvalid, 111 Field: "minCandidateNodesPercentage", 112 }, &field.Error{ 113 Type: field.ErrorTypeInvalid, 114 Field: "minCandidateNodesAbsolute", 115 }, 116 }, 117 }, 118 } 119 120 for name, tc := range cases { 121 t.Run(name, func(t *testing.T) { 122 err := ValidateDefaultPreemptionArgs(nil, &tc.args) 123 if diff := cmp.Diff(tc.wantErrs.ToAggregate(), err, ignoreBadValueDetail); diff != "" { 124 t.Errorf("ValidateDefaultPreemptionArgs returned err (-want,+got):\n%s", diff) 125 } 126 }) 127 } 128 } 129 130 func TestValidateInterPodAffinityArgs(t *testing.T) { 131 cases := map[string]struct { 132 args config.InterPodAffinityArgs 133 wantErr error 134 }{ 135 "valid args": { 136 args: config.InterPodAffinityArgs{ 137 HardPodAffinityWeight: 10, 138 }, 139 }, 140 "hardPodAffinityWeight less than min": { 141 args: config.InterPodAffinityArgs{ 142 HardPodAffinityWeight: -1, 143 }, 144 wantErr: &field.Error{ 145 Type: field.ErrorTypeInvalid, 146 Field: "hardPodAffinityWeight", 147 }, 148 }, 149 "hardPodAffinityWeight more than max": { 150 args: config.InterPodAffinityArgs{ 151 HardPodAffinityWeight: 101, 152 }, 153 wantErr: &field.Error{ 154 Type: field.ErrorTypeInvalid, 155 Field: "hardPodAffinityWeight", 156 }, 157 }, 158 } 159 160 for name, tc := range cases { 161 t.Run(name, func(t *testing.T) { 162 err := ValidateInterPodAffinityArgs(nil, &tc.args) 163 if diff := cmp.Diff(tc.wantErr, err, ignoreBadValueDetail); diff != "" { 164 t.Errorf("ValidateInterPodAffinityArgs returned err (-want,+got):\n%s", diff) 165 } 166 }) 167 } 168 } 169 170 func TestValidatePodTopologySpreadArgs(t *testing.T) { 171 cases := map[string]struct { 172 args *config.PodTopologySpreadArgs 173 wantErrs field.ErrorList 174 }{ 175 "valid config": { 176 args: &config.PodTopologySpreadArgs{ 177 DefaultConstraints: []v1.TopologySpreadConstraint{ 178 { 179 MaxSkew: 1, 180 TopologyKey: "node", 181 WhenUnsatisfiable: v1.DoNotSchedule, 182 }, 183 { 184 MaxSkew: 2, 185 TopologyKey: "zone", 186 WhenUnsatisfiable: v1.ScheduleAnyway, 187 }, 188 }, 189 DefaultingType: config.ListDefaulting, 190 }, 191 }, 192 "maxSkew less than zero": { 193 args: &config.PodTopologySpreadArgs{ 194 DefaultConstraints: []v1.TopologySpreadConstraint{ 195 { 196 MaxSkew: -1, 197 TopologyKey: "node", 198 WhenUnsatisfiable: v1.DoNotSchedule, 199 }, 200 }, 201 DefaultingType: config.ListDefaulting, 202 }, 203 wantErrs: field.ErrorList{ 204 &field.Error{ 205 Type: field.ErrorTypeInvalid, 206 Field: "defaultConstraints[0].maxSkew", 207 }, 208 }, 209 }, 210 "empty topology key": { 211 args: &config.PodTopologySpreadArgs{ 212 DefaultConstraints: []v1.TopologySpreadConstraint{ 213 { 214 MaxSkew: 1, 215 TopologyKey: "", 216 WhenUnsatisfiable: v1.DoNotSchedule, 217 }, 218 }, 219 DefaultingType: config.ListDefaulting, 220 }, 221 wantErrs: field.ErrorList{ 222 &field.Error{ 223 Type: field.ErrorTypeRequired, 224 Field: "defaultConstraints[0].topologyKey", 225 }, 226 }, 227 }, 228 "whenUnsatisfiable is empty": { 229 args: &config.PodTopologySpreadArgs{ 230 DefaultConstraints: []v1.TopologySpreadConstraint{ 231 { 232 MaxSkew: 1, 233 TopologyKey: "node", 234 WhenUnsatisfiable: "", 235 }, 236 }, 237 DefaultingType: config.ListDefaulting, 238 }, 239 wantErrs: field.ErrorList{ 240 &field.Error{ 241 Type: field.ErrorTypeRequired, 242 Field: "defaultConstraints[0].whenUnsatisfiable", 243 }, 244 }, 245 }, 246 "whenUnsatisfiable contains unsupported action": { 247 args: &config.PodTopologySpreadArgs{ 248 DefaultConstraints: []v1.TopologySpreadConstraint{ 249 { 250 MaxSkew: 1, 251 TopologyKey: "node", 252 WhenUnsatisfiable: "unknown action", 253 }, 254 }, 255 DefaultingType: config.ListDefaulting, 256 }, 257 wantErrs: field.ErrorList{ 258 &field.Error{ 259 Type: field.ErrorTypeNotSupported, 260 Field: "defaultConstraints[0].whenUnsatisfiable", 261 }, 262 }, 263 }, 264 "duplicated constraints": { 265 args: &config.PodTopologySpreadArgs{ 266 DefaultConstraints: []v1.TopologySpreadConstraint{ 267 { 268 MaxSkew: 1, 269 TopologyKey: "node", 270 WhenUnsatisfiable: v1.DoNotSchedule, 271 }, 272 { 273 MaxSkew: 2, 274 TopologyKey: "node", 275 WhenUnsatisfiable: v1.DoNotSchedule, 276 }, 277 }, 278 DefaultingType: config.ListDefaulting, 279 }, 280 wantErrs: field.ErrorList{ 281 &field.Error{ 282 Type: field.ErrorTypeDuplicate, 283 Field: "defaultConstraints[1]", 284 }, 285 }, 286 }, 287 "label selector present": { 288 args: &config.PodTopologySpreadArgs{ 289 DefaultConstraints: []v1.TopologySpreadConstraint{ 290 { 291 MaxSkew: 1, 292 TopologyKey: "key", 293 WhenUnsatisfiable: v1.DoNotSchedule, 294 LabelSelector: &metav1.LabelSelector{ 295 MatchLabels: map[string]string{ 296 "a": "b", 297 }, 298 }, 299 }, 300 }, 301 DefaultingType: config.ListDefaulting, 302 }, 303 wantErrs: field.ErrorList{ 304 &field.Error{ 305 Type: field.ErrorTypeForbidden, 306 Field: "defaultConstraints[0].labelSelector", 307 }, 308 }, 309 }, 310 "list default constraints, no constraints": { 311 args: &config.PodTopologySpreadArgs{ 312 DefaultingType: config.ListDefaulting, 313 }, 314 }, 315 "system default constraints": { 316 args: &config.PodTopologySpreadArgs{ 317 DefaultingType: config.SystemDefaulting, 318 }, 319 }, 320 "wrong constraints": { 321 args: &config.PodTopologySpreadArgs{ 322 DefaultingType: "unknown", 323 }, 324 wantErrs: field.ErrorList{ 325 &field.Error{ 326 Type: field.ErrorTypeNotSupported, 327 Field: "defaultingType", 328 }, 329 }, 330 }, 331 "system default constraints, but has constraints": { 332 args: &config.PodTopologySpreadArgs{ 333 DefaultConstraints: []v1.TopologySpreadConstraint{ 334 { 335 MaxSkew: 1, 336 TopologyKey: "key", 337 WhenUnsatisfiable: v1.DoNotSchedule, 338 }, 339 }, 340 DefaultingType: config.SystemDefaulting, 341 }, 342 wantErrs: field.ErrorList{ 343 &field.Error{ 344 Type: field.ErrorTypeInvalid, 345 Field: "defaultingType", 346 }, 347 }, 348 }, 349 } 350 351 for name, tc := range cases { 352 t.Run(name, func(t *testing.T) { 353 err := ValidatePodTopologySpreadArgs(nil, tc.args) 354 if diff := cmp.Diff(tc.wantErrs.ToAggregate(), err, ignoreBadValueDetail); diff != "" { 355 t.Errorf("ValidatePodTopologySpreadArgs returned err (-want,+got):\n%s", diff) 356 } 357 }) 358 } 359 } 360 361 func TestValidateNodeResourcesBalancedAllocationArgs(t *testing.T) { 362 cases := map[string]struct { 363 args *config.NodeResourcesBalancedAllocationArgs 364 wantErrs field.ErrorList 365 }{ 366 "valid config": { 367 args: &config.NodeResourcesBalancedAllocationArgs{ 368 Resources: []config.ResourceSpec{ 369 { 370 Name: "cpu", 371 Weight: 1, 372 }, 373 { 374 Name: "memory", 375 Weight: 1, 376 }, 377 }, 378 }, 379 }, 380 "invalid config": { 381 args: &config.NodeResourcesBalancedAllocationArgs{ 382 Resources: []config.ResourceSpec{ 383 { 384 Name: "cpu", 385 Weight: 2, 386 }, 387 { 388 Name: "memory", 389 Weight: 1, 390 }, 391 }, 392 }, 393 wantErrs: field.ErrorList{ 394 &field.Error{ 395 Type: field.ErrorTypeInvalid, 396 Field: "resources[0].weight", 397 }, 398 }, 399 }, 400 "repeated resources": { 401 args: &config.NodeResourcesBalancedAllocationArgs{ 402 Resources: []config.ResourceSpec{ 403 { 404 Name: "cpu", 405 Weight: 1, 406 }, 407 { 408 Name: "cpu", 409 Weight: 1, 410 }, 411 }, 412 }, 413 wantErrs: field.ErrorList{ 414 &field.Error{ 415 Type: field.ErrorTypeDuplicate, 416 Field: "resources[1].name", 417 }, 418 }, 419 }, 420 } 421 422 for name, tc := range cases { 423 t.Run(name, func(t *testing.T) { 424 err := ValidateNodeResourcesBalancedAllocationArgs(nil, tc.args) 425 if diff := cmp.Diff(tc.wantErrs.ToAggregate(), err, ignoreBadValueDetail); diff != "" { 426 t.Errorf("ValidateNodeResourcesBalancedAllocationArgs returned err (-want,+got):\n%s", diff) 427 } 428 }) 429 } 430 } 431 432 func TestValidateNodeAffinityArgs(t *testing.T) { 433 cases := []struct { 434 name string 435 args config.NodeAffinityArgs 436 wantErr error 437 }{ 438 { 439 name: "empty", 440 }, 441 { 442 name: "valid added affinity", 443 args: config.NodeAffinityArgs{ 444 AddedAffinity: &v1.NodeAffinity{ 445 RequiredDuringSchedulingIgnoredDuringExecution: &v1.NodeSelector{ 446 NodeSelectorTerms: []v1.NodeSelectorTerm{ 447 { 448 MatchExpressions: []v1.NodeSelectorRequirement{ 449 { 450 Key: "label-1", 451 Operator: v1.NodeSelectorOpIn, 452 Values: []string{"label-1-val"}, 453 }, 454 }, 455 }, 456 }, 457 }, 458 PreferredDuringSchedulingIgnoredDuringExecution: []v1.PreferredSchedulingTerm{ 459 { 460 Weight: 1, 461 Preference: v1.NodeSelectorTerm{ 462 MatchFields: []v1.NodeSelectorRequirement{ 463 { 464 Key: "metadata.name", 465 Operator: v1.NodeSelectorOpIn, 466 Values: []string{"node-1"}, 467 }, 468 }, 469 }, 470 }, 471 }, 472 }, 473 }, 474 }, 475 { 476 name: "invalid added affinity", 477 args: config.NodeAffinityArgs{ 478 AddedAffinity: &v1.NodeAffinity{ 479 RequiredDuringSchedulingIgnoredDuringExecution: &v1.NodeSelector{ 480 NodeSelectorTerms: []v1.NodeSelectorTerm{ 481 { 482 MatchExpressions: []v1.NodeSelectorRequirement{ 483 { 484 Key: "invalid/label/key", 485 Operator: v1.NodeSelectorOpIn, 486 Values: []string{"label-1-val"}, 487 }, 488 }, 489 }, 490 }, 491 }, 492 PreferredDuringSchedulingIgnoredDuringExecution: []v1.PreferredSchedulingTerm{ 493 { 494 Weight: 1, 495 Preference: v1.NodeSelectorTerm{ 496 MatchFields: []v1.NodeSelectorRequirement{ 497 { 498 Key: "metadata.name", 499 Operator: v1.NodeSelectorOpIn, 500 Values: []string{"node-1", "node-2"}, 501 }, 502 }, 503 }, 504 }, 505 }, 506 }, 507 }, 508 wantErr: field.ErrorList{ 509 &field.Error{ 510 Type: field.ErrorTypeInvalid, 511 Field: "addedAffinity.requiredDuringSchedulingIgnoredDuringExecution.nodeSelectorTerms[0].matchExpressions[0].key", 512 }, 513 &field.Error{ 514 Type: field.ErrorTypeInvalid, 515 Field: "addedAffinity.preferredDuringSchedulingIgnoredDuringExecution[0].matchFields[0].values", 516 }, 517 }.ToAggregate(), 518 }, 519 } 520 for _, tc := range cases { 521 t.Run(tc.name, func(t *testing.T) { 522 err := ValidateNodeAffinityArgs(nil, &tc.args) 523 if diff := cmp.Diff(tc.wantErr, err, ignoreBadValueDetail); diff != "" { 524 t.Errorf("ValidatedNodeAffinityArgs returned err (-want,+got):\n%s", diff) 525 } 526 }) 527 } 528 } 529 530 func TestValidateVolumeBindingArgs(t *testing.T) { 531 cases := []struct { 532 name string 533 args config.VolumeBindingArgs 534 features map[featuregate.Feature]bool 535 wantErr error 536 }{ 537 { 538 name: "zero is a valid config", 539 args: config.VolumeBindingArgs{ 540 BindTimeoutSeconds: 0, 541 }, 542 }, 543 { 544 name: "positive value is valid config", 545 args: config.VolumeBindingArgs{ 546 BindTimeoutSeconds: 10, 547 }, 548 }, 549 { 550 name: "negative value is invalid config ", 551 args: config.VolumeBindingArgs{ 552 BindTimeoutSeconds: -10, 553 }, 554 wantErr: errors.NewAggregate([]error{&field.Error{ 555 Type: field.ErrorTypeInvalid, 556 Field: "bindTimeoutSeconds", 557 BadValue: int64(-10), 558 Detail: "invalid BindTimeoutSeconds, should not be a negative value", 559 }}), 560 }, 561 { 562 name: "[VolumeCapacityPriority=off] shape should be nil when the feature is off", 563 features: map[featuregate.Feature]bool{ 564 features.VolumeCapacityPriority: false, 565 }, 566 args: config.VolumeBindingArgs{ 567 BindTimeoutSeconds: 10, 568 Shape: nil, 569 }, 570 }, 571 { 572 name: "[VolumeCapacityPriority=off] error if the shape is not nil when the feature is off", 573 features: map[featuregate.Feature]bool{ 574 features.VolumeCapacityPriority: false, 575 }, 576 args: config.VolumeBindingArgs{ 577 BindTimeoutSeconds: 10, 578 Shape: []config.UtilizationShapePoint{ 579 {Utilization: 1, Score: 1}, 580 {Utilization: 3, Score: 3}, 581 }, 582 }, 583 wantErr: errors.NewAggregate([]error{&field.Error{ 584 Type: field.ErrorTypeInvalid, 585 Field: "shape", 586 }}), 587 }, 588 { 589 name: "[VolumeCapacityPriority=on] shape should not be empty", 590 features: map[featuregate.Feature]bool{ 591 features.VolumeCapacityPriority: true, 592 }, 593 args: config.VolumeBindingArgs{ 594 BindTimeoutSeconds: 10, 595 Shape: []config.UtilizationShapePoint{}, 596 }, 597 wantErr: errors.NewAggregate([]error{&field.Error{ 598 Type: field.ErrorTypeRequired, 599 Field: "shape", 600 }}), 601 }, 602 { 603 name: "[VolumeCapacityPriority=on] shape points must be sorted in increasing order", 604 features: map[featuregate.Feature]bool{ 605 features.VolumeCapacityPriority: true, 606 }, 607 args: config.VolumeBindingArgs{ 608 BindTimeoutSeconds: 10, 609 Shape: []config.UtilizationShapePoint{ 610 {Utilization: 3, Score: 3}, 611 {Utilization: 1, Score: 1}, 612 }, 613 }, 614 wantErr: errors.NewAggregate([]error{&field.Error{ 615 Type: field.ErrorTypeInvalid, 616 Field: "shape[1].utilization", 617 Detail: "Invalid value: 1: utilization values must be sorted in increasing order", 618 }}), 619 }, 620 { 621 name: "[VolumeCapacityPriority=on] shape point: invalid utilization and score", 622 features: map[featuregate.Feature]bool{ 623 features.VolumeCapacityPriority: true, 624 }, 625 args: config.VolumeBindingArgs{ 626 BindTimeoutSeconds: 10, 627 Shape: []config.UtilizationShapePoint{ 628 {Utilization: -1, Score: 1}, 629 {Utilization: 10, Score: -1}, 630 {Utilization: 20, Score: 11}, 631 {Utilization: 101, Score: 1}, 632 }, 633 }, 634 wantErr: errors.NewAggregate([]error{ 635 &field.Error{ 636 Type: field.ErrorTypeInvalid, 637 Field: "shape[0].utilization", 638 }, 639 &field.Error{ 640 Type: field.ErrorTypeInvalid, 641 Field: "shape[1].score", 642 }, 643 &field.Error{ 644 Type: field.ErrorTypeInvalid, 645 Field: "shape[2].score", 646 }, 647 &field.Error{ 648 Type: field.ErrorTypeInvalid, 649 Field: "shape[3].utilization", 650 }, 651 }), 652 }, 653 } 654 655 for _, tc := range cases { 656 t.Run(tc.name, func(t *testing.T) { 657 for k, v := range tc.features { 658 defer featuregatetesting.SetFeatureGateDuringTest(t, feature.DefaultFeatureGate, k, v)() 659 } 660 err := ValidateVolumeBindingArgs(nil, &tc.args) 661 if diff := cmp.Diff(tc.wantErr, err, ignoreBadValueDetail); diff != "" { 662 t.Errorf("ValidateVolumeBindingArgs returned err (-want,+got):\n%s", diff) 663 } 664 }) 665 } 666 } 667 668 func TestValidateFitArgs(t *testing.T) { 669 defaultScoringStrategy := &config.ScoringStrategy{ 670 Type: config.LeastAllocated, 671 Resources: []config.ResourceSpec{ 672 {Name: "cpu", Weight: 1}, 673 {Name: "memory", Weight: 1}, 674 }, 675 } 676 argsTest := []struct { 677 name string 678 args config.NodeResourcesFitArgs 679 expect string 680 }{ 681 { 682 name: "IgnoredResources: too long value", 683 args: config.NodeResourcesFitArgs{ 684 IgnoredResources: []string{fmt.Sprintf("longvalue%s", strings.Repeat("a", 64))}, 685 ScoringStrategy: defaultScoringStrategy, 686 }, 687 expect: "name part must be no more than 63 characters", 688 }, 689 { 690 name: "IgnoredResources: name is empty", 691 args: config.NodeResourcesFitArgs{ 692 IgnoredResources: []string{"example.com/"}, 693 ScoringStrategy: defaultScoringStrategy, 694 }, 695 expect: "name part must be non-empty", 696 }, 697 { 698 name: "IgnoredResources: name has too many slash", 699 args: config.NodeResourcesFitArgs{ 700 IgnoredResources: []string{"example.com/aaa/bbb"}, 701 ScoringStrategy: defaultScoringStrategy, 702 }, 703 expect: "a qualified name must consist of alphanumeric characters", 704 }, 705 { 706 name: "IgnoredResources: valid args", 707 args: config.NodeResourcesFitArgs{ 708 IgnoredResources: []string{"example.com"}, 709 ScoringStrategy: defaultScoringStrategy, 710 }, 711 }, 712 { 713 name: "IgnoredResourceGroups: valid args ", 714 args: config.NodeResourcesFitArgs{ 715 IgnoredResourceGroups: []string{"example.com"}, 716 ScoringStrategy: defaultScoringStrategy, 717 }, 718 }, 719 { 720 name: "IgnoredResourceGroups: illegal args", 721 args: config.NodeResourcesFitArgs{ 722 IgnoredResourceGroups: []string{"example.com/"}, 723 ScoringStrategy: defaultScoringStrategy, 724 }, 725 expect: "name part must be non-empty", 726 }, 727 { 728 name: "IgnoredResourceGroups: name is too long", 729 args: config.NodeResourcesFitArgs{ 730 IgnoredResourceGroups: []string{strings.Repeat("a", 64)}, 731 ScoringStrategy: defaultScoringStrategy, 732 }, 733 expect: "name part must be no more than 63 characters", 734 }, 735 { 736 name: "IgnoredResourceGroups: name cannot be contain slash", 737 args: config.NodeResourcesFitArgs{ 738 IgnoredResourceGroups: []string{"example.com/aa"}, 739 ScoringStrategy: defaultScoringStrategy, 740 }, 741 expect: "resource group name can't contain '/'", 742 }, 743 { 744 name: "ScoringStrategy: field is required", 745 args: config.NodeResourcesFitArgs{}, 746 expect: "ScoringStrategy field is required", 747 }, 748 { 749 name: "ScoringStrategy: type is unsupported", 750 args: config.NodeResourcesFitArgs{ 751 ScoringStrategy: &config.ScoringStrategy{ 752 Type: "Invalid", 753 }, 754 }, 755 expect: `Unsupported value: "Invalid"`, 756 }, 757 } 758 759 for _, test := range argsTest { 760 t.Run(test.name, func(t *testing.T) { 761 if err := ValidateNodeResourcesFitArgs(nil, &test.args); err != nil && (!strings.Contains(err.Error(), test.expect)) { 762 t.Errorf("case[%v]: error details do not include %v", test.name, err) 763 } 764 }) 765 } 766 } 767 768 func TestValidateLeastAllocatedScoringStrategy(t *testing.T) { 769 tests := []struct { 770 name string 771 resources []config.ResourceSpec 772 wantErrs field.ErrorList 773 }{ 774 { 775 name: "default config", 776 wantErrs: nil, 777 }, 778 { 779 name: "multi valid resources", 780 resources: []config.ResourceSpec{ 781 { 782 Name: "cpu", 783 Weight: 1, 784 }, 785 { 786 Name: "memory", 787 Weight: 10, 788 }, 789 }, 790 wantErrs: nil, 791 }, 792 { 793 name: "weight less than min", 794 resources: []config.ResourceSpec{ 795 { 796 Name: "cpu", 797 Weight: 0, 798 }, 799 }, 800 wantErrs: field.ErrorList{ 801 { 802 Type: field.ErrorTypeInvalid, 803 Field: "scoringStrategy.resources[0].weight", 804 }, 805 }, 806 }, 807 { 808 name: "weight greater than max", 809 resources: []config.ResourceSpec{ 810 { 811 Name: "cpu", 812 Weight: 101, 813 }, 814 }, 815 wantErrs: field.ErrorList{ 816 { 817 Type: field.ErrorTypeInvalid, 818 Field: "scoringStrategy.resources[0].weight", 819 }, 820 }, 821 }, 822 { 823 name: "multi invalid resources", 824 resources: []config.ResourceSpec{ 825 { 826 Name: "cpu", 827 Weight: 0, 828 }, 829 { 830 Name: "memory", 831 Weight: 101, 832 }, 833 }, 834 wantErrs: field.ErrorList{ 835 { 836 Type: field.ErrorTypeInvalid, 837 Field: "scoringStrategy.resources[0].weight", 838 }, 839 { 840 Type: field.ErrorTypeInvalid, 841 Field: "scoringStrategy.resources[1].weight", 842 }, 843 }, 844 }, 845 } 846 847 for _, test := range tests { 848 t.Run(test.name, func(t *testing.T) { 849 args := config.NodeResourcesFitArgs{ 850 ScoringStrategy: &config.ScoringStrategy{ 851 Type: config.LeastAllocated, 852 Resources: test.resources, 853 }, 854 } 855 err := ValidateNodeResourcesFitArgs(nil, &args) 856 if diff := cmp.Diff(test.wantErrs.ToAggregate(), err, ignoreBadValueDetail); diff != "" { 857 t.Errorf("ValidateNodeResourcesFitArgs returned err (-want,+got):\n%s", diff) 858 } 859 }) 860 } 861 } 862 863 func TestValidateMostAllocatedScoringStrategy(t *testing.T) { 864 tests := []struct { 865 name string 866 resources []config.ResourceSpec 867 wantErrs field.ErrorList 868 }{ 869 { 870 name: "default config", 871 wantErrs: nil, 872 }, 873 { 874 name: "multi valid resources", 875 resources: []config.ResourceSpec{ 876 { 877 Name: "cpu", 878 Weight: 1, 879 }, 880 { 881 Name: "memory", 882 Weight: 10, 883 }, 884 }, 885 wantErrs: nil, 886 }, 887 { 888 name: "weight less than min", 889 resources: []config.ResourceSpec{ 890 { 891 Name: "cpu", 892 Weight: 0, 893 }, 894 }, 895 wantErrs: field.ErrorList{ 896 { 897 Type: field.ErrorTypeInvalid, 898 Field: "scoringStrategy.resources[0].weight", 899 }, 900 }, 901 }, 902 { 903 name: "weight greater than max", 904 resources: []config.ResourceSpec{ 905 { 906 Name: "cpu", 907 Weight: 101, 908 }, 909 }, 910 wantErrs: field.ErrorList{ 911 { 912 Type: field.ErrorTypeInvalid, 913 Field: "scoringStrategy.resources[0].weight", 914 }, 915 }, 916 }, 917 { 918 name: "multi invalid resources", 919 resources: []config.ResourceSpec{ 920 { 921 Name: "cpu", 922 Weight: 0, 923 }, 924 { 925 Name: "memory", 926 Weight: 101, 927 }, 928 }, 929 wantErrs: field.ErrorList{ 930 { 931 Type: field.ErrorTypeInvalid, 932 Field: "scoringStrategy.resources[0].weight", 933 }, 934 { 935 Type: field.ErrorTypeInvalid, 936 Field: "scoringStrategy.resources[1].weight", 937 }, 938 }, 939 }, 940 } 941 942 for _, test := range tests { 943 t.Run(test.name, func(t *testing.T) { 944 args := config.NodeResourcesFitArgs{ 945 ScoringStrategy: &config.ScoringStrategy{ 946 Type: config.MostAllocated, 947 Resources: test.resources, 948 }, 949 } 950 err := ValidateNodeResourcesFitArgs(nil, &args) 951 if diff := cmp.Diff(test.wantErrs.ToAggregate(), err, ignoreBadValueDetail); diff != "" { 952 t.Errorf("ValidateNodeResourcesFitArgs returned err (-want,+got):\n%s", diff) 953 } 954 }) 955 } 956 } 957 958 func TestValidateRequestedToCapacityRatioScoringStrategy(t *testing.T) { 959 defaultShape := []config.UtilizationShapePoint{ 960 { 961 Utilization: 30, 962 Score: 3, 963 }, 964 } 965 tests := []struct { 966 name string 967 resources []config.ResourceSpec 968 shapes []config.UtilizationShapePoint 969 wantErrs field.ErrorList 970 }{ 971 { 972 name: "no shapes", 973 shapes: nil, 974 wantErrs: field.ErrorList{ 975 { 976 Type: field.ErrorTypeRequired, 977 Field: "scoringStrategy.shape", 978 }, 979 }, 980 }, 981 { 982 name: "weight greater than max", 983 shapes: defaultShape, 984 resources: []config.ResourceSpec{ 985 { 986 Name: "cpu", 987 Weight: 101, 988 }, 989 }, 990 wantErrs: field.ErrorList{ 991 { 992 Type: field.ErrorTypeInvalid, 993 Field: "scoringStrategy.resources[0].weight", 994 }, 995 }, 996 }, 997 { 998 name: "weight less than min", 999 shapes: defaultShape, 1000 resources: []config.ResourceSpec{ 1001 { 1002 Name: "cpu", 1003 Weight: 0, 1004 }, 1005 }, 1006 wantErrs: field.ErrorList{ 1007 { 1008 Type: field.ErrorTypeInvalid, 1009 Field: "scoringStrategy.resources[0].weight", 1010 }, 1011 }, 1012 }, 1013 { 1014 name: "valid shapes", 1015 shapes: defaultShape, 1016 wantErrs: nil, 1017 }, 1018 { 1019 name: "utilization less than min", 1020 shapes: []config.UtilizationShapePoint{ 1021 { 1022 Utilization: -1, 1023 Score: 3, 1024 }, 1025 }, 1026 wantErrs: field.ErrorList{ 1027 { 1028 Type: field.ErrorTypeInvalid, 1029 Field: "scoringStrategy.shape[0].utilization", 1030 }, 1031 }, 1032 }, 1033 { 1034 name: "utilization greater than max", 1035 shapes: []config.UtilizationShapePoint{ 1036 { 1037 Utilization: 101, 1038 Score: 3, 1039 }, 1040 }, 1041 wantErrs: field.ErrorList{ 1042 { 1043 Type: field.ErrorTypeInvalid, 1044 Field: "scoringStrategy.shape[0].utilization", 1045 }, 1046 }, 1047 }, 1048 { 1049 name: "duplicated utilization values", 1050 shapes: []config.UtilizationShapePoint{ 1051 { 1052 Utilization: 10, 1053 Score: 3, 1054 }, 1055 { 1056 Utilization: 10, 1057 Score: 3, 1058 }, 1059 }, 1060 wantErrs: field.ErrorList{ 1061 { 1062 Type: field.ErrorTypeInvalid, 1063 Field: "scoringStrategy.shape[1].utilization", 1064 }, 1065 }, 1066 }, 1067 { 1068 name: "increasing utilization values", 1069 shapes: []config.UtilizationShapePoint{ 1070 { 1071 Utilization: 10, 1072 Score: 3, 1073 }, 1074 { 1075 Utilization: 20, 1076 Score: 3, 1077 }, 1078 { 1079 Utilization: 30, 1080 Score: 3, 1081 }, 1082 }, 1083 wantErrs: nil, 1084 }, 1085 { 1086 name: "non-increasing utilization values", 1087 shapes: []config.UtilizationShapePoint{ 1088 { 1089 Utilization: 10, 1090 Score: 3, 1091 }, 1092 { 1093 Utilization: 20, 1094 Score: 3, 1095 }, 1096 { 1097 Utilization: 15, 1098 Score: 3, 1099 }, 1100 }, 1101 wantErrs: field.ErrorList{ 1102 { 1103 Type: field.ErrorTypeInvalid, 1104 Field: "scoringStrategy.shape[2].utilization", 1105 }, 1106 }, 1107 }, 1108 { 1109 name: "score less than min", 1110 shapes: []config.UtilizationShapePoint{ 1111 { 1112 Utilization: 10, 1113 Score: -1, 1114 }, 1115 }, 1116 wantErrs: field.ErrorList{ 1117 { 1118 Type: field.ErrorTypeInvalid, 1119 Field: "scoringStrategy.shape[0].score", 1120 }, 1121 }, 1122 }, 1123 { 1124 name: "score greater than max", 1125 shapes: []config.UtilizationShapePoint{ 1126 { 1127 Utilization: 10, 1128 Score: 11, 1129 }, 1130 }, 1131 wantErrs: field.ErrorList{ 1132 { 1133 Type: field.ErrorTypeInvalid, 1134 Field: "scoringStrategy.shape[0].score", 1135 }, 1136 }, 1137 }, 1138 } 1139 1140 for _, test := range tests { 1141 t.Run(test.name, func(t *testing.T) { 1142 args := config.NodeResourcesFitArgs{ 1143 ScoringStrategy: &config.ScoringStrategy{ 1144 Type: config.RequestedToCapacityRatio, 1145 Resources: test.resources, 1146 RequestedToCapacityRatio: &config.RequestedToCapacityRatioParam{ 1147 Shape: test.shapes, 1148 }, 1149 }, 1150 } 1151 err := ValidateNodeResourcesFitArgs(nil, &args) 1152 if diff := cmp.Diff(test.wantErrs.ToAggregate(), err, ignoreBadValueDetail); diff != "" { 1153 t.Errorf("ValidateNodeResourcesFitArgs returned err (-want,+got):\n%s", diff) 1154 } 1155 }) 1156 } 1157 }