1
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 }
1158
View as plain text