Repository: kubernetes-sigs/kube-scheduler-simulator
Pull request #400 (2024-12-16 07:07:18)

Separate the logic of applying resources from syncer and make it reusable
<!--  Thanks for sending a pull request!  Here are some tips for you:

1. If this is your first time, please read our contributor guidelines: https://git.k8s.io/community/contributors/guide/first-contribution.md#your-first-contribution and developer guide https://git.k8s.io/community/contributors/devel/development.md#development-guide
2. Please label this pull request according to what type of issue you are addressing, especially if this is a release targeted pull request. For reference on required PR/issue labels, read here:
https://git.k8s.io/community/contributors/devel/sig-release/release.md#issuepr-kind-label
3. Ensure you have added or ran the appropriate tests for your PR: https://git.k8s.io/community/contributors/devel/sig-testing/testing.md
4. If you want *faster* PR reviews, read how: https://git.k8s.io/community/contributors/guide/pull-requests.md#best-practices-for-faster-reviews
5. If the PR is unfinished, see how to mark it: https://git.k8s.io/community/contributors/guide/pull-requests.md#marking-unfinished-pull-requests
-->

#### What type of PR is this?

<!--
Add related area tag(s):
/area web
/area simulator
/area scenario

Add one of the following kinds:
/kind bug

/kind documentation
/kind feature

Optionally add one or more of the following kinds if applicable:
/kind api-change
/kind deprecation
/kind failing-test
/kind flake
/kind regression
-->

/area simulator
/kind cleanup

#### What this PR does / why we need it:

`syncer` and `oneshotimporter` have their own logic of applying resources. They can be generalized and the replaying feature (#395) can use it.

This PR separates that of `syncer` and makes it a module `resourceapplier`.

#### Which issue(s) this PR fixes:
<!--
*Automatically closes linked issue when PR is merged.
Usage: `Fixes #<issue number>`, or `Fixes (paste link of issue)`.
_If PR is about `failing-tests or flakes`, please post the related issues/tests in a comment and do not use `Fixes`_*
-->
Part of #395 

#### Special notes for your reviewer:

I'll create another PR to refactor `oneshotimporter` to use `resourceapplier`.

/label tide/merge-method-squash


k8s-ci-robot: [APPROVALNOTIFIER] This PR is **NOT APPROVED**

This pull-request has been approved by: *<a href="https://github.com/kubernetes-sigs/kube-scheduler-simulator/pull/400#" title="Author self-approved">saza-ku</a>*
**Once this PR has been reviewed and has the lgtm label**, please assign [sanposhiho](https://github.com/sanposhiho) for approval. For more information see [the Kubernetes Code Review Process](https://git.k8s.io/community/contributors/guide/owners.md#the-code-review-process).

The full list of commands accepted by this bot can be found [here](https://go.k8s.io/bot-commands?repo=kubernetes-sigs%2Fkube-scheduler-simulator).

<details open>
Needs approval from an approver in each of these files:

- **[OWNERS](https://github.com/kubernetes-sigs/kube-scheduler-simulator/blob/master/OWNERS)**

Approvers can indicate their approval by writing `/approve` in a comment
Approvers can cancel approval by writing `/approve cancel` in a comment
</details>
<!-- META={"approvers":["sanposhiho"]} -->
k8s-ci-robot: Hi @saza-ku. Thanks for your PR.

I'm waiting for a [kubernetes-sigs](https://github.com/orgs/kubernetes-sigs/people) member to verify that this patch is reasonable to test. If it is, they should reply with `/ok-to-test` on its own line. Until that is done, I will not automatically test new commits in this PR, but the usual testing commands by org members will still work. Regular contributors should [join the org](https://git.k8s.io/community/community-membership.md#member) to skip this step.

Once the patch is verified, the new status will be reflected by the `ok-to-test` label.

I understand the commands that are listed [here](https://go.k8s.io/bot-commands?repo=kubernetes-sigs%2Fkube-scheduler-simulator).

<details>

Instructions for interacting with me using PR comments are available [here](https://git.k8s.io/community/contributors/guide/pull-requests.md).  If you have questions or suggestions related to my behavior, please file an issue against the [kubernetes-sigs/prow](https://github.com/kubernetes-sigs/prow/issues/new?title=Prow%20issue:) repository.
</details>

sanposhiho: /ok-to-test
/cc @utam0k @ordovicia 
k8s-ci-robot: @sanposhiho: GitHub didn't allow me to request PR reviews from the following users: ordovicia.

Note that only [kubernetes-sigs members](https://github.com/orgs/kubernetes-sigs/people) and repo collaborators can review this PR, and authors cannot review their own PRs.

<details>

In response to [this](https://github.com/kubernetes-sigs/kube-scheduler-simulator/pull/400#issuecomment-2545361231):

>/ok-to-test
>/cc @utam0k @ordovicia 


Instructions for interacting with me using PR comments are available [here](https://git.k8s.io/community/contributors/guide/pull-requests.md).  If you have questions or suggestions related to my behavior, please file an issue against the [kubernetes-sigs/prow](https://github.com/kubernetes-sigs/prow/issues/new?title=Prow%20issue:) repository.
</details>
k8s-ci-robot: @saza-ku: The following test **failed**, say `/retest` to rerun all failed tests or `/retest-required` to rerun all mandatory failed tests:

Test name | Commit | Details | Required | Rerun command
--- | --- | --- | --- | ---
pull-kube-scheduler-simulator-backend-lint | 969e21bdcbe972920fbb10eacfdbffab672d64d5 | [link](https://prow.k8s.io/view/gs/kubernetes-ci-logs/pr-logs/pull/kubernetes-sigs_kube-scheduler-simulator/400/pull-kube-scheduler-simulator-backend-lint/1868618740506038272) | true | `/test pull-kube-scheduler-simulator-backend-lint`

[Full PR test history](https://prow.k8s.io/pr-history?org=kubernetes-sigs&repo=kube-scheduler-simulator&pr=400). [Your PR dashboard](https://prow.k8s.io/pr?query=is%3Apr%20state%3Aopen%20author%3Asaza-ku). Please help us cut down on flakes by [linking to](https://git.k8s.io/community/contributors/devel/sig-testing/flaky-tests.md#filing-issues-for-flaky-tests) an [open issue](https://github.com/kubernetes-sigs/kube-scheduler-simulator/issues?q=is:issue+is:open) when you hit one in your PR.

<details>

Instructions for interacting with me using PR comments are available [here](https://git.k8s.io/community/contributors/guide/pull-requests.md).  If you have questions or suggestions related to my behavior, please file an issue against the [kubernetes-sigs/prow](https://github.com/kubernetes-sigs/prow/issues/new?title=Prow%20issue:) repository. I understand the commands that are listed [here](https://go.k8s.io/bot-commands).
</details>
<!-- test report -->
saza-ku: /retest
k8s-ci-robot: @saza-ku: The following test **failed**, say `/retest` to rerun all failed tests or `/retest-required` to rerun all mandatory failed tests:

Test name | Commit | Details | Required | Rerun command
--- | --- | --- | --- | ---
pull-kube-scheduler-simulator-backend-lint | d0fceccfb6bff7cba389fc18cf0d8520b9ac68d6 | [link](https://prow.k8s.io/view/gs/kubernetes-ci-logs/pr-logs/pull/kubernetes-sigs_kube-scheduler-simulator/400/pull-kube-scheduler-simulator-backend-lint/1871113591772418048) | true | `/test pull-kube-scheduler-simulator-backend-lint`

[Full PR test history](https://prow.k8s.io/pr-history?org=kubernetes-sigs&repo=kube-scheduler-simulator&pr=400). [Your PR dashboard](https://prow.k8s.io/pr?query=is%3Apr%20state%3Aopen%20author%3Asaza-ku). Please help us cut down on flakes by [linking to](https://git.k8s.io/community/contributors/devel/sig-testing/flaky-tests.md#filing-issues-for-flaky-tests) an [open issue](https://github.com/kubernetes-sigs/kube-scheduler-simulator/issues?q=is:issue+is:open) when you hit one in your PR.

<details>

Instructions for interacting with me using PR comments are available [here](https://git.k8s.io/community/contributors/guide/pull-requests.md).  If you have questions or suggestions related to my behavior, please file an issue against the [kubernetes-sigs/prow](https://github.com/kubernetes-sigs/prow/issues/new?title=Prow%20issue:) repository. I understand the commands that are listed [here](https://go.k8s.io/bot-commands).
</details>
<!-- test report -->
k8s-ci-robot: [APPROVALNOTIFIER] This PR is **NOT APPROVED**

This pull-request has been approved by: *<a href="https://github.com/kubernetes-sigs/kube-scheduler-simulator/pull/400#pullrequestreview-2520210005" title="Approved">ordovicia</a>*, *<a href="https://github.com/kubernetes-sigs/kube-scheduler-simulator/pull/400#" title="Author self-approved">saza-ku</a>*
**Once this PR has been reviewed and has the lgtm label**, please assign [sanposhiho](https://github.com/sanposhiho) for approval. For more information see [the Kubernetes Code Review Process](https://git.k8s.io/community/contributors/guide/owners.md#the-code-review-process).

The full list of commands accepted by this bot can be found [here](https://go.k8s.io/bot-commands?repo=kubernetes-sigs%2Fkube-scheduler-simulator).

<details open>
Needs approval from an approver in each of these files:

- **[OWNERS](https://github.com/kubernetes-sigs/kube-scheduler-simulator/blob/master/OWNERS)**

Approvers can indicate their approval by writing `/approve` in a comment
Approvers can cancel approval by writing `/approve cancel` in a comment
</details>
<!-- META={"approvers":["sanposhiho"]} -->
utam0k: /lgtm

@sanposhiho Would you mind checking this PR because this PR needs `/approve`
saza-ku: @sanposhiho 

> This PR doesn't change oneshotimporter, did you mean you're gonna create another PR to refactor that to use resourceapplier?

Yes. This is because `oneshotimporter` was under revision when I created this PR. (https://github.com/kubernetes-sigs/kube-scheduler-simulator/issues/395#issuecomment-2544775110)
k8s-ci-robot: New changes are detected. LGTM label has been removed.
k8s-ci-robot: [APPROVALNOTIFIER] This PR is **NOT APPROVED**

This pull-request has been approved by: *<a href="https://github.com/kubernetes-sigs/kube-scheduler-simulator/pull/400#pullrequestreview-2520210005" title="Approved">ordovicia</a>*, *<a href="https://github.com/kubernetes-sigs/kube-scheduler-simulator/pull/400#" title="Author self-approved">saza-ku</a>*
**Once this PR has been reviewed and has the lgtm label**, please assign [sanposhiho](https://github.com/sanposhiho) for approval. For more information see [the Code Review Process](https://git.k8s.io/community/contributors/guide/owners.md#the-code-review-process).

The full list of commands accepted by this bot can be found [here](https://go.k8s.io/bot-commands?repo=kubernetes-sigs%2Fkube-scheduler-simulator).

<details open>
Needs approval from an approver in each of these files:

- **[OWNERS](https://github.com/kubernetes-sigs/kube-scheduler-simulator/blob/master/OWNERS)**

Approvers can indicate their approval by writing `/approve` in a comment
Approvers can cancel approval by writing `/approve cancel` in a comment
</details>
<!-- META={"approvers":["sanposhiho"]} -->
k8s-ci-robot: [APPROVALNOTIFIER] This PR is **APPROVED**

This pull-request has been approved by: *<a href="https://github.com/kubernetes-sigs/kube-scheduler-simulator/pull/400#pullrequestreview-2520210005" title="Approved">ordovicia</a>*, *<a href="https://github.com/kubernetes-sigs/kube-scheduler-simulator/pull/400#pullrequestreview-2563425953" title="Approved">sanposhiho</a>*, *<a href="https://github.com/kubernetes-sigs/kube-scheduler-simulator/pull/400#" title="Author self-approved">saza-ku</a>*

The full list of commands accepted by this bot can be found [here](https://go.k8s.io/bot-commands?repo=kubernetes-sigs%2Fkube-scheduler-simulator).

The pull request process is described [here](https://git.k8s.io/community/contributors/guide/owners.md#the-code-review-process)

<details >
Needs approval from an approver in each of these files:

- ~~[OWNERS](https://github.com/kubernetes-sigs/kube-scheduler-simulator/blob/master/OWNERS)~~ [sanposhiho]

Approvers can indicate their approval by writing `/approve` in a comment
Approvers can cancel approval by writing `/approve cancel` in a comment
</details>
<!-- META={"approvers":[]} -->

Patch:
diff --git a/compose.yml b/compose.yml
index 8a1130d5..35d8bc0b 100644
--- a/compose.yml
+++ b/compose.yml
@@ -5,7 +5,7 @@ services:
       image: busybox
       volumes:
         - conf:/config
-        - ${PWD}/simulator/cmd/scheduler:/host-config:ro    
+        - ${PWD}/simulator/cmd/scheduler:/host-config:ro
       command: sh -c "cp -rf /host-config/* /config/"
   simulator-scheduler:
     image: registry.k8s.io/scheduler-simulator/debuggable-scheduler:v0.4.0
@@ -68,4 +68,4 @@ networks:
     driver: bridge
 volumes:
   simulator-etcd-data:
-  conf:
\ No newline at end of file
+  conf:
diff --git a/simulator/cmd/simulator/simulator.go b/simulator/cmd/simulator/simulator.go
index 87717855..3a5305c0 100644
--- a/simulator/cmd/simulator/simulator.go
+++ b/simulator/cmd/simulator/simulator.go
@@ -20,9 +20,9 @@ import (
 	"k8s.io/klog/v2"
 
 	"sigs.k8s.io/kube-scheduler-simulator/simulator/config"
+	"sigs.k8s.io/kube-scheduler-simulator/simulator/resourceapplier"
 	"sigs.k8s.io/kube-scheduler-simulator/simulator/server"
 	"sigs.k8s.io/kube-scheduler-simulator/simulator/server/di"
-	"sigs.k8s.io/kube-scheduler-simulator/simulator/syncer"
 )
 
 const (
@@ -94,7 +94,7 @@ func startSimulator() error {
 		return xerrors.Errorf("kubeapi-server is not ready: %w", err)
 	}
 
-	dic, err := di.NewDIContainer(client, dynamicClient, restMapper, etcdclient, restCfg, cfg.InitialSchedulerCfg, cfg.ExternalImportEnabled, cfg.ResourceSyncEnabled, importClusterResourceClient, importClusterDynamicClient, cfg.Port, syncer.Options{})
+	dic, err := di.NewDIContainer(client, dynamicClient, restMapper, etcdclient, restCfg, cfg.InitialSchedulerCfg, cfg.ExternalImportEnabled, cfg.ResourceSyncEnabled, importClusterResourceClient, importClusterDynamicClient, cfg.Port, resourceapplier.Options{})
 	if err != nil {
 		return xerrors.Errorf("create di container: %w", err)
 	}
diff --git a/simulator/docs/import-cluster-resources.md b/simulator/docs/import-cluster-resources.md
index 5a39acb1..3e61df65 100644
--- a/simulator/docs/import-cluster-resources.md
+++ b/simulator/docs/import-cluster-resources.md
@@ -68,19 +68,23 @@ It imports the following resources, which the scheduler's default plugins take i
 If you need to, you can tweak which resources to import via the option in [/simulator/cmd/simulator/simulator.go](https://github.com/kubernetes-sigs/kube-scheduler-simulator/blob/master/simulator/cmd/simulator/simulator.go):
 
 ```go
-dic, err := di.NewDIContainer(..., syncer.Options{
+dic, err := di.NewDIContainer(..., resourceapplier.Options{
 	// GVRsToSync is a list of GroupVersionResource that will be synced.
 	// If GVRsToSync is nil, defaultGVRs are used.
 	GVRsToSync: []schema.GroupVersionResource{
-    		{Group: "your-group", Version: "v1", Resource: "your-custom-resources"},
-  	}
+		{Group: "your-group", Version: "v1", Resource: "your-custom-resources"},
+	},
 
 	// Actually, more options are available...
- 
-	// AdditionalMutatingFunctions is a list of mutating functions that users add.
-	AdditionalMutatingFunctions:  map[schema.GroupVersionResource]MutatingFunction{...}
-	// AdditionalFilteringFunctions is a list of filtering functions that users add.
-	AdditionalFilteringFunctions: map[schema.GroupVersionResource]FilteringFunction{...}
+
+	// FilterBeforeCreating is a list of additional filtering functions that are applied before creating resources.
+	FilterBeforeCreating: map[schema.GroupVersionResource][]resourceapplier.FilteringFunction{},
+	// MutateBeforeCreating is a list of additional mutating functions that are applied before creating resources.
+	MutateBeforeCreating: map[schema.GroupVersionResource][]resourceapplier.MutatingFunction{},
+	// FilterBeforeUpdating is a list of additional filtering functions that are applied before updating resources.
+	FilterBeforeUpdating: map[schema.GroupVersionResource][]resourceapplier.FilteringFunction{},
+	// MutateBeforeUpdating is a list of additional mutating functions that are applied before updating resources.
+	MutateBeforeUpdating: map[schema.GroupVersionResource][]resourceapplier.MutatingFunction{},
 })
 ```
 
diff --git a/simulator/syncer/resource.go b/simulator/resourceapplier/resource.go
similarity index 55%
rename from simulator/syncer/resource.go
rename to simulator/resourceapplier/resource.go
index 8e4cb5e7..0bccabe7 100644
--- a/simulator/syncer/resource.go
+++ b/simulator/resourceapplier/resource.go
@@ -1,4 +1,4 @@
-package syncer
+package resourceapplier
 
 import (
 	"context"
@@ -11,49 +11,31 @@ import (
 	"k8s.io/klog/v2"
 )
 
-// DefaultGVRs is a list of GroupVersionResource that we sync by default (configurable with Options),
-// which is a suitable resource set for the vanilla scheduler.
-//
-// Note that this order matters - When first importing resources, we want to sync namespaces first, then priorityclasses, storageclasses...
-var DefaultGVRs = []schema.GroupVersionResource{
-	{Group: "", Version: "v1", Resource: "namespaces"},
-	{Group: "scheduling.k8s.io", Version: "v1", Resource: "priorityclasses"},
-	{Group: "storage.k8s.io", Version: "v1", Resource: "storageclasses"},
-	{Group: "", Version: "v1", Resource: "persistentvolumeclaims"},
-	{Group: "", Version: "v1", Resource: "nodes"},
-	{Group: "", Version: "v1", Resource: "persistentvolumes"},
-	{Group: "", Version: "v1", Resource: "pods"},
-}
-
-// Event is a type of events that occur in the source cluster.
-type Event int
-
-const (
-	Add Event = iota
-	Update
-)
+// mandatoryFilterForCreating is FilteringFunctions that we must register for creating.
+// We don't allow users to opt out them.
+var mandatoryFilterForCreating = map[schema.GroupVersionResource]FilteringFunction{}
 
-// mandatoryMutatingFunctions is MutatingFunctions that we must register.
+// mandatoryMutateForCreating is MutatingFunctions that we must register for creating.
 // We don't allow users to opt out them.
-var mandatoryMutatingFunctions = map[schema.GroupVersionResource]MutatingFunction{
+var mandatoryMutateForCreating = map[schema.GroupVersionResource]MutatingFunction{
 	{Group: "", Version: "v1", Resource: "persistentvolumes"}: mutatePV,
 	{Group: "", Version: "v1", Resource: "pods"}:              mutatePods,
 }
 
-// mandatoryFilteringFunctions is FilteringFunctions that we must register.
+// mandatoryFilterForUpdating is FilteringFunctions that we must register.
 // We don't allow users to opt out them.
-var mandatoryFilteringFunctions = map[schema.GroupVersionResource]FilteringFunction{
-	{Group: "", Version: "v1", Resource: "pods"}: filterPods,
+var mandatoryFilterForUpdating = map[schema.GroupVersionResource]FilteringFunction{
+	{Group: "", Version: "v1", Resource: "pods"}: filterPodsForUpdating,
 }
 
-// FilteringFunction is a function that filters a resource.
-// If it returns false, the resource will not be imported.
-type FilteringFunction func(ctx context.Context, resource *unstructured.Unstructured, clients *Clients, event Event) (bool, error)
-
-// MutatingFunction is a function that mutates a resource before importing it.
-type MutatingFunction func(ctx context.Context, resource *unstructured.Unstructured, clients *Clients, event Event) (*unstructured.Unstructured, error)
+// mandatoryMutateForUpdating is MutatingFunctions that we must register for updating.
+// We don't allow users to opt out them.
+var mandatoryMutateForUpdating = map[schema.GroupVersionResource]MutatingFunction{
+	{Group: "", Version: "v1", Resource: "persistentvolumes"}: mutatePV,
+	{Group: "", Version: "v1", Resource: "pods"}:              mutatePods,
+}
 
-func mutatePV(ctx context.Context, resource *unstructured.Unstructured, clients *Clients, _ Event) (*unstructured.Unstructured, error) {
+func mutatePV(ctx context.Context, resource *unstructured.Unstructured, clients *Clients) (*unstructured.Unstructured, error) {
 	var pv v1.PersistentVolume
 	err := runtime.DefaultUnstructuredConverter.FromUnstructured(resource.UnstructuredContent(), &pv)
 	if err != nil {
@@ -64,7 +46,7 @@ func mutatePV(ctx context.Context, resource *unstructured.Unstructured, clients
 		// PersistentVolumeClaims's UID is changed in a destination cluster when importing from a source cluster,
 		// and thus we need to update the PVC UID in the PersistentVolume.
 		// Get PVC of pv.Spec.ClaimRef.Name.
-		pvc, err := clients.SrcDynamicClient.Resource(schema.GroupVersionResource{
+		pvc, err := clients.DynamicClient.Resource(schema.GroupVersionResource{
 			Group:    "",
 			Version:  "v1",
 			Resource: "persistentvolumeclaims",
@@ -80,7 +62,7 @@ func mutatePV(ctx context.Context, resource *unstructured.Unstructured, clients
 	return &unstructured.Unstructured{Object: modifiedUnstructed}, err
 }
 
-func mutatePods(_ context.Context, resource *unstructured.Unstructured, _ *Clients, _ Event) (*unstructured.Unstructured, error) {
+func mutatePods(_ context.Context, resource *unstructured.Unstructured, _ *Clients) (*unstructured.Unstructured, error) {
 	var pod v1.Pod
 	err := runtime.DefaultUnstructuredConverter.FromUnstructured(resource.UnstructuredContent(), &pod)
 	if err != nil {
@@ -100,12 +82,7 @@ func mutatePods(_ context.Context, resource *unstructured.Unstructured, _ *Clien
 
 // filterPods checks if a pod is already scheduled when it's updated.
 // We only want to update pods that are not yet scheduled.
-func filterPods(_ context.Context, resource *unstructured.Unstructured, _ *Clients, event Event) (bool, error) {
-	if event == Add {
-		// We always add a Pod, regardless it's scheduled or not.
-		return true, nil
-	}
-
+func filterPodsForUpdating(_ context.Context, resource *unstructured.Unstructured, _ *Clients) (bool, error) {
 	var pod v1.Pod
 	err := runtime.DefaultUnstructuredConverter.FromUnstructured(resource.UnstructuredContent(), &pod)
 	if err != nil {
diff --git a/simulator/resourceapplier/resourceapplier.go b/simulator/resourceapplier/resourceapplier.go
new file mode 100644
index 00000000..6c191e36
--- /dev/null
+++ b/simulator/resourceapplier/resourceapplier.go
@@ -0,0 +1,317 @@
+package resourceapplier
+
+import (
+	"context"
+
+	"golang.org/x/xerrors"
+	"k8s.io/apimachinery/pkg/api/meta"
+	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
+	"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
+	"k8s.io/apimachinery/pkg/runtime/schema"
+	"k8s.io/client-go/dynamic"
+)
+
+// FilteringFunction is a function that filters a resource.
+// If it returns false, the resource will not be imported.
+type FilteringFunction func(ctx context.Context, resource *unstructured.Unstructured, clients *Clients) (bool, error)
+
+// MutatingFunction is a function that mutates a resource before importing it.
+type MutatingFunction func(ctx context.Context, resource *unstructured.Unstructured, clients *Clients) (*unstructured.Unstructured, error)
+
+// Note: Clients and its fields are exposed intentionally so that users can use it in MutatingFunction and FilteringFunction.
+type Clients struct {
+	// DynamicClient is the dynamic client for the destination cluster, which the resource is supposed to be copied to.
+	DynamicClient dynamic.Interface
+	RestMapper    meta.RESTMapper
+}
+
+type Options struct {
+	GVRsToSync           []schema.GroupVersionResource
+	FilterBeforeCreating map[schema.GroupVersionResource][]FilteringFunction
+	MutateBeforeCreating map[schema.GroupVersionResource][]MutatingFunction
+	FilterBeforeUpdating map[schema.GroupVersionResource][]FilteringFunction
+	MutateBeforeUpdating map[schema.GroupVersionResource][]MutatingFunction
+}
+
+type Service struct {
+	clients *Clients
+
+	mutateBeforeCreating map[schema.GroupVersionResource][]MutatingFunction
+	filterBeforeCreating map[schema.GroupVersionResource][]FilteringFunction
+	mutateBeforeUpdating map[schema.GroupVersionResource][]MutatingFunction
+	filterBeforeUpdating map[schema.GroupVersionResource][]FilteringFunction
+
+	GVRsToSync []schema.GroupVersionResource
+}
+
+func New(dynamicClient dynamic.Interface, restMapper meta.RESTMapper, options Options) *Service {
+	s := &Service{
+		clients: &Clients{
+			DynamicClient: dynamicClient,
+			RestMapper:    restMapper,
+		},
+
+		filterBeforeCreating: map[schema.GroupVersionResource][]FilteringFunction{},
+		mutateBeforeCreating: map[schema.GroupVersionResource][]MutatingFunction{},
+		filterBeforeUpdating: map[schema.GroupVersionResource][]FilteringFunction{},
+		mutateBeforeUpdating: map[schema.GroupVersionResource][]MutatingFunction{},
+
+		GVRsToSync: options.GVRsToSync,
+	}
+
+	for gvr, fn := range mandatoryFilterForCreating {
+		s.addFilterBeforeCreating(gvr, []FilteringFunction{fn})
+	}
+	for gvr, fn := range mandatoryMutateForCreating {
+		s.addMutateBeforeCreating(gvr, []MutatingFunction{fn})
+	}
+	for gvr, fn := range mandatoryFilterForUpdating {
+		s.addFilterBeforeUpdating(gvr, []FilteringFunction{fn})
+	}
+	for gvr, fn := range mandatoryMutateForUpdating {
+		s.addMutateBeforeUpdating(gvr, []MutatingFunction{fn})
+	}
+
+	for gvr, fns := range options.FilterBeforeCreating {
+		s.addFilterBeforeCreating(gvr, fns)
+	}
+	for gvr, fns := range options.MutateBeforeCreating {
+		s.addMutateBeforeCreating(gvr, fns)
+	}
+	for gvr, fns := range options.FilterBeforeUpdating {
+		s.addFilterBeforeUpdating(gvr, fns)
+	}
+	for gvr, fns := range options.MutateBeforeUpdating {
+		s.addMutateBeforeUpdating(gvr, fns)
+	}
+
+	return s
+}
+
+func (s *Service) Create(ctx context.Context, resource *unstructured.Unstructured) error {
+	// Extract the GroupVersionResource from the Unstructured object
+	gvk := resource.GroupVersionKind()
+	gvr, err := s.findGVRForGVK(gvk)
+	if err != nil {
+		return err
+	}
+
+	// Namespaces resources should be created within the namespace defined in the Unstructured object
+	namespace := resource.GetNamespace()
+
+	// Run the filtering function for the resource.
+	if ok, err := s.filterResourceForCreating(ctx, gvr, resource, s.clients); !ok || err != nil {
+		return err
+	}
+
+	// When creating a resource on the destination cluster, we must remove the metadata such as UID and Generation.
+	// It's done for all resources.
+	resource = removeUnnecessaryMetadata(resource)
+
+	// Run the mutating function for the resource.
+	resource, err = s.mutateResourceForCreating(ctx, gvr, resource, s.clients)
+	if err != nil {
+		return xerrors.Errorf("failed to mutate resource: %w", err)
+	}
+
+	// Create the resource on the destination cluster using the dynamic client
+	_, err = s.clients.DynamicClient.Resource(gvr).Namespace(namespace).Create(
+		ctx,
+		resource,
+		metav1.CreateOptions{},
+	)
+	if err != nil {
+		return xerrors.Errorf("failed to create resource: %w", err)
+	}
+
+	return nil
+}
+
+func (s *Service) Update(ctx context.Context, resource *unstructured.Unstructured) error {
+	// Extract the GroupVersionResource from the Unstructured object
+	gvk := resource.GroupVersionKind()
+	gvr, err := s.findGVRForGVK(gvk)
+	if err != nil {
+		return err
+	}
+
+	// Namespaces resources should be created within the namespace defined in the Unstructured object
+	namespace := resource.GetNamespace()
+
+	// Run the filtering function for the resource.
+	if ok, err := s.filterResourceForUpdating(ctx, gvr, resource, s.clients); !ok || err != nil {
+		return err
+	}
+
+	// When updating a resource on the destination cluster, we must remove the metadata such as UID and Generation.
+	// It's done for all resources.
+	resource = removeUnnecessaryMetadata(resource)
+
+	// Run the mutating function for the resource.
+	resource, err = s.mutateResourceForUpdating(ctx, gvr, resource, s.clients)
+	if err != nil {
+		return xerrors.Errorf("failed to mutate resource: %w", err)
+	}
+
+	// Update the resource on the destination cluster using the dynamic client
+	_, err = s.clients.DynamicClient.Resource(gvr).Namespace(namespace).Update(
+		ctx,
+		resource,
+		metav1.UpdateOptions{},
+	)
+	if err != nil {
+		return xerrors.Errorf("failed to update resource: %w", err)
+	}
+
+	return nil
+}
+
+func (s *Service) Delete(
+	ctx context.Context,
+	resource *unstructured.Unstructured,
+) error {
+	// Extract the GroupVersionResource from the Unstructured object
+	gvk := resource.GroupVersionKind()
+	gvr, err := s.findGVRForGVK(gvk)
+	if err != nil {
+		return err
+	}
+
+	// Namespaces resources should be created within the namespace defined in the Unstructured object
+	namespace := resource.GetNamespace()
+
+	// Create the resource on the destination cluster using the dynamic client
+	err = s.clients.DynamicClient.Resource(gvr).Namespace(namespace).Delete(
+		ctx,
+		resource.GetName(),
+		metav1.DeleteOptions{},
+	)
+	if err != nil {
+		return xerrors.Errorf("failed to delete resource: %w", err)
+	}
+
+	return nil
+}
+
+func (s *Service) filterResourceForCreating(ctx context.Context, gvr schema.GroupVersionResource, resource *unstructured.Unstructured, clients *Clients) (bool, error) {
+	filteringFns, ok := s.filterBeforeCreating[gvr]
+	if !ok {
+		return true, nil
+	}
+
+	for _, filteringFn := range filteringFns {
+		ok, err := filteringFn(ctx, resource, clients)
+		if err != nil {
+			return false, err
+		}
+		if !ok {
+			return false, nil
+		}
+	}
+
+	return true, nil
+}
+
+func (s *Service) mutateResourceForCreating(ctx context.Context, gvr schema.GroupVersionResource, resource *unstructured.Unstructured, clients *Clients) (*unstructured.Unstructured, error) {
+	mutatingFns, ok := s.mutateBeforeCreating[gvr]
+	if !ok {
+		return resource, nil
+	}
+
+	for _, mutatingFn := range mutatingFns {
+		modifiedResource, err := mutatingFn(ctx, resource, clients)
+		if err != nil {
+			return nil, err
+		}
+		resource = modifiedResource
+	}
+
+	return resource, nil
+}
+
+func (s *Service) filterResourceForUpdating(ctx context.Context, gvr schema.GroupVersionResource, resource *unstructured.Unstructured, clients *Clients) (bool, error) {
+	filteringFns, ok := s.filterBeforeUpdating[gvr]
+	if !ok {
+		return true, nil
+	}
+
+	for _, filteringFn := range filteringFns {
+		ok, err := filteringFn(ctx, resource, clients)
+		if err != nil {
+			return false, err
+		}
+		if !ok {
+			return false, nil
+		}
+	}
+
+	return true, nil
+}
+
+func (s *Service) mutateResourceForUpdating(ctx context.Context, gvr schema.GroupVersionResource, resource *unstructured.Unstructured, clients *Clients) (*unstructured.Unstructured, error) {
+	mutatingFns, ok := s.mutateBeforeUpdating[gvr]
+	if !ok {
+		return resource, nil
+	}
+
+	for _, mutatingFn := range mutatingFns {
+		modifiedResource, err := mutatingFn(ctx, resource, clients)
+		if err != nil {
+			return nil, err
+		}
+		resource = modifiedResource
+	}
+
+	return resource, nil
+}
+
+// findGVRForGVK uses the discovery client to get the GroupVersionResource for a given GroupVersionKind.
+func (s *Service) findGVRForGVK(gvk schema.GroupVersionKind) (schema.GroupVersionResource, error) {
+	m, err := s.clients.RestMapper.RESTMapping(gvk.GroupKind(), gvk.Version)
+	if err != nil {
+		return schema.GroupVersionResource{}, err
+	}
+
+	return m.Resource, nil
+}
+
+// removeUnnecessaryMetadata removes the metadata from the resource.
+func removeUnnecessaryMetadata(resource *unstructured.Unstructured) *unstructured.Unstructured {
+	resource.SetUID("")
+	resource.SetGeneration(0)
+	resource.SetResourceVersion("")
+
+	return resource
+}
+
+func (s *Service) addFilterBeforeCreating(gvr schema.GroupVersionResource, fn []FilteringFunction) {
+	if _, ok := s.filterBeforeCreating[gvr]; !ok {
+		s.filterBeforeCreating[gvr] = []FilteringFunction{}
+	}
+
+	s.filterBeforeCreating[gvr] = append(s.filterBeforeCreating[gvr], fn...)
+}
+
+func (s *Service) addMutateBeforeCreating(gvr schema.GroupVersionResource, fn []MutatingFunction) {
+	if _, ok := s.mutateBeforeCreating[gvr]; !ok {
+		s.mutateBeforeCreating[gvr] = []MutatingFunction{}
+	}
+
+	s.mutateBeforeCreating[gvr] = append(s.mutateBeforeCreating[gvr], fn...)
+}
+
+func (s *Service) addFilterBeforeUpdating(gvr schema.GroupVersionResource, fn []FilteringFunction) {
+	if _, ok := s.filterBeforeUpdating[gvr]; !ok {
+		s.filterBeforeUpdating[gvr] = []FilteringFunction{}
+	}
+
+	s.filterBeforeUpdating[gvr] = append(s.filterBeforeUpdating[gvr], fn...)
+}
+
+func (s *Service) addMutateBeforeUpdating(gvr schema.GroupVersionResource, fn []MutatingFunction) {
+	if _, ok := s.mutateBeforeUpdating[gvr]; !ok {
+		s.mutateBeforeUpdating[gvr] = []MutatingFunction{}
+	}
+
+	s.mutateBeforeUpdating[gvr] = append(s.mutateBeforeUpdating[gvr], fn...)
+}
diff --git a/simulator/server/di/di.go b/simulator/server/di/di.go
index 91fa28f3..e46ebfd4 100644
--- a/simulator/server/di/di.go
+++ b/simulator/server/di/di.go
@@ -14,6 +14,7 @@ import (
 
 	"sigs.k8s.io/kube-scheduler-simulator/simulator/oneshotimporter"
 	"sigs.k8s.io/kube-scheduler-simulator/simulator/reset"
+	"sigs.k8s.io/kube-scheduler-simulator/simulator/resourceapplier"
 	"sigs.k8s.io/kube-scheduler-simulator/simulator/resourcewatcher"
 	"sigs.k8s.io/kube-scheduler-simulator/simulator/scheduler"
 	"sigs.k8s.io/kube-scheduler-simulator/simulator/snapshot"
@@ -45,7 +46,7 @@ func NewDIContainer(
 	externalClient clientset.Interface,
 	externalDynamicClient dynamic.Interface,
 	simulatorPort int,
-	syncerOptions syncer.Options,
+	resourceapplierOptions resourceapplier.Options,
 ) (*Container, error) {
 	c := &Container{}
 
@@ -62,8 +63,9 @@ func NewDIContainer(
 		extSnapshotSvc := snapshot.NewService(externalClient, c.schedulerService)
 		c.oneshotClusterResourceImporter = oneshotimporter.NewService(snapshotSvc, extSnapshotSvc)
 	}
+	resourceApplierService := resourceapplier.New(dynamicClient, restMapper, resourceapplierOptions)
 	if resourceSyncEnabled {
-		c.resourceSyncer = syncer.New(externalDynamicClient, dynamicClient, restMapper, syncerOptions)
+		c.resourceSyncer = syncer.New(externalDynamicClient, resourceApplierService)
 	}
 	c.resourceWatcherService = resourcewatcher.NewService(client)
 
diff --git a/simulator/syncer/syncer.go b/simulator/syncer/syncer.go
index de5beb5f..36dd2a14 100644
--- a/simulator/syncer/syncer.go
+++ b/simulator/syncer/syncer.go
@@ -5,7 +5,6 @@ import (
 
 	"golang.org/x/xerrors"
 	"k8s.io/apimachinery/pkg/api/errors"
-	"k8s.io/apimachinery/pkg/api/meta"
 	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
 	"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
 	"k8s.io/apimachinery/pkg/runtime/schema"
@@ -13,64 +12,48 @@ import (
 	"k8s.io/client-go/dynamic/dynamicinformer"
 	"k8s.io/client-go/tools/cache"
 	"k8s.io/klog/v2"
-)
-
-type Service struct {
-	clients *Clients
 
-	gvrs               []schema.GroupVersionResource
-	mutatingFunctions  map[schema.GroupVersionResource][]MutatingFunction
-	filteringFunctions map[schema.GroupVersionResource][]FilteringFunction
-}
+	"sigs.k8s.io/kube-scheduler-simulator/simulator/resourceapplier"
+)
 
-// Note: Clients and its fields are exposed intentionally so that users can use it in MutatingFunction and FilteringFunction.
-type Clients struct {
-	// SrcDynamicClient is the dynamic client for the source cluster, which the resource is supposed to be copied from.
-	SrcDynamicClient dynamic.Interface
-	// DestDynamicClient is the dynamic client for the destination cluster, which the resource is supposed to be copied to.
-	DestDynamicClient dynamic.Interface
-	RestMapper        meta.RESTMapper
+// DefaultGVRs is a list of GroupVersionResource that we sync by default (configurable with Options),
+// which is a suitable resource set for the vanilla scheduler.
+//
+// Note that this order matters - When first importing resources, we want to sync namespaces first, then priorityclasses, storageclasses...
+var DefaultGVRs = []schema.GroupVersionResource{
+	{Group: "", Version: "v1", Resource: "namespaces"},
+	{Group: "scheduling.k8s.io", Version: "v1", Resource: "priorityclasses"},
+	{Group: "storage.k8s.io", Version: "v1", Resource: "storageclasses"},
+	{Group: "", Version: "v1", Resource: "persistentvolumeclaims"},
+	{Group: "", Version: "v1", Resource: "nodes"},
+	{Group: "", Version: "v1", Resource: "persistentvolumes"},
+	{Group: "", Version: "v1", Resource: "pods"},
 }
 
-type Options struct {
-	// GVRsToSync is a list of GroupVersionResource that will be synced.
-	// If GVRsToSync is nil, defaultGVRs are used.
-	GVRsToSync []schema.GroupVersionResource
-	// AdditionalMutatingFunctions is a list of mutating functions that users add.
-	AdditionalMutatingFunctions map[schema.GroupVersionResource]MutatingFunction
-	// AdditionalFilteringFunctions is a list of filtering functions that users add.
-	AdditionalFilteringFunctions map[schema.GroupVersionResource]FilteringFunction
+type Service struct {
+	gvrs                   []schema.GroupVersionResource
+	srcDynamicClient       dynamic.Interface
+	resourceApplierService *resourceapplier.Service
 }
 
-func New(srcDynamicClient, destDynamicClient dynamic.Interface, restMapper meta.RESTMapper, options Options) *Service {
+func New(srcDynamicClient dynamic.Interface, resourceApplierService *resourceapplier.Service) *Service {
 	s := &Service{
-		clients: &Clients{
-			SrcDynamicClient:  srcDynamicClient,
-			DestDynamicClient: destDynamicClient,
-			RestMapper:        restMapper,
-		},
-		gvrs:               DefaultGVRs,
-		mutatingFunctions:  map[schema.GroupVersionResource][]MutatingFunction{},
-		filteringFunctions: map[schema.GroupVersionResource][]FilteringFunction{},
+		gvrs:                   DefaultGVRs,
+		srcDynamicClient:       srcDynamicClient,
+		resourceApplierService: resourceApplierService,
 	}
 
-	if options.GVRsToSync != nil {
-		s.gvrs = options.GVRsToSync
+	if resourceApplierService.GVRsToSync != nil {
+		s.gvrs = resourceApplierService.GVRsToSync
 	}
 
-	s.addMutatingFunctoins(mandatoryMutatingFunctions)
-	s.addMutatingFunctoins(options.AdditionalMutatingFunctions)
-
-	s.addFilteringFunctoins(mandatoryFilteringFunctions)
-	s.addFilteringFunctoins(options.AdditionalFilteringFunctions)
-
 	return s
 }
 
 func (s *Service) Run(ctx context.Context) error {
 	klog.Info("Starting the cluster resource importer")
 
-	infFact := dynamicinformer.NewFilteredDynamicSharedInformerFactory(s.clients.SrcDynamicClient, 0, metav1.NamespaceAll, nil)
+	infFact := dynamicinformer.NewFilteredDynamicSharedInformerFactory(s.srcDynamicClient, 0, metav1.NamespaceAll, nil)
 	for _, gvr := range s.gvrs {
 		inf := infFact.ForResource(gvr).Informer()
 		_, err := inf.AddEventHandler(cache.ResourceEventHandlerFuncs{
@@ -90,133 +73,6 @@ func (s *Service) Run(ctx context.Context) error {
 	return nil
 }
 
-// createResourceOnDestinationCluster creates the resource on the destination cluster.
-func (s *Service) createResourceOnDestinationCluster(
-	ctx context.Context,
-	resource *unstructured.Unstructured,
-) error {
-	// Extract the GroupVersionResource from the Unstructured object
-	gvk := resource.GroupVersionKind()
-	gvr, err := s.findGVRForGVK(gvk)
-	if err != nil {
-		return err
-	}
-
-	// Namespaces resources should be created within the namespace defined in the Unstructured object
-	namespace := resource.GetNamespace()
-
-	// Run the filtering function for the resource.
-	if ok, err := s.filterResource(ctx, gvr, resource, Add); !ok || err != nil {
-		return err
-	}
-
-	// When creating a resource on the destination cluster, we must remove the metadata such as UID and Generation.
-	// It's done for all resources.
-	resource = removeUnnecessaryMetadata(resource)
-
-	// Run the mutating function for the resource.
-	resource, err = s.mutateResource(ctx, gvr, resource, Add)
-	if err != nil {
-		return xerrors.Errorf("failed to mutate resource: %w", err)
-	}
-
-	// Create the resource on the destination cluster using the dynamic client
-	_, err = s.clients.DestDynamicClient.Resource(gvr).Namespace(namespace).Create(
-		ctx,
-		resource,
-		metav1.CreateOptions{},
-	)
-	if err != nil {
-		return xerrors.Errorf("failed to create resource: %w", err)
-	}
-
-	return nil
-}
-
-func (s *Service) updateResourceOnDestinationCluster(
-	ctx context.Context,
-	resource *unstructured.Unstructured,
-) error {
-	// Extract the GroupVersionResource from the Unstructured object.
-	gvk := resource.GroupVersionKind()
-	gvr, err := s.findGVRForGVK(gvk)
-	if err != nil {
-		return err
-	}
-
-	// Namespaces resources should be created within the namespace defined in the Unstructured object.
-	namespace := resource.GetNamespace()
-
-	// Run the filtering function for the resource.
-	if ok, err := s.filterResource(ctx, gvr, resource, Update); !ok || err != nil {
-		return err
-	}
-
-	// Run the mutating function for the resource.
-	resource, err = s.mutateResource(ctx, gvr, resource, Update)
-	if err != nil {
-		return xerrors.Errorf("failed to mutate resource: %w", err)
-	}
-
-	// Create the resource on the destination cluster using the dynamic client
-	_, err = s.clients.DestDynamicClient.Resource(gvr).Namespace(namespace).Update(
-		ctx,
-		resource,
-		metav1.UpdateOptions{},
-	)
-	if err != nil {
-		return xerrors.Errorf("failed to create resource: %w", err)
-	}
-
-	return nil
-}
-
-// removeUnnecessaryMetadata removes the metadata from the resource.
-func removeUnnecessaryMetadata(resource *unstructured.Unstructured) *unstructured.Unstructured {
-	resource.SetUID("")
-	resource.SetGeneration(0)
-	resource.SetResourceVersion("")
-
-	return resource
-}
-
-func (s *Service) deleteResourceOnDestinationCluster(
-	ctx context.Context,
-	resource *unstructured.Unstructured,
-) error {
-	// Extract the GroupVersionResource from the Unstructured object
-	gvk := resource.GroupVersionKind()
-	gvr, err := s.findGVRForGVK(gvk)
-	if err != nil {
-		return err
-	}
-
-	// Namespaces resources should be created within the namespace defined in the Unstructured object
-	namespace := resource.GetNamespace()
-
-	// Create the resource on the destination cluster using the dynamic client
-	err = s.clients.DestDynamicClient.Resource(gvr).Namespace(namespace).Delete(
-		ctx,
-		resource.GetName(),
-		metav1.DeleteOptions{},
-	)
-	if err != nil {
-		return xerrors.Errorf("failed to delete resource: %w", err)
-	}
-
-	return nil
-}
-
-// findGVRForGVK uses the discovery client to get the GroupVersionResource for a given GroupVersionKind.
-func (s *Service) findGVRForGVK(gvk schema.GroupVersionKind) (schema.GroupVersionResource, error) {
-	m, err := s.clients.RestMapper.RESTMapping(gvk.GroupKind(), gvk.Version)
-	if err != nil {
-		return schema.GroupVersionResource{}, err
-	}
-
-	return m.Resource, nil
-}
-
 func (s *Service) addFunc(obj interface{}) {
 	ctx := context.Background()
 	unstructObj, ok := obj.(*unstructured.Unstructured)
@@ -225,7 +81,7 @@ func (s *Service) addFunc(obj interface{}) {
 		return
 	}
 
-	err := s.createResourceOnDestinationCluster(ctx, unstructObj)
+	err := s.resourceApplierService.Create(ctx, unstructObj)
 	if err != nil {
 		klog.ErrorS(err, "Failed to create resource on destination cluster")
 	}
@@ -239,7 +95,7 @@ func (s *Service) updateFunc(_, newObj interface{}) {
 		return
 	}
 
-	err := s.updateResourceOnDestinationCluster(ctx, unstructObj)
+	err := s.resourceApplierService.Update(ctx, unstructObj)
 	if err != nil {
 		if errors.IsNotFound(err) {
 			// We just ignore the not found error because the scheduler may preempt the Pods, or users may remove the resources for debugging.
@@ -258,7 +114,7 @@ func (s *Service) deleteFunc(obj interface{}) {
 		return
 	}
 
-	err := s.deleteResourceOnDestinationCluster(ctx, unstructObj)
+	err := s.resourceApplierService.Delete(ctx, unstructObj)
 	if err != nil {
 		if errors.IsNotFound(err) {
 			// We just ignore the not found error because the scheduler may preempt the Pods, or users may remove the resources for debugging.
@@ -268,59 +124,3 @@ func (s *Service) deleteFunc(obj interface{}) {
 		}
 	}
 }
-
-func (s *Service) addMutatingFunctoins(m map[schema.GroupVersionResource]MutatingFunction) {
-	for k, v := range m {
-		if s.mutatingFunctions[k] == nil {
-			s.mutatingFunctions[k] = []MutatingFunction{v}
-		} else {
-			s.mutatingFunctions[k] = append(s.mutatingFunctions[k], v)
-		}
-	}
-}
-
-func (s *Service) addFilteringFunctoins(m map[schema.GroupVersionResource]FilteringFunction) {
-	for k, v := range m {
-		if s.filteringFunctions[k] == nil {
-			s.filteringFunctions[k] = []FilteringFunction{v}
-		} else {
-			s.filteringFunctions[k] = append(s.filteringFunctions[k], v)
-		}
-	}
-}
-
-func (s *Service) mutateResource(ctx context.Context, gvr schema.GroupVersionResource, resource *unstructured.Unstructured, event Event) (*unstructured.Unstructured, error) {
-	mutatingFns, ok := s.mutatingFunctions[gvr]
-	if !ok {
-		return resource, nil
-	}
-
-	for _, mutatingFn := range mutatingFns {
-		var err error
-		resource, err = mutatingFn(ctx, resource, s.clients, event)
-		if err != nil {
-			return resource, err
-		}
-	}
-
-	return resource, nil
-}
-
-func (s *Service) filterResource(ctx context.Context, gvr schema.GroupVersionResource, resource *unstructured.Unstructured, event Event) (bool, error) {
-	filteringFns, ok := s.filteringFunctions[gvr]
-	if !ok {
-		return true, nil
-	}
-
-	for _, filteringFn := range filteringFns {
-		ok, err := filteringFn(ctx, resource, s.clients, event)
-		if err != nil {
-			return false, err
-		}
-		if !ok {
-			return false, nil
-		}
-	}
-
-	return true, nil
-}


Test patch:
diff --git a/simulator/resourceapplier/resourceapplier_test.go b/simulator/resourceapplier/resourceapplier_test.go
new file mode 100644
index 00000000..e3dc072b
--- /dev/null
+++ b/simulator/resourceapplier/resourceapplier_test.go
@@ -0,0 +1,592 @@
+package resourceapplier
+
+import (
+	"context"
+	"testing"
+
+	"github.com/google/go-cmp/cmp"
+	v1 "k8s.io/api/apps/v1"
+	corev1 "k8s.io/api/core/v1"
+	"k8s.io/apimachinery/pkg/api/errors"
+	"k8s.io/apimachinery/pkg/api/meta"
+	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
+	"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
+	"k8s.io/apimachinery/pkg/runtime"
+	"k8s.io/apimachinery/pkg/runtime/schema"
+	dynamicFake "k8s.io/client-go/dynamic/fake"
+	"k8s.io/client-go/restmapper"
+	scheduling "k8s.io/kubernetes/pkg/apis/scheduling/v1"
+	storage "k8s.io/kubernetes/pkg/apis/storage/v1"
+)
+
+func TestResourceApplier_createPods(t *testing.T) {
+	t.Parallel()
+
+	tests := []struct {
+		name           string
+		podToCreate    *corev1.Pod
+		podAfterCreate *corev1.Pod
+		filter         FilteringFunction
+		filtered       bool
+		wantErr        bool
+	}{
+		{
+			name: "create a Pod",
+			podToCreate: &corev1.Pod{
+				TypeMeta: metav1.TypeMeta{
+					Kind:       "Pod",
+					APIVersion: "v1",
+				},
+				ObjectMeta: metav1.ObjectMeta{
+					Name:      "pod-1",
+					Namespace: "default",
+				},
+				Spec: corev1.PodSpec{
+					Containers: []corev1.Container{
+						{
+							Name:  "container-1",
+							Image: "image-1",
+						},
+					},
+				},
+			},
+			podAfterCreate: &corev1.Pod{
+				TypeMeta: metav1.TypeMeta{
+					Kind:       "Pod",
+					APIVersion: "v1",
+				},
+				ObjectMeta: metav1.ObjectMeta{
+					Name:      "pod-1",
+					Namespace: "default",
+				},
+				Spec: corev1.PodSpec{
+					Containers: []corev1.Container{
+						{
+							Name:  "container-1",
+							Image: "image-1",
+						},
+					},
+				},
+			},
+			filter:   nil,
+			filtered: false,
+			wantErr:  false,
+		},
+		{
+			name: "create a Pod but it should not be created because of the filter",
+			podToCreate: &corev1.Pod{
+				TypeMeta: metav1.TypeMeta{
+					Kind:       "Pod",
+					APIVersion: "v1",
+				},
+				ObjectMeta: metav1.ObjectMeta{
+					Name:      "pod-1",
+					Namespace: "default",
+					Labels: map[string]string{
+						"ignore": "true",
+					},
+				},
+				Spec: corev1.PodSpec{
+					Containers: []corev1.Container{
+						{
+							Name:  "container-1",
+							Image: "image-1",
+						},
+					},
+				},
+			},
+			filter: func(_ context.Context, resource *unstructured.Unstructured, _ *Clients) (bool, error) {
+				if resource.GetLabels()["ignore"] == "true" {
+					return false, nil
+				}
+				return true, nil
+			},
+			filtered: true,
+			wantErr:  false,
+		},
+		{
+			name: "create a Pod and it should be pass the filter",
+			podToCreate: &corev1.Pod{
+				TypeMeta: metav1.TypeMeta{
+					Kind:       "Pod",
+					APIVersion: "v1",
+				},
+				ObjectMeta: metav1.ObjectMeta{
+					Name:      "pod-1",
+					Namespace: "default",
+				},
+				Spec: corev1.PodSpec{
+					Containers: []corev1.Container{
+						{
+							Name:  "container-1",
+							Image: "image-1",
+						},
+					},
+				},
+			},
+			podAfterCreate: &corev1.Pod{
+				TypeMeta: metav1.TypeMeta{
+					Kind:       "Pod",
+					APIVersion: "v1",
+				},
+				ObjectMeta: metav1.ObjectMeta{
+					Name:      "pod-1",
+					Namespace: "default",
+				},
+				Spec: corev1.PodSpec{
+					Containers: []corev1.Container{
+						{
+							Name:  "container-1",
+							Image: "image-1",
+						},
+					},
+				},
+			},
+			filter: func(_ context.Context, resource *unstructured.Unstructured, _ *Clients) (bool, error) {
+				if resource.GetLabels()["ignore"] == "true" {
+					return false, nil
+				}
+				return true, nil
+			},
+			filtered: false,
+			wantErr:  false,
+		},
+	}
+
+	for _, tt := range tests {
+		tt := tt
+		t.Run(tt.name, func(t *testing.T) {
+			t.Parallel()
+
+			client, mapper := prepare()
+
+			options := setFilter(tt.podToCreate.GroupVersionKind(), tt.filter, mapper)
+			service := New(client, mapper, options)
+
+			p, err := runtime.DefaultUnstructuredConverter.ToUnstructured(tt.podToCreate)
+			if err != nil {
+				t.Fatalf("failed to convert pod to unstructured: %v", err)
+			}
+			unstructedPod := &unstructured.Unstructured{Object: p}
+			err = service.Create(context.Background(), unstructedPod)
+			if (err != nil) != tt.wantErr {
+				t.Errorf("createPods() error = %v, wantErr %v", err, tt.wantErr)
+				return
+			}
+
+			got, err := getResource(tt.podToCreate.GroupVersionKind(), tt.podToCreate.Name, tt.podToCreate.Namespace, mapper, client)
+			if err != nil {
+				if tt.filtered && errors.IsNotFound(err) || tt.wantErr {
+					return
+				}
+				t.Fatalf("failed to get pod when comparing: %v", err)
+			} else if tt.filtered || tt.wantErr {
+				t.Fatalf("pod should not be created but it exists")
+			}
+
+			var gotPod corev1.Pod
+			err = runtime.DefaultUnstructuredConverter.FromUnstructured(got.UnstructuredContent(), &gotPod)
+			if err != nil {
+				t.Fatalf("failed to convert got unstructured to pod: %v", err)
+			}
+
+			if diff := cmp.Diff(*tt.podAfterCreate, gotPod); diff != "" {
+				t.Errorf("createPods() mismatch (-want +got):\n%s", diff)
+			}
+		})
+	}
+}
+
+func TestResourceApplier_updatePods(t *testing.T) {
+	t.Parallel()
+
+	tests := []struct {
+		name           string
+		originalPod    *corev1.Pod
+		updatePod      func(pod *corev1.Pod)
+		podAfterUpdate *corev1.Pod
+		wantErr        bool
+	}{
+		{
+			name: "update an unscheduled Pod",
+			originalPod: &corev1.Pod{
+				TypeMeta: metav1.TypeMeta{
+					Kind:       "Pod",
+					APIVersion: "v1",
+				},
+				ObjectMeta: metav1.ObjectMeta{
+					Name:      "pod-1",
+					Namespace: "default",
+				},
+				Spec: corev1.PodSpec{
+					Containers: []corev1.Container{
+						{
+							Name:  "container-1",
+							Image: "image-1",
+						},
+					},
+				},
+			},
+			updatePod: func(pod *corev1.Pod) {
+				pod.Spec.Containers[0].Image = "image-2"
+			},
+			podAfterUpdate: &corev1.Pod{
+				TypeMeta: metav1.TypeMeta{
+					Kind:       "Pod",
+					APIVersion: "v1",
+				},
+				ObjectMeta: metav1.ObjectMeta{
+					Name:      "pod-1",
+					Namespace: "default",
+				},
+				Spec: corev1.PodSpec{
+					Containers: []corev1.Container{
+						{
+							Name:  "container-1",
+							Image: "image-2",
+						},
+					},
+				},
+			},
+			wantErr: false,
+		},
+		{
+			name: "update an unscheduled Pod to be scheduled but it should not be updated",
+			originalPod: &corev1.Pod{
+				TypeMeta: metav1.TypeMeta{
+					Kind:       "Pod",
+					APIVersion: "v1",
+				},
+				ObjectMeta: metav1.ObjectMeta{
+					Name:      "pod-1",
+					Namespace: "default",
+				},
+				Spec: corev1.PodSpec{
+					Containers: []corev1.Container{
+						{
+							Name:  "container-1",
+							Image: "image-1",
+						},
+					},
+				},
+			},
+			updatePod: func(pod *corev1.Pod) {
+				pod.Spec.NodeName = "node-1"
+			},
+			podAfterUpdate: &corev1.Pod{
+				TypeMeta: metav1.TypeMeta{
+					Kind:       "Pod",
+					APIVersion: "v1",
+				},
+				ObjectMeta: metav1.ObjectMeta{
+					Name:      "pod-1",
+					Namespace: "default",
+				},
+				Spec: corev1.PodSpec{
+					Containers: []corev1.Container{
+						{
+							Name:  "container-1",
+							Image: "image-1",
+						},
+					},
+				},
+			},
+		},
+		{
+			name: "update a scheduled Pod but it should not be updated",
+			originalPod: &corev1.Pod{
+				TypeMeta: metav1.TypeMeta{
+					Kind:       "Pod",
+					APIVersion: "v1",
+				},
+				ObjectMeta: metav1.ObjectMeta{
+					Name:      "pod-1",
+					Namespace: "default",
+				},
+				Spec: corev1.PodSpec{
+					NodeName: "node-1",
+					Containers: []corev1.Container{
+						{
+							Name:  "container-1",
+							Image: "image-1",
+						},
+					},
+				},
+			},
+			updatePod: func(pod *corev1.Pod) {
+				pod.Spec.Containers[0].Image = "image-2"
+			},
+			podAfterUpdate: &corev1.Pod{
+				TypeMeta: metav1.TypeMeta{
+					Kind:       "Pod",
+					APIVersion: "v1",
+				},
+				ObjectMeta: metav1.ObjectMeta{
+					Name:      "pod-1",
+					Namespace: "default",
+				},
+				Spec: corev1.PodSpec{
+					NodeName: "node-1",
+					Containers: []corev1.Container{
+						{
+							Name:  "container-1",
+							Image: "image-1",
+						},
+					},
+				},
+			},
+			wantErr: false,
+		},
+	}
+
+	for _, tt := range tests {
+		tt := tt
+		t.Run(tt.name, func(t *testing.T) {
+			t.Parallel()
+
+			client, mapper := prepare()
+			service := New(client, mapper, Options{})
+
+			p, err := runtime.DefaultUnstructuredConverter.ToUnstructured(tt.originalPod)
+			if err != nil {
+				t.Fatalf("failed to convert pod to unstructured: %v", err)
+			}
+			unstructedPod := &unstructured.Unstructured{Object: p}
+			err = service.Create(context.Background(), unstructedPod)
+			if (err != nil) != tt.wantErr {
+				t.Errorf("updatePods() error = %v, wantErr %v", err, tt.wantErr)
+				return
+			}
+
+			tt.updatePod(tt.originalPod)
+			p, err = runtime.DefaultUnstructuredConverter.ToUnstructured(tt.originalPod)
+			if err != nil {
+				t.Fatalf("failed to convert pod to unstructured: %v", err)
+			}
+			unstructedPod = &unstructured.Unstructured{Object: p}
+
+			err = service.Update(context.Background(), unstructedPod)
+			if (err != nil) != tt.wantErr {
+				t.Errorf("updatePods() error = %v, wantErr %v", err, tt.wantErr)
+				return
+			}
+
+			got, err := getResource(tt.originalPod.GroupVersionKind(), tt.originalPod.Name, tt.originalPod.Namespace, mapper, client)
+			if err != nil {
+				t.Fatalf("failed to get pod when comparing: %v", err)
+			}
+			var gotPod corev1.Pod
+			err = runtime.DefaultUnstructuredConverter.FromUnstructured(got.UnstructuredContent(), &gotPod)
+			if err != nil {
+				t.Fatalf("failed to convert got unstructured to pod: %v", err)
+			}
+
+			if diff := cmp.Diff(*tt.podAfterUpdate, gotPod); diff != "" {
+				t.Errorf("updatePods() mismatch (-want +got):\n%s", diff)
+			}
+		})
+	}
+}
+
+func TestResourceApplier_deletePods(t *testing.T) {
+	t.Parallel()
+
+	tests := []struct {
+		name    string
+		pod     *corev1.Pod
+		wantErr bool
+	}{
+		{
+			name: "delete a Pod",
+			pod: &corev1.Pod{
+				TypeMeta: metav1.TypeMeta{
+					Kind:       "Pod",
+					APIVersion: "v1",
+				},
+				ObjectMeta: metav1.ObjectMeta{
+					Name:      "pod-1",
+					Namespace: "default",
+				},
+				Spec: corev1.PodSpec{
+					Containers: []corev1.Container{
+						{
+							Name:  "container-1",
+							Image: "image-1",
+						},
+					},
+				},
+			},
+			wantErr: false,
+		},
+	}
+
+	for _, tt := range tests {
+		tt := tt
+		t.Run(tt.name, func(t *testing.T) {
+			t.Parallel()
+
+			client, mapper := prepare()
+			service := New(client, mapper, Options{})
+
+			p, err := runtime.DefaultUnstructuredConverter.ToUnstructured(tt.pod)
+			if err != nil {
+				t.Fatalf("failed to convert pod to unstructured: %v", err)
+			}
+			unstructedPod := &unstructured.Unstructured{Object: p}
+			err = service.Create(context.Background(), unstructedPod)
+			if (err != nil) != tt.wantErr {
+				t.Errorf("deletePods() error = %v, wantErr %v", err, tt.wantErr)
+				return
+			}
+
+			err = service.Delete(context.Background(), unstructedPod)
+			if (err != nil) != tt.wantErr {
+				t.Errorf("deletePods() error = %v, wantErr %v", err, tt.wantErr)
+				return
+			}
+
+			_, err = getResource(tt.pod.GroupVersionKind(), tt.pod.Name, tt.pod.Namespace, mapper, client)
+			if err == nil {
+				t.Fatalf("pod should be deleted but it still exists")
+			}
+
+			if !errors.IsNotFound(err) && !tt.wantErr {
+				t.Fatalf("failed to check if pod is deleted: %v", err)
+			}
+		})
+	}
+}
+
+func TestResourceApplier_createNodes(t *testing.T) {
+	t.Parallel()
+
+	tests := []struct {
+		name           string
+		nodeToApply    *corev1.Node
+		nodeAfterApply *corev1.Node
+		wantErr        bool
+	}{
+		{
+			name: "create a Node",
+			nodeToApply: &corev1.Node{
+				TypeMeta: metav1.TypeMeta{
+					Kind:       "Node",
+					APIVersion: "v1",
+				},
+				ObjectMeta: metav1.ObjectMeta{
+					Name: "node-1",
+				},
+			},
+			nodeAfterApply: &corev1.Node{
+				TypeMeta: metav1.TypeMeta{
+					Kind:       "Node",
+					APIVersion: "v1",
+				},
+				ObjectMeta: metav1.ObjectMeta{
+					Name: "node-1",
+				},
+			},
+			wantErr: false,
+		},
+	}
+
+	for _, tt := range tests {
+		tt := tt
+		t.Run(tt.name, func(t *testing.T) {
+			t.Parallel()
+
+			client, mapper := prepare()
+			service := New(client, mapper, Options{})
+
+			n, err := runtime.DefaultUnstructuredConverter.ToUnstructured(tt.nodeToApply)
+			if err != nil {
+				t.Fatalf("failed to convert node to unstructured: %v", err)
+			}
+			unstructedNode := &unstructured.Unstructured{Object: n}
+			err = service.Create(context.Background(), unstructedNode)
+			if (err != nil) != tt.wantErr {
+				t.Errorf("createNode() error = %v, wantErr %v", err, tt.wantErr)
+				return
+			}
+
+			got, err := getResource(tt.nodeToApply.GroupVersionKind(), tt.nodeToApply.Name, tt.nodeToApply.Namespace, mapper, client)
+			if err != nil {
+				t.Fatalf("failed to get node when comparing: %v", err)
+			}
+			var gotNode corev1.Node
+			err = runtime.DefaultUnstructuredConverter.FromUnstructured(got.UnstructuredContent(), &gotNode)
+			if err != nil {
+				t.Fatalf("failed to convert got unstructured to node: %v", err)
+			}
+
+			if diff := cmp.Diff(*tt.nodeAfterApply, gotNode); diff != "" {
+				t.Errorf("createNode() mismatch (-want +got):\n %s", diff)
+				return
+			}
+		})
+	}
+}
+
+func prepare() (*dynamicFake.FakeDynamicClient, meta.RESTMapper) {
+	s := runtime.NewScheme()
+	v1.AddToScheme(s)
+	scheduling.AddToScheme(s)
+	storage.AddToScheme(s)
+	client := dynamicFake.NewSimpleDynamicClient(s)
+	resources := []*restmapper.APIGroupResources{
+		{
+			Group: metav1.APIGroup{
+				Versions: []metav1.GroupVersionForDiscovery{
+					{Version: "v1"},
+				},
+			},
+			VersionedResources: map[string][]metav1.APIResource{
+				"v1": {
+					{Name: "pods", Namespaced: true, Kind: "Pod"},
+				},
+			},
+		},
+		{
+			Group: metav1.APIGroup{
+				Versions: []metav1.GroupVersionForDiscovery{
+					{Version: "v1"},
+				},
+			},
+			VersionedResources: map[string][]metav1.APIResource{
+				"v1": {
+					{Name: "nodes", Namespaced: false, Kind: "Node"},
+				},
+			},
+		},
+	}
+
+	mapper := restmapper.NewDiscoveryRESTMapper(resources)
+	return client, mapper
+}
+
+func getResource(gvk schema.GroupVersionKind, name, namespace string, mapper meta.RESTMapper, client *dynamicFake.FakeDynamicClient) (*unstructured.Unstructured, error) {
+	mapping, err := mapper.RESTMapping(gvk.GroupKind(), gvk.Version)
+	if err != nil {
+		return nil, err
+	}
+
+	resource := client.Resource(mapping.Resource).Namespace(namespace)
+	return resource.Get(context.Background(), name, metav1.GetOptions{})
+}
+
+func setFilter(gvk schema.GroupVersionKind, filter FilteringFunction, mapper meta.RESTMapper) Options {
+	if filter == nil {
+		return Options{}
+	}
+
+	m, err := mapper.RESTMapping(gvk.GroupKind(), gvk.Version)
+	if err != nil {
+		panic(err)
+	}
+
+	return Options{
+		FilterBeforeCreating: map[schema.GroupVersionResource][]FilteringFunction{
+			m.Resource: {filter},
+		},
+	}
+}
diff --git a/simulator/syncer/syncer_test.go b/simulator/syncer/syncer_test.go
index dc7c0300..a34efc16 100644
--- a/simulator/syncer/syncer_test.go
+++ b/simulator/syncer/syncer_test.go
@@ -19,6 +19,8 @@ import (
 	"k8s.io/client-go/restmapper"
 	scheduling "k8s.io/kubernetes/pkg/apis/scheduling/v1"
 	storage "k8s.io/kubernetes/pkg/apis/storage/v1"
+
+	"sigs.k8s.io/kube-scheduler-simulator/simulator/resourceapplier"
 )
 
 //nolint:gocognit // it is because of huge test cases.
@@ -389,7 +391,8 @@ func TestSyncerWithPod(t *testing.T) {
 				},
 			}
 			mapper := restmapper.NewDiscoveryRESTMapper(resources)
-			service := New(src, dest, mapper, Options{})
+			resourceApplier := resourceapplier.New(dest, mapper, resourceapplier.Options{})
+			service := New(src, resourceApplier)
 
 			ctx, cancel := context.WithCancel(context.Background())