...

Source file src/k8s.io/kubernetes/pkg/scheduler/apis/config/validation/validation_pluginargs_test.go

Documentation: k8s.io/kubernetes/pkg/scheduler/apis/config/validation

     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  }
  1158  

View as plain text