From 64ff958e6931bd9e3511e4af0b1d992aa25cda70 Mon Sep 17 00:00:00 2001 From: Wei Huang Date: Mon, 14 Oct 2019 15:12:22 -0700 Subject: [PATCH] migrate EvenPodsSpread Predicate to Filter plugin --- pkg/scheduler/framework/plugins/BUILD | 2 + .../framework/plugins/default_registry.go | 13 +- .../framework/plugins/podtopologyspread/BUILD | 44 ++ .../podtopologyspread/pod_topology_spread.go | 57 +++ .../pod_topology_spread_test.go | 479 ++++++++++++++++++ 5 files changed, 591 insertions(+), 4 deletions(-) create mode 100644 pkg/scheduler/framework/plugins/podtopologyspread/BUILD create mode 100644 pkg/scheduler/framework/plugins/podtopologyspread/pod_topology_spread.go create mode 100644 pkg/scheduler/framework/plugins/podtopologyspread/pod_topology_spread_test.go diff --git a/pkg/scheduler/framework/plugins/BUILD b/pkg/scheduler/framework/plugins/BUILD index 36d44d0bca0..6b15acf3131 100644 --- a/pkg/scheduler/framework/plugins/BUILD +++ b/pkg/scheduler/framework/plugins/BUILD @@ -18,6 +18,7 @@ go_library( "//pkg/scheduler/framework/plugins/nodepreferavoidpods:go_default_library", "//pkg/scheduler/framework/plugins/noderesources:go_default_library", "//pkg/scheduler/framework/plugins/nodevolumelimits:go_default_library", + "//pkg/scheduler/framework/plugins/podtopologyspread:go_default_library", "//pkg/scheduler/framework/plugins/tainttoleration:go_default_library", "//pkg/scheduler/framework/plugins/volumebinding:go_default_library", "//pkg/scheduler/framework/plugins/volumerestrictions:go_default_library", @@ -52,6 +53,7 @@ filegroup( "//pkg/scheduler/framework/plugins/nodepreferavoidpods:all-srcs", "//pkg/scheduler/framework/plugins/noderesources:all-srcs", "//pkg/scheduler/framework/plugins/nodevolumelimits:all-srcs", + "//pkg/scheduler/framework/plugins/podtopologyspread:all-srcs", "//pkg/scheduler/framework/plugins/tainttoleration:all-srcs", "//pkg/scheduler/framework/plugins/volumebinding:all-srcs", "//pkg/scheduler/framework/plugins/volumerestrictions:all-srcs", diff --git a/pkg/scheduler/framework/plugins/default_registry.go b/pkg/scheduler/framework/plugins/default_registry.go index 329d8358376..5d16527e631 100644 --- a/pkg/scheduler/framework/plugins/default_registry.go +++ b/pkg/scheduler/framework/plugins/default_registry.go @@ -34,6 +34,7 @@ import ( "k8s.io/kubernetes/pkg/scheduler/framework/plugins/nodepreferavoidpods" "k8s.io/kubernetes/pkg/scheduler/framework/plugins/noderesources" "k8s.io/kubernetes/pkg/scheduler/framework/plugins/nodevolumelimits" + "k8s.io/kubernetes/pkg/scheduler/framework/plugins/podtopologyspread" "k8s.io/kubernetes/pkg/scheduler/framework/plugins/tainttoleration" "k8s.io/kubernetes/pkg/scheduler/framework/plugins/volumebinding" "k8s.io/kubernetes/pkg/scheduler/framework/plugins/volumerestrictions" @@ -73,6 +74,7 @@ func NewDefaultRegistry(args *RegistryArgs) framework.Registry { nodeports.Name: nodeports.New, nodepreferavoidpods.Name: nodepreferavoidpods.New, nodeaffinity.Name: nodeaffinity.New, + podtopologyspread.Name: podtopologyspread.New, volumebinding.Name: func(_ *runtime.Unknown, _ framework.FrameworkHandle) (framework.Plugin, error) { return volumebinding.NewFromVolumeBinder(args.VolumeBinder), nil }, @@ -113,6 +115,7 @@ func NewDefaultConfigProducerRegistry() *ConfigProducerRegistry { PredicateToConfigProducer: make(map[string]ConfigProducer), PriorityToConfigProducer: make(map[string]ConfigProducer), } + // Register Predicates. registry.RegisterPredicate(predicates.GeneralPred, func(_ ConfigProducerArgs) (plugins config.Plugins, pluginConfig []config.PluginConfig) { // GeneralPredicate is a combination of predicates. @@ -172,25 +175,27 @@ func NewDefaultConfigProducerRegistry() *ConfigProducerRegistry { plugins.Filter = appendToPluginSet(plugins.Filter, interpodaffinity.Name, nil) return }) - + registry.RegisterPredicate(predicates.EvenPodsSpreadPred, + func(_ ConfigProducerArgs) (plugins config.Plugins, pluginConfig []config.PluginConfig) { + plugins.Filter = appendToPluginSet(plugins.Filter, podtopologyspread.Name, nil) + return + }) + // Register Priorities. registry.RegisterPriority(priorities.TaintTolerationPriority, func(args ConfigProducerArgs) (plugins config.Plugins, pluginConfig []config.PluginConfig) { plugins.Score = appendToPluginSet(plugins.Score, tainttoleration.Name, &args.Weight) return }) - registry.RegisterPriority(priorities.NodeAffinityPriority, func(args ConfigProducerArgs) (plugins config.Plugins, pluginConfig []config.PluginConfig) { plugins.Score = appendToPluginSet(plugins.Score, nodeaffinity.Name, &args.Weight) return }) - registry.RegisterPriority(priorities.ImageLocalityPriority, func(args ConfigProducerArgs) (plugins config.Plugins, pluginConfig []config.PluginConfig) { plugins.Score = appendToPluginSet(plugins.Score, imagelocality.Name, &args.Weight) return }) - registry.RegisterPriority(priorities.NodePreferAvoidPodsPriority, func(args ConfigProducerArgs) (plugins config.Plugins, pluginConfig []config.PluginConfig) { plugins.Score = appendToPluginSet(plugins.Score, nodepreferavoidpods.Name, &args.Weight) diff --git a/pkg/scheduler/framework/plugins/podtopologyspread/BUILD b/pkg/scheduler/framework/plugins/podtopologyspread/BUILD new file mode 100644 index 00000000000..5f58cae2436 --- /dev/null +++ b/pkg/scheduler/framework/plugins/podtopologyspread/BUILD @@ -0,0 +1,44 @@ +load("@io_bazel_rules_go//go:def.bzl", "go_library", "go_test") + +go_library( + name = "go_default_library", + srcs = ["pod_topology_spread.go"], + importpath = "k8s.io/kubernetes/pkg/scheduler/framework/plugins/podtopologyspread", + 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", + ], +) + +go_test( + name = "go_default_test", + srcs = ["pod_topology_spread_test.go"], + embed = [":go_default_library"], + 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", + "//pkg/scheduler/testing:go_default_library", + "//staging/src/k8s.io/api/core/v1: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"], +) diff --git a/pkg/scheduler/framework/plugins/podtopologyspread/pod_topology_spread.go b/pkg/scheduler/framework/plugins/podtopologyspread/pod_topology_spread.go new file mode 100644 index 00000000000..9a76bbe1732 --- /dev/null +++ b/pkg/scheduler/framework/plugins/podtopologyspread/pod_topology_spread.go @@ -0,0 +1,57 @@ +/* +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 podtopologyspread + +import ( + "context" + "fmt" + + "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" +) + +// PodTopologySpread is a plugin that ensures pod's topologySpreadConstraints is satisfied. +type PodTopologySpread struct{} + +var _ framework.FilterPlugin = &PodTopologySpread{} + +// Name is the name of the plugin used in the plugin registry and configurations. +const Name = "PodTopologySpread" + +// Name returns name of the plugin. It is used in logs, etc. +func (pl *PodTopologySpread) Name() string { + return Name +} + +// Filter invoked at the filter extension point. +func (pl *PodTopologySpread) Filter(ctx context.Context, cycleState *framework.CycleState, pod *v1.Pod, nodeInfo *nodeinfo.NodeInfo) *framework.Status { + meta, ok := migration.PredicateMetadata(cycleState).(predicates.PredicateMetadata) + if !ok { + return migration.ErrorToFrameworkStatus(fmt.Errorf("%+v convert to predicates.PredicateMetadata error", cycleState)) + } + _, reasons, err := predicates.EvenPodsSpreadPredicate(pod, meta, nodeInfo) + return migration.PredicateResultToFrameworkStatus(reasons, err) +} + +// New initializes a new plugin and returns it. +func New(_ *runtime.Unknown, _ framework.FrameworkHandle) (framework.Plugin, error) { + return &PodTopologySpread{}, nil +} diff --git a/pkg/scheduler/framework/plugins/podtopologyspread/pod_topology_spread_test.go b/pkg/scheduler/framework/plugins/podtopologyspread/pod_topology_spread_test.go new file mode 100644 index 00000000000..3283639493a --- /dev/null +++ b/pkg/scheduler/framework/plugins/podtopologyspread/pod_topology_spread_test.go @@ -0,0 +1,479 @@ +/* +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 podtopologyspread + +import ( + "context" + "testing" + + "k8s.io/api/core/v1" + "k8s.io/kubernetes/pkg/scheduler/algorithm/predicates" + "k8s.io/kubernetes/pkg/scheduler/framework/plugins/migration" + "k8s.io/kubernetes/pkg/scheduler/framework/v1alpha1" + schedulernodeinfo "k8s.io/kubernetes/pkg/scheduler/nodeinfo" + st "k8s.io/kubernetes/pkg/scheduler/testing" +) + +var hardSpread = v1.DoNotSchedule + +func TestPodTopologySpreadFilter_SingleConstraint(t *testing.T) { + tests := []struct { + name string + pod *v1.Pod + nodes []*v1.Node + existingPods []*v1.Pod + fits map[string]bool + }{ + { + name: "no existing pods", + pod: st.MakePod().Name("p").Label("foo", "").SpreadConstraint( + 1, "zone", hardSpread, st.MakeLabelSelector().Exists("foo").Obj(), + ).Obj(), + nodes: []*v1.Node{ + st.MakeNode().Name("node-a").Label("zone", "zone1").Label("node", "node-a").Obj(), + st.MakeNode().Name("node-b").Label("zone", "zone1").Label("node", "node-b").Obj(), + st.MakeNode().Name("node-x").Label("zone", "zone2").Label("node", "node-x").Obj(), + st.MakeNode().Name("node-y").Label("zone", "zone2").Label("node", "node-y").Obj(), + }, + fits: map[string]bool{ + "node-a": true, + "node-b": true, + "node-x": true, + "node-y": true, + }, + }, + { + name: "no existing pods, incoming pod doesn't match itself", + pod: st.MakePod().Name("p").Label("foo", "").SpreadConstraint( + 1, "zone", hardSpread, st.MakeLabelSelector().Exists("bar").Obj(), + ).Obj(), + nodes: []*v1.Node{ + st.MakeNode().Name("node-a").Label("zone", "zone1").Label("node", "node-a").Obj(), + st.MakeNode().Name("node-b").Label("zone", "zone1").Label("node", "node-b").Obj(), + st.MakeNode().Name("node-x").Label("zone", "zone2").Label("node", "node-x").Obj(), + st.MakeNode().Name("node-y").Label("zone", "zone2").Label("node", "node-y").Obj(), + }, + fits: map[string]bool{ + "node-a": true, + "node-b": true, + "node-x": true, + "node-y": true, + }, + }, + { + name: "existing pods with mis-matched namespace doens't count", + pod: st.MakePod().Name("p").Label("foo", "").SpreadConstraint( + 1, "zone", hardSpread, st.MakeLabelSelector().Exists("foo").Obj(), + ).Obj(), + nodes: []*v1.Node{ + st.MakeNode().Name("node-a").Label("zone", "zone1").Label("node", "node-a").Obj(), + st.MakeNode().Name("node-b").Label("zone", "zone1").Label("node", "node-b").Obj(), + st.MakeNode().Name("node-x").Label("zone", "zone2").Label("node", "node-x").Obj(), + st.MakeNode().Name("node-y").Label("zone", "zone2").Label("node", "node-y").Obj(), + }, + existingPods: []*v1.Pod{ + st.MakePod().Name("p-a1").Namespace("ns1").Node("node-a").Label("foo", "").Obj(), + st.MakePod().Name("p-b1").Namespace("ns2").Node("node-a").Label("foo", "").Obj(), + st.MakePod().Name("p-x1").Node("node-x").Label("foo", "").Obj(), + st.MakePod().Name("p-y1").Node("node-y").Label("foo", "").Obj(), + }, + fits: map[string]bool{ + "node-a": true, + "node-b": true, + "node-x": false, + "node-y": false, + }, + }, + { + name: "pods spread across zones as 3/3, all nodes fit", + pod: st.MakePod().Name("p").Label("foo", "").SpreadConstraint( + 1, "zone", hardSpread, st.MakeLabelSelector().Exists("foo").Obj(), + ).Obj(), + nodes: []*v1.Node{ + st.MakeNode().Name("node-a").Label("zone", "zone1").Label("node", "node-a").Obj(), + st.MakeNode().Name("node-b").Label("zone", "zone1").Label("node", "node-b").Obj(), + st.MakeNode().Name("node-x").Label("zone", "zone2").Label("node", "node-x").Obj(), + st.MakeNode().Name("node-y").Label("zone", "zone2").Label("node", "node-y").Obj(), + }, + existingPods: []*v1.Pod{ + st.MakePod().Name("p-a1").Node("node-a").Label("foo", "").Obj(), + st.MakePod().Name("p-a2").Node("node-a").Label("foo", "").Obj(), + st.MakePod().Name("p-b1").Node("node-b").Label("foo", "").Obj(), + st.MakePod().Name("p-y1").Node("node-y").Label("foo", "").Obj(), + st.MakePod().Name("p-y2").Node("node-y").Label("foo", "").Obj(), + st.MakePod().Name("p-y3").Node("node-y").Label("foo", "").Obj(), + }, + fits: map[string]bool{ + "node-a": true, + "node-b": true, + "node-x": true, + "node-y": true, + }, + }, + { + // TODO(Huang-Wei): maybe document this to remind users that typos on node labels + // can cause unexpected behavior + name: "pods spread across zones as 1/2 due to absence of label 'zone' on node-b", + pod: st.MakePod().Name("p").Label("foo", "").SpreadConstraint( + 1, "zone", hardSpread, st.MakeLabelSelector().Exists("foo").Obj(), + ).Obj(), + nodes: []*v1.Node{ + st.MakeNode().Name("node-a").Label("zone", "zone1").Label("node", "node-a").Obj(), + st.MakeNode().Name("node-b").Label("zon", "zone1").Label("node", "node-b").Obj(), + st.MakeNode().Name("node-x").Label("zone", "zone2").Label("node", "node-x").Obj(), + st.MakeNode().Name("node-y").Label("zone", "zone2").Label("node", "node-y").Obj(), + }, + existingPods: []*v1.Pod{ + st.MakePod().Name("p-a1").Node("node-a").Label("foo", "").Obj(), + st.MakePod().Name("p-b1").Node("node-b").Label("foo", "").Obj(), + st.MakePod().Name("p-x1").Node("node-x").Label("foo", "").Obj(), + st.MakePod().Name("p-y1").Node("node-y").Label("foo", "").Obj(), + }, + fits: map[string]bool{ + "node-a": true, + "node-b": false, + "node-x": false, + "node-y": false, + }, + }, + { + name: "pods spread across nodes as 2/1/0/3, only node-x fits", + pod: st.MakePod().Name("p").Label("foo", "").SpreadConstraint( + 1, "node", hardSpread, st.MakeLabelSelector().Exists("foo").Obj(), + ).Obj(), + nodes: []*v1.Node{ + st.MakeNode().Name("node-a").Label("zone", "zone1").Label("node", "node-a").Obj(), + st.MakeNode().Name("node-b").Label("zone", "zone1").Label("node", "node-b").Obj(), + st.MakeNode().Name("node-x").Label("zone", "zone2").Label("node", "node-x").Obj(), + st.MakeNode().Name("node-y").Label("zone", "zone2").Label("node", "node-y").Obj(), + }, + existingPods: []*v1.Pod{ + st.MakePod().Name("p-a1").Node("node-a").Label("foo", "").Obj(), + st.MakePod().Name("p-a2").Node("node-a").Label("foo", "").Obj(), + st.MakePod().Name("p-b1").Node("node-b").Label("foo", "").Obj(), + st.MakePod().Name("p-y1").Node("node-y").Label("foo", "").Obj(), + st.MakePod().Name("p-y2").Node("node-y").Label("foo", "").Obj(), + st.MakePod().Name("p-y3").Node("node-y").Label("foo", "").Obj(), + }, + fits: map[string]bool{ + "node-a": false, + "node-b": false, + "node-x": true, + "node-y": false, + }, + }, + { + name: "pods spread across nodes as 2/1/0/3, maxSkew is 2, node-b and node-x fit", + pod: st.MakePod().Name("p").Label("foo", "").SpreadConstraint( + 2, "node", hardSpread, st.MakeLabelSelector().Exists("foo").Obj(), + ).Obj(), + nodes: []*v1.Node{ + st.MakeNode().Name("node-a").Label("zone", "zone1").Label("node", "node-a").Obj(), + st.MakeNode().Name("node-b").Label("zone", "zone1").Label("node", "node-b").Obj(), + st.MakeNode().Name("node-x").Label("zone", "zone2").Label("node", "node-x").Obj(), + st.MakeNode().Name("node-y").Label("zone", "zone2").Label("node", "node-y").Obj(), + }, + existingPods: []*v1.Pod{ + st.MakePod().Name("p-a1").Node("node-a").Label("foo", "").Obj(), + st.MakePod().Name("p-a2").Node("node-a").Label("foo", "").Obj(), + st.MakePod().Name("p-b1").Node("node-b").Label("foo", "").Obj(), + st.MakePod().Name("p-y1").Node("node-y").Label("foo", "").Obj(), + st.MakePod().Name("p-y2").Node("node-y").Label("foo", "").Obj(), + st.MakePod().Name("p-y3").Node("node-y").Label("foo", "").Obj(), + }, + fits: map[string]bool{ + "node-a": false, + "node-b": true, + "node-x": true, + "node-y": false, + }, + }, + { + // not a desired case, but it can happen + // TODO(Huang-Wei): document this "pod-not-match-itself" case + // in this case, placement of the new pod doesn't change pod distribution of the cluster + // as the incoming pod doesn't have label "foo" + name: "pods spread across nodes as 2/1/0/3, but pod doesn't match itself", + pod: st.MakePod().Name("p").Label("bar", "").SpreadConstraint( + 1, "node", hardSpread, st.MakeLabelSelector().Exists("foo").Obj(), + ).Obj(), + nodes: []*v1.Node{ + st.MakeNode().Name("node-a").Label("zone", "zone1").Label("node", "node-a").Obj(), + st.MakeNode().Name("node-b").Label("zone", "zone1").Label("node", "node-b").Obj(), + st.MakeNode().Name("node-x").Label("zone", "zone2").Label("node", "node-x").Obj(), + st.MakeNode().Name("node-y").Label("zone", "zone2").Label("node", "node-y").Obj(), + }, + existingPods: []*v1.Pod{ + st.MakePod().Name("p-a1").Node("node-a").Label("foo", "").Obj(), + st.MakePod().Name("p-a2").Node("node-a").Label("foo", "").Obj(), + st.MakePod().Name("p-b1").Node("node-b").Label("foo", "").Obj(), + st.MakePod().Name("p-y1").Node("node-y").Label("foo", "").Obj(), + st.MakePod().Name("p-y2").Node("node-y").Label("foo", "").Obj(), + st.MakePod().Name("p-y3").Node("node-y").Label("foo", "").Obj(), + }, + fits: map[string]bool{ + "node-a": false, + "node-b": true, + "node-x": true, + "node-y": false, + }, + }, + { + // only node-a and node-y are considered, so pods spread as 2/~1~/~0~/3 + // ps: '~num~' is a markdown symbol to denote a crossline through 'num' + // but in this unit test, we don't run NodeAffinityPredicate, so node-b and node-x are + // still expected to be fits; + // the fact that node-a fits can prove the underlying logic works + name: "incoming pod has nodeAffinity, pods spread as 2/~1~/~0~/3, hence node-a fits", + pod: st.MakePod().Name("p").Label("foo", ""). + NodeAffinityIn("node", []string{"node-a", "node-y"}). + SpreadConstraint(1, "node", hardSpread, st.MakeLabelSelector().Exists("foo").Obj()). + Obj(), + nodes: []*v1.Node{ + st.MakeNode().Name("node-a").Label("zone", "zone1").Label("node", "node-a").Obj(), + st.MakeNode().Name("node-b").Label("zone", "zone1").Label("node", "node-b").Obj(), + st.MakeNode().Name("node-x").Label("zone", "zone2").Label("node", "node-x").Obj(), + st.MakeNode().Name("node-y").Label("zone", "zone2").Label("node", "node-y").Obj(), + }, + existingPods: []*v1.Pod{ + st.MakePod().Name("p-a1").Node("node-a").Label("foo", "").Obj(), + st.MakePod().Name("p-a2").Node("node-a").Label("foo", "").Obj(), + st.MakePod().Name("p-b1").Node("node-b").Label("foo", "").Obj(), + st.MakePod().Name("p-y1").Node("node-y").Label("foo", "").Obj(), + st.MakePod().Name("p-y2").Node("node-y").Label("foo", "").Obj(), + st.MakePod().Name("p-y3").Node("node-y").Label("foo", "").Obj(), + }, + fits: map[string]bool{ + "node-a": true, + "node-b": true, // in real case, it's false + "node-x": true, // in real case, it's false + "node-y": false, + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + nodeInfoMap := schedulernodeinfo.CreateNodeNameToInfoMap(tt.existingPods, tt.nodes) + meta := predicates.GetPredicateMetadata(tt.pod, nodeInfoMap) + state := v1alpha1.NewCycleState() + state.Write(migration.PredicatesStateKey, &migration.PredicatesStateData{Reference: meta}) + plugin, _ := New(nil, nil) + for _, node := range tt.nodes { + status := plugin.(*PodTopologySpread).Filter(context.Background(), state, tt.pod, nodeInfoMap[node.Name]) + if status.IsSuccess() != tt.fits[node.Name] { + t.Errorf("[%s]: expected %v got %v", node.Name, tt.fits[node.Name], status.IsSuccess()) + } + } + }) + } +} + +func TestPodTopologySpreadFilter_MultipleConstraints(t *testing.T) { + tests := []struct { + name string + pod *v1.Pod + nodes []*v1.Node + existingPods []*v1.Pod + fits map[string]bool + }{ + { + // 1. to fulfil "zone" constraint, incoming pod can be placed on any zone (hence any node) + // 2. to fulfil "node" constraint, incoming pod can be placed on node-x + // intersection of (1) and (2) returns node-x + name: "two constraints on zone and node, spreads = [3/3, 2/1/0/3]", + pod: st.MakePod().Name("p").Label("foo", ""). + SpreadConstraint(1, "zone", hardSpread, st.MakeLabelSelector().Exists("foo").Obj()). + SpreadConstraint(1, "node", hardSpread, st.MakeLabelSelector().Exists("foo").Obj()). + Obj(), + nodes: []*v1.Node{ + st.MakeNode().Name("node-a").Label("zone", "zone1").Label("node", "node-a").Obj(), + st.MakeNode().Name("node-b").Label("zone", "zone1").Label("node", "node-b").Obj(), + st.MakeNode().Name("node-x").Label("zone", "zone2").Label("node", "node-x").Obj(), + st.MakeNode().Name("node-y").Label("zone", "zone2").Label("node", "node-y").Obj(), + }, + existingPods: []*v1.Pod{ + st.MakePod().Name("p-a1").Node("node-a").Label("foo", "").Obj(), + st.MakePod().Name("p-a2").Node("node-a").Label("foo", "").Obj(), + st.MakePod().Name("p-b1").Node("node-b").Label("foo", "").Obj(), + st.MakePod().Name("p-y1").Node("node-y").Label("foo", "").Obj(), + st.MakePod().Name("p-y2").Node("node-y").Label("foo", "").Obj(), + st.MakePod().Name("p-y3").Node("node-y").Label("foo", "").Obj(), + }, + fits: map[string]bool{ + "node-a": false, + "node-b": false, + "node-x": true, + "node-y": false, + }, + }, + { + // 1. to fulfil "zone" constraint, incoming pod can be placed on zone1 (node-a or node-b) + // 2. to fulfil "node" constraint, incoming pod can be placed on node-x + // intersection of (1) and (2) returns no node + name: "two constraints on zone and node, spreads = [3/4, 2/1/0/4]", + pod: st.MakePod().Name("p").Label("foo", ""). + SpreadConstraint(1, "zone", hardSpread, st.MakeLabelSelector().Exists("foo").Obj()). + SpreadConstraint(1, "node", hardSpread, st.MakeLabelSelector().Exists("foo").Obj()). + Obj(), + nodes: []*v1.Node{ + st.MakeNode().Name("node-a").Label("zone", "zone1").Label("node", "node-a").Obj(), + st.MakeNode().Name("node-b").Label("zone", "zone1").Label("node", "node-b").Obj(), + st.MakeNode().Name("node-x").Label("zone", "zone2").Label("node", "node-x").Obj(), + st.MakeNode().Name("node-y").Label("zone", "zone2").Label("node", "node-y").Obj(), + }, + existingPods: []*v1.Pod{ + st.MakePod().Name("p-a1").Node("node-a").Label("foo", "").Obj(), + st.MakePod().Name("p-a2").Node("node-a").Label("foo", "").Obj(), + st.MakePod().Name("p-b1").Node("node-b").Label("foo", "").Obj(), + st.MakePod().Name("p-y1").Node("node-y").Label("foo", "").Obj(), + st.MakePod().Name("p-y2").Node("node-y").Label("foo", "").Obj(), + st.MakePod().Name("p-y3").Node("node-y").Label("foo", "").Obj(), + st.MakePod().Name("p-y4").Node("node-y").Label("foo", "").Obj(), + }, + fits: map[string]bool{ + "node-a": false, + "node-b": false, + "node-x": false, + "node-y": false, + }, + }, + { + // 1. to fulfil "zone" constraint, incoming pod can be placed on zone2 (node-x or node-y) + // 2. to fulfil "node" constraint, incoming pod can be placed on node-b or node-x + // intersection of (1) and (2) returns node-x + name: "constraints hold different labelSelectors, spreads = [1/0, 1/0/0/1]", + pod: st.MakePod().Name("p").Label("foo", "").Label("bar", ""). + SpreadConstraint(1, "zone", hardSpread, st.MakeLabelSelector().Exists("foo").Obj()). + SpreadConstraint(1, "node", hardSpread, st.MakeLabelSelector().Exists("bar").Obj()). + Obj(), + nodes: []*v1.Node{ + st.MakeNode().Name("node-a").Label("zone", "zone1").Label("node", "node-a").Obj(), + st.MakeNode().Name("node-b").Label("zone", "zone1").Label("node", "node-b").Obj(), + st.MakeNode().Name("node-x").Label("zone", "zone2").Label("node", "node-x").Obj(), + st.MakeNode().Name("node-y").Label("zone", "zone2").Label("node", "node-y").Obj(), + }, + existingPods: []*v1.Pod{ + st.MakePod().Name("p-a1").Node("node-a").Label("foo", "").Obj(), + st.MakePod().Name("p-y1").Node("node-y").Label("bar", "").Obj(), + }, + fits: map[string]bool{ + "node-a": false, + "node-b": false, + "node-x": true, + "node-y": false, + }, + }, + { + // 1. to fulfil "zone" constraint, incoming pod can be placed on zone2 (node-x or node-y) + // 2. to fulfil "node" constraint, incoming pod can be placed on node-a or node-b + // intersection of (1) and (2) returns no node + name: "constraints hold different labelSelectors, spreads = [1/0, 0/0/1/1]", + pod: st.MakePod().Name("p").Label("foo", "").Label("bar", ""). + SpreadConstraint(1, "zone", hardSpread, st.MakeLabelSelector().Exists("foo").Obj()). + SpreadConstraint(1, "node", hardSpread, st.MakeLabelSelector().Exists("bar").Obj()). + Obj(), + nodes: []*v1.Node{ + st.MakeNode().Name("node-a").Label("zone", "zone1").Label("node", "node-a").Obj(), + st.MakeNode().Name("node-b").Label("zone", "zone1").Label("node", "node-b").Obj(), + st.MakeNode().Name("node-x").Label("zone", "zone2").Label("node", "node-x").Obj(), + st.MakeNode().Name("node-y").Label("zone", "zone2").Label("node", "node-y").Obj(), + }, + existingPods: []*v1.Pod{ + st.MakePod().Name("p-a1").Node("node-a").Label("foo", "").Obj(), + st.MakePod().Name("p-x1").Node("node-x").Label("bar", "").Obj(), + st.MakePod().Name("p-y1").Node("node-y").Label("bar", "").Obj(), + }, + fits: map[string]bool{ + "node-a": false, + "node-b": false, + "node-x": false, + "node-y": false, + }, + }, + { + // 1. to fulfil "zone" constraint, incoming pod can be placed on zone1 (node-a or node-b) + // 2. to fulfil "node" constraint, incoming pod can be placed on node-b or node-x + // intersection of (1) and (2) returns node-b + name: "constraints hold different labelSelectors, spreads = [2/3, 1/0/0/1]", + pod: st.MakePod().Name("p").Label("foo", "").Label("bar", ""). + SpreadConstraint(1, "zone", hardSpread, st.MakeLabelSelector().Exists("foo").Obj()). + SpreadConstraint(1, "node", hardSpread, st.MakeLabelSelector().Exists("bar").Obj()). + Obj(), + nodes: []*v1.Node{ + st.MakeNode().Name("node-a").Label("zone", "zone1").Label("node", "node-a").Obj(), + st.MakeNode().Name("node-b").Label("zone", "zone1").Label("node", "node-b").Obj(), + st.MakeNode().Name("node-x").Label("zone", "zone2").Label("node", "node-x").Obj(), + st.MakeNode().Name("node-y").Label("zone", "zone2").Label("node", "node-y").Obj(), + }, + existingPods: []*v1.Pod{ + st.MakePod().Name("p-a1").Node("node-a").Label("foo", "").Obj(), + st.MakePod().Name("p-a2").Node("node-a").Label("foo", "").Label("bar", "").Obj(), + st.MakePod().Name("p-y1").Node("node-y").Label("foo", "").Obj(), + st.MakePod().Name("p-y2").Node("node-y").Label("foo", "").Label("bar", "").Obj(), + st.MakePod().Name("p-y3").Node("node-y").Label("foo", "").Obj(), + }, + fits: map[string]bool{ + "node-a": false, + "node-b": true, + "node-x": false, + "node-y": false, + }, + }, + { + // 1. pod doesn't match itself on "zone" constraint, so it can be put onto any zone + // 2. to fulfil "node" constraint, incoming pod can be placed on node-a or node-b + // intersection of (1) and (2) returns node-a and node-b + name: "constraints hold different labelSelectors but pod doesn't match itself on 'zone' constraint", + pod: st.MakePod().Name("p").Label("bar", ""). + SpreadConstraint(1, "zone", hardSpread, st.MakeLabelSelector().Exists("foo").Obj()). + SpreadConstraint(1, "node", hardSpread, st.MakeLabelSelector().Exists("bar").Obj()). + Obj(), + nodes: []*v1.Node{ + st.MakeNode().Name("node-a").Label("zone", "zone1").Label("node", "node-a").Obj(), + st.MakeNode().Name("node-b").Label("zone", "zone1").Label("node", "node-b").Obj(), + st.MakeNode().Name("node-x").Label("zone", "zone2").Label("node", "node-x").Obj(), + st.MakeNode().Name("node-y").Label("zone", "zone2").Label("node", "node-y").Obj(), + }, + existingPods: []*v1.Pod{ + st.MakePod().Name("p-a1").Node("node-a").Label("foo", "").Obj(), + st.MakePod().Name("p-x1").Node("node-x").Label("bar", "").Obj(), + st.MakePod().Name("p-y1").Node("node-y").Label("bar", "").Obj(), + }, + fits: map[string]bool{ + "node-a": true, + "node-b": true, + "node-x": false, + "node-y": false, + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + nodeInfoMap := schedulernodeinfo.CreateNodeNameToInfoMap(tt.existingPods, tt.nodes) + meta := predicates.GetPredicateMetadata(tt.pod, nodeInfoMap) + state := v1alpha1.NewCycleState() + state.Write(migration.PredicatesStateKey, &migration.PredicatesStateData{Reference: meta}) + plugin, _ := New(nil, nil) + for _, node := range tt.nodes { + status := plugin.(*PodTopologySpread).Filter(context.Background(), state, tt.pod, nodeInfoMap[node.Name]) + if status.IsSuccess() != tt.fits[node.Name] { + t.Errorf("[%s]: expected %v got %v", node.Name, tt.fits[node.Name], status.IsSuccess()) + } + } + }) + } +}