Skip to content

Commit cb19573

Browse files
committed
Add more tests
Signed-off-by: Alik Khilazhev <7482065+alikhil@users.noreply.github.com>
1 parent fa26875 commit cb19573

File tree

7 files changed

+686
-32
lines changed

7 files changed

+686
-32
lines changed

apis/config/scheme/scheme_test.go

Lines changed: 90 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -231,6 +231,78 @@ profiles:
231231
},
232232
},
233233
},
234+
{
235+
name: "v1 NodeMetadata plugin args",
236+
data: []byte(`
237+
apiVersion: kubescheduler.config.k8s.io/v1
238+
kind: KubeSchedulerConfiguration
239+
profiles:
240+
- schedulerName: scheduler-plugins
241+
pluginConfig:
242+
- name: NodeMetadata
243+
args:
244+
metadataKey: "priority"
245+
metadataSource: "Label"
246+
metadataType: "Number"
247+
scoringStrategy: "Highest"
248+
`),
249+
wantProfiles: []schedconfig.KubeSchedulerProfile{
250+
{
251+
SchedulerName: "scheduler-plugins",
252+
Plugins: defaults.PluginsV1,
253+
PluginConfig: []schedconfig.PluginConfig{
254+
{
255+
Name: "NodeMetadata",
256+
Args: &config.NodeMetadataArgs{
257+
MetadataKey: "priority",
258+
MetadataSource: config.MetadataSourceLabel,
259+
MetadataType: config.MetadataTypeNumber,
260+
ScoringStrategy: config.ScoringStrategyHighest,
261+
},
262+
},
263+
{
264+
Name: "DefaultPreemption",
265+
Args: &schedconfig.DefaultPreemptionArgs{MinCandidateNodesPercentage: 10, MinCandidateNodesAbsolute: 100},
266+
},
267+
{
268+
Name: "DynamicResources",
269+
Args: &schedconfig.DynamicResourcesArgs{
270+
FilterTimeout: ptr.To(metav1.Duration{Duration: 10 * time.Second}),
271+
},
272+
},
273+
{
274+
Name: "InterPodAffinity",
275+
Args: &schedconfig.InterPodAffinityArgs{HardPodAffinityWeight: 1},
276+
},
277+
{
278+
Name: "NodeAffinity",
279+
Args: &schedconfig.NodeAffinityArgs{},
280+
},
281+
{
282+
Name: "NodeResourcesBalancedAllocation",
283+
Args: &schedconfig.NodeResourcesBalancedAllocationArgs{Resources: []schedconfig.ResourceSpec{{Name: "cpu", Weight: 1}, {Name: "memory", Weight: 1}}},
284+
},
285+
{
286+
Name: "NodeResourcesFit",
287+
Args: &schedconfig.NodeResourcesFitArgs{
288+
ScoringStrategy: &schedconfig.ScoringStrategy{
289+
Type: schedconfig.LeastAllocated,
290+
Resources: []schedconfig.ResourceSpec{{Name: "cpu", Weight: 1}, {Name: "memory", Weight: 1}},
291+
},
292+
},
293+
},
294+
{
295+
Name: "PodTopologySpread",
296+
Args: &schedconfig.PodTopologySpreadArgs{DefaultingType: schedconfig.SystemDefaulting},
297+
},
298+
{
299+
Name: "VolumeBinding",
300+
Args: &schedconfig.VolumeBindingArgs{BindTimeoutSeconds: 600},
301+
},
302+
},
303+
},
304+
},
305+
},
234306
}
235307
decoder := Codecs.UniversalDecoder()
236308
for _, tt := range testCases {
@@ -348,6 +420,15 @@ func TestCodecsEncodePluginConfig(t *testing.T) {
348420
NetworkTopologyName: "net-topology-v1",
349421
},
350422
},
423+
{
424+
Name: "NodeMetadata",
425+
Args: &config.NodeMetadataArgs{
426+
MetadataKey: "priority",
427+
MetadataSource: config.MetadataSourceLabel,
428+
MetadataType: config.MetadataTypeNumber,
429+
ScoringStrategy: config.ScoringStrategyHighest,
430+
},
431+
},
351432
},
352433
},
353434
},
@@ -445,6 +526,15 @@ profiles:
445526
networkTopologyName: net-topology-v1
446527
weightsName: netCosts
447528
name: NetworkOverhead
529+
- args:
530+
apiVersion: kubescheduler.config.k8s.io/v1
531+
kind: NodeMetadataArgs
532+
metadataKey: priority
533+
metadataSource: Label
534+
metadataType: Number
535+
scoringStrategy: Highest
536+
timestampFormat: ""
537+
name: NodeMetadata
448538
schedulerName: scheduler-plugins
449539
`,
450540
},

apis/config/v1/defaults.go

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,10 +18,13 @@ package v1
1818

1919
import (
2020
"strconv"
21+
"time"
2122

2223
v1 "k8s.io/api/core/v1"
2324
"k8s.io/apimachinery/pkg/api/resource"
2425
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
26+
"k8s.io/utils/ptr"
27+
2528
schedulerconfigv1 "k8s.io/kube-scheduler/config/v1"
2629
k8sschedulerconfigv1 "k8s.io/kubernetes/pkg/scheduler/apis/config/v1"
2730
)
@@ -250,3 +253,10 @@ func SetDefaults_SySchedArgs(obj *SySchedArgs) {
250253
obj.DefaultProfileName = &DefaultSySchedProfileName
251254
}
252255
}
256+
257+
// SetDefaults_NodeMetadataArgs sets the default parameters for NodeMetadataArgs plugin.
258+
func SetDefaults_NodeMetadataArgs(obj *NodeMetadataArgs) {
259+
if obj.TimestampFormat == nil && obj.MetadataType != nil && *obj.MetadataType == MetadataTypeTimestamp {
260+
obj.TimestampFormat = ptr.To(time.RFC3339)
261+
}
262+
}

apis/config/v1/zz_generated.defaults.go

Lines changed: 5 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

apis/config/validation/validation_pluginargs.go

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -109,3 +109,57 @@ func ValidateCoschedulingArgs(args *config.CoschedulingArgs, _ *field.Path) erro
109109
}
110110
return allErrs.ToAggregate()
111111
}
112+
113+
func ValidateNodeMetadataArgs(args *config.NodeMetadataArgs, path *field.Path) error {
114+
var allErrs field.ErrorList
115+
116+
// Validate MetadataKey is not empty
117+
if args.MetadataKey == "" {
118+
allErrs = append(allErrs, field.Invalid(field.NewPath("metadataKey"),
119+
args.MetadataKey, "metadataKey cannot be empty"))
120+
}
121+
122+
// Validate MetadataSource
123+
if args.MetadataSource != config.MetadataSourceLabel && args.MetadataSource != config.MetadataSourceAnnotation {
124+
allErrs = append(allErrs, field.Invalid(field.NewPath("metadataSource"),
125+
args.MetadataSource, "metadataSource must be either \"Label\" or \"Annotation\""))
126+
}
127+
128+
// Validate MetadataType
129+
if args.MetadataType != config.MetadataTypeNumber && args.MetadataType != config.MetadataTypeTimestamp {
130+
allErrs = append(allErrs, field.Invalid(field.NewPath("metadataType"),
131+
args.MetadataType, "metadataType must be either \"Number\" or \"Timestamp\""))
132+
}
133+
134+
// Validate ScoringStrategy
135+
validStrategies := sets.New[string](
136+
string(config.ScoringStrategyHighest),
137+
string(config.ScoringStrategyLowest),
138+
string(config.ScoringStrategyNewest),
139+
string(config.ScoringStrategyOldest),
140+
)
141+
if !validStrategies.Has(string(args.ScoringStrategy)) {
142+
allErrs = append(allErrs, field.Invalid(field.NewPath("scoringStrategy"),
143+
args.ScoringStrategy, "scoringStrategy must be one of \"Highest\", \"Lowest\", \"Newest\", or \"Oldest\""))
144+
}
145+
146+
// Validate compatibility between MetadataType and ScoringStrategy
147+
if args.MetadataType == config.MetadataTypeNumber {
148+
if args.ScoringStrategy == config.ScoringStrategyNewest || args.ScoringStrategy == config.ScoringStrategyOldest {
149+
allErrs = append(allErrs, field.Invalid(field.NewPath("scoringStrategy"),
150+
args.ScoringStrategy, "scoringStrategy \"Newest\" and \"Oldest\" are only valid for metadataType \"Timestamp\""))
151+
}
152+
}
153+
154+
if args.MetadataType == config.MetadataTypeTimestamp {
155+
if args.ScoringStrategy == config.ScoringStrategyHighest || args.ScoringStrategy == config.ScoringStrategyLowest {
156+
allErrs = append(allErrs, field.Invalid(field.NewPath("scoringStrategy"),
157+
args.ScoringStrategy, "scoringStrategy \"Highest\" and \"Lowest\" are only valid for metadataType \"Number\""))
158+
}
159+
}
160+
161+
if len(allErrs) == 0 {
162+
return nil
163+
}
164+
return allErrs.ToAggregate()
165+
}

apis/config/validation/validation_plugingargs_test.go

Lines changed: 126 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -193,3 +193,129 @@ func TestValidateNodeResourcesAllocatableArgs(t *testing.T) {
193193
})
194194
}
195195
}
196+
197+
func TestValidateNodeMetadataArgs(t *testing.T) {
198+
testCases := []struct {
199+
args *config.NodeMetadataArgs
200+
expectedErr error
201+
description string
202+
}{
203+
{
204+
description: "correct config with label source and numeric type",
205+
args: &config.NodeMetadataArgs{
206+
MetadataKey: "priority",
207+
MetadataSource: config.MetadataSourceLabel,
208+
MetadataType: config.MetadataTypeNumber,
209+
ScoringStrategy: config.ScoringStrategyHighest,
210+
},
211+
expectedErr: nil,
212+
},
213+
{
214+
description: "correct config with annotation source and timestamp type",
215+
args: &config.NodeMetadataArgs{
216+
MetadataKey: "lastUpdate",
217+
MetadataSource: config.MetadataSourceAnnotation,
218+
MetadataType: config.MetadataTypeTimestamp,
219+
ScoringStrategy: config.ScoringStrategyNewest,
220+
},
221+
expectedErr: nil,
222+
},
223+
{
224+
description: "missing MetadataKey",
225+
args: &config.NodeMetadataArgs{
226+
MetadataKey: "",
227+
MetadataSource: config.MetadataSourceLabel,
228+
MetadataType: config.MetadataTypeNumber,
229+
ScoringStrategy: config.ScoringStrategyHighest,
230+
},
231+
expectedErr: fmt.Errorf("metadataKey cannot be empty"),
232+
},
233+
{
234+
description: "invalid MetadataSource",
235+
args: &config.NodeMetadataArgs{
236+
MetadataKey: "priority",
237+
MetadataSource: "InvalidSource",
238+
MetadataType: config.MetadataTypeNumber,
239+
ScoringStrategy: config.ScoringStrategyHighest,
240+
},
241+
expectedErr: fmt.Errorf("metadataSource must be either \"Label\" or \"Annotation\""),
242+
},
243+
{
244+
description: "invalid MetadataType",
245+
args: &config.NodeMetadataArgs{
246+
MetadataKey: "priority",
247+
MetadataSource: config.MetadataSourceLabel,
248+
MetadataType: "InvalidType",
249+
ScoringStrategy: config.ScoringStrategyHighest,
250+
},
251+
expectedErr: fmt.Errorf("metadataType must be either \"Number\" or \"Timestamp\""),
252+
},
253+
{
254+
description: "invalid ScoringStrategy",
255+
args: &config.NodeMetadataArgs{
256+
MetadataKey: "priority",
257+
MetadataSource: config.MetadataSourceLabel,
258+
MetadataType: config.MetadataTypeNumber,
259+
ScoringStrategy: "InvalidStrategy",
260+
},
261+
expectedErr: fmt.Errorf("scoringStrategy must be one of \"Highest\", \"Lowest\", \"Newest\", or \"Oldest\""),
262+
},
263+
{
264+
description: "numeric type with Newest strategy (mismatch)",
265+
args: &config.NodeMetadataArgs{
266+
MetadataKey: "priority",
267+
MetadataSource: config.MetadataSourceLabel,
268+
MetadataType: config.MetadataTypeNumber,
269+
ScoringStrategy: config.ScoringStrategyNewest,
270+
},
271+
expectedErr: fmt.Errorf("scoringStrategy \"Newest\" and \"Oldest\" are only valid for metadataType \"Timestamp\""),
272+
},
273+
{
274+
description: "timestamp type with Highest strategy (mismatch)",
275+
args: &config.NodeMetadataArgs{
276+
MetadataKey: "lastUpdate",
277+
MetadataSource: config.MetadataSourceAnnotation,
278+
MetadataType: config.MetadataTypeTimestamp,
279+
ScoringStrategy: config.ScoringStrategyHighest,
280+
},
281+
expectedErr: fmt.Errorf("scoringStrategy \"Highest\" and \"Lowest\" are only valid for metadataType \"Number\""),
282+
},
283+
{
284+
description: "all valid combinations - Lowest with Number",
285+
args: &config.NodeMetadataArgs{
286+
MetadataKey: "cost",
287+
MetadataSource: config.MetadataSourceAnnotation,
288+
MetadataType: config.MetadataTypeNumber,
289+
ScoringStrategy: config.ScoringStrategyLowest,
290+
},
291+
expectedErr: nil,
292+
},
293+
{
294+
description: "all valid combinations - Oldest with Timestamp",
295+
args: &config.NodeMetadataArgs{
296+
MetadataKey: "provisionedAt",
297+
MetadataSource: config.MetadataSourceLabel,
298+
MetadataType: config.MetadataTypeTimestamp,
299+
ScoringStrategy: config.ScoringStrategyOldest,
300+
},
301+
expectedErr: nil,
302+
},
303+
}
304+
305+
for _, testCase := range testCases {
306+
t.Run(testCase.description, func(t *testing.T) {
307+
err := ValidateNodeMetadataArgs(testCase.args, nil)
308+
if testCase.expectedErr != nil {
309+
if err == nil {
310+
t.Fatalf("expected err to equal %v not nil", testCase.expectedErr)
311+
}
312+
if !strings.Contains(err.Error(), testCase.expectedErr.Error()) {
313+
t.Fatalf("expected err to contain %s in error message: %s", testCase.expectedErr.Error(), err.Error())
314+
}
315+
}
316+
if testCase.expectedErr == nil && err != nil {
317+
t.Fatalf("unexpected error: %v", err)
318+
}
319+
})
320+
}
321+
}

pkg/nodemetadata/node_metadata.go

Lines changed: 2 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@ import (
2929
"k8s.io/kubernetes/pkg/scheduler/framework"
3030

3131
"sigs.k8s.io/scheduler-plugins/apis/config"
32+
"sigs.k8s.io/scheduler-plugins/apis/config/validation"
3233
)
3334

3435
// NodeMetadata is a plugin that scores nodes based on their metadata (labels or annotations)
@@ -199,7 +200,7 @@ func New(ctx context.Context, obj runtime.Object, h framework.Handle) (framework
199200
}
200201

201202
// Validate arguments
202-
if err := validateArgs(args); err != nil {
203+
if err := validation.ValidateNodeMetadataArgs(args, nil); err != nil {
203204
return nil, fmt.Errorf("invalid NodeMetadataArgs: %w", err)
204205
}
205206

@@ -209,34 +210,3 @@ func New(ctx context.Context, obj runtime.Object, h framework.Handle) (framework
209210
args: args,
210211
}, nil
211212
}
212-
213-
// validateArgs validates the plugin arguments
214-
func validateArgs(args *config.NodeMetadataArgs) error {
215-
if args.MetadataKey == "" {
216-
return fmt.Errorf("metadataKey cannot be empty")
217-
}
218-
219-
if args.MetadataSource != config.MetadataSourceLabel && args.MetadataSource != config.MetadataSourceAnnotation {
220-
return fmt.Errorf("metadataSource must be either %q or %q", config.MetadataSourceLabel, config.MetadataSourceAnnotation)
221-
}
222-
223-
if args.MetadataType != config.MetadataTypeNumber && args.MetadataType != config.MetadataTypeTimestamp {
224-
return fmt.Errorf("metadataType must be either %q or %q", config.MetadataTypeNumber, config.MetadataTypeTimestamp)
225-
}
226-
227-
// Validate scoring strategy based on metadata type
228-
if args.MetadataType == config.MetadataTypeNumber {
229-
if args.ScoringStrategy != config.ScoringStrategyHighest && args.ScoringStrategy != config.ScoringStrategyLowest {
230-
return fmt.Errorf("for numeric metadata, scoringStrategy must be either %q or %q", config.ScoringStrategyHighest, config.ScoringStrategyLowest)
231-
}
232-
} else if args.MetadataType == config.MetadataTypeTimestamp {
233-
if args.ScoringStrategy != config.ScoringStrategyNewest && args.ScoringStrategy != config.ScoringStrategyOldest {
234-
return fmt.Errorf("for timestamp metadata, scoringStrategy must be either %q or %q", config.ScoringStrategyNewest, config.ScoringStrategyOldest)
235-
}
236-
if args.TimestampFormat == "" {
237-
return fmt.Errorf("timestampFormat cannot be empty when metadataType is %q", config.MetadataTypeTimestamp)
238-
}
239-
}
240-
241-
return nil
242-
}

0 commit comments

Comments
 (0)