diff --git a/pkg/scheduler/api/compatibility/compatibility_test.go b/pkg/scheduler/api/compatibility/compatibility_test.go index 778ae64b33f..4afa1c1f597 100644 --- a/pkg/scheduler/api/compatibility/compatibility_test.go +++ b/pkg/scheduler/api/compatibility/compatibility_test.go @@ -66,7 +66,6 @@ func TestCompatibility_v1_Scheduler(t *testing.T) { ] }`, wantPredicates: sets.NewString( - "MatchNodeSelector", "PodFitsResources", "PodFitsPorts", "TestServiceAffinity", @@ -80,6 +79,7 @@ func TestCompatibility_v1_Scheduler(t *testing.T) { ), wantPlugins: map[string][]kubeschedulerconfig.Plugin{ "FilterPlugin": { + {Name: "NodeAffinity"}, {Name: "VolumeRestrictions"}, }, }, @@ -109,7 +109,6 @@ func TestCompatibility_v1_Scheduler(t *testing.T) { ] }`, wantPredicates: sets.NewString( - "MatchNodeSelector", "PodFitsHostPorts", "PodFitsResources", "TestServiceAffinity", @@ -126,6 +125,7 @@ func TestCompatibility_v1_Scheduler(t *testing.T) { wantPlugins: map[string][]kubeschedulerconfig.Plugin{ "FilterPlugin": { {Name: "NodeName"}, + {Name: "NodeAffinity"}, {Name: "VolumeRestrictions"}, }, }, @@ -161,7 +161,6 @@ func TestCompatibility_v1_Scheduler(t *testing.T) { ] }`, wantPredicates: sets.NewString( - "MatchNodeSelector", "PodFitsResources", "PodFitsHostPorts", "NoVolumeZoneConflict", @@ -184,6 +183,7 @@ func TestCompatibility_v1_Scheduler(t *testing.T) { wantPlugins: map[string][]kubeschedulerconfig.Plugin{ "FilterPlugin": { {Name: "NodeName"}, + {Name: "NodeAffinity"}, {Name: "VolumeRestrictions"}, }, }, @@ -223,7 +223,6 @@ func TestCompatibility_v1_Scheduler(t *testing.T) { ] }`, wantPredicates: sets.NewString( - "MatchNodeSelector", "PodFitsResources", "PodFitsHostPorts", "NoVolumeZoneConflict", @@ -248,6 +247,7 @@ func TestCompatibility_v1_Scheduler(t *testing.T) { wantPlugins: map[string][]kubeschedulerconfig.Plugin{ "FilterPlugin": { {Name: "NodeName"}, + {Name: "NodeAffinity"}, {Name: "VolumeRestrictions"}, {Name: "TaintToleration"}, }, @@ -292,7 +292,6 @@ func TestCompatibility_v1_Scheduler(t *testing.T) { ] }`, wantPredicates: sets.NewString( - "MatchNodeSelector", "PodFitsResources", "PodFitsHostPorts", "NoVolumeZoneConflict", @@ -320,6 +319,7 @@ func TestCompatibility_v1_Scheduler(t *testing.T) { wantPlugins: map[string][]kubeschedulerconfig.Plugin{ "FilterPlugin": { {Name: "NodeName"}, + {Name: "NodeAffinity"}, {Name: "VolumeRestrictions"}, {Name: "TaintToleration"}, }, @@ -373,7 +373,6 @@ func TestCompatibility_v1_Scheduler(t *testing.T) { }] }`, wantPredicates: sets.NewString( - "MatchNodeSelector", "PodFitsResources", "PodFitsHostPorts", "NoVolumeZoneConflict", @@ -401,6 +400,7 @@ func TestCompatibility_v1_Scheduler(t *testing.T) { wantPlugins: map[string][]kubeschedulerconfig.Plugin{ "FilterPlugin": { {Name: "NodeName"}, + {Name: "NodeAffinity"}, {Name: "VolumeRestrictions"}, {Name: "TaintToleration"}, }, @@ -466,7 +466,6 @@ func TestCompatibility_v1_Scheduler(t *testing.T) { }] }`, wantPredicates: sets.NewString( - "MatchNodeSelector", "PodFitsResources", "PodFitsHostPorts", "NoVolumeZoneConflict", @@ -495,6 +494,7 @@ func TestCompatibility_v1_Scheduler(t *testing.T) { wantPlugins: map[string][]kubeschedulerconfig.Plugin{ "FilterPlugin": { {Name: "NodeName"}, + {Name: "NodeAffinity"}, {Name: "VolumeRestrictions"}, {Name: "TaintToleration"}, }, @@ -561,7 +561,6 @@ func TestCompatibility_v1_Scheduler(t *testing.T) { }] }`, wantPredicates: sets.NewString( - "MatchNodeSelector", "PodFitsResources", "PodFitsHostPorts", "NoVolumeZoneConflict", @@ -590,6 +589,7 @@ func TestCompatibility_v1_Scheduler(t *testing.T) { wantPlugins: map[string][]kubeschedulerconfig.Plugin{ "FilterPlugin": { {Name: "NodeName"}, + {Name: "NodeAffinity"}, {Name: "VolumeRestrictions"}, {Name: "TaintToleration"}, {Name: "VolumeBinding"}, @@ -661,7 +661,6 @@ func TestCompatibility_v1_Scheduler(t *testing.T) { }] }`, wantPredicates: sets.NewString( - "MatchNodeSelector", "PodFitsResources", "PodFitsHostPorts", "NoVolumeZoneConflict", @@ -691,6 +690,7 @@ func TestCompatibility_v1_Scheduler(t *testing.T) { wantPlugins: map[string][]kubeschedulerconfig.Plugin{ "FilterPlugin": { {Name: "NodeName"}, + {Name: "NodeAffinity"}, {Name: "VolumeRestrictions"}, {Name: "TaintToleration"}, {Name: "VolumeBinding"}, @@ -774,7 +774,6 @@ func TestCompatibility_v1_Scheduler(t *testing.T) { }] }`, wantPredicates: sets.NewString( - "MatchNodeSelector", "PodFitsResources", "PodFitsHostPorts", "NoVolumeZoneConflict", @@ -805,6 +804,7 @@ func TestCompatibility_v1_Scheduler(t *testing.T) { wantPlugins: map[string][]kubeschedulerconfig.Plugin{ "FilterPlugin": { {Name: "NodeName"}, + {Name: "NodeAffinity"}, {Name: "VolumeRestrictions"}, {Name: "TaintToleration"}, {Name: "VolumeBinding"}, @@ -889,7 +889,6 @@ func TestCompatibility_v1_Scheduler(t *testing.T) { }] }`, wantPredicates: sets.NewString( - "MatchNodeSelector", "PodFitsResources", "PodFitsHostPorts", "NoVolumeZoneConflict", @@ -921,6 +920,7 @@ func TestCompatibility_v1_Scheduler(t *testing.T) { wantPlugins: map[string][]kubeschedulerconfig.Plugin{ "FilterPlugin": { {Name: "NodeName"}, + {Name: "NodeAffinity"}, {Name: "VolumeRestrictions"}, {Name: "TaintToleration"}, {Name: "VolumeBinding"}, @@ -1004,7 +1004,6 @@ func TestCompatibility_v1_Scheduler(t *testing.T) { }] }`, wantPredicates: sets.NewString( - "MatchNodeSelector", "PodFitsResources", "PodFitsHostPorts", "NoVolumeZoneConflict", @@ -1037,6 +1036,7 @@ func TestCompatibility_v1_Scheduler(t *testing.T) { wantPlugins: map[string][]kubeschedulerconfig.Plugin{ "FilterPlugin": { {Name: "NodeName"}, + {Name: "NodeAffinity"}, {Name: "VolumeRestrictions"}, {Name: "TaintToleration"}, {Name: "VolumeBinding"}, @@ -1124,7 +1124,6 @@ func TestCompatibility_v1_Scheduler(t *testing.T) { }] }`, wantPredicates: sets.NewString( - "MatchNodeSelector", "PodFitsResources", "PodFitsHostPorts", "NoVolumeZoneConflict", @@ -1157,6 +1156,7 @@ func TestCompatibility_v1_Scheduler(t *testing.T) { wantPlugins: map[string][]kubeschedulerconfig.Plugin{ "FilterPlugin": { {Name: "NodeName"}, + {Name: "NodeAffinity"}, {Name: "VolumeRestrictions"}, {Name: "TaintToleration"}, {Name: "VolumeBinding"}, @@ -1184,10 +1184,11 @@ func TestCompatibility_v1_Scheduler(t *testing.T) { seenPriorities := sets.NewString() mandatoryPredicates := sets.NewString("CheckNodeCondition") filterToPredicateMap := map[string]string{ - "VolumeRestrictions": "NoDiskConflict", "TaintToleration": "PodToleratesNodeTaints", "NodeName": "HostName", + "NodeAffinity": "MatchNodeSelector", "VolumeBinding": "CheckVolumeBinding", + "VolumeRestrictions": "NoDiskConflict", } scoreToPriorityMap := map[string]string{ "TaintToleration": "TaintTolerationPriority", diff --git a/pkg/scheduler/framework/plugins/BUILD b/pkg/scheduler/framework/plugins/BUILD index dfd247e7e71..22d5d18877b 100644 --- a/pkg/scheduler/framework/plugins/BUILD +++ b/pkg/scheduler/framework/plugins/BUILD @@ -10,6 +10,7 @@ go_library( "//pkg/scheduler/algorithm/predicates:go_default_library", "//pkg/scheduler/algorithm/priorities:go_default_library", "//pkg/scheduler/apis/config:go_default_library", + "//pkg/scheduler/framework/plugins/nodeaffinity:go_default_library", "//pkg/scheduler/framework/plugins/nodename:go_default_library", "//pkg/scheduler/framework/plugins/tainttoleration:go_default_library", "//pkg/scheduler/framework/plugins/volumebinding:go_default_library", @@ -36,6 +37,7 @@ filegroup( ":package-srcs", "//pkg/scheduler/framework/plugins/examples:all-srcs", "//pkg/scheduler/framework/plugins/migration:all-srcs", + "//pkg/scheduler/framework/plugins/nodeaffinity:all-srcs", "//pkg/scheduler/framework/plugins/nodename:all-srcs", "//pkg/scheduler/framework/plugins/tainttoleration:all-srcs", "//pkg/scheduler/framework/plugins/volumebinding:all-srcs", diff --git a/pkg/scheduler/framework/plugins/default_registry.go b/pkg/scheduler/framework/plugins/default_registry.go index de31fce44ae..da565fd76e2 100644 --- a/pkg/scheduler/framework/plugins/default_registry.go +++ b/pkg/scheduler/framework/plugins/default_registry.go @@ -26,6 +26,7 @@ import ( "k8s.io/kubernetes/pkg/scheduler/algorithm/predicates" "k8s.io/kubernetes/pkg/scheduler/algorithm/priorities" "k8s.io/kubernetes/pkg/scheduler/apis/config" + "k8s.io/kubernetes/pkg/scheduler/framework/plugins/nodeaffinity" "k8s.io/kubernetes/pkg/scheduler/framework/plugins/nodename" "k8s.io/kubernetes/pkg/scheduler/framework/plugins/tainttoleration" "k8s.io/kubernetes/pkg/scheduler/framework/plugins/volumebinding" @@ -56,6 +57,7 @@ func NewDefaultRegistry(args *RegistryArgs) framework.Registry { return framework.Registry{ tainttoleration.Name: tainttoleration.New, nodename.Name: nodename.New, + nodeaffinity.Name: nodeaffinity.New, volumebinding.Name: func(_ *runtime.Unknown, _ framework.FrameworkHandle) (framework.Plugin, error) { return volumebinding.NewFromVolumeBinder(args.VolumeBinder), nil }, @@ -97,6 +99,11 @@ func NewDefaultConfigProducerRegistry() *ConfigProducerRegistry { plugins.Filter = appendToPluginSet(plugins.Filter, nodename.Name, nil) return }) + registry.RegisterPredicate(predicates.MatchNodeSelectorPred, + func(_ ConfigProducerArgs) (plugins config.Plugins, pluginConfig []config.PluginConfig) { + plugins.Filter = appendToPluginSet(plugins.Filter, nodeaffinity.Name, nil) + return + }) registry.RegisterPredicate(predicates.CheckVolumeBindingPred, func(args ConfigProducerArgs) (plugins config.Plugins, pluginConfig []config.PluginConfig) { plugins.Filter = appendToPluginSet(plugins.Filter, volumebinding.Name, nil) diff --git a/pkg/scheduler/framework/plugins/nodeaffinity/BUILD b/pkg/scheduler/framework/plugins/nodeaffinity/BUILD new file mode 100644 index 00000000000..49924ff3672 --- /dev/null +++ b/pkg/scheduler/framework/plugins/nodeaffinity/BUILD @@ -0,0 +1,44 @@ +load("@io_bazel_rules_go//go:def.bzl", "go_library", "go_test") + +go_library( + name = "go_default_library", + srcs = ["node_affinity.go"], + importpath = "k8s.io/kubernetes/pkg/scheduler/framework/plugins/nodeaffinity", + visibility = ["//visibility:public"], + deps = [ + "//pkg/scheduler/algorithm/predicates:go_default_library", + "//pkg/scheduler/framework/plugins/migration:go_default_library", + "//pkg/scheduler/framework/v1alpha1:go_default_library", + "//pkg/scheduler/nodeinfo:go_default_library", + "//staging/src/k8s.io/api/core/v1:go_default_library", + "//staging/src/k8s.io/apimachinery/pkg/runtime:go_default_library", + ], +) + +filegroup( + name = "package-srcs", + srcs = glob(["**"]), + tags = ["automanaged"], + visibility = ["//visibility:private"], +) + +filegroup( + name = "all-srcs", + srcs = [":package-srcs"], + tags = ["automanaged"], + visibility = ["//visibility:public"], +) + +go_test( + name = "go_default_test", + srcs = ["node_affinity_test.go"], + embed = [":go_default_library"], + deps = [ + "//pkg/scheduler/algorithm/predicates:go_default_library", + "//pkg/scheduler/api:go_default_library", + "//pkg/scheduler/framework/v1alpha1:go_default_library", + "//pkg/scheduler/nodeinfo:go_default_library", + "//staging/src/k8s.io/api/core/v1:go_default_library", + "//staging/src/k8s.io/apimachinery/pkg/apis/meta/v1:go_default_library", + ], +) diff --git a/pkg/scheduler/framework/plugins/nodeaffinity/node_affinity.go b/pkg/scheduler/framework/plugins/nodeaffinity/node_affinity.go new file mode 100644 index 00000000000..15a20359868 --- /dev/null +++ b/pkg/scheduler/framework/plugins/nodeaffinity/node_affinity.go @@ -0,0 +1,50 @@ +/* +Copyright 2019 The Kubernetes Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package nodeaffinity + +import ( + "k8s.io/api/core/v1" + "k8s.io/apimachinery/pkg/runtime" + "k8s.io/kubernetes/pkg/scheduler/algorithm/predicates" + "k8s.io/kubernetes/pkg/scheduler/framework/plugins/migration" + framework "k8s.io/kubernetes/pkg/scheduler/framework/v1alpha1" + "k8s.io/kubernetes/pkg/scheduler/nodeinfo" +) + +// NodeAffinity is a plugin that checks if a pod node selector matches the node label. +type NodeAffinity struct{} + +var _ = framework.FilterPlugin(&NodeAffinity{}) + +// Name is the name of the plugin used in the plugin registry and configurations. +const Name = "NodeAffinity" + +// Name returns name of the plugin. It is used in logs, etc. +func (pl *NodeAffinity) Name() string { + return Name +} + +// Filter invoked at the filter extension point. +func (pl *NodeAffinity) Filter(_ *framework.CycleState, pod *v1.Pod, nodeInfo *nodeinfo.NodeInfo) *framework.Status { + _, reasons, err := predicates.PodMatchNodeSelector(pod, nil, nodeInfo) + return migration.PredicateResultToFrameworkStatus(reasons, err) +} + +// New initializes a new plugin and returns it. +func New(_ *runtime.Unknown, _ framework.FrameworkHandle) (framework.Plugin, error) { + return &NodeAffinity{}, nil +} diff --git a/pkg/scheduler/framework/plugins/nodeaffinity/node_affinity_test.go b/pkg/scheduler/framework/plugins/nodeaffinity/node_affinity_test.go new file mode 100644 index 00000000000..3f2a41749bd --- /dev/null +++ b/pkg/scheduler/framework/plugins/nodeaffinity/node_affinity_test.go @@ -0,0 +1,706 @@ +/* +Copyright 2019 The Kubernetes Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package nodeaffinity + +import ( + "reflect" + "testing" + + "k8s.io/api/core/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/kubernetes/pkg/scheduler/algorithm/predicates" + schedulerapi "k8s.io/kubernetes/pkg/scheduler/api" + framework "k8s.io/kubernetes/pkg/scheduler/framework/v1alpha1" + schedulernodeinfo "k8s.io/kubernetes/pkg/scheduler/nodeinfo" +) + +// TODO: Add test case for RequiredDuringSchedulingRequiredDuringExecution after it's implemented. +func TestNodeAffinity(t *testing.T) { + tests := []struct { + pod *v1.Pod + labels map[string]string + nodeName string + name string + wantStatus *framework.Status + }{ + { + pod: &v1.Pod{}, + name: "no selector", + }, + { + pod: &v1.Pod{ + Spec: v1.PodSpec{ + NodeSelector: map[string]string{ + "foo": "bar", + }, + }, + }, + name: "missing labels", + wantStatus: framework.NewStatus(framework.UnschedulableAndUnresolvable, predicates.ErrNodeSelectorNotMatch.GetReason()), + }, + { + pod: &v1.Pod{ + Spec: v1.PodSpec{ + NodeSelector: map[string]string{ + "foo": "bar", + }, + }, + }, + labels: map[string]string{ + "foo": "bar", + }, + name: "same labels", + }, + { + pod: &v1.Pod{ + Spec: v1.PodSpec{ + NodeSelector: map[string]string{ + "foo": "bar", + }, + }, + }, + labels: map[string]string{ + "foo": "bar", + "baz": "blah", + }, + name: "node labels are superset", + }, + { + pod: &v1.Pod{ + Spec: v1.PodSpec{ + NodeSelector: map[string]string{ + "foo": "bar", + "baz": "blah", + }, + }, + }, + labels: map[string]string{ + "foo": "bar", + }, + name: "node labels are subset", + wantStatus: framework.NewStatus(framework.UnschedulableAndUnresolvable, predicates.ErrNodeSelectorNotMatch.GetReason()), + }, + { + pod: &v1.Pod{ + Spec: v1.PodSpec{ + Affinity: &v1.Affinity{ + NodeAffinity: &v1.NodeAffinity{ + RequiredDuringSchedulingIgnoredDuringExecution: &v1.NodeSelector{ + NodeSelectorTerms: []v1.NodeSelectorTerm{ + { + MatchExpressions: []v1.NodeSelectorRequirement{ + { + Key: "foo", + Operator: v1.NodeSelectorOpIn, + Values: []string{"bar", "value2"}, + }, + }, + }, + }, + }, + }, + }, + }, + }, + labels: map[string]string{ + "foo": "bar", + }, + name: "Pod with matchExpressions using In operator that matches the existing node", + }, + { + pod: &v1.Pod{ + Spec: v1.PodSpec{ + Affinity: &v1.Affinity{ + NodeAffinity: &v1.NodeAffinity{ + RequiredDuringSchedulingIgnoredDuringExecution: &v1.NodeSelector{ + NodeSelectorTerms: []v1.NodeSelectorTerm{ + { + MatchExpressions: []v1.NodeSelectorRequirement{ + { + Key: "kernel-version", + Operator: v1.NodeSelectorOpGt, + Values: []string{"0204"}, + }, + }, + }, + }, + }, + }, + }, + }, + }, + labels: map[string]string{ + // We use two digit to denote major version and two digit for minor version. + "kernel-version": "0206", + }, + name: "Pod with matchExpressions using Gt operator that matches the existing node", + }, + { + pod: &v1.Pod{ + Spec: v1.PodSpec{ + Affinity: &v1.Affinity{ + NodeAffinity: &v1.NodeAffinity{ + RequiredDuringSchedulingIgnoredDuringExecution: &v1.NodeSelector{ + NodeSelectorTerms: []v1.NodeSelectorTerm{ + { + MatchExpressions: []v1.NodeSelectorRequirement{ + { + Key: "mem-type", + Operator: v1.NodeSelectorOpNotIn, + Values: []string{"DDR", "DDR2"}, + }, + }, + }, + }, + }, + }, + }, + }, + }, + labels: map[string]string{ + "mem-type": "DDR3", + }, + name: "Pod with matchExpressions using NotIn operator that matches the existing node", + }, + { + pod: &v1.Pod{ + Spec: v1.PodSpec{ + Affinity: &v1.Affinity{ + NodeAffinity: &v1.NodeAffinity{ + RequiredDuringSchedulingIgnoredDuringExecution: &v1.NodeSelector{ + NodeSelectorTerms: []v1.NodeSelectorTerm{ + { + MatchExpressions: []v1.NodeSelectorRequirement{ + { + Key: "GPU", + Operator: v1.NodeSelectorOpExists, + }, + }, + }, + }, + }, + }, + }, + }, + }, + labels: map[string]string{ + "GPU": "NVIDIA-GRID-K1", + }, + name: "Pod with matchExpressions using Exists operator that matches the existing node", + }, + { + pod: &v1.Pod{ + Spec: v1.PodSpec{ + Affinity: &v1.Affinity{ + NodeAffinity: &v1.NodeAffinity{ + RequiredDuringSchedulingIgnoredDuringExecution: &v1.NodeSelector{ + NodeSelectorTerms: []v1.NodeSelectorTerm{ + { + MatchExpressions: []v1.NodeSelectorRequirement{ + { + Key: "foo", + Operator: v1.NodeSelectorOpIn, + Values: []string{"value1", "value2"}, + }, + }, + }, + }, + }, + }, + }, + }, + }, + labels: map[string]string{ + "foo": "bar", + }, + name: "Pod with affinity that don't match node's labels won't schedule onto the node", + wantStatus: framework.NewStatus(framework.UnschedulableAndUnresolvable, predicates.ErrNodeSelectorNotMatch.GetReason()), + }, + { + pod: &v1.Pod{ + Spec: v1.PodSpec{ + Affinity: &v1.Affinity{ + NodeAffinity: &v1.NodeAffinity{ + RequiredDuringSchedulingIgnoredDuringExecution: &v1.NodeSelector{ + NodeSelectorTerms: nil, + }, + }, + }, + }, + }, + labels: map[string]string{ + "foo": "bar", + }, + name: "Pod with a nil []NodeSelectorTerm in affinity, can't match the node's labels and won't schedule onto the node", + wantStatus: framework.NewStatus(framework.UnschedulableAndUnresolvable, predicates.ErrNodeSelectorNotMatch.GetReason()), + }, + { + pod: &v1.Pod{ + Spec: v1.PodSpec{ + Affinity: &v1.Affinity{ + NodeAffinity: &v1.NodeAffinity{ + RequiredDuringSchedulingIgnoredDuringExecution: &v1.NodeSelector{ + NodeSelectorTerms: []v1.NodeSelectorTerm{}, + }, + }, + }, + }, + }, + labels: map[string]string{ + "foo": "bar", + }, + name: "Pod with an empty []NodeSelectorTerm in affinity, can't match the node's labels and won't schedule onto the node", + wantStatus: framework.NewStatus(framework.UnschedulableAndUnresolvable, predicates.ErrNodeSelectorNotMatch.GetReason()), + }, + { + pod: &v1.Pod{ + Spec: v1.PodSpec{ + Affinity: &v1.Affinity{ + NodeAffinity: &v1.NodeAffinity{ + RequiredDuringSchedulingIgnoredDuringExecution: &v1.NodeSelector{ + NodeSelectorTerms: []v1.NodeSelectorTerm{ + { + MatchExpressions: []v1.NodeSelectorRequirement{}, + }, + }, + }, + }, + }, + }, + }, + labels: map[string]string{ + "foo": "bar", + }, + name: "Pod with empty MatchExpressions is not a valid value will match no objects and won't schedule onto the node", + wantStatus: framework.NewStatus(framework.UnschedulableAndUnresolvable, predicates.ErrNodeSelectorNotMatch.GetReason()), + }, + { + pod: &v1.Pod{}, + labels: map[string]string{ + "foo": "bar", + }, + name: "Pod with no Affinity will schedule onto a node", + }, + { + pod: &v1.Pod{ + Spec: v1.PodSpec{ + Affinity: &v1.Affinity{ + NodeAffinity: &v1.NodeAffinity{ + RequiredDuringSchedulingIgnoredDuringExecution: nil, + }, + }, + }, + }, + labels: map[string]string{ + "foo": "bar", + }, + name: "Pod with Affinity but nil NodeSelector will schedule onto a node", + }, + { + pod: &v1.Pod{ + Spec: v1.PodSpec{ + Affinity: &v1.Affinity{ + NodeAffinity: &v1.NodeAffinity{ + RequiredDuringSchedulingIgnoredDuringExecution: &v1.NodeSelector{ + NodeSelectorTerms: []v1.NodeSelectorTerm{ + { + MatchExpressions: []v1.NodeSelectorRequirement{ + { + Key: "GPU", + Operator: v1.NodeSelectorOpExists, + }, { + Key: "GPU", + Operator: v1.NodeSelectorOpNotIn, + Values: []string{"AMD", "INTER"}, + }, + }, + }, + }, + }, + }, + }, + }, + }, + labels: map[string]string{ + "GPU": "NVIDIA-GRID-K1", + }, + name: "Pod with multiple matchExpressions ANDed that matches the existing node", + }, + { + pod: &v1.Pod{ + Spec: v1.PodSpec{ + Affinity: &v1.Affinity{ + NodeAffinity: &v1.NodeAffinity{ + RequiredDuringSchedulingIgnoredDuringExecution: &v1.NodeSelector{ + NodeSelectorTerms: []v1.NodeSelectorTerm{ + { + MatchExpressions: []v1.NodeSelectorRequirement{ + { + Key: "GPU", + Operator: v1.NodeSelectorOpExists, + }, { + Key: "GPU", + Operator: v1.NodeSelectorOpIn, + Values: []string{"AMD", "INTER"}, + }, + }, + }, + }, + }, + }, + }, + }, + }, + labels: map[string]string{ + "GPU": "NVIDIA-GRID-K1", + }, + name: "Pod with multiple matchExpressions ANDed that doesn't match the existing node", + wantStatus: framework.NewStatus(framework.UnschedulableAndUnresolvable, predicates.ErrNodeSelectorNotMatch.GetReason()), + }, + { + pod: &v1.Pod{ + Spec: v1.PodSpec{ + Affinity: &v1.Affinity{ + NodeAffinity: &v1.NodeAffinity{ + RequiredDuringSchedulingIgnoredDuringExecution: &v1.NodeSelector{ + NodeSelectorTerms: []v1.NodeSelectorTerm{ + { + MatchExpressions: []v1.NodeSelectorRequirement{ + { + Key: "foo", + Operator: v1.NodeSelectorOpIn, + Values: []string{"bar", "value2"}, + }, + }, + }, + { + MatchExpressions: []v1.NodeSelectorRequirement{ + { + Key: "diffkey", + Operator: v1.NodeSelectorOpIn, + Values: []string{"wrong", "value2"}, + }, + }, + }, + }, + }, + }, + }, + }, + }, + labels: map[string]string{ + "foo": "bar", + }, + name: "Pod with multiple NodeSelectorTerms ORed in affinity, matches the node's labels and will schedule onto the node", + }, + { + pod: &v1.Pod{ + Spec: v1.PodSpec{ + NodeSelector: map[string]string{ + "foo": "bar", + }, + Affinity: &v1.Affinity{ + NodeAffinity: &v1.NodeAffinity{ + RequiredDuringSchedulingIgnoredDuringExecution: &v1.NodeSelector{ + NodeSelectorTerms: []v1.NodeSelectorTerm{ + { + MatchExpressions: []v1.NodeSelectorRequirement{ + { + Key: "foo", + Operator: v1.NodeSelectorOpExists, + }, + }, + }, + }, + }, + }, + }, + }, + }, + labels: map[string]string{ + "foo": "bar", + }, + name: "Pod with an Affinity and a PodSpec.NodeSelector(the old thing that we are deprecating) " + + "both are satisfied, will schedule onto the node", + }, + { + pod: &v1.Pod{ + Spec: v1.PodSpec{ + NodeSelector: map[string]string{ + "foo": "bar", + }, + Affinity: &v1.Affinity{ + NodeAffinity: &v1.NodeAffinity{ + RequiredDuringSchedulingIgnoredDuringExecution: &v1.NodeSelector{ + NodeSelectorTerms: []v1.NodeSelectorTerm{ + { + MatchExpressions: []v1.NodeSelectorRequirement{ + { + Key: "foo", + Operator: v1.NodeSelectorOpExists, + }, + }, + }, + }, + }, + }, + }, + }, + }, + labels: map[string]string{ + "foo": "barrrrrr", + }, + name: "Pod with an Affinity matches node's labels but the PodSpec.NodeSelector(the old thing that we are deprecating) " + + "is not satisfied, won't schedule onto the node", + wantStatus: framework.NewStatus(framework.UnschedulableAndUnresolvable, predicates.ErrNodeSelectorNotMatch.GetReason()), + }, + { + pod: &v1.Pod{ + Spec: v1.PodSpec{ + Affinity: &v1.Affinity{ + NodeAffinity: &v1.NodeAffinity{ + RequiredDuringSchedulingIgnoredDuringExecution: &v1.NodeSelector{ + NodeSelectorTerms: []v1.NodeSelectorTerm{ + { + MatchExpressions: []v1.NodeSelectorRequirement{ + { + Key: "foo", + Operator: v1.NodeSelectorOpNotIn, + Values: []string{"invalid value: ___@#$%^"}, + }, + }, + }, + }, + }, + }, + }, + }, + }, + labels: map[string]string{ + "foo": "bar", + }, + name: "Pod with an invalid value in Affinity term won't be scheduled onto the node", + wantStatus: framework.NewStatus(framework.UnschedulableAndUnresolvable, predicates.ErrNodeSelectorNotMatch.GetReason()), + }, + { + pod: &v1.Pod{ + Spec: v1.PodSpec{ + Affinity: &v1.Affinity{ + NodeAffinity: &v1.NodeAffinity{ + RequiredDuringSchedulingIgnoredDuringExecution: &v1.NodeSelector{ + NodeSelectorTerms: []v1.NodeSelectorTerm{ + { + MatchFields: []v1.NodeSelectorRequirement{ + { + Key: schedulerapi.NodeFieldSelectorKeyNodeName, + Operator: v1.NodeSelectorOpIn, + Values: []string{"node_1"}, + }, + }, + }, + }, + }, + }, + }, + }, + }, + nodeName: "node_1", + name: "Pod with matchFields using In operator that matches the existing node", + }, + { + pod: &v1.Pod{ + Spec: v1.PodSpec{ + Affinity: &v1.Affinity{ + NodeAffinity: &v1.NodeAffinity{ + RequiredDuringSchedulingIgnoredDuringExecution: &v1.NodeSelector{ + NodeSelectorTerms: []v1.NodeSelectorTerm{ + { + MatchFields: []v1.NodeSelectorRequirement{ + { + Key: schedulerapi.NodeFieldSelectorKeyNodeName, + Operator: v1.NodeSelectorOpIn, + Values: []string{"node_1"}, + }, + }, + }, + }, + }, + }, + }, + }, + }, + nodeName: "node_2", + name: "Pod with matchFields using In operator that does not match the existing node", + wantStatus: framework.NewStatus(framework.UnschedulableAndUnresolvable, predicates.ErrNodeSelectorNotMatch.GetReason()), + }, + { + pod: &v1.Pod{ + Spec: v1.PodSpec{ + Affinity: &v1.Affinity{ + NodeAffinity: &v1.NodeAffinity{ + RequiredDuringSchedulingIgnoredDuringExecution: &v1.NodeSelector{ + NodeSelectorTerms: []v1.NodeSelectorTerm{ + { + MatchFields: []v1.NodeSelectorRequirement{ + { + Key: schedulerapi.NodeFieldSelectorKeyNodeName, + Operator: v1.NodeSelectorOpIn, + Values: []string{"node_1"}, + }, + }, + }, + { + MatchExpressions: []v1.NodeSelectorRequirement{ + { + Key: "foo", + Operator: v1.NodeSelectorOpIn, + Values: []string{"bar"}, + }, + }, + }, + }, + }, + }, + }, + }, + }, + nodeName: "node_2", + labels: map[string]string{"foo": "bar"}, + name: "Pod with two terms: matchFields does not match, but matchExpressions matches", + }, + { + pod: &v1.Pod{ + Spec: v1.PodSpec{ + Affinity: &v1.Affinity{ + NodeAffinity: &v1.NodeAffinity{ + RequiredDuringSchedulingIgnoredDuringExecution: &v1.NodeSelector{ + NodeSelectorTerms: []v1.NodeSelectorTerm{ + { + MatchFields: []v1.NodeSelectorRequirement{ + { + Key: schedulerapi.NodeFieldSelectorKeyNodeName, + Operator: v1.NodeSelectorOpIn, + Values: []string{"node_1"}, + }, + }, + MatchExpressions: []v1.NodeSelectorRequirement{ + { + Key: "foo", + Operator: v1.NodeSelectorOpIn, + Values: []string{"bar"}, + }, + }, + }, + }, + }, + }, + }, + }, + }, + nodeName: "node_2", + labels: map[string]string{"foo": "bar"}, + name: "Pod with one term: matchFields does not match, but matchExpressions matches", + wantStatus: framework.NewStatus(framework.UnschedulableAndUnresolvable, predicates.ErrNodeSelectorNotMatch.GetReason()), + }, + { + pod: &v1.Pod{ + Spec: v1.PodSpec{ + Affinity: &v1.Affinity{ + NodeAffinity: &v1.NodeAffinity{ + RequiredDuringSchedulingIgnoredDuringExecution: &v1.NodeSelector{ + NodeSelectorTerms: []v1.NodeSelectorTerm{ + { + MatchFields: []v1.NodeSelectorRequirement{ + { + Key: schedulerapi.NodeFieldSelectorKeyNodeName, + Operator: v1.NodeSelectorOpIn, + Values: []string{"node_1"}, + }, + }, + MatchExpressions: []v1.NodeSelectorRequirement{ + { + Key: "foo", + Operator: v1.NodeSelectorOpIn, + Values: []string{"bar"}, + }, + }, + }, + }, + }, + }, + }, + }, + }, + nodeName: "node_1", + labels: map[string]string{"foo": "bar"}, + name: "Pod with one term: both matchFields and matchExpressions match", + }, + { + pod: &v1.Pod{ + Spec: v1.PodSpec{ + Affinity: &v1.Affinity{ + NodeAffinity: &v1.NodeAffinity{ + RequiredDuringSchedulingIgnoredDuringExecution: &v1.NodeSelector{ + NodeSelectorTerms: []v1.NodeSelectorTerm{ + { + MatchFields: []v1.NodeSelectorRequirement{ + { + Key: schedulerapi.NodeFieldSelectorKeyNodeName, + Operator: v1.NodeSelectorOpIn, + Values: []string{"node_1"}, + }, + }, + }, + { + MatchExpressions: []v1.NodeSelectorRequirement{ + { + Key: "foo", + Operator: v1.NodeSelectorOpIn, + Values: []string{"not-match-to-bar"}, + }, + }, + }, + }, + }, + }, + }, + }, + }, + nodeName: "node_2", + labels: map[string]string{"foo": "bar"}, + name: "Pod with two terms: both matchFields and matchExpressions do not match", + wantStatus: framework.NewStatus(framework.UnschedulableAndUnresolvable, predicates.ErrNodeSelectorNotMatch.GetReason()), + }, + } + + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + node := v1.Node{ObjectMeta: metav1.ObjectMeta{ + Name: test.nodeName, + Labels: test.labels, + }} + nodeInfo := schedulernodeinfo.NewNodeInfo() + nodeInfo.SetNode(&node) + + p, _ := New(nil, nil) + gotStatus := p.(framework.FilterPlugin).Filter(nil, test.pod, nodeInfo) + if !reflect.DeepEqual(gotStatus, test.wantStatus) { + t.Errorf("status does not match: %v, want: %v", gotStatus, test.wantStatus) + } + }) + } +}