Repository: clusternet/clusternet
Pull request #214 (2021-12-18 09:28:23)

use kubectl-clusternet plugin to visit child clusters
Signed-off-by: Di Xu <stephenhsu90@gmail.com>

<!--  Thanks for sending a pull request!

-->

#### What type of PR is this?
kind/documentation

#### What this PR does / why we need it:

`kubectl-clusternet` plugin v0.5.0  introduced new flags `cluster-id` and `child-kubeconfig`. This made easier to visit child clusters through parent cluster using CLI, instead of constructing a complicated and dedicated kubeconfig. 

#### Which issue(s) this PR fixes:
<!--
*Automatically closes linked issue when PR is merged.
Usage: `Fixes #<issue number>`, or `Fixes (paste link of issue)`.
-->
partial mitigate #184 

#### Special notes for your reviewer:


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

[![Impacted file tree graph](https://codecov.io/gh/clusternet/clusternet/pull/214/graphs/tree.svg?width=650&height=150&src=pr&token=4O6PTR0RJN&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=clusternet)](https://codecov.io/gh/clusternet/clusternet/pull/214?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=clusternet)

```diff
@@          Coverage Diff          @@
##            main    #214   +/-   ##
=====================================
  Coverage   8.37%   8.37%           
=====================================
  Files         36      36           
  Lines       4142    4142           
=====================================
  Hits         347     347           
  Misses      3767    3767           
  Partials      28      28           
```


| [Impacted Files](https://codecov.io/gh/clusternet/clusternet/pull/214?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/214/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/214?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/214?src=pr&el=footer&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=clusternet). Last update [b0fd5e9...ebabefc](https://codecov.io/gh/clusternet/clusternet/pull/214?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/docs/tutorials/visiting-child-clusters-with-rbac.md b/docs/tutorials/visiting-child-clusters-with-rbac.md
index 4628cf7..da516b3 100644
--- a/docs/tutorials/visiting-child-clusters-with-rbac.md
+++ b/docs/tutorials/visiting-child-clusters-with-rbac.md
@@ -1,9 +1,13 @@
 # Visiting Managed Clusters With RBAC
 
 - [Using curl](#using-curl)
+    - [If you're using tokens](#if-youre-using-tokens)
+    - [If you're using TLS certificates](#if-youre-using-tls-certificates)
 - [Using KubeConfig](#using-kubeconfig)
-    - [Step 1: Modify Server URL](#step-1-modify-server-url)
-    - [Step 2: Configure Credentials from Child Clusters](#step-2-configure-credentials-from-child-clusters)
+    - [An Easy Way - kubectl-clusternet Plugin (Recommended)](#an-easy-way---kubectl-clusternet-plugin-recommended)
+    - [A Hard Way - Constructing a Dedicated KubeConfig](#a-hard-way---constructing-a-dedicated-kubeconfig)
+        - [Step 1: Modify Server URL](#step-1-modify-server-url)
+        - [Step 2: Configure Credentials from Child Clusters](#step-2-configure-credentials-from-child-clusters)
 
 :thumbsup: ***Clusternet supports visiting all your managed clusters with RBAC directly from parent cluster.***
 
@@ -62,11 +66,46 @@ $ curl -k -XGET  -H "Accept: application/json" \
   "${APISERVER}/apis/proxies.clusternet.io/v1alpha1/sockets/${CHILDCLUSTERID}/proxy/direct/api/v1/namespaces"
 ```
 
-## Using KubeConfig
+### Using KubeConfig
 
-You need to follow below **2 steps** to construct a valid kubeconfig to access a child cluster.
+### An Easy Way - kubectl-clusternet Plugin (Recommended)
 
-### Step 1: Modify Server URL
+First,
+please [install/upgrade `kubectl-clusternet` plugin](https://github.com/clusternet/kubectl-clusternet#installation) with
+a minimum required version `v0.5.0`.
+
+```bash
+$ kubectl clusternet version
+{
+  "gitVersion": "0.5.0",
+  "platform": "darwin/amd64"
+}
+```
+
+```bash
+$ kubectl get mcls -A
+NAMESPACE          NAME       CLUSTER ID                             SYNC MODE   KUBERNETES                   READYZ   AGE
+clusternet-ml6wg   aws-cd     6c085c18-3baf-443c-abff-459751f5e3d3   Dual        v1.18.4                      true     4d6h
+clusternet-z5vqv   azure-cd   7dc5966e-6736-48dd-9a82-2e4d74d30443   Dual        v1.20.4                      true     43h
+$ kubectl clusternet --cluster-id=7dc5966e-6736-48dd-9a82-2e4d74d30443 --child-kubeconfig=./azure-cd-kubeconfig get ns
+NAME                STATUS   AGE
+clusternet-system   Active   4d20h
+default             Active   24d
+kube-node-lease     Active   24d
+kube-public         Active   24d
+kube-system         Active   24d
+test-nginx          Active   11d
+test-systemd        Active   11d
+```
+
+Here the apisever in above kubeconfig file `azure-cd-kubeconfig` could be with an inner address. Now you can easily
+check any objects status in child clusters.
+
+### A Hard Way - Constructing a Dedicated KubeConfig
+
+You need to follow below **2 steps** to construct a dedicated kubeconfig to access a child cluster with `kubectl`.
+
+#### Step 1: Modify Server URL
 
 Append `/apis/proxies.clusternet.io/v1alpha1/sockets/<CLUSTER-ID>/proxy/https/<SERVER-URL>`
 or `/apis/proxies.clusternet.io/v1alpha1/sockets/<CLUSTER-ID>/proxy/direct` at the end of original **parent cluster**
@@ -129,7 +168,7 @@ $ kubectl config set-cluster `kubectl config get-clusters | grep -v NAME` \
 >
 > Then follow above url modification as well.
 
-### Step 2: Configure Credentials from Child Clusters
+#### Step 2: Configure Credentials from Child Clusters
 
 Then update user entry with **credentials from child clusters**
 
@@ -243,7 +282,7 @@ and `CLIENT-KEY-DATE-PLEASE-BASE64-ENCODED-CHANGE-ME` with certficate and privat
 notice the tokens replaced here should be base64 encoded.**
 
 > :pushpin: :pushpin: Important Note:
-> 
+>
 > If anonymous auth is not allowed, please replace `username: system:anonymous` to `token: PARENT-CLUSTER-TOKEN`.
 > Here `PARENT-CLUSTER-TOKEN` can be retrieved with below command,
 >
diff --git a/go.mod b/go.mod
index 3733901..d580b0c 100644
--- a/go.mod
+++ b/go.mod
@@ -4,6 +4,7 @@ go 1.14
 
 require (
 	github.com/containerd/containerd v1.4.12 // indirect
+	github.com/davecgh/go-spew v1.1.1
 	github.com/emicklei/go-restful v2.9.5+incompatible
 	github.com/evanphx/json-patch v4.11.0+incompatible
 	github.com/go-openapi/spec v0.19.5
diff --git a/manifests/crds/apps.clusternet.io_subscriptions.yaml b/manifests/crds/apps.clusternet.io_subscriptions.yaml
index 1e61983..6e01599 100644
--- a/manifests/crds/apps.clusternet.io_subscriptions.yaml
+++ b/manifests/crds/apps.clusternet.io_subscriptions.yaml
@@ -41,6 +41,29 @@ spec:
           spec:
             description: SubscriptionSpec defines the desired state of Subscription
             properties:
+              clusterTolerations:
+                description: ClusterTolerations tolerates any matched taints of ManagedCluster.
+                items:
+                  description: The pod this Toleration is attached to tolerates any taint that matches the triple <key,value,effect> using the matching operator <operator>.
+                  properties:
+                    effect:
+                      description: Effect indicates the taint effect to match. Empty means match all taint effects. When specified, allowed values are NoSchedule, PreferNoSchedule and NoExecute.
+                      type: string
+                    key:
+                      description: Key is the taint key that the toleration applies to. Empty means match all taint keys. If the key is empty, operator must be Exists; this combination means to match all values and all keys.
+                      type: string
+                    operator:
+                      description: Operator represents a key's relationship to the value. Valid operators are Exists and Equal. Defaults to Equal. Exists is equivalent to wildcard for value, so that a pod can tolerate all taints of a particular category.
+                      type: string
+                    tolerationSeconds:
+                      description: TolerationSeconds represents the period of time the toleration (which must be of effect NoExecute, otherwise this field is ignored) tolerates the taint. By default, it is not set, which means tolerate the taint forever (do not evict). Zero and negative values will be treated as 0 (evict immediately) by the system.
+                      format: int64
+                      type: integer
+                    value:
+                      description: Value is the taint value the toleration matches to. If the operator is Exists, the value should be empty, otherwise just a regular string.
+                      type: string
+                  type: object
+                type: array
               feeds:
                 description: Feeds
                 items:
@@ -110,29 +133,6 @@ spec:
                           description: matchLabels is a map of {key,value} pairs. A single {key,value} in the matchLabels map is equivalent to an element of matchExpressions, whose key field is "key", the operator is "In", and the values array contains only "value". The requirements are ANDed.
                           type: object
                       type: object
-                    clusterTolerations:
-                      description: ClusterTolerations tolerates any matched taints of ManagedCluster.
-                      items:
-                        description: The pod this Toleration is attached to tolerates any taint that matches the triple <key,value,effect> using the matching operator <operator>.
-                        properties:
-                          effect:
-                            description: Effect indicates the taint effect to match. Empty means match all taint effects. When specified, allowed values are NoSchedule, PreferNoSchedule and NoExecute.
-                            type: string
-                          key:
-                            description: Key is the taint key that the toleration applies to. Empty means match all taint keys. If the key is empty, operator must be Exists; this combination means to match all values and all keys.
-                            type: string
-                          operator:
-                            description: Operator represents a key's relationship to the value. Valid operators are Exists and Equal. Defaults to Equal. Exists is equivalent to wildcard for value, so that a pod can tolerate all taints of a particular category.
-                            type: string
-                          tolerationSeconds:
-                            description: TolerationSeconds represents the period of time the toleration (which must be of effect NoExecute, otherwise this field is ignored) tolerates the taint. By default, it is not set, which means tolerate the taint forever (do not evict). Zero and negative values will be treated as 0 (evict immediately) by the system.
-                            format: int64
-                            type: integer
-                          value:
-                            description: Value is the taint value the toleration matches to. If the operator is Exists, the value should be empty, otherwise just a regular string.
-                            type: string
-                        type: object
-                      type: array
                   required:
                   - clusterAffinity
                   type: object
@@ -150,12 +150,14 @@ spec:
                   type: string
                 type: array
               completedReleases:
-                description: Total number of completed releases targeted by this deployment.
-                format: int32
+                description: Total number of completed releases targeted by this Subscription.
                 type: integer
               desiredReleases:
                 description: Total number of Helm releases desired by this Subscription.
-                format: int32
+                type: integer
+              specHash:
+                description: SpecHash calculates the hash value of current SubscriptionSpec.
+                format: int64
                 type: integer
             type: object
         required:
diff --git a/pkg/agent/agent.go b/pkg/agent/agent.go
index 06a3b5b..be2b803 100644
--- a/pkg/agent/agent.go
+++ b/pkg/agent/agent.go
@@ -127,7 +127,7 @@ func (agent *Agent) Run() error {
 
 	// if leader election is disabled, so runCommand inline until done.
 	if !agent.controllerOptions.LeaderElection.LeaderElect {
-		wait.UntilWithContext(agent.ctx, agent.run, 0)
+		agent.run(agent.ctx)
 		klog.Warning("finished without leader elect")
 		return nil
 	}
@@ -147,7 +147,7 @@ func (agent *Agent) Run() error {
 		agent.childKubeClientSet,
 		leaderelection.LeaderCallbacks{
 			OnStartedLeading: func(ctx context.Context) {
-				wait.UntilWithContext(ctx, agent.run, 0)
+				agent.run(ctx)
 			},
 			OnStoppedLeading: func() {
 				klog.Error("leader election got lost")
@@ -197,6 +197,8 @@ func (agent *Agent) run(ctx context.Context) {
 			klog.Error(err)
 		}
 	}, time.Duration(0))
+
+	<-ctx.Done()
 }
 
 // registerSelfCluster begins registering. It starts registering and blocked until the context is done.
diff --git a/pkg/apis/apps/v1alpha1/subscription.go b/pkg/apis/apps/v1alpha1/subscription.go
index 8c51bbf..b7ddb35 100644
--- a/pkg/apis/apps/v1alpha1/subscription.go
+++ b/pkg/apis/apps/v1alpha1/subscription.go
@@ -62,6 +62,11 @@ type SubscriptionSpec struct {
 	// +kubebuilder:validation:Required
 	Subscribers []Subscriber `json:"subscribers"`
 
+	// ClusterTolerations tolerates any matched taints of ManagedCluster.
+	//
+	// +optional
+	ClusterTolerations []corev1.Toleration `json:"clusterTolerations,omitempty"`
+
 	// Feeds
 	//
 	// +required
@@ -76,15 +81,20 @@ type SubscriptionStatus struct {
 	// +optional
 	BindingNamespaces []string `json:"bindingNamespaces,omitempty"`
 
+	// SpecHash calculates the hash value of current SubscriptionSpec.
+	//
+	// +optional
+	SpecHash uint64 `json:"specHash,omitempty"`
+
 	// Total number of Helm releases desired by this Subscription.
 	//
 	// +optional
-	DesiredReleases int32 `json:"desiredReleases,omitempty"`
+	DesiredReleases int `json:"desiredReleases,omitempty"`
 
-	// Total number of completed releases targeted by this deployment.
+	// Total number of completed releases targeted by this Subscription.
 	//
 	// +optional
-	CompletedReleases int32 `json:"completedReleases,omitempty"`
+	CompletedReleases int `json:"completedReleases,omitempty"`
 }
 
 // Subscriber defines
@@ -94,10 +104,6 @@ type Subscriber struct {
 	// +required
 	// +kubebuilder:validation:Required
 	ClusterAffinity *metav1.LabelSelector `json:"clusterAffinity"`
-
-	// ClusterTolerations tolerates any matched taints of ManagedCluster.
-	// +optional
-	ClusterTolerations []corev1.Toleration `json:"clusterTolerations,omitempty"`
 }
 
 // Feed defines the resource to be selected.
diff --git a/pkg/apis/apps/v1alpha1/zz_generated.deepcopy.go b/pkg/apis/apps/v1alpha1/zz_generated.deepcopy.go
index b45acc0..0db01bf 100644
--- a/pkg/apis/apps/v1alpha1/zz_generated.deepcopy.go
+++ b/pkg/apis/apps/v1alpha1/zz_generated.deepcopy.go
@@ -721,13 +721,6 @@ func (in *Subscriber) DeepCopyInto(out *Subscriber) {
 		*out = new(v1.LabelSelector)
 		(*in).DeepCopyInto(*out)
 	}
-	if in.ClusterTolerations != nil {
-		in, out := &in.ClusterTolerations, &out.ClusterTolerations
-		*out = make([]corev1.Toleration, len(*in))
-		for i := range *in {
-			(*in)[i].DeepCopyInto(&(*out)[i])
-		}
-	}
 	return
 }
 
@@ -812,6 +805,13 @@ func (in *SubscriptionSpec) DeepCopyInto(out *SubscriptionSpec) {
 			(*in)[i].DeepCopyInto(&(*out)[i])
 		}
 	}
+	if in.ClusterTolerations != nil {
+		in, out := &in.ClusterTolerations, &out.ClusterTolerations
+		*out = make([]corev1.Toleration, len(*in))
+		for i := range *in {
+			(*in)[i].DeepCopyInto(&(*out)[i])
+		}
+	}
 	if in.Feeds != nil {
 		in, out := &in.Feeds, &out.Feeds
 		*out = make([]Feed, len(*in))
diff --git a/pkg/controllers/apps/subscription/subscription.go b/pkg/controllers/apps/subscription/subscription.go
index fae64ec..0aaca9d 100644
--- a/pkg/controllers/apps/subscription/subscription.go
+++ b/pkg/controllers/apps/subscription/subscription.go
@@ -21,13 +21,11 @@ import (
 	"encoding/json"
 	"fmt"
 	"reflect"
-	"sync"
 	"time"
 
 	corev1 "k8s.io/api/core/v1"
 	"k8s.io/apimachinery/pkg/api/errors"
 	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
-	"k8s.io/apimachinery/pkg/labels"
 	"k8s.io/apimachinery/pkg/types"
 	utilruntime "k8s.io/apimachinery/pkg/util/runtime"
 	"k8s.io/apimachinery/pkg/util/wait"
@@ -39,12 +37,9 @@ import (
 	utilpointer "k8s.io/utils/pointer"
 
 	appsapi "github.com/clusternet/clusternet/pkg/apis/apps/v1alpha1"
-	clusterapi "github.com/clusternet/clusternet/pkg/apis/clusters/v1beta1"
 	clusternetclientset "github.com/clusternet/clusternet/pkg/generated/clientset/versioned"
 	appinformers "github.com/clusternet/clusternet/pkg/generated/informers/externalversions/apps/v1alpha1"
-	clusterinformers "github.com/clusternet/clusternet/pkg/generated/informers/externalversions/clusters/v1beta1"
 	applisters "github.com/clusternet/clusternet/pkg/generated/listers/apps/v1alpha1"
-	clusterlisters "github.com/clusternet/clusternet/pkg/generated/listers/clusters/v1beta1"
 	"github.com/clusternet/clusternet/pkg/known"
 	"github.com/clusternet/clusternet/pkg/utils"
 )
@@ -56,9 +51,6 @@ type SyncHandlerFunc func(subscription *appsapi.Subscription) error
 
 // Controller is a controller that handle Subscription
 type Controller struct {
-	lock           sync.RWMutex
-	subscribersMap map[string][]appsapi.Subscriber
-
 	clusternetClient clusternetclientset.Interface
 
 	// workqueue is a rate limited work queue. This is used to queue work to be
@@ -68,33 +60,30 @@ type Controller struct {
 	// simultaneously in two different workers.
 	workqueue workqueue.RateLimitingInterface
 
-	subsLister    applisters.SubscriptionLister
-	subsSynced    cache.InformerSynced
-	baseSynced    cache.InformerSynced
-	clusterLister clusterlisters.ManagedClusterLister
-	clusterSynced cache.InformerSynced
+	subsLister applisters.SubscriptionLister
+	subsSynced cache.InformerSynced
+	baseSynced cache.InformerSynced
 
 	recorder record.EventRecorder
 
 	syncHandlerFunc SyncHandlerFunc
 }
 
+// NewController returns a new Controller, which is only responsible for populating Bases.
+// The scheduling parts are handled by clusternet-scheduler.
 func NewController(clusternetClient clusternetclientset.Interface,
 	subsInformer appinformers.SubscriptionInformer, baseInformer appinformers.BaseInformer,
-	clusterInformer clusterinformers.ManagedClusterInformer, recorder record.EventRecorder, syncHandlerFunc SyncHandlerFunc) (*Controller, error) {
+	recorder record.EventRecorder, syncHandlerFunc SyncHandlerFunc) (*Controller, error) {
 	if syncHandlerFunc == nil {
 		return nil, fmt.Errorf("syncHandlerFunc must be set")
 	}
 
 	c := &Controller{
-		subscribersMap:   make(map[string][]appsapi.Subscriber),
 		clusternetClient: clusternetClient,
 		workqueue:        workqueue.NewNamedRateLimitingQueue(workqueue.DefaultControllerRateLimiter(), "subscription"),
 		subsLister:       subsInformer.Lister(),
 		subsSynced:       subsInformer.Informer().HasSynced,
 		baseSynced:       baseInformer.Informer().HasSynced,
-		clusterLister:    clusterInformer.Lister(),
-		clusterSynced:    clusterInformer.Informer().HasSynced,
 		recorder:         recorder,
 		syncHandlerFunc:  syncHandlerFunc,
 	}
@@ -106,12 +95,6 @@ func NewController(clusternetClient clusternetclientset.Interface,
 		DeleteFunc: c.deleteSubscription,
 	})
 
-	clusterInformer.Informer().AddEventHandler(cache.ResourceEventHandlerFuncs{
-		AddFunc:    c.addCluster,
-		UpdateFunc: c.updateCluster,
-		DeleteFunc: c.deleteCluster,
-	})
-
 	baseInformer.Informer().AddEventHandler(cache.ResourceEventHandlerFuncs{
 		DeleteFunc: c.deleteBase,
 	})
@@ -134,7 +117,6 @@ func (c *Controller) Run(workers int, stopCh <-chan struct{}) {
 	if !cache.WaitForNamedCacheSync("subscription-controller", stopCh,
 		c.subsSynced,
 		c.baseSynced,
-		c.clusterSynced,
 	) {
 		return
 	}
@@ -150,11 +132,6 @@ func (c *Controller) Run(workers int, stopCh <-chan struct{}) {
 
 func (c *Controller) addSubscription(obj interface{}) {
 	sub := obj.(*appsapi.Subscription)
-
-	c.lock.Lock()
-	defer c.lock.Unlock()
-	c.subscribersMap[klog.KObj(sub).String()] = sub.Spec.Subscribers
-
 	klog.V(4).Infof("adding Subscription %q", klog.KObj(sub))
 	c.enqueue(sub)
 }
@@ -168,15 +145,18 @@ func (c *Controller) updateSubscription(old, cur interface{}) {
 		return
 	}
 
-	// Decide whether discovery has reported a spec change.
-	if reflect.DeepEqual(oldSub.Spec, newSub.Spec) {
-		klog.V(4).Infof("no updates on the spec of Subscription %s, skipping syncing", klog.KObj(oldSub))
+	// Decide whether discovery has reported a status change.
+	// clusternet-scheduler is responsible for spec changes.
+	if reflect.DeepEqual(oldSub.Status, newSub.Status) {
+		klog.V(4).Infof("no updates on the status of Subscription %s, skipping syncing", klog.KObj(oldSub))
+		return
+	}
+	// no changes on binding namespaces and spec hash
+	if reflect.DeepEqual(oldSub.Status.BindingNamespaces, newSub.Status.BindingNamespaces) &&
+		oldSub.Status.SpecHash == newSub.Status.SpecHash {
+		klog.V(4).Infof("no changes on binding namespaces and spec hash of Subscription %s, skipping syncing", klog.KObj(oldSub))
 		return
 	}
-
-	c.lock.Lock()
-	defer c.lock.Unlock()
-	c.subscribersMap[klog.KObj(newSub).String()] = newSub.Spec.Subscribers
 
 	klog.V(4).Infof("updating Subscription %q", klog.KObj(oldSub))
 	c.enqueue(newSub)
@@ -197,10 +177,6 @@ func (c *Controller) deleteSubscription(obj interface{}) {
 		}
 	}
 
-	c.lock.Lock()
-	defer c.lock.Unlock()
-	delete(c.subscribersMap, klog.KObj(sub).String())
-
 	klog.V(4).Infof("deleting Subscription %q", klog.KObj(sub))
 	c.enqueue(sub)
 }
@@ -244,63 +220,6 @@ func (c *Controller) resolveControllerRef(name, namespace string, uid types.UID)
 	return sub
 }
 
-func (c *Controller) addCluster(obj interface{}) {
-	mcls := obj.(*clusterapi.ManagedCluster)
-	klog.V(5).Infof("adding ManagedCluster %q", klog.KObj(mcls))
-	c.enqueueSubscriptionForCluster(mcls)
-}
-
-func (c *Controller) updateCluster(old, cur interface{}) {
-	oldMcls := old.(*clusterapi.ManagedCluster)
-	newMcls := cur.(*clusterapi.ManagedCluster)
-
-	if newMcls.DeletionTimestamp != nil {
-		c.deleteCluster(cur)
-		return
-	}
-
-	// Decide whether discovery has reported a label change.
-	if reflect.DeepEqual(oldMcls.Labels, newMcls.Labels) {
-		klog.V(4).Infof("no updates on the labels of ManagedCluster %s, skipping syncing", klog.KObj(oldMcls))
-		return
-	}
-
-	klog.V(5).Infof("updating ManagedCluster %q", klog.KObj(oldMcls))
-	c.enqueueSubscriptionForCluster(newMcls)
-}
-
-func (c *Controller) deleteCluster(obj interface{}) {
-	// when a ManagedCluster is deleted,
-	// - Auto populated objects, like Base and Description, will be auto-deleted on next sync/resync of subscribed Subscriptions
-	// - If current dedicated namespace is deleted, then all objects in this namespaces will be pruned.
-
-	// TODO (dixudx): prune all related objects immediately?
-}
-
-func (c *Controller) enqueueSubscriptionForCluster(mcls *clusterapi.ManagedCluster) {
-	c.lock.RLock()
-	subscribersMap := c.subscribersMap
-	c.lock.RUnlock()
-
-	for key, subscribers := range subscribersMap {
-		for _, subscriber := range subscribers {
-			selector, err := metav1.LabelSelectorAsSelector(subscriber.ClusterAffinity)
-			if err != nil {
-				klog.ErrorDepth(5, fmt.Sprintf("failed to parse labelSelector in Subscription %s: %v", key, err))
-				continue
-			}
-			if !selector.Matches(labels.Set(mcls.Labels)) {
-				continue
-			}
-			if utils.IsClusterUntolerated(mcls, subscriber.ClusterTolerations) {
-				continue
-			}
-			c.workqueue.Add(key)
-			break
-		}
-	}
-}
-
 // runWorker is a long-running function that will continually call the
 // processNextWorkItem function in order to read and process a message on the
 // workqueue.
diff --git a/pkg/controllers/proxies/sockets/socket_controller.go b/pkg/controllers/proxies/sockets/socket_controller.go
index 16fc8ce..607bef9 100644
--- a/pkg/controllers/proxies/sockets/socket_controller.go
+++ b/pkg/controllers/proxies/sockets/socket_controller.go
@@ -22,7 +22,6 @@ import (
 	"net/http"
 	"net/url"
 	"path"
-	"time"
 
 	"github.com/gorilla/websocket"
 	"github.com/rancher/remotedialer"
@@ -34,6 +33,7 @@ import (
 	"k8s.io/klog/v2"
 
 	proxiesapi "github.com/clusternet/clusternet/pkg/apis/proxies/v1alpha1"
+	"github.com/clusternet/clusternet/pkg/known"
 )
 
 // Controller is a controller that helps setup/maintain websocket connection
@@ -97,10 +97,10 @@ func (c *Controller) Run(ctx context.Context, clusterID *types.UID) {
 	wsURL := fmt.Sprintf("%s/%s", c.baseURL, string(*clusterID))
 	klog.V(4).Infof("setting up websocket connection to %s", wsURL)
 
-	wait.UntilWithContext(ctx, func(ctx context.Context) {
+	wait.JitterUntilWithContext(ctx, func(ctx context.Context) {
 		err := remotedialer.ClientConnect(ctx, wsURL, c.headers, c.dialer, func(string, string) bool { return true }, nil)
 		if err != nil {
 			klog.Errorf("websocket connection error: %v", err)
 		}
-	}, time.Duration(0))
+	}, known.DefaultRetryPeriod, 0.3, true)
 }
diff --git a/pkg/hub/deployer/deployer.go b/pkg/hub/deployer/deployer.go
index bf62591..03d7423 100644
--- a/pkg/hub/deployer/deployer.go
+++ b/pkg/hub/deployer/deployer.go
@@ -38,13 +38,13 @@ import (
 	utilfeature "k8s.io/apiserver/pkg/util/feature"
 	kubeinformers "k8s.io/client-go/informers"
 	"k8s.io/client-go/kubernetes"
+	corev1lister "k8s.io/client-go/listers/core/v1"
 	"k8s.io/client-go/tools/cache"
 	"k8s.io/client-go/tools/record"
 	"k8s.io/klog/v2"
 	utilpointer "k8s.io/utils/pointer"
 
 	appsapi "github.com/clusternet/clusternet/pkg/apis/apps/v1alpha1"
-	clusterapi "github.com/clusternet/clusternet/pkg/apis/clusters/v1beta1"
 	"github.com/clusternet/clusternet/pkg/controllers/apps/base"
 	"github.com/clusternet/clusternet/pkg/controllers/apps/helmchart"
 	"github.com/clusternet/clusternet/pkg/controllers/apps/manifest"
@@ -53,7 +53,6 @@ import (
 	clusternetclientset "github.com/clusternet/clusternet/pkg/generated/clientset/versioned"
 	clusternetinformers "github.com/clusternet/clusternet/pkg/generated/informers/externalversions"
 	applisters "github.com/clusternet/clusternet/pkg/generated/listers/apps/v1alpha1"
-	clusterlisters "github.com/clusternet/clusternet/pkg/generated/listers/clusters/v1beta1"
 	"github.com/clusternet/clusternet/pkg/hub/deployer/generic"
 	"github.com/clusternet/clusternet/pkg/hub/deployer/helm"
 	"github.com/clusternet/clusternet/pkg/hub/localizer"
@@ -68,24 +67,20 @@ var (
 	deletePropagationBackground = metav1.DeletePropagationBackground
 )
 
-const (
-	defaultScheduler = "default"
-)
-
 // Deployer defines configuration for the application deployer
 type Deployer struct {
-	chartLister   applisters.HelmChartLister
-	chartSynced   cache.InformerSynced
-	descLister    applisters.DescriptionLister
-	descSynced    cache.InformerSynced
-	baseLister    applisters.BaseLister
-	baseSynced    cache.InformerSynced
-	mfstLister    applisters.ManifestLister
-	mfstSynced    cache.InformerSynced
-	subLister     applisters.SubscriptionLister
-	subSynced     cache.InformerSynced
-	clusterLister clusterlisters.ManagedClusterLister
-	clusterSynced cache.InformerSynced
+	chartLister applisters.HelmChartLister
+	chartSynced cache.InformerSynced
+	descLister  applisters.DescriptionLister
+	descSynced  cache.InformerSynced
+	baseLister  applisters.BaseLister
+	baseSynced  cache.InformerSynced
+	mfstLister  applisters.ManifestLister
+	mfstSynced  cache.InformerSynced
+	subLister   applisters.SubscriptionLister
+	subSynced   cache.InformerSynced
+	nsLister    corev1lister.NamespaceLister
+	nsSynced    cache.InformerSynced
 
 	clusternetClient *clusternetclientset.Clientset
 	kubeClient       *kubernetes.Clientset
@@ -128,8 +123,8 @@ func NewDeployer(apiserverURL, systemNamespace, reservedNamespace string,
 		mfstSynced:        clusternetInformerFactory.Apps().V1alpha1().Manifests().Informer().HasSynced,
 		subLister:         clusternetInformerFactory.Apps().V1alpha1().Subscriptions().Lister(),
 		subSynced:         clusternetInformerFactory.Apps().V1alpha1().Subscriptions().Informer().HasSynced,
-		clusterLister:     clusternetInformerFactory.Clusters().V1beta1().ManagedClusters().Lister(),
-		clusterSynced:     clusternetInformerFactory.Clusters().V1beta1().ManagedClusters().Informer().HasSynced,
+		nsLister:          kubeInformerFactory.Core().V1().Namespaces().Lister(),
+		nsSynced:          kubeInformerFactory.Core().V1().Namespaces().Informer().HasSynced,
 		clusternetClient:  clusternetclient,
 		kubeClient:        kubeclient,
 		recorder:          recorder,
@@ -164,7 +159,6 @@ func NewDeployer(apiserverURL, systemNamespace, reservedNamespace string,
 	subsController, err := subscription.NewController(clusternetclient,
 		clusternetInformerFactory.Apps().V1alpha1().Subscriptions(),
 		clusternetInformerFactory.Apps().V1alpha1().Bases(),
-		clusternetInformerFactory.Clusters().V1beta1().ManagedClusters(),
 		deployer.recorder,
 		deployer.handleSubscription)
 	if err != nil {
@@ -214,8 +208,8 @@ func (deployer *Deployer) Run(workers int, stopCh <-chan struct{}) {
 		deployer.descSynced,
 		deployer.baseSynced,
 		deployer.mfstSynced,
-		deployer.clusterSynced,
 		deployer.subSynced,
+		deployer.nsSynced,
 	) {
 		return
 	}
@@ -275,13 +269,6 @@ func (deployer *Deployer) handleSubscription(sub *appsapi.Subscription) error {
 		return err
 	}
 
-	if sub.Spec.SchedulerName != defaultScheduler {
-		klog.V(4).Infof("Subscription %s is using customized scheduler %q ", klog.KObj(sub), sub.Spec.SchedulerName)
-		deployer.recorder.Event(sub, corev1.EventTypeNormal, "SkipScheduling",
-			fmt.Sprintf("customized scheduler %s is specified", sub.Spec.SchedulerName))
-		return nil
-	}
-
 	err := deployer.populateBases(sub)
 	if err != nil {
 		return err
@@ -291,27 +278,6 @@ func (deployer *Deployer) handleSubscription(sub *appsapi.Subscription) error {
 }
 
 func (deployer *Deployer) populateBases(sub *appsapi.Subscription) error {
-	var mcls []*clusterapi.ManagedCluster
-	for _, subscriber := range sub.Spec.Subscribers {
-		selector, err := metav1.LabelSelectorAsSelector(subscriber.ClusterAffinity)
-		if err != nil {
-			return err
-		}
-		clusters, err := deployer.clusterLister.ManagedClusters("").List(selector)
-		if err != nil {
-			return err
-		}
-
-		if clusters == nil {
-			deployer.recorder.Event(sub, corev1.EventTypeWarning, "NoClusters", "No clusters get matched")
-			return nil
-		}
-
-		clusters = utils.FilterClustersByTolerations(clusters, subscriber.ClusterTolerations)
-
-		mcls = append(mcls, clusters...)
-	}
-
 	allExistingBases, err := deployer.baseLister.List(labels.SelectorFromSet(labels.Set{
 		known.ConfigKindLabel:      subscriptionKind.Kind,
 		known.ConfigNameLabel:      sub.Name,
@@ -328,19 +294,25 @@ func (deployer *Deployer) populateBases(sub *appsapi.Subscription) error {
 	}
 
 	var allErrs []error
-	for _, cluster := range mcls {
+	for _, namespace := range sub.Status.BindingNamespaces {
+		ns, listErr := deployer.nsLister.Get(namespace)
+		if listErr != nil {
+			if apierrors.IsNotFound(listErr) {
+				continue
+			}
+			return fmt.Errorf("failed to populate Bases for Subscription %s: %v", klog.KObj(sub), err)
+		}
+
 		base := &appsapi.Base{
 			ObjectMeta: metav1.ObjectMeta{
 				Name:      sub.Name,
-				Namespace: cluster.Namespace,
+				Namespace: namespace,
 				Labels: map[string]string{
 					known.ObjectCreatedByLabel: known.ClusternetHubName,
 					known.ConfigKindLabel:      subscriptionKind.Kind,
 					known.ConfigNameLabel:      sub.Name,
 					known.ConfigNamespaceLabel: sub.Namespace,
 					known.ConfigUIDLabel:       string(sub.UID),
-					known.ClusterIDLabel:       cluster.Labels[known.ClusterIDLabel],
-					known.ClusterNameLabel:     cluster.Labels[known.ClusterNameLabel],
 					// add subscription info
 					known.ConfigSubscriptionNameLabel:      sub.Name,
 					known.ConfigSubscriptionNamespaceLabel: sub.Namespace,
@@ -355,6 +327,10 @@ func (deployer *Deployer) populateBases(sub *appsapi.Subscription) error {
 				Feeds: sub.Spec.Feeds,
 			},
 		}
+		if ns.Labels != nil {
+			base.Labels[known.ClusterIDLabel] = ns.Labels[known.ClusterIDLabel]
+			base.Labels[known.ClusterNameLabel] = ns.Labels[known.ClusterNameLabel]
+		}
 
 		err := deployer.syncBase(sub, base)
 		if err != nil {
diff --git a/pkg/scheduler/algorithm/generic.go b/pkg/scheduler/algorithm/generic.go
index 4d06dd9..ccaea5b 100644
--- a/pkg/scheduler/algorithm/generic.go
+++ b/pkg/scheduler/algorithm/generic.go
@@ -147,12 +147,18 @@ func (g *genericScheduler) findClustersThatFitSubscription(ctx context.Context,
 		UnschedulablePlugins: sets.NewString(),
 	}
 
+	var allClusters []*clusterapi.ManagedCluster
+	for _, subscriber := range sub.Spec.Subscribers {
+		clusters, err := g.cache.List(subscriber.ClusterAffinity)
+		if err != nil {
+			return nil, diagnosis, err
+		}
+		allClusters = append(allClusters, clusters...)
+	}
+	allClusters = normalizedClusters(allClusters)
+
 	// Run "prefilter" plugins.
 	s := fwk.RunPreFilterPlugins(ctx, sub)
-	allClusters, err := g.cache.List()
-	if err != nil {
-		return nil, diagnosis, err
-	}
 	if !s.IsSuccess() {
 		if !s.IsUnschedulable() {
 			return nil, diagnosis, s.AsError()
@@ -309,3 +315,19 @@ func NewGenericScheduler(cache schedulercache.Cache) ScheduleAlgorithm {
 		percentageOfClustersToScore: schedulerapis.DefaultPercentageOfClustersToScore,
 	}
 }
+
+// normalizedClusters will remove duplicate clusters. Deleting clusters will be removed as well.
+func normalizedClusters(clusters []*clusterapi.ManagedCluster) []*clusterapi.ManagedCluster {
+	allKeys := make(map[string]bool)
+	var uniqueClusters []*clusterapi.ManagedCluster
+	for _, cluster := range clusters {
+		if _, ok := allKeys[klog.KObj(cluster).String()]; !ok {
+			if cluster.DeletionTimestamp != nil {
+				continue
+			}
+			uniqueClusters = append(uniqueClusters, cluster)
+			allKeys[klog.KObj(cluster).String()] = true
+		}
+	}
+	return uniqueClusters
+}
diff --git a/pkg/scheduler/cache/cache.go b/pkg/scheduler/cache/cache.go
index c3ec491..bdd47b2 100644
--- a/pkg/scheduler/cache/cache.go
+++ b/pkg/scheduler/cache/cache.go
@@ -19,7 +19,7 @@ package cache
 import (
 	"fmt"
 
-	"k8s.io/apimachinery/pkg/labels"
+	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
 	utilruntime "k8s.io/apimachinery/pkg/util/runtime"
 	"k8s.io/client-go/tools/cache"
 	"k8s.io/klog/v2"
@@ -33,7 +33,7 @@ type Cache interface {
 	NumClusters() int
 
 	// List returns the list of ManagedCluster(s).
-	List() ([]*clusterapi.ManagedCluster, error)
+	List(labelSelector *metav1.LabelSelector) ([]*clusterapi.ManagedCluster, error)
 
 	// Get returns the ManagedCluster of the given managed cluster.
 	Get(namespacedName string) (*clusterapi.ManagedCluster, error)
@@ -45,7 +45,7 @@ type schedulerCache struct {
 
 // NumClusters returns the number of clusters in the cache.
 func (s *schedulerCache) NumClusters() int {
-	clusters, err := s.List()
+	clusters, err := s.List(&metav1.LabelSelector{})
 	if err != nil {
 		klog.Errorf("failed to list clusters: %v", err)
 		return 0
@@ -54,8 +54,12 @@ func (s *schedulerCache) NumClusters() int {
 }
 
 // List returns the list of clusters in the cache.
-func (s *schedulerCache) List() ([]*clusterapi.ManagedCluster, error) {
-	return s.clusterListers.List(labels.Everything())
+func (s *schedulerCache) List(labelSelector *metav1.LabelSelector) ([]*clusterapi.ManagedCluster, error) {
+	selector, err := metav1.LabelSelectorAsSelector(labelSelector)
+	if err != nil {
+		return nil, err
+	}
+	return s.clusterListers.List(selector)
 }
 
 // Get returns the ManagedCluster of the given cluster.
diff --git a/pkg/scheduler/default_plugins.go b/pkg/scheduler/default_plugins.go
new file mode 100644
index 0000000..25a9fda
--- /dev/null
+++ b/pkg/scheduler/default_plugins.go
@@ -0,0 +1,50 @@
+/*
+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 scheduler
+
+import (
+	schedulerapis "github.com/clusternet/clusternet/pkg/scheduler/apis"
+	"github.com/clusternet/clusternet/pkg/scheduler/framework/plugins/names"
+)
+
+// getDefaultPlugins returns the default set of plugins.
+func getDefaultPlugins() *schedulerapis.Plugins {
+	return &schedulerapis.Plugins{
+		PreFilter: schedulerapis.PluginSet{},
+		Filter: schedulerapis.PluginSet{
+			Enabled: []schedulerapis.Plugin{
+				{Name: names.TaintToleration},
+			},
+		},
+		PostFilter: schedulerapis.PluginSet{},
+		PreScore:   schedulerapis.PluginSet{},
+		Score: schedulerapis.PluginSet{
+			Enabled: []schedulerapis.Plugin{
+				{Name: names.TaintToleration, Weight: 3},
+			},
+		},
+		Reserve: schedulerapis.PluginSet{},
+		Permit:  schedulerapis.PluginSet{},
+		PreBind: schedulerapis.PluginSet{},
+		Bind: schedulerapis.PluginSet{
+			Enabled: []schedulerapis.Plugin{
+				{Name: names.DefaultBinder},
+			},
+		},
+		PostBind: schedulerapis.PluginSet{},
+	}
+}
diff --git a/pkg/scheduler/framework/interfaces/interface.go b/pkg/scheduler/framework/interfaces/interface.go
index 0dfef98..5c361eb 100644
--- a/pkg/scheduler/framework/interfaces/interface.go
+++ b/pkg/scheduler/framework/interfaces/interface.go
@@ -25,13 +25,13 @@ import (
 	"time"
 
 	"k8s.io/apimachinery/pkg/types"
-	"k8s.io/client-go/informers"
-	clientset "k8s.io/client-go/kubernetes"
 	restclient "k8s.io/client-go/rest"
 	"k8s.io/client-go/tools/record"
 
 	appsapi "github.com/clusternet/clusternet/pkg/apis/apps/v1alpha1"
 	clusterapi "github.com/clusternet/clusternet/pkg/apis/clusters/v1beta1"
+	clusternet "github.com/clusternet/clusternet/pkg/generated/clientset/versioned"
+	informers "github.com/clusternet/clusternet/pkg/generated/informers/externalversions"
 	schedulerapis "github.com/clusternet/clusternet/pkg/scheduler/apis"
 	"github.com/clusternet/clusternet/pkg/scheduler/parallelize"
 )
@@ -188,7 +188,7 @@ type ScoreExtensions interface {
 	// NormalizeScore is called for all cluster scores produced by the same plugin's "Score"
 	// method. A successful run of NormalizeScore will update the scores list and return
 	// a success status.
-	NormalizeScore(ctx context.Context, sub *appsapi.Subscription, scores ClusterScoreList) *Status
+	NormalizeScore(ctx context.Context, scores ClusterScoreList) *Status
 }
 
 // ScorePlugin is an interface that must be implemented by "Score" plugins to rank
@@ -201,7 +201,7 @@ type ScorePlugin interface {
 	// the subscription will be rejected.
 	Score(ctx context.Context, sub *appsapi.Subscription, namespacedCluster string) (int64, *Status)
 
-	// ScoreExtensions returns a ScoreExtensions interface if it implements one, or nil if does not.
+	// ScoreExtensions returns a ScoreExtensions interface if it implements one, or nil if not.
 	ScoreExtensions() ScoreExtensions
 }
 
@@ -363,10 +363,10 @@ type Handle interface {
 	// RejectWaitingSubscription rejects a waiting subscription given its UID.
 	RejectWaitingSubscription(uid types.UID)
 
-	// ClientSet returns a kubernetes clientSet.
-	ClientSet() clientset.Interface
+	// ClientSet returns a clusternet clientSet.
+	ClientSet() clusternet.Interface
 
-	// KubeConfig returns the raw kube schedulerapis.
+	// KubeConfig returns the raw kubeconfig.
 	KubeConfig() *restclient.Config
 
 	// EventRecorder returns an event recorder.
diff --git a/pkg/scheduler/framework/plugins/defaultbinder/default_binder.go b/pkg/scheduler/framework/plugins/defaultbinder/default_binder.go
new file mode 100644
index 0000000..eecd3e4
--- /dev/null
+++ b/pkg/scheduler/framework/plugins/defaultbinder/default_binder.go
@@ -0,0 +1,70 @@
+/*
+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 defaultbinder
+
+import (
+	"context"
+	"sort"
+
+	metav1 "k8s.io/apimachinery/pkg/apis/meta/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/scheduler/framework/interfaces"
+	"github.com/clusternet/clusternet/pkg/scheduler/framework/plugins/names"
+	"github.com/clusternet/clusternet/pkg/utils"
+)
+
+// DefaultBinder binds subscriptions to clusters using a clusternet client.
+type DefaultBinder struct {
+	handle framework.Handle
+}
+
+var _ framework.BindPlugin = &DefaultBinder{}
+
+// New creates a DefaultBinder.
+func New(_ runtime.Object, handle framework.Handle) (framework.Plugin, error) {
+	return &DefaultBinder{handle: handle}, nil
+}
+
+// Name returns the name of the plugin.
+func (pl *DefaultBinder) Name() string {
+	return names.DefaultBinder
+}
+
+// Bind binds subscriptions to clusters using the clusternet client.
+func (pl *DefaultBinder) Bind(ctx context.Context, sub *appsapi.Subscription, namespacedClusters []string) *framework.Status {
+	klog.V(3).InfoS("Attempting to bind subscription to clusters",
+		"subscription", klog.KObj(sub), "clusters", namespacedClusters)
+
+	// use an ordered list
+	sort.SliceStable(namespacedClusters, func(i, j int) bool {
+		return namespacedClusters[i] < namespacedClusters[j]
+	})
+
+	subCopy := sub.DeepCopy()
+	subCopy.Status.BindingNamespaces = namespacedClusters
+	subCopy.Status.SpecHash = utils.HashSubscriptionSpec(&subCopy.Spec)
+	subCopy.Status.DesiredReleases = len(namespacedClusters)
+
+	_, err := pl.handle.ClientSet().AppsV1alpha1().Subscriptions(sub.Namespace).UpdateStatus(ctx, subCopy, metav1.UpdateOptions{})
+	if err != nil {
+		return framework.AsStatus(err)
+	}
+	return nil
+}
diff --git a/pkg/scheduler/framework/plugins/helper/normalize_score.go b/pkg/scheduler/framework/plugins/helper/normalize_score.go
new file mode 100644
index 0000000..e80d8ac
--- /dev/null
+++ b/pkg/scheduler/framework/plugins/helper/normalize_score.go
@@ -0,0 +1,56 @@
+/*
+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.
+*/
+
+// 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/scheduler/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.ClusterScoreList) *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/scheduler/framework/plugins/names/names.go b/pkg/scheduler/framework/plugins/names/names.go
new file mode 100644
index 0000000..d89ca79
--- /dev/null
+++ b/pkg/scheduler/framework/plugins/names/names.go
@@ -0,0 +1,23 @@
+/*
+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 names
+
+const (
+	DefaultBinder = "DefaultBinder"
+
+	TaintToleration = "TaintToleration"
+)
diff --git a/pkg/scheduler/framework/plugins/registry.go b/pkg/scheduler/framework/plugins/registry.go
index 00debf9..d361f8e 100644
--- a/pkg/scheduler/framework/plugins/registry.go
+++ b/pkg/scheduler/framework/plugins/registry.go
@@ -17,11 +17,16 @@ limitations under the License.
 package plugins
 
 import (
+	"github.com/clusternet/clusternet/pkg/scheduler/framework/plugins/defaultbinder"
+	"github.com/clusternet/clusternet/pkg/scheduler/framework/plugins/names"
+	"github.com/clusternet/clusternet/pkg/scheduler/framework/plugins/tainttoleration"
 	"github.com/clusternet/clusternet/pkg/scheduler/framework/runtime"
 )
 
 // NewInTreeRegistry builds the registry with all the in-tree plugins.
 func NewInTreeRegistry() runtime.Registry {
-	// TODO: register plugins
-	return runtime.Registry{}
+	return runtime.Registry{
+		names.DefaultBinder:   defaultbinder.New,
+		names.TaintToleration: tainttoleration.New,
+	}
 }
diff --git a/pkg/scheduler/framework/plugins/tainttoleration/taint_toleration.go b/pkg/scheduler/framework/plugins/tainttoleration/taint_toleration.go
new file mode 100644
index 0000000..f6e3916
--- /dev/null
+++ b/pkg/scheduler/framework/plugins/tainttoleration/taint_toleration.go
@@ -0,0 +1,128 @@
+/*
+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 tainttoleration
+
+import (
+	"context"
+	"fmt"
+
+	v1 "k8s.io/api/core/v1"
+	"k8s.io/apimachinery/pkg/runtime"
+	"k8s.io/client-go/tools/cache"
+	v1helper "k8s.io/component-helpers/scheduling/corev1"
+
+	appsapi "github.com/clusternet/clusternet/pkg/apis/apps/v1alpha1"
+	clusterapi "github.com/clusternet/clusternet/pkg/apis/clusters/v1beta1"
+	framework "github.com/clusternet/clusternet/pkg/scheduler/framework/interfaces"
+	"github.com/clusternet/clusternet/pkg/scheduler/framework/plugins/helper"
+	"github.com/clusternet/clusternet/pkg/scheduler/framework/plugins/names"
+)
+
+// TaintToleration is a plugin that checks if a subscription tolerates a cluster'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, sub *appsapi.Subscription, cluster *clusterapi.ManagedCluster) *framework.Status {
+	if cluster == nil {
+		return framework.AsStatus(fmt.Errorf("invalid cluster"))
+	}
+
+	filterPredicate := func(t *v1.Taint) bool {
+		// only interested in NoSchedule and NoExecute taints.
+		return t.Effect == v1.TaintEffectNoSchedule || t.Effect == v1.TaintEffectNoExecute
+	}
+
+	taint, isUntolerated := v1helper.FindMatchingUntoleratedTaint(cluster.Spec.Taints, sub.Spec.ClusterTolerations, filterPredicate)
+	if !isUntolerated {
+		return nil
+	}
+
+	errReason := fmt.Sprintf("clusters(s) had taint {%s: %s}, that the subscription didn't tolerate",
+		taint.Key, taint.Value)
+	return framework.NewStatus(framework.UnschedulableAndUnresolvable, errReason)
+}
+
+// getAllTolerationEffectPreferNoSchedule gets the list of all Tolerations with Effect PreferNoSchedule or with no effect.
+// copied from k8s.io/kubernetes/pkg/scheduler/framework/plugins/tainttoleration/taint_toleration.go
+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 subscription with effect PreferNoSchedule
+// copied from k8s.io/kubernetes/pkg/scheduler/framework/plugins/tainttoleration/taint_toleration.go
+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, sub *appsapi.Subscription, namespacedCluster string) (int64, *framework.Status) {
+	// Convert the namespace/name string into a distinct namespace and name
+	ns, name, err := cache.SplitMetaNamespaceKey(namespacedCluster)
+	if err != nil {
+		return 0, framework.AsStatus(fmt.Errorf("invalid resource key: %s", namespacedCluster))
+	}
+
+	cluster, err := pl.handle.SharedInformerFactory().Clusters().V1beta1().ManagedClusters().Lister().ManagedClusters(ns).Get(name)
+	if err != nil {
+		return 0, framework.AsStatus(fmt.Errorf("getting cluster %s: %v", namespacedCluster, err))
+	}
+
+	score := int64(countIntolerableTaintsPreferNoSchedule(cluster.Spec.Taints,
+		getAllTolerationPreferNoSchedule(sub.Spec.ClusterTolerations)))
+	return score, nil
+}
+
+// NormalizeScore invoked after scoring all clusters.
+func (pl *TaintToleration) NormalizeScore(ctx context.Context, scores framework.ClusterScoreList) *framework.Status {
+	return helper.DefaultNormalizeScore(framework.MaxClusterScore, 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/scheduler/framework/runtime/framework.go b/pkg/scheduler/framework/runtime/framework.go
index 19cb5fb..edfdbdc 100644
--- a/pkg/scheduler/framework/runtime/framework.go
+++ b/pkg/scheduler/framework/runtime/framework.go
@@ -25,14 +25,15 @@ import (
 	"time"
 
 	"k8s.io/apimachinery/pkg/types"
-	"k8s.io/client-go/informers"
-	clientset "k8s.io/client-go/kubernetes"
+	"k8s.io/apimachinery/pkg/util/sets"
 	restclient "k8s.io/client-go/rest"
 	"k8s.io/client-go/tools/record"
 	"k8s.io/klog/v2"
 
 	appsapi "github.com/clusternet/clusternet/pkg/apis/apps/v1alpha1"
 	clusterapi "github.com/clusternet/clusternet/pkg/apis/clusters/v1beta1"
+	clusternet "github.com/clusternet/clusternet/pkg/generated/clientset/versioned"
+	informers "github.com/clusternet/clusternet/pkg/generated/informers/externalversions"
 	schedulerapis "github.com/clusternet/clusternet/pkg/scheduler/apis"
 	framework "github.com/clusternet/clusternet/pkg/scheduler/framework/interfaces"
 	"github.com/clusternet/clusternet/pkg/scheduler/metrics"
@@ -60,7 +61,6 @@ const (
 // frameworkImpl is the component responsible for initializing and running scheduler
 // plugins.
 type frameworkImpl struct {
-	registry             Registry
 	waitingSubscriptions *waitingSubscriptionsMap
 	scorePluginWeight    map[string]int
 	preFilterPlugins     []framework.PreFilterPlugin
@@ -74,7 +74,7 @@ type frameworkImpl struct {
 	postBindPlugins      []framework.PostBindPlugin
 	permitPlugins        []framework.PermitPlugin
 
-	clientSet       clientset.Interface
+	clientSet       clusternet.Interface
 	kubeConfig      *restclient.Config
 	eventRecorder   record.EventRecorder
 	informerFactory informers.SharedInformerFactory
@@ -116,7 +116,7 @@ func (f *frameworkImpl) getExtensionPoints(plugins *schedulerapis.Plugins) []ext
 }
 
 type frameworkOptions struct {
-	clientSet       clientset.Interface
+	clientSet       clusternet.Interface
 	kubeConfig      *restclient.Config
 	eventRecorder   record.EventRecorder
 	informerFactory informers.SharedInformerFactory
@@ -129,7 +129,7 @@ type frameworkOptions struct {
 type Option func(*frameworkOptions)
 
 // WithClientSet sets clientSet for the scheduling frameworkImpl.
-func WithClientSet(clientSet clientset.Interface) Option {
+func WithClientSet(clientSet clusternet.Interface) Option {
 	return func(o *frameworkOptions) {
 		o.clientSet = clientSet
 	}
@@ -181,14 +181,13 @@ func defaultFrameworkOptions() frameworkOptions {
 var _ framework.Framework = &frameworkImpl{}
 
 // NewFramework initializes plugins given the configuration and the registry.
-func NewFramework(r Registry, opts ...Option) (framework.Framework, error) {
+func NewFramework(r Registry, plugins *schedulerapis.Plugins, opts ...Option) (framework.Framework, error) {
 	options := defaultFrameworkOptions()
 	for _, opt := range opts {
 		opt(&options)
 	}
 
 	f := &frameworkImpl{
-		registry:             r,
 		scorePluginWeight:    make(map[string]int),
 		waitingSubscriptions: newWaitingSubscriptionsMap(),
 		clientSet:            options.clientSet,
@@ -201,14 +200,33 @@ func NewFramework(r Registry, opts ...Option) (framework.Framework, error) {
 		profileName:          "default",
 	}
 
-	// TODO: check pluginsNeeded
+	if r == nil {
+		return f, nil
+	}
 
-	// Verifying the score weights again since Plugin.Name() could return a different
-	// value from the one used in the configuration.
-	for _, scorePlugin := range f.scorePlugins {
-		if f.scorePluginWeight[scorePlugin.Name()] == 0 {
-			return nil, fmt.Errorf("score plugin %q is not configured with weight", scorePlugin.Name())
+	// initialize plugins per individual extension points
+	pluginsMap := make(map[string]framework.Plugin)
+	for name, factory := range r {
+		p, err := factory(nil, f)
+		if err != nil {
+			return nil, fmt.Errorf("initializing plugin %q: %w", name, err)
 		}
+		pluginsMap[name] = p
+	}
+
+	for _, e := range f.getExtensionPoints(plugins) {
+		if err := updatePluginList(e.slicePtr, *e.plugins, pluginsMap); err != nil {
+			return nil, err
+		}
+	}
+
+	for _, scorePlugin := range plugins.Score.Enabled {
+		// a weight of zero is not permitted, plugins can be disabled explicitly
+		// when configured.
+		if scorePlugin.Weight == 0 {
+			return nil, fmt.Errorf("score plugin %q is not configured with weight", scorePlugin.Name)
+		}
+		f.scorePluginWeight[scorePlugin.Name] = int(scorePlugin.Weight)
 	}
 
 	if len(f.bindPlugins) == 0 {
@@ -218,6 +236,33 @@ func NewFramework(r Registry, opts ...Option) (framework.Framework, error) {
 	return f, nil
 }
 
+// copied from k8s.io/kubernetes/pkg/scheduler/framework/runtime/framework.go
+func updatePluginList(pluginList interface{}, pluginSet schedulerapis.PluginSet, pluginsMap map[string]framework.Plugin) error {
+	plugins := reflect.ValueOf(pluginList).Elem()
+	pluginType := plugins.Type().Elem()
+	set := sets.NewString()
+	for _, ep := range pluginSet.Enabled {
+		pg, ok := pluginsMap[ep.Name]
+		if !ok {
+			return fmt.Errorf("%s %q does not exist", pluginType.Name(), ep.Name)
+		}
+
+		if !reflect.TypeOf(pg).Implements(pluginType) {
+			return fmt.Errorf("plugin %q does not extend %s plugin", ep.Name, pluginType.Name())
+		}
+
+		if set.Has(ep.Name) {
+			return fmt.Errorf("plugin %q already registered as %q", ep.Name, pluginType.Name())
+		}
+
+		set.Insert(ep.Name)
+
+		newPlugins := reflect.Append(plugins, reflect.ValueOf(pg))
+		plugins.Set(newPlugins)
+	}
+	return nil
+}
+
 // RunPreFilterPlugins runs the set of configured PreFilter plugins. It returns
 // *Status and its code is set to non-success if any of the plugins returns
 // anything but Success. If a non-success status is returned, then the scheduling
@@ -379,7 +424,7 @@ func (f *frameworkImpl) RunScorePlugins(ctx context.Context, sub *appsapi.Subscr
 		if pl.ScoreExtensions() == nil {
 			return
 		}
-		status := f.runScoreExtension(ctx, pl, sub, ClusterScoreList)
+		status := f.runScoreExtension(ctx, pl, ClusterScoreList)
 		if !status.IsSuccess() {
 			err := fmt.Errorf("plugin %q failed with: %w", pl.Name(), status.AsError())
 			errCh.SendErrorWithCancel(err, cancel)
@@ -421,9 +466,9 @@ func (f *frameworkImpl) runScorePlugin(ctx context.Context, pl framework.ScorePl
 	return s, status
 }
 
-func (f *frameworkImpl) runScoreExtension(ctx context.Context, pl framework.ScorePlugin, sub *appsapi.Subscription, ClusterScoreList framework.ClusterScoreList) *framework.Status {
+func (f *frameworkImpl) runScoreExtension(ctx context.Context, pl framework.ScorePlugin, ClusterScoreList framework.ClusterScoreList) *framework.Status {
 	startTime := time.Now()
-	status := pl.ScoreExtensions().NormalizeScore(ctx, sub, ClusterScoreList)
+	status := pl.ScoreExtensions().NormalizeScore(ctx, ClusterScoreList)
 	f.metricsRecorder.observePluginDurationAsync(scoreExtensionNormalize, pl.Name(), status, metrics.SinceInSeconds(startTime))
 	return status
 }
@@ -691,12 +736,12 @@ func (f *frameworkImpl) ListPlugins() *schedulerapis.Plugins {
 	return &m
 }
 
-// ClientSet returns a kubernetes clientset.
-func (f *frameworkImpl) ClientSet() clientset.Interface {
+// ClientSet returns a clusternet clientset.
+func (f *frameworkImpl) ClientSet() clusternet.Interface {
 	return f.clientSet
 }
 
-// KubeConfig returns a kubernetes schedulerapis.
+// KubeConfig returns a kubeconfig.
 func (f *frameworkImpl) KubeConfig() *restclient.Config {
 	return f.kubeConfig
 }
diff --git a/pkg/scheduler/scheduler.go b/pkg/scheduler/scheduler.go
index 893a4f6..7369ad6 100644
--- a/pkg/scheduler/scheduler.go
+++ b/pkg/scheduler/scheduler.go
@@ -19,10 +19,14 @@ package scheduler
 import (
 	"context"
 	"fmt"
+	"reflect"
 	"strings"
+	"sync"
 	"time"
 
 	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/apimachinery/pkg/util/wait"
 	"k8s.io/client-go/kubernetes"
@@ -48,6 +52,7 @@ import (
 	frameworkruntime "github.com/clusternet/clusternet/pkg/scheduler/framework/runtime"
 	"github.com/clusternet/clusternet/pkg/scheduler/metrics"
 	"github.com/clusternet/clusternet/pkg/scheduler/options"
+	"github.com/clusternet/clusternet/pkg/scheduler/parallelize"
 	"github.com/clusternet/clusternet/pkg/utils"
 )
 
@@ -81,6 +86,9 @@ type Scheduler struct {
 	SchedulingQueue workqueue.RateLimitingInterface
 
 	framework framework.Framework
+
+	lock           sync.RWMutex
+	subscribersMap map[string][]appsapi.Subscriber
 }
 
 // NewScheduler returns a new Scheduler.
@@ -121,10 +129,16 @@ func NewScheduler(schedulerOptions *options.SchedulerOptions) (*Scheduler, error
 		registry:                  plugins.NewInTreeRegistry(),
 		scheduleAlgorithm:         algorithm.NewGenericScheduler(schedulerCache),
 		SchedulingQueue:           workqueue.NewRateLimitingQueue(workqueue.DefaultItemBasedRateLimiter()),
+		subscribersMap:            make(map[string][]appsapi.Subscriber),
 	}
 
-	framework, err := frameworkruntime.NewFramework(sched.registry,
+	framework, err := frameworkruntime.NewFramework(sched.registry, getDefaultPlugins(),
 		frameworkruntime.WithEventRecorder(recorder),
+		frameworkruntime.WithInformerFactory(clusternetInformerFactory),
+		frameworkruntime.WithClientSet(clusternetClient),
+		frameworkruntime.WithKubeConfig(clientConfig),
+		frameworkruntime.WithParallelism(parallelize.DefaultParallelism),
+		frameworkruntime.WithRunAllFilters(false),
 	)
 	if err != nil {
 		return nil, err
@@ -134,7 +148,7 @@ func NewScheduler(schedulerOptions *options.SchedulerOptions) (*Scheduler, error
 	// register all metrics
 	metrics.Register()
 
-	addAllEventHandlers(sched)
+	sched.addAllEventHandlers()
 	return sched, nil
 }
 
@@ -352,13 +366,16 @@ func (sched *Scheduler) recordSchedulingFailure(sub *appsapi.Subscription, err e
 
 // addAllEventHandlers is a helper function used in tests and in Scheduler
 // to add event handlers for various informers.
-func addAllEventHandlers(sched *Scheduler) {
+func (sched *Scheduler) addAllEventHandlers() {
 	sched.ClusternetInformerFactory.Apps().V1alpha1().Subscriptions().Informer().AddEventHandler(cache.FilteringResourceEventHandler{
 		FilterFunc: func(obj interface{}) bool {
 			switch t := obj.(type) {
 			case *appsapi.Subscription:
 				sub := obj.(*appsapi.Subscription)
 				if sub.DeletionTimestamp != nil {
+					sched.lock.Lock()
+					defer sched.lock.Unlock()
+					delete(sched.subscribersMap, klog.KObj(sub).String())
 					return false
 				}
 
@@ -377,17 +394,80 @@ func addAllEventHandlers(sched *Scheduler) {
 		},
 		Handler: cache.ResourceEventHandlerFuncs{
 			AddFunc: func(obj interface{}) {
-				// TODO
 				sub := obj.(*appsapi.Subscription)
+				sched.lock.Lock()
+				defer sched.lock.Unlock()
+				sched.subscribersMap[klog.KObj(sub).String()] = sub.Spec.Subscribers
 				sched.SchedulingQueue.AddRateLimited(klog.KObj(sub).String())
 			},
 			UpdateFunc: func(oldObj, newObj interface{}) {
-				// TODO
+				oldSub := oldObj.(*appsapi.Subscription)
 				newSub := newObj.(*appsapi.Subscription)
+
+				// Decide whether discovery has reported a spec change.
+				if reflect.DeepEqual(oldSub.Spec, newSub.Spec) {
+					klog.V(4).Infof("no updates on the spec of Subscription %s, skipping syncing", klog.KObj(oldSub))
+					return
+				}
+
+				sched.lock.Lock()
+				defer sched.lock.Unlock()
+				sched.subscribersMap[klog.KObj(newSub).String()] = newSub.Spec.Subscribers
 				sched.SchedulingQueue.AddRateLimited(klog.KObj(newSub).String())
 			},
 		},
 	})
+
+	enqueueSubscriptionForClusterFunc := func(mcls *clusterapi.ManagedCluster) {
+		sched.lock.RLock()
+		defer sched.lock.RUnlock()
+
+		for key, subscribers := range sched.subscribersMap {
+			for _, subscriber := range subscribers {
+				selector, err := metav1.LabelSelectorAsSelector(subscriber.ClusterAffinity)
+				if err != nil {
+					klog.ErrorDepth(5, fmt.Sprintf("failed to parse labelSelector in Subscription %s: %v", key, err))
+					continue
+				}
+				if !selector.Matches(labels.Set(mcls.Labels)) {
+					continue
+				}
+				sched.SchedulingQueue.AddRateLimited(key)
+				break
+			}
+		}
+	}
+
+	sched.ClusternetInformerFactory.Clusters().V1beta1().ManagedClusters().Informer().AddEventHandler(cache.ResourceEventHandlerFuncs{
+		AddFunc: func(obj interface{}) {
+			mcls := obj.(*clusterapi.ManagedCluster)
+			if mcls.DeletionTimestamp != nil {
+				return
+			}
+			enqueueSubscriptionForClusterFunc(mcls)
+		},
+		UpdateFunc: func(oldObj, newObj interface{}) {
+			oldMcls := oldObj.(*clusterapi.ManagedCluster)
+			newMcls := newObj.(*clusterapi.ManagedCluster)
+
+			if newMcls.DeletionTimestamp != nil {
+				return
+			}
+
+			// no updates on the labels/taints of ManagedCluster
+			if reflect.DeepEqual(oldMcls.Labels, newMcls.Labels) && reflect.DeepEqual(oldMcls.Spec.Taints, newMcls.Spec.Taints) {
+				klog.V(4).Infof("no updates on the labels/taints of ManagedCluster %s, skipping syncing", klog.KObj(oldMcls))
+				return
+			}
+			enqueueSubscriptionForClusterFunc(newMcls)
+		},
+		DeleteFunc: func(obj interface{}) {
+			// when a ManagedCluster is deleted,
+			// - Auto populated objects, like Base and Description, will be auto-deleted on next sync/resync of subscribed Subscriptions
+			// - If current dedicated namespace is deleted, then all objects in this namespaces will be pruned.
+		},
+	})
+
 }
 
 // truncateMessage truncates a message if it hits the NoteLengthLimit.
diff --git a/pkg/utils/deployer.go b/pkg/utils/deployer.go
index c20df72..0a0237c 100644
--- a/pkg/utils/deployer.go
+++ b/pkg/utils/deployer.go
@@ -43,7 +43,6 @@ import (
 	"k8s.io/client-go/kubernetes"
 	"k8s.io/client-go/tools/record"
 	"k8s.io/client-go/util/retry"
-	v1helper "k8s.io/component-helpers/scheduling/corev1"
 	"k8s.io/klog/v2"
 
 	appsapi "github.com/clusternet/clusternet/pkg/apis/apps/v1alpha1"
@@ -680,23 +679,3 @@ func ResourceNeedResync(original pkgruntime.Object, current pkgruntime.Object) b
 
 	return false
 }
-
-// FilterClustersByTolerations filters clusters that match the specified tolerations.
-func FilterClustersByTolerations(clusters []*clusterapi.ManagedCluster, tolerations []corev1.Toleration) []*clusterapi.ManagedCluster {
-	var res []*clusterapi.ManagedCluster
-	for _, cluster := range clusters {
-		if !IsClusterUntolerated(cluster, tolerations) {
-			res = append(res, cluster)
-		}
-	}
-	return res
-}
-
-// IsClusterUntolerated checks if the cluster does not match the specified tolerations.
-func IsClusterUntolerated(cluster *clusterapi.ManagedCluster, tolerations []corev1.Toleration) bool {
-	filterPredicate := func(t *corev1.Taint) bool {
-		return t.Effect == corev1.TaintEffectNoSchedule
-	}
-	_, isUntolerated := v1helper.FindMatchingUntoleratedTaint(cluster.Spec.Taints, tolerations, filterPredicate)
-	return isUntolerated
-}
diff --git a/pkg/utils/feed.go b/pkg/utils/feed.go
index 5463bfe..fec278c 100644
--- a/pkg/utils/feed.go
+++ b/pkg/utils/feed.go
@@ -21,7 +21,9 @@ import (
 	"encoding/json"
 	"errors"
 	"fmt"
+	"hash/fnv"
 
+	"github.com/davecgh/go-spew/spew"
 	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
 	"k8s.io/apimachinery/pkg/labels"
 	"k8s.io/apimachinery/pkg/runtime/schema"
@@ -159,3 +161,16 @@ func HasFeed(feed appsapi.Feed, feeds []appsapi.Feed) bool {
 
 	return false
 }
+
+func HashSubscriptionSpec(subscriptionSpec *appsapi.SubscriptionSpec) uint64 {
+	specJSON, _ := json.Marshal(subscriptionSpec)
+	printer := spew.ConfigState{
+		Indent:         " ",
+		SortKeys:       true,
+		DisableMethods: true,
+		SpewKeys:       true,
+	}
+	hasher := fnv.New32a()
+	printer.Fprintf(hasher, "%#v", specJSON)
+	return uint64(hasher.Sum32())
+}


Test patch:
diff --git a/pkg/scheduler/framework/plugins/defaultbinder/default_binder_test.go b/pkg/scheduler/framework/plugins/defaultbinder/default_binder_test.go
new file mode 100644
index 0000000..d58efa1
--- /dev/null
+++ b/pkg/scheduler/framework/plugins/defaultbinder/default_binder_test.go
@@ -0,0 +1,91 @@
+/*
+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 defaultbinder
+
+import (
+	"context"
+	"errors"
+	"testing"
+
+	"github.com/google/go-cmp/cmp"
+	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
+	"k8s.io/apimachinery/pkg/runtime"
+	clienttesting "k8s.io/client-go/testing"
+
+	appsapi "github.com/clusternet/clusternet/pkg/apis/apps/v1alpha1"
+	"github.com/clusternet/clusternet/pkg/generated/clientset/versioned/fake"
+	frameworkruntime "github.com/clusternet/clusternet/pkg/scheduler/framework/runtime"
+)
+
+func TestDefaultBinder(t *testing.T) {
+	testSubscription := &appsapi.Subscription{
+		ObjectMeta: metav1.ObjectMeta{Name: "foo", Namespace: "ns"},
+	}
+	testClusters := []string{
+		"cluster-ns-03",
+		"cluster-ns-01",
+	}
+	tests := []struct {
+		name           string
+		injectErr      error
+		wantedBindings []string
+	}{
+		{
+			name: "successful",
+			wantedBindings: []string{
+				"cluster-ns-01",
+				"cluster-ns-03",
+			},
+		}, {
+			name:      "binding error",
+			injectErr: errors.New("binding error"),
+		},
+	}
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			var bindedNamespaces []string
+			client := fake.NewSimpleClientset(testSubscription)
+			client.PrependReactor("update", "subscriptions", func(action clienttesting.Action) (bool, runtime.Object, error) {
+				if action.GetSubresource() != "status" {
+					return false, nil, nil
+				}
+				if tt.injectErr != nil {
+					return true, nil, tt.injectErr
+				}
+
+				bindedSubscription := action.(clienttesting.UpdateAction).GetObject().(*appsapi.Subscription)
+				if bindedSubscription != nil {
+					bindedNamespaces = bindedSubscription.Status.BindingNamespaces
+				}
+				return true, bindedSubscription, nil
+			})
+
+			fh, err := frameworkruntime.NewFramework(nil, nil, frameworkruntime.WithClientSet(client))
+			if err != nil {
+				t.Fatal(err)
+			}
+			binder := &DefaultBinder{handle: fh}
+			status := binder.Bind(context.Background(), testSubscription, testClusters)
+			if got := status.AsError(); (tt.injectErr != nil) != (got != nil) {
+				t.Errorf("got error %q, want %q", got, tt.injectErr)
+			}
+			if diff := cmp.Diff(tt.wantedBindings, bindedNamespaces); diff != "" {
+				t.Errorf("got different binding (-want, +got): %s", diff)
+			}
+		})
+	}
+}
diff --git a/pkg/scheduler/framework/plugins/helper/normalize_score_test.go b/pkg/scheduler/framework/plugins/helper/normalize_score_test.go
new file mode 100644
index 0000000..116c9e7
--- /dev/null
+++ b/pkg/scheduler/framework/plugins/helper/normalize_score_test.go
@@ -0,0 +1,85 @@
+/*
+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 helper
+
+import (
+	"fmt"
+	"testing"
+
+	"github.com/google/go-cmp/cmp"
+
+	framework "github.com/clusternet/clusternet/pkg/scheduler/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.ClusterScoreList{}
+			for _, score := range test.scores {
+				scores = append(scores, framework.ClusterScore{Score: score})
+			}
+
+			expectedScores := framework.ClusterScoreList{}
+			for _, score := range test.expectedScores {
+				expectedScores = append(expectedScores, framework.ClusterScore{Score: score})
+			}
+
+			DefaultNormalizeScore(framework.MaxClusterScore, test.reverse, scores)
+			if diff := cmp.Diff(expectedScores, scores); diff != "" {
+				t.Errorf("Unexpected scores (-want, +got):\n%s", diff)
+			}
+		})
+	}
+}
diff --git a/pkg/scheduler/framework/plugins/tainttoleration/taint_toleration_test.go b/pkg/scheduler/framework/plugins/tainttoleration/taint_toleration_test.go
new file mode 100644
index 0000000..badbe7f
--- /dev/null
+++ b/pkg/scheduler/framework/plugins/tainttoleration/taint_toleration_test.go
@@ -0,0 +1,356 @@
+/*
+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 tainttoleration
+
+import (
+	"context"
+	"reflect"
+	"testing"
+	"time"
+
+	v1 "k8s.io/api/core/v1"
+	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
+	"k8s.io/klog/v2"
+
+	appsapi "github.com/clusternet/clusternet/pkg/apis/apps/v1alpha1"
+	clusterapi "github.com/clusternet/clusternet/pkg/apis/clusters/v1beta1"
+	"github.com/clusternet/clusternet/pkg/generated/clientset/versioned/fake"
+	informers "github.com/clusternet/clusternet/pkg/generated/informers/externalversions"
+	framework "github.com/clusternet/clusternet/pkg/scheduler/framework/interfaces"
+	"github.com/clusternet/clusternet/pkg/scheduler/framework/runtime"
+)
+
+func clusterWithTaints(clusterNamespace string, taints []v1.Taint) *clusterapi.ManagedCluster {
+	return &clusterapi.ManagedCluster{
+		ObjectMeta: metav1.ObjectMeta{
+			Name:      "cluster-test",
+			Namespace: clusterNamespace,
+		},
+		Spec: clusterapi.ManagedClusterSpec{
+			Taints: taints,
+		},
+	}
+}
+
+func subscriptionWithTolerations(subName string, tolerations []v1.Toleration) *appsapi.Subscription {
+	return &appsapi.Subscription{
+		ObjectMeta: metav1.ObjectMeta{
+			Name:      subName,
+			Namespace: "default",
+		},
+		Spec: appsapi.SubscriptionSpec{
+			Subscribers: []appsapi.Subscriber{
+				{
+					ClusterAffinity: &metav1.LabelSelector{MatchLabels: map[string]string{}},
+				},
+			},
+			ClusterTolerations: tolerations,
+		},
+	}
+}
+
+func TestTaintTolerationScore(t *testing.T) {
+	tests := []struct {
+		name         string
+		subscription *appsapi.Subscription
+		clusters     []*clusterapi.ManagedCluster
+		expectedList framework.ClusterScoreList
+	}{
+		// basic test case
+		{
+			name: "cluster with taints tolerated by the subscription, gets a higher score than those cluster with intolerable taints",
+			subscription: subscriptionWithTolerations("sub1", []v1.Toleration{{
+				Key:      "foo",
+				Operator: v1.TolerationOpEqual,
+				Value:    "bar",
+				Effect:   v1.TaintEffectPreferNoSchedule,
+			}}),
+			clusters: []*clusterapi.ManagedCluster{
+				clusterWithTaints("cluster-ns-01", []v1.Taint{{
+					Key:    "foo",
+					Value:  "bar",
+					Effect: v1.TaintEffectPreferNoSchedule,
+				}}),
+				clusterWithTaints("cluster-ns-02", []v1.Taint{{
+					Key:    "foo",
+					Value:  "blah",
+					Effect: v1.TaintEffectPreferNoSchedule,
+				}}),
+			},
+			expectedList: []framework.ClusterScore{
+				{NamespacedName: "cluster-ns-01/cluster-test", Score: framework.MaxClusterScore},
+				{NamespacedName: "cluster-ns-02/cluster-test", Score: 0},
+			},
+		},
+		// the count of taints that are tolerated by subscription, does not matter.
+		{
+			name: "the clusters that all of their taints are tolerated by the subscription, get the same score, no matter how many tolerable taints a cluster has",
+			subscription: subscriptionWithTolerations("sub1", []v1.Toleration{
+				{
+					Key:      "cpu-type",
+					Operator: v1.TolerationOpEqual,
+					Value:    "arm64",
+					Effect:   v1.TaintEffectPreferNoSchedule,
+				}, {
+					Key:      "disk-type",
+					Operator: v1.TolerationOpEqual,
+					Value:    "ssd",
+					Effect:   v1.TaintEffectPreferNoSchedule,
+				},
+			}),
+			clusters: []*clusterapi.ManagedCluster{
+				clusterWithTaints("cluster-ns-01", []v1.Taint{}),
+				clusterWithTaints("cluster-ns-02", []v1.Taint{
+					{
+						Key:    "cpu-type",
+						Value:  "arm64",
+						Effect: v1.TaintEffectPreferNoSchedule,
+					},
+				}),
+				clusterWithTaints("cluster-ns-03", []v1.Taint{
+					{
+						Key:    "cpu-type",
+						Value:  "arm64",
+						Effect: v1.TaintEffectPreferNoSchedule,
+					}, {
+						Key:    "disk-type",
+						Value:  "ssd",
+						Effect: v1.TaintEffectPreferNoSchedule,
+					},
+				}),
+			},
+			expectedList: []framework.ClusterScore{
+				{NamespacedName: "cluster-ns-01/cluster-test", Score: framework.MaxClusterScore},
+				{NamespacedName: "cluster-ns-02/cluster-test", Score: framework.MaxClusterScore},
+				{NamespacedName: "cluster-ns-03/cluster-test", Score: framework.MaxClusterScore},
+			},
+		},
+		// the count of taints on a cluster that are not tolerated by subscription, matters.
+		{
+			name: "the more intolerable taints a cluster has, the lower score it gets.",
+			subscription: subscriptionWithTolerations("sub1", []v1.Toleration{{
+				Key:      "foo",
+				Operator: v1.TolerationOpEqual,
+				Value:    "bar",
+				Effect:   v1.TaintEffectPreferNoSchedule,
+			}}),
+			clusters: []*clusterapi.ManagedCluster{
+				clusterWithTaints("cluster-ns-01", []v1.Taint{}),
+				clusterWithTaints("cluster-ns-02", []v1.Taint{
+					{
+						Key:    "cpu-type",
+						Value:  "arm64",
+						Effect: v1.TaintEffectPreferNoSchedule,
+					},
+				}),
+				clusterWithTaints("cluster-ns-03", []v1.Taint{
+					{
+						Key:    "cpu-type",
+						Value:  "arm64",
+						Effect: v1.TaintEffectPreferNoSchedule,
+					}, {
+						Key:    "disk-type",
+						Value:  "ssd",
+						Effect: v1.TaintEffectPreferNoSchedule,
+					},
+				}),
+			},
+			expectedList: []framework.ClusterScore{
+				{NamespacedName: "cluster-ns-01/cluster-test", Score: framework.MaxClusterScore},
+				{NamespacedName: "cluster-ns-02/cluster-test", Score: 50},
+				{NamespacedName: "cluster-ns-03/cluster-test", 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",
+			subscription: subscriptionWithTolerations("sub1", []v1.Toleration{
+				{
+					Key:      "cpu-type",
+					Operator: v1.TolerationOpEqual,
+					Value:    "arm64",
+					Effect:   v1.TaintEffectNoSchedule,
+				}, {
+					Key:      "disk-type",
+					Operator: v1.TolerationOpEqual,
+					Value:    "ssd",
+					Effect:   v1.TaintEffectNoSchedule,
+				},
+			}),
+			clusters: []*clusterapi.ManagedCluster{
+				clusterWithTaints("cluster-ns-01", []v1.Taint{}),
+				clusterWithTaints("cluster-ns-02", []v1.Taint{
+					{
+						Key:    "cpu-type",
+						Value:  "arm64",
+						Effect: v1.TaintEffectNoSchedule,
+					},
+				}),
+				clusterWithTaints("cluster-ns-03", []v1.Taint{
+					{
+						Key:    "cpu-type",
+						Value:  "arm64",
+						Effect: v1.TaintEffectPreferNoSchedule,
+					}, {
+						Key:    "disk-type",
+						Value:  "ssd",
+						Effect: v1.TaintEffectPreferNoSchedule,
+					},
+				}),
+			},
+			expectedList: []framework.ClusterScore{
+				{NamespacedName: "cluster-ns-01/cluster-test", Score: framework.MaxClusterScore},
+				{NamespacedName: "cluster-ns-02/cluster-test", Score: framework.MaxClusterScore},
+				{NamespacedName: "cluster-ns-03/cluster-test", Score: 0},
+			},
+		},
+		{
+			name: "Default behaviour No taints and tolerations, lands on cluster with no taints",
+			// subscription without tolerations
+			subscription: subscriptionWithTolerations("sub2", []v1.Toleration{}),
+			clusters: []*clusterapi.ManagedCluster{
+				// Cluster without taints
+				clusterWithTaints("cluster-ns-01", []v1.Taint{}),
+				clusterWithTaints("cluster-ns-02", []v1.Taint{
+					{
+						Key:    "cpu-type",
+						Value:  "arm64",
+						Effect: v1.TaintEffectPreferNoSchedule,
+					},
+				}),
+			},
+			expectedList: []framework.ClusterScore{
+				{NamespacedName: "cluster-ns-01/cluster-test", Score: framework.MaxClusterScore},
+				{NamespacedName: "cluster-ns-02/cluster-test", Score: 0},
+			},
+		},
+	}
+	for _, test := range tests {
+		t.Run(test.name, func(t *testing.T) {
+			fakeInformerFactory := informers.NewSharedInformerFactory(&fake.Clientset{}, 0*time.Second)
+			for _, cls := range test.clusters {
+				if err := fakeInformerFactory.Clusters().V1beta1().ManagedClusters().Informer().GetStore().Add(cls); 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.ClusterScoreList
+			for _, n := range test.clusters {
+				score, status := p.(framework.ScorePlugin).Score(context.Background(), test.subscription, klog.KObj(n).String())
+				if !status.IsSuccess() {
+					t.Errorf("unexpected error: %v", status)
+				}
+				gotList = append(gotList, framework.ClusterScore{NamespacedName: klog.KObj(n).String(), Score: score})
+			}
+
+			status := p.(framework.ScorePlugin).ScoreExtensions().NormalizeScore(context.Background(), 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
+		subscription *appsapi.Subscription
+		cluster      *clusterapi.ManagedCluster
+		wantStatus   *framework.Status
+	}{
+		{
+			name:         "A subscription having no tolerations can't be scheduled onto a cluster with nonempty taints",
+			subscription: subscriptionWithTolerations("sub1", []v1.Toleration{}),
+			cluster:      clusterWithTaints("clusterA", []v1.Taint{{Key: "dedicated", Value: "user1", Effect: "NoSchedule"}}),
+			wantStatus: framework.NewStatus(framework.UnschedulableAndUnresolvable,
+				"clusters(s) had taint {dedicated: user1}, that the subscription didn't tolerate"),
+		},
+		{
+			name:         "A subscription which can be scheduled on a dedicated cluster assigned to user1 with effect NoSchedule",
+			subscription: subscriptionWithTolerations("sub1", []v1.Toleration{{Key: "dedicated", Value: "user1", Effect: "NoSchedule"}}),
+			cluster:      clusterWithTaints("clusterA", []v1.Taint{{Key: "dedicated", Value: "user1", Effect: "NoSchedule"}}),
+		},
+		{
+			name:         "A subscription which can't be scheduled on a dedicated cluster assigned to user2 with effect NoSchedule",
+			subscription: subscriptionWithTolerations("sub1", []v1.Toleration{{Key: "dedicated", Operator: "Equal", Value: "user2", Effect: "NoSchedule"}}),
+			cluster:      clusterWithTaints("clusterA", []v1.Taint{{Key: "dedicated", Value: "user1", Effect: "NoSchedule"}}),
+			wantStatus: framework.NewStatus(framework.UnschedulableAndUnresolvable,
+				"clusters(s) had taint {dedicated: user1}, that the subscription didn't tolerate"),
+		},
+		{
+			name:         "A subscription can be scheduled onto the cluster, with a toleration uses operator Exists that tolerates the taints on the cluster",
+			subscription: subscriptionWithTolerations("sub1", []v1.Toleration{{Key: "foo", Operator: "Exists", Effect: "NoSchedule"}}),
+			cluster:      clusterWithTaints("clusterA", []v1.Taint{{Key: "foo", Value: "bar", Effect: "NoSchedule"}}),
+		},
+		{
+			name: "A subscription has multiple tolerations, cluster has multiple taints, all the taints are tolerated, subscription can be scheduled onto the cluster",
+			subscription: subscriptionWithTolerations("sub1", []v1.Toleration{
+				{Key: "dedicated", Operator: "Equal", Value: "user2", Effect: "NoSchedule"},
+				{Key: "foo", Operator: "Exists", Effect: "NoSchedule"},
+			}),
+			cluster: clusterWithTaints("clusterA", []v1.Taint{
+				{Key: "dedicated", Value: "user2", Effect: "NoSchedule"},
+				{Key: "foo", Value: "bar", Effect: "NoSchedule"},
+			}),
+		},
+		{
+			name: "A subscription has a toleration that keys and values match the taint on the cluster, but (non-empty) effect doesn't match, " +
+				"can't be scheduled onto the cluster",
+			subscription: subscriptionWithTolerations("sub1", []v1.Toleration{{Key: "foo", Operator: "Equal", Value: "bar", Effect: "PreferNoSchedule"}}),
+			cluster:      clusterWithTaints("clusterA", []v1.Taint{{Key: "foo", Value: "bar", Effect: "NoSchedule"}}),
+			wantStatus: framework.NewStatus(framework.UnschedulableAndUnresolvable,
+				"clusters(s) had taint {foo: bar}, that the subscription didn't tolerate"),
+		},
+		{
+			name: "The subscription has a toleration that keys and values match the taint on the cluster, the effect of toleration is empty, " +
+				"and the effect of taint is NoSchedule. Subscription can be scheduled onto the cluster",
+			subscription: subscriptionWithTolerations("sub1", []v1.Toleration{{Key: "foo", Operator: "Equal", Value: "bar"}}),
+			cluster:      clusterWithTaints("clusterA", []v1.Taint{{Key: "foo", Value: "bar", Effect: "NoSchedule"}}),
+		},
+		{
+			name: "The subscription has a toleration that key and value don't match the taint on the cluster, " +
+				"but the effect of taint on cluster is PreferNoSchedule. Subscription can be scheduled onto the cluster",
+			subscription: subscriptionWithTolerations("sub1", []v1.Toleration{{Key: "dedicated", Operator: "Equal", Value: "user2", Effect: "NoSchedule"}}),
+			cluster:      clusterWithTaints("clusterA", []v1.Taint{{Key: "dedicated", Value: "user1", Effect: "PreferNoSchedule"}}),
+		},
+		{
+			name: "The subscription has no toleration, " +
+				"but the effect of taint on cluster is PreferNoSchedule. Subscription can be scheduled onto the cluster",
+			subscription: subscriptionWithTolerations("sub1", []v1.Toleration{}),
+			cluster:      clusterWithTaints("clusterA", []v1.Taint{{Key: "dedicated", Value: "user1", Effect: "PreferNoSchedule"}}),
+		},
+	}
+	for _, test := range tests {
+		t.Run(test.name, func(t *testing.T) {
+			p, _ := New(nil, nil)
+			gotStatus := p.(framework.FilterPlugin).Filter(context.Background(), test.subscription, test.cluster)
+			if !reflect.DeepEqual(gotStatus, test.wantStatus) {
+				t.Errorf("status does not match: %v, want: %v", gotStatus, test.wantStatus)
+			}
+		})
+	}
+}
diff --git a/pkg/utils/feed_test.go b/pkg/utils/feed_test.go
index 6e1a74c..70495ef 100644
--- a/pkg/utils/feed_test.go
+++ b/pkg/utils/feed_test.go
@@ -21,6 +21,7 @@ import (
 	"testing"
 
 	appsapi "github.com/clusternet/clusternet/pkg/apis/apps/v1alpha1"
+	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
 )
 
 func TestFormatFeed(t *testing.T) {
@@ -259,3 +260,69 @@ func TestHasFeed(t *testing.T) {
 		})
 	}
 }
+
+func TestHashSubscriptionSpec(t *testing.T) {
+	tests := []struct {
+		name             string
+		subscriptionSpec *appsapi.SubscriptionSpec
+		want             uint64
+	}{
+		{
+			subscriptionSpec: &appsapi.SubscriptionSpec{
+				SchedulerName:      "default",
+				SchedulingStrategy: "Replication",
+				Subscribers: []appsapi.Subscriber{
+					{
+						ClusterAffinity: &metav1.LabelSelector{
+							MatchLabels: map[string]string{
+								"cluster-name": "abc",
+							},
+							MatchExpressions: nil,
+						},
+					},
+				},
+				ClusterTolerations: nil,
+				Feeds: []appsapi.Feed{
+					{
+						Kind:       "Demo",
+						APIVersion: "abc.com/v1",
+						Namespace:  "foo",
+						Name:       "bar",
+					},
+				},
+			},
+			want: uint64(2608449479),
+		},
+		{
+			subscriptionSpec: &appsapi.SubscriptionSpec{
+				SchedulerName:      "default",
+				SchedulingStrategy: "Replication",
+				Subscribers: []appsapi.Subscriber{
+					{
+						ClusterAffinity: &metav1.LabelSelector{
+							MatchLabels: map[string]string{
+								"cluster-name": "abc",
+							},
+						},
+					},
+				},
+				Feeds: []appsapi.Feed{
+					{
+						Kind:       "Demo",
+						APIVersion: "abc.com/v1",
+						Namespace:  "foo",
+						Name:       "bar",
+					},
+				},
+			},
+			want: uint64(2608449479),
+		},
+	}
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			if got := HashSubscriptionSpec(tt.subscriptionSpec); got != tt.want {
+				t.Errorf("got %v, want %v", got, tt.want)
+			}
+		})
+	}
+}