Repository: clusternet/clusternet
Pull request #352 (2022-05-27 09:58:32)

fix resource controller run more than one
Signed-off-by: lmxia <xialingming@gmail.com>

<!--  Thanks for sending a pull request!

-->

#### What type of PR is this?
kind/bug

#### What this PR does / why we need it:
resource controller should be added before it runes otherwise it will never be added. And controller cache won't work.
#### Which issue(s) this PR fixes:
<!--
*Automatically closes linked issue when PR is merged.
Usage: `Fixes #<issue number>`, or `Fixes (paste link of issue)`.
-->
Fixes #

#### Special notes for your reviewer:


codecov-commenter: # [Codecov](https://codecov.io/gh/clusternet/clusternet/pull/352?src=pr&el=h1&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=clusternet) Report
> Merging [#352](https://codecov.io/gh/clusternet/clusternet/pull/352?src=pr&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=clusternet) (785902c) into [main](https://codecov.io/gh/clusternet/clusternet/commit/c675d8dcf302d0ac7f5511d2eca1b3102d4a0edd?el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=clusternet) (c675d8d) will **not change** coverage.
> The diff coverage is `n/a`.

```diff
@@           Coverage Diff           @@
##             main     #352   +/-   ##
=======================================
  Coverage   12.48%   12.48%           
=======================================
  Files          47       47           
  Lines        4861     4861           
=======================================
  Hits          607      607           
  Misses       4210     4210           
  Partials       44       44           
```


| [Impacted Files](https://codecov.io/gh/clusternet/clusternet/pull/352?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=clusternet) | Coverage Δ | |
|---|---|---|
| [...ernet/clusternet/pkg/hub/apiserver/shadow/utils.go](https://codecov.io/gh/clusternet/clusternet/pull/352/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=clusternet#diff-c3JjL2dpdGh1Yi5jb20vY2x1c3Rlcm5ldC9jbHVzdGVybmV0L3BrZy9odWIvYXBpc2VydmVyL3NoYWRvdy91dGlscy5nbw==) | `89.28% <0.00%> (ø)` | |

------

[Continue to review full report at Codecov](https://codecov.io/gh/clusternet/clusternet/pull/352?src=pr&el=continue&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=clusternet).
> **Legend** - [Click here to learn more](https://docs.codecov.io/docs/codecov-delta?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=clusternet)
> `Δ = absolute <relative> (impact)`, `ø = not affected`, `? = missing data`
> Powered by [Codecov](https://codecov.io/gh/clusternet/clusternet/pull/352?src=pr&el=footer&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=clusternet). Last update [c675d8d...785902c](https://codecov.io/gh/clusternet/clusternet/pull/352?src=pr&el=lastupdated&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=clusternet). Read the [comment docs](https://docs.codecov.io/docs/pull-request-comments?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=clusternet).


Patch:
diff --git a/pkg/agent/deployer/generic/generic.go b/pkg/agent/deployer/generic/generic.go
index df5730d..3c14e40 100644
--- a/pkg/agent/deployer/generic/generic.go
+++ b/pkg/agent/deployer/generic/generic.go
@@ -167,10 +167,10 @@ func (deployer *Deployer) ResourceCallbackHandler(resource *unstructured.Unstruc
 			return err
 		}
 
-		//TODO when to recycle resource controller or make they live forever as resource controller cache?
+		//DO NOT recycle resource controller so they live forever as resource controller cache
+		deployer.AddController(gvk, resourceController)
 		stopChan := make(chan struct{})
 		resourceController.Run(known.DefaultThreadiness, stopChan)
-		deployer.AddController(gvk, resourceController)
 	}
 	return nil
 }
diff --git a/pkg/predictor/default_plugins.go b/pkg/predictor/default_plugins.go
new file mode 100644
index 0000000..6e27b93
--- /dev/null
+++ b/pkg/predictor/default_plugins.go
@@ -0,0 +1,53 @@
+/*
+Copyright 2022 The Clusternet 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 predictor
+
+import (
+	predictorapis "github.com/clusternet/clusternet/pkg/predictor/apis"
+	"github.com/clusternet/clusternet/pkg/predictor/framework/plugins/names"
+)
+
+// getDefaultPlugins returns the default set of plugins.
+func getDefaultPlugins() *predictorapis.Plugins {
+	return &predictorapis.Plugins{
+		PreFilter: predictorapis.PluginSet{},
+		Filter: predictorapis.PluginSet{
+			Enabled: []predictorapis.Plugin{
+				{Name: names.TaintToleration},
+			},
+		},
+		PostFilter: predictorapis.PluginSet{},
+		PreCompute: predictorapis.PluginSet{},
+		Compute: predictorapis.PluginSet{
+			Enabled: []predictorapis.Plugin{
+				{Name: names.DefaultComputer},
+			},
+		},
+		PreScore: predictorapis.PluginSet{},
+		Score: predictorapis.PluginSet{
+			Enabled: []predictorapis.Plugin{
+				{Name: names.TaintToleration, Weight: 3},
+			},
+		},
+		PreAggregate: predictorapis.PluginSet{},
+		Aggregate: predictorapis.PluginSet{
+			Enabled: []predictorapis.Plugin{
+				{Name: names.DefaultAggregator},
+			},
+		},
+	}
+}
diff --git a/pkg/predictor/framework/plugins/defaultaggregate/default_aggregate.go b/pkg/predictor/framework/plugins/defaultaggregate/default_aggregate.go
new file mode 100644
index 0000000..e0b6592
--- /dev/null
+++ b/pkg/predictor/framework/plugins/defaultaggregate/default_aggregate.go
@@ -0,0 +1,59 @@
+/*
+Copyright 2022 The Clusternet 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 defaultaggregate
+
+import (
+	"context"
+
+	"k8s.io/apimachinery/pkg/runtime"
+	"k8s.io/klog/v2"
+
+	appsapi "github.com/clusternet/clusternet/pkg/apis/apps/v1alpha1"
+	framework "github.com/clusternet/clusternet/pkg/predictor/framework/interfaces"
+	"github.com/clusternet/clusternet/pkg/predictor/framework/plugins/names"
+)
+
+// DefaultAggregator aggregate replicas all.
+type DefaultAggregator struct {
+	handle framework.Handle
+}
+
+var _ framework.AggregatePlugin = &DefaultAggregator{}
+
+// New creates a DefaultAssigner.
+func New(_ runtime.Object, handle framework.Handle) (framework.Plugin, error) {
+	return &DefaultAggregator{handle: handle}, nil
+}
+
+// Name returns the name of the plugin.
+func (pl *DefaultAggregator) Name() string {
+	return names.DefaultAggregator
+}
+
+// Aggregate aggregate acceptable replicas for all filtered node
+func (pl *DefaultAggregator) Aggregate(ctx context.Context, requirements *appsapi.ReplicaRequirements, scores framework.NodeScoreList) (framework.AcceptableReplicas, *framework.Status) {
+	klog.V(5).InfoS("Attempting to Aggregate replicas")
+	result := make(framework.AcceptableReplicas)
+	maxAcceptableReplicas := int32(0)
+	for i := range scores {
+		maxAcceptableReplicas += scores[i].MaxAvailableReplicas
+	}
+	// Todo : support topology-aware replicas
+	result[framework.DefaultAcceptableReplicasKey] = maxAcceptableReplicas
+
+	return result, nil
+}
diff --git a/pkg/predictor/framework/plugins/defaultcompute/default_compute.go b/pkg/predictor/framework/plugins/defaultcompute/default_compute.go
new file mode 100644
index 0000000..78e4655
--- /dev/null
+++ b/pkg/predictor/framework/plugins/defaultcompute/default_compute.go
@@ -0,0 +1,71 @@
+/*
+Copyright 2022 The Clusternet 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 defaultcompute
+
+import (
+	"context"
+
+	corev1 "k8s.io/api/core/v1"
+	"k8s.io/apimachinery/pkg/runtime"
+	"k8s.io/klog/v2"
+
+	appsapi "github.com/clusternet/clusternet/pkg/apis/apps/v1alpha1"
+	framework "github.com/clusternet/clusternet/pkg/predictor/framework/interfaces"
+	"github.com/clusternet/clusternet/pkg/predictor/framework/plugins/names"
+	"github.com/clusternet/clusternet/pkg/utils"
+)
+
+// DefaultComputer compute replicas according to the resource.
+type DefaultComputer struct {
+	handle framework.Handle
+}
+
+var _ framework.ComputePlugin = &DefaultComputer{}
+
+// New creates a DefaultAssigner.
+func New(_ runtime.Object, handle framework.Handle) (framework.Plugin, error) {
+	return &DefaultComputer{handle: handle}, nil
+}
+
+// Name returns the name of the plugin.
+func (pl *DefaultComputer) Name() string {
+	return names.DefaultComputer
+}
+
+// Compute compute acceptable replicas for each filtered node
+func (pl *DefaultComputer) Compute(ctx context.Context, requirements *appsapi.ReplicaRequirements, nodeInfo *framework.NodeInfo) (int32, *framework.Status) {
+	klog.V(5).InfoS("Attempting to compute replicas in nodes", "name :", nodeInfo.Node().Name)
+	allocatableResource := utils.NewResource(nodeInfo.Node().Status.Allocatable)
+
+	// occupied
+	occupiedResource := utils.EmptyResource()
+	occupiedPodNum := 0
+	for _, pod := range nodeInfo.Pods {
+		if pod.Status.Phase == corev1.PodSucceeded || pod.Status.Phase == corev1.PodFailed {
+			continue
+		}
+		occupiedResource.AddPodRequest(&pod.Spec)
+		occupiedPodNum++
+	}
+	occupiedResource.AddResourcePods(int64(occupiedPodNum))
+
+	if err := allocatableResource.Sub(occupiedResource.ResourceList()); err != nil {
+		return 0, framework.NewStatus(framework.Error, err.Error())
+	}
+
+	return int32(allocatableResource.MaxReplicaDivided(requirements.Resources.Requests)), nil
+}
diff --git a/pkg/predictor/framework/plugins/helper/normalize_score.go b/pkg/predictor/framework/plugins/helper/normalize_score.go
new file mode 100644
index 0000000..d606e1c
--- /dev/null
+++ b/pkg/predictor/framework/plugins/helper/normalize_score.go
@@ -0,0 +1,56 @@
+/*
+Copyright 2022 The Clusternet 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.
+*/
+
+// This file was copied from k8s.io/kubernetes/pkg/scheduler/framework/plugins/helper/normalize_score.go and modified.
+
+package helper
+
+import (
+	framework "github.com/clusternet/clusternet/pkg/predictor/framework/interfaces"
+)
+
+// DefaultNormalizeScore generates a Normalize Score function that can normalize the
+// scores to [0, maxPriority]. If reverse is set to true, it reverses the scores by
+// subtracting it from maxPriority.
+func DefaultNormalizeScore(maxPriority int64, reverse bool, scores framework.NodeScoreList) *framework.Status {
+	var maxCount int64
+	for i := range scores {
+		if scores[i].Score > maxCount {
+			maxCount = scores[i].Score
+		}
+	}
+
+	if maxCount == 0 {
+		if reverse {
+			for i := range scores {
+				scores[i].Score = maxPriority
+			}
+		}
+		return nil
+	}
+
+	for i := range scores {
+		score := scores[i].Score
+
+		score = maxPriority * score / maxCount
+		if reverse {
+			score = maxPriority - score
+		}
+
+		scores[i].Score = score
+	}
+	return nil
+}
diff --git a/pkg/predictor/framework/plugins/names/names.go b/pkg/predictor/framework/plugins/names/names.go
new file mode 100644
index 0000000..041af77
--- /dev/null
+++ b/pkg/predictor/framework/plugins/names/names.go
@@ -0,0 +1,27 @@
+/*
+Copyright 2022 The Clusternet 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 names
+
+const (
+	// DefaultComputer compute the max acceptable replicas of each node according to the resource
+	DefaultComputer = "DefaultComputer"
+
+	// DefaultAggregator aggregate the max acceptable replicas of all filtered nodes
+	DefaultAggregator = "DefaultAggregator"
+
+	TaintToleration = "TaintToleration"
+)
diff --git a/pkg/predictor/framework/plugins/registry.go b/pkg/predictor/framework/plugins/registry.go
new file mode 100644
index 0000000..247b86f
--- /dev/null
+++ b/pkg/predictor/framework/plugins/registry.go
@@ -0,0 +1,34 @@
+/*
+Copyright 2022 The Clusternet 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 plugins
+
+import (
+	"github.com/clusternet/clusternet/pkg/predictor/framework/plugins/defaultaggregate"
+	"github.com/clusternet/clusternet/pkg/predictor/framework/plugins/defaultcompute"
+	"github.com/clusternet/clusternet/pkg/predictor/framework/plugins/names"
+	"github.com/clusternet/clusternet/pkg/predictor/framework/plugins/tainttoleration"
+	"github.com/clusternet/clusternet/pkg/predictor/framework/runtime"
+)
+
+// NewInTreeRegistry builds the registry with all the in-tree plugins.
+func NewInTreeRegistry() runtime.Registry {
+	return runtime.Registry{
+		names.DefaultAggregator: defaultaggregate.New,
+		names.DefaultComputer:   defaultcompute.New,
+		names.TaintToleration:   tainttoleration.New,
+	}
+}
diff --git a/pkg/predictor/framework/plugins/tainttoleration/taint_toleration.go b/pkg/predictor/framework/plugins/tainttoleration/taint_toleration.go
new file mode 100644
index 0000000..c832f3f
--- /dev/null
+++ b/pkg/predictor/framework/plugins/tainttoleration/taint_toleration.go
@@ -0,0 +1,117 @@
+/*
+Copyright 2022 The Clusternet 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.
+*/
+
+// This file was copied from k8s.io/kubernetes/pkg/scheduler/framework/plugins/tainttoleration/taint_toleration.go and modified.
+
+package tainttoleration
+
+import (
+	"context"
+	"fmt"
+
+	v1 "k8s.io/api/core/v1"
+	"k8s.io/apimachinery/pkg/runtime"
+	v1helper "k8s.io/component-helpers/scheduling/corev1"
+
+	appsapi "github.com/clusternet/clusternet/pkg/apis/apps/v1alpha1"
+	framework "github.com/clusternet/clusternet/pkg/predictor/framework/interfaces"
+	"github.com/clusternet/clusternet/pkg/predictor/framework/plugins/helper"
+	"github.com/clusternet/clusternet/pkg/predictor/framework/plugins/names"
+)
+
+// TaintToleration is a plugin that checks if a pod tolerates a node's taints.
+type TaintToleration struct {
+	handle framework.Handle
+}
+
+var _ framework.FilterPlugin = &TaintToleration{}
+var _ framework.ScorePlugin = &TaintToleration{}
+
+// Name returns name of the plugin. It is used in logs, etc.
+func (pl *TaintToleration) Name() string {
+	return names.TaintToleration
+}
+
+// Filter invoked at the filter extension point.
+func (pl *TaintToleration) Filter(ctx context.Context, requirements *appsapi.ReplicaRequirements, nodeInfo *framework.NodeInfo) *framework.Status {
+	if nodeInfo == nil || nodeInfo.Node() == nil {
+		return framework.AsStatus(fmt.Errorf("invalid nodeInfo"))
+	}
+
+	filterPredicate := func(t *v1.Taint) bool {
+		// PodToleratesNodeTaints is only interested in NoSchedule and NoExecute taints.
+		return t.Effect == v1.TaintEffectNoSchedule || t.Effect == v1.TaintEffectNoExecute
+	}
+
+	taint, isUntolerated := v1helper.FindMatchingUntoleratedTaint(nodeInfo.Node().Spec.Taints, requirements.Tolerations, filterPredicate)
+	if !isUntolerated {
+		return nil
+	}
+
+	errReason := fmt.Sprintf("node(s) had untolerated taint {%s: %s}", taint.Key, taint.Value)
+	return framework.NewStatus(framework.Unpredictable, errReason)
+}
+
+// getAllTolerationEffectPreferNoSchedule gets the list of all Tolerations with Effect PreferNoSchedule or with no effect.
+func getAllTolerationPreferNoSchedule(tolerations []v1.Toleration) (tolerationList []v1.Toleration) {
+	for _, toleration := range tolerations {
+		// Empty effect means all effects which includes PreferNoSchedule, so we need to collect it as well.
+		if len(toleration.Effect) == 0 || toleration.Effect == v1.TaintEffectPreferNoSchedule {
+			tolerationList = append(tolerationList, toleration)
+		}
+	}
+	return
+}
+
+// CountIntolerableTaintsPreferNoSchedule gives the count of intolerable taints of a pod with effect PreferNoSchedule
+func countIntolerableTaintsPreferNoSchedule(taints []v1.Taint, tolerations []v1.Toleration) (intolerableTaints int) {
+	for _, taint := range taints {
+		// check only on taints that have effect PreferNoSchedule
+		if taint.Effect != v1.TaintEffectPreferNoSchedule {
+			continue
+		}
+
+		if !v1helper.TolerationsTolerateTaint(tolerations, &taint) {
+			intolerableTaints++
+		}
+	}
+	return
+}
+
+// Score invoked at the Score extension point.
+func (pl *TaintToleration) Score(ctx context.Context, requirements *appsapi.ReplicaRequirements, name string) (int64, *framework.Status) {
+	node, err := pl.handle.SharedInformerFactory().Core().V1().Nodes().Lister().Get(name)
+	if err != nil {
+		return 0, framework.AsStatus(fmt.Errorf("getting nodes %s: %v", name, err))
+	}
+	score := int64(countIntolerableTaintsPreferNoSchedule(node.Spec.Taints, getAllTolerationPreferNoSchedule(requirements.Tolerations)))
+	return score, nil
+}
+
+// NormalizeScore invoked after scoring all nodes.
+func (pl *TaintToleration) NormalizeScore(ctx context.Context, requirements *appsapi.ReplicaRequirements, scores framework.NodeScoreList) *framework.Status {
+	return helper.DefaultNormalizeScore(framework.MaxNodeScore, true, scores)
+}
+
+// ScoreExtensions of the Score plugin.
+func (pl *TaintToleration) ScoreExtensions() framework.ScoreExtensions {
+	return pl
+}
+
+// New initializes a new plugin and returns it.
+func New(_ runtime.Object, h framework.Handle) (framework.Plugin, error) {
+	return &TaintToleration{handle: h}, nil
+}
diff --git a/pkg/predictor/predictor.go b/pkg/predictor/predictor.go
new file mode 100644
index 0000000..d270ecb
--- /dev/null
+++ b/pkg/predictor/predictor.go
@@ -0,0 +1,244 @@
+/*
+Copyright 2022 The Clusternet 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 predictor
+
+import (
+	"context"
+	"fmt"
+	"sync/atomic"
+	"time"
+
+	corev1 "k8s.io/api/core/v1"
+	"k8s.io/klog/v2"
+
+	appsapi "github.com/clusternet/clusternet/pkg/apis/apps/v1alpha1"
+	framework "github.com/clusternet/clusternet/pkg/predictor/framework/interfaces"
+	frameworkruntime "github.com/clusternet/clusternet/pkg/predictor/framework/runtime"
+	"github.com/clusternet/clusternet/pkg/predictor/metrics"
+	"github.com/clusternet/clusternet/pkg/scheduler/parallelize"
+)
+
+// findNodesThatFitRequirements filters the nodes to find the ones that fit the requirements based on the framework filter plugins.
+func findNodesThatFitRequirements(
+	ctx context.Context,
+	fwk framework.Framework,
+	requirements *appsapi.ReplicaRequirements,
+	nodes []*framework.NodeInfo,
+) ([]*framework.NodeInfo, error) {
+	// Run "prefilter" plugins.
+	s := fwk.RunPreFilterPlugins(ctx, requirements)
+	if !s.IsSuccess() {
+		if !s.IsUnpredictable() {
+			return nil, s.AsError()
+		}
+		return nil, s.AsError()
+	}
+
+	feasibleNodes, err := findNodesThatPassFilters(ctx, fwk, requirements, nodes)
+	if err != nil {
+		return nil, err
+	}
+
+	if klog.V(6).Enabled() {
+		klog.V(6).InfoS("get feasibleNodes", "num", len(feasibleNodes))
+	}
+
+	return feasibleNodes, nil
+}
+
+// findNodesThatPassFilters finds the nodes that fit the filter plugins.
+func findNodesThatPassFilters(
+	ctx context.Context,
+	fwk framework.Framework,
+	requirements *appsapi.ReplicaRequirements,
+	nodes []*framework.NodeInfo,
+) ([]*framework.NodeInfo, error) {
+
+	// Create feasible list with enough space to avoid growing it
+	// and allow assigning.
+	feasibleNodes := make([]*framework.NodeInfo, len(nodes))
+
+	if !fwk.HasFilterPlugins() {
+		return nodes, nil
+	}
+
+	errCh := parallelize.NewErrorChannel()
+	var feasibleNodesLen int32
+	ctx, cancel := context.WithCancel(ctx)
+	checkNode := func(i int) {
+		nodeInfo := nodes[i]
+		status := fwk.RunFilterPlugins(ctx, requirements, nodeInfo).Merge()
+		if status.Code() == framework.Error {
+			errCh.SendErrorWithCancel(status.AsError(), cancel)
+			return
+		}
+		if status.IsSuccess() {
+			length := atomic.AddInt32(&feasibleNodesLen, 1)
+			feasibleNodes[length-1] = nodeInfo
+		}
+	}
+
+	beginCheckNode := time.Now()
+	statusCode := framework.Success
+	defer func() {
+		// We record Filter extension point latency here instead of in framework.go because framework.RunFilterPlugins
+		// function is called for each node, whereas we want to have an overall latency for all nodes. .
+		metrics.FrameworkExtensionPointDuration.WithLabelValues(frameworkruntime.Filter, statusCode.String(), fwk.ProfileName()).Observe(metrics.SinceInSeconds(beginCheckNode))
+	}()
+
+	// Stops searching for more nodes once the configured number of feasible nodes
+	// are found.
+	fwk.Parallelizer().Until(ctx, len(nodes), checkNode)
+
+	feasibleNodes = feasibleNodes[:feasibleNodesLen]
+	if err := errCh.ReceiveError(); err != nil {
+		statusCode = framework.Error
+		return nil, err
+	}
+	return feasibleNodes, nil
+}
+
+// computeReplicas calculate max accept replicas in each node  based on the framework calculate plugins.
+func computeReplicas(
+	ctx context.Context,
+	fwk framework.Framework,
+	requirements *appsapi.ReplicaRequirements,
+	nodes []*framework.NodeInfo,
+) (framework.NodeScoreList, error) {
+	availableList := make(framework.NodeScoreList, len(nodes))
+	for i := range nodes {
+		availableList[i] = framework.NodeScore{
+			Name: klog.KObj(nodes[i].Node()).String(),
+		}
+	}
+
+	// Initialize.
+	for i := range availableList {
+		availableList[i].MaxAvailableReplicas = -1
+	}
+
+	if !fwk.HasComputePlugins() {
+		return nil, fmt.Errorf("no compute plugin is registry")
+	}
+
+	// Run PreCompute plugins.
+	preComputeStatus := fwk.RunPreComputePlugins(ctx, requirements, nodes)
+	if !preComputeStatus.IsSuccess() {
+		return nil, preComputeStatus.AsError()
+	}
+
+	// Run the Compute plugins.
+	nodeScoreList, compute := fwk.RunComputePlugins(ctx, requirements, nodes, availableList)
+	if !compute.IsSuccess() {
+		return nil, compute.AsError()
+	}
+
+	if klog.V(6).Enabled() {
+		for i := range availableList {
+			klog.V(6).InfoS("compute node's max available replicas", "nodes", nodeScoreList[i].Name, "max available replicas", availableList[i].MaxAvailableReplicas)
+		}
+	}
+	return nodeScoreList, nil
+}
+
+// prioritizeNodes prioritizes the nodes by running the score plugins,
+// which return a score for each node from the call to RunScorePlugins().
+// The scores from each plugin are added together to make the score for that node, then
+// any extenders are run as well.
+// All scores are finally combined (added) to get the total weighted scores of all nodes
+func prioritizeNodes(
+	ctx context.Context,
+	fwk framework.Framework,
+	requirements *appsapi.ReplicaRequirements,
+	nodeInfos []*framework.NodeInfo,
+	result framework.NodeScoreList,
+) (framework.NodeScoreList, error) {
+	// If no priority configs are provided, then all nodes will have a score of one.
+	// This is required to generate the priority list in the required format
+	if !fwk.HasScorePlugins() {
+		for i := range result {
+			result[i].Score = 1
+		}
+		return result, nil
+	}
+
+	nodes := make([]*corev1.Node, len(nodeInfos))
+	for i := range nodeInfos {
+		nodes[i] = nodeInfos[i].Node()
+	}
+
+	// Run PreScore plugins.
+	preScoreStatus := fwk.RunPreScorePlugins(ctx, requirements, nodes)
+	if !preScoreStatus.IsSuccess() {
+		return nil, preScoreStatus.AsError()
+	}
+
+	// Run the Score plugins.
+	scoresMap, scoreStatus := fwk.RunScorePlugins(ctx, requirements, nodes)
+	if !scoreStatus.IsSuccess() {
+		return nil, scoreStatus.AsError()
+	}
+
+	if klog.V(6).Enabled() {
+		for plugin, nodeScoreList := range scoresMap {
+			for _, nodeScore := range nodeScoreList {
+				klog.V(6).InfoS("Plugin scored node", "plugin", plugin, "node", nodeScore.Name, "score", nodeScore.Score)
+			}
+		}
+	}
+
+	// Summarize all scores.
+
+	for i := range nodes {
+		for j := range scoresMap {
+			result[i].Score += scoresMap[j][i].Score
+		}
+	}
+
+	if klog.V(6).Enabled() {
+		for i := range result {
+			klog.V(6).InfoS("Calculated node's final score", "node", result[i].Name, "score", result[i].Score)
+		}
+	}
+	return result, nil
+}
+
+// aggregateReplicas aggregate the  max accept replicas in all selected nodes based on the framework aggregate plugins.
+func aggregateReplicas(
+	ctx context.Context,
+	fwk framework.Framework,
+	requirements *appsapi.ReplicaRequirements,
+	result framework.NodeScoreList,
+) (framework.AcceptableReplicas, error) {
+	if !fwk.HasAggregatePlugins() {
+		return nil, fmt.Errorf("no calculate plugin is registry")
+	}
+
+	// Run PreAggregate plugins.
+	preAggregateStatus := fwk.RunPreAggregatePlugins(ctx, requirements, result)
+	if !preAggregateStatus.IsSuccess() {
+		return nil, preAggregateStatus.AsError()
+	}
+
+	// Run the Aggregate plugins.
+	acceptableReplicas, aggregateStatus := fwk.RunAggregatePlugins(ctx, requirements, result)
+	if !aggregateStatus.IsSuccess() {
+		return nil, aggregateStatus.AsError()
+	}
+
+	return acceptableReplicas, nil
+}
diff --git a/pkg/predictor/server.go b/pkg/predictor/server.go
new file mode 100644
index 0000000..f9ebe17
--- /dev/null
+++ b/pkg/predictor/server.go
@@ -0,0 +1,251 @@
+/*
+Copyright 2022 The Clusternet 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 predictor
+
+import (
+	"context"
+	"fmt"
+	"sync/atomic"
+
+	corev1 "k8s.io/api/core/v1"
+	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
+	"k8s.io/apimachinery/pkg/labels"
+	utilruntime "k8s.io/apimachinery/pkg/util/runtime"
+	"k8s.io/client-go/informers"
+	informerv1 "k8s.io/client-go/informers/core/v1"
+	"k8s.io/client-go/kubernetes"
+	"k8s.io/client-go/kubernetes/scheme"
+	v1core "k8s.io/client-go/kubernetes/typed/core/v1"
+	listerv1 "k8s.io/client-go/listers/core/v1"
+	restclient "k8s.io/client-go/rest"
+	"k8s.io/client-go/tools/cache"
+	"k8s.io/client-go/tools/record"
+	"k8s.io/controller-manager/pkg/clientbuilder"
+	"k8s.io/klog/v2"
+
+	appsapi "github.com/clusternet/clusternet/pkg/apis/apps/v1alpha1"
+	clusterapi "github.com/clusternet/clusternet/pkg/apis/clusters/v1beta1"
+	schedulerapi "github.com/clusternet/clusternet/pkg/apis/scheduler"
+	"github.com/clusternet/clusternet/pkg/known"
+	framework "github.com/clusternet/clusternet/pkg/predictor/framework/interfaces"
+	"github.com/clusternet/clusternet/pkg/predictor/framework/plugins"
+	frameworkruntime "github.com/clusternet/clusternet/pkg/predictor/framework/runtime"
+	"github.com/clusternet/clusternet/pkg/scheduler/parallelize"
+)
+
+const (
+	nodeNameKeyIndex = "spec.nodeName"
+)
+
+// GetPodsAssignedToNodeFunc is a function which accept a node name input
+// and returns the pods that assigned to the node.
+type GetPodsAssignedToNodeFunc func(string) ([]*corev1.Pod, error)
+
+type PredictorServer struct {
+	ctx context.Context
+
+	kubeClient kubernetes.Interface
+
+	informerFactory informers.SharedInformerFactory
+	nodeInformer    informerv1.NodeInformer
+	podInformer     informerv1.PodInformer
+	nodeLister      listerv1.NodeLister
+	podLister       listerv1.PodLister
+
+	getPodFunc GetPodsAssignedToNodeFunc
+
+	// default in-tree registry
+	registry  frameworkruntime.Registry
+	framework framework.Framework
+}
+
+func NewPredictorServer(
+	ctx context.Context,
+	clientConfig *restclient.Config,
+) (*PredictorServer, error) {
+	// creating the clientset
+	rootClientBuilder := clientbuilder.SimpleControllerClientBuilder{
+		ClientConfig: clientConfig,
+	}
+	kubeClient := kubernetes.NewForConfigOrDie(rootClientBuilder.ConfigOrDie("clusternet-predictor"))
+	informerFactory := informers.NewSharedInformerFactory(kubeClient, known.DefaultResync)
+
+	broadcaster := record.NewBroadcaster()
+	broadcaster.StartRecordingToSink(&v1core.EventSinkImpl{
+		Interface: kubeClient.CoreV1().Events(""),
+	})
+	utilruntime.Must(appsapi.AddToScheme(scheme.Scheme))
+	utilruntime.Must(clusterapi.AddToScheme(scheme.Scheme))
+	recorder := broadcaster.NewRecorder(scheme.Scheme, corev1.EventSource{Component: "clusternet-predictor"})
+	ps := &PredictorServer{
+		ctx:             ctx,
+		kubeClient:      kubeClient,
+		informerFactory: informerFactory,
+		nodeInformer:    informerFactory.Core().V1().Nodes(),
+		podInformer:     informerFactory.Core().V1().Pods(),
+		nodeLister:      informerFactory.Core().V1().Nodes().Lister(),
+		podLister:       informerFactory.Core().V1().Pods().Lister(),
+		registry:        plugins.NewInTreeRegistry(),
+	}
+
+	framework, err := frameworkruntime.NewFramework(ps.registry, getDefaultPlugins(),
+		frameworkruntime.WithEventRecorder(recorder),
+		frameworkruntime.WithInformerFactory(informerFactory),
+		frameworkruntime.WithClientSet(kubeClient),
+		frameworkruntime.WithKubeConfig(clientConfig),
+		frameworkruntime.WithParallelism(parallelize.DefaultParallelism),
+		frameworkruntime.WithRunAllFilters(false),
+	)
+	if err != nil {
+		return nil, err
+	}
+	ps.framework = framework
+
+	getPodFunc, err := ps.BuildGetPodsAssignedToNodeFunc(ps.podInformer)
+	if err != nil {
+		return nil, err
+	}
+	ps.getPodFunc = getPodFunc
+
+	return ps, nil
+}
+
+var _ schedulerapi.PredictorProvider = &PredictorServer{}
+
+func (ps *PredictorServer) Run() error {
+	// Start all informers.
+	ps.informerFactory.Start(ps.ctx.Done())
+	// Wait for all caches to sync before scheduling.
+	ps.informerFactory.WaitForCacheSync(ps.ctx.Done())
+
+	ps.run(ps.ctx)
+
+	return nil
+}
+
+func (ps *PredictorServer) run(ctx context.Context) {
+	stopCh := ctx.Done()
+	klog.Infof("Starting clusternet predictor")
+	defer klog.Infof("Shutting down clusternet predictor")
+
+	ps.informerFactory.Start(stopCh)
+	if !cache.WaitForCacheSync(ctx.Done(), ps.podInformer.Informer().HasSynced, ps.nodeInformer.Informer().HasSynced) {
+		klog.Errorf("failed to wait for caches to sync")
+		return
+	}
+	// TODO: add communication interface to scheduler
+}
+
+// BuildGetPodsAssignedToNodeFunc establishes an indexer to map the pods and their assigned nodes.
+// It returns a function to help us get all the pods that assigned to a node based on the indexer.
+func (ps *PredictorServer) BuildGetPodsAssignedToNodeFunc(podInformer informerv1.PodInformer) (GetPodsAssignedToNodeFunc, error) {
+	// Establish an indexer to map the pods and their assigned nodes.
+	err := podInformer.Informer().AddIndexers(cache.Indexers{
+		nodeNameKeyIndex: func(obj interface{}) ([]string, error) {
+			pod, ok := obj.(*corev1.Pod)
+			if !ok {
+				return []string{}, nil
+			}
+			if len(pod.Spec.NodeName) == 0 {
+				return []string{}, nil
+			}
+			return []string{pod.Spec.NodeName}, nil
+		},
+	})
+	if err != nil {
+		return nil, err
+	}
+
+	// The indexer helps us get all the pods that assigned to a node.
+	podIndexer := podInformer.Informer().GetIndexer()
+	getPodsAssignedToNode := func(nodeName string) ([]*corev1.Pod, error) {
+		objs, err := podIndexer.ByIndex(nodeNameKeyIndex, nodeName)
+		if err != nil {
+			return nil, err
+		}
+		pods := make([]*corev1.Pod, 0, len(objs))
+		for _, obj := range objs {
+			pod, ok := obj.(*corev1.Pod)
+			if !ok {
+				continue
+			}
+			pods = append(pods, pod)
+		}
+		return pods, nil
+	}
+	return getPodsAssignedToNode, nil
+}
+
+func (ps *PredictorServer) MaxAcceptableReplicas(ctx context.Context, requirements appsapi.ReplicaRequirements) (map[string]int32, error) {
+
+	defaultAcceptableReplicas := map[string]int32{
+		framework.DefaultAcceptableReplicasKey: 0,
+	}
+
+	nodes, err := ps.nodeLister.List(labels.SelectorFromSet(requirements.NodeSelector))
+	if err != nil {
+		return defaultAcceptableReplicas, fmt.Errorf("failed to get nodes that match node selector, err: %v", err)
+	}
+	nodeInfoList := make([]*framework.NodeInfo, len(nodes))
+	var nodesLen int32
+	getNodeInfo := func(i int) {
+		pods, err := ps.getPodFunc(nodes[i].Name)
+		if err != nil {
+			klog.V(6).InfoS("failed to get pods in nodes", "nodes", nodes[i].Name)
+		} else {
+			nodeInfo := framework.NewNodeInfo(nodes[i], pods)
+			length := atomic.AddInt32(&nodesLen, 1)
+			nodeInfoList[length-1] = nodeInfo
+		}
+	}
+	ps.framework.Parallelizer().Until(ctx, len(nodes), getNodeInfo)
+
+	// Step 1: Filter Nodes.
+	feasibleNodes, err := findNodesThatFitRequirements(ctx, ps.framework, &requirements, nodeInfoList)
+	if err != nil {
+		return defaultAcceptableReplicas, err
+	}
+
+	if len(feasibleNodes) == 0 {
+		return defaultAcceptableReplicas, fmt.Errorf("no feasible nodes found")
+	}
+
+	// Step 2: cal max available replicas for each feasibleNodes.
+	nodeScoreList, err := computeReplicas(ctx, ps.framework, &requirements, feasibleNodes)
+	if err != nil {
+		return defaultAcceptableReplicas, err
+	}
+
+	// Step 3: Prioritize clusters.
+	priorityList, err := prioritizeNodes(ctx, ps.framework, &requirements, feasibleNodes, nodeScoreList)
+	if err != nil {
+		return defaultAcceptableReplicas, err
+	}
+
+	// step4 aggregate the max available replicas
+	result, err := aggregateReplicas(ctx, ps.framework, &requirements, priorityList)
+	if err != nil {
+		return defaultAcceptableReplicas, err
+	}
+	return result, nil
+}
+
+func (ps *PredictorServer) UnschedulableReplicas(ctx context.Context, gvk metav1.GroupVersionKind, namespacedName string,
+	labelSelector map[string]string) (int32, error) {
+	// TODO: add real logic
+	return 0, nil
+}
diff --git a/pkg/utils/resource.go b/pkg/utils/resource.go
new file mode 100644
index 0000000..62a080d
--- /dev/null
+++ b/pkg/utils/resource.go
@@ -0,0 +1,386 @@
+/*
+Copyright 2021 The Clusternet 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 utils
+
+import (
+	"fmt"
+	"math"
+	"reflect"
+	"strings"
+
+	corev1 "k8s.io/api/core/v1"
+	"k8s.io/apimachinery/pkg/api/resource"
+	"k8s.io/apimachinery/pkg/util/validation"
+)
+
+// Resource is a collection of compute resource.
+type Resource struct {
+	MilliCPU         int64
+	Memory           int64
+	EphemeralStorage int64
+	AllowedPodNumber int64
+	// ScalarResources
+	ScalarResources map[corev1.ResourceName]int64
+}
+
+// EmptyResource creates a empty resource object and returns.
+func EmptyResource() *Resource {
+	return &Resource{}
+}
+
+func NewResource(rl corev1.ResourceList) *Resource {
+	r := &Resource{}
+	for rName, rQuant := range rl {
+		switch rName {
+		case corev1.ResourceCPU:
+			r.MilliCPU += rQuant.MilliValue()
+		case corev1.ResourceMemory:
+			r.Memory += rQuant.Value()
+		case corev1.ResourcePods:
+			r.AllowedPodNumber += rQuant.Value()
+		case corev1.ResourceEphemeralStorage:
+			r.EphemeralStorage += rQuant.Value()
+		default:
+			if IsScalarResourceName(rName) {
+				r.AddScalar(rName, rQuant.Value())
+			}
+		}
+	}
+	return r
+}
+
+// Add is used to add two resources.
+func (r *Resource) Add(rl corev1.ResourceList) {
+	for rName, rQuant := range rl {
+		switch rName {
+		case corev1.ResourceCPU:
+			r.MilliCPU += rQuant.MilliValue()
+		case corev1.ResourceMemory:
+			r.Memory += rQuant.Value()
+		case corev1.ResourcePods:
+			r.AllowedPodNumber += rQuant.Value()
+		case corev1.ResourceEphemeralStorage:
+			r.EphemeralStorage += rQuant.Value()
+		default:
+			if IsScalarResourceName(rName) {
+				r.AddScalar(rName, rQuant.Value())
+			}
+		}
+	}
+}
+
+// AddMulti is used to add resources with scale.
+func (r *Resource) AddMulti(rl corev1.ResourceList, ratio int64) {
+	for rName, rQuant := range rl {
+		switch rName {
+		case corev1.ResourceCPU:
+			r.MilliCPU += rQuant.MilliValue() * ratio
+		case corev1.ResourceMemory:
+			r.Memory += rQuant.Value() * ratio
+		case corev1.ResourcePods:
+			r.AllowedPodNumber += rQuant.Value() * ratio
+		case corev1.ResourceEphemeralStorage:
+			r.EphemeralStorage += rQuant.Value() * ratio
+		default:
+			if IsScalarResourceName(rName) {
+				r.AddScalar(rName, rQuant.Value()*ratio)
+			}
+		}
+	}
+}
+
+// Multi is used to scale a resource.
+func (r *Resource) Multi(ratio int64) {
+	r.MilliCPU *= ratio
+	r.Memory *= ratio
+	r.EphemeralStorage *= ratio
+	r.AllowedPodNumber *= ratio
+	for rName, rQuant := range r.ScalarResources {
+		r.ScalarResources[rName] = rQuant * ratio
+	}
+}
+
+// Sub is used to subtract two resources.
+func (r *Resource) Sub(rl corev1.ResourceList) error {
+	for rName, rQuant := range rl {
+		switch rName {
+		case corev1.ResourceCPU:
+			cpu := rQuant.MilliValue()
+			if r.MilliCPU < cpu {
+				return fmt.Errorf("cpu difference is less than 0, remain %d, got %d", r.MilliCPU, cpu)
+			}
+			r.MilliCPU -= cpu
+		case corev1.ResourceMemory:
+			mem := rQuant.Value()
+			if r.Memory < mem {
+				return fmt.Errorf("memory difference is less than 0, remain %d, got %d", r.Memory, mem)
+			}
+			r.Memory -= mem
+		case corev1.ResourcePods:
+			pods := rQuant.Value()
+			if r.AllowedPodNumber < pods {
+				return fmt.Errorf("allowed pod difference is less than 0, remain %d, got %d", r.AllowedPodNumber, pods)
+			}
+			r.AllowedPodNumber -= pods
+		case corev1.ResourceEphemeralStorage:
+			ephemeralStorage := rQuant.Value()
+			if r.EphemeralStorage < ephemeralStorage {
+				return fmt.Errorf("allowed storage number difference is less than 0, remain %d, got %d", r.EphemeralStorage, ephemeralStorage)
+			}
+			r.EphemeralStorage -= ephemeralStorage
+		default:
+			if IsScalarResourceName(rName) {
+				rScalar, ok := r.ScalarResources[rName]
+				scalar := rQuant.Value()
+				if !ok && scalar > 0 {
+					return fmt.Errorf("scalar resources %s does not exist, got %d", rName, scalar)
+				}
+				if rScalar < scalar {
+					return fmt.Errorf("scalar resources %s difference is less than 0, remain %d, got %d", rName, rScalar, scalar)
+				}
+				r.ScalarResources[rName] = rScalar - scalar
+			}
+		}
+	}
+	return nil
+}
+
+// Clone is used to clone a resource type, which is a deep copy function.
+func (r *Resource) Clone() *Resource {
+	clone := &Resource{
+		MilliCPU:         r.MilliCPU,
+		Memory:           r.Memory,
+		EphemeralStorage: r.EphemeralStorage,
+		AllowedPodNumber: r.AllowedPodNumber,
+	}
+
+	if r.ScalarResources != nil {
+		clone.ScalarResources = make(map[corev1.ResourceName]int64)
+		for k, v := range r.ScalarResources {
+			clone.ScalarResources[k] = v
+		}
+	}
+
+	return clone
+}
+
+// SetMaxResource compares with ResourceList and takes max value for each Resource.
+func (r *Resource) SetMaxResource(rl corev1.ResourceList) {
+	for rName, rQuant := range rl {
+		switch rName {
+		case corev1.ResourceCPU:
+			if cpu := rQuant.MilliValue(); cpu > r.MilliCPU {
+				r.MilliCPU = cpu
+			}
+		case corev1.ResourceMemory:
+			if mem := rQuant.Value(); mem > r.Memory {
+				r.Memory = mem
+			}
+		case corev1.ResourceEphemeralStorage:
+			if ephemeralStorage := rQuant.Value(); ephemeralStorage > r.EphemeralStorage {
+				r.EphemeralStorage = ephemeralStorage
+			}
+		case corev1.ResourcePods:
+			if pods := rQuant.Value(); pods > r.AllowedPodNumber {
+				r.AllowedPodNumber = pods
+			}
+		default:
+			if IsScalarResourceName(rName) {
+				if value := rQuant.Value(); value > r.ScalarResources[rName] {
+					r.SetScalar(rName, value)
+				}
+			}
+		}
+	}
+}
+
+func (r *Resource) MaxReplicaDivided(rl corev1.ResourceList) int64 {
+	res := int64(math.MaxInt32)
+	for rName, rQuant := range rl {
+		switch rName {
+		case corev1.ResourceCPU:
+			if cpu := rQuant.MilliValue(); cpu > 0 {
+				res = MinInt64(res, r.MilliCPU/cpu)
+			}
+		case corev1.ResourceMemory:
+			if mem := rQuant.Value(); mem > 0 {
+				res = MinInt64(res, r.Memory/mem)
+			}
+		case corev1.ResourceEphemeralStorage:
+			if ephemeralStorage := rQuant.Value(); ephemeralStorage > 0 {
+				res = MinInt64(res, r.EphemeralStorage/ephemeralStorage)
+			}
+		default:
+			if IsScalarResourceName(rName) {
+				rScalar := r.ScalarResources[rName]
+				if scalar := rQuant.Value(); scalar > 0 {
+					res = MinInt64(res, rScalar/scalar)
+				}
+			}
+		}
+	}
+	res = MinInt64(res, r.AllowedPodNumber)
+	return res
+}
+
+// AddScalar adds a resource by a scalar value of this resource.
+func (r *Resource) AddScalar(name corev1.ResourceName, quantity int64) {
+	r.SetScalar(name, r.ScalarResources[name]+quantity)
+}
+
+// SetScalar sets a resource by a scalar value of this resource.
+func (r *Resource) SetScalar(name corev1.ResourceName, quantity int64) {
+	// Lazily allocate scalar resource map.
+	if r.ScalarResources == nil {
+		r.ScalarResources = map[corev1.ResourceName]int64{}
+	}
+	r.ScalarResources[name] = quantity
+}
+
+// ResourceList returns a resource list of this resource.
+func (r *Resource) ResourceList() corev1.ResourceList {
+	result := corev1.ResourceList{
+		corev1.ResourceCPU:              *resource.NewMilliQuantity(r.MilliCPU, resource.DecimalSI),
+		corev1.ResourceMemory:           *resource.NewQuantity(r.Memory, resource.BinarySI),
+		corev1.ResourceEphemeralStorage: *resource.NewQuantity(r.EphemeralStorage, resource.BinarySI),
+		corev1.ResourcePods:             *resource.NewQuantity(r.AllowedPodNumber, resource.DecimalSI),
+	}
+	for rName, rQuant := range r.ScalarResources {
+		if IsHugePageResourceName(rName) {
+			result[rName] = *resource.NewQuantity(rQuant, resource.BinarySI)
+		} else {
+			result[rName] = *resource.NewQuantity(rQuant, resource.DecimalSI)
+		}
+	}
+	return result
+}
+
+func (r *Resource) Less(rr *Resource) bool {
+	moreFunc := func(l, r int64) bool {
+		return l > r
+	}
+
+	if moreFunc(r.MilliCPU, rr.MilliCPU) {
+		return false
+	}
+	if moreFunc(r.Memory, rr.Memory) {
+		return false
+	}
+	if moreFunc(r.EphemeralStorage, rr.EphemeralStorage) {
+		return false
+	}
+	if moreFunc(r.AllowedPodNumber, rr.AllowedPodNumber) {
+		return false
+	}
+	for rrName, rrQuant := range rr.ScalarResources {
+		rQuant, _ := r.ScalarResources[rrName]
+		if moreFunc(rQuant, rrQuant) {
+			return false
+		}
+	}
+	return !reflect.DeepEqual(r, rr)
+}
+
+// AddPodRequest add the effective request resource of a pod to the origin resource.
+// The Pod's effective request is the higher of:
+// - the sum of all app containers(spec.Containers) request for a resource.
+// - the effective init containers(spec.InitContainers) request for a resource.
+// The effective init containers request is the highest request on all init containers.
+func (r *Resource) AddPodRequest(podSpec *corev1.PodSpec) *Resource {
+	for _, container := range podSpec.Containers {
+		r.Add(container.Resources.Requests)
+	}
+	for _, container := range podSpec.InitContainers {
+		r.SetMaxResource(container.Resources.Requests)
+	}
+	// If Overhead is being utilized, add to the total requests for the pod.
+	// We assume the EnablePodOverhead feature gate of member cluster is set (it is on by default since 1.18).
+	if podSpec.Overhead != nil {
+		r.Add(podSpec.Overhead)
+	}
+	return r
+}
+
+func (r *Resource) AddResourcePods(pods int64) {
+	r.Add(corev1.ResourceList{
+		corev1.ResourcePods: *resource.NewQuantity(pods, resource.DecimalSI),
+	})
+}
+
+// String returns resource details in string format
+func (r *Resource) String() string {
+	str := fmt.Sprintf("cpu %d, memory %d, ephemeral-storage %d, pods %d", r.MilliCPU, r.Memory, r.EphemeralStorage, r.AllowedPodNumber)
+	for rName, rQuant := range r.ScalarResources {
+		str = fmt.Sprintf("%s, %s %d", str, rName, rQuant)
+	}
+	return str
+}
+
+func MinInt64(a, b int64) int64 {
+	if a <= b {
+		return a
+	}
+	return b
+}
+
+// IsScalarResourceName validates the resource for Extended, Hugepages, Native and AttachableVolume resources
+func IsScalarResourceName(name corev1.ResourceName) bool {
+	return IsExtendedResourceName(name) || IsHugePageResourceName(name) ||
+		IsPrefixedNativeResource(name) || IsAttachableVolumeResourceName(name)
+}
+
+// IsExtendedResourceName returns true if:
+// 1. the resource name is not in the default namespace;
+// 2. resource name does not have "requests." prefix,
+// to avoid confusion with the convention in quota
+// 3. it satisfies the rules in IsQualifiedName() after converted into quota resource name
+func IsExtendedResourceName(name corev1.ResourceName) bool {
+	if IsNativeResource(name) || strings.HasPrefix(string(name), corev1.DefaultResourceRequestsPrefix) {
+		return false
+	}
+	// Ensure it satisfies the rules in IsQualifiedName() after converted into quota resource name
+	nameForQuota := fmt.Sprintf("%s%s", corev1.DefaultResourceRequestsPrefix, string(name))
+	if errs := validation.IsQualifiedName(nameForQuota); len(errs) != 0 {
+		return false
+	}
+	return true
+}
+
+// IsPrefixedNativeResource returns true if the resource name is in the
+// *kubernetes.io/ namespace.
+func IsPrefixedNativeResource(name corev1.ResourceName) bool {
+	return strings.Contains(string(name), corev1.ResourceDefaultNamespacePrefix)
+}
+
+// IsNativeResource returns true if the resource name is in the
+// *kubernetes.io/ namespace. Partially-qualified (unprefixed) names are
+// implicitly in the kubernetes.io/ namespace.
+func IsNativeResource(name corev1.ResourceName) bool {
+	return !strings.Contains(string(name), "/") ||
+		IsPrefixedNativeResource(name)
+}
+
+// IsAttachableVolumeResourceName returns true when the resource name is prefixed in attachable volume
+func IsAttachableVolumeResourceName(name corev1.ResourceName) bool {
+	return strings.HasPrefix(string(name), corev1.ResourceAttachableVolumesPrefix)
+}
+
+// IsHugePageResourceName returns true if the resource name has the huge page
+// resource prefix.
+func IsHugePageResourceName(name corev1.ResourceName) bool {
+	return strings.HasPrefix(string(name), corev1.ResourceHugePagesPrefix)
+}


Test patch:
diff --git a/pkg/predictor/framework/plugins/defaultaggregate/default_aggregate_test.go b/pkg/predictor/framework/plugins/defaultaggregate/default_aggregate_test.go
new file mode 100644
index 0000000..802fd62
--- /dev/null
+++ b/pkg/predictor/framework/plugins/defaultaggregate/default_aggregate_test.go
@@ -0,0 +1,61 @@
+/*
+Copyright 2022 The Clusternet 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 defaultaggregate
+
+import (
+	"context"
+	"fmt"
+	"testing"
+
+	framework "github.com/clusternet/clusternet/pkg/predictor/framework/interfaces"
+)
+
+func TestDefaultAggregator(t *testing.T) {
+	tests := []struct {
+		scores framework.NodeScoreList
+		want   int32
+	}{
+		{
+			scores: framework.NodeScoreList{
+				framework.NodeScore{
+					Name:                 "node1",
+					Score:                0,
+					MaxAvailableReplicas: 4,
+				},
+				framework.NodeScore{
+					Name:                 "node2",
+					Score:                0,
+					MaxAvailableReplicas: 10,
+				},
+			},
+			want: 14,
+		},
+	}
+	for i, test := range tests {
+		t.Run(fmt.Sprintf("case_%d", i), func(t *testing.T) {
+			p, _ := New(nil, nil)
+			maxAvailableReplicas, status := p.(framework.AggregatePlugin).Aggregate(context.Background(), nil, test.scores)
+			if got := status.AsError(); got != nil {
+				t.Errorf("status does not match %q, want nil", got)
+			}
+			if maxAvailableReplicas[framework.DefaultAcceptableReplicasKey] != test.want {
+				t.Errorf("got different maxAvailableReplicas got %q, want %q",
+					maxAvailableReplicas[framework.DefaultAcceptableReplicasKey], test.want)
+			}
+		})
+	}
+}
diff --git a/pkg/predictor/framework/plugins/defaultcompute/default_compute_test.go b/pkg/predictor/framework/plugins/defaultcompute/default_compute_test.go
new file mode 100644
index 0000000..4fd0168
--- /dev/null
+++ b/pkg/predictor/framework/plugins/defaultcompute/default_compute_test.go
@@ -0,0 +1,141 @@
+/*
+Copyright 2022 The Clusternet 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 defaultcompute
+
+import (
+	"context"
+	"fmt"
+	"testing"
+
+	corev1 "k8s.io/api/core/v1"
+	"k8s.io/apimachinery/pkg/api/resource"
+	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
+
+	appsapi "github.com/clusternet/clusternet/pkg/apis/apps/v1alpha1"
+	framework "github.com/clusternet/clusternet/pkg/predictor/framework/interfaces"
+)
+
+const (
+	ResourceUnitZero             int64 = 0
+	ResourceUnitCPU              int64 = 1000
+	ResourceUnitMem              int64 = 1024 * 1024 * 1024
+	ResourceUnitPod              int64 = 1
+	ResourceUnitEphemeralStorage int64 = 1024 * 1024 * 1024
+)
+
+func makeNode(node string, milliCPU, memory, pods, ephemeralStorage int64) *corev1.Node {
+	return &corev1.Node{
+		ObjectMeta: metav1.ObjectMeta{Name: node},
+		Status: corev1.NodeStatus{
+			Capacity: corev1.ResourceList{
+				corev1.ResourceCPU:              *resource.NewMilliQuantity(milliCPU, resource.DecimalSI),
+				corev1.ResourceMemory:           *resource.NewQuantity(memory, resource.BinarySI),
+				corev1.ResourcePods:             *resource.NewQuantity(pods, resource.DecimalSI),
+				corev1.ResourceEphemeralStorage: *resource.NewQuantity(ephemeralStorage, resource.BinarySI),
+			},
+			Allocatable: corev1.ResourceList{
+				corev1.ResourceCPU:              *resource.NewMilliQuantity(milliCPU, resource.DecimalSI),
+				corev1.ResourceMemory:           *resource.NewQuantity(memory, resource.BinarySI),
+				corev1.ResourcePods:             *resource.NewQuantity(pods, resource.DecimalSI),
+				corev1.ResourceEphemeralStorage: *resource.NewQuantity(ephemeralStorage, resource.BinarySI),
+			},
+		},
+	}
+}
+
+func makePod(node string, milliCPU, memory, ephemeralStorage int64) *corev1.Pod {
+	return &corev1.Pod{
+		Spec: corev1.PodSpec{
+			NodeName: node,
+			Containers: []corev1.Container{
+				{
+					Resources: corev1.ResourceRequirements{
+						Requests: corev1.ResourceList{
+							corev1.ResourceCPU:              *resource.NewMilliQuantity(milliCPU, resource.DecimalSI),
+							corev1.ResourceMemory:           *resource.NewQuantity(memory, resource.DecimalSI),
+							corev1.ResourceEphemeralStorage: *resource.NewQuantity(ephemeralStorage, resource.BinarySI),
+						},
+					},
+				},
+			},
+		},
+	}
+}
+
+func makeReplicaRequirements(milliCPU, memory, ephemeralStorage int64) *appsapi.ReplicaRequirements {
+	return &appsapi.ReplicaRequirements{
+		Resources: corev1.ResourceRequirements{
+			Requests: corev1.ResourceList{
+				corev1.ResourceCPU:              *resource.NewMilliQuantity(milliCPU, resource.DecimalSI),
+				corev1.ResourceMemory:           *resource.NewQuantity(memory, resource.BinarySI),
+				corev1.ResourceEphemeralStorage: *resource.NewQuantity(ephemeralStorage, resource.BinarySI),
+			},
+		},
+	}
+}
+
+func TestDefaultComputer(t *testing.T) {
+	tests := []struct {
+		nodeInfo     *framework.NodeInfo
+		requirements *appsapi.ReplicaRequirements
+		want         int32
+	}{
+		{
+			nodeInfo: framework.NewNodeInfo(
+				makeNode("machine1", 8*ResourceUnitCPU, 16*ResourceUnitMem, 4*ResourceUnitPod, 16*ResourceUnitEphemeralStorage),
+				[]*corev1.Pod{
+					makePod("machine1", 1*ResourceUnitCPU, 4*ResourceUnitMem, 2*ResourceUnitEphemeralStorage),
+					makePod("machine1", 1*ResourceUnitCPU, 4*ResourceUnitMem, 3*ResourceUnitEphemeralStorage),
+				}),
+			requirements: makeReplicaRequirements(1*ResourceUnitCPU, 1*ResourceUnitMem, ResourceUnitZero),
+			want:         2,
+		},
+		{
+			nodeInfo: framework.NewNodeInfo(
+				makeNode("machine2", 8*ResourceUnitCPU, 16*ResourceUnitMem, 4*ResourceUnitPod, 16*ResourceUnitEphemeralStorage),
+				[]*corev1.Pod{
+					makePod("machine2", 1*ResourceUnitCPU, 4*ResourceUnitMem, 2*ResourceUnitEphemeralStorage),
+					makePod("machine2", 6*ResourceUnitCPU, 4*ResourceUnitMem, 4*ResourceUnitEphemeralStorage),
+				}),
+			requirements: makeReplicaRequirements(1*ResourceUnitCPU, 1*ResourceUnitMem, ResourceUnitZero),
+			want:         1,
+		},
+		{
+			nodeInfo: framework.NewNodeInfo(
+				makeNode("machine3", 8*ResourceUnitCPU, 16*ResourceUnitMem, 4*ResourceUnitPod, 16*ResourceUnitEphemeralStorage),
+				[]*corev1.Pod{
+					makePod("machine3", 1*ResourceUnitCPU, 4*ResourceUnitMem, 2*ResourceUnitEphemeralStorage),
+					makePod("machine3", 1*ResourceUnitCPU, 11*ResourceUnitMem, 1*ResourceUnitEphemeralStorage),
+				}),
+			requirements: makeReplicaRequirements(1*ResourceUnitCPU, 1*ResourceUnitMem, ResourceUnitZero),
+			want:         1,
+		},
+	}
+	for i, test := range tests {
+		t.Run(fmt.Sprintf("case_%d", i), func(t *testing.T) {
+			p, _ := New(nil, nil)
+			maxAvailableReplicas, status := p.(framework.ComputePlugin).Compute(context.Background(), test.requirements, test.nodeInfo)
+			if got := status.AsError(); got != nil {
+				t.Errorf("status does not match %q, want nil", got)
+			}
+			if maxAvailableReplicas != test.want {
+				t.Errorf("got different maxAvailableReplicas got %q, want %q",
+					maxAvailableReplicas, test.want)
+			}
+		})
+	}
+}
diff --git a/pkg/predictor/framework/plugins/helper/normalize_score_test.go b/pkg/predictor/framework/plugins/helper/normalize_score_test.go
new file mode 100644
index 0000000..30bb4b2
--- /dev/null
+++ b/pkg/predictor/framework/plugins/helper/normalize_score_test.go
@@ -0,0 +1,87 @@
+/*
+Copyright 2022 The Clusternet 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.
+*/
+
+// This file was copied from k8s.io/kubernetes/pkg/scheduler/framework/plugins/helper/normalize_score_test.go and modified.
+
+package helper
+
+import (
+	"fmt"
+	"testing"
+
+	"github.com/google/go-cmp/cmp"
+
+	framework "github.com/clusternet/clusternet/pkg/predictor/framework/interfaces"
+)
+
+func TestDefaultNormalizeScore(t *testing.T) {
+	tests := []struct {
+		reverse        bool
+		scores         []int64
+		expectedScores []int64
+	}{
+		{
+			scores:         []int64{1, 2, 3, 4},
+			expectedScores: []int64{25, 50, 75, 100},
+		},
+		{
+			reverse:        true,
+			scores:         []int64{1, 2, 3, 4},
+			expectedScores: []int64{75, 50, 25, 0},
+		},
+		{
+			scores:         []int64{1000, 10, 20, 30},
+			expectedScores: []int64{100, 1, 2, 3},
+		},
+		{
+			reverse:        true,
+			scores:         []int64{1000, 10, 20, 30},
+			expectedScores: []int64{0, 99, 98, 97},
+		},
+		{
+			scores:         []int64{1, 1, 1, 1},
+			expectedScores: []int64{100, 100, 100, 100},
+		},
+		{
+			scores:         []int64{1000, 1, 1, 1},
+			expectedScores: []int64{100, 0, 0, 0},
+		},
+		{
+			reverse:        true,
+			scores:         []int64{0, 1, 1, 1},
+			expectedScores: []int64{100, 0, 0, 0},
+		},
+	}
+
+	for i, test := range tests {
+		t.Run(fmt.Sprintf("case_%d", i), func(t *testing.T) {
+			scores := framework.NodeScoreList{}
+			for _, score := range test.scores {
+				scores = append(scores, framework.NodeScore{Score: score})
+			}
+
+			expectedScores := framework.NodeScoreList{}
+			for _, score := range test.expectedScores {
+				expectedScores = append(expectedScores, framework.NodeScore{Score: score})
+			}
+
+			DefaultNormalizeScore(framework.MaxNodeScore, test.reverse, scores)
+			if diff := cmp.Diff(expectedScores, scores); diff != "" {
+				t.Errorf("Unexpected scores (-want, +got):\n%s", diff)
+			}
+		})
+	}
+}
diff --git a/pkg/predictor/framework/plugins/tainttoleration/taint_toleration_test.go b/pkg/predictor/framework/plugins/tainttoleration/taint_toleration_test.go
new file mode 100644
index 0000000..e1d2ee5
--- /dev/null
+++ b/pkg/predictor/framework/plugins/tainttoleration/taint_toleration_test.go
@@ -0,0 +1,343 @@
+/*
+Copyright 2022 The Clusternet 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.
+*/
+
+// This file was copied from k8s.io/kubernetes/pkg/scheduler/framework/plugins/tainttoleration/taint_toleration_test.go and modified.
+
+package tainttoleration
+
+import (
+	"context"
+	"reflect"
+	"testing"
+	"time"
+
+	v1 "k8s.io/api/core/v1"
+	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
+	"k8s.io/client-go/informers"
+	"k8s.io/client-go/kubernetes/fake"
+
+	appsapi "github.com/clusternet/clusternet/pkg/apis/apps/v1alpha1"
+	framework "github.com/clusternet/clusternet/pkg/predictor/framework/interfaces"
+	"github.com/clusternet/clusternet/pkg/predictor/framework/runtime"
+)
+
+func nodeWithTaints(nodeName string, taints []v1.Taint) *v1.Node {
+	return &v1.Node{
+		ObjectMeta: metav1.ObjectMeta{
+			Name: nodeName,
+		},
+		Spec: v1.NodeSpec{
+			Taints: taints,
+		},
+	}
+}
+
+func requirementsWithTolerations(tolerations []v1.Toleration) *appsapi.ReplicaRequirements {
+	return &appsapi.ReplicaRequirements{
+		Tolerations: tolerations,
+	}
+}
+
+func TestTaintTolerationScore(t *testing.T) {
+	tests := []struct {
+		name         string
+		requirements *appsapi.ReplicaRequirements
+		nodes        []*v1.Node
+		expectedList framework.NodeScoreList
+	}{
+		// basic test case
+		{
+			name: "node with taints tolerated by the requirements, gets a higher score than those node with intolerable taints",
+			requirements: requirementsWithTolerations([]v1.Toleration{{
+				Key:      "foo",
+				Operator: v1.TolerationOpEqual,
+				Value:    "bar",
+				Effect:   v1.TaintEffectPreferNoSchedule,
+			}}),
+			nodes: []*v1.Node{
+				nodeWithTaints("nodeA", []v1.Taint{{
+					Key:    "foo",
+					Value:  "bar",
+					Effect: v1.TaintEffectPreferNoSchedule,
+				}}),
+				nodeWithTaints("nodeB", []v1.Taint{{
+					Key:    "foo",
+					Value:  "blah",
+					Effect: v1.TaintEffectPreferNoSchedule,
+				}}),
+			},
+			expectedList: []framework.NodeScore{
+				{Name: "nodeA", Score: framework.MaxNodeScore},
+				{Name: "nodeB", Score: 0},
+			},
+		},
+		// the count of taints that are tolerated by requirement, does not matter.
+		{
+			name: "the nodes that all of their taints are tolerated by the requirement, get the same score, no matter how many tolerable taints a node has",
+			requirements: requirementsWithTolerations([]v1.Toleration{
+				{
+					Key:      "cpu-type",
+					Operator: v1.TolerationOpEqual,
+					Value:    "arm64",
+					Effect:   v1.TaintEffectPreferNoSchedule,
+				}, {
+					Key:      "disk-type",
+					Operator: v1.TolerationOpEqual,
+					Value:    "ssd",
+					Effect:   v1.TaintEffectPreferNoSchedule,
+				},
+			}),
+			nodes: []*v1.Node{
+				nodeWithTaints("nodeA", []v1.Taint{}),
+				nodeWithTaints("nodeB", []v1.Taint{
+					{
+						Key:    "cpu-type",
+						Value:  "arm64",
+						Effect: v1.TaintEffectPreferNoSchedule,
+					},
+				}),
+				nodeWithTaints("nodeC", []v1.Taint{
+					{
+						Key:    "cpu-type",
+						Value:  "arm64",
+						Effect: v1.TaintEffectPreferNoSchedule,
+					}, {
+						Key:    "disk-type",
+						Value:  "ssd",
+						Effect: v1.TaintEffectPreferNoSchedule,
+					},
+				}),
+			},
+			expectedList: []framework.NodeScore{
+				{Name: "nodeA", Score: framework.MaxNodeScore},
+				{Name: "nodeB", Score: framework.MaxNodeScore},
+				{Name: "nodeC", Score: framework.MaxNodeScore},
+			},
+		},
+		// the count of taints on a node that are not tolerated by requirement, matters.
+		{
+			name: "the more intolerable taints a node has, the lower score it gets.",
+			requirements: requirementsWithTolerations([]v1.Toleration{{
+				Key:      "foo",
+				Operator: v1.TolerationOpEqual,
+				Value:    "bar",
+				Effect:   v1.TaintEffectPreferNoSchedule,
+			}}),
+			nodes: []*v1.Node{
+				nodeWithTaints("nodeA", []v1.Taint{}),
+				nodeWithTaints("nodeB", []v1.Taint{
+					{
+						Key:    "cpu-type",
+						Value:  "arm64",
+						Effect: v1.TaintEffectPreferNoSchedule,
+					},
+				}),
+				nodeWithTaints("nodeC", []v1.Taint{
+					{
+						Key:    "cpu-type",
+						Value:  "arm64",
+						Effect: v1.TaintEffectPreferNoSchedule,
+					}, {
+						Key:    "disk-type",
+						Value:  "ssd",
+						Effect: v1.TaintEffectPreferNoSchedule,
+					},
+				}),
+			},
+			expectedList: []framework.NodeScore{
+				{Name: "nodeA", Score: framework.MaxNodeScore},
+				{Name: "nodeB", Score: 50},
+				{Name: "nodeC", Score: 0},
+			},
+		},
+		// taints-tolerations priority only takes care about the taints and tolerations that have effect PreferNoSchedule
+		{
+			name: "only taints and tolerations that have effect PreferNoSchedule are checked by taints-tolerations priority function",
+			requirements: requirementsWithTolerations([]v1.Toleration{
+				{
+					Key:      "cpu-type",
+					Operator: v1.TolerationOpEqual,
+					Value:    "arm64",
+					Effect:   v1.TaintEffectNoSchedule,
+				}, {
+					Key:      "disk-type",
+					Operator: v1.TolerationOpEqual,
+					Value:    "ssd",
+					Effect:   v1.TaintEffectNoSchedule,
+				},
+			}),
+			nodes: []*v1.Node{
+				nodeWithTaints("nodeA", []v1.Taint{}),
+				nodeWithTaints("nodeB", []v1.Taint{
+					{
+						Key:    "cpu-type",
+						Value:  "arm64",
+						Effect: v1.TaintEffectNoSchedule,
+					},
+				}),
+				nodeWithTaints("nodeC", []v1.Taint{
+					{
+						Key:    "cpu-type",
+						Value:  "arm64",
+						Effect: v1.TaintEffectPreferNoSchedule,
+					}, {
+						Key:    "disk-type",
+						Value:  "ssd",
+						Effect: v1.TaintEffectPreferNoSchedule,
+					},
+				}),
+			},
+			expectedList: []framework.NodeScore{
+				{Name: "nodeA", Score: framework.MaxNodeScore},
+				{Name: "nodeB", Score: framework.MaxNodeScore},
+				{Name: "nodeC", Score: 0},
+			},
+		},
+		{
+			name: "Default behaviour No taints and tolerations, lands on node with no taints",
+			//requirements without tolerations
+			requirements: requirementsWithTolerations([]v1.Toleration{}),
+			nodes: []*v1.Node{
+				//Node without taints
+				nodeWithTaints("nodeA", []v1.Taint{}),
+				nodeWithTaints("nodeB", []v1.Taint{
+					{
+						Key:    "cpu-type",
+						Value:  "arm64",
+						Effect: v1.TaintEffectPreferNoSchedule,
+					},
+				}),
+			},
+			expectedList: []framework.NodeScore{
+				{Name: "nodeA", Score: framework.MaxNodeScore},
+				{Name: "nodeB", Score: 0},
+			},
+		},
+	}
+	for _, test := range tests {
+		t.Run(test.name, func(t *testing.T) {
+			fakeInformerFactory := informers.NewSharedInformerFactory(&fake.Clientset{}, 0*time.Second)
+			for _, node := range test.nodes {
+				if err := fakeInformerFactory.Core().V1().Nodes().Informer().GetStore().Add(node); err != nil {
+					t.Fatal(err)
+				}
+			}
+			fh, err := runtime.NewFramework(nil, nil, runtime.WithInformerFactory(fakeInformerFactory))
+			if err != nil {
+				t.Fatal(err)
+			}
+			p, _ := New(nil, fh)
+			var gotList framework.NodeScoreList
+			for _, n := range test.nodes {
+				nodeName := n.ObjectMeta.Name
+				score, status := p.(framework.ScorePlugin).Score(context.Background(), test.requirements, nodeName)
+				if !status.IsSuccess() {
+					t.Errorf("unexpected error: %v", status)
+				}
+				gotList = append(gotList, framework.NodeScore{Name: nodeName, Score: score})
+			}
+			status := p.(framework.ScorePlugin).ScoreExtensions().NormalizeScore(context.Background(), test.requirements, gotList)
+			if !status.IsSuccess() {
+				t.Errorf("unexpected error: %v", status)
+			}
+
+			if !reflect.DeepEqual(test.expectedList, gotList) {
+				t.Errorf("expected:\n\t%+v,\ngot:\n\t%+v", test.expectedList, gotList)
+			}
+		})
+	}
+}
+
+func TestTaintTolerationFilter(t *testing.T) {
+	tests := []struct {
+		name         string
+		requirements *appsapi.ReplicaRequirements
+		node         *v1.Node
+		wantStatus   *framework.Status
+	}{
+		{
+			name:         "A requirement having no tolerations can't be scheduled onto a node with nonempty taints",
+			requirements: requirementsWithTolerations([]v1.Toleration{}),
+			node:         nodeWithTaints("nodeA", []v1.Taint{{Key: "dedicated", Value: "user1", Effect: "NoSchedule"}}),
+			wantStatus: framework.NewStatus(framework.Unpredictable,
+				"node(s) had untolerated taint {dedicated: user1}"),
+		},
+		{
+			name:         "A requirement which can be scheduled on a dedicated node assigned to user1 with effect NoSchedule",
+			requirements: requirementsWithTolerations([]v1.Toleration{{Key: "dedicated", Value: "user1", Effect: "NoSchedule"}}),
+			node:         nodeWithTaints("nodeA", []v1.Taint{{Key: "dedicated", Value: "user1", Effect: "NoSchedule"}}),
+		},
+		{
+			name:         "A requirement which can't be scheduled on a dedicated node assigned to user2 with effect NoSchedule",
+			requirements: requirementsWithTolerations([]v1.Toleration{{Key: "dedicated", Operator: "Equal", Value: "user2", Effect: "NoSchedule"}}),
+			node:         nodeWithTaints("nodeA", []v1.Taint{{Key: "dedicated", Value: "user1", Effect: "NoSchedule"}}),
+			wantStatus: framework.NewStatus(framework.Unpredictable,
+				"node(s) had untolerated taint {dedicated: user1}"),
+		},
+		{
+			name:         "A requirement can be scheduled onto the node, with a toleration uses operator Exists that tolerates the taints on the node",
+			requirements: requirementsWithTolerations([]v1.Toleration{{Key: "foo", Operator: "Exists", Effect: "NoSchedule"}}),
+			node:         nodeWithTaints("nodeA", []v1.Taint{{Key: "foo", Value: "bar", Effect: "NoSchedule"}}),
+		},
+		{
+			name: "A requirement has multiple tolerations, node has multiple taints, all the taints are tolerated, requirement can be scheduled onto the node",
+			requirements: requirementsWithTolerations([]v1.Toleration{
+				{Key: "dedicated", Operator: "Equal", Value: "user2", Effect: "NoSchedule"},
+				{Key: "foo", Operator: "Exists", Effect: "NoSchedule"},
+			}),
+			node: nodeWithTaints("nodeA", []v1.Taint{
+				{Key: "dedicated", Value: "user2", Effect: "NoSchedule"},
+				{Key: "foo", Value: "bar", Effect: "NoSchedule"},
+			}),
+		},
+		{
+			name: "A requirement has a toleration that keys and values match the taint on the node, but (non-empty) effect doesn't match, " +
+				"can't be scheduled onto the node",
+			requirements: requirementsWithTolerations([]v1.Toleration{{Key: "foo", Operator: "Equal", Value: "bar", Effect: "PreferNoSchedule"}}),
+			node:         nodeWithTaints("nodeA", []v1.Taint{{Key: "foo", Value: "bar", Effect: "NoSchedule"}}),
+			wantStatus: framework.NewStatus(framework.Unpredictable,
+				"node(s) had untolerated taint {foo: bar}"),
+		},
+		{
+			name: "The requirement has a toleration that keys and values match the taint on the node, the effect of toleration is empty, " +
+				"and the effect of taint is NoSchedule. Requirement can be scheduled onto the node",
+			requirements: requirementsWithTolerations([]v1.Toleration{{Key: "foo", Operator: "Equal", Value: "bar"}}),
+			node:         nodeWithTaints("nodeA", []v1.Taint{{Key: "foo", Value: "bar", Effect: "NoSchedule"}}),
+		},
+		{
+			name: "The requirement has a toleration that key and value don't match the taint on the node, " +
+				"but the effect of taint on node is PreferNoSchedule. Requirement can be scheduled onto the node",
+			requirements: requirementsWithTolerations([]v1.Toleration{{Key: "dedicated", Operator: "Equal", Value: "user2", Effect: "NoSchedule"}}),
+			node:         nodeWithTaints("nodeA", []v1.Taint{{Key: "dedicated", Value: "user1", Effect: "PreferNoSchedule"}}),
+		},
+		{
+			name: "The requirement has no toleration, " +
+				"but the effect of taint on node is PreferNoSchedule. Requirement can be scheduled onto the node",
+			requirements: requirementsWithTolerations([]v1.Toleration{}),
+			node:         nodeWithTaints("nodeA", []v1.Taint{{Key: "dedicated", Value: "user1", Effect: "PreferNoSchedule"}}),
+		},
+	}
+	for _, test := range tests {
+		t.Run(test.name, func(t *testing.T) {
+			nodeInfo := framework.NewNodeInfo(test.node, nil)
+			p, _ := New(nil, nil)
+			gotStatus := p.(framework.FilterPlugin).Filter(context.Background(), test.requirements, nodeInfo)
+			if !reflect.DeepEqual(gotStatus, test.wantStatus) {
+				t.Errorf("status does not match: %v, want: %v", gotStatus, test.wantStatus)
+			}
+		})
+	}
+}