Repository: clusternet/clusternet
Pull request #286 (2022-04-03 08:48:10)

add FeedInventory controller
<!--  Thanks for sending a pull request!

-->

#### What type of PR is this?
kind/feature
kind/api
kind/change

#### What this PR does / why we need it:

#### 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/286?src=pr&el=h1&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=clusternet) Report
> Merging [#286](https://codecov.io/gh/clusternet/clusternet/pull/286?src=pr&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=clusternet) (81b5ed5) into [main](https://codecov.io/gh/clusternet/clusternet/commit/d496d49a9d595d1f44f4e3e368d97b9aa71211cf?el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=clusternet) (d496d49) will **increase** coverage by `1.21%`.
> The diff coverage is `n/a`.

```diff
@@            Coverage Diff             @@
##             main     #286      +/-   ##
==========================================
+ Coverage   10.53%   11.74%   +1.21%     
==========================================
  Files          39       44       +5     
  Lines        4283     4665     +382     
==========================================
+ Hits          451      548      +97     
- Misses       3800     4079     +279     
- Partials       32       38       +6     
```


| [Impacted Files](https://codecov.io/gh/clusternet/clusternet/pull/286?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/286/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%> (-10.72%)` | :arrow_down: |
| [...kg/controllers/apps/feedinventory/feedinventory.go](https://codecov.io/gh/clusternet/clusternet/pull/286/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=clusternet#diff-c3JjL2dpdGh1Yi5jb20vY2x1c3Rlcm5ldC9jbHVzdGVybmV0L3BrZy9jb250cm9sbGVycy9hcHBzL2ZlZWRpbnZlbnRvcnkvZmVlZGludmVudG9yeS5nbw==) | `1.73% <0.00%> (ø)` | |
| [...ollers/apps/feedinventory/deployment/deployment.go](https://codecov.io/gh/clusternet/clusternet/pull/286/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=clusternet#diff-c3JjL2dpdGh1Yi5jb20vY2x1c3Rlcm5ldC9jbHVzdGVybmV0L3BrZy9jb250cm9sbGVycy9hcHBzL2ZlZWRpbnZlbnRvcnkvZGVwbG95bWVudC9kZXBsb3ltZW50Lmdv) | `74.41% <0.00%> (ø)` | |
| [...net/pkg/controllers/apps/feedinventory/registry.go](https://codecov.io/gh/clusternet/clusternet/pull/286/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=clusternet#diff-c3JjL2dpdGh1Yi5jb20vY2x1c3Rlcm5ldC9jbHVzdGVybmV0L3BrZy9jb250cm9sbGVycy9hcHBzL2ZlZWRpbnZlbnRvcnkvcmVnaXN0cnkuZ28=) | `0.00% <0.00%> (ø)` | |
| [...ollers/apps/feedinventory/replicaset/replicaset.go](https://codecov.io/gh/clusternet/clusternet/pull/286/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=clusternet#diff-c3JjL2dpdGh1Yi5jb20vY2x1c3Rlcm5ldC9jbHVzdGVybmV0L3BrZy9jb250cm9sbGVycy9hcHBzL2ZlZWRpbnZlbnRvcnkvcmVwbGljYXNldC9yZXBsaWNhc2V0Lmdv) | `74.41% <0.00%> (ø)` | |
| [...lers/apps/feedinventory/statefulset/statefulset.go](https://codecov.io/gh/clusternet/clusternet/pull/286/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=clusternet#diff-c3JjL2dpdGh1Yi5jb20vY2x1c3Rlcm5ldC9jbHVzdGVybmV0L3BrZy9jb250cm9sbGVycy9hcHBzL2ZlZWRpbnZlbnRvcnkvc3RhdGVmdWxzZXQvc3RhdGVmdWxzZXQuZ28=) | `74.41% <0.00%> (ø)` | |

------

[Continue to review full report at Codecov](https://codecov.io/gh/clusternet/clusternet/pull/286?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/286?src=pr&el=footer&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=clusternet). Last update [d496d49...81b5ed5](https://codecov.io/gh/clusternet/clusternet/pull/286?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).

Garrybest: cc @qianjun1993

Patch:
diff --git a/manifests/crds/apps.clusternet.io_subscriptions.yaml b/manifests/crds/apps.clusternet.io_subscriptions.yaml
index 31627ba..93f258e 100644
--- a/manifests/crds/apps.clusternet.io_subscriptions.yaml
+++ b/manifests/crds/apps.clusternet.io_subscriptions.yaml
@@ -192,7 +192,7 @@ spec:
                       description: Static weight of subscriber when dividing replicas.
                         Present only for static divided scheduling.
                       format: int32
-                      minimum: 0
+                      minimum: 1
                       type: integer
                   required:
                   - clusterAffinity
@@ -218,13 +218,12 @@ spec:
                 description: Total number of Helm releases desired by this Subscription.
                 type: integer
               replicas:
-                additionalProperties:
-                  items:
-                    format: int32
-                    type: integer
-                  type: array
                 description: Desired replicas of targeted clusters for each feed.
-                type: object
+                items:
+                  additionalProperties:
+                    type: integer
+                  type: object
+                type: array
               specHash:
                 description: SpecHash calculates the hash value of current SubscriptionSpec.
                 format: int64
diff --git a/manifests/crds/clusters.clusternet.io_managedclusters.yaml b/manifests/crds/clusters.clusternet.io_managedclusters.yaml
index 01d630e..f4ad494 100644
--- a/manifests/crds/clusters.clusternet.io_managedclusters.yaml
+++ b/manifests/crds/clusters.clusternet.io_managedclusters.yaml
@@ -37,8 +37,8 @@ spec:
     - jsonPath: .status.k8sVersion
       name: KUBERNETES
       type: string
-    - jsonPath: .status.conditions[?(@.type=='Ready')].status
-      name: STATUS
+    - jsonPath: .status.readyz
+      name: READYZ
       type: string
     - jsonPath: .metadata.creationTimestamp
       name: AGE
diff --git a/pkg/apis/apps/v1alpha1/subscription.go b/pkg/apis/apps/v1alpha1/subscription.go
index 5152b7c..a24b9e9 100644
--- a/pkg/apis/apps/v1alpha1/subscription.go
+++ b/pkg/apis/apps/v1alpha1/subscription.go
@@ -89,7 +89,7 @@ type SubscriptionStatus struct {
 	// Desired replicas of targeted clusters for each feed.
 	//
 	// +optional
-	Replicas map[string][]int32 `json:"replicas,omitempty"`
+	Replicas []map[string]int `json:"replicas,omitempty"`
 
 	// SpecHash calculates the hash value of current SubscriptionSpec.
 	//
@@ -119,8 +119,8 @@ type Subscriber struct {
 	// Present only for static divided scheduling.
 	//
 	// +optional
-	// +kubebuilder:validation:Minimum=0
-	Weight int32 `json:"weight,omitempty"`
+	// +kubebuilder:validation:Minimum=1
+	Weight *int32 `json:"weight,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 62575f9..6e8daae 100644
--- a/pkg/apis/apps/v1alpha1/zz_generated.deepcopy.go
+++ b/pkg/apis/apps/v1alpha1/zz_generated.deepcopy.go
@@ -890,6 +890,11 @@ func (in *Subscriber) DeepCopyInto(out *Subscriber) {
 		*out = new(v1.LabelSelector)
 		(*in).DeepCopyInto(*out)
 	}
+	if in.Weight != nil {
+		in, out := &in.Weight, &out.Weight
+		*out = new(int32)
+		**out = **in
+	}
 	return
 }
 
@@ -1014,17 +1019,15 @@ func (in *SubscriptionStatus) DeepCopyInto(out *SubscriptionStatus) {
 	}
 	if in.Replicas != nil {
 		in, out := &in.Replicas, &out.Replicas
-		*out = make(map[string][]int32, len(*in))
-		for key, val := range *in {
-			var outVal []int32
-			if val == nil {
-				(*out)[key] = nil
-			} else {
-				in, out := &val, &outVal
-				*out = make([]int32, len(*in))
-				copy(*out, *in)
+		*out = make([]map[string]int, len(*in))
+		for i := range *in {
+			if (*in)[i] != nil {
+				in, out := &(*in)[i], &(*out)[i]
+				*out = make(map[string]int, len(*in))
+				for key, val := range *in {
+					(*out)[key] = val
+				}
 			}
-			(*out)[key] = outVal
 		}
 	}
 	return
diff --git a/pkg/apis/clusters/v1beta1/types.go b/pkg/apis/clusters/v1beta1/types.go
index 7ccf59d..6bd83d4 100644
--- a/pkg/apis/clusters/v1beta1/types.go
+++ b/pkg/apis/clusters/v1beta1/types.go
@@ -282,7 +282,7 @@ type ManagedClusterStatus struct {
 // +kubebuilder:printcolumn:name="CLUSTER TYPE",type=string,JSONPath=`.spec.clusterType`,description="The type of the cluster",priority=100
 // +kubebuilder:printcolumn:name="SYNC MODE",type=string,JSONPath=`.spec.syncMode`,description="The cluster sync mode"
 // +kubebuilder:printcolumn:name="KUBERNETES",type=string,JSONPath=".status.k8sVersion"
-// +kubebuilder:printcolumn:name="STATUS",type=string,JSONPath=".status.conditions[?(@.type=='Ready')].status"
+// +kubebuilder:printcolumn:name="READYZ",type=string,JSONPath=".status.readyz"
 // +kubebuilder:printcolumn:name="AGE",type="date",JSONPath=".metadata.creationTimestamp"
 
 // ManagedCluster is the Schema for the managedclusters API
diff --git a/pkg/controllers/apps/feedinventory/deployment/deployment.go b/pkg/controllers/apps/feedinventory/deployment/deployment.go
new file mode 100644
index 0000000..792428f
--- /dev/null
+++ b/pkg/controllers/apps/feedinventory/deployment/deployment.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.
+*/
+
+package deployment
+
+import (
+	"encoding/json"
+
+	k8sappsv1 "k8s.io/api/apps/v1"
+
+	appsapi "github.com/clusternet/clusternet/pkg/apis/apps/v1alpha1"
+	"github.com/clusternet/clusternet/pkg/controllers/apps/feedinventory/utils"
+)
+
+type Plugin struct {
+	name string
+}
+
+func NewPlugin() *Plugin {
+	return &Plugin{
+		name: "deployment",
+	}
+}
+
+func (pl *Plugin) Parser(rawData []byte) (*int32, appsapi.ReplicaRequirements, string, error) {
+	// use apps/v1 for deserialization
+	// all the fields that we needed are backward compatible with apps/v1beta1, apps/v1beta2, extensions/v1beta1
+	deploy := &k8sappsv1.Deployment{}
+
+	if err := json.Unmarshal(rawData, deploy); err != nil {
+		return nil, appsapi.ReplicaRequirements{}, "", err
+	}
+
+	return deploy.Spec.Replicas, utils.GetReplicaRequirements(deploy.Spec.Template.Spec), "/spec/replicas", nil
+}
+
+func (pl *Plugin) Name() string {
+	return pl.name
+}
+
+func (pl *Plugin) Kind() string {
+	return "Deployment"
+}
diff --git a/pkg/controllers/apps/feedinventory/feedinventory.go b/pkg/controllers/apps/feedinventory/feedinventory.go
new file mode 100644
index 0000000..71c94d9
--- /dev/null
+++ b/pkg/controllers/apps/feedinventory/feedinventory.go
@@ -0,0 +1,435 @@
+/*
+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 feedinventory
+
+import (
+	"context"
+	"encoding/json"
+	"fmt"
+	"reflect"
+	"sync"
+	"time"
+
+	corev1 "k8s.io/api/core/v1"
+	"k8s.io/apimachinery/pkg/api/errors"
+	apierrors "k8s.io/apimachinery/pkg/api/errors"
+	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
+	"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
+	"k8s.io/apimachinery/pkg/labels"
+	"k8s.io/apimachinery/pkg/runtime/schema"
+	utilerrors "k8s.io/apimachinery/pkg/util/errors"
+	utilruntime "k8s.io/apimachinery/pkg/util/runtime"
+	"k8s.io/apimachinery/pkg/util/wait"
+	"k8s.io/client-go/tools/cache"
+	"k8s.io/client-go/tools/record"
+	"k8s.io/client-go/util/workqueue"
+	"k8s.io/klog/v2"
+
+	appsapi "github.com/clusternet/clusternet/pkg/apis/apps/v1alpha1"
+	clusternetclientset "github.com/clusternet/clusternet/pkg/generated/clientset/versioned"
+	appinformers "github.com/clusternet/clusternet/pkg/generated/informers/externalversions/apps/v1alpha1"
+	applisters "github.com/clusternet/clusternet/pkg/generated/listers/apps/v1alpha1"
+	"github.com/clusternet/clusternet/pkg/known"
+	"github.com/clusternet/clusternet/pkg/utils"
+)
+
+var subKind = appsapi.SchemeGroupVersion.WithKind("Subscription")
+
+// Controller is a controller that handle HelmRelease
+type Controller struct {
+	clusternetClient clusternetclientset.Interface
+
+	// workqueue is a rate limited work queue. This is used to queue work to be
+	// processed instead of performing it as soon as a change happens. This
+	// means we can ensure we only process a fixed amount of resources at a
+	// time, and makes it easy to ensure we are never processing the same item
+	// simultaneously in two different workers.
+	workqueue workqueue.RateLimitingInterface
+
+	subLister      applisters.SubscriptionLister
+	subSynced      cache.InformerSynced
+	finvLister     applisters.FeedInventoryLister
+	finvSynced     cache.InformerSynced
+	manifestLister applisters.ManifestLister
+	manifestSynced cache.InformerSynced
+
+	recorder record.EventRecorder
+	registry Registry
+
+	// namespace where Manifests are created
+	reservedNamespace string
+}
+
+func NewController(clusternetClient clusternetclientset.Interface,
+	subsInformer appinformers.SubscriptionInformer,
+	finvInformer appinformers.FeedInventoryInformer,
+	manifestInformer appinformers.ManifestInformer,
+	recorder record.EventRecorder, registry Registry, reservedNamespace string) (*Controller, error) {
+	c := &Controller{
+		clusternetClient:  clusternetClient,
+		workqueue:         workqueue.NewNamedRateLimitingQueue(workqueue.DefaultControllerRateLimiter(), "feedInventory"),
+		subLister:         subsInformer.Lister(),
+		subSynced:         subsInformer.Informer().HasSynced,
+		finvLister:        finvInformer.Lister(),
+		finvSynced:        finvInformer.Informer().HasSynced,
+		manifestLister:    manifestInformer.Lister(),
+		manifestSynced:    manifestInformer.Informer().HasSynced,
+		recorder:          recorder,
+		registry:          registry,
+		reservedNamespace: reservedNamespace,
+	}
+
+	subsInformer.Informer().AddEventHandler(cache.ResourceEventHandlerFuncs{
+		AddFunc:    c.addSubscription,
+		UpdateFunc: c.updateSubscription,
+	})
+
+	finvInformer.Informer().AddEventHandler(cache.ResourceEventHandlerFuncs{
+		DeleteFunc: c.deleteFeedInventory,
+	})
+
+	// When a resource gets scaled, such as Deployment,
+	// re-enqueue all referring Subscription objects
+	manifestInformer.Informer().AddEventHandler(cache.ResourceEventHandlerFuncs{
+		AddFunc:    c.addManifest,
+		UpdateFunc: c.updateManifest,
+	})
+
+	// TODO: helm charts ?
+
+	return c, nil
+}
+
+// Run will set up the event handlers for types we are interested in, as well
+// as syncing informer caches and starting workers. It will block until stopCh
+// is closed, at which point it will shutdown the workqueue and wait for
+// workers to finish processing their current work items.
+func (c *Controller) Run(workers int, stopCh <-chan struct{}) {
+	defer utilruntime.HandleCrash()
+	defer c.workqueue.ShutDown()
+
+	klog.Info("starting feedInventory controller...")
+	defer klog.Info("shutting down feedInventory controller")
+
+	// Wait for the caches to be synced before starting workers
+	if !cache.WaitForNamedCacheSync("feedInventory-controller", stopCh,
+		c.subSynced, c.finvSynced, c.manifestSynced) {
+		return
+	}
+
+	klog.V(5).Infof("starting %d worker threads", workers)
+	// Launch workers to process Subscription resources
+	for i := 0; i < workers; i++ {
+		go wait.Until(c.runWorker, time.Second, stopCh)
+	}
+
+	<-stopCh
+}
+
+func (c *Controller) addSubscription(obj interface{}) {
+	sub := obj.(*appsapi.Subscription)
+	klog.V(4).Infof("[FeedInventory] adding Subscription %q", klog.KObj(sub))
+	c.enqueue(sub)
+}
+
+func (c *Controller) updateSubscription(old, cur interface{}) {
+	oldSub := old.(*appsapi.Subscription)
+	newSub := cur.(*appsapi.Subscription)
+
+	// Decide whether discovery has reported a spec change.
+	if reflect.DeepEqual(oldSub.Spec, newSub.Spec) {
+		klog.V(4).Infof("[FeedInventory] no updates on the spec of Subscription %s, skipping syncing", klog.KObj(newSub))
+		return
+	}
+
+	klog.V(4).Infof("[FeedInventory] updating Subscription %q", klog.KObj(newSub))
+	c.enqueue(newSub)
+}
+
+// enqueue takes a Subscription resource and converts it into a namespace/name
+// string which is then put onto the work queue. This method should *not* be
+// passed resources of any type other than Subscription.
+func (c *Controller) enqueue(sub *appsapi.Subscription) {
+	key, err := cache.MetaNamespaceKeyFunc(sub)
+	if err != nil {
+		utilruntime.HandleError(err)
+		return
+	}
+	c.workqueue.AddRateLimited(key)
+}
+
+// enqueueManifest takes a Manifest resource and converts it into a namespace/name
+// string which is then put onto the work queue.
+func (c *Controller) enqueueManifest(manifest *appsapi.Manifest) {
+	if manifest.DeletionTimestamp != nil {
+		return
+	}
+
+	subUIDs := []string{}
+	for k, v := range manifest.GetLabels() {
+		if v == subKind.Kind {
+			subUIDs = append(subUIDs, k)
+		}
+	}
+
+	allSubs := []*appsapi.Subscription{}
+	for _, subUID := range subUIDs {
+		subs, err := c.subLister.List(labels.SelectorFromSet(labels.Set{
+			subUID: subKind.Kind,
+		}))
+		if err != nil {
+			utilruntime.HandleError(err)
+			return
+		}
+		allSubs = append(allSubs, subs...)
+	}
+
+	for _, sub := range allSubs {
+		c.enqueue(sub)
+	}
+}
+
+func (c *Controller) addManifest(obj interface{}) {
+	manifest := obj.(*appsapi.Manifest)
+	klog.V(4).Infof("[FeedInventory] adding Manifest %q", klog.KObj(manifest))
+	c.enqueueManifest(manifest)
+}
+
+func (c *Controller) updateManifest(old, cur interface{}) {
+	oldManifest := old.(*appsapi.Manifest)
+	newManifest := cur.(*appsapi.Manifest)
+
+	// Decide whether discovery has reported a spec change.
+	if reflect.DeepEqual(oldManifest.Template, newManifest.Template) {
+		klog.V(4).Infof("[FeedInventory] no updates on Manifest template %s, skipping syncing", klog.KObj(oldManifest))
+		return
+	}
+
+	klog.V(4).Infof("[FeedInventory] updating Manifest %q", klog.KObj(oldManifest))
+	c.enqueueManifest(newManifest)
+}
+
+func (c *Controller) deleteFeedInventory(obj interface{}) {
+	finv := obj.(*appsapi.FeedInventory)
+	klog.V(4).Infof("[FeedInventory] deleting FeedInventory %q", klog.KObj(finv))
+	c.workqueue.AddRateLimited(klog.KObj(finv))
+}
+
+// runWorker is a long-running function that will continually call the
+// processNextWorkItem function in order to read and process a message on the
+// workqueue.
+func (c *Controller) runWorker() {
+	for c.processNextWorkItem() {
+	}
+}
+
+// processNextWorkItem will read a single work item off the workqueue and
+// attempt to process it, by calling the syncHandler.
+func (c *Controller) processNextWorkItem() bool {
+	obj, shutdown := c.workqueue.Get()
+
+	if shutdown {
+		return false
+	}
+
+	// We wrap this block in a func so we can defer c.workqueue.Done.
+	err := func(obj interface{}) error {
+		// We call Done here so the workqueue knows we have finished
+		// processing this item. We also must remember to call Forget if we
+		// do not want this work item being re-queued. For example, we do
+		// not call Forget if a transient error occurs, instead the item is
+		// put back on the workqueue and attempted again after a back-off
+		// period.
+		defer c.workqueue.Done(obj)
+		var key string
+		var ok bool
+		// We expect strings to come off the workqueue. These are of the
+		// form namespace/name. We do this as the delayed nature of the
+		// workqueue means the items in the informer cache may actually be
+		// more up to date that when the item was initially put onto the
+		// workqueue.
+		if key, ok = obj.(string); !ok {
+			// As the item in the workqueue is actually invalid, we call
+			// Forget here else we'd go into a loop of attempting to
+			// process a work item that is invalid.
+			c.workqueue.Forget(obj)
+			utilruntime.HandleError(fmt.Errorf("expected string in workqueue but got %#v", obj))
+			return nil
+		}
+		// Run the syncHandler, passing it the namespace/name string of the
+		// Subscription resource to be synced.
+		if err := c.syncHandler(key); err != nil {
+			// Put the item back on the workqueue to handle any transient errors.
+			c.workqueue.AddRateLimited(key)
+			return fmt.Errorf("error syncing '%s': %s, requeuing", key, err.Error())
+		}
+		// Finally, if no error occurs we Forget this item so it does not
+		// get queued again until another change happens.
+		c.workqueue.Forget(obj)
+		klog.Infof("[FeedInventory] successfully synced Subscription %q", key)
+		return nil
+	}(obj)
+
+	if err != nil {
+		utilruntime.HandleError(err)
+		return true
+	}
+
+	return true
+}
+
+// syncHandler compares the actual state with the desired, and attempts to
+// converge the two. It then updates the Status block of the Subscription resource
+// with the current status of the resource.
+func (c *Controller) syncHandler(key string) error {
+	// If an error occurs during handling, we'll requeue the item so we can
+	// attempt processing again later. This could have been caused by a
+	// temporary network failure, or any other transient reason.
+
+	// Convert the namespace/name string into a distinct namespace and name
+	ns, name, err := cache.SplitMetaNamespaceKey(key)
+	if err != nil {
+		utilruntime.HandleError(fmt.Errorf("invalid resource key: %s", key))
+		return nil
+	}
+
+	klog.V(4).Infof("[FeedInventory] start processing Subscription %q", key)
+	// Get the Subscription resource with this name
+	sub, err := c.subLister.Subscriptions(ns).Get(name)
+	// The Subscription resource may no longer exist, in which case we stop processing.
+	if errors.IsNotFound(err) {
+		klog.V(2).Infof("Subscription %q has been deleted", key)
+		return nil
+	}
+	if err != nil {
+		return err
+	}
+
+	if sub.DeletionTimestamp != nil {
+		return nil
+	}
+
+	if sub.Spec.SchedulingStrategy != appsapi.DividingSchedulingStrategyType {
+		return nil
+	}
+
+	sub.Kind = subKind.Kind
+	sub.APIVersion = subKind.GroupVersion().String()
+
+	finv := &appsapi.FeedInventory{
+		ObjectMeta: metav1.ObjectMeta{
+			Name:      sub.Name,
+			Namespace: sub.Namespace,
+			Labels: map[string]string{
+				known.ObjectCreatedByLabel: known.ClusternetHubName,
+				// add subscription info
+				known.ConfigSubscriptionNameLabel:      sub.Name,
+				known.ConfigSubscriptionNamespaceLabel: sub.Namespace,
+				known.ConfigSubscriptionUIDLabel:       string(sub.UID),
+			},
+		},
+		Spec: appsapi.FeedInventorySpec{
+			Feeds: make([]appsapi.FeedOrder, len(sub.Spec.Feeds)),
+		},
+	}
+	finv.SetOwnerReferences([]metav1.OwnerReference{*metav1.NewControllerRef(sub, subKind)})
+
+	wg := sync.WaitGroup{}
+	wg.Add(len(sub.Spec.Feeds))
+	errCh := make(chan error, len(sub.Spec.Feeds))
+	for idx, feed := range sub.Spec.Feeds {
+		go func(idx int, feed appsapi.Feed) {
+			defer wg.Done()
+
+			manifests, err2 := utils.ListManifestsBySelector(c.reservedNamespace, c.manifestLister, feed)
+			if err2 != nil {
+				errCh <- err2
+				return
+			}
+			if manifests == nil {
+				errCh <- apierrors.NewNotFound(schema.GroupResource{Resource: feed.Kind}, feed.Name)
+				return
+			}
+
+			gvk, err2 := getGroupVersionKind(manifests[0].Template.Raw)
+			if err2 != nil {
+				errCh <- err2
+				return
+			}
+
+			var desiredReplicas *int32
+			var replicaRequirements appsapi.ReplicaRequirements
+			var replicaJsonPath string
+			plugin, ok := c.registry[gvk]
+			if ok {
+				klog.V(7).Infof("using plugin %q to parse %q", plugin.Name(), plugin.Kind())
+				desiredReplicas, replicaRequirements, replicaJsonPath, err2 = plugin.Parser(manifests[0].Template.Raw)
+				if err2 != nil {
+					errCh <- err2
+					return
+				}
+			}
+
+			finv.Spec.Feeds[idx] = appsapi.FeedOrder{
+				Feed:                feed,
+				DesiredReplicas:     desiredReplicas,
+				ReplicaRequirements: replicaRequirements,
+				ReplicaJsonPath:     replicaJsonPath,
+			}
+		}(idx, feed)
+	}
+
+	wg.Wait()
+
+	// collect errors
+	close(errCh)
+	var allErrs []error
+	for err3 := range errCh {
+		allErrs = append(allErrs, err3)
+	}
+
+	if len(allErrs) != 0 {
+		err = utilerrors.NewAggregate(allErrs)
+		c.recorder.Event(sub, corev1.EventTypeWarning, "UnParseableFeeds", err.Error())
+		return err
+	}
+
+	_, err = c.clusternetClient.AppsV1alpha1().FeedInventories(finv.Namespace).Create(context.TODO(), finv, metav1.CreateOptions{})
+	if err != nil && apierrors.IsAlreadyExists(err) {
+		_, err = c.clusternetClient.AppsV1alpha1().FeedInventories(finv.Namespace).Update(context.TODO(), finv, metav1.UpdateOptions{})
+	}
+
+	if err != nil {
+		c.recorder.Event(sub, corev1.EventTypeWarning, "FeedInventoryNotSynced", err.Error())
+	} else {
+		c.recorder.Event(sub, corev1.EventTypeNormal, "FeedInventorySynced", "FeedInventory synced successfully")
+	}
+	return err
+}
+
+func getGroupVersionKind(rawData []byte) (schema.GroupVersionKind, error) {
+	object := &unstructured.Unstructured{}
+	if err := json.Unmarshal(rawData, object); err != nil {
+		return schema.GroupVersionKind{}, err
+	}
+
+	groupVersion, err := schema.ParseGroupVersion(object.GetAPIVersion())
+	if err != nil {
+		return schema.GroupVersionKind{}, err
+	}
+	return groupVersion.WithKind(object.GetKind()), nil
+}
diff --git a/pkg/controllers/apps/feedinventory/registry.go b/pkg/controllers/apps/feedinventory/registry.go
new file mode 100644
index 0000000..0688d23
--- /dev/null
+++ b/pkg/controllers/apps/feedinventory/registry.go
@@ -0,0 +1,64 @@
+/*
+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 feedinventory
+
+import (
+	"k8s.io/apimachinery/pkg/runtime/schema"
+
+	appsapi "github.com/clusternet/clusternet/pkg/apis/apps/v1alpha1"
+	"github.com/clusternet/clusternet/pkg/controllers/apps/feedinventory/deployment"
+	"github.com/clusternet/clusternet/pkg/controllers/apps/feedinventory/replicaset"
+	"github.com/clusternet/clusternet/pkg/controllers/apps/feedinventory/statefulset"
+)
+
+// Registry is a collection of all available workload parser.
+type Registry map[schema.GroupVersionKind]PluginFactory
+
+// PluginFactory is an interface that must be implemented for each plugin.
+type PluginFactory interface {
+	// Parser parses the raw data to get the replicas, resource requirements, replica jsonpath, etc.
+	Parser(rawData []byte) (*int32, appsapi.ReplicaRequirements, string, error)
+	// Name returns name of the plugin. It is used in logs, etc.
+	Name() string
+	// Kind returns the resource kind.
+	Kind() string
+}
+
+// NewInTreeRegistry builds the registry with all the in-tree plugins.
+func NewInTreeRegistry() Registry {
+	deployPlugin := deployment.NewPlugin()
+	replicasetPlugin := replicaset.NewPlugin()
+	statefulSetPlugin := statefulset.NewPlugin()
+
+	return map[schema.GroupVersionKind]PluginFactory{
+		// Deployment
+		{Group: "apps", Version: "v1", Kind: deployPlugin.Kind()}:            deployPlugin,
+		{Group: "apps", Version: "v1beta1", Kind: deployPlugin.Kind()}:       deployPlugin,
+		{Group: "apps", Version: "v1beta2", Kind: deployPlugin.Kind()}:       deployPlugin,
+		{Group: "extensions", Version: "v1beta1", Kind: deployPlugin.Kind()}: deployPlugin,
+
+		// ReplicaSet
+		{Group: "apps", Version: "v1", Kind: replicasetPlugin.Kind()}:            replicasetPlugin,
+		{Group: "extensions", Version: "v1beta1", Kind: replicasetPlugin.Kind()}: replicasetPlugin,
+
+		// StatefulSet
+		{Group: "apps", Version: "v1", Kind: deployPlugin.Kind()}:            statefulSetPlugin,
+		{Group: "apps", Version: "v1beta1", Kind: deployPlugin.Kind()}:       statefulSetPlugin,
+		{Group: "apps", Version: "v1beta2", Kind: deployPlugin.Kind()}:       statefulSetPlugin,
+		{Group: "extensions", Version: "v1beta1", Kind: deployPlugin.Kind()}: statefulSetPlugin,
+	}
+}
diff --git a/pkg/controllers/apps/feedinventory/replicaset/replicaset.go b/pkg/controllers/apps/feedinventory/replicaset/replicaset.go
new file mode 100644
index 0000000..4d6cdfb
--- /dev/null
+++ b/pkg/controllers/apps/feedinventory/replicaset/replicaset.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.
+*/
+
+package replicaset
+
+import (
+	"encoding/json"
+
+	k8sappsv1 "k8s.io/api/apps/v1"
+
+	appsapi "github.com/clusternet/clusternet/pkg/apis/apps/v1alpha1"
+	"github.com/clusternet/clusternet/pkg/controllers/apps/feedinventory/utils"
+)
+
+type Plugin struct {
+	name string
+}
+
+func NewPlugin() *Plugin {
+	return &Plugin{
+		name: "replicaset",
+	}
+}
+
+func (pl *Plugin) Parser(rawData []byte) (*int32, appsapi.ReplicaRequirements, string, error) {
+	// use apps/v1 for deserialization
+	// all the fields that we needed are backward compatible with extensions/v1beta1
+	rs := &k8sappsv1.ReplicaSet{}
+
+	if err := json.Unmarshal(rawData, rs); err != nil {
+		return nil, appsapi.ReplicaRequirements{}, "", err
+	}
+
+	return rs.Spec.Replicas, utils.GetReplicaRequirements(rs.Spec.Template.Spec), "/spec/replicas", nil
+}
+
+func (pl *Plugin) Name() string {
+	return pl.name
+}
+
+func (pl *Plugin) Kind() string {
+	return "ReplicaSet"
+}
diff --git a/pkg/controllers/apps/feedinventory/statefulset/statefulset.go b/pkg/controllers/apps/feedinventory/statefulset/statefulset.go
new file mode 100644
index 0000000..e0ec2ff
--- /dev/null
+++ b/pkg/controllers/apps/feedinventory/statefulset/statefulset.go
@@ -0,0 +1,55 @@
+/*
+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 statefulset
+
+import (
+	"encoding/json"
+
+	appsapi "github.com/clusternet/clusternet/pkg/apis/apps/v1alpha1"
+	"github.com/clusternet/clusternet/pkg/controllers/apps/feedinventory/utils"
+	k8sappsv1 "k8s.io/api/apps/v1"
+)
+
+type Plugin struct {
+	name string
+}
+
+func NewPlugin() *Plugin {
+	return &Plugin{
+		name: "statefulset",
+	}
+}
+
+func (pl *Plugin) Parser(rawData []byte) (*int32, appsapi.ReplicaRequirements, string, error) {
+	// use apps/v1 for deserialization
+	// all the fields that we needed are backward compatible with apps/v1beta1, apps/v1beta2, extensions/v1beta1
+	sts := &k8sappsv1.StatefulSet{}
+
+	if err := json.Unmarshal(rawData, sts); err != nil {
+		return nil, appsapi.ReplicaRequirements{}, "", err
+	}
+
+	return sts.Spec.Replicas, utils.GetReplicaRequirements(sts.Spec.Template.Spec), "/spec/replicas", nil
+}
+
+func (pl *Plugin) Name() string {
+	return pl.name
+}
+
+func (pl *Plugin) Kind() string {
+	return "StatefulSet"
+}
diff --git a/pkg/controllers/apps/feedinventory/utils/utils.go b/pkg/controllers/apps/feedinventory/utils/utils.go
new file mode 100644
index 0000000..ed30b8c
--- /dev/null
+++ b/pkg/controllers/apps/feedinventory/utils/utils.go
@@ -0,0 +1,93 @@
+/*
+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 utils
+
+import (
+	corev1 "k8s.io/api/core/v1"
+	"k8s.io/apimachinery/pkg/api/resource"
+
+	appsapi "github.com/clusternet/clusternet/pkg/apis/apps/v1alpha1"
+)
+
+func GetReplicaRequirements(podSpec corev1.PodSpec) appsapi.ReplicaRequirements {
+	resourceLimits := map[corev1.ResourceName]resource.Quantity{}
+	resourceRequests := map[corev1.ResourceName]resource.Quantity{}
+	for _, container := range podSpec.Containers {
+		// calculate total resource limits
+		for resourceName, limit := range container.Resources.Limits {
+			curLimit, ok := resourceLimits[resourceName]
+			if !ok {
+				resourceLimits[resourceName] = limit
+				continue
+			}
+
+			curLimit.Add(limit)
+			resourceLimits[resourceName] = curLimit
+		}
+
+		// calculate total resource requests
+		for resourceName, request := range container.Resources.Requests {
+			curRequest, ok := resourceRequests[resourceName]
+			if !ok {
+				resourceRequests[resourceName] = request
+				continue
+			}
+
+			curRequest.Add(request)
+			resourceRequests[resourceName] = curRequest
+		}
+	}
+
+	// take max_resource(sum_pod, any_init_container)
+	for _, container := range podSpec.InitContainers {
+		// take max resource limits
+		for resourceName, limit := range container.Resources.Limits {
+			curLimit, ok := resourceLimits[resourceName]
+			if !ok {
+				resourceLimits[resourceName] = limit
+				continue
+			}
+
+			if limit.Cmp(curLimit) == 1 {
+				resourceLimits[resourceName] = limit
+			}
+		}
+
+		// take max resource requests
+		for resourceName, request := range container.Resources.Requests {
+			curRequest, ok := resourceRequests[resourceName]
+			if !ok {
+				resourceRequests[resourceName] = request
+				continue
+			}
+
+			if request.Cmp(curRequest) == 1 {
+				resourceRequests[resourceName] = request
+			}
+		}
+	}
+
+	return appsapi.ReplicaRequirements{
+		NodeSelector: podSpec.NodeSelector,
+		Tolerations:  podSpec.Tolerations,
+		Affinity:     podSpec.Affinity,
+		Resources: corev1.ResourceRequirements{
+			Limits:   resourceLimits,
+			Requests: resourceRequests,
+		},
+	}
+}
diff --git a/pkg/controllers/clusters/clusterstatus/clusterstatus_controller.go b/pkg/controllers/clusters/clusterstatus/clusterstatus_controller.go
index a7e6ac1..c195157 100644
--- a/pkg/controllers/clusters/clusterstatus/clusterstatus_controller.go
+++ b/pkg/controllers/clusters/clusterstatus/clusterstatus_controller.go
@@ -169,7 +169,7 @@ func (c *Controller) getHealthStatus(ctx context.Context, path string) bool {
 }
 
 func (c *Controller) getCondition(status clusterapi.ManagedClusterStatus) metav1.Condition {
-	if (status.Livez && status.Readyz) || status.Healthz {
+	if status.Livez && status.Readyz {
 		return metav1.Condition{
 			Type:               clusterapi.ClusterReady,
 			Status:             metav1.ConditionTrue,
diff --git a/pkg/features/features.go b/pkg/features/features.go
index 53373a1..3216d47 100644
--- a/pkg/features/features.go
+++ b/pkg/features/features.go
@@ -70,6 +70,15 @@ const (
 	// alpha: v0.8.0
 	// Setting on agent side.
 	Recovery featuregate.Feature = "Recovery"
+
+	// FeedInventory runs default in-tree registry to parse the schema of a resource, such as Deployment,
+	// Statefulset, etc.
+	// This feature gate should be closed when external FeedInventory controller is used.
+	//
+	// owner: @dixudx
+	// alpha: v0.9.0
+	// Setting on hub side.
+	FeedInventory featuregate.Feature = "FeedInventory"
 )
 
 func init() {
@@ -86,4 +95,5 @@ var defaultClusternetFeatureGates = map[featuregate.Feature]featuregate.FeatureS
 	ShadowAPI:           {Default: false, PreRelease: featuregate.Alpha, LockToDefault: false},
 	FeedInUseProtection: {Default: false, PreRelease: featuregate.Alpha, LockToDefault: false},
 	Recovery:            {Default: false, PreRelease: featuregate.Alpha, LockToDefault: false},
+	FeedInventory:       {Default: false, PreRelease: featuregate.Alpha, LockToDefault: false},
 }
diff --git a/pkg/hub/deployer/deployer.go b/pkg/hub/deployer/deployer.go
index fe55b9b..a99eb24 100644
--- a/pkg/hub/deployer/deployer.go
+++ b/pkg/hub/deployer/deployer.go
@@ -47,6 +47,7 @@ import (
 
 	appsapi "github.com/clusternet/clusternet/pkg/apis/apps/v1alpha1"
 	"github.com/clusternet/clusternet/pkg/controllers/apps/base"
+	"github.com/clusternet/clusternet/pkg/controllers/apps/feedinventory"
 	"github.com/clusternet/clusternet/pkg/controllers/apps/helmchart"
 	"github.com/clusternet/clusternet/pkg/controllers/apps/manifest"
 	"github.com/clusternet/clusternet/pkg/controllers/apps/subscription"
@@ -90,6 +91,7 @@ type Deployer struct {
 	mfstController  *manifest.Controller
 	baseController  *base.Controller
 	chartController *helmchart.Controller
+	finvController  *feedinventory.Controller
 
 	helmDeployer    *helm.Deployer
 	genericDeployer *generic.Deployer
@@ -196,6 +198,18 @@ func NewDeployer(apiserverURL, systemNamespace, reservedNamespace string,
 	}
 	deployer.localizer = l
 
+	finv, err := feedinventory.NewController(clusternetclient,
+		clusternetInformerFactory.Apps().V1alpha1().Subscriptions(),
+		clusternetInformerFactory.Apps().V1alpha1().FeedInventories(),
+		clusternetInformerFactory.Apps().V1alpha1().Manifests(),
+		deployer.recorder,
+		feedinventory.NewInTreeRegistry(),
+		reservedNamespace)
+	if err != nil {
+		return nil, err
+	}
+	deployer.finvController = finv
+
 	return deployer, nil
 }
 
@@ -223,6 +237,11 @@ func (deployer *Deployer) Run(workers int, stopCh <-chan struct{}) {
 	go deployer.baseController.Run(workers, stopCh)
 	go deployer.localizer.Run(workers, stopCh)
 
+	// When using external FeedInventory controller, this feature gate should be closed
+	if utilfeature.DefaultFeatureGate.Enabled(features.FeedInventory) {
+		go deployer.finvController.Run(workers, stopCh)
+	}
+
 	<-stopCh
 }
 
@@ -583,9 +602,18 @@ func (deployer *Deployer) populateDescriptions(base *appsapi.Base) error {
 			Finalizers: []string{
 				known.AppFinalizer,
 			},
+			OwnerReferences: []metav1.OwnerReference{
+				{
+					APIVersion:         baseKind.Version,
+					Kind:               baseKind.Kind,
+					Name:               base.Name,
+					UID:                base.UID,
+					Controller:         utilpointer.BoolPtr(true),
+					BlockOwnerDeletion: utilpointer.BoolPtr(true),
+				},
+			},
 		},
 	}
-	descTemplate.SetOwnerReferences([]metav1.OwnerReference{*metav1.NewControllerRef(base, baseKind)})
 
 	var allErrs []error
 	if len(allChartRefs) > 0 {
diff --git a/pkg/hub/deployer/helm/helm.go b/pkg/hub/deployer/helm/helm.go
index b6273c9..db6b32e 100644
--- a/pkg/hub/deployer/helm/helm.go
+++ b/pkg/hub/deployer/helm/helm.go
@@ -291,6 +291,16 @@ func (deployer *Deployer) populateHelmRelease(desc *appsapi.Description) error {
 				Finalizers: []string{
 					known.AppFinalizer,
 				},
+				OwnerReferences: []metav1.OwnerReference{
+					{
+						APIVersion:         descriptionKind.Version,
+						Kind:               descriptionKind.Kind,
+						Name:               desc.Name,
+						UID:                desc.UID,
+						Controller:         utilpointer.BoolPtr(true),
+						BlockOwnerDeletion: utilpointer.BoolPtr(true),
+					},
+				},
 			},
 			Spec: appsapi.HelmReleaseSpec{
 				ReleaseName:     utilpointer.String(chart.Name), // default to be the HelmChart name
@@ -299,7 +309,6 @@ func (deployer *Deployer) populateHelmRelease(desc *appsapi.Description) error {
 				Overrides:       desc.Spec.Raw[idx],
 			},
 		}
-		hr.SetOwnerReferences([]metav1.OwnerReference{*metav1.NewControllerRef(desc, descriptionKind)})
 		hrsToBeDeleted.Delete(klog.KObj(hr).String())
 
 		err = deployer.syncHelmRelease(desc, hr)
diff --git a/pkg/scheduler/algorithm/dividing/static.go b/pkg/scheduler/algorithm/dividing/static.go
deleted file mode 100644
index fe85116..0000000
--- a/pkg/scheduler/algorithm/dividing/static.go
+++ /dev/null
@@ -1,114 +0,0 @@
-package dividing
-
-import (
-	"sort"
-
-	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
-	"k8s.io/apimachinery/pkg/labels"
-
-	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/utils"
-)
-
-type weightList struct {
-	weightSum int64
-	weights   []int64
-}
-
-// StaticDivideReplicas will fill the target replicas of all feeds based on a weight list and feed finv.
-func StaticDivideReplicas(selected *framework.TargetClusters, sub *appsapi.Subscription, clusters []*clusterapi.ManagedCluster, finv *appsapi.FeedInventory) error {
-	wl := getWeightList(sub.Spec.Subscribers, clusters)
-	if wl.weightSum == 0 {
-		return nil
-	}
-	if selected.Replicas == nil {
-		selected.Replicas = make(map[string][]int32)
-	}
-	for _, feed := range sub.Spec.Feeds {
-		var order *appsapi.FeedOrder
-		for i := range finv.Spec.Feeds {
-			if feed == finv.Spec.Feeds[i].Feed {
-				order = &finv.Spec.Feeds[i]
-				break
-			}
-		}
-		if order != nil && order.DesiredReplicas != nil {
-			replicas := staticDivideReplicas(*order.DesiredReplicas, &wl)
-			selected.Replicas[utils.GetFeedKey(&order.Feed)] = replicas
-		} else {
-			selected.Replicas[utils.GetFeedKey(&order.Feed)] = []int32{}
-		}
-	}
-	return nil
-}
-
-// staticDivideReplicas divides replicas by the weight list.
-func staticDivideReplicas(desiredReplicas int32, wl *weightList) []int32 {
-	res := make([]int32, len(wl.weights))
-	remain := wl.weightSum
-	for i, weight := range wl.weights {
-		replica := weight * int64(desiredReplicas) / wl.weightSum
-		res[i] = int32(replica)
-		remain -= replica
-	}
-	if remain == 0 {
-		return res
-	}
-
-	remainInt := int(remain)
-	clusterIndices := sortByWeight(wl.weights)
-	if remainInt <= len(clusterIndices) {
-		for i := 0; i < remainInt; i++ {
-			res[clusterIndices[i]]++
-		}
-	} else {
-		avg, bounder := remainInt/len(clusterIndices), remainInt%len(clusterIndices)
-		for i := range clusterIndices {
-			if i < bounder {
-				res[clusterIndices[i]] += int32(avg) + 1
-			} else {
-				res[clusterIndices[i]] += int32(avg)
-			}
-		}
-	}
-	return res
-}
-
-func sortByWeight(weights []int64) []int {
-	clusterIndices := make([]int, len(weights))
-	for i := range weights {
-		clusterIndices[i] = i
-	}
-	sort.Slice(clusterIndices, func(i, j int) bool {
-		return weights[clusterIndices[i]] > weights[clusterIndices[j]]
-	})
-	return clusterIndices
-}
-
-// getWeightList analyzes weight from subscribers and form a weightList based on the sequence of clusters.
-func getWeightList(subscribers []appsapi.Subscriber, clusters []*clusterapi.ManagedCluster) weightList {
-	res := weightList{
-		weights: make([]int64, 0, len(clusters)),
-	}
-	for _, cluster := range clusters {
-		var clusterWeightSum int64
-		for _, subscriber := range subscribers {
-			if subscriber.Weight <= 0 {
-				continue
-			}
-			selector, err := metav1.LabelSelectorAsSelector(subscriber.ClusterAffinity)
-			if err != nil {
-				continue
-			}
-			if !selector.Matches(labels.Set(cluster.Labels)) {
-				continue
-			}
-			clusterWeightSum += int64(subscriber.Weight)
-			res.weightSum += int64(subscriber.Weight)
-		}
-		res.weights = append(res.weights, clusterWeightSum)
-	}
-	return res
-}
diff --git a/pkg/scheduler/algorithm/generic.go b/pkg/scheduler/algorithm/generic.go
index bfbad3a..a3c0dd0 100644
--- a/pkg/scheduler/algorithm/generic.go
+++ b/pkg/scheduler/algorithm/generic.go
@@ -31,7 +31,6 @@ import (
 
 	appsapi "github.com/clusternet/clusternet/pkg/apis/apps/v1alpha1"
 	clusterapi "github.com/clusternet/clusternet/pkg/apis/clusters/v1beta1"
-	"github.com/clusternet/clusternet/pkg/scheduler/algorithm/dividing"
 	schedulerapis "github.com/clusternet/clusternet/pkg/scheduler/apis"
 	schedulercache "github.com/clusternet/clusternet/pkg/scheduler/cache"
 	framework "github.com/clusternet/clusternet/pkg/scheduler/framework/interfaces"
@@ -65,7 +64,7 @@ type genericScheduler struct {
 // Schedule tries to schedule the given subscription to multiple clusters.
 // If it succeeds, it will return the namespaced names of ManagedClusters.
 // If it fails, it will return a FitError error with reasons.
-func (g *genericScheduler) Schedule(ctx context.Context, fwk framework.Framework, sub *appsapi.Subscription, finv *appsapi.FeedInventory) (result ScheduleResult, err error) {
+func (g *genericScheduler) Schedule(ctx context.Context, fwk framework.Framework, sub *appsapi.Subscription) (result ScheduleResult, err error) {
 	trace := utiltrace.New("Scheduling", utiltrace.Field{Key: "namespace", Value: sub.Namespace}, utiltrace.Field{Key: "name", Value: sub.Name})
 	defer trace.LogIfLong(100 * time.Millisecond)
 
@@ -92,7 +91,7 @@ func (g *genericScheduler) Schedule(ctx context.Context, fwk framework.Framework
 		return result, err
 	}
 
-	clusters, err := g.selectClusters(priorityList, sub, finv)
+	clusters, err := g.selectClusters(priorityList, sub)
 	trace.Step("Prioritizing done")
 
 	return ScheduleResult{
@@ -104,23 +103,16 @@ func (g *genericScheduler) Schedule(ctx context.Context, fwk framework.Framework
 
 // selectClusters takes a prioritized list of clusters and then picks a fraction of clusters
 // in a reservoir sampling manner from the clusters that had the highest score.
-func (g *genericScheduler) selectClusters(clusterScoreList framework.ClusterScoreList, sub *appsapi.Subscription, finv *appsapi.FeedInventory) (framework.TargetClusters, error) {
+func (g *genericScheduler) selectClusters(clusterScoreList framework.ClusterScoreList, _ *appsapi.Subscription) ([]string, error) {
 	if len(clusterScoreList) == 0 {
-		return framework.TargetClusters{}, fmt.Errorf("empty clusterScoreList")
+		return nil, fmt.Errorf("empty clusterScoreList")
 	}
 
-	var selected framework.TargetClusters
+	var selected []string
 	for _, clusterScore := range clusterScoreList {
 		// TODO: sampling with scores
-		selected.BindingClusters = append(selected.BindingClusters, clusterScore.NamespacedName)
+		selected = append(selected, clusterScore.NamespacedName)
 	}
-
-	if sub.Spec.SchedulingStrategy == appsapi.DividingSchedulingStrategyType {
-		if err := g.divideReplicas(&selected, sub, finv); err != nil {
-			return framework.TargetClusters{}, fmt.Errorf("failed to divide replicas: %v", err)
-		}
-	}
-
 	return selected, nil
 }
 
@@ -263,25 +255,6 @@ func (g *genericScheduler) findClustersThatPassFilters(ctx context.Context, fwk
 	return feasibleClusters, nil
 }
 
-func (g *genericScheduler) divideReplicas(selected *framework.TargetClusters, sub *appsapi.Subscription, finv *appsapi.FeedInventory) error {
-	switch sub.Spec.DividingScheduling.Type {
-	case appsapi.StaticReplicaDividingType:
-		return g.staticDivideReplicas(selected, sub, finv)
-	}
-	return nil
-}
-
-func (g *genericScheduler) staticDivideReplicas(selected *framework.TargetClusters, sub *appsapi.Subscription, finv *appsapi.FeedInventory) (err error) {
-	clusters := make([]*clusterapi.ManagedCluster, len(selected.BindingClusters))
-	for i, name := range selected.BindingClusters {
-		if clusters[i], err = g.cache.Get(name); err != nil {
-			return err
-		}
-	}
-
-	return dividing.StaticDivideReplicas(selected, sub, clusters, finv)
-}
-
 // prioritizeClusters prioritizes the clusters by running the score plugins,
 // which return a score for each cluster from the call to RunScorePlugins().
 // The scores from each plugin are added together to make the score for that cluster, then
diff --git a/pkg/scheduler/algorithm/interface.go b/pkg/scheduler/algorithm/interface.go
index 2ecbc22..077aa2e 100644
--- a/pkg/scheduler/algorithm/interface.go
+++ b/pkg/scheduler/algorithm/interface.go
@@ -26,14 +26,14 @@ import (
 // ScheduleAlgorithm is an interface implemented by things that know how to schedule resources to target
 // managed clusters.
 type ScheduleAlgorithm interface {
-	Schedule(context.Context, framework.Framework, *appsapi.Subscription, *appsapi.FeedInventory) (scheduleResult ScheduleResult, err error)
+	Schedule(context.Context, framework.Framework, *appsapi.Subscription) (scheduleResult ScheduleResult, err error)
 }
 
 // ScheduleResult represents the result of one subscription scheduled. It will contain
 // the final selected clusters, along with the selected intermediate information.
 type ScheduleResult struct {
 	// the scheduler suggest clusters (namespaced name)
-	SuggestedClusters framework.TargetClusters
+	SuggestedClusters []string
 	// Number of clusters scheduler evaluated on one subscription scheduled
 	EvaluatedClusters int
 	// Number of feasible clusters on one subscription scheduled
diff --git a/pkg/scheduler/framework/interfaces/interface.go b/pkg/scheduler/framework/interfaces/interface.go
index 1247449..12281cd 100644
--- a/pkg/scheduler/framework/interfaces/interface.go
+++ b/pkg/scheduler/framework/interfaces/interface.go
@@ -217,13 +217,13 @@ type ReservePlugin interface {
 	// Reserve is called by the scheduling framework when the scheduler cache is
 	// updated. If this method returns a failed Status, the scheduler will call
 	// the Unreserve method for all enabled ReservePlugins.
-	Reserve(ctx context.Context, sub *appsapi.Subscription, targetClusters TargetClusters) *Status
+	Reserve(ctx context.Context, sub *appsapi.Subscription, namespacedClusters []string) *Status
 	// Unreserve is called by the scheduling framework when a reserved subscription was
 	// rejected, an error occurred during reservation of subsequent plugins, or
 	// in a later phase. The Unreserve method implementation must be idempotent
 	// and may be called by the scheduler even if the corresponding Reserve
 	// method for the same plugin was not called.
-	Unreserve(ctx context.Context, sub *appsapi.Subscription, targetClusters TargetClusters)
+	Unreserve(ctx context.Context, sub *appsapi.Subscription, namespacedClusters []string)
 }
 
 // PreBindPlugin is an interface that must be implemented by "PreBind" plugins.
@@ -233,7 +233,7 @@ type PreBindPlugin interface {
 
 	// PreBind is called before binding a subscription. All prebind plugins must return
 	// success or the subscription will be rejected and won't be sent for binding.
-	PreBind(ctx context.Context, sub *appsapi.Subscription, targetClusters TargetClusters) *Status
+	PreBind(ctx context.Context, sub *appsapi.Subscription, namespacedClusters []string) *Status
 }
 
 // PostBindPlugin is an interface that must be implemented by "PostBind" plugins.
@@ -245,7 +245,7 @@ type PostBindPlugin interface {
 	// informational. A common application of this extension point is for cleaning
 	// up. If a plugin needs to clean-up its state after a subscription is scheduled and
 	// bound, PostBind is the extension point that it should register.
-	PostBind(ctx context.Context, sub *appsapi.Subscription, targetClusters TargetClusters)
+	PostBind(ctx context.Context, sub *appsapi.Subscription, namespacedClusters []string)
 }
 
 // PermitPlugin is an interface that must be implemented by "Permit" plugins.
@@ -259,7 +259,7 @@ type PermitPlugin interface {
 	// The subscription will also be rejected if the wait timeout or the subscription is rejected while
 	// waiting. Note that if the plugin returns "wait", the framework will wait only
 	// after running the remaining plugins given that no other plugin rejects the subscription.
-	Permit(ctx context.Context, sub *appsapi.Subscription, targetClusters TargetClusters) (*Status, time.Duration)
+	Permit(ctx context.Context, sub *appsapi.Subscription, namespacedClusters []string) (*Status, time.Duration)
 }
 
 // BindPlugin is an interface that must be implemented by "Bind" plugins. Bind
@@ -273,7 +273,7 @@ type BindPlugin interface {
 	// remaining bind plugins are skipped. When a bind plugin does not handle a Subscription,
 	// it must return Skip in its Status code. If a bind plugin returns an Error, the
 	// subscription is rejected and will not be bound.
-	Bind(ctx context.Context, sub *appsapi.Subscription, targetClusters TargetClusters) *Status
+	Bind(ctx context.Context, sub *appsapi.Subscription, namespacedClusters []string) *Status
 }
 
 // Framework manages the set of plugins in use by the scheduling framework.
@@ -297,11 +297,11 @@ type Framework interface {
 	// configured Reserve plugins. If any of these calls returns an error, it
 	// does not continue running the remaining ones and returns the error. In
 	// such case, subscription will not be scheduled.
-	RunReservePluginsReserve(ctx context.Context, sub *appsapi.Subscription, targetClusters TargetClusters) *Status
+	RunReservePluginsReserve(ctx context.Context, sub *appsapi.Subscription, namespacedClusters []string) *Status
 
 	// RunReservePluginsUnreserve runs the Unreserve method of the set of
 	// configured Reserve plugins.
-	RunReservePluginsUnreserve(ctx context.Context, sub *appsapi.Subscription, targetClusters TargetClusters)
+	RunReservePluginsUnreserve(ctx context.Context, sub *appsapi.Subscription, namespacedClusters []string)
 
 	// RunPermitPlugins runs the set of configured Permit plugins. If any of these
 	// plugins returns a status other than "Success" or "Wait", it does not continue
@@ -309,7 +309,7 @@ type Framework interface {
 	// plugins returns "Wait", then this function will create and add waiting subscription
 	// to a map of currently waiting subscriptions and return status with "Wait" code.
 	// Subscription will remain waiting subscription for the minimum duration returned by the Permit plugins.
-	RunPermitPlugins(ctx context.Context, sub *appsapi.Subscription, targetClusters TargetClusters) *Status
+	RunPermitPlugins(ctx context.Context, sub *appsapi.Subscription, namespacedClusters []string) *Status
 
 	// WaitOnPermit will block, if the subscription is a waiting subscription, until the waiting subscription is rejected or allowed.
 	WaitOnPermit(ctx context.Context, sub *appsapi.Subscription) *Status
@@ -319,17 +319,17 @@ type Framework interface {
 	// anything but Success. If the Status code is Unschedulable, it is
 	// considered as a scheduling check failure, otherwise, it is considered as an
 	// internal error. In either case the subscription is not going to be bound.
-	RunPreBindPlugins(ctx context.Context, sub *appsapi.Subscription, targetClusters TargetClusters) *Status
+	RunPreBindPlugins(ctx context.Context, sub *appsapi.Subscription, namespacedClusters []string) *Status
 
 	// RunPostBindPlugins runs the set of configured PostBind plugins.
-	RunPostBindPlugins(ctx context.Context, sub *appsapi.Subscription, targetClusters TargetClusters)
+	RunPostBindPlugins(ctx context.Context, sub *appsapi.Subscription, namespacedClusters []string)
 
 	// RunBindPlugins runs the set of configured Bind plugins. A Bind plugin may choose
 	// whether or not to handle the given subscription. If a Bind plugin chooses to skip the
 	// binding, it should return code=5("skip") status. Otherwise, it should return "Error"
 	// or "Success". If none of the plugins handled binding, RunBindPlugins returns
 	// code=5("skip") status.
-	RunBindPlugins(ctx context.Context, sub *appsapi.Subscription, targetClusters TargetClusters) *Status
+	RunBindPlugins(ctx context.Context, sub *appsapi.Subscription, namespacedClusters []string) *Status
 
 	// HasFilterPlugins returns true if at least one Filter plugin is defined.
 	HasFilterPlugins() bool
@@ -380,7 +380,7 @@ type Handle interface {
 
 // PostFilterResult wraps needed info for scheduler framework to act upon PostFilter phase.
 type PostFilterResult struct {
-	NominatedtargetClusters TargetClusters
+	NominatedNamespacedClusters []string
 }
 
 // PluginsRunner abstracts operations to run some plugins.
diff --git a/pkg/scheduler/framework/interfaces/types.go b/pkg/scheduler/framework/interfaces/types.go
index d4bca97..cf083f9 100644
--- a/pkg/scheduler/framework/interfaces/types.go
+++ b/pkg/scheduler/framework/interfaces/types.go
@@ -39,32 +39,6 @@ type FitError struct {
 	Diagnosis      Diagnosis
 }
 
-// TargetClusters represents the scheduling result.
-type TargetClusters struct {
-	// Namespaced names of targeted clusters that Subscription binds to.
-	BindingClusters []string
-
-	// Desired replicas of targeted clusters for each feed.
-	Replicas map[string][]int32
-}
-
-func (t TargetClusters) Len() int {
-	return len(t.BindingClusters)
-}
-
-func (t TargetClusters) Less(i, j int) bool {
-	return t.BindingClusters[i] < t.BindingClusters[j]
-}
-
-func (t TargetClusters) Swap(i, j int) {
-	t.BindingClusters[i], t.BindingClusters[j] = t.BindingClusters[j], t.BindingClusters[i]
-	for _, replicas := range t.Replicas {
-		if len(replicas) == len(t.BindingClusters) {
-			replicas[i], replicas[j] = replicas[j], replicas[i]
-		}
-	}
-}
-
 const (
 	// NoClusterAvailableMsg is used to format message when no clusters available.
 	NoClusterAvailableMsg = "0/%v clusters are available"
diff --git a/pkg/scheduler/framework/plugins/defaultbinder/default_binder.go b/pkg/scheduler/framework/plugins/defaultbinder/default_binder.go
index 021801f..3332fff 100644
--- a/pkg/scheduler/framework/plugins/defaultbinder/default_binder.go
+++ b/pkg/scheduler/framework/plugins/defaultbinder/default_binder.go
@@ -48,18 +48,19 @@ func (pl *DefaultBinder) Name() string {
 }
 
 // Bind binds subscriptions to clusters using the clusternet client.
-func (pl *DefaultBinder) Bind(ctx context.Context, sub *appsapi.Subscription, targetClusters framework.TargetClusters) *framework.Status {
+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", targetClusters.BindingClusters)
+		"subscription", klog.KObj(sub), "clusters", namespacedClusters)
 
 	// use an ordered list
-	sort.Stable(targetClusters)
+	sort.SliceStable(namespacedClusters, func(i, j int) bool {
+		return namespacedClusters[i] < namespacedClusters[j]
+	})
 
 	subCopy := sub.DeepCopy()
-	subCopy.Status.BindingClusters = targetClusters.BindingClusters
-	subCopy.Status.Replicas = targetClusters.Replicas
+	subCopy.Status.BindingClusters = namespacedClusters
 	subCopy.Status.SpecHash = utils.HashSubscriptionSpec(&subCopy.Spec)
-	subCopy.Status.DesiredReleases = len(targetClusters.BindingClusters)
+	subCopy.Status.DesiredReleases = len(namespacedClusters)
 
 	_, err := pl.handle.ClientSet().AppsV1alpha1().Subscriptions(sub.Namespace).UpdateStatus(ctx, subCopy, metav1.UpdateOptions{})
 	if err != nil {
diff --git a/pkg/scheduler/framework/runtime/framework.go b/pkg/scheduler/framework/runtime/framework.go
index 7dc077c..f686afa 100644
--- a/pkg/scheduler/framework/runtime/framework.go
+++ b/pkg/scheduler/framework/runtime/framework.go
@@ -476,13 +476,13 @@ func (f *frameworkImpl) runScoreExtension(ctx context.Context, pl framework.Scor
 // RunPreBindPlugins runs the set of configured prebind plugins. It returns a
 // failure (bool) if any of the plugins returns an error. It also returns an
 // error containing the rejection message or the error occurred in the plugin.
-func (f *frameworkImpl) RunPreBindPlugins(ctx context.Context, sub *appsapi.Subscription, targetClusters framework.TargetClusters) (status *framework.Status) {
+func (f *frameworkImpl) RunPreBindPlugins(ctx context.Context, sub *appsapi.Subscription, namespacedClusters []string) (status *framework.Status) {
 	startTime := time.Now()
 	defer func() {
 		metrics.FrameworkExtensionPointDuration.WithLabelValues(preBind, status.Code().String(), f.profileName).Observe(metrics.SinceInSeconds(startTime))
 	}()
 	for _, pl := range f.preBindPlugins {
-		status = f.runPreBindPlugin(ctx, pl, sub, targetClusters)
+		status = f.runPreBindPlugin(ctx, pl, sub, namespacedClusters)
 		if !status.IsSuccess() {
 			err := status.AsError()
 			klog.ErrorS(err, "Failed running PreBind plugin", "plugin", pl.Name(), "sub", klog.KObj(sub))
@@ -492,15 +492,15 @@ func (f *frameworkImpl) RunPreBindPlugins(ctx context.Context, sub *appsapi.Subs
 	return nil
 }
 
-func (f *frameworkImpl) runPreBindPlugin(ctx context.Context, pl framework.PreBindPlugin, sub *appsapi.Subscription, targetClusters framework.TargetClusters) *framework.Status {
+func (f *frameworkImpl) runPreBindPlugin(ctx context.Context, pl framework.PreBindPlugin, sub *appsapi.Subscription, namespacedClusters []string) *framework.Status {
 	startTime := time.Now()
-	status := pl.PreBind(ctx, sub, targetClusters)
+	status := pl.PreBind(ctx, sub, namespacedClusters)
 	f.metricsRecorder.observePluginDurationAsync(preBind, pl.Name(), status, metrics.SinceInSeconds(startTime))
 	return status
 }
 
 // RunBindPlugins runs the set of configured bind plugins until one returns a non `Skip` status.
-func (f *frameworkImpl) RunBindPlugins(ctx context.Context, sub *appsapi.Subscription, targetClusters framework.TargetClusters) (status *framework.Status) {
+func (f *frameworkImpl) RunBindPlugins(ctx context.Context, sub *appsapi.Subscription, namespacedClusters []string) (status *framework.Status) {
 	startTime := time.Now()
 	defer func() {
 		metrics.FrameworkExtensionPointDuration.WithLabelValues(bind, status.Code().String(), f.profileName).Observe(metrics.SinceInSeconds(startTime))
@@ -509,7 +509,7 @@ func (f *frameworkImpl) RunBindPlugins(ctx context.Context, sub *appsapi.Subscri
 		return framework.NewStatus(framework.Skip, "")
 	}
 	for _, bp := range f.bindPlugins {
-		status = f.runBindPlugin(ctx, bp, sub, targetClusters)
+		status = f.runBindPlugin(ctx, bp, sub, namespacedClusters)
 		if status != nil && status.Code() == framework.Skip {
 			continue
 		}
@@ -523,27 +523,27 @@ func (f *frameworkImpl) RunBindPlugins(ctx context.Context, sub *appsapi.Subscri
 	return status
 }
 
-func (f *frameworkImpl) runBindPlugin(ctx context.Context, bp framework.BindPlugin, sub *appsapi.Subscription, targetClusters framework.TargetClusters) *framework.Status {
+func (f *frameworkImpl) runBindPlugin(ctx context.Context, bp framework.BindPlugin, sub *appsapi.Subscription, namespacedClusters []string) *framework.Status {
 	startTime := time.Now()
-	status := bp.Bind(ctx, sub, targetClusters)
+	status := bp.Bind(ctx, sub, namespacedClusters)
 	f.metricsRecorder.observePluginDurationAsync(bind, bp.Name(), status, metrics.SinceInSeconds(startTime))
 	return status
 }
 
 // RunPostBindPlugins runs the set of configured postbind plugins.
-func (f *frameworkImpl) RunPostBindPlugins(ctx context.Context, sub *appsapi.Subscription, targetClusters framework.TargetClusters) {
+func (f *frameworkImpl) RunPostBindPlugins(ctx context.Context, sub *appsapi.Subscription, namespacedClusters []string) {
 	startTime := time.Now()
 	defer func() {
 		metrics.FrameworkExtensionPointDuration.WithLabelValues(postBind, framework.Success.String(), f.profileName).Observe(metrics.SinceInSeconds(startTime))
 	}()
 	for _, pl := range f.postBindPlugins {
-		f.runPostBindPlugin(ctx, pl, sub, targetClusters)
+		f.runPostBindPlugin(ctx, pl, sub, namespacedClusters)
 	}
 }
 
-func (f *frameworkImpl) runPostBindPlugin(ctx context.Context, pl framework.PostBindPlugin, sub *appsapi.Subscription, targetClusters framework.TargetClusters) {
+func (f *frameworkImpl) runPostBindPlugin(ctx context.Context, pl framework.PostBindPlugin, sub *appsapi.Subscription, namespacedClusters []string) {
 	startTime := time.Now()
-	pl.PostBind(ctx, sub, targetClusters)
+	pl.PostBind(ctx, sub, namespacedClusters)
 	f.metricsRecorder.observePluginDurationAsync(postBind, pl.Name(), nil, metrics.SinceInSeconds(startTime))
 }
 
@@ -552,13 +552,13 @@ func (f *frameworkImpl) runPostBindPlugin(ctx context.Context, pl framework.Post
 // continue running the remaining ones and returns the error. In such a case,
 // the subscription will not be scheduled and the caller will be expected to call
 // RunReservePluginsUnreserve.
-func (f *frameworkImpl) RunReservePluginsReserve(ctx context.Context, sub *appsapi.Subscription, targetClusters framework.TargetClusters) (status *framework.Status) {
+func (f *frameworkImpl) RunReservePluginsReserve(ctx context.Context, sub *appsapi.Subscription, namespacedClusters []string) (status *framework.Status) {
 	startTime := time.Now()
 	defer func() {
 		metrics.FrameworkExtensionPointDuration.WithLabelValues(reserve, status.Code().String(), f.profileName).Observe(metrics.SinceInSeconds(startTime))
 	}()
 	for _, pl := range f.reservePlugins {
-		status = f.runReservePluginReserve(ctx, pl, sub, targetClusters)
+		status = f.runReservePluginReserve(ctx, pl, sub, namespacedClusters)
 		if !status.IsSuccess() {
 			err := status.AsError()
 			klog.ErrorS(err, "Failed running Reserve plugin", "plugin", pl.Name(), "subscription", klog.KObj(sub))
@@ -568,16 +568,16 @@ func (f *frameworkImpl) RunReservePluginsReserve(ctx context.Context, sub *appsa
 	return nil
 }
 
-func (f *frameworkImpl) runReservePluginReserve(ctx context.Context, pl framework.ReservePlugin, sub *appsapi.Subscription, targetClusters framework.TargetClusters) *framework.Status {
+func (f *frameworkImpl) runReservePluginReserve(ctx context.Context, pl framework.ReservePlugin, sub *appsapi.Subscription, namespacedClusters []string) *framework.Status {
 	startTime := time.Now()
-	status := pl.Reserve(ctx, sub, targetClusters)
+	status := pl.Reserve(ctx, sub, namespacedClusters)
 	f.metricsRecorder.observePluginDurationAsync(reserve, pl.Name(), status, metrics.SinceInSeconds(startTime))
 	return status
 }
 
 // RunReservePluginsUnreserve runs the Unreserve method in the set of
 // configured reserve plugins.
-func (f *frameworkImpl) RunReservePluginsUnreserve(ctx context.Context, sub *appsapi.Subscription, targetClusters framework.TargetClusters) {
+func (f *frameworkImpl) RunReservePluginsUnreserve(ctx context.Context, sub *appsapi.Subscription, namespacedClusters []string) {
 	startTime := time.Now()
 	defer func() {
 		metrics.FrameworkExtensionPointDuration.WithLabelValues(unreserve, framework.Success.String(), f.profileName).Observe(metrics.SinceInSeconds(startTime))
@@ -585,13 +585,13 @@ func (f *frameworkImpl) RunReservePluginsUnreserve(ctx context.Context, sub *app
 	// Execute the Unreserve operation of each reserve plugin in the
 	// *reverse* order in which the Reserve operation was executed.
 	for i := len(f.reservePlugins) - 1; i >= 0; i-- {
-		f.runReservePluginUnreserve(ctx, f.reservePlugins[i], sub, targetClusters)
+		f.runReservePluginUnreserve(ctx, f.reservePlugins[i], sub, namespacedClusters)
 	}
 }
 
-func (f *frameworkImpl) runReservePluginUnreserve(ctx context.Context, pl framework.ReservePlugin, sub *appsapi.Subscription, targetClusters framework.TargetClusters) {
+func (f *frameworkImpl) runReservePluginUnreserve(ctx context.Context, pl framework.ReservePlugin, sub *appsapi.Subscription, namespacedClusters []string) {
 	startTime := time.Now()
-	pl.Unreserve(ctx, sub, targetClusters)
+	pl.Unreserve(ctx, sub, namespacedClusters)
 	f.metricsRecorder.observePluginDurationAsync(unreserve, pl.Name(), nil, metrics.SinceInSeconds(startTime))
 }
 
@@ -601,7 +601,7 @@ func (f *frameworkImpl) runReservePluginUnreserve(ctx context.Context, pl framew
 // plugins returns "Wait", then this function will create and add waiting subscription
 // to a map of currently waiting subs and return status with "Wait" code.
 // Subscription will remain waiting subscription for the minimum duration returned by the permit plugins.
-func (f *frameworkImpl) RunPermitPlugins(ctx context.Context, sub *appsapi.Subscription, targetClusters framework.TargetClusters) (status *framework.Status) {
+func (f *frameworkImpl) RunPermitPlugins(ctx context.Context, sub *appsapi.Subscription, namespacedClusters []string) (status *framework.Status) {
 	startTime := time.Now()
 	defer func() {
 		metrics.FrameworkExtensionPointDuration.WithLabelValues(permit, status.Code().String(), f.profileName).Observe(metrics.SinceInSeconds(startTime))
@@ -609,7 +609,7 @@ func (f *frameworkImpl) RunPermitPlugins(ctx context.Context, sub *appsapi.Subsc
 	pluginsWaitTime := make(map[string]time.Duration)
 	statusCode := framework.Success
 	for _, pl := range f.permitPlugins {
-		status, timeout := f.runPermitPlugin(ctx, pl, sub, targetClusters)
+		status, timeout := f.runPermitPlugin(ctx, pl, sub, namespacedClusters)
 		if !status.IsSuccess() {
 			if status.IsUnschedulable() {
 				msg := fmt.Sprintf("rejected subscription %q by permit plugin %q: %v", sub.Name, pl.Name(), status.Message())
@@ -641,9 +641,9 @@ func (f *frameworkImpl) RunPermitPlugins(ctx context.Context, sub *appsapi.Subsc
 	return nil
 }
 
-func (f *frameworkImpl) runPermitPlugin(ctx context.Context, pl framework.PermitPlugin, sub *appsapi.Subscription, targetClusters framework.TargetClusters) (*framework.Status, time.Duration) {
+func (f *frameworkImpl) runPermitPlugin(ctx context.Context, pl framework.PermitPlugin, sub *appsapi.Subscription, namespacedClusters []string) (*framework.Status, time.Duration) {
 	startTime := time.Now()
-	status, timeout := pl.Permit(ctx, sub, targetClusters)
+	status, timeout := pl.Permit(ctx, sub, namespacedClusters)
 	f.metricsRecorder.observePluginDurationAsync(permit, pl.Name(), status, metrics.SinceInSeconds(startTime))
 	return status, timeout
 }
diff --git a/pkg/scheduler/scheduler.go b/pkg/scheduler/scheduler.go
index 387d90b..a969031 100644
--- a/pkg/scheduler/scheduler.go
+++ b/pkg/scheduler/scheduler.go
@@ -25,7 +25,6 @@ import (
 	"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"
 	utilruntime "k8s.io/apimachinery/pkg/util/runtime"
@@ -78,9 +77,6 @@ type Scheduler struct {
 	subsLister applisters.SubscriptionLister
 	subsSynced cache.InformerSynced
 
-	inventoryLister applisters.FeedInventoryLister
-	inventorySynced cache.InformerSynced
-
 	// default in-tree registry
 	registry frameworkruntime.Registry
 
@@ -130,8 +126,6 @@ func NewScheduler(schedulerOptions *options.SchedulerOptions) (*Scheduler, error
 		ClusternetInformerFactory: clusternetInformerFactory,
 		subsLister:                clusternetInformerFactory.Apps().V1alpha1().Subscriptions().Lister(),
 		subsSynced:                clusternetInformerFactory.Apps().V1alpha1().Subscriptions().Informer().HasSynced,
-		inventoryLister:           clusternetInformerFactory.Apps().V1alpha1().FeedInventories().Lister(),
-		inventorySynced:           clusternetInformerFactory.Apps().V1alpha1().FeedInventories().Informer().HasSynced,
 		registry:                  plugins.NewInTreeRegistry(),
 		scheduleAlgorithm:         algorithm.NewGenericScheduler(schedulerCache),
 		SchedulingQueue:           workqueue.NewRateLimitingQueue(workqueue.DefaultItemBasedRateLimiter()),
@@ -237,26 +231,13 @@ func (sched *Scheduler) scheduleOne(ctx context.Context) {
 	}
 	klog.V(3).InfoS("Attempting to schedule subscription", "subscription", klog.KObj(sub))
 
-	var finv *appsapi.FeedInventory
-	if sub.Spec.SchedulingStrategy == appsapi.DividingSchedulingStrategyType {
-		finv, err = sched.inventoryLister.FeedInventories(ns).Get(name)
-		// TODO(Garrybest): Ignore some circumstances when subscription does not need to be scheduled so far.
-		// E.g., when feed in subscription has been modified but finv has not been updated.
-		if err != nil {
-			if !errors.IsNotFound(err) {
-				utilruntime.HandleError(err)
-			}
-			return
-		}
-	}
-
 	// Synchronously attempt to find a fit for the subscription.
 	start := time.Now()
 
 	schedulingCycleCtx, cancel := context.WithCancel(ctx)
 	defer cancel()
 
-	scheduleResult, err := sched.scheduleAlgorithm.Schedule(schedulingCycleCtx, sched.framework, sub, finv)
+	scheduleResult, err := sched.scheduleAlgorithm.Schedule(schedulingCycleCtx, sched.framework, sub)
 	if err != nil {
 		sched.recordSchedulingFailure(sub, err, ReasonUnschedulable)
 		return
@@ -264,17 +245,20 @@ func (sched *Scheduler) scheduleOne(ctx context.Context) {
 	metrics.SchedulingAlgorithmLatency.Observe(metrics.SinceInSeconds(start))
 
 	// Run the Reserve method of reserve plugins.
-	targetClusters := scheduleResult.SuggestedClusters
-	if sts := sched.framework.RunReservePluginsReserve(schedulingCycleCtx, sub, targetClusters); !sts.IsSuccess() {
+	var namespacedClusters []string
+	for _, cluster := range scheduleResult.SuggestedClusters {
+		namespacedClusters = append(namespacedClusters, cluster)
+	}
+	if sts := sched.framework.RunReservePluginsReserve(schedulingCycleCtx, sub, namespacedClusters); !sts.IsSuccess() {
 		metrics.SubscriptionScheduleError(sched.framework.ProfileName(), metrics.SinceInSeconds(start))
 		// trigger un-reserve to clean up state associated with the reserved subscription
-		sched.framework.RunReservePluginsUnreserve(schedulingCycleCtx, sub, targetClusters)
+		sched.framework.RunReservePluginsUnreserve(schedulingCycleCtx, sub, namespacedClusters)
 		sched.recordSchedulingFailure(sub, sts.AsError(), SchedulerError)
 		return
 	}
 
 	// Run "permit" plugins.
-	runPermitStatus := sched.framework.RunPermitPlugins(schedulingCycleCtx, sub, targetClusters)
+	runPermitStatus := sched.framework.RunPermitPlugins(schedulingCycleCtx, sub, namespacedClusters)
 	if runPermitStatus.Code() != framework.Wait && !runPermitStatus.IsSuccess() {
 		var reason string
 		if runPermitStatus.IsUnschedulable() {
@@ -285,7 +269,7 @@ func (sched *Scheduler) scheduleOne(ctx context.Context) {
 			reason = SchedulerError
 		}
 		// One of the plugins returned status different from success or wait.
-		sched.framework.RunReservePluginsUnreserve(schedulingCycleCtx, sub, targetClusters)
+		sched.framework.RunReservePluginsUnreserve(schedulingCycleCtx, sub, namespacedClusters)
 		sched.recordSchedulingFailure(sub, runPermitStatus.AsError(), reason)
 		return
 	}
@@ -308,39 +292,39 @@ func (sched *Scheduler) scheduleOne(ctx context.Context) {
 				reason = SchedulerError
 			}
 			// trigger un-reserve plugins to clean up state associated with the reserved subscription
-			sched.framework.RunReservePluginsUnreserve(bindingCycleCtx, sub, targetClusters)
+			sched.framework.RunReservePluginsUnreserve(bindingCycleCtx, sub, namespacedClusters)
 			sched.recordSchedulingFailure(sub, waitOnPermitStatus.AsError(), reason)
 			return
 		}
 
 		// Run "prebind" plugins.
-		preBindStatus := sched.framework.RunPreBindPlugins(bindingCycleCtx, sub, targetClusters)
+		preBindStatus := sched.framework.RunPreBindPlugins(bindingCycleCtx, sub, namespacedClusters)
 		if !preBindStatus.IsSuccess() {
 			metrics.SubscriptionScheduleError(sched.framework.ProfileName(), metrics.SinceInSeconds(start))
 			// trigger un-reserve plugins to clean up state associated with the reserved subscription
-			sched.framework.RunReservePluginsUnreserve(bindingCycleCtx, sub, targetClusters)
+			sched.framework.RunReservePluginsUnreserve(bindingCycleCtx, sub, namespacedClusters)
 			sched.recordSchedulingFailure(sub, preBindStatus.AsError(), SchedulerError)
 			return
 		}
 
-		err := sched.bind(bindingCycleCtx, sub, targetClusters)
+		err := sched.bind(bindingCycleCtx, sub, namespacedClusters)
 		if err != nil {
 			metrics.SubscriptionScheduleError(sched.framework.ProfileName(), metrics.SinceInSeconds(start))
 			// trigger un-reserve plugins to clean up state associated with the reserved subscription
-			sched.framework.RunReservePluginsUnreserve(bindingCycleCtx, sub, targetClusters)
+			sched.framework.RunReservePluginsUnreserve(bindingCycleCtx, sub, namespacedClusters)
 			sched.recordSchedulingFailure(sub, fmt.Errorf("binding rejected: %w", err), SchedulerError)
 		} else {
 			metrics.SubscriptionScheduled(sched.framework.ProfileName(), metrics.SinceInSeconds(start))
 
 			// Run "postbind" plugins.
-			sched.framework.RunPostBindPlugins(bindingCycleCtx, sub, targetClusters)
+			sched.framework.RunPostBindPlugins(bindingCycleCtx, sub, namespacedClusters)
 		}
 	}()
 }
 
 // bind a subscription to given clusters.
 // We expect this to run asynchronously, so we handle binding metrics internally.
-func (sched *Scheduler) bind(ctx context.Context, sub *appsapi.Subscription, targetClusters framework.TargetClusters) (err error) {
+func (sched *Scheduler) bind(ctx context.Context, sub *appsapi.Subscription, namespacedClusters []string) (err error) {
 	defer func() {
 		// finish binding
 		if err != nil {
@@ -352,11 +336,11 @@ func (sched *Scheduler) bind(ctx context.Context, sub *appsapi.Subscription, tar
 			corev1.EventTypeNormal,
 			"Scheduled",
 			"Successfully bound %s to %s",
-			klog.KObj(sub), strings.Join(targetClusters.BindingClusters, ","),
+			klog.KObj(sub), strings.Join(namespacedClusters, ","),
 		)
 	}()
 
-	bindStatus := sched.framework.RunBindPlugins(ctx, sub, targetClusters)
+	bindStatus := sched.framework.RunBindPlugins(ctx, sub, namespacedClusters)
 	if bindStatus.IsSuccess() {
 		return nil
 	}
@@ -434,22 +418,6 @@ func (sched *Scheduler) addAllEventHandlers() {
 		},
 	})
 
-	sched.ClusternetInformerFactory.Apps().V1alpha1().FeedInventories().Informer().AddEventHandler(cache.ResourceEventHandlerFuncs{
-		AddFunc: func(obj interface{}) {
-			finv := obj.(*appsapi.FeedInventory)
-			sched.SchedulingQueue.AddRateLimited(klog.KObj(finv).String())
-		},
-		UpdateFunc: func(oldObj, newObj interface{}) {
-			oldInventory := oldObj.(*appsapi.FeedInventory)
-			newInventory := newObj.(*appsapi.FeedInventory)
-			if newInventory.ResourceVersion == oldInventory.ResourceVersion {
-				// Periodic resync will send update events for all known Inventory.
-				return
-			}
-			sched.SchedulingQueue.AddRateLimited(klog.KObj(newInventory).String())
-		},
-	})
-
 	enqueueSubscriptionForClusterFunc := func(mcls *clusterapi.ManagedCluster) {
 		sched.lock.RLock()
 		defer sched.lock.RUnlock()
diff --git a/pkg/utils/feed.go b/pkg/utils/feed.go
index 43e9fd6..fec278c 100644
--- a/pkg/utils/feed.go
+++ b/pkg/utils/feed.go
@@ -174,11 +174,3 @@ func HashSubscriptionSpec(subscriptionSpec *appsapi.SubscriptionSpec) uint64 {
 	printer.Fprintf(hasher, "%#v", specJSON)
 	return uint64(hasher.Sum32())
 }
-
-func GetFeedKey(feed *appsapi.Feed) string {
-	if len(feed.Namespace) != 0 {
-		return fmt.Sprintf("%s/%s/%s/%s", feed.APIVersion, feed.Kind, feed.Namespace, feed.Name)
-	} else {
-		return fmt.Sprintf("%s/%s/%s", feed.APIVersion, feed.Kind, feed.Name)
-	}
-}


Test patch:
diff --git a/pkg/controllers/apps/feedinventory/deployment/deployment_test.go b/pkg/controllers/apps/feedinventory/deployment/deployment_test.go
new file mode 100644
index 0000000..adb534d
--- /dev/null
+++ b/pkg/controllers/apps/feedinventory/deployment/deployment_test.go
@@ -0,0 +1,588 @@
+/*
+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 deployment
+
+import (
+	"reflect"
+	"testing"
+
+	corev1 "k8s.io/api/core/v1"
+	"k8s.io/apimachinery/pkg/api/resource"
+
+	appsapi "github.com/clusternet/clusternet/pkg/apis/apps/v1alpha1"
+)
+
+func TestPluginParser(t *testing.T) {
+	tests := []struct {
+		name string
+
+		rawData      []byte
+		inputLimit   map[string]string
+		inputRequest map[string]string
+
+		replicas        int32
+		requirements    appsapi.ReplicaRequirements
+		replicaJsonPath string
+	}{
+		{
+			name: "empty requirements",
+			rawData: []byte(`{
+  "apiVersion": "apps/v1",
+  "kind": "Deployment",
+  "metadata": {
+    "name": "rss-site",
+    "labels": {
+      "app": "web"
+    }
+  },
+  "spec": {
+    "replicas": 2,
+    "selector": {
+      "matchLabels": {
+        "app": "web"
+      }
+    },
+    "template": {
+      "metadata": {
+        "labels": {
+          "app": "web"
+        }
+      },
+      "spec": {
+        "containers": [
+          {
+            "name": "front-end",
+            "image": "nginx"
+          },
+          {
+            "name": "rss-reader",
+            "image": "rss-php-nginx:v1"
+          }
+        ]
+      }
+    }
+  }
+}`),
+			replicas: 2,
+			requirements: appsapi.ReplicaRequirements{
+				Resources: corev1.ResourceRequirements{
+					Limits:   map[corev1.ResourceName]resource.Quantity{},
+					Requests: map[corev1.ResourceName]resource.Quantity{},
+				},
+			},
+			replicaJsonPath: "/spec/replicas",
+		},
+
+		{
+			name: "multiple-qos-with-foo-resource",
+			rawData: []byte(`{
+  "apiVersion": "apps/v1",
+  "kind": "Deployment",
+  "metadata": {
+    "name": "qos-demo",
+    "labels": {
+      "app": "qos-demo"
+    }
+  },
+  "spec": {
+    "replicas": 2,
+    "selector": {
+      "matchLabels": {
+        "app": "qos-demo"
+      }
+    },
+    "template": {
+      "metadata": {
+        "labels": {
+          "app": "qos-demo"
+        }
+      },
+      "spec": {
+        "nodeSelector": {
+          "disktype": "ssd"
+        },
+        "tolerations": [
+          {
+            "key": "example-key",
+            "operator": "Exists",
+            "effect": "NoSchedule"
+          }
+        ],
+        "containers": [
+          {
+            "name": "best-effort",
+            "image": "nginx"
+          },
+          {
+            "name": "burstable-with-foo-resource",
+            "image": "nginx",
+            "resources": {
+              "requests": {
+                "memory": "128Mi",
+                "foo": 2
+              },
+              "limits": {
+                "memory": "256Mi",
+                "foo": 3
+              }
+            }
+          },
+          {
+            "name": "guaranteed",
+            "image": "nginx",
+            "resources": {
+              "requests": {
+                "memory": "128Mi",
+                "cpu": "500m"
+              },
+              "limits": {
+                "memory": "128Mi",
+                "cpu": "500m"
+              }
+            }
+          }
+        ],
+        "affinity": {
+          "nodeAffinity": {
+            "requiredDuringSchedulingIgnoredDuringExecution": {
+              "nodeSelectorTerms": [
+                {
+                  "matchExpressions": [
+                    {
+                      "key": "kubernetes.io/os",
+                      "operator": "In",
+                      "values": [
+                        "linux"
+                      ]
+                    }
+                  ]
+                }
+              ]
+            }
+          }
+        }
+      }
+    }
+  }
+}`),
+			inputRequest: map[string]string{
+				"cpu":    "500m",
+				"memory": "256Mi",
+				"foo":    "2",
+			},
+			inputLimit: map[string]string{
+				"cpu":    "500m",
+				"memory": "384Mi",
+				"foo":    "3",
+			},
+			replicas: 2,
+			requirements: appsapi.ReplicaRequirements{
+				NodeSelector: map[string]string{
+					"disktype": "ssd",
+				},
+				Tolerations: []corev1.Toleration{
+					{
+						Key:      "example-key",
+						Operator: corev1.TolerationOpExists,
+						Effect:   corev1.TaintEffectNoSchedule,
+					},
+				},
+				Affinity: &corev1.Affinity{
+					NodeAffinity: &corev1.NodeAffinity{
+						RequiredDuringSchedulingIgnoredDuringExecution: &corev1.NodeSelector{
+							NodeSelectorTerms: []corev1.NodeSelectorTerm{
+								{
+									MatchExpressions: []corev1.NodeSelectorRequirement{
+										{
+											Key:      "kubernetes.io/os",
+											Operator: corev1.NodeSelectorOpIn,
+											Values:   []string{"linux"},
+										},
+									},
+								},
+							},
+						},
+					},
+				},
+				Resources: corev1.ResourceRequirements{
+					Limits:   map[corev1.ResourceName]resource.Quantity{},
+					Requests: map[corev1.ResourceName]resource.Quantity{},
+				},
+			},
+			replicaJsonPath: "/spec/replicas",
+		},
+
+		{
+			name: "init-containers-small-quota",
+			rawData: []byte(`{
+  "apiVersion": "apps/v1",
+  "kind": "Deployment",
+  "metadata": {
+    "name": "qos-demo",
+    "labels": {
+      "app": "qos-demo"
+    }
+  },
+  "spec": {
+    "replicas": 2,
+    "selector": {
+      "matchLabels": {
+        "app": "qos-demo"
+      }
+    },
+    "template": {
+      "metadata": {
+        "labels": {
+          "app": "qos-demo"
+        }
+      },
+      "spec": {
+        "nodeSelector": {
+          "disktype": "ssd"
+        },
+        "tolerations": [
+          {
+            "key": "example-key",
+            "operator": "Exists",
+            "effect": "NoSchedule"
+          }
+        ],
+        "initContainers": [
+          {
+            "name": "init-container-1",
+            "image": "image-test:tag1"
+          },
+          {
+            "name": "init-container-2",
+            "image": "image-test:tag2",
+            "resources": {
+              "requests": {
+                "memory": "128Mi",
+                "bar": 1
+              },
+              "limits": {
+                "memory": "256Mi",
+                "bar": 2
+              }
+            }
+          }
+        ],
+        "containers": [
+          {
+            "name": "best-effort",
+            "image": "nginx"
+          },
+          {
+            "name": "burstable-with-foo-resource",
+            "image": "nginx",
+            "resources": {
+              "requests": {
+                "memory": "128Mi",
+                "foo": 2
+              },
+              "limits": {
+                "memory": "256Mi",
+                "foo": 3
+              }
+            }
+          },
+          {
+            "name": "guaranteed",
+            "image": "nginx",
+            "resources": {
+              "requests": {
+                "memory": "128Mi",
+                "cpu": "500m"
+              },
+              "limits": {
+                "memory": "128Mi",
+                "cpu": "500m"
+              }
+            }
+          }
+        ],
+        "affinity": {
+          "nodeAffinity": {
+            "requiredDuringSchedulingIgnoredDuringExecution": {
+              "nodeSelectorTerms": [
+                {
+                  "matchExpressions": [
+                    {
+                      "key": "kubernetes.io/os",
+                      "operator": "In",
+                      "values": [
+                        "linux"
+                      ]
+                    }
+                  ]
+                }
+              ]
+            }
+          }
+        }
+      }
+    }
+  }
+}`),
+			inputRequest: map[string]string{
+				"cpu":    "500m",
+				"memory": "256Mi",
+				"foo":    "2",
+				"bar":    "1",
+			},
+			inputLimit: map[string]string{
+				"cpu":    "500m",
+				"memory": "384Mi",
+				"foo":    "3",
+				"bar":    "2",
+			},
+			replicas: 2,
+			requirements: appsapi.ReplicaRequirements{
+				NodeSelector: map[string]string{
+					"disktype": "ssd",
+				},
+				Tolerations: []corev1.Toleration{
+					{
+						Key:      "example-key",
+						Operator: corev1.TolerationOpExists,
+						Effect:   corev1.TaintEffectNoSchedule,
+					},
+				},
+				Affinity: &corev1.Affinity{
+					NodeAffinity: &corev1.NodeAffinity{
+						RequiredDuringSchedulingIgnoredDuringExecution: &corev1.NodeSelector{
+							NodeSelectorTerms: []corev1.NodeSelectorTerm{
+								{
+									MatchExpressions: []corev1.NodeSelectorRequirement{
+										{
+											Key:      "kubernetes.io/os",
+											Operator: corev1.NodeSelectorOpIn,
+											Values:   []string{"linux"},
+										},
+									},
+								},
+							},
+						},
+					},
+				},
+				Resources: corev1.ResourceRequirements{
+					Limits:   map[corev1.ResourceName]resource.Quantity{},
+					Requests: map[corev1.ResourceName]resource.Quantity{},
+				},
+			},
+			replicaJsonPath: "/spec/replicas",
+		},
+
+		{
+			name: "init-containers-large-quota",
+			rawData: []byte(`{
+  "apiVersion": "apps/v1",
+  "kind": "Deployment",
+  "metadata": {
+    "name": "qos-demo",
+    "labels": {
+      "app": "qos-demo"
+    }
+  },
+  "spec": {
+    "replicas": 2,
+    "selector": {
+      "matchLabels": {
+        "app": "qos-demo"
+      }
+    },
+    "template": {
+      "metadata": {
+        "labels": {
+          "app": "qos-demo"
+        }
+      },
+      "spec": {
+        "nodeSelector": {
+          "disktype": "ssd"
+        },
+        "tolerations": [
+          {
+            "key": "example-key",
+            "operator": "Exists",
+            "effect": "NoSchedule"
+          }
+        ],
+        "initContainers": [
+          {
+            "name": "init-container-1",
+            "image": "image-test:tag1"
+          },
+          {
+            "name": "init-container-2",
+            "image": "image-test:tag2",
+            "resources": {
+              "requests": {
+                "memory": "512Mi",
+                "foo": 3
+              },
+              "limits": {
+                "memory": "512Mi",
+                "foo": 4
+              }
+            }
+          }
+        ],
+        "containers": [
+          {
+            "name": "best-effort",
+            "image": "nginx"
+          },
+          {
+            "name": "burstable-with-foo-resource",
+            "image": "nginx",
+            "resources": {
+              "requests": {
+                "memory": "128Mi",
+                "foo": 2
+              },
+              "limits": {
+                "memory": "256Mi",
+                "foo": 3
+              }
+            }
+          },
+          {
+            "name": "guaranteed",
+            "image": "nginx",
+            "resources": {
+              "requests": {
+                "memory": "128Mi",
+                "cpu": "500m"
+              },
+              "limits": {
+                "memory": "128Mi",
+                "cpu": "500m"
+              }
+            }
+          }
+        ],
+        "affinity": {
+          "nodeAffinity": {
+            "requiredDuringSchedulingIgnoredDuringExecution": {
+              "nodeSelectorTerms": [
+                {
+                  "matchExpressions": [
+                    {
+                      "key": "kubernetes.io/os",
+                      "operator": "In",
+                      "values": [
+                        "linux"
+                      ]
+                    }
+                  ]
+                }
+              ]
+            }
+          }
+        }
+      }
+    }
+  }
+}`),
+			inputRequest: map[string]string{
+				"cpu":    "500m",
+				"memory": "512Mi",
+				"foo":    "3",
+			},
+			inputLimit: map[string]string{
+				"cpu":    "500m",
+				"memory": "512Mi",
+				"foo":    "4",
+			},
+			replicas: 2,
+			requirements: appsapi.ReplicaRequirements{
+				NodeSelector: map[string]string{
+					"disktype": "ssd",
+				},
+				Tolerations: []corev1.Toleration{
+					{
+						Key:      "example-key",
+						Operator: corev1.TolerationOpExists,
+						Effect:   corev1.TaintEffectNoSchedule,
+					},
+				},
+				Affinity: &corev1.Affinity{
+					NodeAffinity: &corev1.NodeAffinity{
+						RequiredDuringSchedulingIgnoredDuringExecution: &corev1.NodeSelector{
+							NodeSelectorTerms: []corev1.NodeSelectorTerm{
+								{
+									MatchExpressions: []corev1.NodeSelectorRequirement{
+										{
+											Key:      "kubernetes.io/os",
+											Operator: corev1.NodeSelectorOpIn,
+											Values:   []string{"linux"},
+										},
+									},
+								},
+							},
+						},
+					},
+				},
+				Resources: corev1.ResourceRequirements{
+					Limits:   map[corev1.ResourceName]resource.Quantity{},
+					Requests: map[corev1.ResourceName]resource.Quantity{},
+				},
+			},
+			replicaJsonPath: "/spec/replicas",
+		},
+	}
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			for resourceName, value := range tt.inputRequest {
+				quantity, err := resource.ParseQuantity(value)
+				if err != nil {
+					t.Fatalf("failed to parse quantity %s: %v", value, err)
+				}
+				tt.requirements.Resources.Requests[corev1.ResourceName(resourceName)] = quantity
+			}
+
+			for resourceName, value := range tt.inputLimit {
+				quantity, err := resource.ParseQuantity(value)
+				if err != nil {
+					t.Fatalf("failed to parse quantity %s: %v", value, err)
+				}
+				tt.requirements.Resources.Limits[corev1.ResourceName(resourceName)] = quantity
+			}
+
+			pl := &Plugin{
+				name: tt.name,
+			}
+			replicas, requirements, replicaJsonPath, err := pl.Parser(tt.rawData)
+			if err != nil {
+				t.Fatalf("Unexpected error: %v", err)
+			}
+
+			if *replicas != tt.replicas {
+				t.Errorf("Parser() replicas = %d, replicas %d", *replicas, tt.replicas)
+			}
+
+			if !reflect.DeepEqual(requirements, tt.requirements) {
+				t.Errorf("Parser() requirements = %#v\n, requirements %#v", requirements, tt.requirements)
+			}
+
+			if replicaJsonPath != tt.replicaJsonPath {
+				t.Errorf("Parser() replicaJsonPath = %s, replicaJsonPath %s", replicaJsonPath, tt.replicaJsonPath)
+			}
+		})
+	}
+}
diff --git a/pkg/controllers/apps/feedinventory/feedinventory_test.go b/pkg/controllers/apps/feedinventory/feedinventory_test.go
new file mode 100644
index 0000000..a58bbe9
--- /dev/null
+++ b/pkg/controllers/apps/feedinventory/feedinventory_test.go
@@ -0,0 +1,86 @@
+/*
+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 feedinventory
+
+import (
+	"reflect"
+	"testing"
+
+	"k8s.io/apimachinery/pkg/runtime/schema"
+)
+
+func TestGetGroupVersionKind(t *testing.T) {
+	tests := []struct {
+		name    string
+		rawData []byte
+		want    schema.GroupVersionKind
+	}{
+		{
+			name: "Deployment",
+			rawData: []byte(`{
+  "kind": "Deployment",
+  "apiVersion": "apps/v1",
+  "metadata": {
+    "name": "my-dep",
+    "labels": {
+      "app": "my-dep"
+    }
+  },
+  "spec": {
+    "replicas": 2,
+    "selector": {
+      "matchLabels": {
+        "app": "my-dep"
+      }
+    },
+    "template": {
+      "metadata": {
+        "labels": {
+          "app": "my-dep"
+        }
+      },
+      "spec": {
+        "containers": [
+          {
+            "name": "nginx",
+            "image": "nginx"
+          }
+        ]
+      }
+    }
+  }
+}`),
+			want: schema.GroupVersionKind{
+				Group:   "apps",
+				Version: "v1",
+				Kind:    "Deployment",
+			},
+		},
+	}
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			got, err := getGroupVersionKind(tt.rawData)
+			if err != nil {
+				t.Fatalf("unexpected error: %v", err)
+			}
+
+			if !reflect.DeepEqual(got, tt.want) {
+				t.Errorf("getGroupVersionKind() = %v, want %v", got, tt.want)
+			}
+		})
+	}
+}
diff --git a/pkg/controllers/apps/feedinventory/replicaset/replicaset_test.go b/pkg/controllers/apps/feedinventory/replicaset/replicaset_test.go
new file mode 100644
index 0000000..2d605da
--- /dev/null
+++ b/pkg/controllers/apps/feedinventory/replicaset/replicaset_test.go
@@ -0,0 +1,588 @@
+/*
+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 replicaset
+
+import (
+	"reflect"
+	"testing"
+
+	corev1 "k8s.io/api/core/v1"
+	"k8s.io/apimachinery/pkg/api/resource"
+
+	appsapi "github.com/clusternet/clusternet/pkg/apis/apps/v1alpha1"
+)
+
+func TestPluginParser(t *testing.T) {
+	tests := []struct {
+		name string
+
+		rawData      []byte
+		inputLimit   map[string]string
+		inputRequest map[string]string
+
+		replicas        int32
+		requirements    appsapi.ReplicaRequirements
+		replicaJsonPath string
+	}{
+		{
+			name: "empty requirements",
+			rawData: []byte(`{
+  "apiVersion": "apps/v1",
+  "kind": "ReplicaSet",
+  "metadata": {
+    "name": "rss-site",
+    "labels": {
+      "app": "web"
+    }
+  },
+  "spec": {
+    "replicas": 2,
+    "selector": {
+      "matchLabels": {
+        "app": "web"
+      }
+    },
+    "template": {
+      "metadata": {
+        "labels": {
+          "app": "web"
+        }
+      },
+      "spec": {
+        "containers": [
+          {
+            "name": "front-end",
+            "image": "nginx"
+          },
+          {
+            "name": "rss-reader",
+            "image": "rss-php-nginx:v1"
+          }
+        ]
+      }
+    }
+  }
+}`),
+			replicas: 2,
+			requirements: appsapi.ReplicaRequirements{
+				Resources: corev1.ResourceRequirements{
+					Limits:   map[corev1.ResourceName]resource.Quantity{},
+					Requests: map[corev1.ResourceName]resource.Quantity{},
+				},
+			},
+			replicaJsonPath: "/spec/replicas",
+		},
+
+		{
+			name: "multiple-qos-with-foo-resource",
+			rawData: []byte(`{
+  "apiVersion": "apps/v1",
+  "kind": "ReplicaSet",
+  "metadata": {
+    "name": "qos-demo",
+    "labels": {
+      "app": "qos-demo"
+    }
+  },
+  "spec": {
+    "replicas": 2,
+    "selector": {
+      "matchLabels": {
+        "app": "qos-demo"
+      }
+    },
+    "template": {
+      "metadata": {
+        "labels": {
+          "app": "qos-demo"
+        }
+      },
+      "spec": {
+        "nodeSelector": {
+          "disktype": "ssd"
+        },
+        "tolerations": [
+          {
+            "key": "example-key",
+            "operator": "Exists",
+            "effect": "NoSchedule"
+          }
+        ],
+        "containers": [
+          {
+            "name": "best-effort",
+            "image": "nginx"
+          },
+          {
+            "name": "burstable-with-foo-resource",
+            "image": "nginx",
+            "resources": {
+              "requests": {
+                "memory": "128Mi",
+                "foo": 2
+              },
+              "limits": {
+                "memory": "256Mi",
+                "foo": 3
+              }
+            }
+          },
+          {
+            "name": "guaranteed",
+            "image": "nginx",
+            "resources": {
+              "requests": {
+                "memory": "128Mi",
+                "cpu": "500m"
+              },
+              "limits": {
+                "memory": "128Mi",
+                "cpu": "500m"
+              }
+            }
+          }
+        ],
+        "affinity": {
+          "nodeAffinity": {
+            "requiredDuringSchedulingIgnoredDuringExecution": {
+              "nodeSelectorTerms": [
+                {
+                  "matchExpressions": [
+                    {
+                      "key": "kubernetes.io/os",
+                      "operator": "In",
+                      "values": [
+                        "linux"
+                      ]
+                    }
+                  ]
+                }
+              ]
+            }
+          }
+        }
+      }
+    }
+  }
+}`),
+			inputRequest: map[string]string{
+				"cpu":    "500m",
+				"memory": "256Mi",
+				"foo":    "2",
+			},
+			inputLimit: map[string]string{
+				"cpu":    "500m",
+				"memory": "384Mi",
+				"foo":    "3",
+			},
+			replicas: 2,
+			requirements: appsapi.ReplicaRequirements{
+				NodeSelector: map[string]string{
+					"disktype": "ssd",
+				},
+				Tolerations: []corev1.Toleration{
+					{
+						Key:      "example-key",
+						Operator: corev1.TolerationOpExists,
+						Effect:   corev1.TaintEffectNoSchedule,
+					},
+				},
+				Affinity: &corev1.Affinity{
+					NodeAffinity: &corev1.NodeAffinity{
+						RequiredDuringSchedulingIgnoredDuringExecution: &corev1.NodeSelector{
+							NodeSelectorTerms: []corev1.NodeSelectorTerm{
+								{
+									MatchExpressions: []corev1.NodeSelectorRequirement{
+										{
+											Key:      "kubernetes.io/os",
+											Operator: corev1.NodeSelectorOpIn,
+											Values:   []string{"linux"},
+										},
+									},
+								},
+							},
+						},
+					},
+				},
+				Resources: corev1.ResourceRequirements{
+					Limits:   map[corev1.ResourceName]resource.Quantity{},
+					Requests: map[corev1.ResourceName]resource.Quantity{},
+				},
+			},
+			replicaJsonPath: "/spec/replicas",
+		},
+
+		{
+			name: "init-containers-small-quota",
+			rawData: []byte(`{
+  "apiVersion": "apps/v1",
+  "kind": "ReplicaSet",
+  "metadata": {
+    "name": "qos-demo",
+    "labels": {
+      "app": "qos-demo"
+    }
+  },
+  "spec": {
+    "replicas": 2,
+    "selector": {
+      "matchLabels": {
+        "app": "qos-demo"
+      }
+    },
+    "template": {
+      "metadata": {
+        "labels": {
+          "app": "qos-demo"
+        }
+      },
+      "spec": {
+        "nodeSelector": {
+          "disktype": "ssd"
+        },
+        "tolerations": [
+          {
+            "key": "example-key",
+            "operator": "Exists",
+            "effect": "NoSchedule"
+          }
+        ],
+        "initContainers": [
+          {
+            "name": "init-container-1",
+            "image": "image-test:tag1"
+          },
+          {
+            "name": "init-container-2",
+            "image": "image-test:tag2",
+            "resources": {
+              "requests": {
+                "memory": "128Mi",
+                "bar": 1
+              },
+              "limits": {
+                "memory": "256Mi",
+                "bar": 2
+              }
+            }
+          }
+        ],
+        "containers": [
+          {
+            "name": "best-effort",
+            "image": "nginx"
+          },
+          {
+            "name": "burstable-with-foo-resource",
+            "image": "nginx",
+            "resources": {
+              "requests": {
+                "memory": "128Mi",
+                "foo": 2
+              },
+              "limits": {
+                "memory": "256Mi",
+                "foo": 3
+              }
+            }
+          },
+          {
+            "name": "guaranteed",
+            "image": "nginx",
+            "resources": {
+              "requests": {
+                "memory": "128Mi",
+                "cpu": "500m"
+              },
+              "limits": {
+                "memory": "128Mi",
+                "cpu": "500m"
+              }
+            }
+          }
+        ],
+        "affinity": {
+          "nodeAffinity": {
+            "requiredDuringSchedulingIgnoredDuringExecution": {
+              "nodeSelectorTerms": [
+                {
+                  "matchExpressions": [
+                    {
+                      "key": "kubernetes.io/os",
+                      "operator": "In",
+                      "values": [
+                        "linux"
+                      ]
+                    }
+                  ]
+                }
+              ]
+            }
+          }
+        }
+      }
+    }
+  }
+}`),
+			inputRequest: map[string]string{
+				"cpu":    "500m",
+				"memory": "256Mi",
+				"foo":    "2",
+				"bar":    "1",
+			},
+			inputLimit: map[string]string{
+				"cpu":    "500m",
+				"memory": "384Mi",
+				"foo":    "3",
+				"bar":    "2",
+			},
+			replicas: 2,
+			requirements: appsapi.ReplicaRequirements{
+				NodeSelector: map[string]string{
+					"disktype": "ssd",
+				},
+				Tolerations: []corev1.Toleration{
+					{
+						Key:      "example-key",
+						Operator: corev1.TolerationOpExists,
+						Effect:   corev1.TaintEffectNoSchedule,
+					},
+				},
+				Affinity: &corev1.Affinity{
+					NodeAffinity: &corev1.NodeAffinity{
+						RequiredDuringSchedulingIgnoredDuringExecution: &corev1.NodeSelector{
+							NodeSelectorTerms: []corev1.NodeSelectorTerm{
+								{
+									MatchExpressions: []corev1.NodeSelectorRequirement{
+										{
+											Key:      "kubernetes.io/os",
+											Operator: corev1.NodeSelectorOpIn,
+											Values:   []string{"linux"},
+										},
+									},
+								},
+							},
+						},
+					},
+				},
+				Resources: corev1.ResourceRequirements{
+					Limits:   map[corev1.ResourceName]resource.Quantity{},
+					Requests: map[corev1.ResourceName]resource.Quantity{},
+				},
+			},
+			replicaJsonPath: "/spec/replicas",
+		},
+
+		{
+			name: "init-containers-large-quota",
+			rawData: []byte(`{
+  "apiVersion": "apps/v1",
+  "kind": "ReplicaSet",
+  "metadata": {
+    "name": "qos-demo",
+    "labels": {
+      "app": "qos-demo"
+    }
+  },
+  "spec": {
+    "replicas": 2,
+    "selector": {
+      "matchLabels": {
+        "app": "qos-demo"
+      }
+    },
+    "template": {
+      "metadata": {
+        "labels": {
+          "app": "qos-demo"
+        }
+      },
+      "spec": {
+        "nodeSelector": {
+          "disktype": "ssd"
+        },
+        "tolerations": [
+          {
+            "key": "example-key",
+            "operator": "Exists",
+            "effect": "NoSchedule"
+          }
+        ],
+        "initContainers": [
+          {
+            "name": "init-container-1",
+            "image": "image-test:tag1"
+          },
+          {
+            "name": "init-container-2",
+            "image": "image-test:tag2",
+            "resources": {
+              "requests": {
+                "memory": "512Mi",
+                "foo": 3
+              },
+              "limits": {
+                "memory": "512Mi",
+                "foo": 4
+              }
+            }
+          }
+        ],
+        "containers": [
+          {
+            "name": "best-effort",
+            "image": "nginx"
+          },
+          {
+            "name": "burstable-with-foo-resource",
+            "image": "nginx",
+            "resources": {
+              "requests": {
+                "memory": "128Mi",
+                "foo": 2
+              },
+              "limits": {
+                "memory": "256Mi",
+                "foo": 3
+              }
+            }
+          },
+          {
+            "name": "guaranteed",
+            "image": "nginx",
+            "resources": {
+              "requests": {
+                "memory": "128Mi",
+                "cpu": "500m"
+              },
+              "limits": {
+                "memory": "128Mi",
+                "cpu": "500m"
+              }
+            }
+          }
+        ],
+        "affinity": {
+          "nodeAffinity": {
+            "requiredDuringSchedulingIgnoredDuringExecution": {
+              "nodeSelectorTerms": [
+                {
+                  "matchExpressions": [
+                    {
+                      "key": "kubernetes.io/os",
+                      "operator": "In",
+                      "values": [
+                        "linux"
+                      ]
+                    }
+                  ]
+                }
+              ]
+            }
+          }
+        }
+      }
+    }
+  }
+}`),
+			inputRequest: map[string]string{
+				"cpu":    "500m",
+				"memory": "512Mi",
+				"foo":    "3",
+			},
+			inputLimit: map[string]string{
+				"cpu":    "500m",
+				"memory": "512Mi",
+				"foo":    "4",
+			},
+			replicas: 2,
+			requirements: appsapi.ReplicaRequirements{
+				NodeSelector: map[string]string{
+					"disktype": "ssd",
+				},
+				Tolerations: []corev1.Toleration{
+					{
+						Key:      "example-key",
+						Operator: corev1.TolerationOpExists,
+						Effect:   corev1.TaintEffectNoSchedule,
+					},
+				},
+				Affinity: &corev1.Affinity{
+					NodeAffinity: &corev1.NodeAffinity{
+						RequiredDuringSchedulingIgnoredDuringExecution: &corev1.NodeSelector{
+							NodeSelectorTerms: []corev1.NodeSelectorTerm{
+								{
+									MatchExpressions: []corev1.NodeSelectorRequirement{
+										{
+											Key:      "kubernetes.io/os",
+											Operator: corev1.NodeSelectorOpIn,
+											Values:   []string{"linux"},
+										},
+									},
+								},
+							},
+						},
+					},
+				},
+				Resources: corev1.ResourceRequirements{
+					Limits:   map[corev1.ResourceName]resource.Quantity{},
+					Requests: map[corev1.ResourceName]resource.Quantity{},
+				},
+			},
+			replicaJsonPath: "/spec/replicas",
+		},
+	}
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			for resourceName, value := range tt.inputRequest {
+				quantity, err := resource.ParseQuantity(value)
+				if err != nil {
+					t.Fatalf("failed to parse quantity %s: %v", value, err)
+				}
+				tt.requirements.Resources.Requests[corev1.ResourceName(resourceName)] = quantity
+			}
+
+			for resourceName, value := range tt.inputLimit {
+				quantity, err := resource.ParseQuantity(value)
+				if err != nil {
+					t.Fatalf("failed to parse quantity %s: %v", value, err)
+				}
+				tt.requirements.Resources.Limits[corev1.ResourceName(resourceName)] = quantity
+			}
+
+			pl := &Plugin{
+				name: tt.name,
+			}
+			replicas, requirements, replicaJsonPath, err := pl.Parser(tt.rawData)
+			if err != nil {
+				t.Fatalf("Unexpected error: %v", err)
+			}
+
+			if *replicas != tt.replicas {
+				t.Errorf("Parser() replicas = %d, replicas %d", *replicas, tt.replicas)
+			}
+
+			if !reflect.DeepEqual(requirements, tt.requirements) {
+				t.Errorf("Parser() requirements = %#v\n, requirements %#v", requirements, tt.requirements)
+			}
+
+			if replicaJsonPath != tt.replicaJsonPath {
+				t.Errorf("Parser() replicaJsonPath = %s, replicaJsonPath %s", replicaJsonPath, tt.replicaJsonPath)
+			}
+		})
+	}
+}
diff --git a/pkg/controllers/apps/feedinventory/statefulset/statefulset_test.go b/pkg/controllers/apps/feedinventory/statefulset/statefulset_test.go
new file mode 100644
index 0000000..cec0d20
--- /dev/null
+++ b/pkg/controllers/apps/feedinventory/statefulset/statefulset_test.go
@@ -0,0 +1,588 @@
+/*
+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 statefulset
+
+import (
+	"reflect"
+	"testing"
+
+	corev1 "k8s.io/api/core/v1"
+	"k8s.io/apimachinery/pkg/api/resource"
+
+	appsapi "github.com/clusternet/clusternet/pkg/apis/apps/v1alpha1"
+)
+
+func TestPluginParser(t *testing.T) {
+	tests := []struct {
+		name string
+
+		rawData      []byte
+		inputLimit   map[string]string
+		inputRequest map[string]string
+
+		replicas        int32
+		requirements    appsapi.ReplicaRequirements
+		replicaJsonPath string
+	}{
+		{
+			name: "empty requirements",
+			rawData: []byte(`{
+  "apiVersion": "apps/v1",
+  "kind": "StatefulSet",
+  "metadata": {
+    "name": "rss-site",
+    "labels": {
+      "app": "web"
+    }
+  },
+  "spec": {
+    "replicas": 2,
+    "selector": {
+      "matchLabels": {
+        "app": "web"
+      }
+    },
+    "template": {
+      "metadata": {
+        "labels": {
+          "app": "web"
+        }
+      },
+      "spec": {
+        "containers": [
+          {
+            "name": "front-end",
+            "image": "nginx"
+          },
+          {
+            "name": "rss-reader",
+            "image": "rss-php-nginx:v1"
+          }
+        ]
+      }
+    }
+  }
+}`),
+			replicas: 2,
+			requirements: appsapi.ReplicaRequirements{
+				Resources: corev1.ResourceRequirements{
+					Limits:   map[corev1.ResourceName]resource.Quantity{},
+					Requests: map[corev1.ResourceName]resource.Quantity{},
+				},
+			},
+			replicaJsonPath: "/spec/replicas",
+		},
+
+		{
+			name: "multiple-qos-with-foo-resource",
+			rawData: []byte(`{
+  "apiVersion": "apps/v1",
+  "kind": "StatefulSet",
+  "metadata": {
+    "name": "qos-demo",
+    "labels": {
+      "app": "qos-demo"
+    }
+  },
+  "spec": {
+    "replicas": 2,
+    "selector": {
+      "matchLabels": {
+        "app": "qos-demo"
+      }
+    },
+    "template": {
+      "metadata": {
+        "labels": {
+          "app": "qos-demo"
+        }
+      },
+      "spec": {
+        "nodeSelector": {
+          "disktype": "ssd"
+        },
+        "tolerations": [
+          {
+            "key": "example-key",
+            "operator": "Exists",
+            "effect": "NoSchedule"
+          }
+        ],
+        "containers": [
+          {
+            "name": "best-effort",
+            "image": "nginx"
+          },
+          {
+            "name": "burstable-with-foo-resource",
+            "image": "nginx",
+            "resources": {
+              "requests": {
+                "memory": "128Mi",
+                "foo": 2
+              },
+              "limits": {
+                "memory": "256Mi",
+                "foo": 3
+              }
+            }
+          },
+          {
+            "name": "guaranteed",
+            "image": "nginx",
+            "resources": {
+              "requests": {
+                "memory": "128Mi",
+                "cpu": "500m"
+              },
+              "limits": {
+                "memory": "128Mi",
+                "cpu": "500m"
+              }
+            }
+          }
+        ],
+        "affinity": {
+          "nodeAffinity": {
+            "requiredDuringSchedulingIgnoredDuringExecution": {
+              "nodeSelectorTerms": [
+                {
+                  "matchExpressions": [
+                    {
+                      "key": "kubernetes.io/os",
+                      "operator": "In",
+                      "values": [
+                        "linux"
+                      ]
+                    }
+                  ]
+                }
+              ]
+            }
+          }
+        }
+      }
+    }
+  }
+}`),
+			inputRequest: map[string]string{
+				"cpu":    "500m",
+				"memory": "256Mi",
+				"foo":    "2",
+			},
+			inputLimit: map[string]string{
+				"cpu":    "500m",
+				"memory": "384Mi",
+				"foo":    "3",
+			},
+			replicas: 2,
+			requirements: appsapi.ReplicaRequirements{
+				NodeSelector: map[string]string{
+					"disktype": "ssd",
+				},
+				Tolerations: []corev1.Toleration{
+					{
+						Key:      "example-key",
+						Operator: corev1.TolerationOpExists,
+						Effect:   corev1.TaintEffectNoSchedule,
+					},
+				},
+				Affinity: &corev1.Affinity{
+					NodeAffinity: &corev1.NodeAffinity{
+						RequiredDuringSchedulingIgnoredDuringExecution: &corev1.NodeSelector{
+							NodeSelectorTerms: []corev1.NodeSelectorTerm{
+								{
+									MatchExpressions: []corev1.NodeSelectorRequirement{
+										{
+											Key:      "kubernetes.io/os",
+											Operator: corev1.NodeSelectorOpIn,
+											Values:   []string{"linux"},
+										},
+									},
+								},
+							},
+						},
+					},
+				},
+				Resources: corev1.ResourceRequirements{
+					Limits:   map[corev1.ResourceName]resource.Quantity{},
+					Requests: map[corev1.ResourceName]resource.Quantity{},
+				},
+			},
+			replicaJsonPath: "/spec/replicas",
+		},
+
+		{
+			name: "init-containers-small-quota",
+			rawData: []byte(`{
+  "apiVersion": "apps/v1",
+  "kind": "StatefulSet",
+  "metadata": {
+    "name": "qos-demo",
+    "labels": {
+      "app": "qos-demo"
+    }
+  },
+  "spec": {
+    "replicas": 2,
+    "selector": {
+      "matchLabels": {
+        "app": "qos-demo"
+      }
+    },
+    "template": {
+      "metadata": {
+        "labels": {
+          "app": "qos-demo"
+        }
+      },
+      "spec": {
+        "nodeSelector": {
+          "disktype": "ssd"
+        },
+        "tolerations": [
+          {
+            "key": "example-key",
+            "operator": "Exists",
+            "effect": "NoSchedule"
+          }
+        ],
+        "initContainers": [
+          {
+            "name": "init-container-1",
+            "image": "image-test:tag1"
+          },
+          {
+            "name": "init-container-2",
+            "image": "image-test:tag2",
+            "resources": {
+              "requests": {
+                "memory": "128Mi",
+                "bar": 1
+              },
+              "limits": {
+                "memory": "256Mi",
+                "bar": 2
+              }
+            }
+          }
+        ],
+        "containers": [
+          {
+            "name": "best-effort",
+            "image": "nginx"
+          },
+          {
+            "name": "burstable-with-foo-resource",
+            "image": "nginx",
+            "resources": {
+              "requests": {
+                "memory": "128Mi",
+                "foo": 2
+              },
+              "limits": {
+                "memory": "256Mi",
+                "foo": 3
+              }
+            }
+          },
+          {
+            "name": "guaranteed",
+            "image": "nginx",
+            "resources": {
+              "requests": {
+                "memory": "128Mi",
+                "cpu": "500m"
+              },
+              "limits": {
+                "memory": "128Mi",
+                "cpu": "500m"
+              }
+            }
+          }
+        ],
+        "affinity": {
+          "nodeAffinity": {
+            "requiredDuringSchedulingIgnoredDuringExecution": {
+              "nodeSelectorTerms": [
+                {
+                  "matchExpressions": [
+                    {
+                      "key": "kubernetes.io/os",
+                      "operator": "In",
+                      "values": [
+                        "linux"
+                      ]
+                    }
+                  ]
+                }
+              ]
+            }
+          }
+        }
+      }
+    }
+  }
+}`),
+			inputRequest: map[string]string{
+				"cpu":    "500m",
+				"memory": "256Mi",
+				"foo":    "2",
+				"bar":    "1",
+			},
+			inputLimit: map[string]string{
+				"cpu":    "500m",
+				"memory": "384Mi",
+				"foo":    "3",
+				"bar":    "2",
+			},
+			replicas: 2,
+			requirements: appsapi.ReplicaRequirements{
+				NodeSelector: map[string]string{
+					"disktype": "ssd",
+				},
+				Tolerations: []corev1.Toleration{
+					{
+						Key:      "example-key",
+						Operator: corev1.TolerationOpExists,
+						Effect:   corev1.TaintEffectNoSchedule,
+					},
+				},
+				Affinity: &corev1.Affinity{
+					NodeAffinity: &corev1.NodeAffinity{
+						RequiredDuringSchedulingIgnoredDuringExecution: &corev1.NodeSelector{
+							NodeSelectorTerms: []corev1.NodeSelectorTerm{
+								{
+									MatchExpressions: []corev1.NodeSelectorRequirement{
+										{
+											Key:      "kubernetes.io/os",
+											Operator: corev1.NodeSelectorOpIn,
+											Values:   []string{"linux"},
+										},
+									},
+								},
+							},
+						},
+					},
+				},
+				Resources: corev1.ResourceRequirements{
+					Limits:   map[corev1.ResourceName]resource.Quantity{},
+					Requests: map[corev1.ResourceName]resource.Quantity{},
+				},
+			},
+			replicaJsonPath: "/spec/replicas",
+		},
+
+		{
+			name: "init-containers-large-quota",
+			rawData: []byte(`{
+  "apiVersion": "apps/v1",
+  "kind": "StatefulSet",
+  "metadata": {
+    "name": "qos-demo",
+    "labels": {
+      "app": "qos-demo"
+    }
+  },
+  "spec": {
+    "replicas": 2,
+    "selector": {
+      "matchLabels": {
+        "app": "qos-demo"
+      }
+    },
+    "template": {
+      "metadata": {
+        "labels": {
+          "app": "qos-demo"
+        }
+      },
+      "spec": {
+        "nodeSelector": {
+          "disktype": "ssd"
+        },
+        "tolerations": [
+          {
+            "key": "example-key",
+            "operator": "Exists",
+            "effect": "NoSchedule"
+          }
+        ],
+        "initContainers": [
+          {
+            "name": "init-container-1",
+            "image": "image-test:tag1"
+          },
+          {
+            "name": "init-container-2",
+            "image": "image-test:tag2",
+            "resources": {
+              "requests": {
+                "memory": "512Mi",
+                "foo": 3
+              },
+              "limits": {
+                "memory": "512Mi",
+                "foo": 4
+              }
+            }
+          }
+        ],
+        "containers": [
+          {
+            "name": "best-effort",
+            "image": "nginx"
+          },
+          {
+            "name": "burstable-with-foo-resource",
+            "image": "nginx",
+            "resources": {
+              "requests": {
+                "memory": "128Mi",
+                "foo": 2
+              },
+              "limits": {
+                "memory": "256Mi",
+                "foo": 3
+              }
+            }
+          },
+          {
+            "name": "guaranteed",
+            "image": "nginx",
+            "resources": {
+              "requests": {
+                "memory": "128Mi",
+                "cpu": "500m"
+              },
+              "limits": {
+                "memory": "128Mi",
+                "cpu": "500m"
+              }
+            }
+          }
+        ],
+        "affinity": {
+          "nodeAffinity": {
+            "requiredDuringSchedulingIgnoredDuringExecution": {
+              "nodeSelectorTerms": [
+                {
+                  "matchExpressions": [
+                    {
+                      "key": "kubernetes.io/os",
+                      "operator": "In",
+                      "values": [
+                        "linux"
+                      ]
+                    }
+                  ]
+                }
+              ]
+            }
+          }
+        }
+      }
+    }
+  }
+}`),
+			inputRequest: map[string]string{
+				"cpu":    "500m",
+				"memory": "512Mi",
+				"foo":    "3",
+			},
+			inputLimit: map[string]string{
+				"cpu":    "500m",
+				"memory": "512Mi",
+				"foo":    "4",
+			},
+			replicas: 2,
+			requirements: appsapi.ReplicaRequirements{
+				NodeSelector: map[string]string{
+					"disktype": "ssd",
+				},
+				Tolerations: []corev1.Toleration{
+					{
+						Key:      "example-key",
+						Operator: corev1.TolerationOpExists,
+						Effect:   corev1.TaintEffectNoSchedule,
+					},
+				},
+				Affinity: &corev1.Affinity{
+					NodeAffinity: &corev1.NodeAffinity{
+						RequiredDuringSchedulingIgnoredDuringExecution: &corev1.NodeSelector{
+							NodeSelectorTerms: []corev1.NodeSelectorTerm{
+								{
+									MatchExpressions: []corev1.NodeSelectorRequirement{
+										{
+											Key:      "kubernetes.io/os",
+											Operator: corev1.NodeSelectorOpIn,
+											Values:   []string{"linux"},
+										},
+									},
+								},
+							},
+						},
+					},
+				},
+				Resources: corev1.ResourceRequirements{
+					Limits:   map[corev1.ResourceName]resource.Quantity{},
+					Requests: map[corev1.ResourceName]resource.Quantity{},
+				},
+			},
+			replicaJsonPath: "/spec/replicas",
+		},
+	}
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			for resourceName, value := range tt.inputRequest {
+				quantity, err := resource.ParseQuantity(value)
+				if err != nil {
+					t.Fatalf("failed to parse quantity %s: %v", value, err)
+				}
+				tt.requirements.Resources.Requests[corev1.ResourceName(resourceName)] = quantity
+			}
+
+			for resourceName, value := range tt.inputLimit {
+				quantity, err := resource.ParseQuantity(value)
+				if err != nil {
+					t.Fatalf("failed to parse quantity %s: %v", value, err)
+				}
+				tt.requirements.Resources.Limits[corev1.ResourceName(resourceName)] = quantity
+			}
+
+			pl := &Plugin{
+				name: tt.name,
+			}
+			replicas, requirements, replicaJsonPath, err := pl.Parser(tt.rawData)
+			if err != nil {
+				t.Fatalf("Unexpected error: %v", err)
+			}
+
+			if *replicas != tt.replicas {
+				t.Errorf("Parser() replicas = %d, replicas %d", *replicas, tt.replicas)
+			}
+
+			if !reflect.DeepEqual(requirements, tt.requirements) {
+				t.Errorf("Parser() requirements = %#v\n, requirements %#v", requirements, tt.requirements)
+			}
+
+			if replicaJsonPath != tt.replicaJsonPath {
+				t.Errorf("Parser() replicaJsonPath = %s, replicaJsonPath %s", replicaJsonPath, tt.replicaJsonPath)
+			}
+		})
+	}
+}
diff --git a/pkg/controllers/apps/feedinventory/utils/utils_test.go b/pkg/controllers/apps/feedinventory/utils/utils_test.go
new file mode 100644
index 0000000..902e9b6
--- /dev/null
+++ b/pkg/controllers/apps/feedinventory/utils/utils_test.go
@@ -0,0 +1,278 @@
+/*
+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 utils
+
+import (
+	"reflect"
+	"testing"
+
+	corev1 "k8s.io/api/core/v1"
+	"k8s.io/apimachinery/pkg/api/resource"
+
+	appsapi "github.com/clusternet/clusternet/pkg/apis/apps/v1alpha1"
+)
+
+func newQuantity(value string) resource.Quantity {
+	quantity, err := resource.ParseQuantity(value)
+	if err != nil {
+		panic(err)
+	}
+	return quantity
+}
+
+func TestPluginParser(t *testing.T) {
+	tests := []struct {
+		name         string
+		podSpec      corev1.PodSpec
+		requirements appsapi.ReplicaRequirements
+	}{
+		{
+			name: "empty requirements",
+			podSpec: corev1.PodSpec{
+				Containers: []corev1.Container{
+					{
+						Name:  "front-end",
+						Image: "nginx",
+					},
+					{
+						Name:  "rss-reader",
+						Image: "rss-php-nginx:v1",
+					},
+				},
+			},
+			requirements: appsapi.ReplicaRequirements{
+				Resources: corev1.ResourceRequirements{
+					Limits:   map[corev1.ResourceName]resource.Quantity{},
+					Requests: map[corev1.ResourceName]resource.Quantity{},
+				},
+			},
+		},
+
+		{
+			name: "multiple-qos-with-foo-resource",
+			podSpec: corev1.PodSpec{
+				Containers: []corev1.Container{
+					{
+						Name:  "best-effort",
+						Image: "nginx",
+					},
+					{
+						Name:  "burstable-with-foo-resource",
+						Image: "nginx",
+						Resources: corev1.ResourceRequirements{
+							Requests: map[corev1.ResourceName]resource.Quantity{
+								corev1.ResourceMemory:      newQuantity("128Mi"),
+								corev1.ResourceName("foo"): newQuantity("2"),
+							},
+							Limits: map[corev1.ResourceName]resource.Quantity{
+								corev1.ResourceMemory:      newQuantity("256Mi"),
+								corev1.ResourceName("foo"): newQuantity("3"),
+							},
+						},
+					},
+					{
+						Name:  "guaranteed",
+						Image: "nginx",
+						Resources: corev1.ResourceRequirements{
+							Requests: map[corev1.ResourceName]resource.Quantity{
+								corev1.ResourceMemory: newQuantity("128Mi"),
+								corev1.ResourceCPU:    newQuantity("500m"),
+							},
+							Limits: map[corev1.ResourceName]resource.Quantity{
+								corev1.ResourceMemory: newQuantity("128Mi"),
+								corev1.ResourceCPU:    newQuantity("500m"),
+							},
+						},
+					},
+				},
+			},
+			requirements: appsapi.ReplicaRequirements{
+				Resources: corev1.ResourceRequirements{
+					Requests: map[corev1.ResourceName]resource.Quantity{
+						corev1.ResourceMemory:      newQuantity("256Mi"),
+						corev1.ResourceCPU:         newQuantity("500m"),
+						corev1.ResourceName("foo"): newQuantity("2"),
+					},
+					Limits: map[corev1.ResourceName]resource.Quantity{
+						corev1.ResourceMemory:      newQuantity("384Mi"),
+						corev1.ResourceCPU:         newQuantity("500m"),
+						corev1.ResourceName("foo"): newQuantity("3"),
+					},
+				},
+			},
+		},
+
+		{
+			name: "init-containers-small-quota",
+			podSpec: corev1.PodSpec{
+				InitContainers: []corev1.Container{
+					{
+						Name:  "init-container-1",
+						Image: "image-test:tag1",
+					},
+					{
+						Name:  "init-container-2",
+						Image: "image-test:tag2",
+						Resources: corev1.ResourceRequirements{
+							Requests: map[corev1.ResourceName]resource.Quantity{
+								corev1.ResourceMemory:      newQuantity("128Mi"),
+								corev1.ResourceName("bar"): newQuantity("1"),
+							},
+							Limits: map[corev1.ResourceName]resource.Quantity{
+								corev1.ResourceMemory:      newQuantity("256Mi"),
+								corev1.ResourceName("bar"): newQuantity("2"),
+							},
+						},
+					},
+				},
+				Containers: []corev1.Container{
+					{
+						Name:  "best-effort",
+						Image: "nginx",
+					},
+					{
+						Name:  "burstable-with-foo-resource",
+						Image: "nginx",
+						Resources: corev1.ResourceRequirements{
+							Requests: map[corev1.ResourceName]resource.Quantity{
+								corev1.ResourceMemory:      newQuantity("128Mi"),
+								corev1.ResourceName("foo"): newQuantity("2"),
+							},
+							Limits: map[corev1.ResourceName]resource.Quantity{
+								corev1.ResourceMemory:      newQuantity("256Mi"),
+								corev1.ResourceName("foo"): newQuantity("3"),
+							},
+						},
+					},
+					{
+						Name:  "guaranteed",
+						Image: "nginx",
+						Resources: corev1.ResourceRequirements{
+							Requests: map[corev1.ResourceName]resource.Quantity{
+								corev1.ResourceMemory: newQuantity("128Mi"),
+								corev1.ResourceCPU:    newQuantity("500m"),
+							},
+							Limits: map[corev1.ResourceName]resource.Quantity{
+								corev1.ResourceMemory: newQuantity("128Mi"),
+								corev1.ResourceCPU:    newQuantity("500m"),
+							},
+						},
+					},
+				},
+			},
+			requirements: appsapi.ReplicaRequirements{
+				Resources: corev1.ResourceRequirements{
+					Requests: map[corev1.ResourceName]resource.Quantity{
+						corev1.ResourceMemory:      newQuantity("256Mi"),
+						corev1.ResourceCPU:         newQuantity("500m"),
+						corev1.ResourceName("foo"): newQuantity("2"),
+						corev1.ResourceName("bar"): newQuantity("1"),
+					},
+					Limits: map[corev1.ResourceName]resource.Quantity{
+						corev1.ResourceMemory:      newQuantity("384Mi"),
+						corev1.ResourceCPU:         newQuantity("500m"),
+						corev1.ResourceName("foo"): newQuantity("3"),
+						corev1.ResourceName("bar"): newQuantity("2"),
+					},
+				},
+			},
+		},
+
+		{
+			name: "init-containers-large-quota",
+			podSpec: corev1.PodSpec{
+				InitContainers: []corev1.Container{
+					{
+						Name:  "init-container-1",
+						Image: "image-test:tag1",
+					},
+					{
+						Name:  "init-container-2",
+						Image: "image-test:tag2",
+						Resources: corev1.ResourceRequirements{
+							Requests: map[corev1.ResourceName]resource.Quantity{
+								corev1.ResourceMemory:      newQuantity("512Mi"),
+								corev1.ResourceName("foo"): newQuantity("3"),
+							},
+							Limits: map[corev1.ResourceName]resource.Quantity{
+								corev1.ResourceMemory:      newQuantity("512Mi"),
+								corev1.ResourceName("foo"): newQuantity("4"),
+							},
+						},
+					},
+				},
+				Containers: []corev1.Container{
+					{
+						Name:  "best-effort",
+						Image: "nginx",
+					},
+					{
+						Name:  "burstable-with-foo-resource",
+						Image: "nginx",
+						Resources: corev1.ResourceRequirements{
+							Requests: map[corev1.ResourceName]resource.Quantity{
+								corev1.ResourceMemory:      newQuantity("128Mi"),
+								corev1.ResourceName("foo"): newQuantity("2"),
+							},
+							Limits: map[corev1.ResourceName]resource.Quantity{
+								corev1.ResourceMemory:      newQuantity("256Mi"),
+								corev1.ResourceName("foo"): newQuantity("3"),
+							},
+						},
+					},
+					{
+						Name:  "guaranteed",
+						Image: "nginx",
+						Resources: corev1.ResourceRequirements{
+							Requests: map[corev1.ResourceName]resource.Quantity{
+								corev1.ResourceMemory: newQuantity("128Mi"),
+								corev1.ResourceCPU:    newQuantity("500m"),
+							},
+							Limits: map[corev1.ResourceName]resource.Quantity{
+								corev1.ResourceMemory: newQuantity("128Mi"),
+								corev1.ResourceCPU:    newQuantity("500m"),
+							},
+						},
+					},
+				},
+			},
+			requirements: appsapi.ReplicaRequirements{
+				Resources: corev1.ResourceRequirements{
+					Requests: map[corev1.ResourceName]resource.Quantity{
+						corev1.ResourceMemory:      newQuantity("512Mi"),
+						corev1.ResourceCPU:         newQuantity("500m"),
+						corev1.ResourceName("foo"): newQuantity("3"),
+					},
+					Limits: map[corev1.ResourceName]resource.Quantity{
+						corev1.ResourceMemory:      newQuantity("512Mi"),
+						corev1.ResourceCPU:         newQuantity("500m"),
+						corev1.ResourceName("foo"): newQuantity("4"),
+					},
+				},
+			},
+		},
+	}
+
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			requirements := GetReplicaRequirements(tt.podSpec)
+			if !reflect.DeepEqual(requirements, tt.requirements) {
+				t.Errorf("GetReplicaRequirements() requirements = %#v\n, requirements %#v", requirements, tt.requirements)
+			}
+		})
+	}
+}
diff --git a/pkg/scheduler/algorithm/generic_test.go b/pkg/scheduler/algorithm/generic_test.go
deleted file mode 100644
index 07a8da0..0000000
--- a/pkg/scheduler/algorithm/generic_test.go
+++ /dev/null
@@ -1,324 +0,0 @@
-package algorithm
-
-import (
-	"context"
-	"fmt"
-	"reflect"
-	"testing"
-
-	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
-	"k8s.io/client-go/tools/cache"
-
-	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"
-	"github.com/clusternet/clusternet/pkg/known"
-	schedulercache "github.com/clusternet/clusternet/pkg/scheduler/cache"
-	framework "github.com/clusternet/clusternet/pkg/scheduler/framework/interfaces"
-	"github.com/clusternet/clusternet/pkg/utils"
-)
-
-func newClusterWithLabels(name, namespace string, labels map[string]string) *clusterapi.ManagedCluster {
-	return &clusterapi.ManagedCluster{
-		ObjectMeta: metav1.ObjectMeta{
-			Name:      name,
-			Namespace: namespace,
-			Labels:    labels,
-		},
-	}
-}
-
-func newDeploymentFeed(name string) *appsapi.Feed {
-	return &appsapi.Feed{
-		Kind:       "Deployment",
-		APIVersion: "apps/v1",
-		Namespace:  "default",
-		Name:       name,
-	}
-}
-
-func Test_genericScheduler_divideReplicas(t *testing.T) {
-	desiredReplicas := int32(12)
-	type args struct {
-		selected *framework.TargetClusters
-		sub      *appsapi.Subscription
-		finv     *appsapi.FeedInventory
-	}
-	tests := []struct {
-		name         string
-		args         args
-		wantReplicas map[string][]int32
-		wantErr      bool
-	}{
-		{
-			name: "1 cluster",
-			args: args{
-				selected: &framework.TargetClusters{
-					BindingClusters: []string{"cluster-ns-01/member1"},
-				},
-				sub: &appsapi.Subscription{
-					Spec: appsapi.SubscriptionSpec{
-						SchedulingStrategy: appsapi.DividingSchedulingStrategyType,
-						DividingScheduling: &appsapi.DividingSchedulingStrategy{Type: appsapi.StaticReplicaDividingType},
-						Subscribers: []appsapi.Subscriber{
-							{
-								ClusterAffinity: &metav1.LabelSelector{
-									MatchLabels: map[string]string{
-										"a": "1",
-									},
-								},
-								Weight: 1,
-							},
-						},
-						Feeds: []appsapi.Feed{*newDeploymentFeed("example-1")},
-					},
-				},
-				finv: &appsapi.FeedInventory{
-					Spec: appsapi.FeedInventorySpec{Feeds: []appsapi.FeedOrder{
-						{
-							Feed:            *newDeploymentFeed("example-1"),
-							DesiredReplicas: &desiredReplicas,
-						},
-					}},
-				},
-			},
-			wantReplicas: map[string][]int32{
-				utils.GetFeedKey(newDeploymentFeed("example-1")): {desiredReplicas},
-			},
-			wantErr: false,
-		},
-		{
-			name: "2 clusters",
-			args: args{
-				selected: &framework.TargetClusters{
-					BindingClusters: []string{"cluster-ns-01/member1", "cluster-ns-01/member2"},
-				},
-				sub: &appsapi.Subscription{
-					Spec: appsapi.SubscriptionSpec{
-						SchedulingStrategy: appsapi.DividingSchedulingStrategyType,
-						DividingScheduling: &appsapi.DividingSchedulingStrategy{Type: appsapi.StaticReplicaDividingType},
-						Subscribers: []appsapi.Subscriber{
-							{
-								ClusterAffinity: &metav1.LabelSelector{
-									MatchLabels: map[string]string{
-										"a": "1",
-									},
-								},
-								Weight: 1,
-							},
-							{
-								ClusterAffinity: &metav1.LabelSelector{
-									MatchLabels: map[string]string{
-										"b": "2",
-									},
-								},
-								Weight: 2,
-							},
-						},
-						Feeds: []appsapi.Feed{*newDeploymentFeed("example-1")},
-					},
-				},
-				finv: &appsapi.FeedInventory{
-					Spec: appsapi.FeedInventorySpec{Feeds: []appsapi.FeedOrder{
-						{
-							Feed:            *newDeploymentFeed("example-1"),
-							DesiredReplicas: &desiredReplicas,
-						},
-					}},
-				},
-			},
-			wantReplicas: map[string][]int32{
-				utils.GetFeedKey(newDeploymentFeed("example-1")): {4, 8},
-			},
-			wantErr: false,
-		},
-		{
-			name: "3 clusters",
-			args: args{
-				selected: &framework.TargetClusters{
-					BindingClusters: []string{"cluster-ns-01/member1", "cluster-ns-01/member2", "cluster-ns-02/member3"},
-				},
-				sub: &appsapi.Subscription{
-					Spec: appsapi.SubscriptionSpec{
-						SchedulingStrategy: appsapi.DividingSchedulingStrategyType,
-						DividingScheduling: &appsapi.DividingSchedulingStrategy{Type: appsapi.StaticReplicaDividingType},
-						Subscribers: []appsapi.Subscriber{
-							{
-								ClusterAffinity: &metav1.LabelSelector{
-									MatchLabels: map[string]string{
-										"a": "1",
-									},
-								},
-								Weight: 1,
-							},
-							{
-								ClusterAffinity: &metav1.LabelSelector{
-									MatchLabels: map[string]string{
-										"b": "2",
-									},
-								},
-								Weight: 2,
-							},
-						},
-						Feeds: []appsapi.Feed{*newDeploymentFeed("example-1")},
-					},
-				},
-				finv: &appsapi.FeedInventory{
-					Spec: appsapi.FeedInventorySpec{Feeds: []appsapi.FeedOrder{
-						{
-							Feed:            *newDeploymentFeed("example-1"),
-							DesiredReplicas: &desiredReplicas,
-						},
-					}},
-				},
-			},
-			wantReplicas: map[string][]int32{
-				utils.GetFeedKey(newDeploymentFeed("example-1")): {3, 6, 3},
-			},
-			wantErr: false,
-		},
-		{
-			name: "3 clusters with duplicated weight",
-			args: args{
-				selected: &framework.TargetClusters{
-					BindingClusters: []string{"cluster-ns-01/member1", "cluster-ns-01/member2", "cluster-ns-02/member3"},
-				},
-				sub: &appsapi.Subscription{
-					Spec: appsapi.SubscriptionSpec{
-						SchedulingStrategy: appsapi.DividingSchedulingStrategyType,
-						DividingScheduling: &appsapi.DividingSchedulingStrategy{Type: appsapi.StaticReplicaDividingType},
-						Subscribers: []appsapi.Subscriber{
-							{
-								ClusterAffinity: &metav1.LabelSelector{
-									MatchLabels: map[string]string{
-										"a": "1",
-									},
-								},
-								Weight: 1,
-							},
-							{
-								ClusterAffinity: &metav1.LabelSelector{
-									MatchLabels: map[string]string{
-										"b": "2",
-									},
-								},
-								Weight: 0,
-							},
-							{
-								ClusterAffinity: &metav1.LabelSelector{
-									MatchLabels: map[string]string{
-										"b": "1",
-									},
-								},
-								Weight: 2,
-							},
-						},
-						Feeds: []appsapi.Feed{*newDeploymentFeed("example-1")},
-					},
-				},
-				finv: &appsapi.FeedInventory{
-					Spec: appsapi.FeedInventorySpec{Feeds: []appsapi.FeedOrder{
-						{
-							Feed:            *newDeploymentFeed("example-1"),
-							DesiredReplicas: &desiredReplicas,
-						},
-					}},
-				},
-			},
-			wantReplicas: map[string][]int32{
-				utils.GetFeedKey(newDeploymentFeed("example-1")): {3, 0, 9},
-			},
-			wantErr: false,
-		},
-		{
-			name: "2 feeds, 3 clusters with duplicated weight",
-			args: args{
-				selected: &framework.TargetClusters{
-					BindingClusters: []string{"cluster-ns-01/member1", "cluster-ns-01/member2", "cluster-ns-02/member3"},
-				},
-				sub: &appsapi.Subscription{
-					Spec: appsapi.SubscriptionSpec{
-						SchedulingStrategy: appsapi.DividingSchedulingStrategyType,
-						DividingScheduling: &appsapi.DividingSchedulingStrategy{Type: appsapi.StaticReplicaDividingType},
-						Subscribers: []appsapi.Subscriber{
-							{
-								ClusterAffinity: &metav1.LabelSelector{
-									MatchLabels: map[string]string{
-										"a": "1",
-									},
-								},
-								Weight: 1,
-							},
-							{
-								ClusterAffinity: &metav1.LabelSelector{
-									MatchLabels: map[string]string{
-										"b": "2",
-									},
-								},
-								Weight: 0,
-							},
-							{
-								ClusterAffinity: &metav1.LabelSelector{
-									MatchLabels: map[string]string{
-										"b": "1",
-									},
-								},
-								Weight: 2,
-							},
-						},
-						Feeds: []appsapi.Feed{*newDeploymentFeed("example-1"), *newDeploymentFeed("example-2")},
-					},
-				},
-				finv: &appsapi.FeedInventory{
-					Spec: appsapi.FeedInventorySpec{Feeds: []appsapi.FeedOrder{
-						{
-							Feed:            *newDeploymentFeed("example-1"),
-							DesiredReplicas: &desiredReplicas,
-						},
-						{
-							Feed:            *newDeploymentFeed("example-2"),
-							DesiredReplicas: &desiredReplicas,
-						},
-					}},
-				},
-			},
-			wantReplicas: map[string][]int32{
-				utils.GetFeedKey(newDeploymentFeed("example-1")): {3, 0, 9},
-				utils.GetFeedKey(newDeploymentFeed("example-2")): {3, 0, 9},
-			},
-			wantErr: false,
-		},
-	}
-	for _, tt := range tests {
-		t.Run(tt.name, func(t *testing.T) {
-			ctx, cancel := context.WithCancel(context.Background())
-			defer cancel()
-
-			clusternetClient := fake.NewSimpleClientset(
-				newClusterWithLabels("member1", "cluster-ns-01", map[string]string{"a": "1"}),
-				newClusterWithLabels("member2", "cluster-ns-01", map[string]string{"b": "2"}),
-				newClusterWithLabels("member3", "cluster-ns-02", map[string]string{"a": "1", "b": "1"}),
-			)
-			clusternetInformerFactory := informers.NewSharedInformerFactory(clusternetClient, known.DefaultResync)
-			clusterInformer := clusternetInformerFactory.Clusters().V1beta1().ManagedClusters()
-
-			g := &genericScheduler{
-				cache: schedulercache.New(clusterInformer.Lister()),
-			}
-
-			clusternetInformerFactory.Start(ctx.Done())
-			if !cache.WaitForCacheSync(ctx.Done(), clusterInformer.Informer().HasSynced) {
-				t.Fatalf("divideReplicas() error = %v, wantErr %v", fmt.Errorf("failed to wait for cache sync"), tt.wantErr)
-			}
-
-			if err := g.divideReplicas(tt.args.selected, tt.args.sub, tt.args.finv); (err != nil) != tt.wantErr {
-				t.Errorf("divideReplicas() error = %v, wantErr %v", err, tt.wantErr)
-			}
-			if !reflect.DeepEqual(tt.args.selected.Replicas, tt.wantReplicas) {
-				t.Errorf("divideReplicas() gotResponse = %v, want %v", tt.args.selected.Replicas, tt.wantReplicas)
-			}
-
-		})
-	}
-}
diff --git a/pkg/scheduler/framework/plugins/defaultbinder/default_binder_test.go b/pkg/scheduler/framework/plugins/defaultbinder/default_binder_test.go
index eff3951..453c474 100644
--- a/pkg/scheduler/framework/plugins/defaultbinder/default_binder_test.go
+++ b/pkg/scheduler/framework/plugins/defaultbinder/default_binder_test.go
@@ -28,7 +28,6 @@ import (
 
 	appsapi "github.com/clusternet/clusternet/pkg/apis/apps/v1alpha1"
 	"github.com/clusternet/clusternet/pkg/generated/clientset/versioned/fake"
-	framework "github.com/clusternet/clusternet/pkg/scheduler/framework/interfaces"
 	frameworkruntime "github.com/clusternet/clusternet/pkg/scheduler/framework/runtime"
 )
 
@@ -36,11 +35,9 @@ func TestDefaultBinder(t *testing.T) {
 	testSubscription := &appsapi.Subscription{
 		ObjectMeta: metav1.ObjectMeta{Name: "foo", Namespace: "ns"},
 	}
-	testClusters := framework.TargetClusters{
-		BindingClusters: []string{
-			"cluster-ns-03/xyz",
-			"cluster-ns-01/def",
-		},
+	testClusters := []string{
+		"cluster-ns-03/xyz",
+		"cluster-ns-01/def",
 	}
 	tests := []struct {
 		name           string