Repository: spidernet-io/spiderpool
Pull request #1141 (2022-12-07 04:40:21)

auto cherry pick PR 1138 to branch release-v0.3
robot cherry pick pr <https://github.com/spidernet-io/spiderpool/pull/1138>,  action <https://github.com/spidernet-io/spiderpool/actions/runs/3635902519>  , commits 810f3c2324ca265e01da26193b87e8c231aecf90  


Patch:
diff --git a/go.mod b/go.mod
index fc3c8e315..45ff90eca 100644
--- a/go.mod
+++ b/go.mod
@@ -46,7 +46,10 @@ require (
 	sigs.k8s.io/controller-tools v0.8.0
 )
 
-require go.uber.org/multierr v1.8.0
+require (
+	github.com/agiledragon/gomonkey/v2 v2.9.0
+	go.uber.org/multierr v1.8.0
+)
 
 require (
 	cloud.google.com/go/compute v1.7.0 // indirect
diff --git a/go.sum b/go.sum
index e6a640e7a..b891c7990 100644
--- a/go.sum
+++ b/go.sum
@@ -86,6 +86,8 @@ github.com/Masterminds/sprig/v3 v3.2.2/go.mod h1:UoaO7Yp8KlPnJIYWTFkMaqPUYKTfGFP
 github.com/OneOfOne/xxhash v1.2.2/go.mod h1:HSdplMjZKSmBqAxg5vPj2TmRDmfkzw+cTzAElWljhcU=
 github.com/PuerkitoBio/purell v1.1.1/go.mod h1:c11w/QuzBsJSee3cPx9rAFu61PvFxuPbtSwDGJws/X0=
 github.com/PuerkitoBio/urlesc v0.0.0-20170810143723-de5bf2ad4578/go.mod h1:uGdkoq3SwY9Y+13GIhn11/XLaGBb4BfwItxLd5jeuXE=
+github.com/agiledragon/gomonkey/v2 v2.9.0 h1:PDiKKybR596O6FHW+RVSG0Z7uGCBNbmbUXh3uCNQ7Hc=
+github.com/agiledragon/gomonkey/v2 v2.9.0/go.mod h1:ap1AmDzcVOAz1YpeJ3TCzIgstoaWLA6jbbgxfB4w2iY=
 github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc=
 github.com/alecthomas/template v0.0.0-20190718012654-fb15b899a751/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc=
 github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0=
@@ -353,6 +355,7 @@ github.com/googleapis/gax-go/v2 v2.3.0/go.mod h1:b8LNqSzNabLiUpXKkY7HAR5jr6bIT99
 github.com/googleapis/gax-go/v2 v2.4.0/go.mod h1:XOTVJ59hdnfJLIP/dh8n5CGryZR2LxK9wbMD5+iXC6c=
 github.com/googleapis/go-type-adapters v1.0.0/go.mod h1:zHW75FOG2aur7gAO2B+MLby+cLsWGBF62rFAi7WjWO4=
 github.com/googleapis/google-cloud-go-testing v0.0.0-20200911160855-bcd43fbb19e8/go.mod h1:dvDLG8qkwmyD9a/MJJN3XJcT3xFxOKAvTZGvuZmac9g=
+github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY=
 github.com/gorilla/handlers v1.5.1 h1:9lRY6j8DEeeBT10CvO9hGW0gmky0BprnvDI5vfhUHH4=
 github.com/gorilla/handlers v1.5.1/go.mod h1:t8XrUpc4KVXb7HGyJ4/cEnwQiaxrX/hz1Zv/4g96P1Q=
 github.com/grpc-ecosystem/grpc-gateway v1.16.0/go.mod h1:BDjrQk3hbvj6Nolgz8mAMFbcEtjT1g+wF4CSlocrBnw=
@@ -386,6 +389,7 @@ github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnr
 github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo=
 github.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1:6v2b51hI/fHJwM22ozAgKL4VKDeJcHhJFhtBdhmNjmU=
 github.com/jstemmer/go-junit-report v0.9.1/go.mod h1:Brl9GWCQeLvo8nXZwPNNblvFj/XSXhF0NWZEnDohbsk=
+github.com/jtolds/gls v4.20.0+incompatible/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfVYBRgL+9YlvaHOwJU=
 github.com/julienschmidt/httprouter v1.2.0/go.mod h1:SYymIcj16QtmaHHD7aYtjjsJG7VTCxuUUipMqKk8s4w=
 github.com/julienschmidt/httprouter v1.3.0/go.mod h1:JR6WtHb+2LUe8TCKY3cZOxFyyO8IZAc4RVcycCCAKdM=
 github.com/k8snetworkplumbingwg/network-attachment-definition-client v1.3.0 h1:MjRRgZyTGo90G+UrwlDQjU+uG4Z7By65qvQxGoILT/8=
@@ -532,6 +536,8 @@ github.com/sirupsen/logrus v1.4.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPx
 github.com/sirupsen/logrus v1.4.1/go.mod h1:ni0Sbl8bgC9z8RoU9G6nDWqqs/fq4eDPysMBDgk/93Q=
 github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE=
 github.com/sirupsen/logrus v1.6.0/go.mod h1:7uNnSEd1DgxDLC74fIahvMZmmYsHGZGEOFrfsX/uA88=
+github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d/go.mod h1:OnSkiWE9lh6wB0YB77sQom3nweQdgAjqCqsofrRNTgc=
+github.com/smartystreets/goconvey v1.6.4/go.mod h1:syvi0/a8iFYH4r/RixwvyeAJjdLS9QV7WQ/tjFTllLA=
 github.com/spaolacci/murmur3 v0.0.0-20180118202830-f09979ecbc72/go.mod h1:JwIasOWyU6f++ZhiEuf87xNszmSA2myDM2Kzu9HwQUA=
 github.com/spf13/afero v1.2.2/go.mod h1:9ZxEEn6pIJ8Rxe320qSDBk6AsU0r9pR7Q4OcevTdifk=
 github.com/spf13/afero v1.8.2 h1:xehSyVa0YnHWsJ49JFljMpg1HX19V6NDZ1fkm1Xznbo=
@@ -874,6 +880,7 @@ golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3
 golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
 golang.org/x/tools v0.0.0-20190312151545-0bb0c0a6e846/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
 golang.org/x/tools v0.0.0-20190312170243-e65039ee4138/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
+golang.org/x/tools v0.0.0-20190328211700-ab21143f2384/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
 golang.org/x/tools v0.0.0-20190329151228-23e29df326fe/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
 golang.org/x/tools v0.0.0-20190416151739-9c9e1878f421/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
 golang.org/x/tools v0.0.0-20190420181800-aa740d480789/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
diff --git a/pkg/constant/errors.go b/pkg/constant/errors.go
index 6079e46da..fd207c011 100644
--- a/pkg/constant/errors.go
+++ b/pkg/constant/errors.go
@@ -13,3 +13,7 @@ var (
 	ErrRetriesExhausted = errors.New("exhaust all retries")
 	ErrIPUsedOut        = errors.New("all IP addresses used out")
 )
+
+var ErrMissingRequiredParam = errors.New("must be specified")
+
+var ErrUnknown = errors.New("unknown")
diff --git a/pkg/nodemanager/node_manager.go b/pkg/nodemanager/node_manager.go
index 8abe2b6b8..cd1f204a9 100644
--- a/pkg/nodemanager/node_manager.go
+++ b/pkg/nodemanager/node_manager.go
@@ -5,12 +5,14 @@ package nodemanager
 
 import (
 	"context"
-	"errors"
+	"fmt"
 
 	corev1 "k8s.io/api/core/v1"
 	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
 	apitypes "k8s.io/apimachinery/pkg/types"
 	"sigs.k8s.io/controller-runtime/pkg/client"
+
+	"github.com/spidernet-io/spiderpool/pkg/constant"
 )
 
 type NodeManager interface {
@@ -25,8 +27,9 @@ type nodeManager struct {
 
 func NewNodeManager(client client.Client) (NodeManager, error) {
 	if client == nil {
-		return nil, errors.New("k8s client must be specified")
+		return nil, fmt.Errorf("k8s client %w", constant.ErrMissingRequiredParam)
 	}
+
 	return &nodeManager{
 		client: client,
 	}, nil
diff --git a/vendor/github.com/agiledragon/gomonkey/v2/LICENSE b/vendor/github.com/agiledragon/gomonkey/v2/LICENSE
new file mode 100644
index 000000000..d75dc90e6
--- /dev/null
+++ b/vendor/github.com/agiledragon/gomonkey/v2/LICENSE
@@ -0,0 +1,21 @@
+MIT License
+
+Copyright (c) 2018 Zhang Xiaolong
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+SOFTWARE.
diff --git a/vendor/github.com/agiledragon/gomonkey/v2/README.md b/vendor/github.com/agiledragon/gomonkey/v2/README.md
new file mode 100644
index 000000000..b3321ba07
--- /dev/null
+++ b/vendor/github.com/agiledragon/gomonkey/v2/README.md
@@ -0,0 +1,53 @@
+# gomonkey
+
+gomonkey is a library to make monkey patching in unit tests easy, and the core idea of monkey patching comes from [Bouke](https://github.com/bouk), you can read [this blogpost](https://bou.ke/blog/monkey-patching-in-go/) for an explanation on how it works.
+
+## Features
+
++ support a patch for a function
++ support a patch for a public member method
++ support a patch for a private member method
++ support a patch for a interface
++ support a patch for a function variable
++ support a patch for a global variable
++ support patches of a specified sequence for a function
++ support patches of a specified sequence for a member method
++ support patches of a specified sequence for a interface
++ support patches of a specified sequence for a function variable
+
+## Notes
++ gomonkey fails to patch a function or a member method if inlining is enabled, please running your tests with inlining disabled by adding the command line argument that is `-gcflags=-l`(below go1.10) or `-gcflags=all=-l`(go1.10 and above).
++ A panic may happen when a goroutine is patching a function or a member method that is visited by another goroutine at the same time. That is to say, gomonkey is not threadsafe.
+
+## Supported Platform:
+
+- ARCH
+  - amd64
+  - arm64
+  - 386
+
+- OS
+  - Linux
+  - MAC OS X
+  - Windows
+
+## Installation
+- below v2.1.0, for example v2.0.2
+```go
+$ go get github.com/agiledragon/gomonkey@v2.0.2
+```
+- v2.1.0 and above, for example v2.2.0
+```go
+$ go get github.com/agiledragon/gomonkey/v2@v2.2.0
+```
+
+## Test Method
+```go
+$ cd test 
+$ go test -gcflags=all=-l
+```
+
+## Using gomonkey
+
+Please refer to the test cases as idioms, very complete and detailed.
+
diff --git a/vendor/github.com/agiledragon/gomonkey/v2/creflect/ae1.17.go b/vendor/github.com/agiledragon/gomonkey/v2/creflect/ae1.17.go
new file mode 100644
index 000000000..68a7e750e
--- /dev/null
+++ b/vendor/github.com/agiledragon/gomonkey/v2/creflect/ae1.17.go
@@ -0,0 +1,39 @@
+//go:build go1.17
+// +build go1.17
+
+package creflect
+
+import (
+	"unsafe"
+)
+
+// name is an encoded type name with optional extra data.
+type name struct {
+	bytes *byte
+}
+
+func (n name) data(off int, whySafe string) *byte {
+	return (*byte)(add(unsafe.Pointer(n.bytes), uintptr(off), whySafe))
+}
+
+func (n name) readVarint(off int) (int, int) {
+	v := 0
+	for i := 0; ; i++ {
+		x := *n.data(off+i, "read varint")
+		v += int(x&0x7f) << (7 * i)
+		if x&0x80 == 0 {
+			return i + 1, v
+		}
+	}
+}
+
+func (n name) name() (s string) {
+	if n.bytes == nil {
+		return
+	}
+	i, l := n.readVarint(1)
+	hdr := (*String)(unsafe.Pointer(&s))
+	hdr.Data = unsafe.Pointer(n.data(1+i, "non-empty string"))
+	hdr.Len = l
+	return
+}
diff --git a/vendor/github.com/agiledragon/gomonkey/v2/creflect/be1.16.go b/vendor/github.com/agiledragon/gomonkey/v2/creflect/be1.16.go
new file mode 100644
index 000000000..1fb6988af
--- /dev/null
+++ b/vendor/github.com/agiledragon/gomonkey/v2/creflect/be1.16.go
@@ -0,0 +1,25 @@
+//go:build !go1.17
+// +build !go1.17
+
+package creflect
+
+import (
+	"unsafe"
+)
+
+// name is an encoded type name with optional extra data.
+type name struct {
+	bytes *byte
+}
+
+func (n name) name() (s string) {
+	if n.bytes == nil {
+		return
+	}
+	b := (*[4]byte)(unsafe.Pointer(n.bytes))
+
+	hdr := (*String)(unsafe.Pointer(&s))
+	hdr.Data = unsafe.Pointer(&b[3])
+	hdr.Len = int(b[1])<<8 | int(b[2])
+	return s
+}
diff --git a/vendor/github.com/agiledragon/gomonkey/v2/creflect/type.go b/vendor/github.com/agiledragon/gomonkey/v2/creflect/type.go
new file mode 100644
index 000000000..7df1f0920
--- /dev/null
+++ b/vendor/github.com/agiledragon/gomonkey/v2/creflect/type.go
@@ -0,0 +1,179 @@
+// Customized reflect package for gomonkey，copy most code from go/src/reflect/type.go
+
+package creflect
+
+import (
+	"reflect"
+	"unsafe"
+)
+
+// rtype is the common implementation of most values.
+// rtype must be kept in sync with ../runtime/type.go:/^type._type.
+type rtype struct {
+	size       uintptr
+	ptrdata    uintptr // number of bytes in the type that can contain pointers
+	hash       uint32  // hash of type; avoids computation in hash tables
+	tflag      tflag   // extra type information flags
+	align      uint8   // alignment of variable with this type
+	fieldAlign uint8   // alignment of struct field with this type
+	kind       uint8   // enumeration for C
+	// function for comparing objects of this type
+	// (ptr to object A, ptr to object B) -> ==?
+	equal     func(unsafe.Pointer, unsafe.Pointer) bool
+	gcdata    *byte   // garbage collection data
+	str       nameOff // string form
+	ptrToThis typeOff // type for pointer to this type, may be zero
+}
+
+func Create(t reflect.Type) *rtype {
+	i := *(*funcValue)(unsafe.Pointer(&t))
+	r := (*rtype)(i.p)
+	return r
+}
+
+type funcValue struct {
+	_ uintptr
+	p unsafe.Pointer
+}
+
+func funcPointer(v reflect.Method, ok bool) (unsafe.Pointer, bool) {
+	return (*funcValue)(unsafe.Pointer(&v.Func)).p, ok
+}
+func MethodByName(r reflect.Type, name string) (fn unsafe.Pointer, ok bool) {
+	t := Create(r)
+	if r.Kind() == reflect.Interface {
+		return funcPointer(r.MethodByName(name))
+	}
+	ut := t.uncommon(r)
+	if ut == nil {
+		return nil, false
+	}
+
+	for _, p := range ut.methods() {
+		if t.nameOff(p.name).name() == name {
+			return t.Method(p), true
+		}
+	}
+	return nil, false
+}
+
+func (t *rtype) Method(p method) (fn unsafe.Pointer) {
+	tfn := t.textOff(p.tfn)
+	fn = unsafe.Pointer(&tfn)
+	return
+}
+
+type tflag uint8
+type nameOff int32 // offset to a name
+type typeOff int32 // offset to an *rtype
+type textOff int32 // offset from top of text section
+
+//go:linkname resolveTextOff reflect.resolveTextOff
+func resolveTextOff(rtype unsafe.Pointer, off int32) unsafe.Pointer
+
+func (t *rtype) textOff(off textOff) unsafe.Pointer {
+	return resolveTextOff(unsafe.Pointer(t), int32(off))
+}
+
+//go:linkname resolveNameOff reflect.resolveNameOff
+func resolveNameOff(ptrInModule unsafe.Pointer, off int32) unsafe.Pointer
+
+func (t *rtype) nameOff(off nameOff) name {
+	return name{(*byte)(resolveNameOff(unsafe.Pointer(t), int32(off)))}
+}
+
+const (
+	tflagUncommon tflag = 1 << 0
+)
+
+// uncommonType is present only for defined types or types with methods
+type uncommonType struct {
+	pkgPath nameOff // import path; empty for built-in types like int, string
+	mcount  uint16  // number of methods
+	xcount  uint16  // number of exported methods
+	moff    uint32  // offset from this uncommontype to [mcount]method
+	_       uint32  // unused
+}
+
+// ptrType represents a pointer type.
+type ptrType struct {
+	rtype
+	elem *rtype // pointer element (pointed at) type
+}
+
+// funcType represents a function type.
+type funcType struct {
+	rtype
+	inCount  uint16
+	outCount uint16 // top bit is set if last input parameter is ...
+}
+
+func add(p unsafe.Pointer, x uintptr, whySafe string) unsafe.Pointer {
+	return unsafe.Pointer(uintptr(p) + x)
+}
+
+// interfaceType represents an interface type.
+type interfaceType struct {
+	rtype
+	pkgPath name      // import path
+	methods []imethod // sorted by hash
+}
+
+type imethod struct {
+	name nameOff // name of method
+	typ  typeOff // .(*FuncType) underneath
+}
+
+type String struct {
+	Data unsafe.Pointer
+	Len  int
+}
+
+func (t *rtype) uncommon(r reflect.Type) *uncommonType {
+	if t.tflag&tflagUncommon == 0 {
+		return nil
+	}
+	switch r.Kind() {
+	case reflect.Ptr:
+		type u struct {
+			ptrType
+			u uncommonType
+		}
+		return &(*u)(unsafe.Pointer(t)).u
+	case reflect.Func:
+		type u struct {
+			funcType
+			u uncommonType
+		}
+		return &(*u)(unsafe.Pointer(t)).u
+	case reflect.Interface:
+		type u struct {
+			interfaceType
+			u uncommonType
+		}
+		return &(*u)(unsafe.Pointer(t)).u
+	case reflect.Struct:
+		type u struct {
+			interfaceType
+			u uncommonType
+		}
+		return &(*u)(unsafe.Pointer(t)).u
+	default:
+		return nil
+	}
+}
+
+// Method on non-interface type
+type method struct {
+	name nameOff // name of method
+	mtyp typeOff // method type (without receiver)
+	ifn  textOff // fn used in interface call (one-word receiver)
+	tfn  textOff // fn used for normal method call
+}
+
+func (t *uncommonType) methods() []method {
+	if t.mcount == 0 {
+		return nil
+	}
+	return (*[1 << 16]method)(add(unsafe.Pointer(t), uintptr(t.moff), "t.mcount > 0"))[:t.mcount:t.mcount]
+}
diff --git a/vendor/github.com/agiledragon/gomonkey/v2/jmp_386.go b/vendor/github.com/agiledragon/gomonkey/v2/jmp_386.go
new file mode 100644
index 000000000..27c8d4f21
--- /dev/null
+++ b/vendor/github.com/agiledragon/gomonkey/v2/jmp_386.go
@@ -0,0 +1,13 @@
+package gomonkey
+
+func buildJmpDirective(double uintptr) []byte {
+	d0 := byte(double)
+	d1 := byte(double >> 8)
+	d2 := byte(double >> 16)
+	d3 := byte(double >> 24)
+
+	return []byte{
+		0xBA, d0, d1, d2, d3, // MOV edx, double
+		0xFF, 0x22, // JMP [edx]
+	}
+}
diff --git a/vendor/github.com/agiledragon/gomonkey/v2/jmp_amd64.go b/vendor/github.com/agiledragon/gomonkey/v2/jmp_amd64.go
new file mode 100644
index 000000000..02c1c42c7
--- /dev/null
+++ b/vendor/github.com/agiledragon/gomonkey/v2/jmp_amd64.go
@@ -0,0 +1,18 @@
+package gomonkey
+
+func buildJmpDirective(double uintptr) []byte {
+    d0 := byte(double)
+    d1 := byte(double >> 8)
+    d2 := byte(double >> 16)
+    d3 := byte(double >> 24)
+    d4 := byte(double >> 32)
+    d5 := byte(double >> 40)
+    d6 := byte(double >> 48)
+    d7 := byte(double >> 56)
+
+    return []byte{
+        0x48, 0xBA, d0, d1, d2, d3, d4, d5, d6, d7, // MOV rdx, double
+        0xFF, 0x22,     // JMP [rdx]
+    }
+}
+
diff --git a/vendor/github.com/agiledragon/gomonkey/v2/jmp_arm64.go b/vendor/github.com/agiledragon/gomonkey/v2/jmp_arm64.go
new file mode 100644
index 000000000..add3fd999
--- /dev/null
+++ b/vendor/github.com/agiledragon/gomonkey/v2/jmp_arm64.go
@@ -0,0 +1,34 @@
+package gomonkey
+
+import "unsafe"
+
+func buildJmpDirective(double uintptr) []byte {
+	res := make([]byte, 0, 24)
+	d0d1 := double & 0xFFFF
+	d2d3 := double >> 16 & 0xFFFF
+	d4d5 := double >> 32 & 0xFFFF
+	d6d7 := double >> 48 & 0xFFFF
+
+	res = append(res, movImm(0B10, 0, d0d1)...)          // MOVZ x26, double[16:0]
+	res = append(res, movImm(0B11, 1, d2d3)...)          // MOVK x26, double[32:16]
+	res = append(res, movImm(0B11, 2, d4d5)...)          // MOVK x26, double[48:32]
+	res = append(res, movImm(0B11, 3, d6d7)...)          // MOVK x26, double[64:48]
+	res = append(res, []byte{0x4A, 0x03, 0x40, 0xF9}...) // LDR x10, [x26]
+	res = append(res, []byte{0x40, 0x01, 0x1F, 0xD6}...) // BR x10
+
+	return res
+}
+
+func movImm(opc, shift int, val uintptr) []byte {
+	var m uint32 = 26          // rd
+	m |= uint32(val) << 5      // imm16
+	m |= uint32(shift&3) << 21 // hw
+	m |= 0b100101 << 23        // const
+	m |= uint32(opc&0x3) << 29 // opc
+	m |= 0b1 << 31             // sf
+
+	res := make([]byte, 4)
+	*(*uint32)(unsafe.Pointer(&res[0])) = m
+
+	return res
+}
diff --git a/vendor/github.com/agiledragon/gomonkey/v2/modify_binary_darwin.go b/vendor/github.com/agiledragon/gomonkey/v2/modify_binary_darwin.go
new file mode 100644
index 000000000..6c169888e
--- /dev/null
+++ b/vendor/github.com/agiledragon/gomonkey/v2/modify_binary_darwin.go
@@ -0,0 +1,27 @@
+package gomonkey
+
+import "syscall"
+
+func modifyBinary(target uintptr, bytes []byte) {
+	function := entryAddress(target, len(bytes))
+	err := mprotectCrossPage(target, len(bytes), syscall.PROT_READ|syscall.PROT_WRITE|syscall.PROT_EXEC)
+	if err != nil {
+		panic(err)
+	}
+	copy(function, bytes)
+	err = mprotectCrossPage(target, len(bytes), syscall.PROT_READ|syscall.PROT_EXEC)
+	if err != nil {
+		panic(err)
+	}
+}
+
+func mprotectCrossPage(addr uintptr, length int, prot int) error {
+	pageSize := syscall.Getpagesize()
+	for p := pageStart(addr); p < addr+uintptr(length); p += uintptr(pageSize) {
+		page := entryAddress(p, pageSize)
+		if err := syscall.Mprotect(page, prot); err != nil {
+			return err
+		}
+	}
+	return nil
+}
diff --git a/vendor/github.com/agiledragon/gomonkey/v2/modify_binary_linux.go b/vendor/github.com/agiledragon/gomonkey/v2/modify_binary_linux.go
new file mode 100644
index 000000000..6c169888e
--- /dev/null
+++ b/vendor/github.com/agiledragon/gomonkey/v2/modify_binary_linux.go
@@ -0,0 +1,27 @@
+package gomonkey
+
+import "syscall"
+
+func modifyBinary(target uintptr, bytes []byte) {
+	function := entryAddress(target, len(bytes))
+	err := mprotectCrossPage(target, len(bytes), syscall.PROT_READ|syscall.PROT_WRITE|syscall.PROT_EXEC)
+	if err != nil {
+		panic(err)
+	}
+	copy(function, bytes)
+	err = mprotectCrossPage(target, len(bytes), syscall.PROT_READ|syscall.PROT_EXEC)
+	if err != nil {
+		panic(err)
+	}
+}
+
+func mprotectCrossPage(addr uintptr, length int, prot int) error {
+	pageSize := syscall.Getpagesize()
+	for p := pageStart(addr); p < addr+uintptr(length); p += uintptr(pageSize) {
+		page := entryAddress(p, pageSize)
+		if err := syscall.Mprotect(page, prot); err != nil {
+			return err
+		}
+	}
+	return nil
+}
diff --git a/vendor/github.com/agiledragon/gomonkey/v2/modify_binary_windows.go b/vendor/github.com/agiledragon/gomonkey/v2/modify_binary_windows.go
new file mode 100644
index 000000000..ef0dbc756
--- /dev/null
+++ b/vendor/github.com/agiledragon/gomonkey/v2/modify_binary_windows.go
@@ -0,0 +1,25 @@
+package gomonkey
+
+import (
+    "syscall"
+    "unsafe"
+)
+
+func modifyBinary(target uintptr, bytes []byte) {
+    function := entryAddress(target, len(bytes))
+    
+    proc := syscall.NewLazyDLL("kernel32.dll").NewProc("VirtualProtect")
+    const PROT_READ_WRITE = 0x40
+    var old uint32
+    result, _, _ := proc.Call(target, uintptr(len(bytes)), uintptr(PROT_READ_WRITE), uintptr(unsafe.Pointer(&old)))
+    if result == 0 {
+        panic(result)
+    }
+    copy(function, bytes)
+    
+    var ignore uint32
+    result, _, _ = proc.Call(target, uintptr(len(bytes)), uintptr(old), uintptr(unsafe.Pointer(&ignore)))
+    if result == 0 {
+        panic(result)
+    }
+}
\ No newline at end of file
diff --git a/vendor/github.com/agiledragon/gomonkey/v2/patch.go b/vendor/github.com/agiledragon/gomonkey/v2/patch.go
new file mode 100644
index 000000000..468ae6708
--- /dev/null
+++ b/vendor/github.com/agiledragon/gomonkey/v2/patch.go
@@ -0,0 +1,365 @@
+package gomonkey
+
+import (
+	"fmt"
+	"reflect"
+	"syscall"
+	"unsafe"
+
+	"github.com/agiledragon/gomonkey/v2/creflect"
+)
+
+type Patches struct {
+	originals    map[uintptr][]byte
+	values       map[reflect.Value]reflect.Value
+	valueHolders map[reflect.Value]reflect.Value
+}
+
+type Params []interface{}
+type OutputCell struct {
+	Values Params
+	Times  int
+}
+
+func ApplyFunc(target, double interface{}) *Patches {
+	return create().ApplyFunc(target, double)
+}
+
+func ApplyMethod(target interface{}, methodName string, double interface{}) *Patches {
+	return create().ApplyMethod(target, methodName, double)
+}
+
+func ApplyMethodFunc(target interface{}, methodName string, doubleFunc interface{}) *Patches {
+	return create().ApplyMethodFunc(target, methodName, doubleFunc)
+}
+
+func ApplyPrivateMethod(target interface{}, methodName string, double interface{}) *Patches {
+	return create().ApplyPrivateMethod(target, methodName, double)
+}
+
+func ApplyGlobalVar(target, double interface{}) *Patches {
+	return create().ApplyGlobalVar(target, double)
+}
+
+func ApplyFuncVar(target, double interface{}) *Patches {
+	return create().ApplyFuncVar(target, double)
+}
+
+func ApplyFuncSeq(target interface{}, outputs []OutputCell) *Patches {
+	return create().ApplyFuncSeq(target, outputs)
+}
+
+func ApplyMethodSeq(target interface{}, methodName string, outputs []OutputCell) *Patches {
+	return create().ApplyMethodSeq(target, methodName, outputs)
+}
+
+func ApplyFuncVarSeq(target interface{}, outputs []OutputCell) *Patches {
+	return create().ApplyFuncVarSeq(target, outputs)
+}
+
+func ApplyFuncReturn(target interface{}, output ...interface{}) *Patches {
+	return create().ApplyFuncReturn(target, output...)
+}
+
+func ApplyMethodReturn(target interface{}, methodName string, output ...interface{}) *Patches {
+	return create().ApplyMethodReturn(target, methodName, output...)
+}
+
+func ApplyFuncVarReturn(target interface{}, output ...interface{}) *Patches {
+	return create().ApplyFuncVarReturn(target, output...)
+}
+
+func create() *Patches {
+	return &Patches{originals: make(map[uintptr][]byte), values: make(map[reflect.Value]reflect.Value), valueHolders: make(map[reflect.Value]reflect.Value)}
+}
+
+func NewPatches() *Patches {
+	return create()
+}
+
+func (this *Patches) ApplyFunc(target, double interface{}) *Patches {
+	t := reflect.ValueOf(target)
+	d := reflect.ValueOf(double)
+	return this.ApplyCore(t, d)
+}
+
+func (this *Patches) ApplyMethod(target interface{}, methodName string, double interface{}) *Patches {
+	m, ok := castRType(target).MethodByName(methodName)
+	if !ok {
+		panic("retrieve method by name failed")
+	}
+	d := reflect.ValueOf(double)
+	return this.ApplyCore(m.Func, d)
+}
+
+func (this *Patches) ApplyMethodFunc(target interface{}, methodName string, doubleFunc interface{}) *Patches {
+	m, ok := castRType(target).MethodByName(methodName)
+	if !ok {
+		panic("retrieve method by name failed")
+	}
+	d := funcToMethod(m.Type, doubleFunc)
+	return this.ApplyCore(m.Func, d)
+}
+
+func (this *Patches) ApplyPrivateMethod(target interface{}, methodName string, double interface{}) *Patches {
+	m, ok := creflect.MethodByName(castRType(target), methodName)
+	if !ok {
+		panic("retrieve method by name failed")
+	}
+	d := reflect.ValueOf(double)
+	return this.ApplyCoreOnlyForPrivateMethod(m, d)
+}
+
+func (this *Patches) ApplyGlobalVar(target, double interface{}) *Patches {
+	t := reflect.ValueOf(target)
+	if t.Type().Kind() != reflect.Ptr {
+		panic("target is not a pointer")
+	}
+
+	this.values[t] = reflect.ValueOf(t.Elem().Interface())
+	d := reflect.ValueOf(double)
+	t.Elem().Set(d)
+	return this
+}
+
+func (this *Patches) ApplyFuncVar(target, double interface{}) *Patches {
+	t := reflect.ValueOf(target)
+	d := reflect.ValueOf(double)
+	if t.Type().Kind() != reflect.Ptr {
+		panic("target is not a pointer")
+	}
+	this.check(t.Elem(), d)
+	return this.ApplyGlobalVar(target, double)
+}
+
+func (this *Patches) ApplyFuncSeq(target interface{}, outputs []OutputCell) *Patches {
+	funcType := reflect.TypeOf(target)
+	t := reflect.ValueOf(target)
+	d := getDoubleFunc(funcType, outputs)
+	return this.ApplyCore(t, d)
+}
+
+func (this *Patches) ApplyMethodSeq(target interface{}, methodName string, outputs []OutputCell) *Patches {
+	m, ok := castRType(target).MethodByName(methodName)
+	if !ok {
+		panic("retrieve method by name failed")
+	}
+	d := getDoubleFunc(m.Type, outputs)
+	return this.ApplyCore(m.Func, d)
+}
+
+func (this *Patches) ApplyFuncVarSeq(target interface{}, outputs []OutputCell) *Patches {
+	t := reflect.ValueOf(target)
+	if t.Type().Kind() != reflect.Ptr {
+		panic("target is not a pointer")
+	}
+	if t.Elem().Kind() != reflect.Func {
+		panic("target is not a func")
+	}
+
+	funcType := reflect.TypeOf(target).Elem()
+	double := getDoubleFunc(funcType, outputs).Interface()
+	return this.ApplyGlobalVar(target, double)
+}
+
+func (this *Patches) ApplyFuncReturn(target interface{}, returns ...interface{}) *Patches {
+	funcType := reflect.TypeOf(target)
+	t := reflect.ValueOf(target)
+	outputs := []OutputCell{{Values: returns, Times: -1}}
+	d := getDoubleFunc(funcType, outputs)
+	return this.ApplyCore(t, d)
+}
+
+func (this *Patches) ApplyMethodReturn(target interface{}, methodName string, returns ...interface{}) *Patches {
+	m, ok := reflect.TypeOf(target).MethodByName(methodName)
+	if !ok {
+		panic("retrieve method by name failed")
+	}
+
+	outputs := []OutputCell{{Values: returns, Times: -1}}
+	d := getDoubleFunc(m.Type, outputs)
+	return this.ApplyCore(m.Func, d)
+}
+
+func (this *Patches) ApplyFuncVarReturn(target interface{}, returns ...interface{}) *Patches {
+	t := reflect.ValueOf(target)
+	if t.Type().Kind() != reflect.Ptr {
+		panic("target is not a pointer")
+	}
+	if t.Elem().Kind() != reflect.Func {
+		panic("target is not a func")
+	}
+
+	funcType := reflect.TypeOf(target).Elem()
+	outputs := []OutputCell{{Values: returns, Times: -1}}
+	double := getDoubleFunc(funcType, outputs).Interface()
+	return this.ApplyGlobalVar(target, double)
+}
+
+func (this *Patches) Reset() {
+	for target, bytes := range this.originals {
+		modifyBinary(target, bytes)
+		delete(this.originals, target)
+	}
+
+	for target, variable := range this.values {
+		target.Elem().Set(variable)
+	}
+}
+
+func (this *Patches) ApplyCore(target, double reflect.Value) *Patches {
+	this.check(target, double)
+	assTarget := *(*uintptr)(getPointer(target))
+	original := replace(assTarget, uintptr(getPointer(double)))
+	if _, ok := this.originals[assTarget]; !ok {
+		this.originals[assTarget] = original
+	}
+	this.valueHolders[double] = double
+	return this
+}
+
+func (this *Patches) ApplyCoreOnlyForPrivateMethod(target unsafe.Pointer, double reflect.Value) *Patches {
+	if double.Kind() != reflect.Func {
+		panic("double is not a func")
+	}
+	assTarget := *(*uintptr)(target)
+	original := replace(assTarget, uintptr(getPointer(double)))
+	if _, ok := this.originals[assTarget]; !ok {
+		this.originals[assTarget] = original
+	}
+	this.valueHolders[double] = double
+	return this
+}
+
+func (this *Patches) check(target, double reflect.Value) {
+	if target.Kind() != reflect.Func {
+		panic("target is not a func")
+	}
+
+	if double.Kind() != reflect.Func {
+		panic("double is not a func")
+	}
+
+	targetType := target.Type()
+	doubleType := double.Type()
+
+	if targetType.NumIn() < doubleType.NumIn() ||
+		targetType.NumOut() != doubleType.NumOut() ||
+		(targetType.NumIn() == doubleType.NumIn() && targetType.IsVariadic() != doubleType.IsVariadic()) {
+		panic(fmt.Sprintf("target type(%s) and double type(%s) are different", target.Type(), double.Type()))
+	}
+
+	for i, size := 0, doubleType.NumIn(); i < size; i++ {
+		targetIn := targetType.In(i)
+		doubleIn := doubleType.In(i)
+
+		if targetIn.AssignableTo(doubleIn) {
+			continue
+		}
+
+		panic(fmt.Sprintf("target type(%s) and double type(%s) are different", target.Type(), double.Type()))
+	}
+}
+
+func replace(target, double uintptr) []byte {
+	code := buildJmpDirective(double)
+	bytes := entryAddress(target, len(code))
+	original := make([]byte, len(bytes))
+	copy(original, bytes)
+	modifyBinary(target, code)
+	return original
+}
+
+func getDoubleFunc(funcType reflect.Type, outputs []OutputCell) reflect.Value {
+	if funcType.NumOut() != len(outputs[0].Values) {
+		panic(fmt.Sprintf("func type has %v return values, but only %v values provided as double",
+			funcType.NumOut(), len(outputs[0].Values)))
+	}
+
+	needReturn := false
+	slice := make([]Params, 0)
+	for _, output := range outputs {
+		if output.Times == -1 {
+			needReturn = true
+			slice = []Params{output.Values}
+			break
+		}
+		t := 0
+		if output.Times <= 1 {
+			t = 1
+		} else {
+			t = output.Times
+		}
+		for j := 0; j < t; j++ {
+			slice = append(slice, output.Values)
+		}
+	}
+
+	i := 0
+	lenOutputs := len(slice)
+	return reflect.MakeFunc(funcType, func(_ []reflect.Value) []reflect.Value {
+		if needReturn {
+			return GetResultValues(funcType, slice[0]...)
+		}
+		if i < lenOutputs {
+			i++
+			return GetResultValues(funcType, slice[i-1]...)
+		}
+		panic("double seq is less than call seq")
+	})
+}
+
+func GetResultValues(funcType reflect.Type, results ...interface{}) []reflect.Value {
+	var resultValues []reflect.Value
+	for i, r := range results {
+		var resultValue reflect.Value
+		if r == nil {
+			resultValue = reflect.Zero(funcType.Out(i))
+		} else {
+			v := reflect.New(funcType.Out(i))
+			v.Elem().Set(reflect.ValueOf(r))
+			resultValue = v.Elem()
+		}
+		resultValues = append(resultValues, resultValue)
+	}
+	return resultValues
+}
+
+type funcValue struct {
+	_ uintptr
+	p unsafe.Pointer
+}
+
+func getPointer(v reflect.Value) unsafe.Pointer {
+	return (*funcValue)(unsafe.Pointer(&v)).p
+}
+
+func entryAddress(p uintptr, l int) []byte {
+	return *(*[]byte)(unsafe.Pointer(&reflect.SliceHeader{Data: p, Len: l, Cap: l}))
+}
+
+func pageStart(ptr uintptr) uintptr {
+	return ptr & ^(uintptr(syscall.Getpagesize() - 1))
+}
+
+func funcToMethod(funcType reflect.Type, doubleFunc interface{}) reflect.Value {
+	rf := reflect.TypeOf(doubleFunc)
+	if rf.Kind() != reflect.Func {
+		panic("doubleFunc is not a func")
+	}
+	vf := reflect.ValueOf(doubleFunc)
+	return reflect.MakeFunc(funcType, func(in []reflect.Value) []reflect.Value {
+		if funcType.IsVariadic() {
+			return vf.CallSlice(in[1:])
+		} else {
+			return vf.Call(in[1:])
+		}
+	})
+}
+
+func castRType(val interface{}) reflect.Type {
+	if rTypeVal, ok := val.(reflect.Type); ok {
+		return rTypeVal
+	}
+	return reflect.TypeOf(val)
+}
diff --git a/vendor/k8s.io/apimachinery/pkg/util/rand/rand.go b/vendor/k8s.io/apimachinery/pkg/util/rand/rand.go
new file mode 100644
index 000000000..82a473bb1
--- /dev/null
+++ b/vendor/k8s.io/apimachinery/pkg/util/rand/rand.go
@@ -0,0 +1,127 @@
+/*
+Copyright 2015 The Kubernetes 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 rand provides utilities related to randomization.
+package rand
+
+import (
+	"math/rand"
+	"sync"
+	"time"
+)
+
+var rng = struct {
+	sync.Mutex
+	rand *rand.Rand
+}{
+	rand: rand.New(rand.NewSource(time.Now().UnixNano())),
+}
+
+// Int returns a non-negative pseudo-random int.
+func Int() int {
+	rng.Lock()
+	defer rng.Unlock()
+	return rng.rand.Int()
+}
+
+// Intn generates an integer in range [0,max).
+// By design this should panic if input is invalid, <= 0.
+func Intn(max int) int {
+	rng.Lock()
+	defer rng.Unlock()
+	return rng.rand.Intn(max)
+}
+
+// IntnRange generates an integer in range [min,max).
+// By design this should panic if input is invalid, <= 0.
+func IntnRange(min, max int) int {
+	rng.Lock()
+	defer rng.Unlock()
+	return rng.rand.Intn(max-min) + min
+}
+
+// IntnRange generates an int64 integer in range [min,max).
+// By design this should panic if input is invalid, <= 0.
+func Int63nRange(min, max int64) int64 {
+	rng.Lock()
+	defer rng.Unlock()
+	return rng.rand.Int63n(max-min) + min
+}
+
+// Seed seeds the rng with the provided seed.
+func Seed(seed int64) {
+	rng.Lock()
+	defer rng.Unlock()
+
+	rng.rand = rand.New(rand.NewSource(seed))
+}
+
+// Perm returns, as a slice of n ints, a pseudo-random permutation of the integers [0,n)
+// from the default Source.
+func Perm(n int) []int {
+	rng.Lock()
+	defer rng.Unlock()
+	return rng.rand.Perm(n)
+}
+
+const (
+	// We omit vowels from the set of available characters to reduce the chances
+	// of "bad words" being formed.
+	alphanums = "bcdfghjklmnpqrstvwxz2456789"
+	// No. of bits required to index into alphanums string.
+	alphanumsIdxBits = 5
+	// Mask used to extract last alphanumsIdxBits of an int.
+	alphanumsIdxMask = 1<<alphanumsIdxBits - 1
+	// No. of random letters we can extract from a single int63.
+	maxAlphanumsPerInt = 63 / alphanumsIdxBits
+)
+
+// String generates a random alphanumeric string, without vowels, which is n
+// characters long.  This will panic if n is less than zero.
+// How the random string is created:
+// - we generate random int63's
+// - from each int63, we are extracting multiple random letters by bit-shifting and masking
+// - if some index is out of range of alphanums we neglect it (unlikely to happen multiple times in a row)
+func String(n int) string {
+	b := make([]byte, n)
+	rng.Lock()
+	defer rng.Unlock()
+
+	randomInt63 := rng.rand.Int63()
+	remaining := maxAlphanumsPerInt
+	for i := 0; i < n; {
+		if remaining == 0 {
+			randomInt63, remaining = rng.rand.Int63(), maxAlphanumsPerInt
+		}
+		if idx := int(randomInt63 & alphanumsIdxMask); idx < len(alphanums) {
+			b[i] = alphanums[idx]
+			i++
+		}
+		randomInt63 >>= alphanumsIdxBits
+		remaining--
+	}
+	return string(b)
+}
+
+// SafeEncodeString encodes s using the same characters as rand.String. This reduces the chances of bad words and
+// ensures that strings generated from hash functions appear consistent throughout the API.
+func SafeEncodeString(s string) string {
+	r := make([]byte, len(s))
+	for i, b := range []rune(s) {
+		r[i] = alphanums[(int(b) % len(alphanums))]
+	}
+	return string(r)
+}
diff --git a/vendor/modules.txt b/vendor/modules.txt
index 54a9ad302..ef6b8d519 100644
--- a/vendor/modules.txt
+++ b/vendor/modules.txt
@@ -29,6 +29,10 @@ github.com/Masterminds/semver/v3
 # github.com/Masterminds/sprig/v3 v3.2.2
 ## explicit; go 1.13
 github.com/Masterminds/sprig/v3
+# github.com/agiledragon/gomonkey/v2 v2.9.0
+## explicit; go 1.14
+github.com/agiledragon/gomonkey/v2
+github.com/agiledragon/gomonkey/v2/creflect
 # github.com/asaskevich/govalidator v0.0.0-20210307081110-f21760c49a8d
 ## explicit; go 1.13
 github.com/asaskevich/govalidator
@@ -740,6 +744,7 @@ k8s.io/apimachinery/pkg/util/managedfields
 k8s.io/apimachinery/pkg/util/mergepatch
 k8s.io/apimachinery/pkg/util/naming
 k8s.io/apimachinery/pkg/util/net
+k8s.io/apimachinery/pkg/util/rand
 k8s.io/apimachinery/pkg/util/runtime
 k8s.io/apimachinery/pkg/util/sets
 k8s.io/apimachinery/pkg/util/strategicpatch
@@ -1137,6 +1142,7 @@ sigs.k8s.io/controller-runtime/pkg/certwatcher/metrics
 sigs.k8s.io/controller-runtime/pkg/client
 sigs.k8s.io/controller-runtime/pkg/client/apiutil
 sigs.k8s.io/controller-runtime/pkg/client/config
+sigs.k8s.io/controller-runtime/pkg/client/fake
 sigs.k8s.io/controller-runtime/pkg/cluster
 sigs.k8s.io/controller-runtime/pkg/config
 sigs.k8s.io/controller-runtime/pkg/config/v1alpha1
diff --git a/vendor/sigs.k8s.io/controller-runtime/pkg/client/fake/client.go b/vendor/sigs.k8s.io/controller-runtime/pkg/client/fake/client.go
new file mode 100644
index 000000000..b7ca2de47
--- /dev/null
+++ b/vendor/sigs.k8s.io/controller-runtime/pkg/client/fake/client.go
@@ -0,0 +1,787 @@
+/*
+Copyright 2018 The Kubernetes 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 (
+	"context"
+	"encoding/json"
+	"errors"
+	"fmt"
+	"reflect"
+	"strconv"
+	"strings"
+	"sync"
+
+	apierrors "k8s.io/apimachinery/pkg/api/errors"
+	"k8s.io/apimachinery/pkg/api/meta"
+	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
+	"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
+	"k8s.io/apimachinery/pkg/runtime"
+	"k8s.io/apimachinery/pkg/runtime/schema"
+	utilrand "k8s.io/apimachinery/pkg/util/rand"
+	"k8s.io/apimachinery/pkg/util/validation/field"
+	"k8s.io/apimachinery/pkg/watch"
+	"k8s.io/client-go/kubernetes/scheme"
+	"k8s.io/client-go/testing"
+
+	"sigs.k8s.io/controller-runtime/pkg/client"
+	"sigs.k8s.io/controller-runtime/pkg/client/apiutil"
+	"sigs.k8s.io/controller-runtime/pkg/internal/objectutil"
+)
+
+type versionedTracker struct {
+	testing.ObjectTracker
+	scheme *runtime.Scheme
+}
+
+type fakeClient struct {
+	tracker         versionedTracker
+	scheme          *runtime.Scheme
+	restMapper      meta.RESTMapper
+	schemeWriteLock sync.Mutex
+}
+
+var _ client.WithWatch = &fakeClient{}
+
+const (
+	maxNameLength          = 63
+	randomLength           = 5
+	maxGeneratedNameLength = maxNameLength - randomLength
+)
+
+// NewFakeClient creates a new fake client for testing.
+// You can choose to initialize it with a slice of runtime.Object.
+//
+// Deprecated: Please use NewClientBuilder instead.
+func NewFakeClient(initObjs ...runtime.Object) client.WithWatch {
+	return NewClientBuilder().WithRuntimeObjects(initObjs...).Build()
+}
+
+// NewFakeClientWithScheme creates a new fake client with the given scheme
+// for testing.
+// You can choose to initialize it with a slice of runtime.Object.
+//
+// Deprecated: Please use NewClientBuilder instead.
+func NewFakeClientWithScheme(clientScheme *runtime.Scheme, initObjs ...runtime.Object) client.WithWatch {
+	return NewClientBuilder().WithScheme(clientScheme).WithRuntimeObjects(initObjs...).Build()
+}
+
+// NewClientBuilder returns a new builder to create a fake client.
+func NewClientBuilder() *ClientBuilder {
+	return &ClientBuilder{}
+}
+
+// ClientBuilder builds a fake client.
+type ClientBuilder struct {
+	scheme             *runtime.Scheme
+	restMapper         meta.RESTMapper
+	initObject         []client.Object
+	initLists          []client.ObjectList
+	initRuntimeObjects []runtime.Object
+	objectTracker      testing.ObjectTracker
+}
+
+// WithScheme sets this builder's internal scheme.
+// If not set, defaults to client-go's global scheme.Scheme.
+func (f *ClientBuilder) WithScheme(scheme *runtime.Scheme) *ClientBuilder {
+	f.scheme = scheme
+	return f
+}
+
+// WithRESTMapper sets this builder's restMapper.
+// The restMapper is directly set as mapper in the Client. This can be used for example
+// with a meta.DefaultRESTMapper to provide a static rest mapping.
+// If not set, defaults to an empty meta.DefaultRESTMapper.
+func (f *ClientBuilder) WithRESTMapper(restMapper meta.RESTMapper) *ClientBuilder {
+	f.restMapper = restMapper
+	return f
+}
+
+// WithObjects can be optionally used to initialize this fake client with client.Object(s).
+func (f *ClientBuilder) WithObjects(initObjs ...client.Object) *ClientBuilder {
+	f.initObject = append(f.initObject, initObjs...)
+	return f
+}
+
+// WithLists can be optionally used to initialize this fake client with client.ObjectList(s).
+func (f *ClientBuilder) WithLists(initLists ...client.ObjectList) *ClientBuilder {
+	f.initLists = append(f.initLists, initLists...)
+	return f
+}
+
+// WithRuntimeObjects can be optionally used to initialize this fake client with runtime.Object(s).
+func (f *ClientBuilder) WithRuntimeObjects(initRuntimeObjs ...runtime.Object) *ClientBuilder {
+	f.initRuntimeObjects = append(f.initRuntimeObjects, initRuntimeObjs...)
+	return f
+}
+
+// WithObjectTracker can be optionally used to initialize this fake client with testing.ObjectTracker.
+func (f *ClientBuilder) WithObjectTracker(ot testing.ObjectTracker) *ClientBuilder {
+	f.objectTracker = ot
+	return f
+}
+
+// Build builds and returns a new fake client.
+func (f *ClientBuilder) Build() client.WithWatch {
+	if f.scheme == nil {
+		f.scheme = scheme.Scheme
+	}
+	if f.restMapper == nil {
+		f.restMapper = meta.NewDefaultRESTMapper([]schema.GroupVersion{})
+	}
+
+	var tracker versionedTracker
+
+	if f.objectTracker == nil {
+		tracker = versionedTracker{ObjectTracker: testing.NewObjectTracker(f.scheme, scheme.Codecs.UniversalDecoder()), scheme: f.scheme}
+	} else {
+		tracker = versionedTracker{ObjectTracker: f.objectTracker, scheme: f.scheme}
+	}
+
+	for _, obj := range f.initObject {
+		if err := tracker.Add(obj); err != nil {
+			panic(fmt.Errorf("failed to add object %v to fake client: %w", obj, err))
+		}
+	}
+	for _, obj := range f.initLists {
+		if err := tracker.Add(obj); err != nil {
+			panic(fmt.Errorf("failed to add list %v to fake client: %w", obj, err))
+		}
+	}
+	for _, obj := range f.initRuntimeObjects {
+		if err := tracker.Add(obj); err != nil {
+			panic(fmt.Errorf("failed to add runtime object %v to fake client: %w", obj, err))
+		}
+	}
+	return &fakeClient{
+		tracker:    tracker,
+		scheme:     f.scheme,
+		restMapper: f.restMapper,
+	}
+}
+
+const trackerAddResourceVersion = "999"
+
+func (t versionedTracker) Add(obj runtime.Object) error {
+	var objects []runtime.Object
+	if meta.IsListType(obj) {
+		var err error
+		objects, err = meta.ExtractList(obj)
+		if err != nil {
+			return err
+		}
+	} else {
+		objects = []runtime.Object{obj}
+	}
+	for _, obj := range objects {
+		accessor, err := meta.Accessor(obj)
+		if err != nil {
+			return fmt.Errorf("failed to get accessor for object: %w", err)
+		}
+		if accessor.GetResourceVersion() == "" {
+			// We use a "magic" value of 999 here because this field
+			// is parsed as uint and and 0 is already used in Update.
+			// As we can't go lower, go very high instead so this can
+			// be recognized
+			accessor.SetResourceVersion(trackerAddResourceVersion)
+		}
+
+		obj, err = convertFromUnstructuredIfNecessary(t.scheme, obj)
+		if err != nil {
+			return err
+		}
+		if err := t.ObjectTracker.Add(obj); err != nil {
+			return err
+		}
+	}
+
+	return nil
+}
+
+func (t versionedTracker) Create(gvr schema.GroupVersionResource, obj runtime.Object, ns string) error {
+	accessor, err := meta.Accessor(obj)
+	if err != nil {
+		return fmt.Errorf("failed to get accessor for object: %w", err)
+	}
+	if accessor.GetName() == "" {
+		return apierrors.NewInvalid(
+			obj.GetObjectKind().GroupVersionKind().GroupKind(),
+			accessor.GetName(),
+			field.ErrorList{field.Required(field.NewPath("metadata.name"), "name is required")})
+	}
+	if accessor.GetResourceVersion() != "" {
+		return apierrors.NewBadRequest("resourceVersion can not be set for Create requests")
+	}
+	accessor.SetResourceVersion("1")
+	obj, err = convertFromUnstructuredIfNecessary(t.scheme, obj)
+	if err != nil {
+		return err
+	}
+	if err := t.ObjectTracker.Create(gvr, obj, ns); err != nil {
+		accessor.SetResourceVersion("")
+		return err
+	}
+
+	return nil
+}
+
+// convertFromUnstructuredIfNecessary will convert *unstructured.Unstructured for a GVK that is recocnized
+// by the schema into the whatever the schema produces with New() for said GVK.
+// This is required because the tracker unconditionally saves on manipulations, but its List() implementation
+// tries to assign whatever it finds into a ListType it gets from schema.New() - Thus we have to ensure
+// we save as the very same type, otherwise subsequent List requests will fail.
+func convertFromUnstructuredIfNecessary(s *runtime.Scheme, o runtime.Object) (runtime.Object, error) {
+	u, isUnstructured := o.(*unstructured.Unstructured)
+	if !isUnstructured || !s.Recognizes(u.GroupVersionKind()) {
+		return o, nil
+	}
+
+	typed, err := s.New(u.GroupVersionKind())
+	if err != nil {
+		return nil, fmt.Errorf("scheme recognizes %s but failed to produce an object for it: %w", u.GroupVersionKind().String(), err)
+	}
+
+	unstructuredSerialized, err := json.Marshal(u)
+	if err != nil {
+		return nil, fmt.Errorf("failed to serialize %T: %w", unstructuredSerialized, err)
+	}
+	if err := json.Unmarshal(unstructuredSerialized, typed); err != nil {
+		return nil, fmt.Errorf("failed to unmarshal the content of %T into %T: %w", u, typed, err)
+	}
+
+	return typed, nil
+}
+
+func (t versionedTracker) Update(gvr schema.GroupVersionResource, obj runtime.Object, ns string) error {
+	accessor, err := meta.Accessor(obj)
+	if err != nil {
+		return fmt.Errorf("failed to get accessor for object: %w", err)
+	}
+
+	if accessor.GetName() == "" {
+		return apierrors.NewInvalid(
+			obj.GetObjectKind().GroupVersionKind().GroupKind(),
+			accessor.GetName(),
+			field.ErrorList{field.Required(field.NewPath("metadata.name"), "name is required")})
+	}
+
+	gvk := obj.GetObjectKind().GroupVersionKind()
+	if gvk.Empty() {
+		gvk, err = apiutil.GVKForObject(obj, t.scheme)
+		if err != nil {
+			return err
+		}
+	}
+
+	oldObject, err := t.ObjectTracker.Get(gvr, ns, accessor.GetName())
+	if err != nil {
+		// If the resource is not found and the resource allows create on update, issue a
+		// create instead.
+		if apierrors.IsNotFound(err) && allowsCreateOnUpdate(gvk) {
+			return t.Create(gvr, obj, ns)
+		}
+		return err
+	}
+
+	oldAccessor, err := meta.Accessor(oldObject)
+	if err != nil {
+		return err
+	}
+
+	// If the new object does not have the resource version set and it allows unconditional update,
+	// default it to the resource version of the existing resource
+	if accessor.GetResourceVersion() == "" && allowsUnconditionalUpdate(gvk) {
+		accessor.SetResourceVersion(oldAccessor.GetResourceVersion())
+	}
+	if accessor.GetResourceVersion() != oldAccessor.GetResourceVersion() {
+		return apierrors.NewConflict(gvr.GroupResource(), accessor.GetName(), errors.New("object was modified"))
+	}
+	if oldAccessor.GetResourceVersion() == "" {
+		oldAccessor.SetResourceVersion("0")
+	}
+	intResourceVersion, err := strconv.ParseUint(oldAccessor.GetResourceVersion(), 10, 64)
+	if err != nil {
+		return fmt.Errorf("can not convert resourceVersion %q to int: %w", oldAccessor.GetResourceVersion(), err)
+	}
+	intResourceVersion++
+	accessor.SetResourceVersion(strconv.FormatUint(intResourceVersion, 10))
+	if !accessor.GetDeletionTimestamp().IsZero() && len(accessor.GetFinalizers()) == 0 {
+		return t.ObjectTracker.Delete(gvr, accessor.GetNamespace(), accessor.GetName())
+	}
+	obj, err = convertFromUnstructuredIfNecessary(t.scheme, obj)
+	if err != nil {
+		return err
+	}
+	return t.ObjectTracker.Update(gvr, obj, ns)
+}
+
+func (c *fakeClient) Get(ctx context.Context, key client.ObjectKey, obj client.Object, opts ...client.GetOption) error {
+	gvr, err := getGVRFromObject(obj, c.scheme)
+	if err != nil {
+		return err
+	}
+	o, err := c.tracker.Get(gvr, key.Namespace, key.Name)
+	if err != nil {
+		return err
+	}
+
+	gvk, err := apiutil.GVKForObject(obj, c.scheme)
+	if err != nil {
+		return err
+	}
+	ta, err := meta.TypeAccessor(o)
+	if err != nil {
+		return err
+	}
+	ta.SetKind(gvk.Kind)
+	ta.SetAPIVersion(gvk.GroupVersion().String())
+
+	j, err := json.Marshal(o)
+	if err != nil {
+		return err
+	}
+	decoder := scheme.Codecs.UniversalDecoder()
+	zero(obj)
+	_, _, err = decoder.Decode(j, nil, obj)
+	return err
+}
+
+func (c *fakeClient) Watch(ctx context.Context, list client.ObjectList, opts ...client.ListOption) (watch.Interface, error) {
+	gvk, err := apiutil.GVKForObject(list, c.scheme)
+	if err != nil {
+		return nil, err
+	}
+
+	gvk.Kind = strings.TrimSuffix(gvk.Kind, "List")
+
+	listOpts := client.ListOptions{}
+	listOpts.ApplyOptions(opts)
+
+	gvr, _ := meta.UnsafeGuessKindToResource(gvk)
+	return c.tracker.Watch(gvr, listOpts.Namespace)
+}
+
+func (c *fakeClient) List(ctx context.Context, obj client.ObjectList, opts ...client.ListOption) error {
+	gvk, err := apiutil.GVKForObject(obj, c.scheme)
+	if err != nil {
+		return err
+	}
+
+	originalKind := gvk.Kind
+
+	gvk.Kind = strings.TrimSuffix(gvk.Kind, "List")
+
+	if _, isUnstructuredList := obj.(*unstructured.UnstructuredList); isUnstructuredList && !c.scheme.Recognizes(gvk) {
+		// We need to register the ListKind with UnstructuredList:
+		// https://github.com/kubernetes/kubernetes/blob/7b2776b89fb1be28d4e9203bdeec079be903c103/staging/src/k8s.io/client-go/dynamic/fake/simple.go#L44-L51
+		c.schemeWriteLock.Lock()
+		c.scheme.AddKnownTypeWithName(gvk.GroupVersion().WithKind(gvk.Kind+"List"), &unstructured.UnstructuredList{})
+		c.schemeWriteLock.Unlock()
+	}
+
+	listOpts := client.ListOptions{}
+	listOpts.ApplyOptions(opts)
+
+	gvr, _ := meta.UnsafeGuessKindToResource(gvk)
+	o, err := c.tracker.List(gvr, gvk, listOpts.Namespace)
+	if err != nil {
+		return err
+	}
+
+	ta, err := meta.TypeAccessor(o)
+	if err != nil {
+		return err
+	}
+	ta.SetKind(originalKind)
+	ta.SetAPIVersion(gvk.GroupVersion().String())
+
+	j, err := json.Marshal(o)
+	if err != nil {
+		return err
+	}
+	decoder := scheme.Codecs.UniversalDecoder()
+	zero(obj)
+	_, _, err = decoder.Decode(j, nil, obj)
+	if err != nil {
+		return err
+	}
+
+	if listOpts.LabelSelector != nil {
+		objs, err := meta.ExtractList(obj)
+		if err != nil {
+			return err
+		}
+		filteredObjs, err := objectutil.FilterWithLabels(objs, listOpts.LabelSelector)
+		if err != nil {
+			return err
+		}
+		err = meta.SetList(obj, filteredObjs)
+		if err != nil {
+			return err
+		}
+	}
+	return nil
+}
+
+func (c *fakeClient) Scheme() *runtime.Scheme {
+	return c.scheme
+}
+
+func (c *fakeClient) RESTMapper() meta.RESTMapper {
+	return c.restMapper
+}
+
+func (c *fakeClient) Create(ctx context.Context, obj client.Object, opts ...client.CreateOption) error {
+	createOptions := &client.CreateOptions{}
+	createOptions.ApplyOptions(opts)
+
+	for _, dryRunOpt := range createOptions.DryRun {
+		if dryRunOpt == metav1.DryRunAll {
+			return nil
+		}
+	}
+
+	gvr, err := getGVRFromObject(obj, c.scheme)
+	if err != nil {
+		return err
+	}
+	accessor, err := meta.Accessor(obj)
+	if err != nil {
+		return err
+	}
+
+	if accessor.GetName() == "" && accessor.GetGenerateName() != "" {
+		base := accessor.GetGenerateName()
+		if len(base) > maxGeneratedNameLength {
+			base = base[:maxGeneratedNameLength]
+		}
+		accessor.SetName(fmt.Sprintf("%s%s", base, utilrand.String(randomLength)))
+	}
+
+	return c.tracker.Create(gvr, obj, accessor.GetNamespace())
+}
+
+func (c *fakeClient) Delete(ctx context.Context, obj client.Object, opts ...client.DeleteOption) error {
+	gvr, err := getGVRFromObject(obj, c.scheme)
+	if err != nil {
+		return err
+	}
+	accessor, err := meta.Accessor(obj)
+	if err != nil {
+		return err
+	}
+	delOptions := client.DeleteOptions{}
+	delOptions.ApplyOptions(opts)
+
+	for _, dryRunOpt := range delOptions.DryRun {
+		if dryRunOpt == metav1.DryRunAll {
+			return nil
+		}
+	}
+
+	// Check the ResourceVersion if that Precondition was specified.
+	if delOptions.Preconditions != nil && delOptions.Preconditions.ResourceVersion != nil {
+		name := accessor.GetName()
+		dbObj, err := c.tracker.Get(gvr, accessor.GetNamespace(), name)
+		if err != nil {
+			return err
+		}
+		oldAccessor, err := meta.Accessor(dbObj)
+		if err != nil {
+			return err
+		}
+		actualRV := oldAccessor.GetResourceVersion()
+		expectRV := *delOptions.Preconditions.ResourceVersion
+		if actualRV != expectRV {
+			msg := fmt.Sprintf(
+				"the ResourceVersion in the precondition (%s) does not match the ResourceVersion in record (%s). "+
+					"The object might have been modified",
+				expectRV, actualRV)
+			return apierrors.NewConflict(gvr.GroupResource(), name, errors.New(msg))
+		}
+	}
+
+	return c.deleteObject(gvr, accessor)
+}
+
+func (c *fakeClient) DeleteAllOf(ctx context.Context, obj client.Object, opts ...client.DeleteAllOfOption) error {
+	gvk, err := apiutil.GVKForObject(obj, c.scheme)
+	if err != nil {
+		return err
+	}
+
+	dcOptions := client.DeleteAllOfOptions{}
+	dcOptions.ApplyOptions(opts)
+
+	for _, dryRunOpt := range dcOptions.DryRun {
+		if dryRunOpt == metav1.DryRunAll {
+			return nil
+		}
+	}
+
+	gvr, _ := meta.UnsafeGuessKindToResource(gvk)
+	o, err := c.tracker.List(gvr, gvk, dcOptions.Namespace)
+	if err != nil {
+		return err
+	}
+
+	objs, err := meta.ExtractList(o)
+	if err != nil {
+		return err
+	}
+	filteredObjs, err := objectutil.FilterWithLabels(objs, dcOptions.LabelSelector)
+	if err != nil {
+		return err
+	}
+	for _, o := range filteredObjs {
+		accessor, err := meta.Accessor(o)
+		if err != nil {
+			return err
+		}
+		err = c.deleteObject(gvr, accessor)
+		if err != nil {
+			return err
+		}
+	}
+	return nil
+}
+
+func (c *fakeClient) Update(ctx context.Context, obj client.Object, opts ...client.UpdateOption) error {
+	updateOptions := &client.UpdateOptions{}
+	updateOptions.ApplyOptions(opts)
+
+	for _, dryRunOpt := range updateOptions.DryRun {
+		if dryRunOpt == metav1.DryRunAll {
+			return nil
+		}
+	}
+
+	gvr, err := getGVRFromObject(obj, c.scheme)
+	if err != nil {
+		return err
+	}
+	accessor, err := meta.Accessor(obj)
+	if err != nil {
+		return err
+	}
+	return c.tracker.Update(gvr, obj, accessor.GetNamespace())
+}
+
+func (c *fakeClient) Patch(ctx context.Context, obj client.Object, patch client.Patch, opts ...client.PatchOption) error {
+	patchOptions := &client.PatchOptions{}
+	patchOptions.ApplyOptions(opts)
+
+	for _, dryRunOpt := range patchOptions.DryRun {
+		if dryRunOpt == metav1.DryRunAll {
+			return nil
+		}
+	}
+
+	gvr, err := getGVRFromObject(obj, c.scheme)
+	if err != nil {
+		return err
+	}
+	accessor, err := meta.Accessor(obj)
+	if err != nil {
+		return err
+	}
+	data, err := patch.Data(obj)
+	if err != nil {
+		return err
+	}
+
+	reaction := testing.ObjectReaction(c.tracker)
+	handled, o, err := reaction(testing.NewPatchAction(gvr, accessor.GetNamespace(), accessor.GetName(), patch.Type(), data))
+	if err != nil {
+		return err
+	}
+	if !handled {
+		panic("tracker could not handle patch method")
+	}
+
+	gvk, err := apiutil.GVKForObject(obj, c.scheme)
+	if err != nil {
+		return err
+	}
+	ta, err := meta.TypeAccessor(o)
+	if err != nil {
+		return err
+	}
+	ta.SetKind(gvk.Kind)
+	ta.SetAPIVersion(gvk.GroupVersion().String())
+
+	j, err := json.Marshal(o)
+	if err != nil {
+		return err
+	}
+	decoder := scheme.Codecs.UniversalDecoder()
+	zero(obj)
+	_, _, err = decoder.Decode(j, nil, obj)
+	return err
+}
+
+func (c *fakeClient) Status() client.StatusWriter {
+	return &fakeStatusWriter{client: c}
+}
+
+func (c *fakeClient) deleteObject(gvr schema.GroupVersionResource, accessor metav1.Object) error {
+	old, err := c.tracker.Get(gvr, accessor.GetNamespace(), accessor.GetName())
+	if err == nil {
+		oldAccessor, err := meta.Accessor(old)
+		if err == nil {
+			if len(oldAccessor.GetFinalizers()) > 0 {
+				now := metav1.Now()
+				oldAccessor.SetDeletionTimestamp(&now)
+				return c.tracker.Update(gvr, old, accessor.GetNamespace())
+			}
+		}
+	}
+
+	//TODO: implement propagation
+	return c.tracker.Delete(gvr, accessor.GetNamespace(), accessor.GetName())
+}
+
+func getGVRFromObject(obj runtime.Object, scheme *runtime.Scheme) (schema.GroupVersionResource, error) {
+	gvk, err := apiutil.GVKForObject(obj, scheme)
+	if err != nil {
+		return schema.GroupVersionResource{}, err
+	}
+	gvr, _ := meta.UnsafeGuessKindToResource(gvk)
+	return gvr, nil
+}
+
+type fakeStatusWriter struct {
+	client *fakeClient
+}
+
+func (sw *fakeStatusWriter) Update(ctx context.Context, obj client.Object, opts ...client.UpdateOption) error {
+	// TODO(droot): This results in full update of the obj (spec + status). Need
+	// a way to update status field only.
+	return sw.client.Update(ctx, obj, opts...)
+}
+
+func (sw *fakeStatusWriter) Patch(ctx context.Context, obj client.Object, patch client.Patch, opts ...client.PatchOption) error {
+	// TODO(droot): This results in full update of the obj (spec + status). Need
+	// a way to update status field only.
+	return sw.client.Patch(ctx, obj, patch, opts...)
+}
+
+func allowsUnconditionalUpdate(gvk schema.GroupVersionKind) bool {
+	switch gvk.Group {
+	case "apps":
+		switch gvk.Kind {
+		case "ControllerRevision", "DaemonSet", "Deployment", "ReplicaSet", "StatefulSet":
+			return true
+		}
+	case "autoscaling":
+		switch gvk.Kind {
+		case "HorizontalPodAutoscaler":
+			return true
+		}
+	case "batch":
+		switch gvk.Kind {
+		case "CronJob", "Job":
+			return true
+		}
+	case "certificates":
+		switch gvk.Kind {
+		case "Certificates":
+			return true
+		}
+	case "flowcontrol":
+		switch gvk.Kind {
+		case "FlowSchema", "PriorityLevelConfiguration":
+			return true
+		}
+	case "networking":
+		switch gvk.Kind {
+		case "Ingress", "IngressClass", "NetworkPolicy":
+			return true
+		}
+	case "policy":
+		switch gvk.Kind {
+		case "PodSecurityPolicy":
+			return true
+		}
+	case "rbac":
+		switch gvk.Kind {
+		case "ClusterRole", "ClusterRoleBinding", "Role", "RoleBinding":
+			return true
+		}
+	case "scheduling":
+		switch gvk.Kind {
+		case "PriorityClass":
+			return true
+		}
+	case "settings":
+		switch gvk.Kind {
+		case "PodPreset":
+			return true
+		}
+	case "storage":
+		switch gvk.Kind {
+		case "StorageClass":
+			return true
+		}
+	case "":
+		switch gvk.Kind {
+		case "ConfigMap", "Endpoint", "Event", "LimitRange", "Namespace", "Node",
+			"PersistentVolume", "PersistentVolumeClaim", "Pod", "PodTemplate",
+			"ReplicationController", "ResourceQuota", "Secret", "Service",
+			"ServiceAccount", "EndpointSlice":
+			return true
+		}
+	}
+
+	return false
+}
+
+func allowsCreateOnUpdate(gvk schema.GroupVersionKind) bool {
+	switch gvk.Group {
+	case "coordination":
+		switch gvk.Kind {
+		case "Lease":
+			return true
+		}
+	case "node":
+		switch gvk.Kind {
+		case "RuntimeClass":
+			return true
+		}
+	case "rbac":
+		switch gvk.Kind {
+		case "ClusterRole", "ClusterRoleBinding", "Role", "RoleBinding":
+			return true
+		}
+	case "":
+		switch gvk.Kind {
+		case "Endpoint", "Event", "LimitRange", "Service":
+			return true
+		}
+	}
+
+	return false
+}
+
+// zero zeros the value of a pointer.
+func zero(x interface{}) {
+	if x == nil {
+		return
+	}
+	res := reflect.ValueOf(x).Elem()
+	res.Set(reflect.Zero(res.Type()))
+}
diff --git a/vendor/sigs.k8s.io/controller-runtime/pkg/client/fake/doc.go b/vendor/sigs.k8s.io/controller-runtime/pkg/client/fake/doc.go
new file mode 100644
index 000000000..d0614666e
--- /dev/null
+++ b/vendor/sigs.k8s.io/controller-runtime/pkg/client/fake/doc.go
@@ -0,0 +1,38 @@
+/*
+Copyright 2018 The Kubernetes 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 provides a fake client for testing.
+
+A fake client is backed by its simple object store indexed by GroupVersionResource.
+You can create a fake client with optional objects.
+
+	client := NewFakeClientWithScheme(scheme, initObjs...) // initObjs is a slice of runtime.Object
+
+You can invoke the methods defined in the Client interface.
+
+When in doubt, it's almost always better not to use this package and instead use
+envtest.Environment with a real client and API server.
+
+WARNING: ⚠️ Current Limitations / Known Issues with the fake Client ⚠️
+  - This client does not have a way to inject specific errors to test handled vs. unhandled errors.
+  - There is some support for sub resources which can cause issues with tests if you're trying to update
+    e.g. metadata and status in the same reconcile.
+  - No OpenAPI validation is performed when creating or updating objects.
+  - ObjectMeta's `Generation` and `ResourceVersion` don't behave properly, Patch or Update
+    operations that rely on these fields will fail, or give false positives.
+*/
+package fake


Test patch:
diff --git a/pkg/nodemanager/node_manager_suite_test.go b/pkg/nodemanager/node_manager_suite_test.go
new file mode 100644
index 000000000..547fd9b37
--- /dev/null
+++ b/pkg/nodemanager/node_manager_suite_test.go
@@ -0,0 +1,39 @@
+// Copyright 2022 Authors of spidernet-io
+// SPDX-License-Identifier: Apache-2.0
+
+package nodemanager_test
+
+import (
+	"testing"
+
+	. "github.com/onsi/ginkgo/v2"
+	. "github.com/onsi/gomega"
+	corev1 "k8s.io/api/core/v1"
+	"k8s.io/apimachinery/pkg/runtime"
+	"sigs.k8s.io/controller-runtime/pkg/client"
+	"sigs.k8s.io/controller-runtime/pkg/client/fake"
+
+	"github.com/spidernet-io/spiderpool/pkg/nodemanager"
+)
+
+func TestNodeManager(t *testing.T) {
+	RegisterFailHandler(Fail)
+	RunSpecs(t, "NodeManager Suite", Label("node_manager", "unitest"))
+}
+
+var scheme *runtime.Scheme
+var fakeClient client.Client
+var nodeManager nodemanager.NodeManager
+
+var _ = BeforeSuite(func() {
+	scheme = runtime.NewScheme()
+	err := corev1.AddToScheme(scheme)
+	Expect(err).NotTo(HaveOccurred())
+
+	fakeClient = fake.NewClientBuilder().
+		WithScheme(scheme).
+		Build()
+
+	nodeManager, err = nodemanager.NewNodeManager(fakeClient)
+	Expect(err).NotTo(HaveOccurred())
+})
diff --git a/pkg/nodemanager/node_manager_test.go b/pkg/nodemanager/node_manager_test.go
index f67f780aa..2097090a1 100644
--- a/pkg/nodemanager/node_manager_test.go
+++ b/pkg/nodemanager/node_manager_test.go
@@ -4,9 +4,217 @@
 package nodemanager_test
 
 import (
+	"context"
+	"fmt"
+	"sync/atomic"
+
+	"github.com/agiledragon/gomonkey/v2"
 	. "github.com/onsi/ginkgo/v2"
+	. "github.com/onsi/gomega"
+	corev1 "k8s.io/api/core/v1"
+	apierrors "k8s.io/apimachinery/pkg/api/errors"
+	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
+	"sigs.k8s.io/controller-runtime/pkg/client"
+
+	"github.com/spidernet-io/spiderpool/pkg/constant"
+	"github.com/spidernet-io/spiderpool/pkg/nodemanager"
 )
 
-var _ = Describe("Nodemanager", Label("unittest", "nodemanager_test"), func() {
+var _ = Describe("NodeManager", Label("node_manager_test"), func() {
+	Describe("New NodeManager", func() {
+		It("inputs nil client", func() {
+			manager, err := nodemanager.NewNodeManager(nil)
+			Expect(err).To(MatchError(constant.ErrMissingRequiredParam))
+			Expect(manager).To(BeNil())
+		})
+	})
+
+	Describe("Test NodeManager's method", func() {
+		var count uint64
+		var nodeName string
+		var labels map[string]string
+		var nodeT *corev1.Node
+
+		BeforeEach(func() {
+			atomic.AddUint64(&count, 1)
+			nodeName = fmt.Sprintf("node-%v", count)
+			labels = map[string]string{"foo": fmt.Sprintf("bar-%v", count)}
+			nodeT = &corev1.Node{
+				TypeMeta: metav1.TypeMeta{
+					Kind:       "Node",
+					APIVersion: "v1",
+				},
+				ObjectMeta: metav1.ObjectMeta{
+					Name:   nodeName,
+					Labels: labels,
+				},
+				Spec: corev1.NodeSpec{},
+			}
+		})
+
+		var deleteOption *client.DeleteOptions
+
+		AfterEach(func() {
+			zero := int64(0)
+			policy := metav1.DeletePropagationForeground
+			deleteOption = &client.DeleteOptions{
+				GracePeriodSeconds: &zero,
+				PropagationPolicy:  &policy,
+			}
+
+			ctx := context.TODO()
+			err := fakeClient.Delete(ctx, nodeT, deleteOption)
+			Expect(client.IgnoreNotFound(err)).NotTo(HaveOccurred())
+		})
+
+		Describe("GetNodeByName", func() {
+			It("gets non-existent Node", func() {
+				ctx := context.TODO()
+				node, err := nodeManager.GetNodeByName(ctx, nodeName)
+				Expect(apierrors.IsNotFound(err)).To(BeTrue())
+				Expect(node).To(BeNil())
+			})
+
+			It("gets an existing Node", func() {
+				ctx := context.TODO()
+				err := fakeClient.Create(ctx, nodeT)
+				Expect(err).NotTo(HaveOccurred())
+
+				node, err := nodeManager.GetNodeByName(ctx, nodeName)
+				Expect(err).NotTo(HaveOccurred())
+				Expect(node).NotTo(BeNil())
+
+				Expect(node).To(Equal(nodeT))
+			})
+		})
+
+		Describe("ListNodes", func() {
+			It("failed to list Nodes due to some unknown errors", func() {
+				patches := gomonkey.ApplyMethodReturn(fakeClient, "List", constant.ErrUnknown)
+				defer patches.Reset()
+
+				ctx := context.TODO()
+				err := fakeClient.Create(ctx, nodeT)
+				Expect(err).NotTo(HaveOccurred())
+
+				nodeList, err := nodeManager.ListNodes(ctx)
+				Expect(err).To(MatchError(constant.ErrUnknown))
+				Expect(nodeList).To(BeNil())
+			})
+
+			It("lists all Nodes", func() {
+				ctx := context.TODO()
+				err := fakeClient.Create(ctx, nodeT)
+				Expect(err).NotTo(HaveOccurred())
+
+				nodeList, err := nodeManager.ListNodes(ctx)
+				Expect(err).NotTo(HaveOccurred())
+				Expect(nodeList.Items).NotTo(BeEmpty())
+
+				hasNode := false
+				for _, node := range nodeList.Items {
+					if node.Name == nodeName {
+						hasNode = true
+						break
+					}
+				}
+				Expect(hasNode).To(BeTrue())
+			})
+
+			It("filters results by label selector", func() {
+				ctx := context.TODO()
+				err := fakeClient.Create(ctx, nodeT)
+				Expect(err).NotTo(HaveOccurred())
+
+				nodeList, err := nodeManager.ListNodes(ctx, client.MatchingLabels(labels))
+				Expect(err).NotTo(HaveOccurred())
+				Expect(nodeList.Items).NotTo(BeEmpty())
+
+				hasNode := false
+				for _, node := range nodeList.Items {
+					if node.Name == nodeName {
+						hasNode = true
+						break
+					}
+				}
+				Expect(hasNode).To(BeTrue())
+			})
+
+			It("filters results by field selector", func() {
+				ctx := context.TODO()
+				err := fakeClient.Create(ctx, nodeT)
+				Expect(err).NotTo(HaveOccurred())
+
+				nodeList, err := nodeManager.ListNodes(ctx, client.MatchingFields{metav1.ObjectNameField: nodeName})
+				Expect(err).NotTo(HaveOccurred())
+				Expect(nodeList.Items).NotTo(BeEmpty())
+
+				hasNode := false
+				for _, node := range nodeList.Items {
+					if node.Name == nodeName {
+						hasNode = true
+						break
+					}
+				}
+				Expect(hasNode).To(BeTrue())
+			})
+		})
+
+		Describe("MatchLabelSelector", func() {
+			It("failed to list Nodes due to some unknown errors", func() {
+				patches := gomonkey.ApplyMethodReturn(fakeClient, "List", constant.ErrUnknown)
+				defer patches.Reset()
+
+				ctx := context.TODO()
+				err := fakeClient.Create(ctx, nodeT)
+				Expect(err).NotTo(HaveOccurred())
+
+				match, err := nodeManager.MatchLabelSelector(ctx, nodeName, &metav1.LabelSelector{MatchLabels: labels})
+				Expect(err).To(MatchError(constant.ErrUnknown))
+				Expect(match).To(BeFalse())
+			})
+
+			It("checks non-existent Node", func() {
+				ctx := context.TODO()
+				match, err := nodeManager.MatchLabelSelector(ctx, nodeName, &metav1.LabelSelector{MatchLabels: labels})
+				Expect(err).NotTo(HaveOccurred())
+				Expect(match).To(BeFalse())
+			})
+
+			It("matches invalid label selector", func() {
+				ctx := context.TODO()
+				err := fakeClient.Create(ctx, nodeT)
+				Expect(err).NotTo(HaveOccurred())
+
+				invalidSelector := &metav1.LabelSelector{
+					MatchLabels: map[string]string{
+						"": "",
+					},
+				}
+				match, err := nodeManager.MatchLabelSelector(ctx, nodeName, invalidSelector)
+				Expect(err).To(HaveOccurred())
+				Expect(match).To(BeFalse())
+			})
+
+			It("matches nothing", func() {
+				ctx := context.TODO()
+				err := fakeClient.Create(ctx, nodeT)
+				Expect(err).NotTo(HaveOccurred())
+
+				match, err := nodeManager.MatchLabelSelector(ctx, nodeName, nil)
+				Expect(err).NotTo(HaveOccurred())
+				Expect(match).To(BeFalse())
+			})
+
+			It("matches the label selector", func() {
+				ctx := context.TODO()
+				err := fakeClient.Create(ctx, nodeT)
+				Expect(err).NotTo(HaveOccurred())
 
+				match, err := nodeManager.MatchLabelSelector(ctx, nodeName, &metav1.LabelSelector{MatchLabels: labels})
+				Expect(err).NotTo(HaveOccurred())
+				Expect(match).To(BeTrue())
+			})
+		})
+	})
 })
diff --git a/pkg/nodemanager/nodemanager_suite_test.go b/pkg/nodemanager/nodemanager_suite_test.go
deleted file mode 100644
index b35c3ca2d..000000000
--- a/pkg/nodemanager/nodemanager_suite_test.go
+++ /dev/null
@@ -1,16 +0,0 @@
-// Copyright 2022 Authors of spidernet-io
-// SPDX-License-Identifier: Apache-2.0
-
-package nodemanager_test
-
-import (
-	"testing"
-
-	. "github.com/onsi/ginkgo/v2"
-	. "github.com/onsi/gomega"
-)
-
-func TestNodemanager(t *testing.T) {
-	RegisterFailHandler(Fail)
-	RunSpecs(t, "Nodemanager Suite")
-}
diff --git a/test/Makefile b/test/Makefile
index a2d9e73c9..cf821cf28 100644
--- a/test/Makefile
+++ b/test/Makefile
@@ -281,7 +281,7 @@ e2e_test:
 		RESULT=0 ; \
 		$(ROOT_DIR)/tools/scripts/ginkgo.sh \
 			--race --timeout=$(E2E_TIMEOUT) --output-interceptor-mode=none --poll-progress-after=120s --poll-progress-interval=30s \
-			--json-report e2ereport.json --output-dir $(ROOT_DIR) --procs $(E2E_GINKGO_PROCS) \
+			--json-report e2ereport.json --output-dir $(ROOT_DIR) --procs $(E2E_GINKGO_PROCS) -gcflags=-l \
 			--label-filter="$(E2E_GINKGO_LABELS)" -randomize-suites -randomize-all  -vv --fail-fast  $(GINKGO_OPTION) \
 			-r e2e/*  || RESULT=1  ; \
 		echo "=========== after test `date` ===========" >> $(E2E_LOG_FILE) ; \