diff --git a/pkg/scheduler/framework/plugins/nodeaffinity/node_affinity.go b/pkg/scheduler/framework/plugins/nodeaffinity/node_affinity.go index e4ab811409b..032ba237899 100644 --- a/pkg/scheduler/framework/plugins/nodeaffinity/node_affinity.go +++ b/pkg/scheduler/framework/plugins/nodeaffinity/node_affinity.go @@ -21,7 +21,6 @@ import ( "fmt" v1 "k8s.io/api/core/v1" - "k8s.io/apimachinery/pkg/labels" "k8s.io/apimachinery/pkg/runtime" "k8s.io/component-helpers/scheduling/corev1/nodeaffinity" "k8s.io/kubernetes/pkg/scheduler/apis/config" @@ -63,8 +62,7 @@ func (pl *NodeAffinity) Name() string { } type preFilterState struct { - requiredNodeSelector labels.Selector - requiredNodeAffinity *nodeaffinity.LazyErrorNodeSelector + requiredNodeSelectorAndAffinity nodeaffinity.RequiredNodeAffinity } // Clone just returns the same state because it is not affected by pod additions or deletions. @@ -74,7 +72,7 @@ func (s *preFilterState) Clone() framework.StateData { // PreFilter builds and writes cycle state used by Filter. func (pl *NodeAffinity) PreFilter(ctx context.Context, cycleState *framework.CycleState, pod *v1.Pod) *framework.Status { - state := getPodRequiredNodeSelectorAndAffinity(pod) + state := &preFilterState{requiredNodeSelectorAndAffinity: nodeaffinity.GetRequiredNodeAffinity(pod)} cycleState.Write(preFilterStateKey, state) return nil } @@ -99,21 +97,15 @@ func (pl *NodeAffinity) Filter(ctx context.Context, state *framework.CycleState, if err != nil { // Fallback to calculate requiredNodeSelector and requiredNodeAffinity // here when PreFilter is disabled. - s = getPodRequiredNodeSelectorAndAffinity(pod) + s = &preFilterState{requiredNodeSelectorAndAffinity: nodeaffinity.GetRequiredNodeAffinity(pod)} } - if s.requiredNodeSelector != nil { - if !s.requiredNodeSelector.Matches(labels.Set(node.Labels)) { - return framework.NewStatus(framework.UnschedulableAndUnresolvable, ErrReasonPod) - } - } - if s.requiredNodeAffinity != nil { - // Ignore parsing errors for backwards compatibility. - matches, _ := s.requiredNodeAffinity.Match(node) - if !matches { - return framework.NewStatus(framework.UnschedulableAndUnresolvable, ErrReasonPod) - } + // Ignore parsing errors for backwards compatibility. + match, _ := s.requiredNodeSelectorAndAffinity.Match(node) + if !match { + return framework.NewStatus(framework.UnschedulableAndUnresolvable, ErrReasonPod) } + return nil } @@ -248,21 +240,6 @@ func getPreScoreState(cycleState *framework.CycleState) (*preScoreState, error) return s, nil } -func getPodRequiredNodeSelectorAndAffinity(pod *v1.Pod) *preFilterState { - var selector labels.Selector - if len(pod.Spec.NodeSelector) > 0 { - selector = labels.SelectorFromSet(pod.Spec.NodeSelector) - } - // Use LazyErrorNodeSelector for backwards compatibility of parsing errors. - var affinity *nodeaffinity.LazyErrorNodeSelector - if pod.Spec.Affinity != nil && - pod.Spec.Affinity.NodeAffinity != nil && - pod.Spec.Affinity.NodeAffinity.RequiredDuringSchedulingIgnoredDuringExecution != nil { - affinity = nodeaffinity.NewLazyErrorNodeSelector(pod.Spec.Affinity.NodeAffinity.RequiredDuringSchedulingIgnoredDuringExecution) - } - return &preFilterState{requiredNodeSelector: selector, requiredNodeAffinity: affinity} -} - func getPreFilterState(cycleState *framework.CycleState) (*preFilterState, error) { c, err := cycleState.Read(preFilterStateKey) if err != nil { diff --git a/pkg/scheduler/framework/plugins/podtopologyspread/filtering.go b/pkg/scheduler/framework/plugins/podtopologyspread/filtering.go index b66037bd2a0..6d86cdc84b6 100644 --- a/pkg/scheduler/framework/plugins/podtopologyspread/filtering.go +++ b/pkg/scheduler/framework/plugins/podtopologyspread/filtering.go @@ -24,9 +24,9 @@ import ( v1 "k8s.io/api/core/v1" "k8s.io/apimachinery/pkg/labels" + "k8s.io/component-helpers/scheduling/corev1/nodeaffinity" "k8s.io/klog/v2" "k8s.io/kubernetes/pkg/scheduler/framework" - "k8s.io/kubernetes/pkg/scheduler/framework/plugins/helper" ) const preFilterStateKey = "PreFilter" + Name @@ -222,6 +222,7 @@ func (pl *PodTopologySpread) calPreFilterState(pod *v1.Pod) (*preFilterState, er TpKeyToCriticalPaths: make(map[string]*criticalPaths, len(constraints)), TpPairToMatchNum: make(map[topologyPair]*int32, sizeHeuristic(len(allNodes), constraints)), } + requiredSchedulingTerm := nodeaffinity.GetRequiredNodeAffinity(pod) for _, n := range allNodes { node := n.Node() if node == nil { @@ -230,7 +231,9 @@ func (pl *PodTopologySpread) calPreFilterState(pod *v1.Pod) (*preFilterState, er } // In accordance to design, if NodeAffinity or NodeSelector is defined, // spreading is applied to nodes that pass those filters. - if !helper.PodMatchesNodeSelectorAndAffinityTerms(pod, node) { + // Ignore parsing errors for backwards compatibility. + match, _ := requiredSchedulingTerm.Match(node) + if !match { continue } // Ensure current node's labels contains all topologyKeys in 'Constraints'. diff --git a/pkg/scheduler/framework/plugins/podtopologyspread/scoring.go b/pkg/scheduler/framework/plugins/podtopologyspread/scoring.go index 3203e1dd1b4..f1b9ff7e6aa 100644 --- a/pkg/scheduler/framework/plugins/podtopologyspread/scoring.go +++ b/pkg/scheduler/framework/plugins/podtopologyspread/scoring.go @@ -24,8 +24,8 @@ import ( v1 "k8s.io/api/core/v1" "k8s.io/apimachinery/pkg/util/sets" + "k8s.io/component-helpers/scheduling/corev1/nodeaffinity" "k8s.io/kubernetes/pkg/scheduler/framework" - pluginhelper "k8s.io/kubernetes/pkg/scheduler/framework/plugins/helper" ) const preScoreStateKey = "PreScore" + Name @@ -136,6 +136,8 @@ func (pl *PodTopologySpread) PreScore( return nil } + // Ignore parsing errors for backwards compatibility. + requiredNodeAffinity := nodeaffinity.GetRequiredNodeAffinity(pod) processAllNode := func(i int) { nodeInfo := allNodes[i] node := nodeInfo.Node() @@ -144,8 +146,8 @@ func (pl *PodTopologySpread) PreScore( } // (1) `node` should satisfy incoming pod's NodeSelector/NodeAffinity // (2) All topologyKeys need to be present in `node` - if !pluginhelper.PodMatchesNodeSelectorAndAffinityTerms(pod, node) || - !nodeLabelsMatchSpreadConstraints(node.Labels, state.Constraints) { + match, _ := requiredNodeAffinity.Match(node) + if !match || !nodeLabelsMatchSpreadConstraints(node.Labels, state.Constraints) { return } diff --git a/staging/src/k8s.io/component-helpers/scheduling/corev1/nodeaffinity/nodeaffinity.go b/staging/src/k8s.io/component-helpers/scheduling/corev1/nodeaffinity/nodeaffinity.go index 4157f772cad..27caf69b920 100644 --- a/staging/src/k8s.io/component-helpers/scheduling/corev1/nodeaffinity/nodeaffinity.go +++ b/staging/src/k8s.io/component-helpers/scheduling/corev1/nodeaffinity/nodeaffinity.go @@ -287,3 +287,38 @@ type preferredSchedulingTerm struct { nodeSelectorTerm weight int } + +type RequiredNodeAffinity struct { + labelSelector labels.Selector + nodeSelector *LazyErrorNodeSelector +} + +// GetRequiredNodeAffinity returns the parsing result of pod's nodeSelector and nodeAffinity. +func GetRequiredNodeAffinity(pod *v1.Pod) RequiredNodeAffinity { + var selector labels.Selector + if len(pod.Spec.NodeSelector) > 0 { + selector = labels.SelectorFromSet(pod.Spec.NodeSelector) + } + // Use LazyErrorNodeSelector for backwards compatibility of parsing errors. + var affinity *LazyErrorNodeSelector + if pod.Spec.Affinity != nil && + pod.Spec.Affinity.NodeAffinity != nil && + pod.Spec.Affinity.NodeAffinity.RequiredDuringSchedulingIgnoredDuringExecution != nil { + affinity = NewLazyErrorNodeSelector(pod.Spec.Affinity.NodeAffinity.RequiredDuringSchedulingIgnoredDuringExecution) + } + return RequiredNodeAffinity{labelSelector: selector, nodeSelector: affinity} +} + +// Match checks whether the pod is schedulable onto nodes according to +// the requirements in both nodeSelector and nodeAffinity. +func (s RequiredNodeAffinity) Match(node *v1.Node) (bool, error) { + if s.labelSelector != nil { + if !s.labelSelector.Matches(labels.Set(node.Labels)) { + return false, nil + } + } + if s.nodeSelector != nil { + return s.nodeSelector.Match(node) + } + return true, nil +} diff --git a/staging/src/k8s.io/component-helpers/scheduling/corev1/nodeaffinity/nodeaffinity_test.go b/staging/src/k8s.io/component-helpers/scheduling/corev1/nodeaffinity/nodeaffinity_test.go index d08c8c93acf..dc0cee84c62 100644 --- a/staging/src/k8s.io/component-helpers/scheduling/corev1/nodeaffinity/nodeaffinity_test.go +++ b/staging/src/k8s.io/component-helpers/scheduling/corev1/nodeaffinity/nodeaffinity_test.go @@ -358,3 +358,690 @@ func TestNodeSelectorRequirementsAsSelector(t *testing.T) { } } } + +func TestPodMatchesNodeSelectorAndAffinityTerms(t *testing.T) { + tests := []struct { + name string + pod *v1.Pod + labels map[string]string + nodeName string + want bool + }{ + { + name: "no selector", + pod: &v1.Pod{}, + want: true, + }, + { + name: "missing labels", + pod: &v1.Pod{ + Spec: v1.PodSpec{ + NodeSelector: map[string]string{ + "foo": "bar", + }, + }, + }, + want: false, + }, + { + name: "same labels", + pod: &v1.Pod{ + Spec: v1.PodSpec{ + NodeSelector: map[string]string{ + "foo": "bar", + }, + }, + }, + labels: map[string]string{ + "foo": "bar", + }, + want: true, + }, + { + name: "node labels are superset", + pod: &v1.Pod{ + Spec: v1.PodSpec{ + NodeSelector: map[string]string{ + "foo": "bar", + }, + }, + }, + labels: map[string]string{ + "foo": "bar", + "baz": "blah", + }, + want: true, + }, + { + name: "node labels are subset", + pod: &v1.Pod{ + Spec: v1.PodSpec{ + NodeSelector: map[string]string{ + "foo": "bar", + "baz": "blah", + }, + }, + }, + labels: map[string]string{ + "foo": "bar", + }, + want: false, + }, + { + 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: "foo", + Operator: v1.NodeSelectorOpIn, + Values: []string{"bar", "value2"}, + }, + }, + }, + }, + }, + }, + }, + }, + }, + labels: map[string]string{ + "foo": "bar", + }, + want: true, + }, + { + 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: "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", + }, + want: true, + }, + { + 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: "mem-type", + Operator: v1.NodeSelectorOpNotIn, + Values: []string{"DDR", "DDR2"}, + }, + }, + }, + }, + }, + }, + }, + }, + }, + labels: map[string]string{ + "mem-type": "DDR3", + }, + want: true, + }, + { + 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: "GPU", + Operator: v1.NodeSelectorOpExists, + }, + }, + }, + }, + }, + }, + }, + }, + }, + labels: map[string]string{ + "GPU": "NVIDIA-GRID-K1", + }, + want: true, + }, + { + 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", + }, + want: false, + name: "Pod with affinity that don't match node's labels won't schedule onto the node", + }, + { + pod: &v1.Pod{ + Spec: v1.PodSpec{ + Affinity: &v1.Affinity{ + NodeAffinity: &v1.NodeAffinity{ + RequiredDuringSchedulingIgnoredDuringExecution: &v1.NodeSelector{ + NodeSelectorTerms: nil, + }, + }, + }, + }, + }, + labels: map[string]string{ + "foo": "bar", + }, + want: false, + name: "Pod with a nil []NodeSelectorTerm in affinity, can't match the node's labels and won't schedule onto the node", + }, + { + pod: &v1.Pod{ + Spec: v1.PodSpec{ + Affinity: &v1.Affinity{ + NodeAffinity: &v1.NodeAffinity{ + RequiredDuringSchedulingIgnoredDuringExecution: &v1.NodeSelector{ + NodeSelectorTerms: []v1.NodeSelectorTerm{}, + }, + }, + }, + }, + }, + labels: map[string]string{ + "foo": "bar", + }, + want: false, + name: "Pod with an empty []NodeSelectorTerm in affinity, can't match the node's labels and won't schedule onto the node", + }, + { + name: "Pod with empty MatchExpressions is not a valid value will match no objects and won't schedule onto the node", + 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", + }, + want: false, + }, + { + name: "Pod with no Affinity will schedule onto a node", + pod: &v1.Pod{}, + labels: map[string]string{ + "foo": "bar", + }, + want: true, + }, + { + 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: nil, + }, + }, + }, + }, + labels: map[string]string{ + "foo": "bar", + }, + want: true, + }, + { + 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.NodeSelectorOpNotIn, + Values: []string{"AMD", "INTER"}, + }, + }, + }, + }, + }, + }, + }, + }, + }, + labels: map[string]string{ + "GPU": "NVIDIA-GRID-K1", + }, + want: true, + }, + { + name: "Pod with multiple matchExpressions ANDed that doesn't match 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", + }, + want: false, + }, + { + 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{ + 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", + }, + want: true, + }, + { + 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": "bar", + }, + want: true, + }, + { + 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", + 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", + }, + want: false, + }, + { + name: "Pod with an invalid value in Affinity term won't be scheduled onto the 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.NodeSelectorOpNotIn, + Values: []string{"invalid value: ___@#$%^"}, + }, + }, + }, + }, + }, + }, + }, + }, + }, + labels: map[string]string{ + "foo": "bar", + }, + want: false, + }, + { + 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: metav1.ObjectNameField, + Operator: v1.NodeSelectorOpIn, + Values: []string{"node_1"}, + }, + }, + }, + }, + }, + }, + }, + }, + }, + nodeName: "node_1", + want: true, + }, + { + name: "Pod with matchFields using In operator that does not match 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: metav1.ObjectNameField, + Operator: v1.NodeSelectorOpIn, + Values: []string{"node_1"}, + }, + }, + }, + }, + }, + }, + }, + }, + }, + nodeName: "node_2", + want: false, + }, + { + 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: metav1.ObjectNameField, + 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"}, + want: true, + }, + { + name: "Pod with one term: 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: metav1.ObjectNameField, + 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"}, + want: false, + }, + { + 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: metav1.ObjectNameField, + 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"}, + want: true, + }, + { + name: "Pod with two terms: both matchFields and matchExpressions do not match", + pod: &v1.Pod{ + Spec: v1.PodSpec{ + Affinity: &v1.Affinity{ + NodeAffinity: &v1.NodeAffinity{ + RequiredDuringSchedulingIgnoredDuringExecution: &v1.NodeSelector{ + NodeSelectorTerms: []v1.NodeSelectorTerm{ + { + MatchFields: []v1.NodeSelectorRequirement{ + { + Key: metav1.ObjectNameField, + 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"}, + want: false, + }, + } + + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + node := v1.Node{ObjectMeta: metav1.ObjectMeta{ + Name: test.nodeName, + Labels: test.labels, + }} + got, _ := GetRequiredNodeAffinity(test.pod).Match(&node) + if test.want != got { + t.Errorf("expected: %v got %v", test.want, got) + } + }) + } +}