Repository: openyurtio/openyurt
Pull request #270 (2021-04-20 10:08:44)

feature: add dns controller to sync cluster node dns records
<!-- 
Please make sure you have read and understood the contributing guidelines;
https://github.com/openyurtio/openyurt/blob/master/CONTRIBUTING.md -->

### Ⅰ. Describe what this PR does
In the edge computing scenario, the IP addresses of the edge nodes are likely to be the same. So we can not rely on the node IP to forward the request but should use the node hostname(unique in one cluster). 
This PR provides the ability for the tunnel-server to handle requests in the form of `scheme://[hostname]:[port]/[req_path]`.

### How to use it
The dns controller will generate a ConfigMap named `yurt-tunnel-nodes` in the `kube-system` namespace.
You need to manually config CoreDNS to use it according to the steps like the following:
First, mount the ConfigMap as volume of CoreDNS Pod, for example
```yaml
        volumeMounts:
        - mountPath: /etc/edge
          name: hosts
          readOnly: true
...

      volumes:
      - configMap:
          defaultMode: 420
          name: yurt-tunnel-nodes
        name: hosts
```
Second, update CoreDNS ConfigMap (`kube-system/coredns`), add the `hosts` section
```yaml
apiVersion: v1
data:
  Corefile: |
    .:53 {
        errors
        health :10260
        hosts /etc/edge/tunnel-nodes {
            reload 300ms
            fallthrough
        }
        kubernetes cluster.local in-addr.arpa ip6.arpa {
           pods insecure
           upstream
           fallthrough in-addr.arpa ip6.arpa
           endpoint http://169.254.2.1:10261
        }
        prometheus :9153
        proxy . /etc/resolv.conf
        cache 30
        reload
    }
kind: ConfigMap
metadata:
  name: coredns
  namespace: kube-system
```

### Ⅱ. Does this pull request fix one issue?
<!--If so, add "fixes #xxxx" below in the next line, for example, fixes #15. Otherwise, add "NONE" -->


### Ⅲ. List the added test cases (unit test/integration test) if any, please explain if no tests are needed.


### Ⅳ. Describe how to verify it


### Ⅴ. Special notes for reviews




rambohe-ch: @SataQiu Thank you for making the pull request.
would you add more detail info like background, reason, solution etc to explain why we need this pull request?

Patch:
diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml
index 2add7cf7..2c187171 100644
--- a/.github/workflows/ci.yaml
+++ b/.github/workflows/ci.yaml
@@ -51,18 +51,18 @@ jobs:
         run: find  ./* -name  "*"  | xargs misspell -error
       - name: Lint markdown files
         run: find  ./ -name  "*.md" | grep -v enhancements |  grep -v .github |  xargs mdl -r ~MD010,~MD013,~MD022,~MD024,~MD029,~MD031,~MD032,~MD033,~MD034,~MD036
-#      - name: Check markdown links
-#        run: |
-#          set +e
-#          for name in $(find . -name \*.md | grep -v CHANGELOG); do
-#            if [ -f $name ]; then
-#              markdown-link-check -q $name -c .github/workflows/markdown-link-check.config.json;
-#              if [ $? -ne 0 ]; then
-#                code=1
-#              fi
-#            fi
-#          done
-#          bash -c "exit $code";
+      - name: Check markdown links
+        run: |
+          set +e
+          for name in $(find . -name \*.md | grep -v CHANGELOG); do
+            if [ -f $name ]; then
+              markdown-link-check -q $name -c .github/workflows/markdown-link-check.config.json;
+              if [ $? -ne 0 ]; then
+                code=1
+              fi
+            fi
+          done
+          bash -c "exit $code";
 
   unit-tests:
     runs-on: ubuntu-18.04
diff --git a/Makefile b/Makefile
index c2281f8f..047d0866 100644
--- a/Makefile
+++ b/Makefile
@@ -1,11 +1,11 @@
 # Copyright 2020 The OpenYurt 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.
@@ -17,16 +17,16 @@
 all: test build
 
 # Build binaries in the host environment
-build:
+build: 
 	bash hack/make-rules/build.sh $(WHAT)
 
-# generate yaml files
+# generate yaml files 
 gen-yaml:
 	hack/make-rules/genyaml.sh $(WHAT)
 
 # Run test
 test: fmt vet
-	go test -v ./pkg/... ./cmd/... -coverprofile cover.out
+	go test ./pkg/... ./cmd/... -coverprofile cover.out
 	go test -v  -coverpkg=./pkg/yurttunnel/...  -coverprofile=yurttunnel-cover.out ./test/integration/yurttunnel_test.go
 
 # Run go fmt against code
@@ -37,7 +37,7 @@ fmt:
 vet:
 	go vet ./pkg/... ./cmd/...
 
-# Build binaries and docker images.
+# Build binaries and docker images.  
 # NOTE: this rule can take time, as we build binaries inside containers
 #
 # ARGS:
@@ -47,18 +47,18 @@ vet:
 #   	set it as cn.
 #
 # Examples:
-#   # compile yurthub, yurt-controller-manager and yurtctl-servant with
+#   # compile yurthub, yurt-controller-manager and yurtctl-servant with 
 #   # architectures arm64 and arm in the mainland China
 #   make release WHAT="yurthub yurt-controller-manager yurtctl-servant" ARCH="arm64 arm" REGION=cn
 #
 #   # compile all components with all architectures (i.e., amd64, arm64, arm)
-#   make relase
+#   make relase 
 release:
 	bash hack/make-rules/release-images.sh
 
-clean:
+clean: 
 	-rm -Rf _output
 	-rm -Rf dockerbuild
 
-e2e:
-	hack/make-rules/build-e2e.sh
+e2e: 
+	hack/make-rules/build-e2e.sh 
diff --git a/cmd/yurt-tunnel-server/app/config/config.go b/cmd/yurt-tunnel-server/app/config/config.go
index 25961c45..f8940a7c 100644
--- a/cmd/yurt-tunnel-server/app/config/config.go
+++ b/cmd/yurt-tunnel-server/app/config/config.go
@@ -28,7 +28,9 @@ import (
 type Config struct {
 	EgressSelectorEnabled       bool
 	EnableIptables              bool
+	EnableDNSController         bool
 	IptablesSyncPeriod          int
+	DNSSyncPeriod               int
 	CertDNSNames                []string
 	CertIPs                     []net.IP
 	ListenAddrForAgent          string
diff --git a/cmd/yurt-tunnel-server/app/options/options.go b/cmd/yurt-tunnel-server/app/options/options.go
index 1c733589..aa729751 100644
--- a/cmd/yurt-tunnel-server/app/options/options.go
+++ b/cmd/yurt-tunnel-server/app/options/options.go
@@ -43,8 +43,10 @@ type ServerOptions struct {
 	CertIPs                string
 	Version                bool
 	EnableIptables         bool
+	EnableDNSController    bool
 	EgressSelectorEnabled  bool
 	IptablesSyncPeriod     int
+	DNSSyncPeriod          int
 	TunnelAgentConnectPort string
 	SecurePort             string
 	InsecurePort           string
@@ -59,7 +61,9 @@ func NewServerOptions() *ServerOptions {
 		BindAddr:               "0.0.0.0",
 		InsecureBindAddr:       "127.0.0.1",
 		EnableIptables:         true,
+		EnableDNSController:    true,
 		IptablesSyncPeriod:     60,
+		DNSSyncPeriod:          1800,
 		ServerCount:            1,
 		TunnelAgentConnectPort: constants.YurttunnelServerAgentPort,
 		SecurePort:             constants.YurttunnelServerMasterPort,
@@ -88,8 +92,10 @@ func (o *ServerOptions) AddFlags(fs *pflag.FlagSet) {
 	fs.StringVar(&o.CertDNSNames, "cert-dns-names", o.CertDNSNames, "DNS names that will be added into server's certificate. (e.g., dns1,dns2)")
 	fs.StringVar(&o.CertIPs, "cert-ips", o.CertIPs, "IPs that will be added into server's certificate. (e.g., ip1,ip2)")
 	fs.BoolVar(&o.EnableIptables, "enable-iptables", o.EnableIptables, "If allow iptable manager to set the dnat rule.")
+	fs.BoolVar(&o.EnableDNSController, "enable-dns-controller", o.EnableDNSController, "If allow DNS controller to set the dns rules.")
 	fs.BoolVar(&o.EgressSelectorEnabled, "egress-selector-enable", o.EgressSelectorEnabled, "If the apiserver egress selector has been enabled.")
 	fs.IntVar(&o.IptablesSyncPeriod, "iptables-sync-period", o.IptablesSyncPeriod, "The synchronization period of the iptable manager.")
+	fs.IntVar(&o.DNSSyncPeriod, "dns-sync-period", o.DNSSyncPeriod, "The synchronization period of the DNS controller.")
 	fs.IntVar(&o.ServerCount, "server-count", o.ServerCount, "The number of proxy server instances, should be 1 unless it is an HA server.")
 	fs.StringVar(&o.ProxyStrategy, "proxy-strategy", o.ProxyStrategy, "The strategy of proxying requests from tunnel server to agent.")
 	fs.StringVar(&o.TunnelAgentConnectPort, "tunnel-agent-connect-port", o.TunnelAgentConnectPort, "The port on which to serve tcp packets from tunnel agent")
@@ -103,7 +109,9 @@ func (o *ServerOptions) Config() (*config.Config, error) {
 	cfg := &config.Config{
 		EgressSelectorEnabled: o.EgressSelectorEnabled,
 		EnableIptables:        o.EnableIptables,
+		EnableDNSController:   o.EnableDNSController,
 		IptablesSyncPeriod:    o.IptablesSyncPeriod,
+		DNSSyncPeriod:         o.DNSSyncPeriod,
 		CertDNSNames:          make([]string, 0),
 		CertIPs:               make([]net.IP, 0),
 		ServerCount:           o.ServerCount,
diff --git a/cmd/yurt-tunnel-server/app/start.go b/cmd/yurt-tunnel-server/app/start.go
index bf9f1500..b5b2b7b8 100644
--- a/cmd/yurt-tunnel-server/app/start.go
+++ b/cmd/yurt-tunnel-server/app/start.go
@@ -24,6 +24,7 @@ import (
 	"github.com/openyurtio/openyurt/cmd/yurt-tunnel-server/app/options"
 	"github.com/openyurtio/openyurt/pkg/projectinfo"
 	"github.com/openyurtio/openyurt/pkg/yurttunnel/constants"
+	"github.com/openyurtio/openyurt/pkg/yurttunnel/dns"
 	"github.com/openyurtio/openyurt/pkg/yurttunnel/handlerwrapper/initializer"
 	"github.com/openyurtio/openyurt/pkg/yurttunnel/handlerwrapper/wraphandler"
 	"github.com/openyurtio/openyurt/pkg/yurttunnel/iptables"
@@ -73,6 +74,17 @@ func NewYurttunnelServerCommand(stopCh <-chan struct{}) *cobra.Command {
 
 // run starts the yurttunel-server
 func Run(cfg *config.CompletedConfig, stopCh <-chan struct{}) error {
+	// 0. start the DNS controller
+	if cfg.EnableDNSController {
+		dnsController, err := dns.NewCoreDNSRecordController(cfg.Client,
+			cfg.SharedInformerFactory,
+			cfg.ListenInsecureAddrForMaster,
+			cfg.DNSSyncPeriod)
+		if err != nil {
+			return fmt.Errorf("fail to create a new dnsController, %v", err)
+		}
+		go dnsController.Run(stopCh)
+	}
 	// 1. start the IP table manager
 	if cfg.EnableIptables {
 		iptablesMgr := iptables.NewIptablesManager(cfg.Client,
diff --git a/config/setup/yurt-tunnel-server.yaml b/config/setup/yurt-tunnel-server.yaml
index 2b8141d3..53013cee 100644
--- a/config/setup/yurt-tunnel-server.yaml
+++ b/config/setup/yurt-tunnel-server.yaml
@@ -127,6 +127,7 @@ spec:
         - yurt-tunnel-server
         args:
         - --bind-address=$(NODE_IP)
+        - --insecure-bind-address=$(NODE_IP)
         - --proxy-strategy=destHost
         - --v=2
         env:
diff --git a/config/yaml-template/yurt-tunnel-server.yaml b/config/yaml-template/yurt-tunnel-server.yaml
index f566f963..8938ddad 100644
--- a/config/yaml-template/yurt-tunnel-server.yaml
+++ b/config/yaml-template/yurt-tunnel-server.yaml
@@ -127,6 +127,7 @@ spec:
         - __project_prefix__-tunnel-server
         args:
         - --bind-address=$(NODE_IP)
+        - --insecure-bind-address=$(NODE_IP)
         - --proxy-strategy=destHost
         - --v=2
         env:
diff --git a/pkg/yurtctl/constants/yurt-tunnel-server-tmpl.go b/pkg/yurtctl/constants/yurt-tunnel-server-tmpl.go
index c26129c7..e49bf00e 100644
--- a/pkg/yurtctl/constants/yurt-tunnel-server-tmpl.go
+++ b/pkg/yurtctl/constants/yurt-tunnel-server-tmpl.go
@@ -152,6 +152,7 @@ spec:
         - yurt-tunnel-server
         args:
         - --bind-address=$(NODE_IP)
+        - --insecure-bind-address=$(NODE_IP)
         - --server-count=1
         env:
         - name: NODE_IP
diff --git a/pkg/yurthub/cachemanager/cache_manager.go b/pkg/yurthub/cachemanager/cache_manager.go
index 0ee44d96..df05c938 100644
--- a/pkg/yurthub/cachemanager/cache_manager.go
+++ b/pkg/yurthub/cachemanager/cache_manager.go
@@ -55,10 +55,9 @@ type CacheManager interface {
 
 type cacheManager struct {
 	sync.RWMutex
-	storage               StorageWrapper
-	serializerManager     *serializer.SerializerManager
-	cacheAgents           map[string]bool
-	listSelectorCollector map[string]string
+	storage           StorageWrapper
+	serializerManager *serializer.SerializerManager
+	cacheAgents       map[string]bool
 }
 
 // NewCacheManager creates a new CacheManager
@@ -67,10 +66,9 @@ func NewCacheManager(
 	serializerMgr *serializer.SerializerManager,
 ) (CacheManager, error) {
 	cm := &cacheManager{
-		storage:               storage,
-		serializerManager:     serializerMgr,
-		cacheAgents:           make(map[string]bool),
-		listSelectorCollector: make(map[string]string),
+		storage:           storage,
+		serializerManager: serializerMgr,
+		cacheAgents:       make(map[string]bool),
 	}
 
 	err := cm.initCacheAgents()
@@ -144,30 +142,17 @@ func (cm *cacheManager) queryListObject(req *http.Request) (runtime.Object, erro
 		return nil, err
 	}
 
-	var gvk schema.GroupVersionKind
-	var kind string
 	objs, err := cm.storage.List(key)
 	if err != nil {
 		return nil, err
 	} else if len(objs) == 0 {
-		gvk, err = serializer.UnsafeDefaultRESTMapper.KindFor(schema.GroupVersionResource{
-			Group:    info.APIGroup,
-			Version:  info.APIVersion,
-			Resource: info.Resource,
-		})
-		if err != nil {
-			return nil, err
-		}
-		kind = gvk.Kind
-	} else {
-		kind = objs[0].GetObjectKind().GroupVersionKind().Kind
+		return nil, nil
 	}
-
 	var listObj runtime.Object
 	listGvk := schema.GroupVersionKind{
 		Group:   info.APIGroup,
 		Version: info.APIVersion,
-		Kind:    kind + "List",
+		Kind:    objs[0].GetObjectKind().GroupVersionKind().Kind + "List",
 	}
 	if scheme.Scheme.Recognizes(listGvk) {
 		listObj, err = scheme.Scheme.New(listGvk)
@@ -319,12 +304,14 @@ func (cm *cacheManager) saveListObject(ctx context.Context, info *apirequest.Req
 	}
 
 	list, err := serializer.DecodeResp(serializers, b, reqContentType, respContentType)
-	if err != nil || list == nil {
+	if err != nil {
 		klog.Errorf("failed to decode response in saveOneObject %v", err)
 		return err
 	}
 
-	if _, ok := list.(*metav1.Status); ok {
+	switch list.(type) {
+	case *metav1.Status:
+		// it's not need to cache for status
 		klog.Infof("it's not need to cache metav1.Status")
 		return nil
 	}
@@ -349,48 +336,48 @@ func (cm *cacheManager) saveListObject(ctx context.Context, info *apirequest.Req
 	// in local disk, so when cloud-edge network disconnected,
 	// yurthub can return empty objects instead of 404 code(not found)
 	if len(items) == 0 {
-		// list returns no objects
 		key, _ := util.KeyFunc(comp, info.Resource, info.Namespace, "")
 		return cm.storage.Create(key, nil)
-	} else if info.Name != "" {
-		// list with fieldSelector=metadata.name=xxx
-		if len(items) != 1 {
-			return fmt.Errorf("%s with fieldSelector=metadata.name=%s, but return more than one objects: %d", util.ReqInfoString(info), info.Name, len(items))
+	}
+
+	var errs []error
+	for i := range items {
+		name, err := accessor.Name(items[i])
+		if err != nil || name == "" {
+			klog.Errorf("failed to get name of list items object, %v", err)
+			continue
 		}
-		accessor.SetKind(items[0], kind)
-		accessor.SetAPIVersion(items[0], apiVersion)
-		name, _ := accessor.Name(items[0])
-		ns, _ := accessor.Namespace(items[0])
-		if ns == "" {
+
+		ns, err := accessor.Namespace(items[i])
+		if err != nil {
+			klog.Errorf("failed to get namespace of list items object, %v", err)
+			continue
+		} else if ns == "" {
 			ns = info.Namespace
 		}
-		key, _ := util.KeyFunc(comp, info.Resource, ns, name)
-		err = cm.saveOneObjectWithValidation(key, items[0])
-		if err == storage.ErrStorageAccessConflict {
-			klog.V(2).Infof("skip to cache list object because key(%s) is under processing", key)
-			return nil
-		}
 
-		return err
-	} else {
-		// list all of objects or fieldselector/labelselector
-		rootKey, _ := util.KeyFunc(comp, info.Resource, info.Namespace, info.Name)
-		objs := make(map[string]runtime.Object)
-		for i := range items {
-			accessor.SetKind(items[i], kind)
-			accessor.SetAPIVersion(items[i], apiVersion)
-			name, _ := accessor.Name(items[i])
-			ns, _ := accessor.Namespace(items[i])
-			if ns == "" {
-				ns = info.Namespace
-			}
+		klog.V(5).Infof("path for list item(%d): %s/%s/%s/%s", i, comp, info.Resource, ns, name)
+		key, err := util.KeyFunc(comp, info.Resource, ns, name)
+		if err != nil || key == "" {
+			klog.Errorf("failed to get cache key(%s:%s:%s:%s), %v", comp, info.Resource, ns, name, err)
+			return err
+		}
 
-			key, _ := util.KeyFunc(comp, info.Resource, ns, name)
-			objs[key] = items[i]
+		accessor.SetKind(items[i], kind)
+		accessor.SetAPIVersion(items[i], apiVersion)
+		err = cm.saveOneObjectWithValidation(key, items[i])
+		if err == storage.ErrStorageAccessConflict {
+			klog.V(2).Infof("skip to cache list object because key(%s) is under processing", key)
+		} else if err != nil {
+			errs = append(errs, fmt.Errorf("failed to save object(%s), %v", key, err))
 		}
+	}
 
-		return cm.storage.Replace(rootKey, objs)
+	if len(errs) != 0 {
+		return fmt.Errorf("failed to save list object, %#+v", errs)
 	}
+
+	return nil
 }
 
 func (cm *cacheManager) saveOneObject(ctx context.Context, info *apirequest.RequestInfo, b []byte) error {
@@ -410,11 +397,14 @@ func (cm *cacheManager) saveOneObject(ctx context.Context, info *apirequest.Requ
 		klog.Errorf("failed to decode response in saveOneObject(reqContentType:%s, respContentType:%s): %s, %v", reqContentType, respContentType, util.ReqInfoString(info), err)
 		return err
 	} else if obj == nil {
-		klog.Info("failed to decode nil object. skip cache")
-		return nil
-	} else if _, ok := obj.(*metav1.Status); ok {
 		klog.Infof("it's not need to cache metav1.Status.")
 		return nil
+	} else {
+		switch obj.(type) {
+		case *metav1.Status:
+			// it's not need to cache for status
+			return nil
+		}
 	}
 
 	var name string
@@ -545,37 +535,5 @@ func (cm *cacheManager) CanCacheFor(req *http.Request) bool {
 		return false
 	}
 
-	cm.Lock()
-	defer cm.Unlock()
-	if info.Verb == "list" && info.Name == "" {
-		key, _ := util.KeyFunc(comp, info.Resource, info.Namespace, info.Name)
-		selector, _ := util.ListSelectorFrom(ctx)
-		if oldSelector, ok := cm.listSelectorCollector[key]; ok {
-			if oldSelector != selector {
-				// list requests that have the same path but with different selector, for example:
-				// request1: http://{ip:port}/api/v1/default/pods?labelSelector=foo=bar
-				// request2: http://{ip:port}/api/v1/default/pods?labelSelector=foo2=bar2
-				// because func queryListObject() will get all pods for both requests instead of
-				// getting pods by request selector. so cache manager can not support same path list
-				// requests that has different selector.
-				return false
-			}
-		} else {
-			// list requests that get the same resources but with different path, for example:
-			// request1: http://{ip/port}/api/v1/pods?fieldSelector=spec.nodeName=foo
-			// request2: http://{ip/port}/api/v1/default/pods?fieldSelector=spec.nodeName=foo
-			// because func queryListObject() will get all pods for both requests instead of
-			// getting pods by request selector. so cache manager can not support getting same resource
-			// list requests that has different path.
-			for k := range cm.listSelectorCollector {
-				if len(k) > len(key) && strings.Contains(k, key) {
-					return false
-				} else if len(k) < len(key) && strings.Contains(key, k) {
-					return false
-				}
-			}
-			cm.listSelectorCollector[key] = selector
-		}
-	}
 	return true
 }
diff --git a/pkg/yurthub/cachemanager/fake_storage_wrapper.go b/pkg/yurthub/cachemanager/fake_storage_wrapper.go
new file mode 100644
index 00000000..fedf425c
--- /dev/null
+++ b/pkg/yurthub/cachemanager/fake_storage_wrapper.go
@@ -0,0 +1,102 @@
+/*
+Copyright 2020 The OpenYurt 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 cachemanager
+
+import (
+	"os"
+	"strings"
+
+	"github.com/openyurtio/openyurt/pkg/yurthub/storage"
+	"github.com/openyurtio/openyurt/pkg/yurthub/storage/fake"
+
+	"k8s.io/apimachinery/pkg/runtime"
+)
+
+type fakeStorageWrapper struct {
+	s    storage.Store
+	data map[string]runtime.Object
+}
+
+// NewFakeStorageWrapper new fake storage wrapper
+func NewFakeStorageWrapper() StorageWrapper {
+	s, _ := fake.NewFakeStorage()
+	return &fakeStorageWrapper{
+		s:    s,
+		data: make(map[string]runtime.Object),
+	}
+}
+
+func (fsw *fakeStorageWrapper) Create(key string, obj runtime.Object) error {
+	if fsw.data == nil {
+		fsw.data = make(map[string]runtime.Object)
+	}
+	fsw.data[key] = obj
+
+	return nil
+}
+
+func (fsw *fakeStorageWrapper) Delete(key string) error {
+	delete(fsw.data, key)
+
+	return nil
+}
+
+func (fsw *fakeStorageWrapper) Get(key string) (runtime.Object, error) {
+	obj, ok := fsw.data[key]
+	if ok {
+		return obj, nil
+	}
+
+	return nil, os.ErrNotExist
+}
+
+func (fsw *fakeStorageWrapper) ListKeys(key string) ([]string, error) {
+	keys := make([]string, 0, len(fsw.data))
+	for k := range fsw.data {
+		keys = append(keys, k)
+	}
+
+	return keys, nil
+}
+
+func (fsw *fakeStorageWrapper) List(key string) ([]runtime.Object, error) {
+	objs := make([]runtime.Object, 0, len(fsw.data))
+	for k, obj := range fsw.data {
+		if strings.HasPrefix(k, key) && obj != nil {
+			objs = append(objs, obj)
+		}
+	}
+
+	return objs, nil
+}
+
+func (fsw *fakeStorageWrapper) Update(key string, obj runtime.Object) error {
+	if fsw.data == nil {
+		fsw.data = make(map[string]runtime.Object)
+	}
+	fsw.data[key] = obj
+
+	return nil
+}
+
+func (fsw *fakeStorageWrapper) GetRaw(key string) ([]byte, error) {
+	return fsw.s.Get(key)
+}
+
+func (fsw *fakeStorageWrapper) UpdateRaw(key string, contents []byte) error {
+	return fsw.s.Update(key, contents)
+}
diff --git a/pkg/yurthub/cachemanager/storage_wrapper.go b/pkg/yurthub/cachemanager/storage_wrapper.go
index 146de591..fe75833b 100644
--- a/pkg/yurthub/cachemanager/storage_wrapper.go
+++ b/pkg/yurthub/cachemanager/storage_wrapper.go
@@ -39,8 +39,6 @@ type StorageWrapper interface {
 	ListKeys(key string) ([]string, error)
 	List(key string) ([]runtime.Object, error)
 	Update(key string, obj runtime.Object) error
-	Replace(rootKey string, objs map[string]runtime.Object) error
-	DeleteCollection(rootKey string) error
 	GetRaw(key string) ([]byte, error)
 	UpdateRaw(key string, contents []byte) error
 }
@@ -213,28 +211,6 @@ func (sw *storageWrapper) Update(key string, obj runtime.Object) error {
 	return nil
 }
 
-// Replace will delete the old objects, and use the given objs instead.
-func (sw *storageWrapper) Replace(rootKey string, objs map[string]runtime.Object) error {
-	var buf bytes.Buffer
-	contents := make(map[string][]byte, len(objs))
-	for key, obj := range objs {
-		if err := sw.backendSerializer.Encode(obj, &buf); err != nil {
-			klog.Errorf("failed to encode object in update for %s, %v", key, err)
-			return err
-		}
-		contents[key] = make([]byte, len(buf.Bytes()))
-		copy(contents[key], buf.Bytes())
-		buf.Reset()
-	}
-
-	return sw.store.Replace(rootKey, contents)
-}
-
-// DeleteCollection will delete all objects under rootKey
-func (sw *storageWrapper) DeleteCollection(rootKey string) error {
-	return sw.store.DeleteCollection(rootKey)
-}
-
 // GetRaw get byte data for specified key
 func (sw *storageWrapper) GetRaw(key string) ([]byte, error) {
 	return sw.store.Get(key)
diff --git a/pkg/yurthub/proxy/proxy.go b/pkg/yurthub/proxy/proxy.go
index 71e66cd4..87b6b3b8 100644
--- a/pkg/yurthub/proxy/proxy.go
+++ b/pkg/yurthub/proxy/proxy.go
@@ -86,7 +86,6 @@ func (p *yurtReverseProxy) buildHandlerChain(handler http.Handler) http.Handler
 	handler = util.WithRequestContentType(handler)
 	handler = util.WithCacheHeaderCheck(handler)
 	handler = util.WithRequestTimeout(handler)
-	handler = util.WithListRequestSelector(handler)
 	handler = util.WithRequestClientComponent(handler)
 	handler = filters.WithRequestInfo(handler, p.resolver)
 	handler = util.WithMaxInFlightLimit(handler, p.maxRequestsInFlight)
diff --git a/pkg/yurthub/proxy/util/util.go b/pkg/yurthub/proxy/util/util.go
index fccafca3..b06f21bb 100644
--- a/pkg/yurthub/proxy/util/util.go
+++ b/pkg/yurthub/proxy/util/util.go
@@ -28,8 +28,6 @@ import (
 	"k8s.io/apimachinery/pkg/api/errors"
 	metainternalversion "k8s.io/apimachinery/pkg/apis/meta/internalversion"
 	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
-	"k8s.io/apimachinery/pkg/fields"
-	"k8s.io/apimachinery/pkg/labels"
 	apirequest "k8s.io/apiserver/pkg/endpoints/request"
 	"k8s.io/klog"
 )
@@ -89,53 +87,6 @@ func WithCacheHeaderCheck(handler http.Handler) http.Handler {
 	})
 }
 
-// selectorString returns the string of label and field selector
-func selectorString(lSelector labels.Selector, fSelector fields.Selector) string {
-	var ls string
-	var fs string
-	if lSelector != nil {
-		ls = lSelector.String()
-	}
-
-	if fSelector != nil {
-		fs = fSelector.String()
-	}
-
-	switch {
-	case ls != "" && fs != "":
-		return strings.Join([]string{ls, fs}, "&")
-
-	case ls != "":
-		return ls
-
-	case fs != "":
-		return fs
-	}
-
-	return ""
-}
-
-// WithListRequestSelector add label selector and field selector string in list request context.
-func WithListRequestSelector(handler http.Handler) http.Handler {
-	return http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) {
-		ctx := req.Context()
-		if info, ok := apirequest.RequestInfoFrom(ctx); ok {
-			if info.IsResourceRequest && info.Verb == "list" && info.Name == "" {
-				// list request with fieldSelector=metadata.name does not need to set selector string
-				opts := metainternalversion.ListOptions{}
-				if err := metainternalversion.ParameterCodec.DecodeParameters(req.URL.Query(), metav1.SchemeGroupVersion, &opts); err == nil {
-					if str := selectorString(opts.LabelSelector, opts.FieldSelector); str != "" {
-						ctx = util.WithListSelector(ctx, str)
-						req = req.WithContext(ctx)
-					}
-				}
-			}
-		}
-
-		handler.ServeHTTP(w, req)
-	})
-}
-
 // WithRequestClientComponent add component field in request context.
 // component is extracted from User-Agent Header, and only the content
 // before the "/" when User-Agent include "/".
diff --git a/pkg/yurthub/storage/disk/storage.go b/pkg/yurthub/storage/disk/storage.go
index 8a2910d7..5c151b84 100644
--- a/pkg/yurthub/storage/disk/storage.go
+++ b/pkg/yurthub/storage/disk/storage.go
@@ -343,85 +343,6 @@ func (ds *diskStorage) Update(key string, contents []byte) error {
 	return os.Rename(filepath.Join(ds.baseDir, tmpKey), filepath.Join(ds.baseDir, key))
 }
 
-// Replace will delete all files under rootKey dir and create new files with contents.
-func (ds *diskStorage) Replace(rootKey string, contents map[string][]byte) error {
-	if rootKey == "" {
-		return storage.ErrKeyIsEmpty
-	} else if len(contents) == 0 {
-		return storage.ErrKeyHasNoContent
-	}
-
-	for key := range contents {
-		if !strings.Contains(key, rootKey) {
-			return storage.ErrRootKeyInvalid
-		}
-	}
-
-	if !ds.lockKey(rootKey) {
-		return storage.ErrStorageAccessConflict
-	}
-	defer ds.unLockKey(rootKey)
-
-	// 1. mv old dir into tmp_dir when rootKey dir already exists
-	absPath := filepath.Join(ds.baseDir, rootKey)
-	tmpRootKey := getTmpKey(rootKey)
-	tmpPath := filepath.Join(ds.baseDir, tmpRootKey)
-	dirExisted := false
-	if info, err := os.Stat(absPath); err == nil {
-		if info.IsDir() {
-			err := os.Rename(absPath, tmpPath)
-			if err != nil {
-				return err
-			}
-			dirExisted = true
-		}
-	}
-
-	// 2. create new file with contents
-	// TODO: if error happens, we may need retry mechanism, or add some mechanism to do consistency check.
-	for key, data := range contents {
-		err := ds.create(key, data)
-		if err != nil {
-			klog.Errorf("failed to create %s in replace, %v", key, err)
-			continue
-		}
-	}
-
-	//  3. delete old tmp dir
-	if dirExisted {
-		return os.RemoveAll(tmpPath)
-	}
-
-	return nil
-}
-
-// DeleteCollection delete file or dir that specified by rootKey
-func (ds *diskStorage) DeleteCollection(rootKey string) error {
-	if rootKey == "" {
-		return storage.ErrKeyIsEmpty
-	}
-
-	if !ds.lockKey(rootKey) {
-		return storage.ErrStorageAccessConflict
-	}
-	defer ds.unLockKey(rootKey)
-
-	absKey := filepath.Join(ds.baseDir, rootKey)
-	info, err := os.Stat(absKey)
-	if err != nil {
-		if os.IsNotExist(err) {
-			return nil
-		}
-		return err
-	} else if info.Mode().IsRegular() {
-		return os.Remove(absKey)
-	} else if info.IsDir() {
-		return os.RemoveAll(absKey)
-	}
-
-	return fmt.Errorf("%s is exist, but not recognized, %v", rootKey, info.Mode())
-}
-
 // Recover recover storage error
 func (ds *diskStorage) Recover(key string) error {
 	if !ds.lockKey(key) {
@@ -474,6 +395,7 @@ func (ds *diskStorage) lockKey(key string) bool {
 		}
 	}
 	ds.keyPendingStatus[key] = struct{}{}
+
 	return true
 }
 
diff --git a/pkg/yurthub/storage/fake/fake_storage.go b/pkg/yurthub/storage/fake/fake_storage.go
new file mode 100644
index 00000000..0f3da0b3
--- /dev/null
+++ b/pkg/yurthub/storage/fake/fake_storage.go
@@ -0,0 +1,69 @@
+/*
+Copyright 2020 The OpenYurt 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 fake
+
+import "github.com/openyurtio/openyurt/pkg/yurthub/storage"
+
+type fakeStorage struct {
+	data map[string]string
+}
+
+// NewFakeStorage creates a fake storage
+func NewFakeStorage() (storage.Store, error) {
+	return &fakeStorage{
+		data: make(map[string]string),
+	}, nil
+}
+
+func (fs *fakeStorage) Create(key string, contents []byte) error {
+	fs.data[key] = string(contents)
+	return nil
+}
+
+func (fs *fakeStorage) Delete(key string) error {
+	delete(fs.data, key)
+	return nil
+}
+
+func (fs *fakeStorage) Get(key string) ([]byte, error) {
+	s, ok := fs.data[key]
+	if ok {
+		return []byte(s), nil
+	}
+	return []byte{}, nil
+}
+
+func (fs *fakeStorage) ListKeys(key string) ([]string, error) {
+	keys := make([]string, 0, len(fs.data))
+	for k := range fs.data {
+		keys = append(keys, k)
+	}
+	return keys, nil
+}
+
+func (fs *fakeStorage) List(key string) ([][]byte, error) {
+	bb := make([][]byte, 0, len(fs.data))
+	for _, v := range fs.data {
+		bb = append(bb, []byte(v))
+	}
+	return bb, nil
+}
+
+func (fs *fakeStorage) Update(key string, contents []byte) error {
+	fs.data[key] = string(contents)
+	return nil
+}
diff --git a/pkg/yurthub/storage/store.go b/pkg/yurthub/storage/store.go
index f0ac4dae..7cea9efd 100644
--- a/pkg/yurthub/storage/store.go
+++ b/pkg/yurthub/storage/store.go
@@ -26,15 +26,12 @@ var ErrStorageAccessConflict = errors.New("specified key is under accessing")
 // ErrStorageNotFound is an error for not found accessing key
 var ErrStorageNotFound = errors.New("specified key is not found")
 
-// ErrKeyHasNoContent is an error for file key that has no contents
+// ErrorKeyHasNoContent is an error for file key that has no contents
 var ErrKeyHasNoContent = errors.New("specified key has no contents")
 
-// ErrKeyIsEmpty is an error for key is empty
+// ErrorKeyWithoutContent is an error for key is empty
 var ErrKeyIsEmpty = errors.New("specified key is empty")
 
-// ErrRootKeyInvalid is an error for root key is invalid
-var ErrRootKeyInvalid = errors.New("root key is invalid")
-
 // Store is an interface for caching data into backend storage
 type Store interface {
 	Create(key string, contents []byte) error
@@ -43,6 +40,4 @@ type Store interface {
 	ListKeys(key string) ([]string, error)
 	List(key string) ([][]byte, error)
 	Update(key string, contents []byte) error
-	Replace(rootKey string, contents map[string][]byte) error
-	DeleteCollection(rootKey string) error
 }
diff --git a/pkg/yurthub/util/util.go b/pkg/yurthub/util/util.go
index 6feb4d6e..75dee15f 100644
--- a/pkg/yurthub/util/util.go
+++ b/pkg/yurthub/util/util.go
@@ -51,8 +51,6 @@ const (
 	ProxyClientComponent
 	// ProxyReqCanCache represents request can cache context key
 	ProxyReqCanCache
-	// ProxyListSelector represents label selector and filed selector string for list request
-	ProxyListSelector
 )
 
 // WithValue returns a copy of parent in which the value associated with key is val.
@@ -104,17 +102,6 @@ func ReqCanCacheFrom(ctx context.Context) (bool, bool) {
 	return info, ok
 }
 
-// WithListSelector returns a copy of parent in which the list request selector string is set
-func WithListSelector(parent context.Context, selector string) context.Context {
-	return WithValue(parent, ProxyListSelector, selector)
-}
-
-// ListSelectorFrom returns the value of the list request selector string on the ctx
-func ListSelectorFrom(ctx context.Context) (string, bool) {
-	info, ok := ctx.Value(ProxyListSelector).(string)
-	return info, ok
-}
-
 // ReqString formats a string for request
 func ReqString(req *http.Request) string {
 	ctx := req.Context()
diff --git a/pkg/yurttunnel/dns/dns.go b/pkg/yurttunnel/dns/dns.go
new file mode 100644
index 00000000..46ec8cc9
--- /dev/null
+++ b/pkg/yurttunnel/dns/dns.go
@@ -0,0 +1,462 @@
+/*
+Copyright 2021 The OpenYurt 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 dns
+
+import (
+	"context"
+	"fmt"
+	"net"
+	"os"
+	"sort"
+	"strconv"
+	"strings"
+	"sync"
+	"time"
+
+	corev1 "k8s.io/api/core/v1"
+	apierrors "k8s.io/apimachinery/pkg/api/errors"
+	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
+	"k8s.io/apimachinery/pkg/labels"
+	"k8s.io/apimachinery/pkg/util/intstr"
+	utilruntime "k8s.io/apimachinery/pkg/util/runtime"
+	"k8s.io/apimachinery/pkg/util/uuid"
+	"k8s.io/apimachinery/pkg/util/wait"
+	"k8s.io/client-go/informers"
+	coreinformers "k8s.io/client-go/informers/core/v1"
+	clientset "k8s.io/client-go/kubernetes"
+	corelister "k8s.io/client-go/listers/core/v1"
+	"k8s.io/client-go/tools/cache"
+	"k8s.io/client-go/tools/leaderelection"
+	"k8s.io/client-go/tools/leaderelection/resourcelock"
+	"k8s.io/client-go/util/workqueue"
+	"k8s.io/klog/v2"
+
+	"github.com/openyurtio/openyurt/pkg/projectinfo"
+	"github.com/openyurtio/openyurt/pkg/yurttunnel/constants"
+	"github.com/openyurtio/openyurt/pkg/yurttunnel/util"
+)
+
+const (
+	maxRetries    = 15
+	minSyncPeriod = 30
+
+	yurttunnelDNSRecordConfigMapNs = "kube-system"
+	yurttunnelDNSRecordNodeDataKey = "tunnel-nodes"
+
+	dnatPortPrefix = "dnat-"
+)
+
+var (
+	yurttunnelDNSRecordConfigMapName = fmt.Sprintf("%s-tunnel-nodes",
+		strings.TrimRightFunc(projectinfo.GetProjectPrefix(), func(c rune) bool { return c == '-' }))
+)
+
+// DNSRecordController interface defines the method for synchronizing
+// the node dns records with k8s DNS component(such as CoreDNS)
+type DNSRecordController interface {
+	Run(stopCh <-chan struct{})
+}
+
+// coreDNSRecordController implements the DNSRecordController
+type coreDNSRecordController struct {
+	lock                 sync.Mutex
+	kubeClient           clientset.Interface
+	sharedInformerFactor informers.SharedInformerFactory
+	nodeLister           corelister.NodeLister
+	nodeListerSynced     cache.InformerSynced
+	svcInformerSynced    cache.InformerSynced
+	cmInformerSynced     cache.InformerSynced
+	queue                workqueue.RateLimitingInterface
+	tunnelServerIP       string
+	insecurePort         int
+	syncPeriod           int
+}
+
+// NewCoreDNSRecordController create a CoreDNSRecordController that synchronizes node dns records with CoreDNS configuration
+func NewCoreDNSRecordController(client clientset.Interface,
+	informerFactory informers.SharedInformerFactory,
+	listenInsecureAddr string,
+	syncPeriod int) (DNSRecordController, error) {
+
+	_, insecurePortStr, err := net.SplitHostPort(listenInsecureAddr)
+	if err != nil {
+		return nil, err
+	}
+
+	insecurePort, err := strconv.Atoi(insecurePortStr)
+	if err != nil {
+		return nil, err
+	}
+
+	dnsctl := &coreDNSRecordController{
+		kubeClient:           client,
+		syncPeriod:           syncPeriod,
+		insecurePort:         insecurePort,
+		sharedInformerFactor: informerFactory,
+		queue:                workqueue.NewNamedRateLimitingQueue(workqueue.DefaultControllerRateLimiter(), "tunnel-dns"),
+	}
+
+	nodeInformer := informerFactory.Core().V1().Nodes()
+	nodeInformer.Informer().AddEventHandler(cache.ResourceEventHandlerFuncs{
+		AddFunc:    dnsctl.addNode,
+		UpdateFunc: dnsctl.updateNode,
+		DeleteFunc: dnsctl.deleteNode,
+	})
+	dnsctl.nodeLister = nodeInformer.Lister()
+	dnsctl.nodeListerSynced = nodeInformer.Informer().HasSynced
+
+	svcInformer := informerFactory.InformerFor(&corev1.Service{}, newServiceInformer)
+	svcInformer.AddEventHandler(cache.ResourceEventHandlerFuncs{
+		AddFunc:    dnsctl.addService,
+		UpdateFunc: dnsctl.updateService,
+		DeleteFunc: dnsctl.deleteService,
+	})
+	dnsctl.svcInformerSynced = svcInformer.HasSynced
+
+	cmInformer := informerFactory.InformerFor(&corev1.ConfigMap{}, newConfigMapInformer)
+	cmInformer.AddEventHandler(cache.ResourceEventHandlerFuncs{
+		AddFunc:    dnsctl.addConfigMap,
+		UpdateFunc: dnsctl.updateConfigMap,
+		DeleteFunc: dnsctl.deleteConfigMap,
+	})
+	dnsctl.cmInformerSynced = cmInformer.HasSynced
+
+	// override syncPeriod when the specified value is too small
+	if dnsctl.syncPeriod < minSyncPeriod {
+		dnsctl.syncPeriod = minSyncPeriod
+	}
+
+	return dnsctl, nil
+}
+
+// newServiceInformer creates a shared index informer that returns only interested services
+func newServiceInformer(cs clientset.Interface, resyncPeriod time.Duration) cache.SharedIndexInformer {
+	selector := fmt.Sprintf("metadata.name=%v", constants.YurttunnelServerServiceName)
+	tweakListOptions := func(options *metav1.ListOptions) {
+		options.FieldSelector = selector
+	}
+	return coreinformers.NewFilteredServiceInformer(cs, constants.YurttunnelServerServiceNs, resyncPeriod, nil, tweakListOptions)
+}
+
+// newConfigMapInformer creates a shared index informer that returns only interested configmaps
+func newConfigMapInformer(cs clientset.Interface, resyncPeriod time.Duration) cache.SharedIndexInformer {
+	selector := fmt.Sprintf("metadata.name=%v", util.YurttunnelServerDnatConfigMapName)
+	tweakListOptions := func(options *metav1.ListOptions) {
+		options.FieldSelector = selector
+	}
+	return coreinformers.NewFilteredConfigMapInformer(cs, util.YurttunnelServerDnatConfigMapNs, resyncPeriod, nil, tweakListOptions)
+}
+
+func (dnsctl *coreDNSRecordController) Run(stopCh <-chan struct{}) {
+	electionChecker := leaderelection.NewLeaderHealthzAdaptor(time.Second * 20)
+	id, err := os.Hostname()
+	if err != nil {
+		klog.Fatalf("failed to get hostname, %v", err)
+	}
+	rl, err := resourcelock.New("leases", metav1.NamespaceSystem, "tunnel-dns-controller",
+		dnsctl.kubeClient.CoreV1(),
+		dnsctl.kubeClient.CoordinationV1(),
+		resourcelock.ResourceLockConfig{
+			Identity: id + "_" + string(uuid.NewUUID()),
+		})
+	if err != nil {
+		klog.Fatalf("error creating tunnel-dns-controller lock, %v", err)
+	}
+
+	leaderelection.RunOrDie(context.TODO(), leaderelection.LeaderElectionConfig{
+		Lock:          rl,
+		LeaseDuration: metav1.Duration{Duration: time.Second * time.Duration(15)}.Duration,
+		RenewDeadline: metav1.Duration{Duration: time.Second * time.Duration(10)}.Duration,
+		RetryPeriod:   metav1.Duration{Duration: time.Second * time.Duration(2)}.Duration,
+		Callbacks: leaderelection.LeaderCallbacks{
+			OnStartedLeading: func(ctx context.Context) {
+				dnsctl.run(stopCh)
+			},
+			OnStoppedLeading: func() {
+				klog.Fatalf("leaderelection lost")
+			},
+		},
+		WatchDog: electionChecker,
+		Name:     "tunnel-dns-controller",
+	})
+	panic("unreachable")
+}
+
+func (dnsctl *coreDNSRecordController) run(stopCh <-chan struct{}) {
+	defer utilruntime.HandleCrash()
+	defer dnsctl.queue.ShutDown()
+
+	klog.Infof("starting tunnel dns controller")
+	defer klog.Infof("shutting down tunnel dns controller")
+
+	if !cache.WaitForNamedCacheSync("tunnel-dns-controller", stopCh,
+		dnsctl.nodeListerSynced, dnsctl.svcInformerSynced, dnsctl.cmInformerSynced) {
+		return
+	}
+
+	if err := dnsctl.ensureCoreDNSRecordConfigMap(); err != nil {
+		klog.Errorf("failed to ensure dns record ConfigMap %v/%v, %v",
+			yurttunnelDNSRecordConfigMapNs, yurttunnelDNSRecordConfigMapName, err)
+		return
+	}
+
+	go wait.Until(dnsctl.worker, time.Second, stopCh)
+
+	// sync dns hosts as a whole
+	go wait.Until(dnsctl.syncDNSRecordAsWhole, time.Duration(dnsctl.syncPeriod)*time.Second, stopCh)
+
+	// sync tunnel server svc
+	go wait.Until(func() {
+		if err := dnsctl.syncTunnelServerServiceAsWhole(); err != nil {
+			klog.Errorf("failed to sync tunnel server service, %v", err)
+		}
+	}, time.Duration(dnsctl.syncPeriod)*time.Second, stopCh)
+
+	<-stopCh
+}
+
+func (dnsctl *coreDNSRecordController) enqueue(obj interface{}, eventType EventType) {
+	e := &Event{
+		Obj:  obj,
+		Type: eventType,
+	}
+	dnsctl.queue.Add(e)
+}
+
+func (dnsctl *coreDNSRecordController) worker() {
+	for dnsctl.processNextWorkItem() {
+	}
+}
+
+func (dnsctl *coreDNSRecordController) processNextWorkItem() bool {
+	event, quit := dnsctl.queue.Get()
+	if quit {
+		return false
+	}
+	defer dnsctl.queue.Done(event)
+
+	err := dnsctl.dispatch(event.(*Event))
+	dnsctl.handleErr(err, event)
+
+	return true
+}
+
+func (dnsctl *coreDNSRecordController) dispatch(event *Event) error {
+	switch event.Type {
+	case NodeAdd:
+		return dnsctl.onNodeAdd(event.Obj.(*corev1.Node))
+	case NodeUpdate:
+		return dnsctl.onNodeUpdate(event.Obj.(*corev1.Node))
+	case NodeDelete:
+		return dnsctl.onNodeDelete(event.Obj.(*corev1.Node))
+	case ServiceAdd:
+		return dnsctl.onServiceAdd(event.Obj.(*corev1.Service))
+	case ServiceUpdate:
+		return dnsctl.onServiceUpdate(event.Obj.(*corev1.Service))
+	case ServiceDelete:
+		return dnsctl.onServiceDelete(event.Obj.(*corev1.Service))
+	case ConfigMapAdd:
+		return dnsctl.onConfigMapAdd(event.Obj.(*corev1.ConfigMap))
+	case ConfigMapUpdate:
+		return dnsctl.onConfigMapUpdate(event.Obj.(*corev1.ConfigMap))
+	case ConfigMapDelete:
+		return dnsctl.onConfigMapDelete(event.Obj.(*corev1.ConfigMap))
+	default:
+		return nil
+	}
+}
+
+func (dnsctl *coreDNSRecordController) handleErr(err error, event interface{}) {
+	if err == nil {
+		dnsctl.queue.Forget(event)
+		return
+	}
+
+	if dnsctl.queue.NumRequeues(event) < maxRetries {
+		klog.Infof("error syncing event %v: %v", event, err)
+		dnsctl.queue.AddRateLimited(event)
+		return
+	}
+
+	utilruntime.HandleError(err)
+	klog.Infof("dropping event %q out of the queue: %v", event, err)
+	dnsctl.queue.Forget(event)
+}
+
+func (dnsctl *coreDNSRecordController) ensureCoreDNSRecordConfigMap() error {
+	_, err := dnsctl.kubeClient.CoreV1().ConfigMaps(constants.YurttunnelServerServiceNs).
+		Get(yurttunnelDNSRecordConfigMapName, metav1.GetOptions{})
+	if err != nil && apierrors.IsNotFound(err) {
+		cm := &corev1.ConfigMap{
+			ObjectMeta: metav1.ObjectMeta{
+				Name:      yurttunnelDNSRecordConfigMapName,
+				Namespace: constants.YurttunnelServerServiceNs,
+			},
+			Data: map[string]string{
+				yurttunnelDNSRecordNodeDataKey: "",
+			},
+		}
+		_, err = dnsctl.kubeClient.CoreV1().ConfigMaps(constants.YurttunnelServerServiceNs).Create(cm)
+		if err != nil {
+			return fmt.Errorf("failed to create ConfigMap %v/%v, %v",
+				constants.YurttunnelServerServiceNs, yurttunnelDNSRecordConfigMapName, err)
+		}
+	}
+	return err
+}
+
+func (dnsctl *coreDNSRecordController) syncTunnelServerServiceAsWhole() error {
+	klog.V(2).Info("sync tunnel server service as whole")
+	dnatPorts, err := util.GetConfiguredDnatPorts(dnsctl.kubeClient, strconv.Itoa(dnsctl.insecurePort))
+	if err != nil {
+		return err
+	}
+	return dnsctl.updateTunnelServerSvcDnatPorts(dnatPorts)
+}
+
+func (dnsctl *coreDNSRecordController) syncDNSRecordAsWhole() {
+	klog.V(2).Info("sync dns record as whole")
+
+	dnsctl.lock.Lock()
+	defer dnsctl.lock.Unlock()
+
+	ip, err := dnsctl.getTunnelServerIP(false)
+	if err != nil {
+		klog.Errorf("failed to sync dns record as whole, %v", err)
+		return
+	}
+
+	nodes, err := dnsctl.nodeLister.List(labels.Everything())
+	if err != nil {
+		klog.Errorf("failed to sync dns record as whole, %v", err)
+		return
+	}
+
+	records := make([]string, 0, len(nodes))
+	for i := range nodes {
+		node := nodes[i]
+		if !isEdgeNode(node) {
+			ip, err = getNodeHostIP(node)
+			if err != nil {
+				klog.Errorf("failed to parse node address for %v, %v", node.Name, err)
+				continue
+			}
+		}
+		records = append(records, formatDNSRecord(ip, node.Name))
+	}
+
+	if err := dnsctl.updateDNSRecords(records); err != nil {
+		klog.Errorf("failed to sync dns record as whole, %v", err)
+	}
+}
+
+func (dnsctl *coreDNSRecordController) getTunnelServerIP(useCache bool) (string, error) {
+	if useCache && len(dnsctl.tunnelServerIP) != 0 {
+		return dnsctl.tunnelServerIP, nil
+	}
+
+	svc, err := dnsctl.kubeClient.CoreV1().Services(constants.YurttunnelServerServiceNs).
+		Get(constants.YurttunnelServerServiceName, metav1.GetOptions{})
+	if err != nil {
+		return "", fmt.Errorf("failed to get %v/%v service, %v",
+			constants.YurttunnelServerServiceNs, constants.YurttunnelServerServiceName, err)
+	}
+	if len(svc.Spec.ClusterIP) == 0 {
+		return "", fmt.Errorf("unable find ClusterIP from %s/%s service, %v",
+			constants.YurttunnelServerServiceNs, constants.YurttunnelServerServiceName, err)
+	}
+
+	// cache result
+	dnsctl.tunnelServerIP = svc.Spec.ClusterIP
+
+	return dnsctl.tunnelServerIP, nil
+}
+
+func (dnsctl *coreDNSRecordController) updateDNSRecords(records []string) error {
+	// keep sorted
+	sort.Strings(records)
+
+	cm, err := dnsctl.kubeClient.CoreV1().ConfigMaps(constants.YurttunnelServerServiceNs).
+		Get(yurttunnelDNSRecordConfigMapName, metav1.GetOptions{})
+	if err != nil {
+		return err
+	}
+	cm.Data[yurttunnelDNSRecordNodeDataKey] = strings.Join(records, "\n")
+	if _, err := dnsctl.kubeClient.CoreV1().ConfigMaps(constants.YurttunnelServerServiceNs).Update(cm); err != nil {
+		return fmt.Errorf("failed to update configmap %v/%v, %v",
+			constants.YurttunnelServerServiceNs, yurttunnelDNSRecordConfigMapName, err)
+	}
+	return nil
+}
+
+func (dnsctl *coreDNSRecordController) updateTunnelServerSvcDnatPorts(ports []string) error {
+	svc, err := dnsctl.kubeClient.CoreV1().Services(constants.YurttunnelServerServiceNs).
+		Get(constants.YurttunnelServerServiceName, metav1.GetOptions{})
+	if err != nil {
+		return fmt.Errorf("failed to sync tunnel server service, %v", err)
+	}
+
+	changed := false
+
+	svcPortMap := make(map[int32]corev1.ServicePort)
+	for i := range svc.Spec.Ports {
+		port := svc.Spec.Ports[i]
+		svcPortMap[port.Port] = port
+	}
+
+	dnatPortMap := make(map[int]bool)
+	for _, dnatPort := range ports {
+		portInt, err := strconv.Atoi(dnatPort)
+		if err != nil {
+			klog.Errorf("failed to parse dnat port %q, %v", dnatPort, err)
+			continue
+		}
+		dnatPortMap[portInt] = true
+
+		if p, ok := svcPortMap[int32(portInt)]; !ok || p.Protocol != corev1.ProtocolTCP {
+			port := corev1.ServicePort{
+				Name:       fmt.Sprintf("%v%v", dnatPortPrefix, dnatPort),
+				Port:       int32(portInt),
+				Protocol:   corev1.ProtocolTCP,
+				TargetPort: intstr.FromInt(dnsctl.insecurePort),
+			}
+			svc.Spec.Ports = append(svc.Spec.Ports, port)
+			changed = true
+		}
+	}
+
+	updatedSvcPorts := make([]corev1.ServicePort, 0, len(svc.Spec.Ports))
+	for i := range svc.Spec.Ports {
+		port := svc.Spec.Ports[i]
+		if strings.HasPrefix(port.Name, dnatPortPrefix) && !dnatPortMap[int(port.Port)] {
+			changed = true
+			continue
+		}
+		updatedSvcPorts = append(updatedSvcPorts, port)
+	}
+
+	if !changed {
+		return nil
+	}
+
+	svc.Spec.Ports = updatedSvcPorts
+	_, err = dnsctl.kubeClient.CoreV1().Services(constants.YurttunnelServerServiceNs).Update(svc)
+	if err != nil {
+		return fmt.Errorf("failed to sync tunnel server service, %v", err)
+	}
+	return nil
+}
diff --git a/pkg/yurttunnel/dns/event.go b/pkg/yurttunnel/dns/event.go
new file mode 100644
index 00000000..539fb4f3
--- /dev/null
+++ b/pkg/yurttunnel/dns/event.go
@@ -0,0 +1,36 @@
+/*
+Copyright 2021 The OpenYurt 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 dns
+
+type EventType string
+
+const (
+	NodeAdd         EventType = "NODE_ADD"
+	NodeUpdate      EventType = "NODE_UPDATE"
+	NodeDelete      EventType = "NODE_DELETE"
+	ServiceAdd      EventType = "SERVICE_ADD"
+	ServiceUpdate   EventType = "SERVICE_UPDATE"
+	ServiceDelete   EventType = "SERVICE_DELETE"
+	ConfigMapAdd    EventType = "CONFIGMAP_ADD"
+	ConfigMapUpdate EventType = "CONFIGMAP_UPDATE"
+	ConfigMapDelete EventType = "CONFIGMAP_DELETE"
+)
+
+type Event struct {
+	Obj  interface{}
+	Type EventType
+}
diff --git a/pkg/yurttunnel/dns/handler.go b/pkg/yurttunnel/dns/handler.go
new file mode 100644
index 00000000..b050905d
--- /dev/null
+++ b/pkg/yurttunnel/dns/handler.go
@@ -0,0 +1,246 @@
+/*
+Copyright 2021 The OpenYurt 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 dns
+
+import (
+	"fmt"
+	"reflect"
+	"strings"
+
+	"github.com/openyurtio/openyurt/pkg/yurttunnel/constants"
+	corev1 "k8s.io/api/core/v1"
+	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
+	utilruntime "k8s.io/apimachinery/pkg/util/runtime"
+	"k8s.io/client-go/tools/cache"
+	"k8s.io/klog/v2"
+)
+
+func (dnsctl *coreDNSRecordController) addNode(obj interface{}) {
+	node, ok := obj.(*corev1.Node)
+	if !ok {
+		return
+	}
+	if node.DeletionTimestamp != nil {
+		dnsctl.deleteNode(node)
+		return
+	}
+	klog.V(2).Infof("enqueue node add event for %v", node.Name)
+	dnsctl.enqueue(node, NodeAdd)
+}
+
+func (dnsctl *coreDNSRecordController) updateNode(oldObj, newObj interface{}) {
+	oldNode, ok := oldObj.(*corev1.Node)
+	if !ok {
+		return
+	}
+	newNode, ok := newObj.(*corev1.Node)
+	if !ok {
+		return
+	}
+
+	oldIsEdgeNode, newIsEdgeNode := isEdgeNode(oldNode), isEdgeNode(newNode)
+	if oldIsEdgeNode == newIsEdgeNode {
+		return
+	}
+
+	klog.V(2).Infof("enqueue node update event for %v, will update dns record", newNode.Name)
+	dnsctl.enqueue(newNode, NodeUpdate)
+}
+
+func (dnsctl *coreDNSRecordController) deleteNode(obj interface{}) {
+	node, ok := obj.(*corev1.Node)
+	if !ok {
+		tombstone, ok := obj.(cache.DeletedFinalStateUnknown)
+		if !ok {
+			utilruntime.HandleError(fmt.Errorf("can not get object from tombstone %#v", obj))
+			return
+		}
+		node, ok = tombstone.Obj.(*corev1.Node)
+		if !ok {
+			utilruntime.HandleError(fmt.Errorf("tombstone contained object is not a node %#v", obj))
+			return
+		}
+	}
+	klog.V(2).Infof("enqueue node delete event for %v", node.Name)
+	dnsctl.enqueue(node, NodeDelete)
+}
+
+func (dnsctl *coreDNSRecordController) addConfigMap(obj interface{}) {
+	cm, ok := obj.(*corev1.ConfigMap)
+	if !ok {
+		return
+	}
+	if cm.DeletionTimestamp != nil {
+		dnsctl.deleteConfigMap(cm)
+		return
+	}
+	klog.V(2).Infof("enqueue configmap add event for %v/%v", cm.Namespace, cm.Name)
+	dnsctl.enqueue(cm, ConfigMapAdd)
+}
+
+func (dnsctl *coreDNSRecordController) updateConfigMap(oldObj, newObj interface{}) {
+	oldConfigMap, ok := oldObj.(*corev1.ConfigMap)
+	if !ok {
+		return
+	}
+	newConfigMap, ok := newObj.(*corev1.ConfigMap)
+	if !ok {
+		return
+	}
+
+	if reflect.DeepEqual(oldConfigMap.Data, newConfigMap.Data) {
+		return
+	}
+
+	klog.V(2).Infof("enqueue configmap update event for %v/%v, will sync tunnel server svc", newConfigMap.Namespace, newConfigMap.Name)
+	dnsctl.enqueue(newConfigMap, ConfigMapUpdate)
+}
+
+func (dnsctl *coreDNSRecordController) deleteConfigMap(obj interface{}) {
+	cm, ok := obj.(*corev1.ConfigMap)
+	if !ok {
+		tombstone, ok := obj.(cache.DeletedFinalStateUnknown)
+		if !ok {
+			utilruntime.HandleError(fmt.Errorf("can not get object from tombstone %#v", obj))
+			return
+		}
+		cm, ok = tombstone.Obj.(*corev1.ConfigMap)
+		if !ok {
+			utilruntime.HandleError(fmt.Errorf("tombstone contained object is not a node %#v", obj))
+			return
+		}
+	}
+	klog.V(2).Infof("enqueue configmap delete event for %v/%v", cm.Namespace, cm.Name)
+	dnsctl.enqueue(cm, ConfigMapDelete)
+}
+
+func (dnsctl *coreDNSRecordController) addService(obj interface{}) {
+	svc, ok := obj.(*corev1.Service)
+	if !ok {
+		return
+	}
+	if svc.Namespace != constants.YurttunnelServerServiceNs || svc.Name != constants.YurttunnelServerServiceName {
+		return
+	}
+	klog.V(2).Infof("enqueue service add event for %v/%v", svc.Namespace, svc.Name)
+	dnsctl.enqueue(svc, ServiceAdd)
+}
+
+func (dnsctl *coreDNSRecordController) updateService(oldObj, newObj interface{}) {
+	// do nothing
+}
+
+func (dnsctl *coreDNSRecordController) deleteService(obj interface{}) {
+	// do nothing
+}
+
+func (dnsctl *coreDNSRecordController) onConfigMapAdd(cm *corev1.ConfigMap) error {
+	return dnsctl.syncTunnelServerServiceAsWhole()
+}
+
+func (dnsctl *coreDNSRecordController) onConfigMapUpdate(cm *corev1.ConfigMap) error {
+	return dnsctl.syncTunnelServerServiceAsWhole()
+}
+
+func (dnsctl *coreDNSRecordController) onConfigMapDelete(cm *corev1.ConfigMap) error {
+	return dnsctl.syncTunnelServerServiceAsWhole()
+}
+
+func (dnsctl *coreDNSRecordController) onNodeAdd(node *corev1.Node) error {
+	klog.V(2).Infof("adding node dns record for %v", node.Name)
+	return dnsctl.addOrUpdateNode(node)
+}
+
+func (dnsctl *coreDNSRecordController) onNodeUpdate(node *corev1.Node) error {
+	klog.V(2).Infof("updating node dns record for %v", node.Name)
+	return dnsctl.addOrUpdateNode(node)
+}
+
+func (dnsctl *coreDNSRecordController) addOrUpdateNode(node *corev1.Node) error {
+	ip, err := getNodeHostIP(node)
+	if err != nil {
+		return err
+	}
+	if isEdgeNode(node) {
+		ip, err = dnsctl.getTunnelServerIP(true)
+		if err != nil {
+			return err
+		}
+	}
+
+	records, err := dnsctl.getCurrentDNSRecords()
+	if err != nil {
+		return err
+	}
+
+	updatedRecords, changed, err := addOrUpdateRecord(records, formatDNSRecord(ip, node.Name))
+	if err != nil {
+		return err
+	}
+	if !changed {
+		return nil
+	}
+
+	return dnsctl.updateDNSRecords(updatedRecords)
+}
+
+func (dnsctl *coreDNSRecordController) onNodeDelete(node *corev1.Node) error {
+	klog.V(2).Infof("deleting node dns record for %v", node.Name)
+
+	dnsctl.lock.Lock()
+	defer dnsctl.lock.Unlock()
+
+	records, err := dnsctl.getCurrentDNSRecords()
+	if err != nil {
+		return err
+	}
+	mergedRecords, changed := removeRecordByHostname(records, node.Name)
+	if !changed {
+		return nil
+	}
+
+	return dnsctl.updateDNSRecords(mergedRecords)
+}
+
+func (dnsctl *coreDNSRecordController) getCurrentDNSRecords() ([]string, error) {
+	cm, err := dnsctl.kubeClient.CoreV1().ConfigMaps(constants.YurttunnelServerServiceNs).
+		Get(yurttunnelDNSRecordConfigMapName, metav1.GetOptions{})
+	if err != nil {
+		return nil, err
+	}
+
+	data, ok := cm.Data[yurttunnelDNSRecordNodeDataKey]
+	if !ok {
+		return nil, fmt.Errorf("key %q not found in %s/%s ConfigMap, %v",
+			yurttunnelDNSRecordNodeDataKey, constants.YurttunnelServerServiceNs, yurttunnelDNSRecordConfigMapName, err)
+	}
+
+	return strings.Split(data, "\n"), nil
+}
+
+func (dnsctl *coreDNSRecordController) onServiceAdd(svc *corev1.Service) error {
+	dnsctl.syncDNSRecordAsWhole()
+	return nil
+}
+
+func (dnsctl *coreDNSRecordController) onServiceUpdate(svc *corev1.Service) error {
+	return nil
+}
+
+func (dnsctl *coreDNSRecordController) onServiceDelete(svc *corev1.Service) error {
+	return nil
+}
diff --git a/pkg/yurttunnel/dns/util.go b/pkg/yurttunnel/dns/util.go
new file mode 100644
index 00000000..ea135b23
--- /dev/null
+++ b/pkg/yurttunnel/dns/util.go
@@ -0,0 +1,116 @@
+/*
+Copyright 2021 The OpenYurt 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 dns
+
+import (
+	"fmt"
+	"net"
+	"strings"
+
+	corev1 "k8s.io/api/core/v1"
+	v1 "k8s.io/api/core/v1"
+
+	"github.com/openyurtio/openyurt/pkg/projectinfo"
+)
+
+func isEdgeNode(node *corev1.Node) bool {
+	isEdgeNode, ok := node.Labels[projectinfo.GetEdgeWorkerLabelKey()]
+	if ok && isEdgeNode == "true" {
+		return true
+	}
+	return false
+}
+
+func formatDNSRecord(ip, host string) string {
+	return fmt.Sprintf("%s\t%s", ip, host)
+}
+
+// getNodeHostIP returns the provided node's "primary" IP
+func getNodeHostIP(node *v1.Node) (string, error) {
+	// re-sort the addresses with InternalIPs first and then ExternalIPs
+	allIPs := make([]net.IP, 0, len(node.Status.Addresses))
+	for _, addr := range node.Status.Addresses {
+		if addr.Type == v1.NodeInternalIP {
+			ip := net.ParseIP(addr.Address)
+			if ip != nil {
+				allIPs = append(allIPs, ip)
+				break
+			}
+		}
+	}
+	for _, addr := range node.Status.Addresses {
+		if addr.Type == v1.NodeExternalIP {
+			ip := net.ParseIP(addr.Address)
+			if ip != nil {
+				allIPs = append(allIPs, ip)
+				break
+			}
+		}
+	}
+	if len(allIPs) == 0 {
+		return "", fmt.Errorf("host IP unknown; known addresses: %v", node.Status.Addresses)
+	}
+
+	return allIPs[0].String(), nil
+}
+
+func removeRecordByHostname(records []string, hostname string) (result []string, changed bool) {
+	result = make([]string, 0, len(records))
+	for _, v := range result {
+		if !strings.HasSuffix(v, hostname) {
+			result = append(result, v)
+		}
+	}
+	return result, len(records) == len(result)
+}
+
+func parseHostnameFromDNSRecord(record string) (string, error) {
+	arr := strings.Split(record, "\t")
+	if len(arr) != 2 {
+		return "", fmt.Errorf("failed to parse hostname, invalid dns record %q", record)
+	}
+	return arr[1], nil
+}
+
+func addOrUpdateRecord(records []string, record string) (result []string, changed bool, err error) {
+	hostname, err := parseHostnameFromDNSRecord(record)
+	if err != nil {
+		return nil, false, err
+	}
+
+	result = make([]string, len(records))
+	copy(result, records)
+
+	found := false
+	for i, v := range result {
+		if strings.HasSuffix(v, hostname) {
+			found = true
+			if v != record {
+				result[i] = record
+				changed = true
+				break
+			}
+		}
+	}
+
+	if !found {
+		result = append(result, record)
+		changed = true
+	}
+
+	return result, changed, nil
+}
diff --git a/pkg/yurttunnel/iptables/iptables.go b/pkg/yurttunnel/iptables/iptables.go
index dc5c139a..d14c3309 100644
--- a/pkg/yurttunnel/iptables/iptables.go
+++ b/pkg/yurttunnel/iptables/iptables.go
@@ -23,8 +23,6 @@ import (
 	"time"
 
 	corev1 "k8s.io/api/core/v1"
-	apierrors "k8s.io/apimachinery/pkg/api/errors"
-	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
 	"k8s.io/apimachinery/pkg/labels"
 	coreinformer "k8s.io/client-go/informers/core/v1"
 	clientset "k8s.io/client-go/kubernetes"
@@ -37,6 +35,7 @@ import (
 
 	"github.com/openyurtio/openyurt/pkg/projectinfo"
 	"github.com/openyurtio/openyurt/pkg/yurttunnel/server/metrics"
+	"github.com/openyurtio/openyurt/pkg/yurttunnel/util"
 )
 
 const (
@@ -48,15 +47,9 @@ const (
 	yurttunnelServerPortChain = "TUNNEL-PORT"
 	yurttunnelPortChainPrefix = "TUNNEL-PORT-"
 	defaultSyncPeriod         = 15
-
-	// constants related dnat rules configmap
-	yurttunnelServerDnatConfigMapNs = "kube-system"
-	yurttunnelServerDnatDataKey     = "dnat-ports-pair"
 )
 
 var (
-	yurttunnelServerDnatConfigMapName = fmt.Sprintf("%s-tunnel-server-cfg",
-		strings.TrimRightFunc(projectinfo.GetProjectPrefix(), func(c rune) bool { return c == '-' }))
 	tunnelCommentStr   = strings.ReplaceAll(projectinfo.GetTunnelName(), "-", " ")
 	iptablesJumpChains = []iptablesJumpChain{
 		{
@@ -201,46 +194,6 @@ func (im *iptablesManager) deleteJumpChains(jumpChains []iptablesJumpChain) erro
 	return nil
 }
 
-func (im *iptablesManager) getConfiguredDnatPorts() []string {
-	ports := make([]string, 0)
-	c, err := im.kubeClient.CoreV1().
-		ConfigMaps(yurttunnelServerDnatConfigMapNs).
-		Get(yurttunnelServerDnatConfigMapName, metav1.GetOptions{})
-	if err != nil {
-		if apierrors.IsNotFound(err) {
-			klog.V(4).Infof("configmap %s/%s is not found",
-				yurttunnelServerDnatConfigMapNs,
-				yurttunnelServerDnatConfigMapName)
-		} else {
-			klog.Errorf("fail to get configmap %s/%s: %v",
-				yurttunnelServerDnatConfigMapNs,
-				yurttunnelServerDnatConfigMapName, err)
-		}
-		return ports
-	}
-
-	pairStr, ok := c.Data[yurttunnelServerDnatDataKey]
-	if !ok || len(pairStr) == 0 {
-		return ports
-	}
-
-	portsPair := strings.Split(pairStr, ",")
-	for _, pair := range portsPair {
-		portPair := strings.Split(pair, "=")
-		// we only allowed user to add dnat rule that uses insecure port as the
-		// destination port.
-		if len(portPair) == 2 &&
-			portPair[1] == im.insecurePort &&
-			len(portPair[0]) != 0 {
-			if portPair[0] != "10250" && portPair[0] != "10255" {
-				ports = append(ports, portPair[0])
-			}
-		}
-	}
-
-	return ports
-}
-
 // getIPOfNodesWithoutAgent returns the ip addresses of all nodes that
 // are not running yurttunnel-agent
 func (im *iptablesManager) getIPOfNodesWithoutAgent() []string {
@@ -508,7 +461,11 @@ func parametersWithFamily(isIPv6 bool, parameters ...string) []string {
 // while the request to access the cloud node is returned
 func (im *iptablesManager) syncIptableSetting() {
 	// check if there are new dnat ports
-	dnatPorts := im.getConfiguredDnatPorts()
+	dnatPorts, err := util.GetConfiguredDnatPorts(im.kubeClient, im.insecurePort)
+	if err != nil {
+		klog.Errorf("failed to sync iptables rules, %v", err)
+		return
+	}
 	portsChanged, deletedDnatPorts := im.getDeletedPorts(dnatPorts)
 	currentDnatPorts := append(dnatPorts, kubeletSecurePort, kubeletInsecurePort)
 
@@ -518,7 +475,7 @@ func (im *iptablesManager) syncIptableSetting() {
 	currentNodesIP := append(nodesIP, loopbackAddr)
 
 	// update the iptable setting if necessary
-	err := im.ensurePortsIptables(currentDnatPorts, deletedDnatPorts, currentNodesIP, deletedNodesIP)
+	err = im.ensurePortsIptables(currentDnatPorts, deletedDnatPorts, currentNodesIP, deletedNodesIP)
 	if err != nil {
 		klog.Errorf("failed to ensurePortsIptables: %v", err)
 		return
diff --git a/pkg/yurttunnel/util/util.go b/pkg/yurttunnel/util/util.go
index 56997161..c3b00099 100644
--- a/pkg/yurttunnel/util/util.go
+++ b/pkg/yurttunnel/util/util.go
@@ -1,13 +1,30 @@
 package util
 
 import (
+	"fmt"
 	"net/http"
+	"strings"
 
-	"github.com/openyurtio/openyurt/pkg/profile"
+	"github.com/gorilla/mux"
 	"github.com/prometheus/client_golang/prometheus/promhttp"
+	apierrors "k8s.io/apimachinery/pkg/api/errors"
+	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
+	clientset "k8s.io/client-go/kubernetes"
 	"k8s.io/klog/v2"
 
-	"github.com/gorilla/mux"
+	"github.com/openyurtio/openyurt/pkg/profile"
+	"github.com/openyurtio/openyurt/pkg/projectinfo"
+)
+
+const (
+	// constants related dnat rules configmap
+	YurttunnelServerDnatConfigMapNs = "kube-system"
+	yurttunnelServerDnatDataKey     = "dnat-ports-pair"
+)
+
+var (
+	YurttunnelServerDnatConfigMapName = fmt.Sprintf("%s-tunnel-server-cfg",
+		strings.TrimRightFunc(projectinfo.GetProjectPrefix(), func(c rune) bool { return c == '-' }))
 )
 
 // RunMetaServer start a http server for serving metrics and pprof requests.
@@ -33,3 +50,42 @@ func RunMetaServer(addr string) {
 		klog.InfoS("meta server stopped listening", "server endpoint", addr)
 	}()
 }
+
+// GetConfiguredDnatPorts returns the DNAT ports configured for tunnel server.
+// NOTE: We only allow user to add dnat rule that uses insecure port as the destination port currently.
+func GetConfiguredDnatPorts(client clientset.Interface, insecurePort string) ([]string, error) {
+	ports := make([]string, 0)
+	c, err := client.CoreV1().
+		ConfigMaps(YurttunnelServerDnatConfigMapNs).
+		Get(YurttunnelServerDnatConfigMapName, metav1.GetOptions{})
+	if err != nil {
+		if apierrors.IsNotFound(err) {
+			return nil, fmt.Errorf("configmap %s/%s is not found",
+				YurttunnelServerDnatConfigMapNs,
+				YurttunnelServerDnatConfigMapName)
+		} else {
+			return nil, fmt.Errorf("fail to get configmap %s/%s: %v",
+				YurttunnelServerDnatConfigMapNs,
+				YurttunnelServerDnatConfigMapName, err)
+		}
+	}
+
+	pairStr, ok := c.Data[yurttunnelServerDnatDataKey]
+	if !ok || len(pairStr) == 0 {
+		return ports, nil
+	}
+
+	portsPair := strings.Split(pairStr, ",")
+	for _, pair := range portsPair {
+		portPair := strings.Split(pair, "=")
+		if len(portPair) == 2 &&
+			portPair[1] == insecurePort &&
+			len(portPair[0]) != 0 {
+			if portPair[0] != "10250" && portPair[0] != "10255" {
+				ports = append(ports, portPair[0])
+			}
+		}
+	}
+
+	return ports, nil
+}


Test patch:
diff --git a/pkg/yurthub/cachemanager/cache_agent_test.go b/pkg/yurthub/cachemanager/cache_agent_test.go
index ca69a680..8d0c4cbc 100644
--- a/pkg/yurthub/cachemanager/cache_agent_test.go
+++ b/pkg/yurthub/cachemanager/cache_agent_test.go
@@ -20,18 +20,12 @@ import (
 	"strings"
 	"testing"
 
-	"github.com/openyurtio/openyurt/pkg/yurthub/storage/disk"
-
 	"k8s.io/apimachinery/pkg/util/sets"
 	"k8s.io/apiserver/pkg/endpoints/request"
 )
 
 func TestInitCacheAgents(t *testing.T) {
-	dStorage, err := disk.NewDiskStorage(rootDir)
-	if err != nil {
-		t.Errorf("failed to create disk storage, %v", err)
-	}
-	s := NewStorageWrapper(dStorage)
+	s := NewFakeStorageWrapper()
 	m, _ := NewCacheManager(s, nil)
 
 	// default cache agents in fake store
@@ -68,32 +62,23 @@ func TestInitCacheAgents(t *testing.T) {
 	if !compareAgents(gotAgents2, m.ListCacheAgents()) {
 		t.Errorf("Got agents: %v, cache agents map: %v", gotAgents2, m.ListCacheAgents())
 	}
-
-	err = s.Delete(cacheAgentsKey)
-	if err != nil {
-		t.Errorf("failed to delete cache agents key, %v", err)
-	}
 }
 
 func TestUpdateCacheAgents(t *testing.T) {
-	dStorage, err := disk.NewDiskStorage(rootDir)
-	if err != nil {
-		t.Errorf("failed to create disk storage, %v", err)
-	}
-	s := NewStorageWrapper(dStorage)
+	s := NewFakeStorageWrapper()
 	m, _ := NewCacheManager(s, nil)
 
-	testcases := map[string]struct {
+	tests := []struct {
 		desc         string
 		addAgents    []string
 		expectAgents []string
 	}{
-		"add one agent":               {addAgents: []string{"agent1"}, expectAgents: append(defaultCacheAgents, "agent1")},
-		"update with two agents":      {addAgents: []string{"agent2", "agent3"}, expectAgents: append(defaultCacheAgents, "agent2", "agent3")},
-		"update with more two agents": {addAgents: []string{"agent4", "agent5"}, expectAgents: append(defaultCacheAgents, "agent4", "agent5")},
+		{desc: "add one agent", addAgents: []string{"agent1"}, expectAgents: append(defaultCacheAgents, "agent1")},
+		{desc: "update with two agents", addAgents: []string{"agent2", "agent3"}, expectAgents: append(defaultCacheAgents, "agent2", "agent3")},
+		{desc: "update with more two agents", addAgents: []string{"agent4", "agent5"}, expectAgents: append(defaultCacheAgents, "agent4", "agent5")},
 	}
-	for k, tt := range testcases {
-		t.Run(k, func(t *testing.T) {
+	for _, tt := range tests {
+		t.Run(tt.desc, func(t *testing.T) {
 
 			// add agents
 			err := m.UpdateCacheAgents(tt.addAgents)
@@ -114,11 +99,6 @@ func TestUpdateCacheAgents(t *testing.T) {
 			if !compareAgents(gotAgents, m.ListCacheAgents()) {
 				t.Errorf("Got agents: %v, cache agents map: %v", gotAgents, m.ListCacheAgents())
 			}
-
-			err = s.Delete(cacheAgentsKey)
-			if err != nil {
-				t.Errorf("failed to delete cache agents key, %v", err)
-			}
 		})
 	}
 }
diff --git a/pkg/yurthub/cachemanager/cache_manager_test.go b/pkg/yurthub/cachemanager/cache_manager_test.go
index 721567eb..5b58821a 100644
--- a/pkg/yurthub/cachemanager/cache_manager_test.go
+++ b/pkg/yurthub/cachemanager/cache_manager_test.go
@@ -30,10 +30,7 @@ import (
 
 	"github.com/openyurtio/openyurt/pkg/yurthub/kubernetes/serializer"
 	proxyutil "github.com/openyurtio/openyurt/pkg/yurthub/proxy/util"
-	"github.com/openyurtio/openyurt/pkg/yurthub/storage"
-	"github.com/openyurtio/openyurt/pkg/yurthub/storage/disk"
 	"github.com/openyurtio/openyurt/pkg/yurthub/util"
-
 	v1 "k8s.io/api/core/v1"
 	nodev1beta1 "k8s.io/api/node/v1beta1"
 	"k8s.io/apimachinery/pkg/api/meta"
@@ -49,25 +46,24 @@ import (
 	restclientwatch "k8s.io/client-go/rest/watch"
 )
 
-var (
-	rootDir = "/tmp/cache-manager"
-)
-
-func TestCacheGetResponse(t *testing.T) {
-	//storage := NewFakeStorageWrapper()
-	dStorage, err := disk.NewDiskStorage(rootDir)
-	if err != nil {
-		t.Errorf("failed to create disk storage, %v", err)
-	}
-	sWrapper := NewStorageWrapper(dStorage)
+func TestCacheResponse(t *testing.T) {
+	storage := NewFakeStorageWrapper()
 	serializerM := serializer.NewSerializerManager()
 	yurtCM := &cacheManager{
-		storage:           sWrapper,
+		storage:           storage,
 		serializerManager: serializerM,
 		cacheAgents:       make(map[string]bool),
 	}
 
-	testcases := map[string]struct {
+	type expectData struct {
+		err  bool
+		rv   string
+		name string
+		ns   string
+		kind string
+	}
+	tests := []struct {
+		desc         string
 		group        string
 		version      string
 		key          string
@@ -78,16 +74,10 @@ func TestCacheGetResponse(t *testing.T) {
 		path         string
 		resource     string
 		namespaced   bool
-		expectResult struct {
-			err  bool
-			rv   string
-			name string
-			ns   string
-			kind string
-		}
-		cacheErr error
+		expectResult expectData
 	}{
-		"cache response for get pod": {
+		{
+			desc:    "cache response for get pod",
 			group:   "",
 			version: "v1",
 			key:     "kubelet/pods/default/mypod1",
@@ -108,20 +98,15 @@ func TestCacheGetResponse(t *testing.T) {
 			path:       "/api/v1/namespaces/default/pods/mypod1",
 			resource:   "pods",
 			namespaced: true,
-			expectResult: struct {
-				err  bool
-				rv   string
-				name string
-				ns   string
-				kind string
-			}{
+			expectResult: expectData{
 				rv:   "1",
 				name: "mypod1",
 				ns:   "default",
 				kind: "Pod",
 			},
 		},
-		"cache response for get pod2": {
+		{
+			desc:    "cache response for get pod2",
 			group:   "",
 			version: "v1",
 			key:     "kubelet/pods/default/mypod2",
@@ -142,20 +127,15 @@ func TestCacheGetResponse(t *testing.T) {
 			path:       "/api/v1/namespaces/default/pods/mypod2",
 			resource:   "pods",
 			namespaced: true,
-			expectResult: struct {
-				err  bool
-				rv   string
-				name string
-				ns   string
-				kind string
-			}{
+			expectResult: expectData{
 				rv:   "3",
 				name: "mypod2",
 				ns:   "default",
 				kind: "Pod",
 			},
 		},
-		"cache response for get node": {
+		{
+			desc:    "cache response for get node",
 			group:   "",
 			version: "v1",
 			key:     "kubelet/nodes/mynode1",
@@ -175,19 +155,14 @@ func TestCacheGetResponse(t *testing.T) {
 			path:       "/api/v1/nodes/mynode1",
 			resource:   "nodes",
 			namespaced: false,
-			expectResult: struct {
-				err  bool
-				rv   string
-				name string
-				ns   string
-				kind string
-			}{
+			expectResult: expectData{
 				rv:   "4",
 				name: "mynode1",
 				kind: "Node",
 			},
 		},
-		"cache response for get node2": {
+		{
+			desc:    "cache response for get node2",
 			group:   "",
 			version: "v1",
 			key:     "kubelet/nodes/mynode2",
@@ -207,20 +182,16 @@ func TestCacheGetResponse(t *testing.T) {
 			path:       "/api/v1/nodes/mynode2",
 			resource:   "nodes",
 			namespaced: false,
-			expectResult: struct {
-				err  bool
-				rv   string
-				name string
-				ns   string
-				kind string
-			}{
+			expectResult: expectData{
 				rv:   "6",
 				name: "mynode2",
 				kind: "Node",
 			},
 		},
+
 		//used to test whether custom resources can be cached correctly
-		"cache response for get crontab": {
+		{
+			desc:    "cache response for get crontab",
 			group:   "stable.example.com",
 			version: "v1",
 			key:     "kubelet/crontabs/default/crontab1",
@@ -241,20 +212,15 @@ func TestCacheGetResponse(t *testing.T) {
 			path:       "/apis/stable.example.com/v1/namespaces/default/crontabs/crontab1",
 			resource:   "crontabs",
 			namespaced: true,
-			expectResult: struct {
-				err  bool
-				rv   string
-				name string
-				ns   string
-				kind string
-			}{
+			expectResult: expectData{
 				rv:   "1",
 				name: "crontab1",
 				ns:   "default",
 				kind: "CronTab",
 			},
 		},
-		"cache response for get crontab2": {
+		{
+			desc:    "cache response for get crontab2",
 			group:   "stable.example.com",
 			version: "v1",
 			key:     "kubelet/crontabs/default/crontab2",
@@ -275,20 +241,15 @@ func TestCacheGetResponse(t *testing.T) {
 			path:       "/apis/stable.example.com/v1/namespaces/default/crontabs/crontab2",
 			resource:   "crontabs",
 			namespaced: true,
-			expectResult: struct {
-				err  bool
-				rv   string
-				name string
-				ns   string
-				kind string
-			}{
+			expectResult: expectData{
 				rv:   "3",
 				name: "crontab2",
 				ns:   "default",
 				kind: "CronTab",
 			},
 		},
-		"cache response for get foo without namespace": {
+		{
+			desc:    "cache response for get foo without namespace",
 			group:   "samplecontroller.k8s.io",
 			version: "v1",
 			key:     "kubelet/foos/foo1",
@@ -308,19 +269,14 @@ func TestCacheGetResponse(t *testing.T) {
 			path:       "/apis/samplecontroller.k8s.io/v1/foos/foo1",
 			resource:   "foos",
 			namespaced: false,
-			expectResult: struct {
-				err  bool
-				rv   string
-				name string
-				ns   string
-				kind string
-			}{
+			expectResult: expectData{
 				rv:   "3",
 				name: "foo1",
 				kind: "Foo",
 			},
 		},
-		"cache response for get foo2 without namespace": {
+		{
+			desc:    "cache response for get foo2 without namespace",
 			group:   "samplecontroller.k8s.io",
 			version: "v1",
 			key:     "kubelet/foos/foo2",
@@ -340,76 +296,27 @@ func TestCacheGetResponse(t *testing.T) {
 			path:       "/apis/samplecontroller.k8s.io/v1/foos/foo2",
 			resource:   "foos",
 			namespaced: false,
-			expectResult: struct {
-				err  bool
-				rv   string
-				name string
-				ns   string
-				kind string
-			}{
+			expectResult: expectData{
 				rv:   "5",
 				name: "foo2",
 				kind: "Foo",
 			},
 		},
-		"cache response for Status": {
-			group:   "",
-			version: "v1",
-			key:     "kubelet/nodes/test",
-			inputObj: runtime.Object(&metav1.Status{
-				TypeMeta: metav1.TypeMeta{
-					APIVersion: "v1",
-					Kind:       "Status",
-				},
-				Status:  "Failure",
-				Message: "node test is not exist",
-				Reason:  "NotFound",
-				Code:    404,
-			}),
-			userAgent: "kubelet",
-			accept:    "application/json",
-			verb:      "GET",
-			path:      "/api/v1/nodes/test",
-			resource:  "nodes",
-			cacheErr:  storage.ErrStorageNotFound,
-		},
-		"cache response for nil object": {
-			group:     "",
-			version:   "v1",
-			key:       "kubelet/nodes/test",
-			inputObj:  nil,
-			userAgent: "kubelet",
-			accept:    "application/json",
-			verb:      "GET",
-			path:      "/api/v1/nodes/test",
-			resource:  "nodes",
-			expectResult: struct {
-				err  bool
-				rv   string
-				name string
-				ns   string
-				kind string
-			}{
-				err: true,
-			},
-		},
 	}
 
 	accessor := meta.NewAccessor()
 	resolver := newTestRequestInfoResolver()
-	for k, tt := range testcases {
-		t.Run(k, func(t *testing.T) {
+	for _, tt := range tests {
+		t.Run(tt.desc, func(t *testing.T) {
 			encoder, err := serializerM.CreateSerializers(tt.accept, tt.group, tt.version, tt.resource)
 			if err != nil {
 				t.Fatalf("could not create serializer, %v", err)
 			}
 
 			buf := bytes.NewBuffer([]byte{})
-			if tt.inputObj != nil {
-				err = encoder.Encoder.Encode(tt.inputObj, buf)
-				if err != nil {
-					t.Fatalf("could not encode input object, %v", err)
-				}
+			err = encoder.Encoder.Encode(tt.inputObj, buf)
+			if err != nil {
+				t.Fatalf("could not encode input object, %v", err)
 			}
 
 			req, _ := http.NewRequest(tt.verb, tt.path, nil)
@@ -434,21 +341,20 @@ func TestCacheGetResponse(t *testing.T) {
 			handler = filters.WithRequestInfo(handler, resolver)
 			handler.ServeHTTP(httptest.NewRecorder(), req)
 
-			if tt.expectResult.err && err == nil {
-				t.Errorf("expect err, but do not get error")
-			}
-
-			if len(tt.expectResult.name) == 0 {
-				return
-			}
-
-			obj, err := sWrapper.Get(tt.key)
-			if err != nil || obj == nil {
-				if tt.cacheErr != err {
-					t.Errorf("expect get error %v, but got %v", tt.cacheErr, err)
+			if tt.expectResult.err {
+				if err == nil {
+					t.Errorf("Got no error, but expect err")
 				}
-				t.Logf("get expected err %v for key %s", tt.cacheErr, tt.key)
 			} else {
+				if err != nil {
+					t.Errorf("Got error %v", err)
+				}
+
+				obj, err := storage.Get(tt.key)
+				if err != nil || obj == nil {
+					t.Errorf("failed to get object from storage")
+				}
+
 				name, _ := accessor.Name(obj)
 				rv, _ := accessor.ResourceVersion(obj)
 				kind, _ := accessor.Kind(obj)
@@ -470,12 +376,6 @@ func TestCacheGetResponse(t *testing.T) {
 				if tt.expectResult.kind != kind {
 					t.Errorf("Got kind %s, but expect kind %s", kind, tt.expectResult.kind)
 				}
-				t.Logf("get key %s successfully", tt.key)
-			}
-
-			err = sWrapper.DeleteCollection("kubelet")
-			if err != nil {
-				t.Errorf("failed to delete collection: kubelet, %v", err)
 			}
 		})
 	}
@@ -489,7 +389,14 @@ func getEncoder() runtime.Encoder {
 	return directCodecFactory.EncoderForVersion(jsonSerializer, v1.SchemeGroupVersion)
 }
 
-func TestCacheWatchResponse(t *testing.T) {
+func resetStorage(s StorageWrapper, key string) {
+	keys, _ := s.ListKeys(key)
+	for i := range keys {
+		s.Delete(keys[i])
+	}
+}
+
+func TestCacheResponseForWatch(t *testing.T) {
 	mkPod := func(id string, rv string) *v1.Pod {
 		return &v1.Pod{
 			TypeMeta:   metav1.TypeMeta{APIVersion: "", Kind: "Pod"},
@@ -512,19 +419,20 @@ func TestCacheWatchResponse(t *testing.T) {
 		}
 	}
 
-	dStorage, err := disk.NewDiskStorage(rootDir)
-	if err != nil {
-		t.Errorf("failed to create disk storage, %v", err)
-	}
-	sWrapper := NewStorageWrapper(dStorage)
+	storage := NewFakeStorageWrapper()
 	serializerM := serializer.NewSerializerManager()
 	yurtCM := &cacheManager{
-		storage:           sWrapper,
+		storage:           storage,
 		serializerManager: serializerM,
 		cacheAgents:       make(map[string]bool),
 	}
 
-	testcases := map[string]struct {
+	type expectData struct {
+		err  bool
+		data map[string]struct{}
+	}
+	tests := []struct {
+		desc         string
 		group        string
 		version      string
 		key          string
@@ -534,12 +442,10 @@ func TestCacheWatchResponse(t *testing.T) {
 		verb         string
 		path         string
 		namespaced   bool
-		expectResult struct {
-			err  bool
-			data map[string]struct{}
-		}
+		expectResult expectData
 	}{
-		"add pods": {
+		{
+			desc:    "cache response for watch add pods",
 			group:   "",
 			version: "v1",
 			key:     "kubelet/pods/default",
@@ -553,10 +459,7 @@ func TestCacheWatchResponse(t *testing.T) {
 			verb:       "GET",
 			path:       "/api/v1/namespaces/default/pods?watch=true",
 			namespaced: true,
-			expectResult: struct {
-				err  bool
-				data map[string]struct{}
-			}{
+			expectResult: expectData{
 				data: map[string]struct{}{
 					"pod-default-mypod1-2": {},
 					"pod-default-mypod2-4": {},
@@ -564,7 +467,8 @@ func TestCacheWatchResponse(t *testing.T) {
 				},
 			},
 		},
-		"add and delete pods": {
+		{
+			desc:    "cache response for watch add and delete pods",
 			group:   "",
 			version: "v1",
 			key:     "kubelet/pods/default",
@@ -578,16 +482,14 @@ func TestCacheWatchResponse(t *testing.T) {
 			verb:       "GET",
 			path:       "/api/v1/namespaces/default/pods?watch=true",
 			namespaced: true,
-			expectResult: struct {
-				err  bool
-				data map[string]struct{}
-			}{
+			expectResult: expectData{
 				data: map[string]struct{}{
 					"pod-default-mypod3-6": {},
 				},
 			},
 		},
-		"add and update pods": {
+		{
+			desc:    "cache response for watch add and update pods",
 			group:   "",
 			version: "v1",
 			key:     "kubelet/pods/default",
@@ -601,17 +503,15 @@ func TestCacheWatchResponse(t *testing.T) {
 			verb:       "GET",
 			path:       "/api/v1/namespaces/default/pods?watch=true",
 			namespaced: true,
-			expectResult: struct {
-				err  bool
-				data map[string]struct{}
-			}{
+			expectResult: expectData{
 				data: map[string]struct{}{
 					"pod-default-mypod1-4": {},
 					"pod-default-mypod3-6": {},
 				},
 			},
 		},
-		"not update pods": {
+		{
+			desc:    "cache response for watch not update pods",
 			group:   "",
 			version: "v1",
 			key:     "kubelet/pods/default",
@@ -625,17 +525,16 @@ func TestCacheWatchResponse(t *testing.T) {
 			verb:       "GET",
 			path:       "/api/v1/namespaces/default/pods?watch=true",
 			namespaced: true,
-			expectResult: struct {
-				err  bool
-				data map[string]struct{}
-			}{
+			expectResult: expectData{
 				data: map[string]struct{}{
 					"pod-default-mypod1-6": {},
 				},
 			},
 		},
+
 		//used to test whether custom resource's watch-events can be cached correctly
-		"cache response for watch add crontabs": {
+		{
+			desc:    "cache response for watch add crontabs",
 			group:   "stable.example.com",
 			version: "v1",
 			key:     "kubelet/crontabs/default",
@@ -649,10 +548,7 @@ func TestCacheWatchResponse(t *testing.T) {
 			verb:       "GET",
 			path:       "/apis/stable.example.com/v1/namespaces/default/crontabs?watch=true",
 			namespaced: true,
-			expectResult: struct {
-				err  bool
-				data map[string]struct{}
-			}{
+			expectResult: expectData{
 				data: map[string]struct{}{
 					"crontab-default-crontab1-2": {},
 					"crontab-default-crontab2-4": {},
@@ -660,7 +556,8 @@ func TestCacheWatchResponse(t *testing.T) {
 				},
 			},
 		},
-		"cache response for watch add and delete crontabs": {
+		{
+			desc:    "cache response for watch add and delete crontabs",
 			group:   "stable.example.com",
 			version: "v1",
 			key:     "kubelet/crontabs/default",
@@ -674,16 +571,14 @@ func TestCacheWatchResponse(t *testing.T) {
 			verb:       "GET",
 			path:       "/apis/stable.example.com/v1/namespaces/default/crontabs?watch=true",
 			namespaced: true,
-			expectResult: struct {
-				err  bool
-				data map[string]struct{}
-			}{
+			expectResult: expectData{
 				data: map[string]struct{}{
 					"crontab-default-crontab3-6": {},
 				},
 			},
 		},
-		"cache response for watch add and update crontabs": {
+		{
+			desc:    "cache response for watch add and update crontabs",
 			group:   "stable.example.com",
 			version: "v1",
 			key:     "kubelet/crontabs/default",
@@ -697,17 +592,15 @@ func TestCacheWatchResponse(t *testing.T) {
 			verb:       "GET",
 			path:       "/apis/stable.example.com/v1/namespaces/default/crontabs?watch=true",
 			namespaced: true,
-			expectResult: struct {
-				err  bool
-				data map[string]struct{}
-			}{
+			expectResult: expectData{
 				data: map[string]struct{}{
 					"crontab-default-crontab1-4": {},
 					"crontab-default-crontab3-6": {},
 				},
 			},
 		},
-		"cache response for watch not update crontabs": {
+		{
+			desc:    "cache response for watch not update crontabs",
 			group:   "stable.example.com",
 			version: "v1",
 			key:     "kubelet/crontabs/default",
@@ -721,10 +614,7 @@ func TestCacheWatchResponse(t *testing.T) {
 			verb:       "GET",
 			path:       "/apis/stable.example.com/v1/namespaces/default/crontabs?watch=true",
 			namespaced: true,
-			expectResult: struct {
-				err  bool
-				data map[string]struct{}
-			}{
+			expectResult: expectData{
 				data: map[string]struct{}{
 					"crontab-default-crontab1-6": {},
 				},
@@ -734,8 +624,8 @@ func TestCacheWatchResponse(t *testing.T) {
 
 	accessor := meta.NewAccessor()
 	resolver := newTestRequestInfoResolver()
-	for k, tt := range testcases {
-		t.Run(k, func(t *testing.T) {
+	for _, tt := range tests {
+		t.Run(tt.desc, func(t *testing.T) {
 			r, w := io.Pipe()
 			go func(w *io.PipeWriter) {
 				//For unregistered GVKs, the normal encoding is used by default and the original GVK information is set
@@ -774,64 +664,62 @@ func TestCacheWatchResponse(t *testing.T) {
 			handler = filters.WithRequestInfo(handler, resolver)
 			handler.ServeHTTP(httptest.NewRecorder(), req)
 
-			if tt.expectResult.err && err == nil {
-				t.Errorf("expect err, but do not got err")
-			}
-
-			if len(tt.expectResult.data) == 0 {
-				return
-			}
+			if tt.expectResult.err {
+				if err == nil {
+					t.Errorf("Got no error, but expect err")
+				}
+			} else {
+				if err != nil && err != io.EOF {
+					t.Errorf("Got error %v", err)
+				}
 
-			objs, err := sWrapper.List(tt.key)
-			if err != nil || len(objs) == 0 {
-				t.Errorf("failed to get object from storage")
-			}
+				objs, err := storage.List(tt.key)
+				if err != nil || len(objs) == 0 {
+					t.Errorf("failed to get object from storage")
+				}
 
-			if len(objs) != len(tt.expectResult.data) {
-				t.Errorf("Got %d objects, but expect %d objects", len(objs), len(tt.expectResult.data))
-			}
+				if len(objs) != len(tt.expectResult.data) {
+					t.Errorf("Got %d objects, but expect %d objects", len(objs), len(tt.expectResult.data))
+				}
 
-			for _, obj := range objs {
-				name, _ := accessor.Name(obj)
-				ns, _ := accessor.Namespace(obj)
-				rv, _ := accessor.ResourceVersion(obj)
-				kind, _ := accessor.Kind(obj)
+				for _, obj := range objs {
+					name, _ := accessor.Name(obj)
+					ns, _ := accessor.Namespace(obj)
+					rv, _ := accessor.ResourceVersion(obj)
+					kind, _ := accessor.Kind(obj)
 
-				var objKey string
-				if tt.namespaced {
-					objKey = fmt.Sprintf("%s-%s-%s-%s", strings.ToLower(kind), ns, name, rv)
-				} else {
-					objKey = fmt.Sprintf("%s-%s-%s", strings.ToLower(kind), name, rv)
-				}
+					var objKey string
+					if tt.namespaced {
+						objKey = fmt.Sprintf("%s-%s-%s-%s", strings.ToLower(kind), ns, name, rv)
+					} else {
+						objKey = fmt.Sprintf("%s-%s-%s", strings.ToLower(kind), name, rv)
+					}
 
-				if _, ok := tt.expectResult.data[objKey]; !ok {
-					t.Errorf("Got %s %s/%s with rv %s", kind, ns, name, rv)
+					if _, ok := tt.expectResult.data[objKey]; !ok {
+						t.Errorf("Got %s %s/%s with rv %s", kind, ns, name, rv)
+					}
 				}
-			}
-
-			err = sWrapper.DeleteCollection("kubelet")
-			if err != nil {
-				t.Errorf("failed to delete collection: kubelet, %v", err)
+				resetStorage(storage, tt.key)
 			}
 		})
 	}
 }
 
-func TestCacheListResponse(t *testing.T) {
-	dStorage, err := disk.NewDiskStorage(rootDir)
-	if err != nil {
-		t.Errorf("failed to create disk storage, %v", err)
-	}
-	sWrapper := NewStorageWrapper(dStorage)
-
+func TestCacheResponseForList(t *testing.T) {
+	storage := NewFakeStorageWrapper()
 	serializerM := serializer.NewSerializerManager()
 	yurtCM := &cacheManager{
-		storage:           sWrapper,
+		storage:           storage,
 		serializerManager: serializerM,
 		cacheAgents:       make(map[string]bool),
 	}
 
-	testcases := map[string]struct {
+	type expectData struct {
+		err  bool
+		data map[string]struct{}
+	}
+	tests := []struct {
+		desc         string
 		group        string
 		version      string
 		key          string
@@ -842,13 +730,10 @@ func TestCacheListResponse(t *testing.T) {
 		path         string
 		resource     string
 		namespaced   bool
-		expectResult struct {
-			err  bool
-			data map[string]struct{}
-		}
-		cacheErr error
+		expectResult expectData
 	}{
-		"list pods": {
+		{
+			desc:    "cache response for list pods",
 			group:   "",
 			version: "v1",
 			key:     "kubelet/pods/default",
@@ -904,10 +789,7 @@ func TestCacheListResponse(t *testing.T) {
 			path:       "/api/v1/namespaces/default/pods",
 			resource:   "pods",
 			namespaced: true,
-			expectResult: struct {
-				err  bool
-				data map[string]struct{}
-			}{
+			expectResult: expectData{
 				data: map[string]struct{}{
 					"pod-default-mypod1-1": {},
 					"pod-default-mypod2-3": {},
@@ -915,7 +797,8 @@ func TestCacheListResponse(t *testing.T) {
 				},
 			},
 		},
-		"list nodes": {
+		{
+			desc:    "cache response for list nodes",
 			group:   "",
 			version: "v1",
 			key:     "kubelet/nodes",
@@ -978,10 +861,7 @@ func TestCacheListResponse(t *testing.T) {
 			path:       "/api/v1/nodes",
 			resource:   "nodes",
 			namespaced: false,
-			expectResult: struct {
-				err  bool
-				data map[string]struct{}
-			}{
+			expectResult: expectData{
 				data: map[string]struct{}{
 					"node-mynode1-6":  {},
 					"node-mynode2-8":  {},
@@ -990,7 +870,8 @@ func TestCacheListResponse(t *testing.T) {
 				},
 			},
 		},
-		"list nodes with fieldselector": {
+		{
+			desc:    "cache response for list nodes with fieldselector",
 			group:   "",
 			version: "v1",
 			key:     "kubelet/nodes",
@@ -1021,21 +902,18 @@ func TestCacheListResponse(t *testing.T) {
 			accept:     "application/json",
 			verb:       "GET",
 			path:       "/api/v1/nodes?fieldselector=meatadata.name=mynode",
-			resource:   "nodes",
 			namespaced: false,
-			expectResult: struct {
-				err  bool
-				data map[string]struct{}
-			}{
+			expectResult: expectData{
 				data: map[string]struct{}{
 					"node-mynode-12": {},
 				},
 			},
 		},
-		"list runtimeclasses with no objects": {
+		{
+			desc:    "cache response for list runtimeclasses with no objects",
 			group:   "node.k8s.io",
 			version: "v1beta1",
-			key:     "kubelet/runtimeclasses",
+			key:     "kubelet/runtimeclass",
 			inputObj: runtime.Object(
 				&nodev1beta1.RuntimeClassList{
 					TypeMeta: metav1.TypeMeta{
@@ -1052,41 +930,15 @@ func TestCacheListResponse(t *testing.T) {
 			accept:     "application/json",
 			verb:       "GET",
 			path:       "/apis/node.k8s.io/v1beta1/runtimeclasses",
-			resource:   "runtimeclasses",
 			namespaced: false,
-			expectResult: struct {
-				err  bool
-				data map[string]struct{}
-			}{
+			expectResult: expectData{
 				data: map[string]struct{}{},
 			},
 		},
-		"list with status": {
-			group:   "",
-			version: "v1",
-			key:     "kubelet/nodetest",
-			inputObj: runtime.Object(
-				&metav1.Status{
-					TypeMeta: metav1.TypeMeta{
-						APIVersion: "v1",
-						Kind:       "Status",
-					},
-					Status:  "Failure",
-					Message: "nodetest is not exist",
-					Reason:  "NotFound",
-					Code:    404,
-				},
-			),
-			userAgent:  "kubelet",
-			accept:     "application/json",
-			verb:       "GET",
-			path:       "/api/v1/node",
-			resource:   "nodes",
-			namespaced: false,
-			cacheErr:   storage.ErrStorageNotFound,
-		},
+
 		//used to test whether custom resource list can be cached correctly
-		"cache response for list crontabs": {
+		{
+			desc:    "cache response for list crontabs",
 			group:   "stable.example.com",
 			version: "v1",
 			key:     "kubelet/crontabs/default",
@@ -1133,17 +985,15 @@ func TestCacheListResponse(t *testing.T) {
 			path:       "/apis/stable.example.com/v1/namespaces/default/crontabs",
 			resource:   "crontabs",
 			namespaced: true,
-			expectResult: struct {
-				err  bool
-				data map[string]struct{}
-			}{
+			expectResult: expectData{
 				data: map[string]struct{}{
 					"crontab-default-crontab1-1": {},
 					"crontab-default-crontab2-2": {},
 				},
 			},
 		},
-		"cache response for list foos without namespace": {
+		{
+			desc:    "cache response for list foos without namespace",
 			group:   "samplecontroller.k8s.io",
 			version: "v1",
 			key:     "kubelet/foos",
@@ -1188,10 +1038,7 @@ func TestCacheListResponse(t *testing.T) {
 			path:       "/apis/samplecontroller.k8s.io/v1/foos",
 			resource:   "foos",
 			namespaced: false,
-			expectResult: struct {
-				err  bool
-				data map[string]struct{}
-			}{
+			expectResult: expectData{
 				data: map[string]struct{}{
 					"foo-foo1-1": {},
 					"foo-foo2-2": {},
@@ -1202,8 +1049,8 @@ func TestCacheListResponse(t *testing.T) {
 
 	accessor := meta.NewAccessor()
 	resolver := newTestRequestInfoResolver()
-	for k, tt := range testcases {
-		t.Run(k, func(t *testing.T) {
+	for _, tt := range tests {
+		t.Run(tt.desc, func(t *testing.T) {
 			encoder, err := serializerM.CreateSerializers(tt.accept, tt.group, tt.version, tt.resource)
 			if err != nil {
 				t.Fatalf("could not create serializer, %v", err)
@@ -1246,11 +1093,9 @@ func TestCacheListResponse(t *testing.T) {
 					t.Errorf("Got error %v", err)
 				}
 
-				objs, err := sWrapper.List(tt.key)
+				objs, err := storage.List(tt.key)
 				if err != nil {
-					if err != tt.cacheErr {
-						t.Errorf("expect error %v, but got %v", tt.cacheErr, err)
-					}
+					t.Errorf("failed to list objects from storage, %v", err)
 				}
 
 				if len(objs) != len(tt.expectResult.data) {
@@ -1275,28 +1120,29 @@ func TestCacheListResponse(t *testing.T) {
 					}
 				}
 			}
-			err = sWrapper.DeleteCollection("kubelet")
-			if err != nil {
-				t.Errorf("failed to delete collection: kubelet, %v", err)
-			}
+			resetStorage(storage, tt.key)
 		})
 	}
 }
 
 func TestQueryCacheForGet(t *testing.T) {
-	dStorage, err := disk.NewDiskStorage(rootDir)
-	if err != nil {
-		t.Errorf("failed to create disk storage, %v", err)
-	}
-	sWrapper := NewStorageWrapper(dStorage)
+	storage := NewFakeStorageWrapper()
 	serializerM := serializer.NewSerializerManager()
 	yurtCM := &cacheManager{
-		storage:           sWrapper,
+		storage:           storage,
 		serializerManager: serializerM,
 		cacheAgents:       make(map[string]bool),
 	}
 
-	testcases := map[string]struct {
+	type expectData struct {
+		err  bool
+		rv   string
+		name string
+		ns   string
+		kind string
+	}
+	tests := []struct {
+		desc         string
 		key          string
 		inputObj     runtime.Object
 		userAgent    string
@@ -1304,50 +1150,34 @@ func TestQueryCacheForGet(t *testing.T) {
 		verb         string
 		path         string
 		namespaced   bool
-		expectResult struct {
-			err  bool
-			rv   string
-			name string
-			ns   string
-			kind string
-		}
+		expectResult expectData
 	}{
-		"no client": {
+		{
+			desc:       "no client",
 			accept:     "application/json",
 			verb:       "GET",
 			path:       "/api/v1/namespaces/default/pods/mypod1",
 			namespaced: true,
-			expectResult: struct {
-				err  bool
-				rv   string
-				name string
-				ns   string
-				kind string
-			}{
+			expectResult: expectData{
 				err: true,
 			},
 		},
-		"not resource request": {
+		{
+			desc:       "not resource request",
 			accept:     "application/json",
 			verb:       "GET",
 			path:       "/healthz",
 			namespaced: true,
-			expectResult: struct {
-				err  bool
-				rv   string
-				name string
-				ns   string
-				kind string
-			}{
+			expectResult: expectData{
 				err: true,
 			},
 		},
-		"post pod": {
-			key: "kubelet/pods/default/mypod1",
+		{
+			desc: "query post pod",
+			key:  "kubelet/pods/default/mypod1",
 			inputObj: runtime.Object(&v1.Pod{
 				TypeMeta: metav1.TypeMeta{
-					APIVersion: "v1",
-					Kind:       "Pod",
+					Kind: "Pod",
 				},
 				ObjectMeta: metav1.ObjectMeta{
 					Name:            "mypod1",
@@ -1358,24 +1188,18 @@ func TestQueryCacheForGet(t *testing.T) {
 			userAgent:  "kubelet",
 			accept:     "application/json",
 			verb:       "POST",
-			path:       "/api/v1/namespaces/default/pods",
+			path:       "/api/v1/namespaces/default/pods/mypod1",
 			namespaced: true,
-			expectResult: struct {
-				err  bool
-				rv   string
-				name string
-				ns   string
-				kind string
-			}{
+			expectResult: expectData{
 				err: true,
 			},
 		},
-		"get pod": {
-			key: "kubelet/pods/default/mypod1",
+		{
+			desc: "query get pod",
+			key:  "kubelet/pods/default/mypod1",
 			inputObj: runtime.Object(&v1.Pod{
 				TypeMeta: metav1.TypeMeta{
-					APIVersion: "v1",
-					Kind:       "Pod",
+					Kind: "Pod",
 				},
 				ObjectMeta: metav1.ObjectMeta{
 					Name:            "mypod1",
@@ -1388,25 +1212,19 @@ func TestQueryCacheForGet(t *testing.T) {
 			verb:       "GET",
 			path:       "/api/v1/namespaces/default/pods/mypod1",
 			namespaced: true,
-			expectResult: struct {
-				err  bool
-				rv   string
-				name string
-				ns   string
-				kind string
-			}{
+			expectResult: expectData{
 				rv:   "1",
 				name: "mypod1",
 				ns:   "default",
 				kind: "Pod",
 			},
 		},
-		"update pod": {
-			key: "kubelet/pods/default/mypod2",
+		{
+			desc: "query update pod",
+			key:  "kubelet/pods/default/mypod2",
 			inputObj: runtime.Object(&v1.Pod{
 				TypeMeta: metav1.TypeMeta{
-					APIVersion: "v1",
-					Kind:       "Pod",
+					Kind: "Pod",
 				},
 				ObjectMeta: metav1.ObjectMeta{
 					Name:            "mypod2",
@@ -1419,25 +1237,19 @@ func TestQueryCacheForGet(t *testing.T) {
 			verb:       "PUT",
 			path:       "/api/v1/namespaces/default/pods/mypod2",
 			namespaced: true,
-			expectResult: struct {
-				err  bool
-				rv   string
-				name string
-				ns   string
-				kind string
-			}{
+			expectResult: expectData{
 				rv:   "2",
 				name: "mypod2",
 				ns:   "default",
 				kind: "Pod",
 			},
 		},
-		"update node": {
-			key: "kubelet/nodes/mynode1",
+		{
+			desc: "query update node",
+			key:  "kubelet/nodes/mynode1",
 			inputObj: runtime.Object(&v1.Node{
 				TypeMeta: metav1.TypeMeta{
-					APIVersion: "v1",
-					Kind:       "Node",
+					Kind: "Node",
 				},
 				ObjectMeta: metav1.ObjectMeta{
 					Name:            "mynode1",
@@ -1449,24 +1261,18 @@ func TestQueryCacheForGet(t *testing.T) {
 			verb:       "PUT",
 			path:       "/api/v1/nodes/mynode1",
 			namespaced: false,
-			expectResult: struct {
-				err  bool
-				rv   string
-				name string
-				ns   string
-				kind string
-			}{
+			expectResult: expectData{
 				rv:   "3",
 				name: "mynode1",
 				kind: "Node",
 			},
 		},
-		"patch node": {
-			key: "kubelet/nodes/mynode2",
+		{
+			desc: "query patch node",
+			key:  "kubelet/nodes/mynode2",
 			inputObj: runtime.Object(&v1.Node{
 				TypeMeta: metav1.TypeMeta{
-					APIVersion: "v1",
-					Kind:       "Node",
+					Kind: "Node",
 				},
 				ObjectMeta: metav1.ObjectMeta{
 					Name:            "mynode2",
@@ -1478,13 +1284,7 @@ func TestQueryCacheForGet(t *testing.T) {
 			verb:       "PATCH",
 			path:       "/api/v1/nodes/mynode2/status",
 			namespaced: false,
-			expectResult: struct {
-				err  bool
-				rv   string
-				name string
-				ns   string
-				kind string
-			}{
+			expectResult: expectData{
 				rv:   "4",
 				name: "mynode2",
 				kind: "Node",
@@ -1492,23 +1292,19 @@ func TestQueryCacheForGet(t *testing.T) {
 		},
 
 		//used to test whether the query local Custom Resource request can be handled correctly
-		"no client for crontab": {
+		{
+			desc:       "no client",
 			accept:     "application/json",
 			verb:       "GET",
 			path:       "/apis/stable.example.com/v1/namespaces/default/crontabs/crontab1",
 			namespaced: true,
-			expectResult: struct {
-				err  bool
-				rv   string
-				name string
-				ns   string
-				kind string
-			}{
+			expectResult: expectData{
 				err: true,
 			},
 		},
-		"query post crontab": {
-			key: "kubelet/crontabs/default/crontab1",
+		{
+			desc: "query post crontab",
+			key:  "kubelet/crontabs/default/crontab1",
 			inputObj: runtime.Object(&unstructured.Unstructured{
 				Object: map[string]interface{}{
 					"apiVersion": "stable.example.com/v1",
@@ -1525,18 +1321,13 @@ func TestQueryCacheForGet(t *testing.T) {
 			verb:       "POST",
 			path:       "/apis/stable.example.com/v1/namespaces/default/crontabs/crontab1",
 			namespaced: true,
-			expectResult: struct {
-				err  bool
-				rv   string
-				name string
-				ns   string
-				kind string
-			}{
+			expectResult: expectData{
 				err: true,
 			},
 		},
-		"query get crontab": {
-			key: "kubelet/crontabs/default/crontab1",
+		{
+			desc: "query get crontab",
+			key:  "kubelet/crontabs/default/crontab1",
 			inputObj: runtime.Object(&unstructured.Unstructured{
 				Object: map[string]interface{}{
 					"apiVersion": "stable.example.com/v1",
@@ -1553,21 +1344,16 @@ func TestQueryCacheForGet(t *testing.T) {
 			verb:       "GET",
 			path:       "/apis/stable.example.com/v1/namespaces/default/crontabs/crontab1",
 			namespaced: true,
-			expectResult: struct {
-				err  bool
-				rv   string
-				name string
-				ns   string
-				kind string
-			}{
+			expectResult: expectData{
 				rv:   "1",
 				name: "crontab1",
 				ns:   "default",
 				kind: "CronTab",
 			},
 		},
-		"query update crontab": {
-			key: "kubelet/crontabs/default/crontab2",
+		{
+			desc: "query update crontab",
+			key:  "kubelet/crontabs/default/crontab2",
 			inputObj: runtime.Object(&unstructured.Unstructured{
 				Object: map[string]interface{}{
 					"apiVersion": "stable.example.com/v1",
@@ -1584,21 +1370,16 @@ func TestQueryCacheForGet(t *testing.T) {
 			verb:       "PUT",
 			path:       "/apis/stable.example.com/v1/namespaces/default/crontabs/crontab2",
 			namespaced: true,
-			expectResult: struct {
-				err  bool
-				rv   string
-				name string
-				ns   string
-				kind string
-			}{
+			expectResult: expectData{
 				rv:   "2",
 				name: "crontab2",
 				ns:   "default",
 				kind: "CronTab",
 			},
 		},
-		"query patch crontab": {
-			key: "kubelet/crontabs/default/crontab3",
+		{
+			desc: "query patch crontab",
+			key:  "kubelet/crontabs/default/crontab3",
 			inputObj: runtime.Object(&unstructured.Unstructured{
 				Object: map[string]interface{}{
 					"apiVersion": "stable.example.com/v1",
@@ -1615,21 +1396,16 @@ func TestQueryCacheForGet(t *testing.T) {
 			verb:       "PATCH",
 			path:       "/apis/stable.example.com/v1/namespaces/default/crontabs/crontab3/status",
 			namespaced: true,
-			expectResult: struct {
-				err  bool
-				rv   string
-				name string
-				ns   string
-				kind string
-			}{
+			expectResult: expectData{
 				rv:   "4",
 				name: "crontab3",
 				ns:   "default",
 				kind: "CronTab",
 			},
 		},
-		"query post foo": {
-			key: "kubelet/foos/foo1",
+		{
+			desc: "query post foo",
+			key:  "kubelet/foos/foo1",
 			inputObj: runtime.Object(&unstructured.Unstructured{
 				Object: map[string]interface{}{
 					"apiVersion": "samplecontroller.k8s.io/v1",
@@ -1645,18 +1421,13 @@ func TestQueryCacheForGet(t *testing.T) {
 			verb:       "POST",
 			path:       "/apis/samplecontroller.k8s.io/v1/foos/foo1",
 			namespaced: false,
-			expectResult: struct {
-				err  bool
-				rv   string
-				name string
-				ns   string
-				kind string
-			}{
+			expectResult: expectData{
 				err: true,
 			},
 		},
-		"query get foo": {
-			key: "kubelet/foos/foo1",
+		{
+			desc: "query get foo",
+			key:  "kubelet/foos/foo1",
 			inputObj: runtime.Object(&unstructured.Unstructured{
 				Object: map[string]interface{}{
 					"apiVersion": "samplecontroller.k8s.io/v1",
@@ -1672,20 +1443,15 @@ func TestQueryCacheForGet(t *testing.T) {
 			verb:       "GET",
 			path:       "/apis/samplecontroller.k8s.io/v1/foos/foo1",
 			namespaced: false,
-			expectResult: struct {
-				err  bool
-				rv   string
-				name string
-				ns   string
-				kind string
-			}{
+			expectResult: expectData{
 				rv:   "1",
 				name: "foo1",
 				kind: "Foo",
 			},
 		},
-		"query update foo": {
-			key: "kubelet/foos/foo2",
+		{
+			desc: "query update foo",
+			key:  "kubelet/foos/foo2",
 			inputObj: runtime.Object(&unstructured.Unstructured{
 				Object: map[string]interface{}{
 					"apiVersion": "samplecontroller.k8s.io/v1",
@@ -1701,20 +1467,15 @@ func TestQueryCacheForGet(t *testing.T) {
 			verb:       "PUT",
 			path:       "/apis/samplecontroller.k8s.io/v1/foos/foo2",
 			namespaced: false,
-			expectResult: struct {
-				err  bool
-				rv   string
-				name string
-				ns   string
-				kind string
-			}{
+			expectResult: expectData{
 				rv:   "2",
 				name: "foo2",
 				kind: "Foo",
 			},
 		},
-		"query patch foo": {
-			key: "kubelet/foos/foo3",
+		{
+			desc: "query patch foo",
+			key:  "kubelet/foos/foo3",
 			inputObj: runtime.Object(&unstructured.Unstructured{
 				Object: map[string]interface{}{
 					"apiVersion": "samplecontroller.k8s.io/v1",
@@ -1730,13 +1491,7 @@ func TestQueryCacheForGet(t *testing.T) {
 			verb:       "PATCH",
 			path:       "/apis/samplecontroller.k8s.io/v1/foos/foo3/status",
 			namespaced: false,
-			expectResult: struct {
-				err  bool
-				rv   string
-				name string
-				ns   string
-				kind string
-			}{
+			expectResult: expectData{
 				rv:   "4",
 				name: "foo3",
 				kind: "Foo",
@@ -1746,9 +1501,9 @@ func TestQueryCacheForGet(t *testing.T) {
 
 	accessor := meta.NewAccessor()
 	resolver := newTestRequestInfoResolver()
-	for k, tt := range testcases {
-		t.Run(k, func(t *testing.T) {
-			_ = sWrapper.Create(tt.key, tt.inputObj)
+	for _, tt := range tests {
+		t.Run(tt.desc, func(t *testing.T) {
+			_ = storage.Create(tt.key, tt.inputObj)
 			req, _ := http.NewRequest(tt.verb, tt.path, nil)
 			if len(tt.userAgent) != 0 {
 				req.Header.Set("User-Agent", tt.userAgent)
@@ -1804,64 +1559,52 @@ func TestQueryCacheForGet(t *testing.T) {
 					t.Errorf("Got kind %s, but expect kind %s", kind, tt.expectResult.kind)
 				}
 			}
-
-			err = sWrapper.DeleteCollection("kubelet")
-			if err != nil {
-				t.Errorf("failed to delete collection: kubelet, %v", err)
-			}
 		})
 	}
 }
 
 func TestQueryCacheForList(t *testing.T) {
-	dStorage, err := disk.NewDiskStorage(rootDir)
-	if err != nil {
-		t.Errorf("failed to create disk storage, %v", err)
-	}
-	sWrapper := NewStorageWrapper(dStorage)
+	storage := NewFakeStorageWrapper()
 	serializerM := serializer.NewSerializerManager()
 	yurtCM := &cacheManager{
-		storage:           sWrapper,
+		storage:           storage,
 		serializerManager: serializerM,
 		cacheAgents:       make(map[string]bool),
 	}
 
-	testcases := map[string]struct {
+	type expectData struct {
+		err  bool
+		rv   string
+		data map[string]struct{}
+	}
+	tests := []struct {
+		desc         string
 		keyPrefix    string
-		noObjs       bool
 		inputObj     []runtime.Object
 		userAgent    string
 		accept       string
 		verb         string
 		path         string
 		namespaced   bool
-		expectResult struct {
-			err  bool
-			rv   string
-			data map[string]struct{}
-		}
-		queryErr error
+		expectResult expectData
 	}{
-		"list with no user agent": {
+		{
+			desc:       "no user agent",
 			accept:     "application/json",
 			verb:       "GET",
 			path:       "/api/v1/namespaces/default/pods",
 			namespaced: true,
-			expectResult: struct {
-				err  bool
-				rv   string
-				data map[string]struct{}
-			}{
+			expectResult: expectData{
 				err: true,
 			},
 		},
-		"list pods": {
+		{
+			desc:      "query list pods",
 			keyPrefix: "kubelet/pods/default",
 			inputObj: []runtime.Object{
 				&v1.Pod{
 					TypeMeta: metav1.TypeMeta{
-						APIVersion: "v1",
-						Kind:       "Pod",
+						Kind: "Pod",
 					},
 					ObjectMeta: metav1.ObjectMeta{
 						Name:            "mypod1",
@@ -1871,8 +1614,7 @@ func TestQueryCacheForList(t *testing.T) {
 				},
 				&v1.Pod{
 					TypeMeta: metav1.TypeMeta{
-						APIVersion: "v1",
-						Kind:       "Pod",
+						Kind: "Pod",
 					},
 					ObjectMeta: metav1.ObjectMeta{
 						Name:            "mypod2",
@@ -1882,8 +1624,7 @@ func TestQueryCacheForList(t *testing.T) {
 				},
 				&v1.Pod{
 					TypeMeta: metav1.TypeMeta{
-						APIVersion: "v1",
-						Kind:       "Pod",
+						Kind: "Pod",
 					},
 					ObjectMeta: metav1.ObjectMeta{
 						Name:            "mypod3",
@@ -1897,11 +1638,7 @@ func TestQueryCacheForList(t *testing.T) {
 			verb:       "GET",
 			path:       "/api/v1/namespaces/default/pods",
 			namespaced: true,
-			expectResult: struct {
-				err  bool
-				rv   string
-				data map[string]struct{}
-			}{
+			expectResult: expectData{
 				rv: "5",
 				data: map[string]struct{}{
 					"pod-default-mypod1-1": {},
@@ -1910,13 +1647,13 @@ func TestQueryCacheForList(t *testing.T) {
 				},
 			},
 		},
-		"list nodes": {
+		{
+			desc:      "query list nodes",
 			keyPrefix: "kubelet/nodes",
 			inputObj: []runtime.Object{
 				&v1.Node{
 					TypeMeta: metav1.TypeMeta{
-						APIVersion: "v1",
-						Kind:       "Node",
+						Kind: "Node",
 					},
 					ObjectMeta: metav1.ObjectMeta{
 						Name:            "mynode1",
@@ -1925,8 +1662,7 @@ func TestQueryCacheForList(t *testing.T) {
 				},
 				&v1.Node{
 					TypeMeta: metav1.TypeMeta{
-						APIVersion: "v1",
-						Kind:       "Node",
+						Kind: "Node",
 					},
 					ObjectMeta: metav1.ObjectMeta{
 						Name:            "mynode2",
@@ -1935,8 +1671,7 @@ func TestQueryCacheForList(t *testing.T) {
 				},
 				&v1.Node{
 					TypeMeta: metav1.TypeMeta{
-						APIVersion: "v1",
-						Kind:       "Node",
+						Kind: "Node",
 					},
 					ObjectMeta: metav1.ObjectMeta{
 						Name:            "mynode3",
@@ -1945,8 +1680,7 @@ func TestQueryCacheForList(t *testing.T) {
 				},
 				&v1.Node{
 					TypeMeta: metav1.TypeMeta{
-						APIVersion: "v1",
-						Kind:       "Node",
+						Kind: "Node",
 					},
 					ObjectMeta: metav1.ObjectMeta{
 						Name:            "mynode4",
@@ -1959,11 +1693,7 @@ func TestQueryCacheForList(t *testing.T) {
 			verb:       "GET",
 			path:       "/api/v1/nodes",
 			namespaced: false,
-			expectResult: struct {
-				err  bool
-				rv   string
-				data map[string]struct{}
-			}{
+			expectResult: expectData{
 				rv: "12",
 				data: map[string]struct{}{
 					"node-mynode1-6":  {},
@@ -1975,7 +1705,8 @@ func TestQueryCacheForList(t *testing.T) {
 		},
 
 		//used to test whether the query local Custom Resource list request can be handled correctly
-		"list crontabs": {
+		{
+			desc:      "query list crontabs",
 			keyPrefix: "kubelet/crontabs/default",
 			inputObj: []runtime.Object{
 				&unstructured.Unstructured{
@@ -2017,11 +1748,7 @@ func TestQueryCacheForList(t *testing.T) {
 			verb:       "GET",
 			path:       "/apis/stable.example.com/v1/namespaces/default/crontabs",
 			namespaced: true,
-			expectResult: struct {
-				err  bool
-				rv   string
-				data map[string]struct{}
-			}{
+			expectResult: expectData{
 				rv: "5",
 				data: map[string]struct{}{
 					"crontab-default-crontab1-1": {},
@@ -2030,7 +1757,8 @@ func TestQueryCacheForList(t *testing.T) {
 				},
 			},
 		},
-		"list foos": {
+		{
+			desc:      "query list foos",
 			keyPrefix: "kubelet/foos",
 			inputObj: []runtime.Object{
 				&unstructured.Unstructured{
@@ -2069,11 +1797,7 @@ func TestQueryCacheForList(t *testing.T) {
 			verb:       "GET",
 			path:       "/apis/samplecontroller.k8s.io/v1/foos",
 			namespaced: false,
-			expectResult: struct {
-				err  bool
-				rv   string
-				data map[string]struct{}
-			}{
+			expectResult: expectData{
 				rv: "5",
 				data: map[string]struct{}{
 					"foo-foo1-1": {},
@@ -2082,68 +1806,16 @@ func TestQueryCacheForList(t *testing.T) {
 				},
 			},
 		},
-		"list runtimeclass": {
-			keyPrefix:  "kubelet/runtimeclasses",
-			noObjs:     true,
-			userAgent:  "kubelet",
-			accept:     "application/json",
-			verb:       "GET",
-			path:       "/apis/node.k8s.io/v1beta1/runtimeclasses",
-			namespaced: false,
-			expectResult: struct {
-				err  bool
-				rv   string
-				data map[string]struct{}
-			}{
-				data: map[string]struct{}{},
-			},
-		},
-		"list pods and no pods in cache": {
-			keyPrefix:  "kubelet/pods",
-			noObjs:     true,
-			userAgent:  "kubelet",
-			accept:     "application/json",
-			verb:       "GET",
-			path:       "/api/v1/pods",
-			namespaced: false,
-			expectResult: struct {
-				err  bool
-				rv   string
-				data map[string]struct{}
-			}{
-				err: true,
-			},
-			queryErr: storage.ErrStorageNotFound,
-		},
-		"list resources not exist": {
-			userAgent:  "kubelet",
-			accept:     "application/json",
-			verb:       "GET",
-			path:       "/api/v1/nodes",
-			namespaced: false,
-			expectResult: struct {
-				err  bool
-				rv   string
-				data map[string]struct{}
-			}{
-				err: true,
-			},
-			queryErr: storage.ErrStorageNotFound,
-		},
 	}
 
 	accessor := meta.NewAccessor()
 	resolver := newTestRequestInfoResolver()
-	for k, tt := range testcases {
-		t.Run(k, func(t *testing.T) {
+	for _, tt := range tests {
+		t.Run(tt.desc, func(t *testing.T) {
 			for i := range tt.inputObj {
 				v, _ := accessor.Name(tt.inputObj[i])
 				key := filepath.Join(tt.keyPrefix, v)
-				_ = sWrapper.Create(key, tt.inputObj[i])
-			}
-
-			if tt.noObjs {
-				_ = sWrapper.Create(tt.keyPrefix, nil)
+				_ = storage.Create(key, tt.inputObj[i])
 			}
 
 			req, _ := http.NewRequest(tt.verb, tt.path, nil)
@@ -2178,16 +1850,12 @@ func TestQueryCacheForList(t *testing.T) {
 				if err == nil {
 					t.Errorf("Got no error, but expect err")
 				}
-
-				if tt.queryErr != nil && tt.queryErr != err {
-					t.Errorf("expect err %v, but got %v", tt.queryErr, err)
-				}
 			} else {
 				if err != nil {
 					t.Errorf("Got error %v", err)
 				}
 
-				if tt.expectResult.rv != "" && tt.expectResult.rv != rv {
+				if tt.expectResult.rv != rv {
 					t.Errorf("Got rv %s, but expect rv %s", rv, tt.expectResult.rv)
 				}
 
@@ -2212,272 +1880,164 @@ func TestQueryCacheForList(t *testing.T) {
 						t.Errorf("Got item key %s, but expect key %s", itemKey, expectKey)
 					}
 				}
-			}
-			err = sWrapper.DeleteCollection("kubelet")
-			if err != nil {
-				t.Errorf("failed to delete collection: kubelet, %v", err)
+
 			}
 		})
 	}
 }
 
 func TestCanCacheFor(t *testing.T) {
-	dStorage, err := disk.NewDiskStorage(rootDir)
-	if err != nil {
-		t.Errorf("failed to create disk storage, %v", err)
-	}
-	s := NewStorageWrapper(dStorage)
+	s := NewFakeStorageWrapper()
 	m, _ := NewCacheManager(s, nil)
 
-	type proxyRequest struct {
-		userAgent string
-		verb      string
-		path      string
-		header    map[string]string
-	}
-
-	testcases := map[string]struct {
-		preRequest     *proxyRequest
-		preExpectCache bool
-		request        *proxyRequest
-		expectCache    bool
+	tests := []struct {
+		desc        string
+		userAgent   string
+		verb        string
+		path        string
+		header      map[string]string
+		expectCache bool
 	}{
-		"no user agent": {
-			request: &proxyRequest{
-				verb: "GET",
-				path: "/api/v1/nodes/mynode",
-			},
+		{
+			desc:        "no user agent",
+			verb:        "GET",
+			path:        "/api/v1/nodes/mynode",
 			expectCache: false,
 		},
-		"not default user agent": {
-			request: &proxyRequest{
-				userAgent: "kubelet-test",
-				verb:      "GET",
-				path:      "/api/v1/nodes/mynode",
-			},
+		{
+			desc:        "not default user agent",
+			userAgent:   "kubelet-test",
+			verb:        "GET",
+			path:        "/api/v1/nodes/mynode",
 			expectCache: false,
 		},
-		"default user agent kubelet": {
-			request: &proxyRequest{
-				userAgent: "kubelet",
-				verb:      "GET",
-				path:      "/api/v1/nodes/mynode",
-			},
+		{
+			desc:        "default user agent kubelet",
+			userAgent:   "kubelet",
+			verb:        "GET",
+			path:        "/api/v1/nodes/mynode",
 			expectCache: true,
 		},
-		"default user agent flanneld": {
-			request: &proxyRequest{
-				userAgent: "flanneld",
-				verb:      "POST",
-				path:      "/api/v1/nodes/mynode",
-			},
+		{
+			desc:        "default user agent flanneld",
+			userAgent:   "flanneld",
+			verb:        "POST",
+			path:        "/api/v1/nodes/mynode",
 			expectCache: true,
 		},
-		"default user agent coredns": {
-			request: &proxyRequest{
-				userAgent: "coredns",
-				verb:      "PUT",
-				path:      "/api/v1/nodes/mynode",
-			},
+		{
+			desc:        "default user agent coredns",
+			userAgent:   "coredns",
+			verb:        "PUT",
+			path:        "/api/v1/nodes/mynode",
 			expectCache: true,
 		},
-		"default user agent kube-proxy": {
-			request: &proxyRequest{
-				userAgent: "kube-proxy",
-				verb:      "PATCH",
-				path:      "/api/v1/nodes/mynode",
-			},
+		{
+			desc:        "default user agent kube-proxy",
+			userAgent:   "kube-proxy",
+			verb:        "PATCH",
+			path:        "/api/v1/nodes/mynode",
 			expectCache: true,
 		},
-		"default user agent edge-tunnel-agent": {
-			request: &proxyRequest{
-				userAgent: "edge-tunnel-agent",
-				verb:      "HEAD",
-				path:      "/api/v1/nodes/mynode",
-			},
+		{
+			desc:        "default user agent edge-tunnel-agent",
+			userAgent:   "edge-tunnel-agent",
+			verb:        "HEAD",
+			path:        "/api/v1/nodes/mynode",
 			expectCache: true,
 		},
-		"with cache header": {
-			request: &proxyRequest{
-				userAgent: "test1",
-				verb:      "GET",
-				path:      "/api/v1/nodes/mynode",
-				header:    map[string]string{"Edge-Cache": "true"},
-			},
+		{
+			desc:        "with cache header",
+			userAgent:   "test1",
+			verb:        "GET",
+			path:        "/api/v1/nodes/mynode",
+			header:      map[string]string{"Edge-Cache": "true"},
 			expectCache: true,
 		},
-		"with cache header false": {
-			request: &proxyRequest{
-				userAgent: "test2",
-				verb:      "GET",
-				path:      "/api/v1/nodes/mynode",
-				header:    map[string]string{"Edge-Cache": "false"},
-			},
+		{
+			desc:        "with cache header false",
+			userAgent:   "test2",
+			verb:        "GET",
+			path:        "/api/v1/nodes/mynode",
+			header:      map[string]string{"Edge-Cache": "false"},
 			expectCache: false,
 		},
-		"not resource request": {
-			request: &proxyRequest{
-				userAgent: "test2",
-				verb:      "GET",
-				path:      "/healthz",
-				header:    map[string]string{"Edge-Cache": "true"},
-			},
+		{
+			desc:        "not resource request",
+			userAgent:   "test2",
+			verb:        "GET",
+			path:        "/healthz",
+			header:      map[string]string{"Edge-Cache": "true"},
 			expectCache: false,
 		},
-		"delete request": {
-			request: &proxyRequest{
-				userAgent: "kubelet",
-				verb:      "DELETE",
-				path:      "/api/v1/nodes/mynode",
-			},
+		{
+			desc:        "delete request",
+			userAgent:   "kubelet",
+			verb:        "DELETE",
+			path:        "/api/v1/nodes/mynode",
 			expectCache: false,
 		},
-		"delete collection request": {
-			request: &proxyRequest{
-				userAgent: "kubelet",
-				verb:      "DELETE",
-				path:      "/api/v1/namespaces/default/pods",
-			},
-			expectCache: false,
-		},
-		"proxy request": {
-			request: &proxyRequest{
-				userAgent: "kubelet",
-				verb:      "GET",
-				path:      "/api/v1/proxy/namespaces/default/pods/test",
-			},
+		{
+			desc:        "delete collection request",
+			userAgent:   "kubelet",
+			verb:        "DELETE",
+			path:        "/api/v1/namespaces/default/pods",
 			expectCache: false,
 		},
-		"get status sub resource request": {
-			request: &proxyRequest{
-				userAgent: "kubelet",
-				verb:      "GET",
-				path:      "/api/v1/namespaces/default/pods/test/status",
-			},
-			expectCache: true,
-		},
-		"get not status sub resource request": {
-			request: &proxyRequest{
-				userAgent: "kubelet",
-				verb:      "GET",
-				path:      "/api/v1/namespaces/default/pods/test/proxy",
-			},
+		{
+			desc:        "proxy request",
+			userAgent:   "kubelet",
+			verb:        "GET",
+			path:        "/api/v1/proxy/namespaces/default/pods/test",
 			expectCache: false,
 		},
-		"list requests with no selectors": {
-			preRequest: &proxyRequest{
-				userAgent: "kubelet",
-				verb:      "GET",
-				path:      "/api/v1/namespaces/default/pods",
-			},
-			preExpectCache: true,
-			request: &proxyRequest{
-				userAgent: "kubelet",
-				verb:      "GET",
-				path:      "/api/v1/namespaces/default/pods",
-			},
-			expectCache: true,
-		},
-		"list requests with label selectors": {
-			preRequest: &proxyRequest{
-				userAgent: "kubelet",
-				verb:      "GET",
-				path:      "/api/v1/namespaces/kube-system/pods?labelSelector=foo=bar",
-			},
-			preExpectCache: true,
-			request: &proxyRequest{
-				userAgent: "kubelet",
-				verb:      "GET",
-				path:      "/api/v1/namespaces/kube-system/pods?labelSelector=foo=bar",
-			},
-			expectCache: true,
-		},
-		"list requests with field selectors": {
-			preRequest: &proxyRequest{
-				userAgent: "kubelet",
-				verb:      "GET",
-				path:      "/api/v1/namespaces/test2/pods?fieldSelector=spec.nodeName=test",
-			},
-			preExpectCache: true,
-			request: &proxyRequest{
-				userAgent: "kubelet",
-				verb:      "GET",
-				path:      "/api/v1/namespaces/test2/pods?fieldSelector=spec.nodeName=test",
-			},
+		{
+			desc:        "get status sub resource request",
+			userAgent:   "kubelet",
+			verb:        "GET",
+			path:        "/api/v1/namespaces/default/pods/test/status",
 			expectCache: true,
 		},
-		"list requests have same path but with different selectors": {
-			preRequest: &proxyRequest{
-				userAgent: "kubelet",
-				verb:      "GET",
-				path:      "/api/v1/namespaces/test2/secrets?labelSelector=foo=bar1",
-			},
-			preExpectCache: true,
-			request: &proxyRequest{
-				userAgent: "kubelet",
-				verb:      "GET",
-				path:      "/api/v1/namespaces/test2/secrets?labelSelector=foo=bar2",
-			},
-			expectCache: false,
-		},
-		"list requests get same resouces but with different path": {
-			preRequest: &proxyRequest{
-				userAgent: "kubelet",
-				verb:      "GET",
-				path:      "/api/v1/namespaces/test2/configmaps?labelSelector=foo=bar1",
-			},
-			preExpectCache: true,
-			request: &proxyRequest{
-				userAgent: "kubelet",
-				verb:      "GET",
-				path:      "/api/v1/configmaps?labelSelector=foo=bar2",
-			},
+		{
+			desc:        "get not status sub resource request",
+			userAgent:   "kubelet",
+			verb:        "GET",
+			path:        "/api/v1/namespaces/default/pods/test/proxy",
 			expectCache: false,
 		},
 	}
 
-	for k, tt := range testcases {
-		t.Run(k, func(t *testing.T) {
-			if tt.preRequest != nil {
-				reqCanCache := checkReqCanCache(m, tt.preRequest.userAgent, tt.preRequest.verb, tt.preRequest.path, tt.preRequest.header)
-				if reqCanCache != tt.preExpectCache {
-					t.Errorf("Got request pre can cache %v, but expect request pre can cache %v", reqCanCache, tt.preExpectCache)
-				}
+	resolver := newTestRequestInfoResolver()
+	for _, tt := range tests {
+		t.Run(tt.desc, func(t *testing.T) {
+
+			req, _ := http.NewRequest(tt.verb, tt.path, nil)
+			if len(tt.userAgent) != 0 {
+				req.Header.Set("User-Agent", tt.userAgent)
 			}
 
-			if tt.request != nil {
-				reqCanCache := checkReqCanCache(m, tt.request.userAgent, tt.request.verb, tt.request.path, tt.request.header)
-				if reqCanCache != tt.expectCache {
-					t.Errorf("Got request can cache %v, but expect request can cache %v", reqCanCache, tt.expectCache)
+			if len(tt.header) != 0 {
+				for k, v := range tt.header {
+					req.Header.Set(k, v)
 				}
 			}
-		})
-	}
-}
-
-func checkReqCanCache(m CacheManager, userAgent, verb, path string, header map[string]string) bool {
-	req, _ := http.NewRequest(verb, path, nil)
-	if len(userAgent) != 0 {
-		req.Header.Set("User-Agent", userAgent)
-	}
 
-	for k, v := range header {
-		req.Header.Set(k, v)
-	}
-
-	req.RemoteAddr = "127.0.0.1"
+			req.RemoteAddr = "127.0.0.1"
 
-	var reqCanCache bool
-	var handler http.Handler = http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) {
-		reqCanCache = m.CanCacheFor(req)
+			var reqCanCache bool
+			var handler http.Handler = http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) {
+				reqCanCache = m.CanCacheFor(req)
 
-	})
+			})
 
-	handler = proxyutil.WithListRequestSelector(handler)
-	handler = proxyutil.WithCacheHeaderCheck(handler)
-	handler = proxyutil.WithRequestClientComponent(handler)
-	handler = filters.WithRequestInfo(handler, newTestRequestInfoResolver())
-	handler.ServeHTTP(httptest.NewRecorder(), req)
+			handler = proxyutil.WithCacheHeaderCheck(handler)
+			handler = proxyutil.WithRequestClientComponent(handler)
+			handler = filters.WithRequestInfo(handler, resolver)
+			handler.ServeHTTP(httptest.NewRecorder(), req)
 
-	return reqCanCache
+			if reqCanCache != tt.expectCache {
+				t.Errorf("Got request can cache %v, but expect request can cache %v", reqCanCache, tt.expectCache)
+			}
+		})
+	}
 }
diff --git a/pkg/yurthub/proxy/local/local_test.go b/pkg/yurthub/proxy/local/local_test.go
index 7312d80f..84bc8b0c 100644
--- a/pkg/yurthub/proxy/local/local_test.go
+++ b/pkg/yurthub/proxy/local/local_test.go
@@ -28,8 +28,6 @@ import (
 	"github.com/openyurtio/openyurt/pkg/yurthub/cachemanager"
 	"github.com/openyurtio/openyurt/pkg/yurthub/kubernetes/serializer"
 	proxyutil "github.com/openyurtio/openyurt/pkg/yurthub/proxy/util"
-	"github.com/openyurtio/openyurt/pkg/yurthub/storage/disk"
-
 	v1 "k8s.io/api/core/v1"
 	"k8s.io/apimachinery/pkg/api/meta"
 	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
@@ -39,10 +37,6 @@ import (
 	"k8s.io/apiserver/pkg/endpoints/request"
 )
 
-var (
-	rootDir = "/tmp/cache-local"
-)
-
 func newTestRequestInfoResolver() *request.RequestInfoFactory {
 	return &request.RequestInfoFactory{
 		APIPrefixes:          sets.NewString("api", "apis"),
@@ -51,13 +45,9 @@ func newTestRequestInfoResolver() *request.RequestInfoFactory {
 }
 
 func TestServeHTTPForWatch(t *testing.T) {
-	dStorage, err := disk.NewDiskStorage(rootDir)
-	if err != nil {
-		t.Errorf("failed to create disk storage, %v", err)
-	}
-	sWrapper := cachemanager.NewStorageWrapper(dStorage)
+	storage := cachemanager.NewFakeStorageWrapper()
 	serializerM := serializer.NewSerializerManager()
-	cacheM, _ := cachemanager.NewCacheManager(sWrapper, serializerM)
+	cacheM, _ := cachemanager.NewCacheManager(storage, serializerM)
 
 	fn := func() bool {
 		return false
@@ -65,7 +55,8 @@ func TestServeHTTPForWatch(t *testing.T) {
 
 	lp := NewLocalProxy(cacheM, fn)
 
-	testcases := map[string]struct {
+	testcases := []struct {
+		desc      string
 		userAgent string
 		accept    string
 		verb      string
@@ -74,7 +65,8 @@ func TestServeHTTPForWatch(t *testing.T) {
 		floor     time.Duration
 		ceil      time.Duration
 	}{
-		"watch request": {
+		{
+			desc:      "watch request",
 			userAgent: "kubelet",
 			accept:    "application/json",
 			verb:      "GET",
@@ -83,7 +75,8 @@ func TestServeHTTPForWatch(t *testing.T) {
 			floor:     4 * time.Second,
 			ceil:      6 * time.Second,
 		},
-		"watch request without timeout": {
+		{
+			desc:      "watch request without timeout",
 			userAgent: "kubelet",
 			accept:    "application/json",
 			verb:      "GET",
@@ -95,8 +88,8 @@ func TestServeHTTPForWatch(t *testing.T) {
 
 	resolver := newTestRequestInfoResolver()
 
-	for k, tt := range testcases {
-		t.Run(k, func(t *testing.T) {
+	for _, tt := range testcases {
+		t.Run(tt.desc, func(t *testing.T) {
 			req, _ := http.NewRequest(tt.verb, tt.path, nil)
 			if len(tt.accept) != 0 {
 				req.Header.Set("Accept", tt.accept)
@@ -139,13 +132,9 @@ func TestServeHTTPForWatch(t *testing.T) {
 }
 
 func TestServeHTTPForWatchWithHealthyChange(t *testing.T) {
-	dStorage, err := disk.NewDiskStorage(rootDir)
-	if err != nil {
-		t.Errorf("failed to create disk storage, %v", err)
-	}
-	sWrapper := cachemanager.NewStorageWrapper(dStorage)
+	storage := cachemanager.NewFakeStorageWrapper()
 	serializerM := serializer.NewSerializerManager()
-	cacheM, _ := cachemanager.NewCacheManager(sWrapper, serializerM)
+	cacheM, _ := cachemanager.NewCacheManager(storage, serializerM)
 
 	cnt := 0
 	fn := func() bool {
@@ -155,7 +144,8 @@ func TestServeHTTPForWatchWithHealthyChange(t *testing.T) {
 
 	lp := NewLocalProxy(cacheM, fn)
 
-	testcases := map[string]struct {
+	testcases := []struct {
+		desc      string
 		userAgent string
 		accept    string
 		verb      string
@@ -164,7 +154,8 @@ func TestServeHTTPForWatchWithHealthyChange(t *testing.T) {
 		floor     time.Duration
 		ceil      time.Duration
 	}{
-		"watch request": {
+		{
+			desc:      "watch request",
 			userAgent: "kubelet",
 			accept:    "application/json",
 			verb:      "GET",
@@ -177,8 +168,8 @@ func TestServeHTTPForWatchWithHealthyChange(t *testing.T) {
 
 	resolver := newTestRequestInfoResolver()
 
-	for k, tt := range testcases {
-		t.Run(k, func(t *testing.T) {
+	for _, tt := range testcases {
+		t.Run(tt.desc, func(t *testing.T) {
 			req, _ := http.NewRequest(tt.verb, tt.path, nil)
 			if len(tt.accept) != 0 {
 				req.Header.Set("Accept", tt.accept)
@@ -221,13 +212,9 @@ func TestServeHTTPForWatchWithHealthyChange(t *testing.T) {
 }
 
 func TestServeHTTPForPost(t *testing.T) {
-	dStorage, err := disk.NewDiskStorage(rootDir)
-	if err != nil {
-		t.Errorf("failed to create disk storage, %v", err)
-	}
-	sWrapper := cachemanager.NewStorageWrapper(dStorage)
+	storage := cachemanager.NewFakeStorageWrapper()
 	serializerM := serializer.NewSerializerManager()
-	cacheM, _ := cachemanager.NewCacheManager(sWrapper, serializerM)
+	cacheM, _ := cachemanager.NewCacheManager(storage, serializerM)
 
 	fn := func() bool {
 		return false
@@ -235,7 +222,8 @@ func TestServeHTTPForPost(t *testing.T) {
 
 	lp := NewLocalProxy(cacheM, fn)
 
-	testcases := map[string]struct {
+	testcases := []struct {
+		desc      string
 		userAgent string
 		accept    string
 		verb      string
@@ -243,7 +231,8 @@ func TestServeHTTPForPost(t *testing.T) {
 		data      string
 		code      int
 	}{
-		"post request": {
+		{
+			desc:      "post request",
 			userAgent: "kubelet",
 			accept:    "application/json",
 			verb:      "POST",
@@ -255,8 +244,8 @@ func TestServeHTTPForPost(t *testing.T) {
 
 	resolver := newTestRequestInfoResolver()
 
-	for k, tt := range testcases {
-		t.Run(k, func(t *testing.T) {
+	for _, tt := range testcases {
+		t.Run(tt.desc, func(t *testing.T) {
 			buf := bytes.NewBufferString(tt.data)
 			req, _ := http.NewRequest(tt.verb, tt.path, buf)
 			if len(tt.accept) != 0 {
@@ -297,13 +286,9 @@ func TestServeHTTPForPost(t *testing.T) {
 }
 
 func TestServeHTTPForDelete(t *testing.T) {
-	dStorage, err := disk.NewDiskStorage(rootDir)
-	if err != nil {
-		t.Errorf("failed to create disk storage, %v", err)
-	}
-	sWrapper := cachemanager.NewStorageWrapper(dStorage)
+	storage := cachemanager.NewFakeStorageWrapper()
 	serializerM := serializer.NewSerializerManager()
-	cacheM, _ := cachemanager.NewCacheManager(sWrapper, serializerM)
+	cacheM, _ := cachemanager.NewCacheManager(storage, serializerM)
 
 	fn := func() bool {
 		return false
@@ -311,14 +296,16 @@ func TestServeHTTPForDelete(t *testing.T) {
 
 	lp := NewLocalProxy(cacheM, fn)
 
-	testcases := map[string]struct {
+	testcases := []struct {
+		desc      string
 		userAgent string
 		accept    string
 		verb      string
 		path      string
 		code      int
 	}{
-		"delete request": {
+		{
+			desc:      "delete request",
 			userAgent: "kubelet",
 			accept:    "application/json",
 			verb:      "DELETE",
@@ -329,8 +316,8 @@ func TestServeHTTPForDelete(t *testing.T) {
 
 	resolver := newTestRequestInfoResolver()
 
-	for k, tt := range testcases {
-		t.Run(k, func(t *testing.T) {
+	for _, tt := range testcases {
+		t.Run(tt.desc, func(t *testing.T) {
 			req, _ := http.NewRequest(tt.verb, tt.path, nil)
 			if len(tt.accept) != 0 {
 				req.Header.Set("Accept", tt.accept)
@@ -360,13 +347,9 @@ func TestServeHTTPForDelete(t *testing.T) {
 }
 
 func TestServeHTTPForGetReqCache(t *testing.T) {
-	dStorage, err := disk.NewDiskStorage(rootDir)
-	if err != nil {
-		t.Errorf("failed to create disk storage, %v", err)
-	}
-	sWrapper := cachemanager.NewStorageWrapper(dStorage)
+	storage := cachemanager.NewFakeStorageWrapper()
 	serializerM := serializer.NewSerializerManager()
-	cacheM, _ := cachemanager.NewCacheManager(sWrapper, serializerM)
+	cacheM, _ := cachemanager.NewCacheManager(storage, serializerM)
 
 	fn := func() bool {
 		return false
@@ -374,7 +357,14 @@ func TestServeHTTPForGetReqCache(t *testing.T) {
 
 	lp := NewLocalProxy(cacheM, fn)
 
-	testcases := map[string]struct {
+	type expectData struct {
+		ns   string
+		name string
+		rv   string
+	}
+
+	testcases := []struct {
+		desc      string
 		userAgent string
 		keyPrefix string
 		inputObj  []runtime.Object
@@ -383,13 +373,10 @@ func TestServeHTTPForGetReqCache(t *testing.T) {
 		path      string
 		resource  string
 		code      int
-		data      struct {
-			ns   string
-			name string
-			rv   string
-		}
+		data      expectData
 	}{
-		"get pod request": {
+		{
+			desc:      "get pod request",
 			userAgent: "kubelet",
 			keyPrefix: "kubelet/pods/default",
 			inputObj: []runtime.Object{
@@ -410,11 +397,7 @@ func TestServeHTTPForGetReqCache(t *testing.T) {
 			path:     "/api/v1/namespaces/default/pods/mypod1",
 			resource: "pods",
 			code:     http.StatusOK,
-			data: struct {
-				ns   string
-				name string
-				rv   string
-			}{
+			data: expectData{
 				ns:   "default",
 				name: "mypod1",
 				rv:   "1",
@@ -423,14 +406,15 @@ func TestServeHTTPForGetReqCache(t *testing.T) {
 	}
 
 	resolver := newTestRequestInfoResolver()
-	for k, tt := range testcases {
-		t.Run(k, func(t *testing.T) {
+
+	for _, tt := range testcases {
+		t.Run(tt.desc, func(t *testing.T) {
 			jsonDecoder, _ := serializerM.CreateSerializers(tt.accept, "", "v1", tt.resource)
 			accessor := meta.NewAccessor()
 			for i := range tt.inputObj {
 				name, _ := accessor.Name(tt.inputObj[i])
 				key := filepath.Join(tt.keyPrefix, name)
-				_ = sWrapper.Update(key, tt.inputObj[i])
+				_ = storage.Update(key, tt.inputObj[i])
 			}
 
 			req, _ := http.NewRequest(tt.verb, tt.path, nil)
@@ -485,23 +469,14 @@ func TestServeHTTPForGetReqCache(t *testing.T) {
 			if pod.ResourceVersion != tt.data.rv {
 				t.Errorf("Got rv %s, but expect rv %s", pod.ResourceVersion, tt.data.rv)
 			}
-
-			err = sWrapper.DeleteCollection("kubelet")
-			if err != nil {
-				t.Errorf("failed to delete collection: kubelet, %v", err)
-			}
 		})
 	}
 }
 
 func TestServeHTTPForListReqCache(t *testing.T) {
-	dStorage, err := disk.NewDiskStorage(rootDir)
-	if err != nil {
-		t.Errorf("failed to create disk storage, %v", err)
-	}
-	sWrapper := cachemanager.NewStorageWrapper(dStorage)
+	storage := cachemanager.NewFakeStorageWrapper()
 	serializerM := serializer.NewSerializerManager()
-	cacheM, _ := cachemanager.NewCacheManager(sWrapper, serializerM)
+	cacheM, _ := cachemanager.NewCacheManager(storage, serializerM)
 
 	fn := func() bool {
 		return false
@@ -509,7 +484,13 @@ func TestServeHTTPForListReqCache(t *testing.T) {
 
 	lp := NewLocalProxy(cacheM, fn)
 
-	testcases := map[string]struct {
+	type expectData struct {
+		rv   string
+		data map[string]struct{}
+	}
+
+	testcases := []struct {
+		desc      string
 		userAgent string
 		keyPrefix string
 		inputObj  []runtime.Object
@@ -518,12 +499,10 @@ func TestServeHTTPForListReqCache(t *testing.T) {
 		path      string
 		resource  string
 		code      int
-		expectD   struct {
-			rv   string
-			data map[string]struct{}
-		}
+		expectD   expectData
 	}{
-		"list pods request": {
+		{
+			desc:      "list pods request",
 			userAgent: "kubelet",
 			keyPrefix: "kubelet/pods/default",
 			inputObj: []runtime.Object{
@@ -566,10 +545,7 @@ func TestServeHTTPForListReqCache(t *testing.T) {
 			path:     "/api/v1/namespaces/default/pods",
 			resource: "pods",
 			code:     http.StatusOK,
-			expectD: struct {
-				rv   string
-				data map[string]struct{}
-			}{
+			expectD: expectData{
 				rv: "6",
 				data: map[string]struct{}{
 					"default-mypod2-3": {},
@@ -581,14 +557,15 @@ func TestServeHTTPForListReqCache(t *testing.T) {
 	}
 
 	resolver := newTestRequestInfoResolver()
-	for k, tt := range testcases {
-		t.Run(k, func(t *testing.T) {
+
+	for _, tt := range testcases {
+		t.Run(tt.desc, func(t *testing.T) {
 			jsonDecoder, _ := serializerM.CreateSerializers(tt.accept, "", "v1", tt.resource)
 			accessor := meta.NewAccessor()
 			for i := range tt.inputObj {
 				name, _ := accessor.Name(tt.inputObj[i])
 				key := filepath.Join(tt.keyPrefix, name)
-				_ = sWrapper.Update(key, tt.inputObj[i])
+				_ = storage.Update(key, tt.inputObj[i])
 			}
 
 			req, _ := http.NewRequest(tt.verb, tt.path, nil)
@@ -646,11 +623,6 @@ func TestServeHTTPForListReqCache(t *testing.T) {
 					t.Errorf("Got pod %s, but not expect pod", key)
 				}
 			}
-
-			err = sWrapper.DeleteCollection("kubelet")
-			if err != nil {
-				t.Errorf("failed to delete collection: kubelet, %v", err)
-			}
 		})
 	}
 }
diff --git a/pkg/yurthub/proxy/util/util_test.go b/pkg/yurthub/proxy/util/util_test.go
index 5c0251c7..fb0748e3 100644
--- a/pkg/yurthub/proxy/util/util_test.go
+++ b/pkg/yurthub/proxy/util/util_test.go
@@ -269,71 +269,3 @@ func TestWithRequestTimeout(t *testing.T) {
 		}
 	}
 }
-
-func TestWithListRequestSelector(t *testing.T) {
-	testcases := map[string]struct {
-		Verb        string
-		Path        string
-		HasSelector bool
-		Selector    string
-	}{
-		"list all pods": {
-			Verb:        "GET",
-			Path:        "/api/v1/pods?resourceVersion=1494416105",
-			HasSelector: false,
-			Selector:    "",
-		},
-		"list pods with metadata.name": {
-			Verb:        "GET",
-			Path:        "/api/v1/namespaces/kube-system/pods?resourceVersion=1494416105&fieldSelector=metadata.name=test",
-			HasSelector: false,
-			Selector:    "",
-		},
-		"list pods with spec nodename": {
-			Verb:        "GET",
-			Path:        "/api/v1/namespaces/kube-system/pods?resourceVersion=1494416105&fieldSelector=spec.nodeName=test",
-			HasSelector: true,
-			Selector:    "spec.nodeName=test",
-		},
-		"list pods with label selector": {
-			Verb:        "GET",
-			Path:        "/api/v1/namespaces/kube-system/pods?resourceVersion=1494416105&labelSelector=foo=bar",
-			HasSelector: true,
-			Selector:    "foo=bar",
-		},
-		"list pods with label selector and field selector": {
-			Verb:        "GET",
-			Path:        "/api/v1/namespaces/kube-system/pods?fieldSelector=spec.nodeName=test&labelSelector=foo=bar",
-			HasSelector: true,
-			Selector:    "foo=bar&spec.nodeName=test",
-		},
-	}
-
-	resolver := newTestRequestInfoResolver()
-
-	for k, tc := range testcases {
-		t.Run(k, func(t *testing.T) {
-			req, _ := http.NewRequest(tc.Verb, tc.Path, nil)
-			req.RemoteAddr = "127.0.0.1"
-
-			var hasSelector bool
-			var selector string
-			var handler http.Handler = http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) {
-				ctx := req.Context()
-				selector, hasSelector = util.ListSelectorFrom(ctx)
-			})
-
-			handler = WithListRequestSelector(handler)
-			handler = filters.WithRequestInfo(handler, resolver)
-			handler.ServeHTTP(httptest.NewRecorder(), req)
-
-			if hasSelector != tc.HasSelector {
-				t.Errorf("expect has selector: %v, but got %v", tc.HasSelector, hasSelector)
-			}
-
-			if selector != tc.Selector {
-				t.Errorf("expect list selector %v, but got %v", tc.Selector, selector)
-			}
-		})
-	}
-}