Repository: spidernet-io/spiderpool
Pull request #472 (2022-07-26 02:20:53)

 Fixed the address conflict in pkg/ipam/tool.go
Owing to the variable version has defined within one time, I reassign a new version variable in each branch of if to handle the address conflict.


Patch:
diff --git a/.github/workflows/build-image-base.yaml b/.github/workflows/build-image-base.yaml
index cae0108c8..974bb1c1f 100644
--- a/.github/workflows/build-image-base.yaml
+++ b/.github/workflows/build-image-base.yaml
@@ -106,7 +106,7 @@ jobs:
 
       - name: Release build ${{ env.IMAGE_NAME }}
         if: ${{ steps.tag-in-repositories.outputs.exists == 'false' }}
-        uses: docker/build-push-action@v3.1.0
+        uses: docker/build-push-action@v3.0.0
         continue-on-error: false
         id: docker_build_release
         with:
diff --git a/.github/workflows/build-image-ci.yaml b/.github/workflows/build-image-ci.yaml
index 4e9f8c031..fdb15322b 100644
--- a/.github/workflows/build-image-ci.yaml
+++ b/.github/workflows/build-image-ci.yaml
@@ -131,7 +131,7 @@ jobs:
 
       # Import GitHub's cache build to docker cache
       - name: Copy ${{ matrix.name }} Golang cache to docker cache
-        uses: docker/build-push-action@v3.1.0
+        uses: docker/build-push-action@v3.0.0
         with:
           context: /tmp/.cache/${{ matrix.name }}
           file: ./images/cache/Dockerfile
@@ -144,7 +144,7 @@ jobs:
       # build normal image
       - name: CI Build ${{ matrix.name }}
         if: ${{ github.event_name != 'pull_request_target' }}
-        uses: docker/build-push-action@v3.1.0
+        uses: docker/build-push-action@v3.0.0
         continue-on-error: false
         id: docker_build_ci_master
         with:
@@ -166,7 +166,7 @@ jobs:
       # build debug image who turn on race and deadlock detection
       - name: CI race detection Build ${{ matrix.name }}
         if: ${{ github.event_name != 'pull_request_target' }}
-        uses: docker/build-push-action@v3.1.0
+        uses: docker/build-push-action@v3.0.0
         continue-on-error: false
         id: docker_build_ci_master_detect_race_condition
         with:
@@ -198,7 +198,7 @@ jobs:
       # =========== trigger by PR updates , build 2 images
       - name: CI Build ${{ matrix.name }}
         if: ${{ github.event_name == 'pull_request_target' }}
-        uses: docker/build-push-action@v3.1.0
+        uses: docker/build-push-action@v3.0.0
         continue-on-error: false
         id: docker_build_ci_pr
         with:
@@ -216,7 +216,7 @@ jobs:
 
       - name: CI race detection Build ${{ matrix.name }}
         if: ${{ github.event_name == 'pull_request_target' }}
-        uses: docker/build-push-action@v3.1.0
+        uses: docker/build-push-action@v3.0.0
         continue-on-error: false
         id: docker_build_ci_pr_detect_race_condition
         with:
@@ -262,7 +262,7 @@ jobs:
       # Store docker's golang's cache build locally only on the main branch
       - name: Store ${{ matrix.name }} Golang cache build locally
         if: ${{ github.event_name != 'pull_request_target' && steps.cache.outputs.cache-hit != 'true' }}
-        uses: docker/build-push-action@v3.1.0
+        uses: docker/build-push-action@v3.0.0
         with:
           context: .
           file: ./images/cache/Dockerfile
diff --git a/.github/workflows/call-release-image.yaml b/.github/workflows/call-release-image.yaml
index 84e12d3a1..88e56d1d9 100644
--- a/.github/workflows/call-release-image.yaml
+++ b/.github/workflows/call-release-image.yaml
@@ -78,7 +78,7 @@ jobs:
           echo ::set-output name=committime::${GIT_COMMIT_TIME}
 
       - name: Build Image ${{ matrix.name }}
-        uses: docker/build-push-action@v3.1.0
+        uses: docker/build-push-action@v3.0.0
         id: docker_build_release
         with:
           context: ${{ matrix.context }}
diff --git a/charts/spiderpool/crds/spiderpool.spidernet.io_spiderippools.yaml b/charts/spiderpool/crds/spiderpool.spidernet.io_spiderippools.yaml
index b36f01d87..f2fc9a5a4 100644
--- a/charts/spiderpool/crds/spiderpool.spidernet.io_spiderippools.yaml
+++ b/charts/spiderpool/crds/spiderpool.spidernet.io_spiderippools.yaml
@@ -70,6 +70,7 @@ spec:
               gateway:
                 type: string
               ipVersion:
+                default: IPv4
                 enum:
                 - IPv4
                 - IPv6
diff --git a/charts/spiderpool/templates/webhook.yaml b/charts/spiderpool/templates/webhook.yaml
index 2d1b96b5e..1c3cc0421 100644
--- a/charts/spiderpool/templates/webhook.yaml
+++ b/charts/spiderpool/templates/webhook.yaml
@@ -31,7 +31,7 @@ webhooks:
     - CREATE
     - UPDATE
     resources:
-    - spiderippools
+    - ippools
   sideEffects: None
 ---
 apiVersion: admissionregistration.k8s.io/v1
@@ -67,5 +67,5 @@ webhooks:
     - UPDATE
     - DELETE
     resources:
-    - spiderippools
+    - ippools
   sideEffects: None
diff --git a/cmd/spiderpool-controller/cmd/config.go b/cmd/spiderpool-controller/cmd/config.go
index d48a01163..36c3826c7 100644
--- a/cmd/spiderpool-controller/cmd/config.go
+++ b/cmd/spiderpool-controller/cmd/config.go
@@ -12,7 +12,6 @@ import (
 
 	"github.com/spidernet-io/spiderpool/api/v1/controller/server"
 	"github.com/spidernet-io/spiderpool/pkg/constant"
-	"github.com/spidernet-io/spiderpool/pkg/election"
 	"github.com/spidernet-io/spiderpool/pkg/gcmanager"
 	"github.com/spidernet-io/spiderpool/pkg/ippoolmanager"
 	"github.com/spidernet-io/spiderpool/pkg/namespacemanager"
@@ -62,18 +61,15 @@ var envInfo = []envConf{
 	{"SPIDERPOOL_GC_SIGNAL_TIMEOUT_DURATION", "3", true, nil, nil, &gcIPConfig.GCSignalTimeoutDuration},
 	{"SPIDERPOOL_GC_HTTP_REQUEST_TIME_GAP", "1", true, nil, nil, &gcIPConfig.GCSignalGapDuration},
 	{"SPIDERPOOL_GC_ADDITIONAL_GRACE_DELAY", "5", true, nil, nil, &gcIPConfig.AdditionalGraceDelay},
-	{"SPIDERPOOL_POD_NAMESPACE", "", true, &controllerContext.Cfg.ControllerPodNamespace, nil, nil},
-	{"SPIDERPOOL_POD_NAME", "", true, &controllerContext.Cfg.ControllerPodName, nil, nil},
-	{"SPIDERPOOL_GC_LEADER_DURATION", "15", true, nil, nil, &controllerContext.Cfg.LeaseDuration},
-	{"SPIDERPOOL_GC_LEADER_RENEW_DEADLINE", "10", true, nil, nil, &controllerContext.Cfg.LeaseRenewDeadline},
-	{"SPIDERPOOL_GC_LEADER_RETRY_PERIOD", "2", true, nil, nil, &controllerContext.Cfg.LeaseRetryPeriod},
-	{"SPIDERPOOL_GC_LEADER_RETRY_GAP", "1", true, nil, nil, &controllerContext.Cfg.LeaseRetryGap},
+	{"SPIDERPOOL_POD_NAMESPACE", "", true, &gcIPConfig.ControllerPodNamespace, nil, nil},
+	{"SPIDERPOOL_POD_NAME", "", true, &gcIPConfig.ControllerPodName, nil, nil},
+	{"SPIDERPOOL_GC_LEADER_DURATION", "15", true, nil, nil, &gcIPConfig.LeaseDuration},
+	{"SPIDERPOOL_GC_LEADER_RENEW_DEADLINE", "10", true, nil, nil, &gcIPConfig.LeaseRenewDeadline},
+	{"SPIDERPOOL_GC_LEADER_RETRY_PERIOD", "2", true, nil, nil, &gcIPConfig.LeaseRetryPeriod},
+	{"SPIDERPOOL_GC_LEADER_RETRY_GAP", "1", true, nil, nil, &gcIPConfig.LeaseRetryGap},
 }
 
 type Config struct {
-	ControllerPodName      string
-	ControllerPodNamespace string
-
 	// flags
 	ConfigPath        string
 	TlsServerCertPath string
@@ -95,11 +91,6 @@ type Config struct {
 	UpdateCRRetryUnitTime             int
 	WorkloadEndpointMaxHistoryRecords int
 	IPPoolMaxAllocatedIPs             int
-
-	LeaseDuration      int
-	LeaseRenewDeadline int
-	LeaseRetryPeriod   int
-	LeaseRetryGap      int
 }
 
 type ControllerContext struct {
@@ -119,7 +110,6 @@ type ControllerContext struct {
 	IPPoolManager ippoolmanager.IPPoolManager
 	PodManager    podmanager.PodManager
 	GCManager     gcmanager.GCManager
-	Leader        election.SpiderLeaseElector
 
 	// handler
 	HttpServer *server.Server
diff --git a/cmd/spiderpool-controller/cmd/crd_manager.go b/cmd/spiderpool-controller/cmd/crd_manager.go
index fcba8985a..ffef3c80d 100644
--- a/cmd/spiderpool-controller/cmd/crd_manager.go
+++ b/cmd/spiderpool-controller/cmd/crd_manager.go
@@ -20,6 +20,7 @@ import (
 	"sigs.k8s.io/controller-runtime/pkg/client"
 
 	spiderpoolv1 "github.com/spidernet-io/spiderpool/pkg/k8s/apis/v1"
+	"github.com/spidernet-io/spiderpool/pkg/webhook"
 )
 
 var scheme = runtime.NewScheme()
@@ -55,5 +56,12 @@ func newCRDManager() (ctrl.Manager, error) {
 		return nil, err
 	}
 
+	if err = (&webhook.IPPoolWebhook{
+		Client: mgr.GetClient(),
+		Scheme: mgr.GetScheme(),
+	}).SetupWebhookWithManager(mgr); err != nil {
+		return nil, err
+	}
+
 	return mgr, nil
 }
diff --git a/cmd/spiderpool-controller/cmd/daemon.go b/cmd/spiderpool-controller/cmd/daemon.go
index d9d3ba82b..91853fba1 100644
--- a/cmd/spiderpool-controller/cmd/daemon.go
+++ b/cmd/spiderpool-controller/cmd/daemon.go
@@ -6,18 +6,12 @@ package cmd
 import (
 	"context"
 	"fmt"
-
 	"net/http"
 	"os"
 	"os/signal"
 	"syscall"
 	"time"
 
-	"github.com/google/gops/agent"
-	"github.com/pyroscope-io/client/pyroscope"
-
-	"github.com/spidernet-io/spiderpool/pkg/constant"
-	"github.com/spidernet-io/spiderpool/pkg/election"
 	"github.com/spidernet-io/spiderpool/pkg/gcmanager"
 	"github.com/spidernet-io/spiderpool/pkg/ippoolmanager"
 	"github.com/spidernet-io/spiderpool/pkg/logutils"
@@ -26,6 +20,9 @@ import (
 	"github.com/spidernet-io/spiderpool/pkg/podmanager"
 	"github.com/spidernet-io/spiderpool/pkg/reservedipmanager"
 	"github.com/spidernet-io/spiderpool/pkg/workloadendpointmanager"
+
+	"github.com/google/gops/agent"
+	"github.com/pyroscope-io/client/pyroscope"
 )
 
 // DaemonMain runs controllerContext handlers.
@@ -155,10 +152,6 @@ func WatchSignal(sigCh chan os.Signal) {
 }
 
 func initControllerServiceManagers(ctx context.Context) {
-	// init spiderpool controller leader election
-	logger.Debug("Begin to initialize spiderpool controller leader election")
-	initSpiderControllerLeaderElect(controllerContext.InnerCtx)
-
 	logger.Debug("Begin to initialize WorkloadEndpoint Manager")
 	retrys := controllerContext.Cfg.UpdateCRMaxRetrys
 	unitTime := time.Duration(controllerContext.Cfg.UpdateCRRetryUnitTime) * time.Millisecond
@@ -204,25 +197,11 @@ func initControllerServiceManagers(ctx context.Context) {
 		logger.Fatal(err.Error())
 	}
 	controllerContext.IPPoolManager = ipPoolManager
-
-	// set up spiderpool controller IPPool reconcile
-	logger.Debug("Begin to set up IPPool reconcile")
-	err = controllerContext.IPPoolManager.SetupReconcile(controllerContext.CRDManager, controllerContext.Leader)
-	if err != nil {
-		logger.Fatal(err.Error())
-	}
-
-	// set up spiderpool controller IPPool webhook
-	logger.Debug("Begin to set up IPPool webhook")
-	err = controllerContext.IPPoolManager.SetupWebhook(controllerContext.CRDManager)
-	if err != nil {
-		logger.Fatal(err.Error())
-	}
 }
 
 func initGCManager(ctx context.Context) {
 	gcManager, err := gcmanager.NewGCManager(ctx, controllerContext.CRDManager.GetClient(), gcIPConfig, controllerContext.CRDManager,
-		controllerContext.WEPManager, controllerContext.IPPoolManager, controllerContext.PodManager, controllerContext.Leader)
+		controllerContext.WEPManager, controllerContext.IPPoolManager, controllerContext.PodManager)
 	if nil != err {
 		logger.Fatal(err.Error())
 	}
@@ -234,17 +213,3 @@ func initGCManager(ctx context.Context) {
 		logger.Fatal(fmt.Sprintf("start gc manager failed, err: '%v'", err))
 	}
 }
-
-func initSpiderControllerLeaderElect(ctx context.Context) {
-	leaseDuration := time.Duration(controllerContext.Cfg.LeaseDuration) * time.Second
-	renewDeadline := time.Duration(controllerContext.Cfg.LeaseRenewDeadline) * time.Second
-	leaseRetryPeriod := time.Duration(controllerContext.Cfg.LeaseRetryPeriod) * time.Second
-	leaderRetryElectGap := time.Duration(controllerContext.Cfg.LeaseRetryGap) * time.Second
-	leaderElector, err := election.NewLeaseElector(ctx, controllerContext.Cfg.ControllerPodNamespace, constant.SpiderControllerElectorLockName,
-		controllerContext.Cfg.ControllerPodName, &leaseDuration, &renewDeadline, &leaseRetryPeriod, &leaderRetryElectGap)
-	if nil != err {
-		logger.Fatal(err.Error())
-	}
-
-	controllerContext.Leader = leaderElector
-}
diff --git a/go.mod b/go.mod
index bd456f1df..502f3381f 100644
--- a/go.mod
+++ b/go.mod
@@ -18,7 +18,7 @@ require (
 	github.com/google/gops v0.3.25
 	github.com/jessevdk/go-flags v1.5.0
 	github.com/onsi/ginkgo/v2 v2.1.4
-	github.com/onsi/gomega v1.20.0
+	github.com/onsi/gomega v1.19.0
 	github.com/pyroscope-io/client v0.3.0
 	github.com/sasha-s/go-deadlock v0.3.1
 	github.com/spf13/cobra v1.5.0
@@ -66,7 +66,7 @@ require (
 	github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da // indirect
 	github.com/golang/protobuf v1.5.2 // indirect
 	github.com/google/gnostic v0.5.7-v3refs // indirect
-	github.com/google/go-cmp v0.5.8 // indirect
+	github.com/google/go-cmp v0.5.7 // indirect
 	github.com/google/gofuzz v1.1.0 // indirect
 	github.com/google/pprof v0.0.0-20210720184732-4bb14d4b1be1 // indirect
 	github.com/google/uuid v1.2.0 // indirect
@@ -117,7 +117,7 @@ require (
 	golang.org/x/time v0.0.0-20220210224613-90d013bbcef8 // indirect
 	gomodules.xyz/jsonpatch/v2 v2.2.0 // indirect
 	google.golang.org/appengine v1.6.7 // indirect
-	google.golang.org/protobuf v1.28.0 // indirect
+	google.golang.org/protobuf v1.27.1 // indirect
 	gopkg.in/inf.v0 v0.9.1 // indirect
 	gopkg.in/ini.v1 v1.66.3 // indirect
 	gopkg.in/yaml.v2 v2.4.0 // indirect
diff --git a/go.sum b/go.sum
index dafbb91d5..e269bd9b7 100644
--- a/go.sum
+++ b/go.sum
@@ -319,9 +319,8 @@ github.com/google/go-cmp v0.5.3/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/
 github.com/google/go-cmp v0.5.4/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
 github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
 github.com/google/go-cmp v0.5.6/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
+github.com/google/go-cmp v0.5.7 h1:81/ik6ipDQS2aGcBfIN5dHDB36BwrStyeAQquSYCV4o=
 github.com/google/go-cmp v0.5.7/go.mod h1:n+brtR0CgQNWTVd5ZUFpTBC8YFBDLK/h/bpaJ8/DtOE=
-github.com/google/go-cmp v0.5.8 h1:e6P7q2lk1O+qJJb4BtCQXlK8vWEO8V1ZeuEdJNOqZyg=
-github.com/google/go-cmp v0.5.8/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
 github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
 github.com/google/gofuzz v1.1.0 h1:Hsa8mG0dQ46ij8Sl2AYJDUv1oA9/d6Vk+3LG99Oe02g=
 github.com/google/gofuzz v1.1.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
@@ -511,8 +510,8 @@ github.com/onsi/gomega v0.0.0-20170829124025-dcabb60a477c/go.mod h1:C1qb7wdrVGGV
 github.com/onsi/gomega v1.7.1/go.mod h1:XdKZgCCFLUoM/7CFJVPcG8C1xQ1AJ0vpAezJrB7JYyY=
 github.com/onsi/gomega v1.10.1/go.mod h1:iN09h71vgCQne3DLsj+A5owkum+a2tYe+TOCB1ybHNo=
 github.com/onsi/gomega v1.17.0/go.mod h1:HnhC7FXeEQY45zxNK3PPoIUhzk/80Xly9PcubAlGdZY=
-github.com/onsi/gomega v1.20.0 h1:8W0cWlwFkflGPLltQvLRB7ZVD5HuP6ng320w2IS245Q=
-github.com/onsi/gomega v1.20.0/go.mod h1:DtrZpjmvpn2mPm4YWQa0/ALMDj9v4YxLgojwPeREyVo=
+github.com/onsi/gomega v1.19.0 h1:4ieX6qQjPP/BfC3mpsAtIGGlxTWPeA3Inl/7DtXw1tw=
+github.com/onsi/gomega v1.19.0/go.mod h1:LY+I3pBVzYsTBU1AnDwOSxaYi9WoWiqgwooUqq9yPro=
 github.com/opentracing/opentracing-go v1.1.0/go.mod h1:UkNAQd3GIcIGf0SeVgPpRdFStlNbqXla1AfSYxPUl2o=
 github.com/opentracing/opentracing-go v1.2.0 h1:uEJPy/1a5RIPAJ0Ov+OIO8OxWu77jEv+1B0VhjKrZUs=
 github.com/opentracing/opentracing-go v1.2.0/go.mod h1:GxEUsuufX4nBwe+T+Wl9TAgYrxe9dPLANfrWvHYVTgc=
@@ -1022,6 +1021,7 @@ golang.org/x/tools v0.1.11/go.mod h1:SgwaegtQh8clINPpECJMqnxLv9I09HLqnW3RMqW0CA4
 golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
 golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
 golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
+golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 h1:go1bK/D/BFZV2I8cIQd1NKEZ+0owSTG1fDTci4IqFcE=
 golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
 gomodules.xyz/jsonpatch/v2 v2.2.0 h1:4pT439QV83L+G9FkcCriY6EkpcK6r6bK+A5FBUMI7qY=
 gomodules.xyz/jsonpatch/v2 v2.2.0/go.mod h1:WXp+iVDkoLQqPudfQ9GBlwB2eZ5DKOnjQZCYdOS8GPY=
@@ -1165,9 +1165,8 @@ google.golang.org/protobuf v1.24.0/go.mod h1:r/3tXBNzIEhYS9I1OUVjXDlt8tc493IdKGj
 google.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlbajtzgsN7c=
 google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw=
 google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc=
+google.golang.org/protobuf v1.27.1 h1:SnqbnDw1V7RiZcXPx5MEeqPv2s79L9i7BJUlG/+RurQ=
 google.golang.org/protobuf v1.27.1/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc=
-google.golang.org/protobuf v1.28.0 h1:w43yiav+6bVFTBQFZX0r7ipe9JQ1QsbMgHwbBziscLw=
-google.golang.org/protobuf v1.28.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I=
 gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw=
 gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
 gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
diff --git a/pkg/constant/k8s.go b/pkg/constant/k8s.go
index 4c3c50fc3..15cea6270 100644
--- a/pkg/constant/k8s.go
+++ b/pkg/constant/k8s.go
@@ -58,9 +58,9 @@ const (
 	AnnoNSDefautlV6Pool = AnnotationPre + "/defaultv6ippool"
 )
 
+const SpiderWorkloadEndpointFinalizer = "spiderpool.spidernet.io"
+
 const QualifiedK8sObjNameFmt = "[a-z0-9]([-a-z0-9]*[a-z0-9])?(\\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*"
 
-const SpiderFinalizer = "spiderpool.spidernet.io"
-const SpiderpoolAgent = "spiderpool-agent"
-const SpiderpoolController = "spiderpool-controller"
-const SpiderControllerElectorLockName = SpiderpoolController + "-" + resourcelock.LeasesResourceLock
+const SpiderIPGarbageCollect = "ip-gc"
+const SpiderIPGarbageCollectElectorLockName = AnnotationPre + "-" + SpiderIPGarbageCollect + "-" + resourcelock.LeasesResourceLock
diff --git a/pkg/gcmanager/gc_manager.go b/pkg/gcmanager/gc_manager.go
index e15d636a7..fa06fb034 100644
--- a/pkg/gcmanager/gc_manager.go
+++ b/pkg/gcmanager/gc_manager.go
@@ -12,6 +12,7 @@ import (
 	ctrl "sigs.k8s.io/controller-runtime"
 	"sigs.k8s.io/controller-runtime/pkg/client"
 
+	"github.com/spidernet-io/spiderpool/pkg/constant"
 	"github.com/spidernet-io/spiderpool/pkg/election"
 	"github.com/spidernet-io/spiderpool/pkg/ippoolmanager"
 	"github.com/spidernet-io/spiderpool/pkg/logutils"
@@ -20,6 +21,9 @@ import (
 )
 
 type GarbageCollectionConfig struct {
+	ControllerPodName      string
+	ControllerPodNamespace string
+
 	EnableGCIP                bool
 	EnableGCForTerminatingPod bool
 
@@ -32,6 +36,11 @@ type GarbageCollectionConfig struct {
 	GCSignalTimeoutDuration   int
 	GCSignalGapDuration       int
 	AdditionalGraceDelay      int
+
+	LeaseDuration      int
+	LeaseRenewDeadline int
+	LeaseRetryPeriod   int
+	LeaseRetryGap      int
 }
 
 var logger *zap.Logger
@@ -71,8 +80,7 @@ func NewGCManager(ctx context.Context, client client.Client, config *GarbageColl
 	crdMgr ctrl.Manager,
 	wepManager workloadendpointmanager.WorkloadEndpointManager,
 	ippoolManager ippoolmanager.IPPoolManager,
-	podManager podmanager.PodManager,
-	spiderControllerLeader election.SpiderLeaseElector) (GCManager, error) {
+	podManager podmanager.PodManager) (GCManager, error) {
 	if config == nil {
 		return nil, fmt.Errorf("gc configuration must be specified")
 	}
@@ -97,8 +105,14 @@ func NewGCManager(ctx context.Context, client client.Client, config *GarbageColl
 		return nil, fmt.Errorf("pod manager must be specified")
 	}
 
-	if spiderControllerLeader == nil {
-		return nil, fmt.Errorf("spiderpool controller leader must be specified")
+	leaseDuration := time.Duration(config.LeaseDuration) * time.Second
+	renewDeadline := time.Duration(config.LeaseRenewDeadline) * time.Second
+	leaseRetryPeriod := time.Duration(config.LeaseRetryPeriod) * time.Second
+	leaderRetryElectGap := time.Duration(config.LeaseRetryGap) * time.Second
+	leaderElector, err := election.NewLeaseElector(ctx, config.ControllerPodNamespace, constant.SpiderIPGarbageCollectElectorLockName, config.ControllerPodName,
+		&leaseDuration, &renewDeadline, &leaseRetryPeriod, &leaderRetryElectGap)
+	if nil != err {
+		return nil, fmt.Errorf("failed to new leader elector, error: %w", err)
 	}
 
 	logger = logutils.Logger.Named("IP-GarbageCollection")
@@ -116,7 +130,7 @@ func NewGCManager(ctx context.Context, client client.Client, config *GarbageColl
 		ippoolMgr:     ippoolManager,
 		podMgr:        podManager,
 
-		leader: spiderControllerLeader,
+		leader: leaderElector,
 	}
 
 	return spiderGC, nil
diff --git a/pkg/gcmanager/tracePod_worker.go b/pkg/gcmanager/tracePod_worker.go
index 05d7f0503..cb998d53d 100644
--- a/pkg/gcmanager/tracePod_worker.go
+++ b/pkg/gcmanager/tracePod_worker.go
@@ -107,8 +107,8 @@ func (s *SpiderGC) releaseIPPoolIPExecutor(ctx context.Context, workerIndex int)
 					gcIPPoolIPDetail.PodNamespace, gcIPPoolIPDetail.PodName, err)
 			}
 
-			loggerReleaseIP.Sugar().Debugf("remove wep '%v/%v' finalizer '%s' successfully",
-				gcIPPoolIPDetail.PodNamespace, gcIPPoolIPDetail.PodName, constant.SpiderFinalizer)
+			loggerReleaseIP.Sugar().Debugf("remove '%v/%v' finalizer '%s' successfully",
+				gcIPPoolIPDetail.PodNamespace, gcIPPoolIPDetail.PodName, constant.SpiderWorkloadEndpointFinalizer)
 
 		case <-ctx.Done():
 			loggerReleaseIP.Info("receive ctx done, stop running releaseIPPoolIPExecutor")
diff --git a/pkg/ip/ips.go b/pkg/ip/ips.go
index 748a286b4..ad2524885 100644
--- a/pkg/ip/ips.go
+++ b/pkg/ip/ips.go
@@ -4,33 +4,28 @@
 package ip
 
 import (
-	"fmt"
 	"math/big"
 	"net"
 	"strings"
-
-	"github.com/asaskevich/govalidator"
-
-	"github.com/spidernet-io/spiderpool/pkg/constant"
 )
 
-func ParseIPRanges(ipRanges []string) ([]net.IP, error) {
-	var sum []net.IP
-	for _, r := range ipRanges {
-		_, err := ValidateIPRange(r)
-		if nil != err {
-			return nil, err
-		}
+type IPs []net.IP
 
+func ParseIPRanges(ipRanges []string) IPs {
+	var sum IPs
+	for _, r := range ipRanges {
 		ips := parseIPRange(r)
+		if ips == nil {
+			return nil
+		}
 		sum = append(sum, ips...)
 	}
 
-	return sum, nil
+	return sum
 }
 
-func parseIPRange(ipRange string) []net.IP {
-	var ips []net.IP
+func parseIPRange(ipRange string) IPs {
+	var ips IPs
 	arr := strings.Split(ipRange, "-")
 	n := len(arr)
 
@@ -55,8 +50,8 @@ func parseIPRange(ipRange string) []net.IP {
 	return ips
 }
 
-func IPsDiffSet(a, b []net.IP) []net.IP {
-	var ips []net.IP
+func IPsDiffSet(a, b IPs) IPs {
+	var ips IPs
 	marks := make(map[string]bool)
 	for _, ip := range a {
 		if ip != nil {
@@ -103,10 +98,6 @@ func PrevIP(ip net.IP) net.IP {
 	return intToIP(i.Sub(i, big.NewInt(1)))
 }
 
-// Cmp compares two IPs, returning the usual ordering:
-// a < b : -1
-// a == b : 0
-// a > b : 1
 func Cmp(a, b net.IP) int {
 	aa := ipToInt(a)
 	bb := ipToInt(b)
@@ -123,54 +114,3 @@ func ipToInt(ip net.IP) *big.Int {
 func intToIP(i *big.Int) net.IP {
 	return net.IP(i.Bytes())
 }
-
-// ValidateIPRange check the format for the given ip range.
-// it can be a single one just like '192.168.1.0',
-// and it also could be an IP range just like '192.168.1.0-192.168.1.10'.
-// Notice: the following formats are invalid
-// 1. '192.168.1.0 - 192.168.1.10', there can not be space between two IP.
-// 2. '192.168.1.1-2001:db8:a0b:12f0::1', the combination with one IPv4 and IPv6 is invalid.
-// 3. '192.168.1.10-192.168.1.1', the IP range must be ordered.
-func ValidateIPRange(ipRange string) (ipVersion int, err error) {
-	split := strings.Split(ipRange, "-")
-	length := len(split)
-
-	// single IP
-	if length == 1 {
-		if govalidator.IsIPv4(split[0]) {
-			return int(constant.IPv4), nil
-		}
-
-		if govalidator.IsIPv6(split[0]) {
-			return int(constant.IPv6), nil
-		}
-
-		return 0, fmt.Errorf("failed to parse IP range '%s' , it's not a regular IP address", split)
-	} else if length == 2 {
-		// IP range
-		if govalidator.IsIPv4(split[0]) && govalidator.IsIPv4(split[1]) {
-			// the previous IP can't greater than the latter one
-			if Cmp(net.ParseIP(split[0]), net.ParseIP(split[1])) == 1 {
-				return 0, fmt.Errorf("IP range '%s' is not regular", ipRange)
-			}
-
-			return int(constant.IPv4), nil
-		}
-
-		if govalidator.IsIPv6(split[0]) && govalidator.IsIPv6(split[1]) {
-			// the previous IP can't greater than the latter one
-			if Cmp(net.ParseIP(split[0]), net.ParseIP(split[1])) == 1 {
-				return 0, fmt.Errorf("IP range '%s' is not regular", ipRange)
-			}
-
-			return int(constant.IPv6), nil
-		}
-
-		err = fmt.Errorf("IP range '%s' is not regular", ipRange)
-	} else {
-		// not a regular IP format
-		err = fmt.Errorf("IP range '%s' is not regular", ipRange)
-	}
-
-	return
-}
diff --git a/pkg/ipam/tools.go b/pkg/ipam/tools.go
index c1e4a1301..482f3d4d6 100644
--- a/pkg/ipam/tools.go
+++ b/pkg/ipam/tools.go
@@ -17,9 +17,8 @@ import (
 func convertToIPConfigs(details []spiderpoolv1.IPAllocationDetail) []*models.IPConfig {
 	var ipConfigs []*models.IPConfig
 	for _, d := range details {
-		var version int64
 		if d.IPv4 != nil {
-			version = constant.IPv4
+			version := constant.IPv4
 			ipConfigs = append(ipConfigs, &models.IPConfig{
 				Address: d.IPv4,
 				Nic:     &d.NIC,
@@ -30,7 +29,7 @@ func convertToIPConfigs(details []spiderpoolv1.IPAllocationDetail) []*models.IPC
 		}
 
 		if d.IPv6 != nil {
-			version = constant.IPv6
+			version := constant.IPv6
 			ipConfigs = append(ipConfigs, &models.IPConfig{
 				Address: d.IPv6,
 				Nic:     &d.NIC,
diff --git a/pkg/ippoolmanager/ippool_manager.go b/pkg/ippoolmanager/ippool_manager.go
index 9a50df354..326537652 100644
--- a/pkg/ippoolmanager/ippool_manager.go
+++ b/pkg/ippoolmanager/ippool_manager.go
@@ -11,20 +11,15 @@ import (
 	"net"
 	"time"
 
+	spiderpoolv1 "github.com/spidernet-io/spiderpool/pkg/k8s/apis/v1"
 	corev1 "k8s.io/api/core/v1"
 	apierrors "k8s.io/apimachinery/pkg/api/errors"
 	apitypes "k8s.io/apimachinery/pkg/types"
-	"k8s.io/utils/strings/slices"
-	ctrl "sigs.k8s.io/controller-runtime"
 	"sigs.k8s.io/controller-runtime/pkg/client"
-	"sigs.k8s.io/controller-runtime/pkg/controller/controllerutil"
 
 	"github.com/spidernet-io/spiderpool/api/v1/agent/models"
 	"github.com/spidernet-io/spiderpool/pkg/constant"
-	"github.com/spidernet-io/spiderpool/pkg/election"
 	"github.com/spidernet-io/spiderpool/pkg/ip"
-	spiderpoolip "github.com/spidernet-io/spiderpool/pkg/ip"
-	spiderpoolv1 "github.com/spidernet-io/spiderpool/pkg/k8s/apis/v1"
 	"github.com/spidernet-io/spiderpool/pkg/logutils"
 	"github.com/spidernet-io/spiderpool/pkg/namespacemanager"
 	"github.com/spidernet-io/spiderpool/pkg/nodemanager"
@@ -32,8 +27,6 @@ import (
 	"github.com/spidernet-io/spiderpool/pkg/reservedipmanager"
 )
 
-var logger = logutils.Logger.Named("IPPool_Manager")
-
 type IPPoolManager interface {
 	AllocateIP(ctx context.Context, poolName, containerID, nic string, pod *corev1.Pod) (*models.IPConfig, error)
 	ReleaseIP(ctx context.Context, poolName string, ipAndCIDs []IPAndCID) error
@@ -41,11 +34,6 @@ type IPPoolManager interface {
 	SelectByPod(ctx context.Context, version spiderpoolv1.IPVersion, poolName string, pod *corev1.Pod) (bool, error)
 	CheckVlanSame(ctx context.Context, poolList []string) (map[spiderpoolv1.Vlan][]string, bool, error)
 	CheckPoolCIDROverlap(ctx context.Context, poolList1 []string, poolList2 []string) (bool, error)
-	GetIPPoolByName(ctx context.Context, podName string) (*spiderpoolv1.IPPool, error)
-	RemoveFinalizer(ctx context.Context, poolName string) error
-	AssembleTotalIP(ctx context.Context, poolName string) ([]net.IP, error)
-	SetupReconcile(mgr ctrl.Manager, leader election.SpiderLeaseElector) error
-	SetupWebhook(mgr ctrl.Manager) error
 }
 
 type ipPoolManager struct {
@@ -57,8 +45,6 @@ type ipPoolManager struct {
 	maxAllocatedIPs       int
 	maxConflictRetrys     int
 	conflictRetryUnitTime time.Duration
-
-	leader election.SpiderLeaseElector
 }
 
 func NewIPPoolManager(c client.Client, rIPManager reservedipmanager.ReservedIPManager, nodeManager nodemanager.NodeManager, nsManager namespacemanager.NamespaceManager, podManager podmanager.PodManager, maxAllocatedIPs, maxConflictRetrys int, conflictRetryUnitTime time.Duration) (IPPoolManager, error) {
@@ -163,26 +149,10 @@ func (r *ipPoolManager) AllocateIP(ctx context.Context, poolName, containerID, n
 
 func randomIP(all []string, used []string, exclude []string, reserved []string) (net.IP, error) {
 	// TODO(iiiceoo): Check nil
-	reservedIPs, err := ip.ParseIPRanges(reserved)
-	if nil != err {
-		return nil, err
-	}
-
-	usedIPs, err := ip.ParseIPRanges(used)
-	if nil != err {
-		return nil, err
-	}
-
-	expectIPs, err := ip.ParseIPRanges(all)
-	if nil != err {
-		return nil, err
-	}
-
-	excludeIPs, err := ip.ParseIPRanges(exclude)
-	if nil != err {
-		return nil, err
-	}
-
+	reservedIPs := ip.ParseIPRanges(reserved)
+	usedIPs := ip.ParseIPRanges(used)
+	expectIPs := ip.ParseIPRanges(all)
+	excludeIPs := ip.ParseIPRanges(exclude)
 	availableIPs := ip.IPsDiffSet(expectIPs, append(reservedIPs, append(usedIPs, excludeIPs...)...))
 
 	if len(availableIPs) == 0 {
@@ -363,60 +333,3 @@ func (r *ipPoolManager) CheckVlanSame(ctx context.Context, poolList []string) (m
 func (r *ipPoolManager) CheckPoolCIDROverlap(ctx context.Context, poolList1 []string, poolList2 []string) (bool, error) {
 	return false, nil
 }
-
-func (r *ipPoolManager) GetIPPoolByName(ctx context.Context, poolName string) (*spiderpoolv1.IPPool, error) {
-	var ipPool spiderpoolv1.IPPool
-	err := r.client.Get(ctx, apitypes.NamespacedName{Name: poolName}, &ipPool)
-	if nil != err {
-		return nil, err
-	}
-
-	return &ipPool, nil
-}
-
-func (r *ipPoolManager) RemoveFinalizer(ctx context.Context, poolName string) error {
-	ipPool, err := r.GetIPPoolByName(ctx, poolName)
-	if nil != err {
-		if apierrors.IsNotFound(err) {
-			logger.Sugar().Debugf("IPPool '%s' not found", poolName)
-			return nil
-		}
-
-		return err
-	}
-
-	if slices.Contains(ipPool.Finalizers, constant.SpiderFinalizer) {
-		controllerutil.RemoveFinalizer(ipPool, constant.SpiderFinalizer)
-
-		err = r.client.Update(ctx, ipPool)
-		if nil != err {
-			return err
-		}
-	}
-
-	return nil
-}
-
-// AssembleTotalIP will calculate an IPPool CR object usable IPs number, it summaries the IPPool IPs then subtracts ExcludeIPs
-// notice: this method would not filter ReservedIP CR object data!
-func (r *ipPoolManager) AssembleTotalIP(ctx context.Context, poolName string) ([]net.IP, error) {
-	ipPool, err := r.GetIPPoolByName(ctx, poolName)
-	if nil != err {
-		return nil, err
-	}
-
-	// TODO (Icarus9913): ips could be nil, should we return error?
-	ips, err := spiderpoolip.ParseIPRanges(ipPool.Spec.IPs)
-	if nil != err {
-		return nil, err
-	}
-
-	excludeIPs, err := spiderpoolip.ParseIPRanges(ipPool.Spec.ExcludeIPs)
-	if nil != err {
-		return nil, err
-	}
-
-	usableIPs := spiderpoolip.IPsDiffSet(ips, excludeIPs)
-
-	return usableIPs, nil
-}
diff --git a/pkg/ippoolmanager/ippool_reconcile.go b/pkg/ippoolmanager/ippool_reconcile.go
deleted file mode 100644
index 14975cb5f..000000000
--- a/pkg/ippoolmanager/ippool_reconcile.go
+++ /dev/null
@@ -1,110 +0,0 @@
-// Copyright 2022 Authors of spidernet-io
-// SPDX-License-Identifier: Apache-2.0
-
-package ippoolmanager
-
-import (
-	"context"
-	"fmt"
-
-	apierrors "k8s.io/apimachinery/pkg/api/errors"
-	ctrl "sigs.k8s.io/controller-runtime"
-	"sigs.k8s.io/controller-runtime/pkg/reconcile"
-
-	"github.com/spidernet-io/spiderpool/pkg/constant"
-	"github.com/spidernet-io/spiderpool/pkg/election"
-	spiderpoolv1 "github.com/spidernet-io/spiderpool/pkg/k8s/apis/v1"
-)
-
-// SetupReconcile registers IPPool CR object informer
-func (im *ipPoolManager) SetupReconcile(mgr ctrl.Manager, leader election.SpiderLeaseElector) error {
-	if mgr == nil {
-		return fmt.Errorf("failed to set up IPPool reconcile, mgr must be specified")
-	}
-
-	if leader == nil {
-		return fmt.Errorf("failed to set up IPPool reconcile, leader must be specified")
-	}
-
-	im.leader = leader
-
-	return ctrl.NewControllerManagedBy(mgr).
-		For(&spiderpoolv1.IPPool{}).
-		Complete(im)
-}
-
-// Reconcile will watch every event that related with IPPool CR object
-func (im *ipPoolManager) Reconcile(ctx context.Context, request reconcile.Request) (reconcile.Result, error) {
-	// backup controller could be elected as master
-	if !im.leader.IsElected() {
-		return reconcile.Result{}, nil
-	}
-
-	ipPool, err := im.GetIPPoolByName(ctx, request.Name)
-	if nil != err {
-		if apierrors.IsNotFound(err) {
-			logger.Sugar().Debugf("reconcile found deleted IPPool '%+v'", request.NamespacedName)
-		} else {
-			//TODO (Icarus9913): we will meet error here, should we return it and requeue ?
-			logger.Sugar().Errorf("failed to fetch IPPool '%+v', error: %v", request.NamespacedName, err)
-		}
-
-		return reconcile.Result{}, nil
-	}
-
-	// calculate IPPool subresource status TotalIPCount when the CR object isn't deleting
-	// TODO (Icarus9913): maybe we could abstract here to an API in the future?
-	if ipPool.DeletionTimestamp == nil {
-		// get IPPool usable IPs number
-		totalIPs, err := im.AssembleTotalIP(ctx, request.Name)
-		if nil != err {
-			//TODO (Icarus9913): we will meet error here, should we return it and requeue ?
-			logger.Sugar().Errorf("failed to calculate IPPool '%s/%s' total IP count, error: %v", request.Namespace, request.Name, err)
-			return reconcile.Result{}, nil
-		}
-
-		totalIPCount := int32(len(totalIPs))
-
-		// check the IPPool CR object subresource status TotalIPCount should be changed or not
-		isChange := false
-		if ipPool.Status.TotalIPCount != nil {
-			if *ipPool.Status.TotalIPCount != totalIPCount {
-				logger.Sugar().Infof("IPPool '%s/%s' total IP count changed from '%d' to '%d'",
-					ipPool.Namespace, ipPool.Name, *ipPool.Status.TotalIPCount, totalIPCount)
-				isChange = true
-			}
-		} else {
-			logger.Sugar().Infof("IPPool '%s/%s' status total IP count changed from nil to '%d'",
-				ipPool.Namespace, ipPool.Name, totalIPCount)
-			isChange = true
-		}
-
-		// check the IPPool CR object subresource status TotalIPCount and return,
-		// because once subresource updated successfully the CR object resource version changed,
-		// then we will meet conflict if we update other data.
-		if isChange {
-			ipPool.Status.TotalIPCount = &totalIPCount
-			err = im.client.Status().Update(ctx, ipPool)
-			if nil != err {
-				logger.Sugar().Errorf("failed to update IPPool '%s/%s' status IP count to '%d', error: %v",
-					ipPool.Namespace, ipPool.Name, totalIPCount, err)
-				//TODO (Icarus9913): we will meet error here, should we return it and requeue ?
-				return reconcile.Result{}, nil
-			}
-			return reconcile.Result{}, nil
-		}
-	} else {
-		// remove finalizer for IPPool CR object when it's dying and no more IPs are used
-		if len(ipPool.Status.AllocatedIPs) == 0 {
-			err = im.RemoveFinalizer(ctx, request.Name)
-			if nil != err {
-				//TODO (Icarus9913): we will meet error here, should we return it and requeue ?
-				logger.Sugar().Errorf("failed to remove IPPool '%+v' finalizer '%s', error: %v", request.NamespacedName, constant.SpiderFinalizer, err)
-				return reconcile.Result{}, nil
-			}
-			logger.Sugar().Infof("remove IPPool '%+v' finalizer '%s' successfully", request.NamespacedName, constant.SpiderFinalizer)
-		}
-	}
-
-	return reconcile.Result{}, nil
-}
diff --git a/pkg/ippoolmanager/ippool_webhook.go b/pkg/ippoolmanager/ippool_webhook.go
deleted file mode 100644
index 339599de4..000000000
--- a/pkg/ippoolmanager/ippool_webhook.go
+++ /dev/null
@@ -1,83 +0,0 @@
-// Copyright 2022 Authors of spidernet-io
-// SPDX-License-Identifier: Apache-2.0
-
-package ippoolmanager
-
-import (
-	"context"
-	"fmt"
-	"strings"
-
-	"github.com/asaskevich/govalidator"
-	"k8s.io/apimachinery/pkg/runtime"
-	"k8s.io/utils/strings/slices"
-	ctrl "sigs.k8s.io/controller-runtime"
-	"sigs.k8s.io/controller-runtime/pkg/webhook"
-
-	"github.com/spidernet-io/spiderpool/pkg/constant"
-	spiderpoolv1 "github.com/spidernet-io/spiderpool/pkg/k8s/apis/v1"
-)
-
-var _ webhook.CustomDefaulter = (*ipPoolManager)(nil)
-
-func (im *ipPoolManager) SetupWebhook(mgr ctrl.Manager) error {
-	if mgr == nil {
-		return fmt.Errorf("failed to set up IPPool webhook, mgr must be specified")
-	}
-
-	return ctrl.NewWebhookManagedBy(mgr).
-		For(&spiderpoolv1.IPPool{}).
-		WithDefaulter(im).
-		WithValidator(im).
-		Complete()
-}
-
-func (im *ipPoolManager) Default(ctx context.Context, obj runtime.Object) error {
-	ipPool, ok := obj.(*spiderpoolv1.IPPool)
-	if !ok {
-		return fmt.Errorf("webhook mutating got mismatch IPPool type: %+v", obj.GetObjectKind().GroupVersionKind())
-	}
-
-	// do not mutate when it's a deleting object
-	if ipPool.DeletionTimestamp != nil {
-		return nil
-	}
-
-	if ipPool.Spec.IPVersion == nil {
-		ipPool.Spec.IPVersion = new(spiderpoolv1.IPVersion)
-		ip, _, _ := strings.Cut(ipPool.Spec.Subnet, "/")
-
-		if govalidator.IsIPv4(ip) {
-			*ipPool.Spec.IPVersion = spiderpoolv1.IPv4
-			logger.Sugar().Infof("IPPool CR object '%s/%s' set ipVersion '%s'", ipPool.Namespace, ipPool.Name, spiderpoolv1.IPv4)
-		} else if govalidator.IsIPv6(ip) {
-			*ipPool.Spec.IPVersion = spiderpoolv1.IPv6
-			logger.Sugar().Infof("IPPool CR object '%s/%s' set ipVersion '%s'", ipPool.Namespace, ipPool.Name, spiderpoolv1.IPv6)
-		}
-	}
-
-	// add finalizer for IPPool CR
-	if !slices.Contains(ipPool.Finalizers, constant.SpiderFinalizer) {
-		ipPool.Finalizers = append(ipPool.Finalizers, constant.SpiderFinalizer)
-		logger.Sugar().Infof("IPPool CR object '%s/%s' set finalizer '%s' successfully", ipPool.Namespace, ipPool.Name, constant.SpiderFinalizer)
-	}
-
-	return nil
-}
-
-var _ webhook.CustomValidator = (*ipPoolManager)(nil)
-
-// ValidateCreate implements webhook.CustomValidator so a webhook will be registered for the type
-func (im *ipPoolManager) ValidateCreate(ctx context.Context, obj runtime.Object) error {
-	return nil
-}
-
-// ValidateUpdate implements webhook.CustomValidator so a webhook will be registered for the type
-func (im *ipPoolManager) ValidateUpdate(ctx context.Context, oldObj, newObj runtime.Object) error {
-	return nil
-}
-
-// ValidateDelete implements webhook.CustomValidator so a webhook will be registered for the type
-func (im *ipPoolManager) ValidateDelete(ctx context.Context, obj runtime.Object) error {
-	return nil
-}
diff --git a/pkg/k8s/apis/v1/ippool_types.go b/pkg/k8s/apis/v1/ippool_types.go
index 36dc3150c..12864d9eb 100644
--- a/pkg/k8s/apis/v1/ippool_types.go
+++ b/pkg/k8s/apis/v1/ippool_types.go
@@ -15,6 +15,7 @@ type IPPoolSpec struct {
 	// - "IPv4":
 	// - "IPv6":
 
+	// +kubebuilder:default=IPv4
 	// +kubebuilder:validation:Optional
 	IPVersion *IPVersion `json:"ipVersion,omitempty"`
 
diff --git a/pkg/webhook/ippool_webhook.go b/pkg/webhook/ippool_webhook.go
new file mode 100644
index 000000000..a046ca025
--- /dev/null
+++ b/pkg/webhook/ippool_webhook.go
@@ -0,0 +1,52 @@
+// Copyright 2022 Authors of spidernet-io
+// SPDX-License-Identifier: Apache-2.0
+
+package webhook
+
+import (
+	"context"
+
+	"k8s.io/apimachinery/pkg/runtime"
+	ctrl "sigs.k8s.io/controller-runtime"
+	"sigs.k8s.io/controller-runtime/pkg/client"
+	"sigs.k8s.io/controller-runtime/pkg/webhook"
+
+	spiderpoolv1 "github.com/spidernet-io/spiderpool/pkg/k8s/apis/v1"
+)
+
+type IPPoolWebhook struct {
+	client.Client
+	Scheme *runtime.Scheme
+}
+
+func (r *IPPoolWebhook) SetupWebhookWithManager(mgr ctrl.Manager) error {
+	return ctrl.NewWebhookManagedBy(mgr).
+		For(&spiderpoolv1.IPPool{}).
+		WithDefaulter(r).
+		WithValidator(r).
+		Complete()
+}
+
+var _ webhook.CustomDefaulter = (*IPPoolWebhook)(nil)
+
+// Default implements webhook.CustomDefaulter so a webhook will be registered for the type
+func (r *IPPoolWebhook) Default(ctx context.Context, obj runtime.Object) error {
+	return nil
+}
+
+var _ webhook.CustomValidator = (*IPPoolWebhook)(nil)
+
+// ValidateCreate implements webhook.CustomValidator so a webhook will be registered for the type
+func (r *IPPoolWebhook) ValidateCreate(ctx context.Context, obj runtime.Object) error {
+	return nil
+}
+
+// ValidateUpdate implements webhook.CustomValidator so a webhook will be registered for the type
+func (r *IPPoolWebhook) ValidateUpdate(ctx context.Context, oldObj, newObj runtime.Object) error {
+	return nil
+}
+
+// ValidateDelete implements webhook.CustomValidator so a webhook will be registered for the type
+func (r *IPPoolWebhook) ValidateDelete(ctx context.Context, obj runtime.Object) error {
+	return nil
+}
diff --git a/pkg/workloadendpointmanager/workloadendpoint_manager.go b/pkg/workloadendpointmanager/workloadendpoint_manager.go
index df899ef54..a47acfb5e 100644
--- a/pkg/workloadendpointmanager/workloadendpoint_manager.go
+++ b/pkg/workloadendpointmanager/workloadendpoint_manager.go
@@ -14,7 +14,6 @@ import (
 	apierrors "k8s.io/apimachinery/pkg/api/errors"
 	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
 	apitypes "k8s.io/apimachinery/pkg/types"
-	"k8s.io/utils/strings/slices"
 	ctrl "sigs.k8s.io/controller-runtime"
 	"sigs.k8s.io/controller-runtime/pkg/client"
 	"sigs.k8s.io/controller-runtime/pkg/controller/controllerutil"
@@ -121,7 +120,7 @@ func (r *workloadEndpointManager) MarkIPAllocation(ctx context.Context, node, na
 			},
 		}
 
-		controllerutil.AddFinalizer(newWE, constant.SpiderFinalizer)
+		controllerutil.AddFinalizer(newWE, constant.SpiderWorkloadEndpointFinalizer)
 		if err := controllerutil.SetOwnerReference(&pod, newWE, r.runtimeMgr.GetScheme()); err != nil {
 			return err
 		}
@@ -272,13 +271,11 @@ func (r *workloadEndpointManager) RemoveFinalizer(ctx context.Context, namespace
 	}
 
 	// remove wep finalizer
-	if slices.Contains(wep.Finalizers, constant.SpiderFinalizer) {
-		controllerutil.RemoveFinalizer(wep, constant.SpiderFinalizer)
+	controllerutil.RemoveFinalizer(wep, constant.SpiderWorkloadEndpointFinalizer)
 
-		err = r.client.Update(ctx, wep)
-		if nil != err {
-			return err
-		}
+	err = r.client.Update(ctx, wep)
+	if nil != err {
+		return err
 	}
 
 	return nil
diff --git a/vendor/github.com/google/go-cmp/cmp/compare.go b/vendor/github.com/google/go-cmp/cmp/compare.go
index fd2b3a42b..2a5446762 100644
--- a/vendor/github.com/google/go-cmp/cmp/compare.go
+++ b/vendor/github.com/google/go-cmp/cmp/compare.go
@@ -40,8 +40,6 @@ import (
 	"github.com/google/go-cmp/cmp/internal/value"
 )
 
-// TODO(≥go1.18): Use any instead of interface{}.
-
 // Equal reports whether x and y are equal by recursively applying the
 // following rules in the given order to x and y and all of their sub-values:
 //
diff --git a/vendor/github.com/google/go-cmp/cmp/report_compare.go b/vendor/github.com/google/go-cmp/cmp/report_compare.go
index 1ef65ac1d..104bb3053 100644
--- a/vendor/github.com/google/go-cmp/cmp/report_compare.go
+++ b/vendor/github.com/google/go-cmp/cmp/report_compare.go
@@ -116,10 +116,7 @@ func (opts formatOptions) FormatDiff(v *valueNode, ptrs *pointerReferences) (out
 	}
 
 	// For leaf nodes, format the value based on the reflect.Values alone.
-	// As a special case, treat equal []byte as a leaf nodes.
-	isBytes := v.Type.Kind() == reflect.Slice && v.Type.Elem() == reflect.TypeOf(byte(0))
-	isEqualBytes := isBytes && v.NumDiff+v.NumIgnored+v.NumTransformed == 0
-	if v.MaxDepth == 0 || isEqualBytes {
+	if v.MaxDepth == 0 {
 		switch opts.DiffMode {
 		case diffUnknown, diffIdentical:
 			// Format Equal.
diff --git a/vendor/github.com/google/go-cmp/cmp/report_reflect.go b/vendor/github.com/google/go-cmp/cmp/report_reflect.go
index 287b89358..76c04fdbd 100644
--- a/vendor/github.com/google/go-cmp/cmp/report_reflect.go
+++ b/vendor/github.com/google/go-cmp/cmp/report_reflect.go
@@ -211,7 +211,7 @@ func (opts formatOptions) FormatValue(v reflect.Value, parentKind reflect.Kind,
 			if len(b) > 0 && utf8.Valid(b) && len(bytes.TrimFunc(b, isPrintSpace)) == 0 {
 				out = opts.formatString("", string(b))
 				skipType = true
-				return opts.FormatType(t, out)
+				return opts.WithTypeMode(emitType).FormatType(t, out)
 			}
 		}
 
@@ -282,12 +282,7 @@ func (opts formatOptions) FormatValue(v reflect.Value, parentKind reflect.Kind,
 		}
 		defer ptrs.Pop()
 
-		// Skip the name only if this is an unnamed pointer type.
-		// Otherwise taking the address of a value does not reproduce
-		// the named pointer type.
-		if v.Type().Name() == "" {
-			skipType = true // Let the underlying value print the type instead
-		}
+		skipType = true // Let the underlying value print the type instead
 		out = opts.FormatValue(v.Elem(), t.Kind(), ptrs)
 		out = wrapTrunkReference(ptrRef, opts.PrintAddresses, out)
 		out = &textWrap{Prefix: "&", Value: out}
@@ -298,6 +293,7 @@ func (opts formatOptions) FormatValue(v reflect.Value, parentKind reflect.Kind,
 		}
 		// Interfaces accept different concrete types,
 		// so configure the underlying value to explicitly print the type.
+		skipType = true // Print the concrete type instead
 		return opts.WithTypeMode(emitType).FormatValue(v.Elem(), t.Kind(), ptrs)
 	default:
 		panic(fmt.Sprintf("%v kind not handled", v.Kind()))
diff --git a/vendor/github.com/onsi/gomega/CHANGELOG.md b/vendor/github.com/onsi/gomega/CHANGELOG.md
index 75af957f2..4375bbc64 100644
--- a/vendor/github.com/onsi/gomega/CHANGELOG.md
+++ b/vendor/github.com/onsi/gomega/CHANGELOG.md
@@ -1,26 +1,3 @@
-## 1.20.0
-
-## Features
-- New [`gleak`](https://onsi.github.io/gomega/#codegleakcode-finding-leaked-goroutines) experimental goroutine leak detection package! (#538) [85ba7bc]
-- New `BeComparableTo` matcher(#546) that uses `gocmp` to make comparisons [e77ea75]
-- New `HaveExistingField` matcher (#553) [fd130e1]
-- Document how to wrap Gomega (#539) [56714a4]
-
-## Fixes
-- Support pointer receivers in HaveField; fixes #543 (#544) [8dab36e]
-
-## Maintenance
-- Bump various dependencies:
-    - Upgrade to yaml.v3 (#556) [f5a83b1]
-    - Bump github/codeql-action from 1 to 2 (#549) [52f5adf]
-    - Bump github.com/google/go-cmp from 0.5.7 to 0.5.8 (#551) [5f3942d]
-    - Bump nokogiri from 1.13.4 to 1.13.6 in /docs (#554) [eb4b4c2]
-    - Use latest ginkgo (#535) [1c29028]
-    - Bump nokogiri from 1.13.3 to 1.13.4 in /docs (#541) [1ce84d5]
-    - Bump actions/setup-go from 2 to 3 (#540) [755485e]
-    - Bump nokogiri from 1.12.5 to 1.13.3 in /docs (#522) [4fbb0dc]
-    - Bump actions/checkout from 2 to 3 (#526) [ac49202]
-
 ## 1.19.0
 
 ## Features
diff --git a/vendor/github.com/onsi/gomega/gomega_dsl.go b/vendor/github.com/onsi/gomega/gomega_dsl.go
index 9b67f5190..dcb7e8879 100644
--- a/vendor/github.com/onsi/gomega/gomega_dsl.go
+++ b/vendor/github.com/onsi/gomega/gomega_dsl.go
@@ -22,7 +22,7 @@ import (
 	"github.com/onsi/gomega/types"
 )
 
-const GOMEGA_VERSION = "1.20.0"
+const GOMEGA_VERSION = "1.19.0"
 
 const nilGomegaPanic = `You are trying to make an assertion, but haven't registered Gomega's fail handler.
 If you're using Ginkgo then you probably forgot to put your assertion in an It().
diff --git a/vendor/github.com/onsi/gomega/matchers.go b/vendor/github.com/onsi/gomega/matchers.go
index d6a099060..b58dd67cb 100644
--- a/vendor/github.com/onsi/gomega/matchers.go
+++ b/vendor/github.com/onsi/gomega/matchers.go
@@ -3,7 +3,6 @@ package gomega
 import (
 	"time"
 
-	"github.com/google/go-cmp/cmp"
 	"github.com/onsi/gomega/matchers"
 	"github.com/onsi/gomega/types"
 )
@@ -27,15 +26,6 @@ func BeEquivalentTo(expected interface{}) types.GomegaMatcher {
 	}
 }
 
-//BeComparableTo uses gocmp.Equal to compare. You can pass cmp.Option as options.
-//It is an error for actual and expected to be nil.  Use BeNil() instead.
-func BeComparableTo(expected interface{}, opts ...cmp.Option) types.GomegaMatcher {
-	return &matchers.BeComparableToMatcher{
-		Expected: expected,
-		Options:  opts,
-	}
-}
-
 //BeIdenticalTo uses the == operator to compare actual with expected.
 //BeIdenticalTo is strict about types when performing comparisons.
 //It is an error for both actual and expected to be nil.  Use BeNil() instead.
@@ -404,19 +394,6 @@ func HaveField(field string, expected interface{}) types.GomegaMatcher {
 	}
 }
 
-// HaveExistingField succeeds if actual is a struct and the specified field
-// exists.
-//
-// HaveExistingField can be combined with HaveField in order to cover use cases
-// with optional fields. HaveField alone would trigger an error in such situations.
-//
-//     Expect(MrHarmless).NotTo(And(HaveExistingField("Title"), HaveField("Title", "Supervillain")))
-func HaveExistingField(field string) types.GomegaMatcher {
-	return &matchers.HaveExistingFieldMatcher{
-		Field: field,
-	}
-}
-
 // HaveValue applies the given matcher to the value of actual, optionally and
 // repeatedly dereferencing pointers or taking the concrete value of interfaces.
 // Thus, the matcher will always be applied to non-pointer and non-interface
diff --git a/vendor/github.com/onsi/gomega/matchers/be_comparable_to_matcher.go b/vendor/github.com/onsi/gomega/matchers/be_comparable_to_matcher.go
deleted file mode 100644
index 8ab4bb919..000000000
--- a/vendor/github.com/onsi/gomega/matchers/be_comparable_to_matcher.go
+++ /dev/null
@@ -1,49 +0,0 @@
-package matchers
-
-import (
-	"bytes"
-	"fmt"
-
-	"github.com/google/go-cmp/cmp"
-	"github.com/onsi/gomega/format"
-)
-
-type BeComparableToMatcher struct {
-	Expected interface{}
-	Options  cmp.Options
-}
-
-func (matcher *BeComparableToMatcher) Match(actual interface{}) (success bool, matchErr error) {
-	if actual == nil && matcher.Expected == nil {
-		return false, fmt.Errorf("Refusing to compare <nil> to <nil>.\nBe explicit and use BeNil() instead.  This is to avoid mistakes where both sides of an assertion are erroneously uninitialized.")
-	}
-	// Shortcut for byte slices.
-	// Comparing long byte slices with reflect.DeepEqual is very slow,
-	// so use bytes.Equal if actual and expected are both byte slices.
-	if actualByteSlice, ok := actual.([]byte); ok {
-		if expectedByteSlice, ok := matcher.Expected.([]byte); ok {
-			return bytes.Equal(actualByteSlice, expectedByteSlice), nil
-		}
-	}
-
-	defer func() {
-		if r := recover(); r != nil {
-			success = false
-			if err, ok := r.(error); ok {
-				matchErr = err
-			} else if errMsg, ok := r.(string); ok {
-				matchErr = fmt.Errorf(errMsg)
-			}
-		}
-	}()
-
-	return cmp.Equal(actual, matcher.Expected, matcher.Options...), nil
-}
-
-func (matcher *BeComparableToMatcher) FailureMessage(actual interface{}) (message string) {
-	return cmp.Diff(matcher.Expected, actual, matcher.Options)
-}
-
-func (matcher *BeComparableToMatcher) NegatedFailureMessage(actual interface{}) (message string) {
-	return format.Message(actual, "not to equal", matcher.Expected)
-}
diff --git a/vendor/github.com/onsi/gomega/matchers/have_existing_field_matcher.go b/vendor/github.com/onsi/gomega/matchers/have_existing_field_matcher.go
deleted file mode 100644
index b57018745..000000000
--- a/vendor/github.com/onsi/gomega/matchers/have_existing_field_matcher.go
+++ /dev/null
@@ -1,36 +0,0 @@
-package matchers
-
-import (
-	"errors"
-	"fmt"
-
-	"github.com/onsi/gomega/format"
-)
-
-type HaveExistingFieldMatcher struct {
-	Field string
-}
-
-func (matcher *HaveExistingFieldMatcher) Match(actual interface{}) (success bool, err error) {
-	// we don't care about the field's actual value, just about any error in
-	// trying to find the field (or method).
-	_, err = extractField(actual, matcher.Field, "HaveExistingField")
-	if err == nil {
-		return true, nil
-	}
-	var mferr missingFieldError
-	if errors.As(err, &mferr) {
-		// missing field errors aren't errors in this context, but instead
-		// unsuccessful matches.
-		return false, nil
-	}
-	return false, err
-}
-
-func (matcher *HaveExistingFieldMatcher) FailureMessage(actual interface{}) (message string) {
-	return fmt.Sprintf("Expected\n%s\nto have field '%s'", format.Object(actual, 1), matcher.Field)
-}
-
-func (matcher *HaveExistingFieldMatcher) NegatedFailureMessage(actual interface{}) (message string) {
-	return fmt.Sprintf("Expected\n%s\nnot to have field '%s'", format.Object(actual, 1), matcher.Field)
-}
diff --git a/vendor/github.com/onsi/gomega/matchers/have_field.go b/vendor/github.com/onsi/gomega/matchers/have_field.go
index 6989f78c4..e1fe934d5 100644
--- a/vendor/github.com/onsi/gomega/matchers/have_field.go
+++ b/vendor/github.com/onsi/gomega/matchers/have_field.go
@@ -8,16 +8,7 @@ import (
 	"github.com/onsi/gomega/format"
 )
 
-// missingFieldError represents a missing field extraction error that
-// HaveExistingFieldMatcher can ignore, as opposed to other, sever field
-// extraction errors, such as nil pointers, et cetera.
-type missingFieldError string
-
-func (e missingFieldError) Error() string {
-	return string(e)
-}
-
-func extractField(actual interface{}, field string, matchername string) (interface{}, error) {
+func extractField(actual interface{}, field string) (interface{}, error) {
 	fields := strings.SplitN(field, ".", 2)
 	actualValue := reflect.ValueOf(actual)
 
@@ -25,39 +16,36 @@ func extractField(actual interface{}, field string, matchername string) (interfa
 		actualValue = actualValue.Elem()
 	}
 	if actualValue == (reflect.Value{}) {
-		return nil, fmt.Errorf("%s encountered nil while dereferencing a pointer of type %T.", matchername, actual)
+		return nil, fmt.Errorf("HaveField encountered nil while dereferencing a pointer of type %T.", actual)
 	}
 
 	if actualValue.Kind() != reflect.Struct {
-		return nil, fmt.Errorf("%s encountered:\n%s\nWhich is not a struct.", matchername, format.Object(actual, 1))
+		return nil, fmt.Errorf("HaveField encountered:\n%s\nWhich is not a struct.", format.Object(actual, 1))
 	}
 
 	var extractedValue reflect.Value
 
 	if strings.HasSuffix(fields[0], "()") {
 		extractedValue = actualValue.MethodByName(strings.TrimSuffix(fields[0], "()"))
-		if extractedValue == (reflect.Value{}) && actualValue.CanAddr() {
-			extractedValue = actualValue.Addr().MethodByName(strings.TrimSuffix(fields[0], "()"))
-		}
 		if extractedValue == (reflect.Value{}) {
-			return nil, missingFieldError(fmt.Sprintf("%s could not find method named '%s' in struct of type %T.", matchername, fields[0], actual))
+			return nil, fmt.Errorf("HaveField could not find method named '%s' in struct of type %T.", fields[0], actual)
 		}
 		t := extractedValue.Type()
 		if t.NumIn() != 0 || t.NumOut() != 1 {
-			return nil, fmt.Errorf("%s found an invalid method named '%s' in struct of type %T.\nMethods must take no arguments and return exactly one value.", matchername, fields[0], actual)
+			return nil, fmt.Errorf("HaveField found an invalid method named '%s' in struct of type %T.\nMethods must take no arguments and return exactly one value.", fields[0], actual)
 		}
 		extractedValue = extractedValue.Call([]reflect.Value{})[0]
 	} else {
 		extractedValue = actualValue.FieldByName(fields[0])
 		if extractedValue == (reflect.Value{}) {
-			return nil, missingFieldError(fmt.Sprintf("%s could not find field named '%s' in struct:\n%s", matchername, fields[0], format.Object(actual, 1)))
+			return nil, fmt.Errorf("HaveField could not find field named '%s' in struct:\n%s", fields[0], format.Object(actual, 1))
 		}
 	}
 
 	if len(fields) == 1 {
 		return extractedValue.Interface(), nil
 	} else {
-		return extractField(extractedValue.Interface(), fields[1], matchername)
+		return extractField(extractedValue.Interface(), fields[1])
 	}
 }
 
@@ -70,7 +58,7 @@ type HaveFieldMatcher struct {
 }
 
 func (matcher *HaveFieldMatcher) Match(actual interface{}) (success bool, err error) {
-	matcher.extractedField, err = extractField(actual, matcher.Field, "HaveField")
+	matcher.extractedField, err = extractField(actual, matcher.Field)
 	if err != nil {
 		return false, err
 	}
diff --git a/vendor/github.com/onsi/gomega/matchers/match_yaml_matcher.go b/vendor/github.com/onsi/gomega/matchers/match_yaml_matcher.go
index 2cb6b47db..0c83c2b63 100644
--- a/vendor/github.com/onsi/gomega/matchers/match_yaml_matcher.go
+++ b/vendor/github.com/onsi/gomega/matchers/match_yaml_matcher.go
@@ -5,7 +5,7 @@ import (
 	"strings"
 
 	"github.com/onsi/gomega/format"
-	"gopkg.in/yaml.v3"
+	"gopkg.in/yaml.v2"
 )
 
 type MatchYAMLMatcher struct {
diff --git a/vendor/google.golang.org/protobuf/encoding/protowire/wire.go b/vendor/google.golang.org/protobuf/encoding/protowire/wire.go
index 9c61112f5..a427f8b70 100644
--- a/vendor/google.golang.org/protobuf/encoding/protowire/wire.go
+++ b/vendor/google.golang.org/protobuf/encoding/protowire/wire.go
@@ -21,11 +21,10 @@ import (
 type Number int32
 
 const (
-	MinValidNumber        Number = 1
-	FirstReservedNumber   Number = 19000
-	LastReservedNumber    Number = 19999
-	MaxValidNumber        Number = 1<<29 - 1
-	DefaultRecursionLimit        = 10000
+	MinValidNumber      Number = 1
+	FirstReservedNumber Number = 19000
+	LastReservedNumber  Number = 19999
+	MaxValidNumber      Number = 1<<29 - 1
 )
 
 // IsValid reports whether the field number is semantically valid.
@@ -56,7 +55,6 @@ const (
 	errCodeOverflow
 	errCodeReserved
 	errCodeEndGroup
-	errCodeRecursionDepth
 )
 
 var (
@@ -114,10 +112,6 @@ func ConsumeField(b []byte) (Number, Type, int) {
 // When parsing a group, the length includes the end group marker and
 // the end group is verified to match the starting field number.
 func ConsumeFieldValue(num Number, typ Type, b []byte) (n int) {
-	return consumeFieldValueD(num, typ, b, DefaultRecursionLimit)
-}
-
-func consumeFieldValueD(num Number, typ Type, b []byte, depth int) (n int) {
 	switch typ {
 	case VarintType:
 		_, n = ConsumeVarint(b)
@@ -132,9 +126,6 @@ func consumeFieldValueD(num Number, typ Type, b []byte, depth int) (n int) {
 		_, n = ConsumeBytes(b)
 		return n
 	case StartGroupType:
-		if depth < 0 {
-			return errCodeRecursionDepth
-		}
 		n0 := len(b)
 		for {
 			num2, typ2, n := ConsumeTag(b)
@@ -149,7 +140,7 @@ func consumeFieldValueD(num Number, typ Type, b []byte, depth int) (n int) {
 				return n0 - len(b)
 			}
 
-			n = consumeFieldValueD(num2, typ2, b, depth-1)
+			n = ConsumeFieldValue(num2, typ2, b)
 			if n < 0 {
 				return n // forward error code
 			}
diff --git a/vendor/google.golang.org/protobuf/internal/encoding/text/decode.go b/vendor/google.golang.org/protobuf/internal/encoding/text/decode.go
index 37803773f..eb10ea102 100644
--- a/vendor/google.golang.org/protobuf/internal/encoding/text/decode.go
+++ b/vendor/google.golang.org/protobuf/internal/encoding/text/decode.go
@@ -381,7 +381,7 @@ func (d *Decoder) currentOpenKind() (Kind, byte) {
 	case '[':
 		return ListOpen, ']'
 	}
-	panic(fmt.Sprintf("Decoder: openStack contains invalid byte %c", openCh))
+	panic(fmt.Sprintf("Decoder: openStack contains invalid byte %s", string(openCh)))
 }
 
 func (d *Decoder) pushOpenStack(ch byte) {
diff --git a/vendor/google.golang.org/protobuf/internal/errors/is_go112.go b/vendor/google.golang.org/protobuf/internal/errors/is_go112.go
index fbcd34920..f90e909b3 100644
--- a/vendor/google.golang.org/protobuf/internal/errors/is_go112.go
+++ b/vendor/google.golang.org/protobuf/internal/errors/is_go112.go
@@ -2,7 +2,6 @@
 // Use of this source code is governed by a BSD-style
 // license that can be found in the LICENSE file.
 
-//go:build !go1.13
 // +build !go1.13
 
 package errors
diff --git a/vendor/google.golang.org/protobuf/internal/errors/is_go113.go b/vendor/google.golang.org/protobuf/internal/errors/is_go113.go
index 5e72f1cde..dc05f4191 100644
--- a/vendor/google.golang.org/protobuf/internal/errors/is_go113.go
+++ b/vendor/google.golang.org/protobuf/internal/errors/is_go113.go
@@ -2,7 +2,6 @@
 // Use of this source code is governed by a BSD-style
 // license that can be found in the LICENSE file.
 
-//go:build go1.13
 // +build go1.13
 
 package errors
diff --git a/vendor/google.golang.org/protobuf/internal/flags/proto_legacy_disable.go b/vendor/google.golang.org/protobuf/internal/flags/proto_legacy_disable.go
index bda8e8cf3..a72995f02 100644
--- a/vendor/google.golang.org/protobuf/internal/flags/proto_legacy_disable.go
+++ b/vendor/google.golang.org/protobuf/internal/flags/proto_legacy_disable.go
@@ -2,7 +2,6 @@
 // Use of this source code is governed by a BSD-style
 // license that can be found in the LICENSE file.
 
-//go:build !protolegacy
 // +build !protolegacy
 
 package flags
diff --git a/vendor/google.golang.org/protobuf/internal/flags/proto_legacy_enable.go b/vendor/google.golang.org/protobuf/internal/flags/proto_legacy_enable.go
index 6d8d9bd6b..772e2f0e4 100644
--- a/vendor/google.golang.org/protobuf/internal/flags/proto_legacy_enable.go
+++ b/vendor/google.golang.org/protobuf/internal/flags/proto_legacy_enable.go
@@ -2,7 +2,6 @@
 // Use of this source code is governed by a BSD-style
 // license that can be found in the LICENSE file.
 
-//go:build protolegacy
 // +build protolegacy
 
 package flags
diff --git a/vendor/google.golang.org/protobuf/internal/impl/codec_map_go111.go b/vendor/google.golang.org/protobuf/internal/impl/codec_map_go111.go
index 4b15493f2..2706bb67f 100644
--- a/vendor/google.golang.org/protobuf/internal/impl/codec_map_go111.go
+++ b/vendor/google.golang.org/protobuf/internal/impl/codec_map_go111.go
@@ -2,7 +2,6 @@
 // Use of this source code is governed by a BSD-style
 // license that can be found in the LICENSE file.
 
-//go:build !go1.12
 // +build !go1.12
 
 package impl
diff --git a/vendor/google.golang.org/protobuf/internal/impl/codec_map_go112.go b/vendor/google.golang.org/protobuf/internal/impl/codec_map_go112.go
index 0b31b66ea..1533ef600 100644
--- a/vendor/google.golang.org/protobuf/internal/impl/codec_map_go112.go
+++ b/vendor/google.golang.org/protobuf/internal/impl/codec_map_go112.go
@@ -2,7 +2,6 @@
 // Use of this source code is governed by a BSD-style
 // license that can be found in the LICENSE file.
 
-//go:build go1.12
 // +build go1.12
 
 package impl
diff --git a/vendor/google.golang.org/protobuf/internal/impl/codec_reflect.go b/vendor/google.golang.org/protobuf/internal/impl/codec_reflect.go
index 145c577bd..90705e3ae 100644
--- a/vendor/google.golang.org/protobuf/internal/impl/codec_reflect.go
+++ b/vendor/google.golang.org/protobuf/internal/impl/codec_reflect.go
@@ -2,7 +2,6 @@
 // Use of this source code is governed by a BSD-style
 // license that can be found in the LICENSE file.
 
-//go:build purego || appengine
 // +build purego appengine
 
 package impl
diff --git a/vendor/google.golang.org/protobuf/internal/impl/codec_unsafe.go b/vendor/google.golang.org/protobuf/internal/impl/codec_unsafe.go
index 757642e23..e118af1e2 100644
--- a/vendor/google.golang.org/protobuf/internal/impl/codec_unsafe.go
+++ b/vendor/google.golang.org/protobuf/internal/impl/codec_unsafe.go
@@ -2,7 +2,6 @@
 // Use of this source code is governed by a BSD-style
 // license that can be found in the LICENSE file.
 
-//go:build !purego && !appengine
 // +build !purego,!appengine
 
 package impl
diff --git a/vendor/google.golang.org/protobuf/internal/impl/decode.go b/vendor/google.golang.org/protobuf/internal/impl/decode.go
index c65b0325c..949dc49a6 100644
--- a/vendor/google.golang.org/protobuf/internal/impl/decode.go
+++ b/vendor/google.golang.org/protobuf/internal/impl/decode.go
@@ -18,7 +18,6 @@ import (
 )
 
 var errDecode = errors.New("cannot parse invalid wire-format data")
-var errRecursionDepth = errors.New("exceeded maximum recursion depth")
 
 type unmarshalOptions struct {
 	flags    protoiface.UnmarshalInputFlags
@@ -26,7 +25,6 @@ type unmarshalOptions struct {
 		FindExtensionByName(field protoreflect.FullName) (protoreflect.ExtensionType, error)
 		FindExtensionByNumber(message protoreflect.FullName, field protoreflect.FieldNumber) (protoreflect.ExtensionType, error)
 	}
-	depth int
 }
 
 func (o unmarshalOptions) Options() proto.UnmarshalOptions {
@@ -46,7 +44,6 @@ func (o unmarshalOptions) IsDefault() bool {
 
 var lazyUnmarshalOptions = unmarshalOptions{
 	resolver: preg.GlobalTypes,
-	depth:    protowire.DefaultRecursionLimit,
 }
 
 type unmarshalOutput struct {
@@ -65,7 +62,6 @@ func (mi *MessageInfo) unmarshal(in piface.UnmarshalInput) (piface.UnmarshalOutp
 	out, err := mi.unmarshalPointer(in.Buf, p, 0, unmarshalOptions{
 		flags:    in.Flags,
 		resolver: in.Resolver,
-		depth:    in.Depth,
 	})
 	var flags piface.UnmarshalOutputFlags
 	if out.initialized {
@@ -86,10 +82,6 @@ var errUnknown = errors.New("unknown")
 
 func (mi *MessageInfo) unmarshalPointer(b []byte, p pointer, groupTag protowire.Number, opts unmarshalOptions) (out unmarshalOutput, err error) {
 	mi.init()
-	opts.depth--
-	if opts.depth < 0 {
-		return out, errRecursionDepth
-	}
 	if flags.ProtoLegacy && mi.isMessageSet {
 		return unmarshalMessageSet(mi, b, p, opts)
 	}
diff --git a/vendor/google.golang.org/protobuf/internal/impl/pointer_reflect.go b/vendor/google.golang.org/protobuf/internal/impl/pointer_reflect.go
index 4c491bdf4..9e3ed821e 100644
--- a/vendor/google.golang.org/protobuf/internal/impl/pointer_reflect.go
+++ b/vendor/google.golang.org/protobuf/internal/impl/pointer_reflect.go
@@ -2,7 +2,6 @@
 // Use of this source code is governed by a BSD-style
 // license that can be found in the LICENSE file.
 
-//go:build purego || appengine
 // +build purego appengine
 
 package impl
diff --git a/vendor/google.golang.org/protobuf/internal/impl/pointer_unsafe.go b/vendor/google.golang.org/protobuf/internal/impl/pointer_unsafe.go
index ee0e0573e..9ecf23a85 100644
--- a/vendor/google.golang.org/protobuf/internal/impl/pointer_unsafe.go
+++ b/vendor/google.golang.org/protobuf/internal/impl/pointer_unsafe.go
@@ -2,7 +2,6 @@
 // Use of this source code is governed by a BSD-style
 // license that can be found in the LICENSE file.
 
-//go:build !purego && !appengine
 // +build !purego,!appengine
 
 package impl
diff --git a/vendor/google.golang.org/protobuf/internal/strs/strings_pure.go b/vendor/google.golang.org/protobuf/internal/strs/strings_pure.go
index a1f6f3338..85e074c97 100644
--- a/vendor/google.golang.org/protobuf/internal/strs/strings_pure.go
+++ b/vendor/google.golang.org/protobuf/internal/strs/strings_pure.go
@@ -2,7 +2,6 @@
 // Use of this source code is governed by a BSD-style
 // license that can be found in the LICENSE file.
 
-//go:build purego || appengine
 // +build purego appengine
 
 package strs
diff --git a/vendor/google.golang.org/protobuf/internal/strs/strings_unsafe.go b/vendor/google.golang.org/protobuf/internal/strs/strings_unsafe.go
index 56a8a4ed3..2160c7019 100644
--- a/vendor/google.golang.org/protobuf/internal/strs/strings_unsafe.go
+++ b/vendor/google.golang.org/protobuf/internal/strs/strings_unsafe.go
@@ -2,7 +2,6 @@
 // Use of this source code is governed by a BSD-style
 // license that can be found in the LICENSE file.
 
-//go:build !purego && !appengine
 // +build !purego,!appengine
 
 package strs
diff --git a/vendor/google.golang.org/protobuf/internal/version/version.go b/vendor/google.golang.org/protobuf/internal/version/version.go
index 3d40d5249..14e774fb2 100644
--- a/vendor/google.golang.org/protobuf/internal/version/version.go
+++ b/vendor/google.golang.org/protobuf/internal/version/version.go
@@ -52,8 +52,8 @@ import (
 //	10. Send out the CL for review and submit it.
 const (
 	Major      = 1
-	Minor      = 28
-	Patch      = 0
+	Minor      = 27
+	Patch      = 1
 	PreRelease = ""
 )
 
diff --git a/vendor/google.golang.org/protobuf/proto/decode.go b/vendor/google.golang.org/protobuf/proto/decode.go
index 11bf7173b..49f9b8c88 100644
--- a/vendor/google.golang.org/protobuf/proto/decode.go
+++ b/vendor/google.golang.org/protobuf/proto/decode.go
@@ -42,25 +42,18 @@ type UnmarshalOptions struct {
 		FindExtensionByName(field protoreflect.FullName) (protoreflect.ExtensionType, error)
 		FindExtensionByNumber(message protoreflect.FullName, field protoreflect.FieldNumber) (protoreflect.ExtensionType, error)
 	}
-
-	// RecursionLimit limits how deeply messages may be nested.
-	// If zero, a default limit is applied.
-	RecursionLimit int
 }
 
 // Unmarshal parses the wire-format message in b and places the result in m.
 // The provided message must be mutable (e.g., a non-nil pointer to a message).
 func Unmarshal(b []byte, m Message) error {
-	_, err := UnmarshalOptions{RecursionLimit: protowire.DefaultRecursionLimit}.unmarshal(b, m.ProtoReflect())
+	_, err := UnmarshalOptions{}.unmarshal(b, m.ProtoReflect())
 	return err
 }
 
 // Unmarshal parses the wire-format message in b and places the result in m.
 // The provided message must be mutable (e.g., a non-nil pointer to a message).
 func (o UnmarshalOptions) Unmarshal(b []byte, m Message) error {
-	if o.RecursionLimit == 0 {
-		o.RecursionLimit = protowire.DefaultRecursionLimit
-	}
 	_, err := o.unmarshal(b, m.ProtoReflect())
 	return err
 }
@@ -70,9 +63,6 @@ func (o UnmarshalOptions) Unmarshal(b []byte, m Message) error {
 // This method permits fine-grained control over the unmarshaler.
 // Most users should use Unmarshal instead.
 func (o UnmarshalOptions) UnmarshalState(in protoiface.UnmarshalInput) (protoiface.UnmarshalOutput, error) {
-	if o.RecursionLimit == 0 {
-		o.RecursionLimit = protowire.DefaultRecursionLimit
-	}
 	return o.unmarshal(in.Buf, in.Message)
 }
 
@@ -96,17 +86,12 @@ func (o UnmarshalOptions) unmarshal(b []byte, m protoreflect.Message) (out proto
 			Message:  m,
 			Buf:      b,
 			Resolver: o.Resolver,
-			Depth:    o.RecursionLimit,
 		}
 		if o.DiscardUnknown {
 			in.Flags |= protoiface.UnmarshalDiscardUnknown
 		}
 		out, err = methods.Unmarshal(in)
 	} else {
-		o.RecursionLimit--
-		if o.RecursionLimit < 0 {
-			return out, errors.New("exceeded max recursion depth")
-		}
 		err = o.unmarshalMessageSlow(b, m)
 	}
 	if err != nil {
diff --git a/vendor/google.golang.org/protobuf/proto/proto_methods.go b/vendor/google.golang.org/protobuf/proto/proto_methods.go
index 465e057b3..d8dd604f6 100644
--- a/vendor/google.golang.org/protobuf/proto/proto_methods.go
+++ b/vendor/google.golang.org/protobuf/proto/proto_methods.go
@@ -3,7 +3,6 @@
 // license that can be found in the LICENSE file.
 
 // The protoreflect build tag disables use of fast-path methods.
-//go:build !protoreflect
 // +build !protoreflect
 
 package proto
diff --git a/vendor/google.golang.org/protobuf/proto/proto_reflect.go b/vendor/google.golang.org/protobuf/proto/proto_reflect.go
index 494d6ceef..b103d4320 100644
--- a/vendor/google.golang.org/protobuf/proto/proto_reflect.go
+++ b/vendor/google.golang.org/protobuf/proto/proto_reflect.go
@@ -3,7 +3,6 @@
 // license that can be found in the LICENSE file.
 
 // The protoreflect build tag disables use of fast-path methods.
-//go:build protoreflect
 // +build protoreflect
 
 package proto
diff --git a/vendor/google.golang.org/protobuf/reflect/protoreflect/methods.go b/vendor/google.golang.org/protobuf/reflect/protoreflect/methods.go
index d5d5af6eb..6be5d16e9 100644
--- a/vendor/google.golang.org/protobuf/reflect/protoreflect/methods.go
+++ b/vendor/google.golang.org/protobuf/reflect/protoreflect/methods.go
@@ -53,7 +53,6 @@ type (
 			FindExtensionByName(field FullName) (ExtensionType, error)
 			FindExtensionByNumber(message FullName, field FieldNumber) (ExtensionType, error)
 		}
-		Depth int
 	}
 	unmarshalOutput = struct {
 		pragma.NoUnkeyedLiterals
diff --git a/vendor/google.golang.org/protobuf/reflect/protoreflect/value_pure.go b/vendor/google.golang.org/protobuf/reflect/protoreflect/value_pure.go
index 7ced876f4..918e685e1 100644
--- a/vendor/google.golang.org/protobuf/reflect/protoreflect/value_pure.go
+++ b/vendor/google.golang.org/protobuf/reflect/protoreflect/value_pure.go
@@ -2,7 +2,6 @@
 // Use of this source code is governed by a BSD-style
 // license that can be found in the LICENSE file.
 
-//go:build purego || appengine
 // +build purego appengine
 
 package protoreflect
diff --git a/vendor/google.golang.org/protobuf/reflect/protoreflect/value_union.go b/vendor/google.golang.org/protobuf/reflect/protoreflect/value_union.go
index eb7764c30..5a3414724 100644
--- a/vendor/google.golang.org/protobuf/reflect/protoreflect/value_union.go
+++ b/vendor/google.golang.org/protobuf/reflect/protoreflect/value_union.go
@@ -41,31 +41,6 @@ import (
 // Converting to/from a Value and a concrete Go value panics on type mismatch.
 // For example, ValueOf("hello").Int() panics because this attempts to
 // retrieve an int64 from a string.
-//
-// List, Map, and Message Values are called "composite" values.
-//
-// A composite Value may alias (reference) memory at some location,
-// such that changes to the Value updates the that location.
-// A composite value acquired with a Mutable method, such as Message.Mutable,
-// always references the source object.
-//
-// For example:
-//	// Append a 0 to a "repeated int32" field.
-//	// Since the Value returned by Mutable is guaranteed to alias
-//	// the source message, modifying the Value modifies the message.
-//	message.Mutable(fieldDesc).(List).Append(protoreflect.ValueOfInt32(0))
-//
-//	// Assign [0] to a "repeated int32" field by creating a new Value,
-//	// modifying it, and assigning it.
-//	list := message.NewField(fieldDesc).(List)
-//	list.Append(protoreflect.ValueOfInt32(0))
-//	message.Set(fieldDesc, list)
-//	// ERROR: Since it is not defined whether Set aliases the source,
-//	// appending to the List here may or may not modify the message.
-//	list.Append(protoreflect.ValueOfInt32(0))
-//
-// Some operations, such as Message.Get, may return an "empty, read-only"
-// composite Value. Modifying an empty, read-only value panics.
 type Value value
 
 // The protoreflect API uses a custom Value union type instead of interface{}
diff --git a/vendor/google.golang.org/protobuf/reflect/protoreflect/value_unsafe.go b/vendor/google.golang.org/protobuf/reflect/protoreflect/value_unsafe.go
index 702ddf22a..c45debdca 100644
--- a/vendor/google.golang.org/protobuf/reflect/protoreflect/value_unsafe.go
+++ b/vendor/google.golang.org/protobuf/reflect/protoreflect/value_unsafe.go
@@ -2,7 +2,6 @@
 // Use of this source code is governed by a BSD-style
 // license that can be found in the LICENSE file.
 
-//go:build !purego && !appengine
 // +build !purego,!appengine
 
 package protoreflect
diff --git a/vendor/google.golang.org/protobuf/runtime/protoiface/methods.go b/vendor/google.golang.org/protobuf/runtime/protoiface/methods.go
index 44cf467d8..32c04f67e 100644
--- a/vendor/google.golang.org/protobuf/runtime/protoiface/methods.go
+++ b/vendor/google.golang.org/protobuf/runtime/protoiface/methods.go
@@ -103,7 +103,6 @@ type UnmarshalInput = struct {
 		FindExtensionByName(field protoreflect.FullName) (protoreflect.ExtensionType, error)
 		FindExtensionByNumber(message protoreflect.FullName, field protoreflect.FieldNumber) (protoreflect.ExtensionType, error)
 	}
-	Depth int
 }
 
 // UnmarshalOutput is output from the Unmarshal method.
diff --git a/vendor/k8s.io/apiextensions-apiserver/pkg/client/clientset/clientset/clientset.go b/vendor/k8s.io/apiextensions-apiserver/pkg/client/clientset/clientset/clientset.go
new file mode 100644
index 000000000..65ae76067
--- /dev/null
+++ b/vendor/k8s.io/apiextensions-apiserver/pkg/client/clientset/clientset/clientset.go
@@ -0,0 +1,134 @@
+/*
+Copyright 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.
+*/
+
+// Code generated by client-gen. DO NOT EDIT.
+
+package clientset
+
+import (
+	"fmt"
+	"net/http"
+
+	apiextensionsv1 "k8s.io/apiextensions-apiserver/pkg/client/clientset/clientset/typed/apiextensions/v1"
+	apiextensionsv1beta1 "k8s.io/apiextensions-apiserver/pkg/client/clientset/clientset/typed/apiextensions/v1beta1"
+	discovery "k8s.io/client-go/discovery"
+	rest "k8s.io/client-go/rest"
+	flowcontrol "k8s.io/client-go/util/flowcontrol"
+)
+
+type Interface interface {
+	Discovery() discovery.DiscoveryInterface
+	ApiextensionsV1beta1() apiextensionsv1beta1.ApiextensionsV1beta1Interface
+	ApiextensionsV1() apiextensionsv1.ApiextensionsV1Interface
+}
+
+// Clientset contains the clients for groups. Each group has exactly one
+// version included in a Clientset.
+type Clientset struct {
+	*discovery.DiscoveryClient
+	apiextensionsV1beta1 *apiextensionsv1beta1.ApiextensionsV1beta1Client
+	apiextensionsV1      *apiextensionsv1.ApiextensionsV1Client
+}
+
+// ApiextensionsV1beta1 retrieves the ApiextensionsV1beta1Client
+func (c *Clientset) ApiextensionsV1beta1() apiextensionsv1beta1.ApiextensionsV1beta1Interface {
+	return c.apiextensionsV1beta1
+}
+
+// ApiextensionsV1 retrieves the ApiextensionsV1Client
+func (c *Clientset) ApiextensionsV1() apiextensionsv1.ApiextensionsV1Interface {
+	return c.apiextensionsV1
+}
+
+// Discovery retrieves the DiscoveryClient
+func (c *Clientset) Discovery() discovery.DiscoveryInterface {
+	if c == nil {
+		return nil
+	}
+	return c.DiscoveryClient
+}
+
+// NewForConfig creates a new Clientset for the given config.
+// If config's RateLimiter is not set and QPS and Burst are acceptable,
+// NewForConfig will generate a rate-limiter in configShallowCopy.
+// NewForConfig is equivalent to NewForConfigAndClient(c, httpClient),
+// where httpClient was generated with rest.HTTPClientFor(c).
+func NewForConfig(c *rest.Config) (*Clientset, error) {
+	configShallowCopy := *c
+
+	if configShallowCopy.UserAgent == "" {
+		configShallowCopy.UserAgent = rest.DefaultKubernetesUserAgent()
+	}
+
+	// share the transport between all clients
+	httpClient, err := rest.HTTPClientFor(&configShallowCopy)
+	if err != nil {
+		return nil, err
+	}
+
+	return NewForConfigAndClient(&configShallowCopy, httpClient)
+}
+
+// NewForConfigAndClient creates a new Clientset for the given config and http client.
+// Note the http client provided takes precedence over the configured transport values.
+// If config's RateLimiter is not set and QPS and Burst are acceptable,
+// NewForConfigAndClient will generate a rate-limiter in configShallowCopy.
+func NewForConfigAndClient(c *rest.Config, httpClient *http.Client) (*Clientset, error) {
+	configShallowCopy := *c
+	if configShallowCopy.RateLimiter == nil && configShallowCopy.QPS > 0 {
+		if configShallowCopy.Burst <= 0 {
+			return nil, fmt.Errorf("burst is required to be greater than 0 when RateLimiter is not set and QPS is set to greater than 0")
+		}
+		configShallowCopy.RateLimiter = flowcontrol.NewTokenBucketRateLimiter(configShallowCopy.QPS, configShallowCopy.Burst)
+	}
+
+	var cs Clientset
+	var err error
+	cs.apiextensionsV1beta1, err = apiextensionsv1beta1.NewForConfigAndClient(&configShallowCopy, httpClient)
+	if err != nil {
+		return nil, err
+	}
+	cs.apiextensionsV1, err = apiextensionsv1.NewForConfigAndClient(&configShallowCopy, httpClient)
+	if err != nil {
+		return nil, err
+	}
+
+	cs.DiscoveryClient, err = discovery.NewDiscoveryClientForConfigAndClient(&configShallowCopy, httpClient)
+	if err != nil {
+		return nil, err
+	}
+	return &cs, nil
+}
+
+// NewForConfigOrDie creates a new Clientset for the given config and
+// panics if there is an error in the config.
+func NewForConfigOrDie(c *rest.Config) *Clientset {
+	cs, err := NewForConfig(c)
+	if err != nil {
+		panic(err)
+	}
+	return cs
+}
+
+// New creates a new Clientset for the given RESTClient.
+func New(c rest.Interface) *Clientset {
+	var cs Clientset
+	cs.apiextensionsV1beta1 = apiextensionsv1beta1.New(c)
+	cs.apiextensionsV1 = apiextensionsv1.New(c)
+
+	cs.DiscoveryClient = discovery.NewDiscoveryClient(c)
+	return &cs
+}
diff --git a/vendor/k8s.io/apiextensions-apiserver/pkg/client/clientset/clientset/doc.go b/vendor/k8s.io/apiextensions-apiserver/pkg/client/clientset/clientset/doc.go
new file mode 100644
index 000000000..ee865e56d
--- /dev/null
+++ b/vendor/k8s.io/apiextensions-apiserver/pkg/client/clientset/clientset/doc.go
@@ -0,0 +1,20 @@
+/*
+Copyright 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.
+*/
+
+// Code generated by client-gen. DO NOT EDIT.
+
+// This package has the automatically generated clientset.
+package clientset
diff --git a/vendor/k8s.io/apiextensions-apiserver/pkg/client/clientset/clientset/scheme/doc.go b/vendor/k8s.io/apiextensions-apiserver/pkg/client/clientset/clientset/scheme/doc.go
new file mode 100644
index 000000000..7dc375616
--- /dev/null
+++ b/vendor/k8s.io/apiextensions-apiserver/pkg/client/clientset/clientset/scheme/doc.go
@@ -0,0 +1,20 @@
+/*
+Copyright 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.
+*/
+
+// Code generated by client-gen. DO NOT EDIT.
+
+// This package contains the scheme of the automatically generated clientset.
+package scheme
diff --git a/vendor/k8s.io/apiextensions-apiserver/pkg/client/clientset/clientset/scheme/register.go b/vendor/k8s.io/apiextensions-apiserver/pkg/client/clientset/clientset/scheme/register.go
new file mode 100644
index 000000000..144c20666
--- /dev/null
+++ b/vendor/k8s.io/apiextensions-apiserver/pkg/client/clientset/clientset/scheme/register.go
@@ -0,0 +1,58 @@
+/*
+Copyright 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.
+*/
+
+// Code generated by client-gen. DO NOT EDIT.
+
+package scheme
+
+import (
+	apiextensionsv1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1"
+	apiextensionsv1beta1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1beta1"
+	v1 "k8s.io/apimachinery/pkg/apis/meta/v1"
+	runtime "k8s.io/apimachinery/pkg/runtime"
+	schema "k8s.io/apimachinery/pkg/runtime/schema"
+	serializer "k8s.io/apimachinery/pkg/runtime/serializer"
+	utilruntime "k8s.io/apimachinery/pkg/util/runtime"
+)
+
+var Scheme = runtime.NewScheme()
+var Codecs = serializer.NewCodecFactory(Scheme)
+var ParameterCodec = runtime.NewParameterCodec(Scheme)
+var localSchemeBuilder = runtime.SchemeBuilder{
+	apiextensionsv1beta1.AddToScheme,
+	apiextensionsv1.AddToScheme,
+}
+
+// AddToScheme adds all types of this clientset into the given scheme. This allows composition
+// of clientsets, like in:
+//
+//   import (
+//     "k8s.io/client-go/kubernetes"
+//     clientsetscheme "k8s.io/client-go/kubernetes/scheme"
+//     aggregatorclientsetscheme "k8s.io/kube-aggregator/pkg/client/clientset_generated/clientset/scheme"
+//   )
+//
+//   kclientset, _ := kubernetes.NewForConfig(c)
+//   _ = aggregatorclientsetscheme.AddToScheme(clientsetscheme.Scheme)
+//
+// After this, RawExtensions in Kubernetes types will serialize kube-aggregator types
+// correctly.
+var AddToScheme = localSchemeBuilder.AddToScheme
+
+func init() {
+	v1.AddToGroupVersion(Scheme, schema.GroupVersion{Version: "v1"})
+	utilruntime.Must(AddToScheme(Scheme))
+}
diff --git a/vendor/k8s.io/apiextensions-apiserver/pkg/client/clientset/clientset/typed/apiextensions/v1/apiextensions_client.go b/vendor/k8s.io/apiextensions-apiserver/pkg/client/clientset/clientset/typed/apiextensions/v1/apiextensions_client.go
new file mode 100644
index 000000000..0bdc44c40
--- /dev/null
+++ b/vendor/k8s.io/apiextensions-apiserver/pkg/client/clientset/clientset/typed/apiextensions/v1/apiextensions_client.go
@@ -0,0 +1,107 @@
+/*
+Copyright 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.
+*/
+
+// Code generated by client-gen. DO NOT EDIT.
+
+package v1
+
+import (
+	"net/http"
+
+	v1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1"
+	"k8s.io/apiextensions-apiserver/pkg/client/clientset/clientset/scheme"
+	rest "k8s.io/client-go/rest"
+)
+
+type ApiextensionsV1Interface interface {
+	RESTClient() rest.Interface
+	CustomResourceDefinitionsGetter
+}
+
+// ApiextensionsV1Client is used to interact with features provided by the apiextensions.k8s.io group.
+type ApiextensionsV1Client struct {
+	restClient rest.Interface
+}
+
+func (c *ApiextensionsV1Client) CustomResourceDefinitions() CustomResourceDefinitionInterface {
+	return newCustomResourceDefinitions(c)
+}
+
+// NewForConfig creates a new ApiextensionsV1Client for the given config.
+// NewForConfig is equivalent to NewForConfigAndClient(c, httpClient),
+// where httpClient was generated with rest.HTTPClientFor(c).
+func NewForConfig(c *rest.Config) (*ApiextensionsV1Client, error) {
+	config := *c
+	if err := setConfigDefaults(&config); err != nil {
+		return nil, err
+	}
+	httpClient, err := rest.HTTPClientFor(&config)
+	if err != nil {
+		return nil, err
+	}
+	return NewForConfigAndClient(&config, httpClient)
+}
+
+// NewForConfigAndClient creates a new ApiextensionsV1Client for the given config and http client.
+// Note the http client provided takes precedence over the configured transport values.
+func NewForConfigAndClient(c *rest.Config, h *http.Client) (*ApiextensionsV1Client, error) {
+	config := *c
+	if err := setConfigDefaults(&config); err != nil {
+		return nil, err
+	}
+	client, err := rest.RESTClientForConfigAndClient(&config, h)
+	if err != nil {
+		return nil, err
+	}
+	return &ApiextensionsV1Client{client}, nil
+}
+
+// NewForConfigOrDie creates a new ApiextensionsV1Client for the given config and
+// panics if there is an error in the config.
+func NewForConfigOrDie(c *rest.Config) *ApiextensionsV1Client {
+	client, err := NewForConfig(c)
+	if err != nil {
+		panic(err)
+	}
+	return client
+}
+
+// New creates a new ApiextensionsV1Client for the given RESTClient.
+func New(c rest.Interface) *ApiextensionsV1Client {
+	return &ApiextensionsV1Client{c}
+}
+
+func setConfigDefaults(config *rest.Config) error {
+	gv := v1.SchemeGroupVersion
+	config.GroupVersion = &gv
+	config.APIPath = "/apis"
+	config.NegotiatedSerializer = scheme.Codecs.WithoutConversion()
+
+	if config.UserAgent == "" {
+		config.UserAgent = rest.DefaultKubernetesUserAgent()
+	}
+
+	return nil
+}
+
+// RESTClient returns a RESTClient that is used to communicate
+// with API server by this client implementation.
+func (c *ApiextensionsV1Client) RESTClient() rest.Interface {
+	if c == nil {
+		return nil
+	}
+	return c.restClient
+}
diff --git a/vendor/k8s.io/apiextensions-apiserver/pkg/client/clientset/clientset/typed/apiextensions/v1/customresourcedefinition.go b/vendor/k8s.io/apiextensions-apiserver/pkg/client/clientset/clientset/typed/apiextensions/v1/customresourcedefinition.go
new file mode 100644
index 000000000..5569b12d9
--- /dev/null
+++ b/vendor/k8s.io/apiextensions-apiserver/pkg/client/clientset/clientset/typed/apiextensions/v1/customresourcedefinition.go
@@ -0,0 +1,184 @@
+/*
+Copyright 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.
+*/
+
+// Code generated by client-gen. DO NOT EDIT.
+
+package v1
+
+import (
+	"context"
+	"time"
+
+	v1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1"
+	scheme "k8s.io/apiextensions-apiserver/pkg/client/clientset/clientset/scheme"
+	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
+	types "k8s.io/apimachinery/pkg/types"
+	watch "k8s.io/apimachinery/pkg/watch"
+	rest "k8s.io/client-go/rest"
+)
+
+// CustomResourceDefinitionsGetter has a method to return a CustomResourceDefinitionInterface.
+// A group's client should implement this interface.
+type CustomResourceDefinitionsGetter interface {
+	CustomResourceDefinitions() CustomResourceDefinitionInterface
+}
+
+// CustomResourceDefinitionInterface has methods to work with CustomResourceDefinition resources.
+type CustomResourceDefinitionInterface interface {
+	Create(ctx context.Context, customResourceDefinition *v1.CustomResourceDefinition, opts metav1.CreateOptions) (*v1.CustomResourceDefinition, error)
+	Update(ctx context.Context, customResourceDefinition *v1.CustomResourceDefinition, opts metav1.UpdateOptions) (*v1.CustomResourceDefinition, error)
+	UpdateStatus(ctx context.Context, customResourceDefinition *v1.CustomResourceDefinition, opts metav1.UpdateOptions) (*v1.CustomResourceDefinition, error)
+	Delete(ctx context.Context, name string, opts metav1.DeleteOptions) error
+	DeleteCollection(ctx context.Context, opts metav1.DeleteOptions, listOpts metav1.ListOptions) error
+	Get(ctx context.Context, name string, opts metav1.GetOptions) (*v1.CustomResourceDefinition, error)
+	List(ctx context.Context, opts metav1.ListOptions) (*v1.CustomResourceDefinitionList, error)
+	Watch(ctx context.Context, opts metav1.ListOptions) (watch.Interface, error)
+	Patch(ctx context.Context, name string, pt types.PatchType, data []byte, opts metav1.PatchOptions, subresources ...string) (result *v1.CustomResourceDefinition, err error)
+	CustomResourceDefinitionExpansion
+}
+
+// customResourceDefinitions implements CustomResourceDefinitionInterface
+type customResourceDefinitions struct {
+	client rest.Interface
+}
+
+// newCustomResourceDefinitions returns a CustomResourceDefinitions
+func newCustomResourceDefinitions(c *ApiextensionsV1Client) *customResourceDefinitions {
+	return &customResourceDefinitions{
+		client: c.RESTClient(),
+	}
+}
+
+// Get takes name of the customResourceDefinition, and returns the corresponding customResourceDefinition object, and an error if there is any.
+func (c *customResourceDefinitions) Get(ctx context.Context, name string, options metav1.GetOptions) (result *v1.CustomResourceDefinition, err error) {
+	result = &v1.CustomResourceDefinition{}
+	err = c.client.Get().
+		Resource("customresourcedefinitions").
+		Name(name).
+		VersionedParams(&options, scheme.ParameterCodec).
+		Do(ctx).
+		Into(result)
+	return
+}
+
+// List takes label and field selectors, and returns the list of CustomResourceDefinitions that match those selectors.
+func (c *customResourceDefinitions) List(ctx context.Context, opts metav1.ListOptions) (result *v1.CustomResourceDefinitionList, err error) {
+	var timeout time.Duration
+	if opts.TimeoutSeconds != nil {
+		timeout = time.Duration(*opts.TimeoutSeconds) * time.Second
+	}
+	result = &v1.CustomResourceDefinitionList{}
+	err = c.client.Get().
+		Resource("customresourcedefinitions").
+		VersionedParams(&opts, scheme.ParameterCodec).
+		Timeout(timeout).
+		Do(ctx).
+		Into(result)
+	return
+}
+
+// Watch returns a watch.Interface that watches the requested customResourceDefinitions.
+func (c *customResourceDefinitions) Watch(ctx context.Context, opts metav1.ListOptions) (watch.Interface, error) {
+	var timeout time.Duration
+	if opts.TimeoutSeconds != nil {
+		timeout = time.Duration(*opts.TimeoutSeconds) * time.Second
+	}
+	opts.Watch = true
+	return c.client.Get().
+		Resource("customresourcedefinitions").
+		VersionedParams(&opts, scheme.ParameterCodec).
+		Timeout(timeout).
+		Watch(ctx)
+}
+
+// Create takes the representation of a customResourceDefinition and creates it.  Returns the server's representation of the customResourceDefinition, and an error, if there is any.
+func (c *customResourceDefinitions) Create(ctx context.Context, customResourceDefinition *v1.CustomResourceDefinition, opts metav1.CreateOptions) (result *v1.CustomResourceDefinition, err error) {
+	result = &v1.CustomResourceDefinition{}
+	err = c.client.Post().
+		Resource("customresourcedefinitions").
+		VersionedParams(&opts, scheme.ParameterCodec).
+		Body(customResourceDefinition).
+		Do(ctx).
+		Into(result)
+	return
+}
+
+// Update takes the representation of a customResourceDefinition and updates it. Returns the server's representation of the customResourceDefinition, and an error, if there is any.
+func (c *customResourceDefinitions) Update(ctx context.Context, customResourceDefinition *v1.CustomResourceDefinition, opts metav1.UpdateOptions) (result *v1.CustomResourceDefinition, err error) {
+	result = &v1.CustomResourceDefinition{}
+	err = c.client.Put().
+		Resource("customresourcedefinitions").
+		Name(customResourceDefinition.Name).
+		VersionedParams(&opts, scheme.ParameterCodec).
+		Body(customResourceDefinition).
+		Do(ctx).
+		Into(result)
+	return
+}
+
+// UpdateStatus was generated because the type contains a Status member.
+// Add a +genclient:noStatus comment above the type to avoid generating UpdateStatus().
+func (c *customResourceDefinitions) UpdateStatus(ctx context.Context, customResourceDefinition *v1.CustomResourceDefinition, opts metav1.UpdateOptions) (result *v1.CustomResourceDefinition, err error) {
+	result = &v1.CustomResourceDefinition{}
+	err = c.client.Put().
+		Resource("customresourcedefinitions").
+		Name(customResourceDefinition.Name).
+		SubResource("status").
+		VersionedParams(&opts, scheme.ParameterCodec).
+		Body(customResourceDefinition).
+		Do(ctx).
+		Into(result)
+	return
+}
+
+// Delete takes name of the customResourceDefinition and deletes it. Returns an error if one occurs.
+func (c *customResourceDefinitions) Delete(ctx context.Context, name string, opts metav1.DeleteOptions) error {
+	return c.client.Delete().
+		Resource("customresourcedefinitions").
+		Name(name).
+		Body(&opts).
+		Do(ctx).
+		Error()
+}
+
+// DeleteCollection deletes a collection of objects.
+func (c *customResourceDefinitions) DeleteCollection(ctx context.Context, opts metav1.DeleteOptions, listOpts metav1.ListOptions) error {
+	var timeout time.Duration
+	if listOpts.TimeoutSeconds != nil {
+		timeout = time.Duration(*listOpts.TimeoutSeconds) * time.Second
+	}
+	return c.client.Delete().
+		Resource("customresourcedefinitions").
+		VersionedParams(&listOpts, scheme.ParameterCodec).
+		Timeout(timeout).
+		Body(&opts).
+		Do(ctx).
+		Error()
+}
+
+// Patch applies the patch and returns the patched customResourceDefinition.
+func (c *customResourceDefinitions) Patch(ctx context.Context, name string, pt types.PatchType, data []byte, opts metav1.PatchOptions, subresources ...string) (result *v1.CustomResourceDefinition, err error) {
+	result = &v1.CustomResourceDefinition{}
+	err = c.client.Patch(pt).
+		Resource("customresourcedefinitions").
+		Name(name).
+		SubResource(subresources...).
+		VersionedParams(&opts, scheme.ParameterCodec).
+		Body(data).
+		Do(ctx).
+		Into(result)
+	return
+}
diff --git a/vendor/k8s.io/apiextensions-apiserver/pkg/client/clientset/clientset/typed/apiextensions/v1/doc.go b/vendor/k8s.io/apiextensions-apiserver/pkg/client/clientset/clientset/typed/apiextensions/v1/doc.go
new file mode 100644
index 000000000..3af5d054f
--- /dev/null
+++ b/vendor/k8s.io/apiextensions-apiserver/pkg/client/clientset/clientset/typed/apiextensions/v1/doc.go
@@ -0,0 +1,20 @@
+/*
+Copyright 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.
+*/
+
+// Code generated by client-gen. DO NOT EDIT.
+
+// This package has the automatically generated typed clients.
+package v1
diff --git a/vendor/k8s.io/apiextensions-apiserver/pkg/client/clientset/clientset/typed/apiextensions/v1/generated_expansion.go b/vendor/k8s.io/apiextensions-apiserver/pkg/client/clientset/clientset/typed/apiextensions/v1/generated_expansion.go
new file mode 100644
index 000000000..e594636af
--- /dev/null
+++ b/vendor/k8s.io/apiextensions-apiserver/pkg/client/clientset/clientset/typed/apiextensions/v1/generated_expansion.go
@@ -0,0 +1,21 @@
+/*
+Copyright 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.
+*/
+
+// Code generated by client-gen. DO NOT EDIT.
+
+package v1
+
+type CustomResourceDefinitionExpansion interface{}
diff --git a/vendor/k8s.io/apiextensions-apiserver/pkg/client/clientset/clientset/typed/apiextensions/v1beta1/apiextensions_client.go b/vendor/k8s.io/apiextensions-apiserver/pkg/client/clientset/clientset/typed/apiextensions/v1beta1/apiextensions_client.go
new file mode 100644
index 000000000..657ce2ca8
--- /dev/null
+++ b/vendor/k8s.io/apiextensions-apiserver/pkg/client/clientset/clientset/typed/apiextensions/v1beta1/apiextensions_client.go
@@ -0,0 +1,107 @@
+/*
+Copyright 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.
+*/
+
+// Code generated by client-gen. DO NOT EDIT.
+
+package v1beta1
+
+import (
+	"net/http"
+
+	v1beta1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1beta1"
+	"k8s.io/apiextensions-apiserver/pkg/client/clientset/clientset/scheme"
+	rest "k8s.io/client-go/rest"
+)
+
+type ApiextensionsV1beta1Interface interface {
+	RESTClient() rest.Interface
+	CustomResourceDefinitionsGetter
+}
+
+// ApiextensionsV1beta1Client is used to interact with features provided by the apiextensions.k8s.io group.
+type ApiextensionsV1beta1Client struct {
+	restClient rest.Interface
+}
+
+func (c *ApiextensionsV1beta1Client) CustomResourceDefinitions() CustomResourceDefinitionInterface {
+	return newCustomResourceDefinitions(c)
+}
+
+// NewForConfig creates a new ApiextensionsV1beta1Client for the given config.
+// NewForConfig is equivalent to NewForConfigAndClient(c, httpClient),
+// where httpClient was generated with rest.HTTPClientFor(c).
+func NewForConfig(c *rest.Config) (*ApiextensionsV1beta1Client, error) {
+	config := *c
+	if err := setConfigDefaults(&config); err != nil {
+		return nil, err
+	}
+	httpClient, err := rest.HTTPClientFor(&config)
+	if err != nil {
+		return nil, err
+	}
+	return NewForConfigAndClient(&config, httpClient)
+}
+
+// NewForConfigAndClient creates a new ApiextensionsV1beta1Client for the given config and http client.
+// Note the http client provided takes precedence over the configured transport values.
+func NewForConfigAndClient(c *rest.Config, h *http.Client) (*ApiextensionsV1beta1Client, error) {
+	config := *c
+	if err := setConfigDefaults(&config); err != nil {
+		return nil, err
+	}
+	client, err := rest.RESTClientForConfigAndClient(&config, h)
+	if err != nil {
+		return nil, err
+	}
+	return &ApiextensionsV1beta1Client{client}, nil
+}
+
+// NewForConfigOrDie creates a new ApiextensionsV1beta1Client for the given config and
+// panics if there is an error in the config.
+func NewForConfigOrDie(c *rest.Config) *ApiextensionsV1beta1Client {
+	client, err := NewForConfig(c)
+	if err != nil {
+		panic(err)
+	}
+	return client
+}
+
+// New creates a new ApiextensionsV1beta1Client for the given RESTClient.
+func New(c rest.Interface) *ApiextensionsV1beta1Client {
+	return &ApiextensionsV1beta1Client{c}
+}
+
+func setConfigDefaults(config *rest.Config) error {
+	gv := v1beta1.SchemeGroupVersion
+	config.GroupVersion = &gv
+	config.APIPath = "/apis"
+	config.NegotiatedSerializer = scheme.Codecs.WithoutConversion()
+
+	if config.UserAgent == "" {
+		config.UserAgent = rest.DefaultKubernetesUserAgent()
+	}
+
+	return nil
+}
+
+// RESTClient returns a RESTClient that is used to communicate
+// with API server by this client implementation.
+func (c *ApiextensionsV1beta1Client) RESTClient() rest.Interface {
+	if c == nil {
+		return nil
+	}
+	return c.restClient
+}
diff --git a/vendor/k8s.io/apiextensions-apiserver/pkg/client/clientset/clientset/typed/apiextensions/v1beta1/customresourcedefinition.go b/vendor/k8s.io/apiextensions-apiserver/pkg/client/clientset/clientset/typed/apiextensions/v1beta1/customresourcedefinition.go
new file mode 100644
index 000000000..2d16ca709
--- /dev/null
+++ b/vendor/k8s.io/apiextensions-apiserver/pkg/client/clientset/clientset/typed/apiextensions/v1beta1/customresourcedefinition.go
@@ -0,0 +1,184 @@
+/*
+Copyright 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.
+*/
+
+// Code generated by client-gen. DO NOT EDIT.
+
+package v1beta1
+
+import (
+	"context"
+	"time"
+
+	v1beta1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1beta1"
+	scheme "k8s.io/apiextensions-apiserver/pkg/client/clientset/clientset/scheme"
+	v1 "k8s.io/apimachinery/pkg/apis/meta/v1"
+	types "k8s.io/apimachinery/pkg/types"
+	watch "k8s.io/apimachinery/pkg/watch"
+	rest "k8s.io/client-go/rest"
+)
+
+// CustomResourceDefinitionsGetter has a method to return a CustomResourceDefinitionInterface.
+// A group's client should implement this interface.
+type CustomResourceDefinitionsGetter interface {
+	CustomResourceDefinitions() CustomResourceDefinitionInterface
+}
+
+// CustomResourceDefinitionInterface has methods to work with CustomResourceDefinition resources.
+type CustomResourceDefinitionInterface interface {
+	Create(ctx context.Context, customResourceDefinition *v1beta1.CustomResourceDefinition, opts v1.CreateOptions) (*v1beta1.CustomResourceDefinition, error)
+	Update(ctx context.Context, customResourceDefinition *v1beta1.CustomResourceDefinition, opts v1.UpdateOptions) (*v1beta1.CustomResourceDefinition, error)
+	UpdateStatus(ctx context.Context, customResourceDefinition *v1beta1.CustomResourceDefinition, opts v1.UpdateOptions) (*v1beta1.CustomResourceDefinition, error)
+	Delete(ctx context.Context, name string, opts v1.DeleteOptions) error
+	DeleteCollection(ctx context.Context, opts v1.DeleteOptions, listOpts v1.ListOptions) error
+	Get(ctx context.Context, name string, opts v1.GetOptions) (*v1beta1.CustomResourceDefinition, error)
+	List(ctx context.Context, opts v1.ListOptions) (*v1beta1.CustomResourceDefinitionList, error)
+	Watch(ctx context.Context, opts v1.ListOptions) (watch.Interface, error)
+	Patch(ctx context.Context, name string, pt types.PatchType, data []byte, opts v1.PatchOptions, subresources ...string) (result *v1beta1.CustomResourceDefinition, err error)
+	CustomResourceDefinitionExpansion
+}
+
+// customResourceDefinitions implements CustomResourceDefinitionInterface
+type customResourceDefinitions struct {
+	client rest.Interface
+}
+
+// newCustomResourceDefinitions returns a CustomResourceDefinitions
+func newCustomResourceDefinitions(c *ApiextensionsV1beta1Client) *customResourceDefinitions {
+	return &customResourceDefinitions{
+		client: c.RESTClient(),
+	}
+}
+
+// Get takes name of the customResourceDefinition, and returns the corresponding customResourceDefinition object, and an error if there is any.
+func (c *customResourceDefinitions) Get(ctx context.Context, name string, options v1.GetOptions) (result *v1beta1.CustomResourceDefinition, err error) {
+	result = &v1beta1.CustomResourceDefinition{}
+	err = c.client.Get().
+		Resource("customresourcedefinitions").
+		Name(name).
+		VersionedParams(&options, scheme.ParameterCodec).
+		Do(ctx).
+		Into(result)
+	return
+}
+
+// List takes label and field selectors, and returns the list of CustomResourceDefinitions that match those selectors.
+func (c *customResourceDefinitions) List(ctx context.Context, opts v1.ListOptions) (result *v1beta1.CustomResourceDefinitionList, err error) {
+	var timeout time.Duration
+	if opts.TimeoutSeconds != nil {
+		timeout = time.Duration(*opts.TimeoutSeconds) * time.Second
+	}
+	result = &v1beta1.CustomResourceDefinitionList{}
+	err = c.client.Get().
+		Resource("customresourcedefinitions").
+		VersionedParams(&opts, scheme.ParameterCodec).
+		Timeout(timeout).
+		Do(ctx).
+		Into(result)
+	return
+}
+
+// Watch returns a watch.Interface that watches the requested customResourceDefinitions.
+func (c *customResourceDefinitions) Watch(ctx context.Context, opts v1.ListOptions) (watch.Interface, error) {
+	var timeout time.Duration
+	if opts.TimeoutSeconds != nil {
+		timeout = time.Duration(*opts.TimeoutSeconds) * time.Second
+	}
+	opts.Watch = true
+	return c.client.Get().
+		Resource("customresourcedefinitions").
+		VersionedParams(&opts, scheme.ParameterCodec).
+		Timeout(timeout).
+		Watch(ctx)
+}
+
+// Create takes the representation of a customResourceDefinition and creates it.  Returns the server's representation of the customResourceDefinition, and an error, if there is any.
+func (c *customResourceDefinitions) Create(ctx context.Context, customResourceDefinition *v1beta1.CustomResourceDefinition, opts v1.CreateOptions) (result *v1beta1.CustomResourceDefinition, err error) {
+	result = &v1beta1.CustomResourceDefinition{}
+	err = c.client.Post().
+		Resource("customresourcedefinitions").
+		VersionedParams(&opts, scheme.ParameterCodec).
+		Body(customResourceDefinition).
+		Do(ctx).
+		Into(result)
+	return
+}
+
+// Update takes the representation of a customResourceDefinition and updates it. Returns the server's representation of the customResourceDefinition, and an error, if there is any.
+func (c *customResourceDefinitions) Update(ctx context.Context, customResourceDefinition *v1beta1.CustomResourceDefinition, opts v1.UpdateOptions) (result *v1beta1.CustomResourceDefinition, err error) {
+	result = &v1beta1.CustomResourceDefinition{}
+	err = c.client.Put().
+		Resource("customresourcedefinitions").
+		Name(customResourceDefinition.Name).
+		VersionedParams(&opts, scheme.ParameterCodec).
+		Body(customResourceDefinition).
+		Do(ctx).
+		Into(result)
+	return
+}
+
+// UpdateStatus was generated because the type contains a Status member.
+// Add a +genclient:noStatus comment above the type to avoid generating UpdateStatus().
+func (c *customResourceDefinitions) UpdateStatus(ctx context.Context, customResourceDefinition *v1beta1.CustomResourceDefinition, opts v1.UpdateOptions) (result *v1beta1.CustomResourceDefinition, err error) {
+	result = &v1beta1.CustomResourceDefinition{}
+	err = c.client.Put().
+		Resource("customresourcedefinitions").
+		Name(customResourceDefinition.Name).
+		SubResource("status").
+		VersionedParams(&opts, scheme.ParameterCodec).
+		Body(customResourceDefinition).
+		Do(ctx).
+		Into(result)
+	return
+}
+
+// Delete takes name of the customResourceDefinition and deletes it. Returns an error if one occurs.
+func (c *customResourceDefinitions) Delete(ctx context.Context, name string, opts v1.DeleteOptions) error {
+	return c.client.Delete().
+		Resource("customresourcedefinitions").
+		Name(name).
+		Body(&opts).
+		Do(ctx).
+		Error()
+}
+
+// DeleteCollection deletes a collection of objects.
+func (c *customResourceDefinitions) DeleteCollection(ctx context.Context, opts v1.DeleteOptions, listOpts v1.ListOptions) error {
+	var timeout time.Duration
+	if listOpts.TimeoutSeconds != nil {
+		timeout = time.Duration(*listOpts.TimeoutSeconds) * time.Second
+	}
+	return c.client.Delete().
+		Resource("customresourcedefinitions").
+		VersionedParams(&listOpts, scheme.ParameterCodec).
+		Timeout(timeout).
+		Body(&opts).
+		Do(ctx).
+		Error()
+}
+
+// Patch applies the patch and returns the patched customResourceDefinition.
+func (c *customResourceDefinitions) Patch(ctx context.Context, name string, pt types.PatchType, data []byte, opts v1.PatchOptions, subresources ...string) (result *v1beta1.CustomResourceDefinition, err error) {
+	result = &v1beta1.CustomResourceDefinition{}
+	err = c.client.Patch(pt).
+		Resource("customresourcedefinitions").
+		Name(name).
+		SubResource(subresources...).
+		VersionedParams(&opts, scheme.ParameterCodec).
+		Body(data).
+		Do(ctx).
+		Into(result)
+	return
+}
diff --git a/vendor/k8s.io/apiextensions-apiserver/pkg/client/clientset/clientset/typed/apiextensions/v1beta1/doc.go b/vendor/k8s.io/apiextensions-apiserver/pkg/client/clientset/clientset/typed/apiextensions/v1beta1/doc.go
new file mode 100644
index 000000000..771101956
--- /dev/null
+++ b/vendor/k8s.io/apiextensions-apiserver/pkg/client/clientset/clientset/typed/apiextensions/v1beta1/doc.go
@@ -0,0 +1,20 @@
+/*
+Copyright 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.
+*/
+
+// Code generated by client-gen. DO NOT EDIT.
+
+// This package has the automatically generated typed clients.
+package v1beta1
diff --git a/vendor/k8s.io/apiextensions-apiserver/pkg/client/clientset/clientset/typed/apiextensions/v1beta1/generated_expansion.go b/vendor/k8s.io/apiextensions-apiserver/pkg/client/clientset/clientset/typed/apiextensions/v1beta1/generated_expansion.go
new file mode 100644
index 000000000..2a989d4be
--- /dev/null
+++ b/vendor/k8s.io/apiextensions-apiserver/pkg/client/clientset/clientset/typed/apiextensions/v1beta1/generated_expansion.go
@@ -0,0 +1,21 @@
+/*
+Copyright 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.
+*/
+
+// Code generated by client-gen. DO NOT EDIT.
+
+package v1beta1
+
+type CustomResourceDefinitionExpansion interface{}
diff --git a/vendor/k8s.io/client-go/util/retry/OWNERS b/vendor/k8s.io/client-go/util/retry/OWNERS
new file mode 100644
index 000000000..75736b5aa
--- /dev/null
+++ b/vendor/k8s.io/client-go/util/retry/OWNERS
@@ -0,0 +1,4 @@
+# See the OWNERS docs at https://go.k8s.io/owners
+
+reviewers:
+  - caesarxuchao
diff --git a/vendor/k8s.io/client-go/util/retry/util.go b/vendor/k8s.io/client-go/util/retry/util.go
new file mode 100644
index 000000000..772f5bd7a
--- /dev/null
+++ b/vendor/k8s.io/client-go/util/retry/util.go
@@ -0,0 +1,105 @@
+/*
+Copyright 2016 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 retry
+
+import (
+	"time"
+
+	"k8s.io/apimachinery/pkg/api/errors"
+	"k8s.io/apimachinery/pkg/util/wait"
+)
+
+// DefaultRetry is the recommended retry for a conflict where multiple clients
+// are making changes to the same resource.
+var DefaultRetry = wait.Backoff{
+	Steps:    5,
+	Duration: 10 * time.Millisecond,
+	Factor:   1.0,
+	Jitter:   0.1,
+}
+
+// DefaultBackoff is the recommended backoff for a conflict where a client
+// may be attempting to make an unrelated modification to a resource under
+// active management by one or more controllers.
+var DefaultBackoff = wait.Backoff{
+	Steps:    4,
+	Duration: 10 * time.Millisecond,
+	Factor:   5.0,
+	Jitter:   0.1,
+}
+
+// OnError allows the caller to retry fn in case the error returned by fn is retriable
+// according to the provided function. backoff defines the maximum retries and the wait
+// interval between two retries.
+func OnError(backoff wait.Backoff, retriable func(error) bool, fn func() error) error {
+	var lastErr error
+	err := wait.ExponentialBackoff(backoff, func() (bool, error) {
+		err := fn()
+		switch {
+		case err == nil:
+			return true, nil
+		case retriable(err):
+			lastErr = err
+			return false, nil
+		default:
+			return false, err
+		}
+	})
+	if err == wait.ErrWaitTimeout {
+		err = lastErr
+	}
+	return err
+}
+
+// RetryOnConflict is used to make an update to a resource when you have to worry about
+// conflicts caused by other code making unrelated updates to the resource at the same
+// time. fn should fetch the resource to be modified, make appropriate changes to it, try
+// to update it, and return (unmodified) the error from the update function. On a
+// successful update, RetryOnConflict will return nil. If the update function returns a
+// "Conflict" error, RetryOnConflict will wait some amount of time as described by
+// backoff, and then try again. On a non-"Conflict" error, or if it retries too many times
+// and gives up, RetryOnConflict will return an error to the caller.
+//
+//     err := retry.RetryOnConflict(retry.DefaultRetry, func() error {
+//         // Fetch the resource here; you need to refetch it on every try, since
+//         // if you got a conflict on the last update attempt then you need to get
+//         // the current version before making your own changes.
+//         pod, err := c.Pods("mynamespace").Get(name, metav1.GetOptions{})
+//         if err != nil {
+//             return err
+//         }
+//
+//         // Make whatever updates to the resource are needed
+//         pod.Status.Phase = v1.PodFailed
+//
+//         // Try to update
+//         _, err = c.Pods("mynamespace").UpdateStatus(pod)
+//         // You have to return err itself here (not wrapped inside another error)
+//         // so that RetryOnConflict can identify it correctly.
+//         return err
+//     })
+//     if err != nil {
+//         // May be conflict if max retries were hit, or may be something unrelated
+//         // like permissions or a network error
+//         return err
+//     }
+//     ...
+//
+// TODO: Make Backoff an interface?
+func RetryOnConflict(backoff wait.Backoff, fn func() error) error {
+	return OnError(backoff, errors.IsConflict, fn)
+}
diff --git a/vendor/modules.txt b/vendor/modules.txt
index 599d3814c..a7c976401 100644
--- a/vendor/modules.txt
+++ b/vendor/modules.txt
@@ -164,8 +164,8 @@ github.com/google/gnostic/extensions
 github.com/google/gnostic/jsonschema
 github.com/google/gnostic/openapiv2
 github.com/google/gnostic/openapiv3
-# github.com/google/go-cmp v0.5.8
-## explicit; go 1.13
+# github.com/google/go-cmp v0.5.7
+## explicit; go 1.11
 github.com/google/go-cmp/cmp
 github.com/google/go-cmp/cmp/internal/diff
 github.com/google/go-cmp/cmp/internal/flags
@@ -278,7 +278,7 @@ github.com/onsi/ginkgo/v2/internal/parallel_support
 github.com/onsi/ginkgo/v2/internal/testingtproxy
 github.com/onsi/ginkgo/v2/reporters
 github.com/onsi/ginkgo/v2/types
-# github.com/onsi/gomega v1.20.0
+# github.com/onsi/gomega v1.19.0
 ## explicit; go 1.18
 github.com/onsi/gomega
 github.com/onsi/gomega/format
@@ -504,8 +504,8 @@ google.golang.org/appengine/internal/modules
 google.golang.org/appengine/internal/remote_api
 google.golang.org/appengine/internal/urlfetch
 google.golang.org/appengine/urlfetch
-# google.golang.org/protobuf v1.28.0
-## explicit; go 1.11
+# google.golang.org/protobuf v1.27.1
+## explicit; go 1.9
 google.golang.org/protobuf/encoding/prototext
 google.golang.org/protobuf/encoding/protowire
 google.golang.org/protobuf/internal/descfmt
@@ -605,6 +605,10 @@ k8s.io/api/storage/v1beta1
 k8s.io/apiextensions-apiserver/pkg/apis/apiextensions
 k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1
 k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1beta1
+k8s.io/apiextensions-apiserver/pkg/client/clientset/clientset
+k8s.io/apiextensions-apiserver/pkg/client/clientset/clientset/scheme
+k8s.io/apiextensions-apiserver/pkg/client/clientset/clientset/typed/apiextensions/v1
+k8s.io/apiextensions-apiserver/pkg/client/clientset/clientset/typed/apiextensions/v1beta1
 # k8s.io/apimachinery v0.24.0
 ## explicit; go 1.16
 k8s.io/apimachinery/pkg/api/equality
@@ -783,6 +787,7 @@ k8s.io/client-go/util/flowcontrol
 k8s.io/client-go/util/homedir
 k8s.io/client-go/util/jsonpath
 k8s.io/client-go/util/keyutil
+k8s.io/client-go/util/retry
 k8s.io/client-go/util/workqueue
 # k8s.io/component-base v0.24.0
 ## explicit; go 1.16
@@ -834,15 +839,21 @@ sigs.k8s.io/controller-runtime/pkg/config/v1alpha1
 sigs.k8s.io/controller-runtime/pkg/controller
 sigs.k8s.io/controller-runtime/pkg/controller/controllerutil
 sigs.k8s.io/controller-runtime/pkg/conversion
+sigs.k8s.io/controller-runtime/pkg/envtest
 sigs.k8s.io/controller-runtime/pkg/event
 sigs.k8s.io/controller-runtime/pkg/handler
 sigs.k8s.io/controller-runtime/pkg/healthz
 sigs.k8s.io/controller-runtime/pkg/internal/controller
 sigs.k8s.io/controller-runtime/pkg/internal/controller/metrics
+sigs.k8s.io/controller-runtime/pkg/internal/flock
 sigs.k8s.io/controller-runtime/pkg/internal/httpserver
 sigs.k8s.io/controller-runtime/pkg/internal/log
 sigs.k8s.io/controller-runtime/pkg/internal/objectutil
 sigs.k8s.io/controller-runtime/pkg/internal/recorder
+sigs.k8s.io/controller-runtime/pkg/internal/testing/addr
+sigs.k8s.io/controller-runtime/pkg/internal/testing/certs
+sigs.k8s.io/controller-runtime/pkg/internal/testing/controlplane
+sigs.k8s.io/controller-runtime/pkg/internal/testing/process
 sigs.k8s.io/controller-runtime/pkg/leaderelection
 sigs.k8s.io/controller-runtime/pkg/log
 sigs.k8s.io/controller-runtime/pkg/manager
diff --git a/vendor/sigs.k8s.io/controller-runtime/pkg/internal/flock/doc.go b/vendor/sigs.k8s.io/controller-runtime/pkg/internal/flock/doc.go
new file mode 100644
index 000000000..11e39823e
--- /dev/null
+++ b/vendor/sigs.k8s.io/controller-runtime/pkg/internal/flock/doc.go
@@ -0,0 +1,21 @@
+/*
+Copyright 2021 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 flock is copied from k8s.io/kubernetes/pkg/util/flock to avoid
+// importing k8s.io/kubernetes as a dependency.
+//
+// Provides file locking functionalities on unix systems.
+package flock
diff --git a/vendor/sigs.k8s.io/controller-runtime/pkg/internal/flock/errors.go b/vendor/sigs.k8s.io/controller-runtime/pkg/internal/flock/errors.go
new file mode 100644
index 000000000..ee7a43437
--- /dev/null
+++ b/vendor/sigs.k8s.io/controller-runtime/pkg/internal/flock/errors.go
@@ -0,0 +1,24 @@
+/*
+Copyright 2021 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 flock
+
+import "errors"
+
+var (
+	// ErrAlreadyLocked is returned when the file is already locked.
+	ErrAlreadyLocked = errors.New("the file is already locked")
+)
diff --git a/vendor/sigs.k8s.io/controller-runtime/pkg/internal/flock/flock_other.go b/vendor/sigs.k8s.io/controller-runtime/pkg/internal/flock/flock_other.go
new file mode 100644
index 000000000..069a5b3a2
--- /dev/null
+++ b/vendor/sigs.k8s.io/controller-runtime/pkg/internal/flock/flock_other.go
@@ -0,0 +1,24 @@
+// +build !linux,!darwin,!freebsd,!openbsd,!netbsd,!dragonfly
+
+/*
+Copyright 2016 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 flock
+
+// Acquire is not implemented on non-unix systems.
+func Acquire(path string) error {
+	return nil
+}
diff --git a/vendor/sigs.k8s.io/controller-runtime/pkg/internal/flock/flock_unix.go b/vendor/sigs.k8s.io/controller-runtime/pkg/internal/flock/flock_unix.go
new file mode 100644
index 000000000..3a904f3f5
--- /dev/null
+++ b/vendor/sigs.k8s.io/controller-runtime/pkg/internal/flock/flock_unix.go
@@ -0,0 +1,47 @@
+// +build linux darwin freebsd openbsd netbsd dragonfly
+
+/*
+Copyright 2016 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 flock
+
+import (
+	"errors"
+	"fmt"
+	"os"
+
+	"golang.org/x/sys/unix"
+)
+
+// Acquire acquires a lock on a file for the duration of the process. This method
+// is reentrant.
+func Acquire(path string) error {
+	fd, err := unix.Open(path, unix.O_CREAT|unix.O_RDWR|unix.O_CLOEXEC, 0600)
+	if err != nil {
+		if errors.Is(err, os.ErrExist) {
+			return fmt.Errorf("cannot lock file %q: %w", path, ErrAlreadyLocked)
+		}
+		return err
+	}
+
+	// We don't need to close the fd since we should hold
+	// it until the process exits.
+	err = unix.Flock(fd, unix.LOCK_NB|unix.LOCK_EX)
+	if errors.Is(err, unix.EWOULDBLOCK) { // This condition requires LOCK_NB.
+		return fmt.Errorf("cannot lock file %q: %w", path, ErrAlreadyLocked)
+	}
+	return err
+}


Test patch:
diff --git a/pkg/webhook/webhook_suite_test.go b/pkg/webhook/webhook_suite_test.go
new file mode 100644
index 000000000..5cf0f6d32
--- /dev/null
+++ b/pkg/webhook/webhook_suite_test.go
@@ -0,0 +1,112 @@
+// SPDX-License-Identifier: Apache-2.0
+// Copyright Authors of Cilium
+
+package webhook_test
+
+import (
+	"context"
+	"crypto/tls"
+	"fmt"
+	"net"
+	"path/filepath"
+	"testing"
+	"time"
+
+	. "github.com/onsi/ginkgo/v2"
+	. "github.com/onsi/gomega"
+
+	admissionv1 "k8s.io/api/admission/v1"
+	"k8s.io/apimachinery/pkg/runtime"
+	"k8s.io/client-go/rest"
+	ctrl "sigs.k8s.io/controller-runtime"
+	"sigs.k8s.io/controller-runtime/pkg/client"
+	"sigs.k8s.io/controller-runtime/pkg/envtest"
+
+	spiderpoolv1 "github.com/spidernet-io/spiderpool/pkg/k8s/apis/v1"
+	"github.com/spidernet-io/spiderpool/pkg/webhook"
+)
+
+var err error
+var cfg *rest.Config
+var k8sClient client.Client
+var testEnv *envtest.Environment
+var ctx context.Context
+var cancel context.CancelFunc
+
+func TestWebhook(t *testing.T) {
+	RegisterFailHandler(Fail)
+	RunSpecs(t, "Webhook Suite")
+}
+
+var _ = BeforeSuite(func() {
+	ctx, cancel = context.WithCancel(context.TODO())
+	DeferCleanup(cancel)
+
+	By("Bootstrapping test environment")
+	testEnv = &envtest.Environment{
+		CRDDirectoryPaths:     []string{filepath.Join("..", "..", "charts", "spiderpool", "crds")},
+		ErrorIfCRDPathMissing: false,
+		WebhookInstallOptions: envtest.WebhookInstallOptions{
+			Paths: []string{filepath.Join("..", "..", "charts", "spiderpool", "webhook")},
+		},
+	}
+
+	cfg, err = testEnv.Start()
+	Expect(err).NotTo(HaveOccurred())
+	Expect(cfg).NotTo(BeNil())
+
+	By("Bootstrapping test environment")
+	scheme := runtime.NewScheme()
+	err = spiderpoolv1.AddToScheme(scheme)
+	Expect(err).NotTo(HaveOccurred())
+
+	err = admissionv1.AddToScheme(scheme)
+	Expect(err).NotTo(HaveOccurred())
+
+	k8sClient, err = client.New(cfg, client.Options{Scheme: scheme})
+	Expect(err).NotTo(HaveOccurred())
+	Expect(k8sClient).NotTo(BeNil())
+
+	By("Starting webhook server using Manager")
+	webhookInstallOptions := &testEnv.WebhookInstallOptions
+	mgr, err := ctrl.NewManager(cfg, ctrl.Options{
+		Scheme:                 scheme,
+		Host:                   webhookInstallOptions.LocalServingHost,
+		Port:                   webhookInstallOptions.LocalServingPort,
+		CertDir:                webhookInstallOptions.LocalServingCertDir,
+		MetricsBindAddress:     "0",
+		HealthProbeBindAddress: "0",
+	})
+	Expect(err).NotTo(HaveOccurred())
+
+	err = (&webhook.IPPoolWebhook{
+		Client: mgr.GetClient(),
+		Scheme: mgr.GetScheme(),
+	}).SetupWebhookWithManager(mgr)
+	Expect(err).NotTo(HaveOccurred())
+
+	go func() {
+		defer GinkgoRecover()
+
+		err = mgr.Start(ctx)
+		Expect(err).NotTo(HaveOccurred())
+	}()
+
+	By("Waitting for the webhook server to get ready")
+	dialer := &net.Dialer{Timeout: time.Second}
+	addrPort := fmt.Sprintf("%s:%d", webhookInstallOptions.LocalServingHost, webhookInstallOptions.LocalServingPort)
+
+	Eventually(func(g Gomega) {
+		conn, err := tls.DialWithDialer(dialer, "tcp", addrPort, &tls.Config{InsecureSkipVerify: true})
+		Expect(err).NotTo(HaveOccurred())
+		conn.Close()
+	}).WithTimeout(10 * time.Second).
+		WithPolling(1 * time.Second).
+		Should(Succeed())
+})
+
+var _ = AfterSuite(func() {
+	By("Tearing down the test environment")
+	err := testEnv.Stop()
+	Expect(err).NotTo(HaveOccurred())
+})
diff --git a/test/e2e/common/deployment.go b/test/e2e/common/deployment.go
index c1937061f..95ada0590 100644
--- a/test/e2e/common/deployment.go
+++ b/test/e2e/common/deployment.go
@@ -93,28 +93,3 @@ func ScaleDeployUntilExpectedReplicas(frame *e2e.Framework, deploy *appsv1.Deplo
 		time.Sleep(time.Second)
 	}
 }
-
-func CreateDeployUntilExpectedReplicas(frame *e2e.Framework, deploy *appsv1.Deployment, ctx context.Context) (pods *corev1.PodList, err error) {
-
-	if frame == nil || deploy == nil {
-		return nil, e2e.ErrWrongInput
-	}
-
-	err = frame.CreateDeployment(deploy)
-	Expect(err).NotTo(HaveOccurred())
-
-	for {
-		select {
-		case <-ctx.Done():
-			return nil, fmt.Errorf("time out to wait expected replicas: %v ", *deploy.Spec.Replicas)
-		default:
-			podList, err := frame.GetPodListByLabel(deploy.Spec.Selector.MatchLabels)
-			Expect(err).NotTo(HaveOccurred())
-			if int32(len(podList.Items)) != *deploy.Spec.Replicas {
-				break
-			}
-			return podList, nil
-		}
-		time.Sleep(time.Second)
-	}
-}
diff --git a/test/e2e/common/reservedip.go b/test/e2e/common/reservedip.go
deleted file mode 100644
index 9ad8aa4fa..000000000
--- a/test/e2e/common/reservedip.go
+++ /dev/null
@@ -1,139 +0,0 @@
-// Copyright 2022 Authors of spidernet-io
-// SPDX-License-Identifier: Apache-2.0
-package common
-
-import (
-	"context"
-	"errors"
-	"time"
-
-	. "github.com/onsi/ginkgo/v2"
-	frame "github.com/spidernet-io/e2eframework/framework"
-	"github.com/spidernet-io/e2eframework/tools"
-	spiderpool "github.com/spidernet-io/spiderpool/pkg/k8s/apis/v1"
-	api_errors "k8s.io/apimachinery/pkg/api/errors"
-	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
-	apitypes "k8s.io/apimachinery/pkg/types"
-	"sigs.k8s.io/controller-runtime/pkg/client"
-)
-
-func CreateReservedIP(f *frame.Framework, ReservedIP *spiderpool.ReservedIP, opts ...client.CreateOption) error {
-	if f == nil || ReservedIP == nil {
-		return frame.ErrWrongInput
-	}
-	// Try to wait for finish last deleting
-	fake := &spiderpool.ReservedIP{
-		ObjectMeta: metav1.ObjectMeta{
-			Name: ReservedIP.ObjectMeta.Name,
-		},
-	}
-	key := client.ObjectKeyFromObject(fake)
-	existing := &spiderpool.ReservedIP{}
-	e := f.GetResource(key, existing)
-	if e == nil && existing.ObjectMeta.DeletionTimestamp == nil {
-		return errors.New("failed to create , a same reservedip exists")
-	} else {
-		t := func() bool {
-			existing := &spiderpool.ReservedIP{}
-			e := f.GetResource(key, existing)
-			b := api_errors.IsNotFound(e)
-			if !b {
-				GinkgoWriter.Printf("waiting for a same reservedIP %v to finish deleting \n", ReservedIP.ObjectMeta.Name)
-				return false
-			}
-			return true
-		}
-		if !tools.Eventually(t, f.Config.ResourceDeleteTimeout, time.Second) {
-			return errors.New("failed to create , a same reservedip exists")
-		}
-	}
-	return f.CreateResource(ReservedIP, opts...)
-}
-
-func DeleteReservedIPByName(f *frame.Framework, reservedIPName string, opts ...client.DeleteOption) error {
-	if reservedIPName == "" || f == nil {
-		return frame.ErrWrongInput
-	}
-	reservedIP := &spiderpool.ReservedIP{
-		ObjectMeta: metav1.ObjectMeta{
-			Name: reservedIPName,
-		},
-	}
-	return f.DeleteResource(reservedIP, opts...)
-}
-
-func GetReservedIPByName(f *frame.Framework, reservedIPName string) *spiderpool.ReservedIP {
-	if reservedIPName == "" || f == nil {
-		return nil
-	}
-
-	v := apitypes.NamespacedName{Name: reservedIPName}
-	existing := &spiderpool.ReservedIP{}
-	e := f.GetResource(v, existing)
-	if e != nil {
-		return nil
-	}
-	return existing
-}
-
-func DeleteResverdIPUntilFinish(ctx context.Context, f *frame.Framework, reservedIPName string, opts ...client.DeleteOption) error {
-	if f == nil || reservedIPName == "" {
-		return frame.ErrWrongInput
-	}
-	err := DeleteReservedIPByName(f, reservedIPName, opts...)
-	if err != nil {
-		return err
-	}
-	for {
-		select {
-		case <-ctx.Done():
-			return frame.ErrTimeOut
-		default:
-			pool := GetReservedIPByName(f, reservedIPName)
-			if pool == nil {
-				return nil
-			}
-			time.Sleep(time.Second)
-		}
-	}
-}
-
-func GenerateExampleV4ReservedIpObject(ips []string) (string, *spiderpool.ReservedIP) {
-	var v4Ipversion = new(spiderpool.IPVersion)
-	var iPv4ReservedIpObj *spiderpool.ReservedIP
-	var v4ReservedIpName string
-
-	*v4Ipversion = spiderpool.IPv4
-	v4ReservedIpName = "v4-sr-" + tools.RandomName()
-
-	iPv4ReservedIpObj = &spiderpool.ReservedIP{
-		ObjectMeta: metav1.ObjectMeta{
-			Name: v4ReservedIpName,
-		},
-		Spec: spiderpool.ReservedIPSpec{
-			IPVersion: v4Ipversion,
-			IPs:       ips,
-		},
-	}
-	return v4ReservedIpName, iPv4ReservedIpObj
-}
-
-func GenerateExampleV6ReservedIpObject(ips []string) (string, *spiderpool.ReservedIP) {
-	var v6Ipversion = new(spiderpool.IPVersion)
-	var iPv6ReservedIpObj *spiderpool.ReservedIP
-	var v6ReservedIpName string
-
-	*v6Ipversion = spiderpool.IPv6
-	v6ReservedIpName = "v6-sr-" + tools.RandomName()
-
-	iPv6ReservedIpObj = &spiderpool.ReservedIP{
-		ObjectMeta: metav1.ObjectMeta{
-			Name: v6ReservedIpName,
-		},
-		Spec: spiderpool.ReservedIPSpec{
-			IPVersion: v6Ipversion,
-			IPs:       ips,
-		},
-	}
-	return v6ReservedIpName, iPv6ReservedIpObj
-}
diff --git a/test/e2e/common/spiderpool.go b/test/e2e/common/spiderpool.go
index 686a7a220..20491d6fe 100644
--- a/test/e2e/common/spiderpool.go
+++ b/test/e2e/common/spiderpool.go
@@ -478,11 +478,7 @@ OUTER_FOR:
 			iPPoolName, iPPoolObj = GenerateExampleIpv6poolObject(ipNum)
 		}
 
-		ips, err := ip.ParseIPRanges(iPPoolObj.Spec.IPs)
-		if err != nil {
-			return nil, err
-		}
-
+		ips := ip.ParseIPRanges(iPPoolObj.Spec.IPs)
 		for _, v := range ips {
 			if d, ok := ipMap[string(v)]; ok {
 				GinkgoWriter.Printf("ippool objects %v and %v have conflicted ip: %v \n", d, iPPoolName, v)
@@ -491,7 +487,7 @@ OUTER_FOR:
 			}
 			ipMap[string(v)] = iPPoolName
 		}
-		err = CreateIppool(f, iPPoolObj)
+		err := CreateIppool(f, iPPoolObj)
 		if err != nil {
 			return nil, err
 		}
diff --git a/test/e2e/reservedip/reservedip_test.go b/test/e2e/reservedip/reservedip_test.go
deleted file mode 100644
index f8ffbb2e7..000000000
--- a/test/e2e/reservedip/reservedip_test.go
+++ /dev/null
@@ -1,153 +0,0 @@
-// Copyright 2022 Authors of spidernet-io
-// SPDX-License-Identifier: Apache-2.0
-package reservedip_test
-
-import (
-	"context"
-	"encoding/json"
-	"time"
-
-	. "github.com/onsi/ginkgo/v2"
-	. "github.com/onsi/gomega"
-	"github.com/spidernet-io/e2eframework/tools"
-	"github.com/spidernet-io/spiderpool/pkg/constant"
-	spiderpool "github.com/spidernet-io/spiderpool/pkg/k8s/apis/v1"
-	"github.com/spidernet-io/spiderpool/pkg/types"
-	"github.com/spidernet-io/spiderpool/test/e2e/common"
-)
-
-var _ = Describe("test reservedIP", Label("reservedIP"), func() {
-	var nsName, DeployName, v4PoolName, v6PoolName, v4ReservedIpName, v6ReservedIpName, nic, podAnnoStr string
-	var v4PoolNameList, v6PoolNameList []string
-	var iPv4PoolObj, iPv6PoolObj *spiderpool.IPPool
-	var v4ReservedIpObj, v6ReservedIpObj *spiderpool.ReservedIP
-	var err error
-
-	BeforeEach(func() {
-		nic = "eth0"
-		//Init namespace name and create
-		nsName = "ns" + tools.RandomName()
-		GinkgoWriter.Printf("Try to create namespace %v \n", nsName)
-		err := frame.CreateNamespace(nsName)
-		Expect(err).NotTo(HaveOccurred(), "Failed to create namespace %v", nsName)
-		// Init test Deployment/Pod name
-		DeployName = "sr-pod" + tools.RandomName()
-
-		if frame.Info.IpV4Enabled {
-			v4PoolName, iPv4PoolObj = common.GenerateExampleIpv4poolObject(3)
-			err := common.CreateIppool(frame, iPv4PoolObj)
-			Expect(err).NotTo(HaveOccurred(), "Failed to create v4 Pool %v \n", v4PoolName)
-			GinkgoWriter.Printf("Successfully created v4 Pool: %v \n", v4PoolName)
-			v4PoolNameList = append(v4PoolNameList, v4PoolName)
-		}
-
-		if frame.Info.IpV6Enabled {
-			v6PoolName, iPv6PoolObj = common.GenerateExampleIpv6poolObject(1)
-			err := common.CreateIppool(frame, iPv6PoolObj)
-			Expect(err).NotTo(HaveOccurred(), "Failed to create v6 Pool %v \n", v6PoolName)
-			GinkgoWriter.Printf("Successfully created v6 Pool: %v \n", v6PoolName)
-			v6PoolNameList = append(v6PoolNameList, v6PoolName)
-		}
-		// Clean test env
-		DeferCleanup(func() {
-			GinkgoWriter.Printf("Delete namespace %v \n", nsName)
-			err = frame.DeleteNamespace(nsName)
-			Expect(err).NotTo(HaveOccurred(), "Failed to delete namespace %v", nsName)
-
-			if frame.Info.IpV4Enabled {
-				err := common.DeleteIPPoolByName(frame, v4PoolName)
-				Expect(err).NotTo(HaveOccurred())
-			}
-			if frame.Info.IpV6Enabled {
-				err := common.DeleteIPPoolByName(frame, v6PoolName)
-				Expect(err).NotTo(HaveOccurred())
-			}
-		})
-	})
-
-	It("an IP who is set in ReservedIP CRD, should not be assigned to a pod", Label("S00001", "smoke"), func() {
-
-		if frame.Info.IpV4Enabled {
-			v4ReservedIpName, v4ReservedIpObj = common.GenerateExampleV4ReservedIpObject(iPv4PoolObj.Spec.IPs)
-			err = common.CreateReservedIP(frame, v4ReservedIpObj)
-			Expect(err).NotTo(HaveOccurred())
-			GinkgoWriter.Printf("Successfully created v4 reservedIP: %v \n", v4ReservedIpName)
-		}
-
-		if frame.Info.IpV6Enabled {
-			v6ReservedIpName, v6ReservedIpObj = common.GenerateExampleV6ReservedIpObject(iPv6PoolObj.Spec.IPs)
-			err = common.CreateReservedIP(frame, v6ReservedIpObj)
-			Expect(err).NotTo(HaveOccurred())
-			GinkgoWriter.Printf("Successfully created v6 reservedIP : %v \n", v6ReservedIpName)
-		}
-
-		podAnno := types.AnnoPodIPPoolValue{
-			NIC:       &nic,
-			IPv4Pools: v4PoolNameList,
-			IPv6Pools: v6PoolNameList,
-		}
-		b, e := json.Marshal(podAnno)
-		Expect(e).NotTo(HaveOccurred())
-		podAnnoStr = string(b)
-
-		// Generate deployment yaml and annotation
-		deployObject := common.GenerateExampleDeploymentYaml(DeployName, nsName, int32(1))
-		deployObject.Spec.Template.Annotations = map[string]string{constant.AnnoPodIPPool: podAnnoStr}
-		GinkgoWriter.Printf("Try to create Deployment: %v/%v with annotation %v = %v \n", nsName, DeployName, constant.AnnoPodIPPool, podAnnoStr)
-		ctx1, cancel1 := context.WithTimeout(context.Background(), time.Minute)
-		defer cancel1()
-		// Try to create a deployment and wait for replicas to meet expectations（because the pod is not running）
-		podlist, err := common.CreateDeployUntilExpectedReplicas(frame, deployObject, ctx1)
-		Expect(err).NotTo(HaveOccurred())
-		Expect(int32(len(podlist.Items))).Should(Equal(*deployObject.Spec.Replicas))
-
-		// Get the Pod creation failure Event
-		ctx, cancel := context.WithTimeout(context.Background(), time.Minute)
-		defer cancel()
-		for _, pod := range podlist.Items {
-			Expect(frame.WaitExceptEventOccurred(ctx, common.PodEventKind, pod.Name, pod.Namespace, common.CNIFailedToSetUpNetwork)).To(Succeed())
-			Expect(err).NotTo(HaveOccurred(), "Failed to get Event %v/%v = %v \n", pod.Namespace, pod.Name, common.CNIFailedToSetUpNetwork)
-			GinkgoWriter.Printf("IP assignment for Deployment/Pod: %v/%v fails when an IP is set in the ReservedIP CRD. \n", pod.Namespace, pod.Name)
-		}
-
-		// Try to delete reservedIP
-		ctx2, cancel2 := context.WithTimeout(context.Background(), time.Minute)
-		defer cancel2()
-		if frame.Info.IpV4Enabled {
-			Expect(common.DeleteResverdIPUntilFinish(ctx2, frame, v4ReservedIpName)).To(Succeed())
-			GinkgoWriter.Printf("Delete v4 reservedIP: %v successfully \n", v4ReservedIpName)
-		}
-		if frame.Info.IpV6Enabled {
-			Expect(common.DeleteResverdIPUntilFinish(ctx2, frame, v6ReservedIpName)).To(Succeed())
-			GinkgoWriter.Printf("Delete v6 reservedIP: %v successfully \n", v6ReservedIpName)
-		}
-
-		// After removing the reservedIP, wait for the Pod to restart until running
-		err = frame.RestartDeploymentPodUntilReady(DeployName, nsName, time.Minute)
-		Expect(err).NotTo(HaveOccurred())
-
-		// Get the list of Deployments/Pod after reboot
-		podlist, err = frame.GetPodListByLabel(deployObject.Spec.Selector.MatchLabels)
-		Expect(err).NotTo(HaveOccurred())
-		Expect(int32(len(podlist.Items))).Should(Equal(*deployObject.Spec.Replicas))
-
-		// Succeeded to assign IPv4、IPv6 ip for Pod
-		err = frame.CheckPodListIpReady(podlist)
-		Expect(err).NotTo(HaveOccurred(), "Failed to check IPv4 or IPv6 ")
-		GinkgoWriter.Printf("Succeeded to assign IPv4、IPv6 ip for Pod %v/%v \n", nsName, DeployName)
-
-		// Check Deployment Pod IP recorded in IPPool
-		ok, _, _, e := common.CheckPodIpRecordInIppool(frame, v4PoolNameList, v6PoolNameList, podlist)
-		Expect(e).NotTo(HaveOccurred(), "Failed to check Pod IP record in IPPool")
-		Expect(ok).To(BeTrue())
-		GinkgoWriter.Printf("Deployment/Pod: %v/%v IP recorded in IPPool %v,%v \n", nsName, DeployName, v4PoolNameList, v6PoolNameList)
-
-		// Try to delete Deployment
-		Expect(frame.DeleteDeploymentUntilFinish(DeployName, nsName, time.Minute)).To(Succeed())
-		GinkgoWriter.Printf("Delete Deployment: %v/%v successfully \n", nsName, DeployName)
-
-		// Check if the Pod IP in IPPool reclaimed normally
-		Expect(common.WaitIPReclaimedFinish(frame, v4PoolNameList, v6PoolNameList, podlist, time.Minute)).To(Succeed())
-		GinkgoWriter.Println("Deployment/Pod IP successfully released")
-	})
-})
diff --git a/vendor/sigs.k8s.io/controller-runtime/pkg/envtest/crd.go b/vendor/sigs.k8s.io/controller-runtime/pkg/envtest/crd.go
new file mode 100644
index 000000000..a15c1daca
--- /dev/null
+++ b/vendor/sigs.k8s.io/controller-runtime/pkg/envtest/crd.go
@@ -0,0 +1,451 @@
+/*
+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 envtest
+
+import (
+	"bufio"
+	"bytes"
+	"context"
+	"fmt"
+	"io"
+	"io/ioutil"
+	"os"
+	"path/filepath"
+	"time"
+
+	apiextensionsv1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1"
+	"k8s.io/apiextensions-apiserver/pkg/client/clientset/clientset"
+	apierrors "k8s.io/apimachinery/pkg/api/errors"
+	"k8s.io/apimachinery/pkg/runtime"
+	"k8s.io/apimachinery/pkg/runtime/schema"
+	"k8s.io/apimachinery/pkg/util/sets"
+	"k8s.io/apimachinery/pkg/util/wait"
+	k8syaml "k8s.io/apimachinery/pkg/util/yaml"
+	"k8s.io/client-go/kubernetes/scheme"
+	"k8s.io/client-go/rest"
+	"k8s.io/client-go/util/retry"
+	"k8s.io/utils/pointer"
+	"sigs.k8s.io/yaml"
+
+	"sigs.k8s.io/controller-runtime/pkg/client"
+	"sigs.k8s.io/controller-runtime/pkg/webhook/conversion"
+)
+
+// CRDInstallOptions are the options for installing CRDs.
+type CRDInstallOptions struct {
+	// Scheme is used to determine if conversion webhooks should be enabled
+	// for a particular CRD / object.
+	//
+	// Conversion webhooks are going to be enabled if an object in the scheme
+	// implements Hub and Spoke conversions.
+	//
+	// If nil, scheme.Scheme is used.
+	Scheme *runtime.Scheme
+
+	// Paths is a list of paths to the directories or files containing CRDs
+	Paths []string
+
+	// CRDs is a list of CRDs to install
+	CRDs []*apiextensionsv1.CustomResourceDefinition
+
+	// ErrorIfPathMissing will cause an error if a Path does not exist
+	ErrorIfPathMissing bool
+
+	// MaxTime is the max time to wait
+	MaxTime time.Duration
+
+	// PollInterval is the interval to check
+	PollInterval time.Duration
+
+	// CleanUpAfterUse will cause the CRDs listed for installation to be
+	// uninstalled when terminating the test environment.
+	// Defaults to false.
+	CleanUpAfterUse bool
+
+	// WebhookOptions contains the conversion webhook information to install
+	// on the CRDs. This field is usually inherited by the EnvTest options.
+	//
+	// If you're passing this field manually, you need to make sure that
+	// the CA information and host port is filled in properly.
+	WebhookOptions WebhookInstallOptions
+}
+
+const defaultPollInterval = 100 * time.Millisecond
+const defaultMaxWait = 10 * time.Second
+
+// InstallCRDs installs a collection of CRDs into a cluster by reading the crd yaml files from a directory.
+func InstallCRDs(config *rest.Config, options CRDInstallOptions) ([]*apiextensionsv1.CustomResourceDefinition, error) {
+	defaultCRDOptions(&options)
+
+	// Read the CRD yamls into options.CRDs
+	if err := readCRDFiles(&options); err != nil {
+		return nil, fmt.Errorf("unable to read CRD files: %w", err)
+	}
+
+	if err := modifyConversionWebhooks(options.CRDs, options.Scheme, options.WebhookOptions); err != nil {
+		return nil, err
+	}
+
+	// Create the CRDs in the apiserver
+	if err := CreateCRDs(config, options.CRDs); err != nil {
+		return options.CRDs, fmt.Errorf("unable to create CRD instances: %w", err)
+	}
+
+	// Wait for the CRDs to appear as Resources in the apiserver
+	if err := WaitForCRDs(config, options.CRDs, options); err != nil {
+		return options.CRDs, fmt.Errorf("something went wrong waiting for CRDs to appear as API resources: %w", err)
+	}
+
+	return options.CRDs, nil
+}
+
+// readCRDFiles reads the directories of CRDs in options.Paths and adds the CRD structs to options.CRDs.
+func readCRDFiles(options *CRDInstallOptions) error {
+	if len(options.Paths) > 0 {
+		crdList, err := renderCRDs(options)
+		if err != nil {
+			return err
+		}
+
+		options.CRDs = append(options.CRDs, crdList...)
+	}
+	return nil
+}
+
+// defaultCRDOptions sets the default values for CRDs.
+func defaultCRDOptions(o *CRDInstallOptions) {
+	if o.Scheme == nil {
+		o.Scheme = scheme.Scheme
+	}
+	if o.MaxTime == 0 {
+		o.MaxTime = defaultMaxWait
+	}
+	if o.PollInterval == 0 {
+		o.PollInterval = defaultPollInterval
+	}
+}
+
+// WaitForCRDs waits for the CRDs to appear in discovery.
+func WaitForCRDs(config *rest.Config, crds []*apiextensionsv1.CustomResourceDefinition, options CRDInstallOptions) error {
+	// Add each CRD to a map of GroupVersion to Resource
+	waitingFor := map[schema.GroupVersion]*sets.String{}
+	for _, crd := range crds {
+		gvs := []schema.GroupVersion{}
+		for _, version := range crd.Spec.Versions {
+			if version.Served {
+				gvs = append(gvs, schema.GroupVersion{Group: crd.Spec.Group, Version: version.Name})
+			}
+		}
+
+		for _, gv := range gvs {
+			log.V(1).Info("adding API in waitlist", "GV", gv)
+			if _, found := waitingFor[gv]; !found {
+				// Initialize the set
+				waitingFor[gv] = &sets.String{}
+			}
+			// Add the Resource
+			waitingFor[gv].Insert(crd.Spec.Names.Plural)
+		}
+	}
+
+	// Poll until all resources are found in discovery
+	p := &poller{config: config, waitingFor: waitingFor}
+	return wait.PollImmediate(options.PollInterval, options.MaxTime, p.poll)
+}
+
+// poller checks if all the resources have been found in discovery, and returns false if not.
+type poller struct {
+	// config is used to get discovery
+	config *rest.Config
+
+	// waitingFor is the map of resources keyed by group version that have not yet been found in discovery
+	waitingFor map[schema.GroupVersion]*sets.String
+}
+
+// poll checks if all the resources have been found in discovery, and returns false if not.
+func (p *poller) poll() (done bool, err error) {
+	// Create a new clientset to avoid any client caching of discovery
+	cs, err := clientset.NewForConfig(p.config)
+	if err != nil {
+		return false, err
+	}
+
+	allFound := true
+	for gv, resources := range p.waitingFor {
+		// All resources found, do nothing
+		if resources.Len() == 0 {
+			delete(p.waitingFor, gv)
+			continue
+		}
+
+		// Get the Resources for this GroupVersion
+		// TODO: Maybe the controller-runtime client should be able to do this...
+		resourceList, err := cs.Discovery().ServerResourcesForGroupVersion(gv.Group + "/" + gv.Version)
+		if err != nil {
+			return false, nil //nolint:nilerr
+		}
+
+		// Remove each found resource from the resources set that we are waiting for
+		for _, resource := range resourceList.APIResources {
+			resources.Delete(resource.Name)
+		}
+
+		// Still waiting on some resources in this group version
+		if resources.Len() != 0 {
+			allFound = false
+		}
+	}
+	return allFound, nil
+}
+
+// UninstallCRDs uninstalls a collection of CRDs by reading the crd yaml files from a directory.
+func UninstallCRDs(config *rest.Config, options CRDInstallOptions) error {
+	// Read the CRD yamls into options.CRDs
+	if err := readCRDFiles(&options); err != nil {
+		return err
+	}
+
+	// Delete the CRDs from the apiserver
+	cs, err := client.New(config, client.Options{})
+	if err != nil {
+		return err
+	}
+
+	// Uninstall each CRD
+	for _, crd := range options.CRDs {
+		crd := crd
+		log.V(1).Info("uninstalling CRD", "crd", crd.GetName())
+		if err := cs.Delete(context.TODO(), crd); err != nil {
+			// If CRD is not found, we can consider success
+			if !apierrors.IsNotFound(err) {
+				return err
+			}
+		}
+	}
+
+	return nil
+}
+
+// CreateCRDs creates the CRDs.
+func CreateCRDs(config *rest.Config, crds []*apiextensionsv1.CustomResourceDefinition) error {
+	cs, err := client.New(config, client.Options{})
+	if err != nil {
+		return fmt.Errorf("unable to create client: %w", err)
+	}
+
+	// Create each CRD
+	for _, crd := range crds {
+		crd := crd
+		log.V(1).Info("installing CRD", "crd", crd.GetName())
+		existingCrd := crd.DeepCopy()
+		err := cs.Get(context.TODO(), client.ObjectKey{Name: crd.GetName()}, existingCrd)
+		switch {
+		case apierrors.IsNotFound(err):
+			if err := cs.Create(context.TODO(), crd); err != nil {
+				return fmt.Errorf("unable to create CRD %q: %w", crd.GetName(), err)
+			}
+		case err != nil:
+			return fmt.Errorf("unable to get CRD %q to check if it exists: %w", crd.GetName(), err)
+		default:
+			log.V(1).Info("CRD already exists, updating", "crd", crd.GetName())
+			if err := retry.RetryOnConflict(retry.DefaultBackoff, func() error {
+				if err := cs.Get(context.TODO(), client.ObjectKey{Name: crd.GetName()}, existingCrd); err != nil {
+					return err
+				}
+				crd.SetResourceVersion(existingCrd.GetResourceVersion())
+				return cs.Update(context.TODO(), crd)
+			}); err != nil {
+				return err
+			}
+		}
+	}
+	return nil
+}
+
+// renderCRDs iterate through options.Paths and extract all CRD files.
+func renderCRDs(options *CRDInstallOptions) ([]*apiextensionsv1.CustomResourceDefinition, error) {
+	var (
+		err   error
+		info  os.FileInfo
+		files []os.FileInfo
+	)
+
+	type GVKN struct {
+		GVK  schema.GroupVersionKind
+		Name string
+	}
+
+	crds := map[GVKN]*apiextensionsv1.CustomResourceDefinition{}
+
+	for _, path := range options.Paths {
+		var filePath = path
+
+		// Return the error if ErrorIfPathMissing exists
+		if info, err = os.Stat(path); os.IsNotExist(err) {
+			if options.ErrorIfPathMissing {
+				return nil, err
+			}
+			continue
+		}
+
+		if !info.IsDir() {
+			filePath, files = filepath.Dir(path), []os.FileInfo{info}
+		} else if files, err = ioutil.ReadDir(path); err != nil {
+			return nil, err
+		}
+
+		log.V(1).Info("reading CRDs from path", "path", path)
+		crdList, err := readCRDs(filePath, files)
+		if err != nil {
+			return nil, err
+		}
+
+		for i, crd := range crdList {
+			gvkn := GVKN{GVK: crd.GroupVersionKind(), Name: crd.GetName()}
+			if _, found := crds[gvkn]; found {
+				// Currently, we only print a log when there are duplicates. We may want to error out if that makes more sense.
+				log.Info("there are more than one CRD definitions with the same <Group, Version, Kind, Name>", "GVKN", gvkn)
+			}
+			// We always use the CRD definition that we found last.
+			crds[gvkn] = crdList[i]
+		}
+	}
+
+	// Converting map to a list to return
+	res := []*apiextensionsv1.CustomResourceDefinition{}
+	for _, obj := range crds {
+		res = append(res, obj)
+	}
+	return res, nil
+}
+
+// modifyConversionWebhooks takes all the registered CustomResourceDefinitions and applies modifications
+// to conditionally enable webhooks if the type is registered within the scheme.
+func modifyConversionWebhooks(crds []*apiextensionsv1.CustomResourceDefinition, scheme *runtime.Scheme, webhookOptions WebhookInstallOptions) error {
+	if len(webhookOptions.LocalServingCAData) == 0 {
+		return nil
+	}
+
+	// Determine all registered convertible types.
+	convertibles := map[schema.GroupKind]struct{}{}
+	for gvk := range scheme.AllKnownTypes() {
+		obj, err := scheme.New(gvk)
+		if err != nil {
+			return err
+		}
+		if ok, err := conversion.IsConvertible(scheme, obj); ok && err == nil {
+			convertibles[gvk.GroupKind()] = struct{}{}
+		}
+	}
+
+	// generate host port.
+	hostPort, err := webhookOptions.generateHostPort()
+	if err != nil {
+		return err
+	}
+	url := pointer.StringPtr(fmt.Sprintf("https://%s/convert", hostPort))
+
+	for i := range crds {
+		// Continue if we're preserving unknown fields.
+		if crds[i].Spec.PreserveUnknownFields {
+			continue
+		}
+		// Continue if the GroupKind isn't registered as being convertible.
+		if _, ok := convertibles[schema.GroupKind{
+			Group: crds[i].Spec.Group,
+			Kind:  crds[i].Spec.Names.Kind,
+		}]; !ok {
+			continue
+		}
+		if crds[i].Spec.Conversion == nil {
+			crds[i].Spec.Conversion = &apiextensionsv1.CustomResourceConversion{
+				Webhook: &apiextensionsv1.WebhookConversion{},
+			}
+		}
+		crds[i].Spec.Conversion.Strategy = apiextensionsv1.WebhookConverter
+		crds[i].Spec.Conversion.Webhook.ConversionReviewVersions = []string{"v1", "v1beta1"}
+		crds[i].Spec.Conversion.Webhook.ClientConfig = &apiextensionsv1.WebhookClientConfig{
+			Service:  nil,
+			URL:      url,
+			CABundle: webhookOptions.LocalServingCAData,
+		}
+	}
+
+	return nil
+}
+
+// readCRDs reads the CRDs from files and Unmarshals them into structs.
+func readCRDs(basePath string, files []os.FileInfo) ([]*apiextensionsv1.CustomResourceDefinition, error) {
+	var crds []*apiextensionsv1.CustomResourceDefinition
+
+	// White list the file extensions that may contain CRDs
+	crdExts := sets.NewString(".json", ".yaml", ".yml")
+
+	for _, file := range files {
+		// Only parse allowlisted file types
+		if !crdExts.Has(filepath.Ext(file.Name())) {
+			continue
+		}
+
+		// Unmarshal CRDs from file into structs
+		docs, err := readDocuments(filepath.Join(basePath, file.Name()))
+		if err != nil {
+			return nil, err
+		}
+
+		for _, doc := range docs {
+			crd := &apiextensionsv1.CustomResourceDefinition{}
+			if err = yaml.Unmarshal(doc, crd); err != nil {
+				return nil, err
+			}
+
+			if crd.Kind != "CustomResourceDefinition" || crd.Spec.Names.Kind == "" || crd.Spec.Group == "" {
+				continue
+			}
+			crds = append(crds, crd)
+		}
+
+		log.V(1).Info("read CRDs from file", "file", file.Name())
+	}
+	return crds, nil
+}
+
+// readDocuments reads documents from file.
+func readDocuments(fp string) ([][]byte, error) {
+	b, err := ioutil.ReadFile(fp) //nolint:gosec
+	if err != nil {
+		return nil, err
+	}
+
+	docs := [][]byte{}
+	reader := k8syaml.NewYAMLReader(bufio.NewReader(bytes.NewReader(b)))
+	for {
+		// Read document
+		doc, err := reader.Read()
+		if err != nil {
+			if err == io.EOF {
+				break
+			}
+
+			return nil, err
+		}
+
+		docs = append(docs, doc)
+	}
+
+	return docs, nil
+}
diff --git a/vendor/sigs.k8s.io/controller-runtime/pkg/envtest/doc.go b/vendor/sigs.k8s.io/controller-runtime/pkg/envtest/doc.go
new file mode 100644
index 000000000..412e794cc
--- /dev/null
+++ b/vendor/sigs.k8s.io/controller-runtime/pkg/envtest/doc.go
@@ -0,0 +1,26 @@
+/*
+Copyright 2017 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 envtest provides libraries for integration testing by starting a local control plane
+//
+// Control plane binaries (etcd and kube-apiserver) are loaded by default from
+// /usr/local/kubebuilder/bin.  This can be overridden by setting the
+// KUBEBUILDER_ASSETS environment variable, or by directly creating a
+// ControlPlane for the Environment to use.
+//
+// Environment can also be configured to work with an existing cluster, and
+// simply load CRDs and provide client configuration.
+package envtest
diff --git a/vendor/sigs.k8s.io/controller-runtime/pkg/envtest/helper.go b/vendor/sigs.k8s.io/controller-runtime/pkg/envtest/helper.go
new file mode 100644
index 000000000..d3b52017d
--- /dev/null
+++ b/vendor/sigs.k8s.io/controller-runtime/pkg/envtest/helper.go
@@ -0,0 +1,69 @@
+/*
+Copyright 2021 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 envtest
+
+import (
+	apiextensionsv1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1"
+	"k8s.io/client-go/kubernetes/scheme"
+)
+
+var (
+	crdScheme = scheme.Scheme
+)
+
+// init is required to correctly initialize the crdScheme package variable.
+func init() {
+	_ = apiextensionsv1.AddToScheme(crdScheme)
+}
+
+// mergePaths merges two string slices containing paths.
+// This function makes no guarantees about order of the merged slice.
+func mergePaths(s1, s2 []string) []string {
+	m := make(map[string]struct{})
+	for _, s := range s1 {
+		m[s] = struct{}{}
+	}
+	for _, s := range s2 {
+		m[s] = struct{}{}
+	}
+	merged := make([]string, len(m))
+	i := 0
+	for key := range m {
+		merged[i] = key
+		i++
+	}
+	return merged
+}
+
+// mergeCRDs merges two CRD slices using their names.
+// This function makes no guarantees about order of the merged slice.
+func mergeCRDs(s1, s2 []*apiextensionsv1.CustomResourceDefinition) []*apiextensionsv1.CustomResourceDefinition {
+	m := make(map[string]*apiextensionsv1.CustomResourceDefinition)
+	for _, obj := range s1 {
+		m[obj.GetName()] = obj
+	}
+	for _, obj := range s2 {
+		m[obj.GetName()] = obj
+	}
+	merged := make([]*apiextensionsv1.CustomResourceDefinition, len(m))
+	i := 0
+	for _, obj := range m {
+		merged[i] = obj.DeepCopy()
+		i++
+	}
+	return merged
+}
diff --git a/vendor/sigs.k8s.io/controller-runtime/pkg/envtest/server.go b/vendor/sigs.k8s.io/controller-runtime/pkg/envtest/server.go
new file mode 100644
index 000000000..5347f074d
--- /dev/null
+++ b/vendor/sigs.k8s.io/controller-runtime/pkg/envtest/server.go
@@ -0,0 +1,375 @@
+/*
+Copyright 2016 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 envtest
+
+import (
+	"fmt"
+	"os"
+	"strings"
+	"time"
+
+	apiextensionsv1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1"
+	"k8s.io/apimachinery/pkg/runtime"
+	"k8s.io/client-go/kubernetes/scheme"
+	"k8s.io/client-go/rest"
+
+	"sigs.k8s.io/controller-runtime/pkg/client/config"
+	logf "sigs.k8s.io/controller-runtime/pkg/internal/log"
+	"sigs.k8s.io/controller-runtime/pkg/internal/testing/controlplane"
+	"sigs.k8s.io/controller-runtime/pkg/internal/testing/process"
+)
+
+var log = logf.RuntimeLog.WithName("test-env")
+
+/*
+It's possible to override some defaults, by setting the following environment variables:
+	USE_EXISTING_CLUSTER (boolean): if set to true, envtest will use an existing cluster
+	TEST_ASSET_KUBE_APISERVER (string): path to the api-server binary to use
+	TEST_ASSET_ETCD (string): path to the etcd binary to use
+	TEST_ASSET_KUBECTL (string): path to the kubectl binary to use
+	KUBEBUILDER_ASSETS (string): directory containing the binaries to use (api-server, etcd and kubectl). Defaults to /usr/local/kubebuilder/bin.
+	KUBEBUILDER_CONTROLPLANE_START_TIMEOUT (string supported by time.ParseDuration): timeout for test control plane to start. Defaults to 20s.
+	KUBEBUILDER_CONTROLPLANE_STOP_TIMEOUT (string supported by time.ParseDuration): timeout for test control plane to start. Defaults to 20s.
+	KUBEBUILDER_ATTACH_CONTROL_PLANE_OUTPUT (boolean): if set to true, the control plane's stdout and stderr are attached to os.Stdout and os.Stderr
+
+*/
+const (
+	envUseExistingCluster = "USE_EXISTING_CLUSTER"
+	envStartTimeout       = "KUBEBUILDER_CONTROLPLANE_START_TIMEOUT"
+	envStopTimeout        = "KUBEBUILDER_CONTROLPLANE_STOP_TIMEOUT"
+	envAttachOutput       = "KUBEBUILDER_ATTACH_CONTROL_PLANE_OUTPUT"
+	StartTimeout          = 60
+	StopTimeout           = 60
+
+	defaultKubebuilderControlPlaneStartTimeout = 20 * time.Second
+	defaultKubebuilderControlPlaneStopTimeout  = 20 * time.Second
+)
+
+// internal types we expose as part of our public API.
+type (
+	// ControlPlane is the re-exported ControlPlane type from the internal testing package.
+	ControlPlane = controlplane.ControlPlane
+
+	// APIServer is the re-exported APIServer from the internal testing package.
+	APIServer = controlplane.APIServer
+
+	// Etcd is the re-exported Etcd from the internal testing package.
+	Etcd = controlplane.Etcd
+
+	// User represents a Kubernetes user to provision for auth purposes.
+	User = controlplane.User
+
+	// AuthenticatedUser represets a Kubernetes user that's been provisioned.
+	AuthenticatedUser = controlplane.AuthenticatedUser
+
+	// ListenAddr indicates the address and port that the API server should listen on.
+	ListenAddr = process.ListenAddr
+
+	// SecureServing contains details describing how the API server should serve
+	// its secure endpoint.
+	SecureServing = controlplane.SecureServing
+
+	// Authn is an authentication method that can be used with the control plane to
+	// provision users.
+	Authn = controlplane.Authn
+
+	// Arguments allows configuring a process's flags.
+	Arguments = process.Arguments
+
+	// Arg is a single flag with one or more values.
+	Arg = process.Arg
+)
+
+var (
+	// EmptyArguments constructs a new set of flags with nothing set.
+	//
+	// This is mostly useful for testing helper methods -- you'll want to call
+	// Configure on the APIServer (or etcd) to configure their arguments.
+	EmptyArguments = process.EmptyArguments
+)
+
+// Environment creates a Kubernetes test environment that will start / stop the Kubernetes control plane and
+// install extension APIs.
+type Environment struct {
+	// ControlPlane is the ControlPlane including the apiserver and etcd
+	ControlPlane controlplane.ControlPlane
+
+	// Scheme is used to determine if conversion webhooks should be enabled
+	// for a particular CRD / object.
+	//
+	// Conversion webhooks are going to be enabled if an object in the scheme
+	// implements Hub and Spoke conversions.
+	//
+	// If nil, scheme.Scheme is used.
+	Scheme *runtime.Scheme
+
+	// Config can be used to talk to the apiserver.  It's automatically
+	// populated if not set using the standard controller-runtime config
+	// loading.
+	Config *rest.Config
+
+	// CRDInstallOptions are the options for installing CRDs.
+	CRDInstallOptions CRDInstallOptions
+
+	// WebhookInstallOptions are the options for installing webhooks.
+	WebhookInstallOptions WebhookInstallOptions
+
+	// ErrorIfCRDPathMissing provides an interface for the underlying
+	// CRDInstallOptions.ErrorIfPathMissing. It prevents silent failures
+	// for missing CRD paths.
+	ErrorIfCRDPathMissing bool
+
+	// CRDs is a list of CRDs to install.
+	// If both this field and CRDs field in CRDInstallOptions are specified, the
+	// values are merged.
+	CRDs []*apiextensionsv1.CustomResourceDefinition
+
+	// CRDDirectoryPaths is a list of paths containing CRD yaml or json configs.
+	// If both this field and Paths field in CRDInstallOptions are specified, the
+	// values are merged.
+	CRDDirectoryPaths []string
+
+	// BinaryAssetsDirectory is the path where the binaries required for the envtest are
+	// located in the local environment. This field can be overridden by setting KUBEBUILDER_ASSETS.
+	BinaryAssetsDirectory string
+
+	// UseExistingCluster indicates that this environments should use an
+	// existing kubeconfig, instead of trying to stand up a new control plane.
+	// This is useful in cases that need aggregated API servers and the like.
+	UseExistingCluster *bool
+
+	// ControlPlaneStartTimeout is the maximum duration each controlplane component
+	// may take to start. It defaults to the KUBEBUILDER_CONTROLPLANE_START_TIMEOUT
+	// environment variable or 20 seconds if unspecified
+	ControlPlaneStartTimeout time.Duration
+
+	// ControlPlaneStopTimeout is the maximum duration each controlplane component
+	// may take to stop. It defaults to the KUBEBUILDER_CONTROLPLANE_STOP_TIMEOUT
+	// environment variable or 20 seconds if unspecified
+	ControlPlaneStopTimeout time.Duration
+
+	// KubeAPIServerFlags is the set of flags passed while starting the api server.
+	//
+	// Deprecated: use ControlPlane.GetAPIServer().Configure() instead.
+	KubeAPIServerFlags []string
+
+	// AttachControlPlaneOutput indicates if control plane output will be attached to os.Stdout and os.Stderr.
+	// Enable this to get more visibility of the testing control plane.
+	// It respect KUBEBUILDER_ATTACH_CONTROL_PLANE_OUTPUT environment variable.
+	AttachControlPlaneOutput bool
+}
+
+// Stop stops a running server.
+// Previously installed CRDs, as listed in CRDInstallOptions.CRDs, will be uninstalled
+// if CRDInstallOptions.CleanUpAfterUse are set to true.
+func (te *Environment) Stop() error {
+	if te.CRDInstallOptions.CleanUpAfterUse {
+		if err := UninstallCRDs(te.Config, te.CRDInstallOptions); err != nil {
+			return err
+		}
+	}
+
+	if err := te.WebhookInstallOptions.Cleanup(); err != nil {
+		return err
+	}
+
+	if te.useExistingCluster() {
+		return nil
+	}
+
+	return te.ControlPlane.Stop()
+}
+
+// Start starts a local Kubernetes server and updates te.ApiserverPort with the port it is listening on.
+func (te *Environment) Start() (*rest.Config, error) {
+	if te.useExistingCluster() {
+		log.V(1).Info("using existing cluster")
+		if te.Config == nil {
+			// we want to allow people to pass in their own config, so
+			// only load a config if it hasn't already been set.
+			log.V(1).Info("automatically acquiring client configuration")
+
+			var err error
+			te.Config, err = config.GetConfig()
+			if err != nil {
+				return nil, fmt.Errorf("unable to get configuration for existing cluster: %w", err)
+			}
+		}
+	} else {
+		apiServer := te.ControlPlane.GetAPIServer()
+		if len(apiServer.Args) == 0 { //nolint:staticcheck
+			// pass these through separately from above in case something like
+			// AddUser defaults APIServer.
+			//
+			// TODO(directxman12): if/when we feel like making a bigger
+			// breaking change here, just make APIServer and Etcd non-pointers
+			// in ControlPlane.
+
+			// NB(directxman12): we still pass these in so that things work if the
+			// user manually specifies them, but in most cases we expect them to
+			// be nil so that we use the new .Configure() logic.
+			apiServer.Args = te.KubeAPIServerFlags //nolint:staticcheck
+		}
+		if te.ControlPlane.Etcd == nil {
+			te.ControlPlane.Etcd = &controlplane.Etcd{}
+		}
+
+		if os.Getenv(envAttachOutput) == "true" {
+			te.AttachControlPlaneOutput = true
+		}
+		if apiServer.Out == nil && te.AttachControlPlaneOutput {
+			apiServer.Out = os.Stdout
+		}
+		if apiServer.Err == nil && te.AttachControlPlaneOutput {
+			apiServer.Err = os.Stderr
+		}
+		if te.ControlPlane.Etcd.Out == nil && te.AttachControlPlaneOutput {
+			te.ControlPlane.Etcd.Out = os.Stdout
+		}
+		if te.ControlPlane.Etcd.Err == nil && te.AttachControlPlaneOutput {
+			te.ControlPlane.Etcd.Err = os.Stderr
+		}
+
+		apiServer.Path = process.BinPathFinder("kube-apiserver", te.BinaryAssetsDirectory)
+		te.ControlPlane.Etcd.Path = process.BinPathFinder("etcd", te.BinaryAssetsDirectory)
+		te.ControlPlane.KubectlPath = process.BinPathFinder("kubectl", te.BinaryAssetsDirectory)
+
+		if err := te.defaultTimeouts(); err != nil {
+			return nil, fmt.Errorf("failed to default controlplane timeouts: %w", err)
+		}
+		te.ControlPlane.Etcd.StartTimeout = te.ControlPlaneStartTimeout
+		te.ControlPlane.Etcd.StopTimeout = te.ControlPlaneStopTimeout
+		apiServer.StartTimeout = te.ControlPlaneStartTimeout
+		apiServer.StopTimeout = te.ControlPlaneStopTimeout
+
+		log.V(1).Info("starting control plane")
+		if err := te.startControlPlane(); err != nil {
+			return nil, fmt.Errorf("unable to start control plane itself: %w", err)
+		}
+
+		// Create the *rest.Config for creating new clients
+		baseConfig := &rest.Config{
+			// gotta go fast during tests -- we don't really care about overwhelming our test API server
+			QPS:   1000.0,
+			Burst: 2000.0,
+		}
+
+		adminInfo := User{Name: "admin", Groups: []string{"system:masters"}}
+		adminUser, err := te.ControlPlane.AddUser(adminInfo, baseConfig)
+		if err != nil {
+			return te.Config, fmt.Errorf("unable to provision admin user: %w", err)
+		}
+		te.Config = adminUser.Config()
+	}
+
+	// Set the default scheme if nil.
+	if te.Scheme == nil {
+		te.Scheme = scheme.Scheme
+	}
+
+	// Call PrepWithoutInstalling to setup certificates first
+	// and have them available to patch CRD conversion webhook as well.
+	if err := te.WebhookInstallOptions.PrepWithoutInstalling(); err != nil {
+		return nil, err
+	}
+
+	log.V(1).Info("installing CRDs")
+	te.CRDInstallOptions.CRDs = mergeCRDs(te.CRDInstallOptions.CRDs, te.CRDs)
+	te.CRDInstallOptions.Paths = mergePaths(te.CRDInstallOptions.Paths, te.CRDDirectoryPaths)
+	te.CRDInstallOptions.ErrorIfPathMissing = te.ErrorIfCRDPathMissing
+	te.CRDInstallOptions.WebhookOptions = te.WebhookInstallOptions
+	crds, err := InstallCRDs(te.Config, te.CRDInstallOptions)
+	if err != nil {
+		return te.Config, fmt.Errorf("unable to install CRDs onto control plane: %w", err)
+	}
+	te.CRDs = crds
+
+	log.V(1).Info("installing webhooks")
+	if err := te.WebhookInstallOptions.Install(te.Config); err != nil {
+		return nil, fmt.Errorf("unable to install webhooks onto control plane: %w", err)
+	}
+	return te.Config, nil
+}
+
+// AddUser provisions a new user for connecting to this Environment.  The user will
+// have the specified name & belong to the specified groups.
+//
+// If you specify a "base" config, the returned REST Config will contain those
+// settings as well as any required by the authentication method.  You can use
+// this to easily specify options like QPS.
+//
+// This is effectively a convinience alias for ControlPlane.AddUser -- see that
+// for more low-level details.
+func (te *Environment) AddUser(user User, baseConfig *rest.Config) (*AuthenticatedUser, error) {
+	return te.ControlPlane.AddUser(user, baseConfig)
+}
+
+func (te *Environment) startControlPlane() error {
+	numTries, maxRetries := 0, 5
+	var err error
+	for ; numTries < maxRetries; numTries++ {
+		// Start the control plane - retry if it fails
+		err = te.ControlPlane.Start()
+		if err == nil {
+			break
+		}
+		log.Error(err, "unable to start the controlplane", "tries", numTries)
+	}
+	if numTries == maxRetries {
+		return fmt.Errorf("failed to start the controlplane. retried %d times: %w", numTries, err)
+	}
+	return nil
+}
+
+func (te *Environment) defaultTimeouts() error {
+	var err error
+	if te.ControlPlaneStartTimeout == 0 {
+		if envVal := os.Getenv(envStartTimeout); envVal != "" {
+			te.ControlPlaneStartTimeout, err = time.ParseDuration(envVal)
+			if err != nil {
+				return err
+			}
+		} else {
+			te.ControlPlaneStartTimeout = defaultKubebuilderControlPlaneStartTimeout
+		}
+	}
+
+	if te.ControlPlaneStopTimeout == 0 {
+		if envVal := os.Getenv(envStopTimeout); envVal != "" {
+			te.ControlPlaneStopTimeout, err = time.ParseDuration(envVal)
+			if err != nil {
+				return err
+			}
+		} else {
+			te.ControlPlaneStopTimeout = defaultKubebuilderControlPlaneStopTimeout
+		}
+	}
+	return nil
+}
+
+func (te *Environment) useExistingCluster() bool {
+	if te.UseExistingCluster == nil {
+		return strings.ToLower(os.Getenv(envUseExistingCluster)) == "true"
+	}
+	return *te.UseExistingCluster
+}
+
+// DefaultKubeAPIServerFlags exposes the default args for the APIServer so that
+// you can use those to append your own additional arguments.
+//
+// Deprecated: use APIServer.Configure() instead.
+var DefaultKubeAPIServerFlags = controlplane.APIServerDefaultArgs //nolint:staticcheck
diff --git a/vendor/sigs.k8s.io/controller-runtime/pkg/envtest/webhook.go b/vendor/sigs.k8s.io/controller-runtime/pkg/envtest/webhook.go
new file mode 100644
index 000000000..8552d3ba6
--- /dev/null
+++ b/vendor/sigs.k8s.io/controller-runtime/pkg/envtest/webhook.go
@@ -0,0 +1,428 @@
+/*
+Copyright 2019 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 envtest
+
+import (
+	"context"
+	"fmt"
+	"io/ioutil"
+	"net"
+	"os"
+	"path/filepath"
+	"time"
+
+	admissionv1 "k8s.io/api/admissionregistration/v1"
+	apierrors "k8s.io/apimachinery/pkg/api/errors"
+	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
+	"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
+	"k8s.io/apimachinery/pkg/runtime/schema"
+	"k8s.io/apimachinery/pkg/util/sets"
+	"k8s.io/apimachinery/pkg/util/wait"
+	"k8s.io/client-go/kubernetes/scheme"
+	"k8s.io/client-go/rest"
+	"sigs.k8s.io/controller-runtime/pkg/client/apiutil"
+	"sigs.k8s.io/yaml"
+
+	"sigs.k8s.io/controller-runtime/pkg/client"
+	"sigs.k8s.io/controller-runtime/pkg/internal/testing/addr"
+	"sigs.k8s.io/controller-runtime/pkg/internal/testing/certs"
+)
+
+// WebhookInstallOptions are the options for installing mutating or validating webhooks.
+type WebhookInstallOptions struct {
+	// Paths is a list of paths to the directories or files containing the mutating or validating webhooks yaml or json configs.
+	Paths []string
+
+	// MutatingWebhooks is a list of MutatingWebhookConfigurations to install
+	MutatingWebhooks []*admissionv1.MutatingWebhookConfiguration
+
+	// ValidatingWebhooks is a list of ValidatingWebhookConfigurations to install
+	ValidatingWebhooks []*admissionv1.ValidatingWebhookConfiguration
+
+	// IgnoreErrorIfPathMissing will ignore an error if a DirectoryPath does not exist when set to true
+	IgnoreErrorIfPathMissing bool
+
+	// LocalServingHost is the host for serving webhooks on.
+	// it will be automatically populated
+	LocalServingHost string
+
+	// LocalServingPort is the allocated port for serving webhooks on.
+	// it will be automatically populated by a random available local port
+	LocalServingPort int
+
+	// LocalServingCertDir is the allocated directory for serving certificates.
+	// it will be automatically populated by the local temp dir
+	LocalServingCertDir string
+
+	// CAData is the CA that can be used to trust the serving certificates in LocalServingCertDir.
+	LocalServingCAData []byte
+
+	// LocalServingHostExternalName is the hostname to use to reach the webhook server.
+	LocalServingHostExternalName string
+
+	// MaxTime is the max time to wait
+	MaxTime time.Duration
+
+	// PollInterval is the interval to check
+	PollInterval time.Duration
+}
+
+// ModifyWebhookDefinitions modifies webhook definitions by:
+// - applying CABundle based on the provided tinyca
+// - if webhook client config uses service spec, it's removed and replaced with direct url.
+func (o *WebhookInstallOptions) ModifyWebhookDefinitions() error {
+	caData := o.LocalServingCAData
+
+	// generate host port.
+	hostPort, err := o.generateHostPort()
+	if err != nil {
+		return err
+	}
+
+	for i := range o.MutatingWebhooks {
+		for j := range o.MutatingWebhooks[i].Webhooks {
+			updateClientConfig(&o.MutatingWebhooks[i].Webhooks[j].ClientConfig, hostPort, caData)
+		}
+	}
+
+	for i := range o.ValidatingWebhooks {
+		for j := range o.ValidatingWebhooks[i].Webhooks {
+			updateClientConfig(&o.ValidatingWebhooks[i].Webhooks[j].ClientConfig, hostPort, caData)
+		}
+	}
+	return nil
+}
+
+func updateClientConfig(cc *admissionv1.WebhookClientConfig, hostPort string, caData []byte) {
+	cc.CABundle = caData
+	if cc.Service != nil && cc.Service.Path != nil {
+		url := fmt.Sprintf("https://%s/%s", hostPort, *cc.Service.Path)
+		cc.URL = &url
+		cc.Service = nil
+	}
+}
+
+func (o *WebhookInstallOptions) generateHostPort() (string, error) {
+	if o.LocalServingPort == 0 {
+		port, host, err := addr.Suggest(o.LocalServingHost)
+		if err != nil {
+			return "", fmt.Errorf("unable to grab random port for serving webhooks on: %v", err)
+		}
+		o.LocalServingPort = port
+		o.LocalServingHost = host
+	}
+	host := o.LocalServingHostExternalName
+	if host == "" {
+		host = o.LocalServingHost
+	}
+	return net.JoinHostPort(host, fmt.Sprintf("%d", o.LocalServingPort)), nil
+}
+
+// PrepWithoutInstalling does the setup parts of Install (populating host-port,
+// setting up CAs, etc), without actually truing to do anything with webhook
+// definitions.  This is largely useful for internal testing of
+// controller-runtime, where we need a random host-port & caData for webhook
+// tests, but may be useful in similar scenarios.
+func (o *WebhookInstallOptions) PrepWithoutInstalling() error {
+	if err := o.setupCA(); err != nil {
+		return err
+	}
+
+	if err := parseWebhook(o); err != nil {
+		return err
+	}
+
+	return o.ModifyWebhookDefinitions()
+}
+
+// Install installs specified webhooks to the API server.
+func (o *WebhookInstallOptions) Install(config *rest.Config) error {
+	if len(o.LocalServingCAData) == 0 {
+		if err := o.PrepWithoutInstalling(); err != nil {
+			return err
+		}
+	}
+
+	if err := createWebhooks(config, o.MutatingWebhooks, o.ValidatingWebhooks); err != nil {
+		return err
+	}
+
+	return WaitForWebhooks(config, o.MutatingWebhooks, o.ValidatingWebhooks, *o)
+}
+
+// Cleanup cleans up cert directories.
+func (o *WebhookInstallOptions) Cleanup() error {
+	if o.LocalServingCertDir != "" {
+		return os.RemoveAll(o.LocalServingCertDir)
+	}
+	return nil
+}
+
+// WaitForWebhooks waits for the Webhooks to be available through API server.
+func WaitForWebhooks(config *rest.Config,
+	mutatingWebhooks []*admissionv1.MutatingWebhookConfiguration,
+	validatingWebhooks []*admissionv1.ValidatingWebhookConfiguration,
+	options WebhookInstallOptions) error {
+	waitingFor := map[schema.GroupVersionKind]*sets.String{}
+
+	for _, hook := range mutatingWebhooks {
+		h := hook
+		gvk, err := apiutil.GVKForObject(h, scheme.Scheme)
+		if err != nil {
+			return fmt.Errorf("unable to get gvk for MutatingWebhookConfiguration %s: %v", hook.GetName(), err)
+		}
+
+		if _, ok := waitingFor[gvk]; !ok {
+			waitingFor[gvk] = &sets.String{}
+		}
+		waitingFor[gvk].Insert(h.GetName())
+	}
+
+	for _, hook := range validatingWebhooks {
+		h := hook
+		gvk, err := apiutil.GVKForObject(h, scheme.Scheme)
+		if err != nil {
+			return fmt.Errorf("unable to get gvk for ValidatingWebhookConfiguration %s: %v", hook.GetName(), err)
+		}
+
+		if _, ok := waitingFor[gvk]; !ok {
+			waitingFor[gvk] = &sets.String{}
+		}
+		waitingFor[gvk].Insert(hook.GetName())
+	}
+
+	// Poll until all resources are found in discovery
+	p := &webhookPoller{config: config, waitingFor: waitingFor}
+	return wait.PollImmediate(options.PollInterval, options.MaxTime, p.poll)
+}
+
+// poller checks if all the resources have been found in discovery, and returns false if not.
+type webhookPoller struct {
+	// config is used to get discovery
+	config *rest.Config
+
+	// waitingFor is the map of resources keyed by group version that have not yet been found in discovery
+	waitingFor map[schema.GroupVersionKind]*sets.String
+}
+
+// poll checks if all the resources have been found in discovery, and returns false if not.
+func (p *webhookPoller) poll() (done bool, err error) {
+	// Create a new clientset to avoid any client caching of discovery
+	c, err := client.New(p.config, client.Options{})
+	if err != nil {
+		return false, err
+	}
+
+	allFound := true
+	for gvk, names := range p.waitingFor {
+		if names.Len() == 0 {
+			delete(p.waitingFor, gvk)
+			continue
+		}
+		for _, name := range names.List() {
+			var obj = &unstructured.Unstructured{}
+			obj.SetGroupVersionKind(gvk)
+			err := c.Get(context.Background(), client.ObjectKey{
+				Namespace: "",
+				Name:      name,
+			}, obj)
+
+			if err == nil {
+				names.Delete(name)
+			}
+
+			if apierrors.IsNotFound(err) {
+				allFound = false
+			}
+			if err != nil {
+				return false, err
+			}
+		}
+	}
+	return allFound, nil
+}
+
+// setupCA creates CA for testing and writes them to disk.
+func (o *WebhookInstallOptions) setupCA() error {
+	hookCA, err := certs.NewTinyCA()
+	if err != nil {
+		return fmt.Errorf("unable to set up webhook CA: %v", err)
+	}
+
+	names := []string{"localhost", o.LocalServingHost, o.LocalServingHostExternalName}
+	hookCert, err := hookCA.NewServingCert(names...)
+	if err != nil {
+		return fmt.Errorf("unable to set up webhook serving certs: %v", err)
+	}
+
+	localServingCertsDir, err := ioutil.TempDir("", "envtest-serving-certs-")
+	o.LocalServingCertDir = localServingCertsDir
+	if err != nil {
+		return fmt.Errorf("unable to create directory for webhook serving certs: %v", err)
+	}
+
+	certData, keyData, err := hookCert.AsBytes()
+	if err != nil {
+		return fmt.Errorf("unable to marshal webhook serving certs: %v", err)
+	}
+
+	if err := ioutil.WriteFile(filepath.Join(localServingCertsDir, "tls.crt"), certData, 0640); err != nil { //nolint:gosec
+		return fmt.Errorf("unable to write webhook serving cert to disk: %v", err)
+	}
+	if err := ioutil.WriteFile(filepath.Join(localServingCertsDir, "tls.key"), keyData, 0640); err != nil { //nolint:gosec
+		return fmt.Errorf("unable to write webhook serving key to disk: %v", err)
+	}
+
+	o.LocalServingCAData = certData
+	return err
+}
+
+func createWebhooks(config *rest.Config, mutHooks []*admissionv1.MutatingWebhookConfiguration, valHooks []*admissionv1.ValidatingWebhookConfiguration) error {
+	cs, err := client.New(config, client.Options{})
+	if err != nil {
+		return err
+	}
+
+	// Create each webhook
+	for _, hook := range mutHooks {
+		hook := hook
+		log.V(1).Info("installing mutating webhook", "webhook", hook.GetName())
+		if err := ensureCreated(cs, hook); err != nil {
+			return err
+		}
+	}
+	for _, hook := range valHooks {
+		hook := hook
+		log.V(1).Info("installing validating webhook", "webhook", hook.GetName())
+		if err := ensureCreated(cs, hook); err != nil {
+			return err
+		}
+	}
+	return nil
+}
+
+// ensureCreated creates or update object if already exists in the cluster.
+func ensureCreated(cs client.Client, obj client.Object) error {
+	existing := obj.DeepCopyObject().(client.Object)
+	err := cs.Get(context.Background(), client.ObjectKey{Name: obj.GetName()}, existing)
+	switch {
+	case apierrors.IsNotFound(err):
+		if err := cs.Create(context.Background(), obj); err != nil {
+			return err
+		}
+	case err != nil:
+		return err
+	default:
+		log.V(1).Info("Webhook configuration already exists, updating", "webhook", obj.GetName())
+		obj.SetResourceVersion(existing.GetResourceVersion())
+		if err := cs.Update(context.Background(), obj); err != nil {
+			return err
+		}
+	}
+	return nil
+}
+
+// parseWebhook reads the directories or files of Webhooks in options.Paths and adds the Webhook structs to options.
+func parseWebhook(options *WebhookInstallOptions) error {
+	if len(options.Paths) > 0 {
+		for _, path := range options.Paths {
+			_, err := os.Stat(path)
+			if options.IgnoreErrorIfPathMissing && os.IsNotExist(err) {
+				continue // skip this path
+			}
+			if !options.IgnoreErrorIfPathMissing && os.IsNotExist(err) {
+				return err // treat missing path as error
+			}
+			mutHooks, valHooks, err := readWebhooks(path)
+			if err != nil {
+				return err
+			}
+			options.MutatingWebhooks = append(options.MutatingWebhooks, mutHooks...)
+			options.ValidatingWebhooks = append(options.ValidatingWebhooks, valHooks...)
+		}
+	}
+	return nil
+}
+
+// readWebhooks reads the Webhooks from files and Unmarshals them into structs
+// returns slice of mutating and validating webhook configurations.
+func readWebhooks(path string) ([]*admissionv1.MutatingWebhookConfiguration, []*admissionv1.ValidatingWebhookConfiguration, error) {
+	// Get the webhook files
+	var files []os.FileInfo
+	var err error
+	log.V(1).Info("reading Webhooks from path", "path", path)
+	info, err := os.Stat(path)
+	if err != nil {
+		return nil, nil, err
+	}
+	if !info.IsDir() {
+		path, files = filepath.Dir(path), []os.FileInfo{info}
+	} else if files, err = ioutil.ReadDir(path); err != nil {
+		return nil, nil, err
+	}
+
+	// file extensions that may contain Webhooks
+	resourceExtensions := sets.NewString(".json", ".yaml", ".yml")
+
+	var mutHooks []*admissionv1.MutatingWebhookConfiguration
+	var valHooks []*admissionv1.ValidatingWebhookConfiguration
+	for _, file := range files {
+		// Only parse allowlisted file types
+		if !resourceExtensions.Has(filepath.Ext(file.Name())) {
+			continue
+		}
+
+		// Unmarshal Webhooks from file into structs
+		docs, err := readDocuments(filepath.Join(path, file.Name()))
+		if err != nil {
+			return nil, nil, err
+		}
+
+		for _, doc := range docs {
+			var generic metav1.PartialObjectMetadata
+			if err = yaml.Unmarshal(doc, &generic); err != nil {
+				return nil, nil, err
+			}
+
+			const (
+				admissionregv1 = "admissionregistration.k8s.io/v1"
+			)
+			switch {
+			case generic.Kind == "MutatingWebhookConfiguration":
+				if generic.APIVersion != admissionregv1 {
+					return nil, nil, fmt.Errorf("only v1 is supported right now for MutatingWebhookConfiguration (name: %s)", generic.Name)
+				}
+				hook := &admissionv1.MutatingWebhookConfiguration{}
+				if err := yaml.Unmarshal(doc, hook); err != nil {
+					return nil, nil, err
+				}
+				mutHooks = append(mutHooks, hook)
+			case generic.Kind == "ValidatingWebhookConfiguration":
+				if generic.APIVersion != admissionregv1 {
+					return nil, nil, fmt.Errorf("only v1 is supported right now for ValidatingWebhookConfiguration (name: %s)", generic.Name)
+				}
+				hook := &admissionv1.ValidatingWebhookConfiguration{}
+				if err := yaml.Unmarshal(doc, hook); err != nil {
+					return nil, nil, err
+				}
+				valHooks = append(valHooks, hook)
+			default:
+				continue
+			}
+		}
+
+		log.V(1).Info("read webhooks from file", "file", file.Name())
+	}
+	return mutHooks, valHooks, nil
+}
diff --git a/vendor/sigs.k8s.io/controller-runtime/pkg/internal/testing/addr/manager.go b/vendor/sigs.k8s.io/controller-runtime/pkg/internal/testing/addr/manager.go
new file mode 100644
index 000000000..caaafa262
--- /dev/null
+++ b/vendor/sigs.k8s.io/controller-runtime/pkg/internal/testing/addr/manager.go
@@ -0,0 +1,126 @@
+/*
+Copyright 2021 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 addr
+
+import (
+	"errors"
+	"fmt"
+	"io/fs"
+	"net"
+	"os"
+	"path/filepath"
+	"strings"
+	"time"
+
+	"sigs.k8s.io/controller-runtime/pkg/internal/flock"
+)
+
+// TODO(directxman12): interface / release functionality for external port managers
+
+const (
+	portReserveTime   = 2 * time.Minute
+	portConflictRetry = 100
+	portFilePrefix    = "port-"
+)
+
+var (
+	cacheDir string
+)
+
+func init() {
+	baseDir, err := os.UserCacheDir()
+	if err != nil {
+		baseDir = os.TempDir()
+	}
+	cacheDir = filepath.Join(baseDir, "kubebuilder-envtest")
+	if err := os.MkdirAll(cacheDir, 0750); err != nil {
+		panic(err)
+	}
+}
+
+type portCache struct{}
+
+func (c *portCache) add(port int) (bool, error) {
+	// Remove outdated ports.
+	if err := fs.WalkDir(os.DirFS(cacheDir), ".", func(path string, d fs.DirEntry, err error) error {
+		if err != nil {
+			return err
+		}
+		if d.IsDir() || !d.Type().IsRegular() || !strings.HasPrefix(path, portFilePrefix) {
+			return nil
+		}
+		info, err := d.Info()
+		if err != nil {
+			return err
+		}
+		if time.Since(info.ModTime()) > portReserveTime {
+			if err := os.Remove(filepath.Join(cacheDir, path)); err != nil {
+				return err
+			}
+		}
+		return nil
+	}); err != nil {
+		return false, err
+	}
+	// Try allocating new port, by acquiring a file.
+	path := fmt.Sprintf("%s/%s%d", cacheDir, portFilePrefix, port)
+	if err := flock.Acquire(path); errors.Is(err, flock.ErrAlreadyLocked) {
+		return false, nil
+	} else if err != nil {
+		return false, err
+	}
+	return true, nil
+}
+
+var cache = &portCache{}
+
+func suggest(listenHost string) (*net.TCPListener, int, string, error) {
+	if listenHost == "" {
+		listenHost = "localhost"
+	}
+	addr, err := net.ResolveTCPAddr("tcp", net.JoinHostPort(listenHost, "0"))
+	if err != nil {
+		return nil, -1, "", err
+	}
+	l, err := net.ListenTCP("tcp", addr)
+	if err != nil {
+		return nil, -1, "", err
+	}
+	return l, l.Addr().(*net.TCPAddr).Port,
+		addr.IP.String(),
+		nil
+}
+
+// Suggest suggests an address a process can listen on. It returns
+// a tuple consisting of a free port and the hostname resolved to its IP.
+// It makes sure that new port allocated does not conflict with old ports
+// allocated within 1 minute.
+func Suggest(listenHost string) (int, string, error) {
+	for i := 0; i < portConflictRetry; i++ {
+		listener, port, resolvedHost, err := suggest(listenHost)
+		if err != nil {
+			return -1, "", err
+		}
+		defer listener.Close()
+		if ok, err := cache.add(port); ok {
+			return port, resolvedHost, nil
+		} else if err != nil {
+			return -1, "", err
+		}
+	}
+	return -1, "", fmt.Errorf("no free ports found after %d retries", portConflictRetry)
+}
diff --git a/vendor/sigs.k8s.io/controller-runtime/pkg/internal/testing/certs/tinyca.go b/vendor/sigs.k8s.io/controller-runtime/pkg/internal/testing/certs/tinyca.go
new file mode 100644
index 000000000..55b044c5b
--- /dev/null
+++ b/vendor/sigs.k8s.io/controller-runtime/pkg/internal/testing/certs/tinyca.go
@@ -0,0 +1,224 @@
+/*
+Copyright 2021 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 certs
+
+// NB(directxman12): nothing has verified that this has good settings.  In fact,
+// the setting generated here are probably terrible, but they're fine for integration
+// tests.  These ABSOLUTELY SHOULD NOT ever be exposed in the public API.  They're
+// ONLY for use with envtest's ability to configure webhook testing.
+// If I didn't otherwise not want to add a dependency on cfssl, I'd just use that.
+
+import (
+	"crypto"
+	"crypto/ecdsa"
+	"crypto/elliptic"
+	crand "crypto/rand"
+	"crypto/x509"
+	"crypto/x509/pkix"
+	"encoding/pem"
+	"fmt"
+	"math/big"
+	"net"
+	"time"
+
+	certutil "k8s.io/client-go/util/cert"
+)
+
+var (
+	ellipticCurve = elliptic.P256()
+	bigOne        = big.NewInt(1)
+)
+
+// CertPair is a private key and certificate for use for client auth, as a CA, or serving.
+type CertPair struct {
+	Key  crypto.Signer
+	Cert *x509.Certificate
+}
+
+// CertBytes returns the PEM-encoded version of the certificate for this pair.
+func (k CertPair) CertBytes() []byte {
+	return pem.EncodeToMemory(&pem.Block{
+		Type:  "CERTIFICATE",
+		Bytes: k.Cert.Raw,
+	})
+}
+
+// AsBytes encodes keypair in the appropriate formats for on-disk storage (PEM and
+// PKCS8, respectively).
+func (k CertPair) AsBytes() (cert []byte, key []byte, err error) {
+	cert = k.CertBytes()
+
+	rawKeyData, err := x509.MarshalPKCS8PrivateKey(k.Key)
+	if err != nil {
+		return nil, nil, fmt.Errorf("unable to encode private key: %v", err)
+	}
+
+	key = pem.EncodeToMemory(&pem.Block{
+		Type:  "PRIVATE KEY",
+		Bytes: rawKeyData,
+	})
+
+	return cert, key, nil
+}
+
+// TinyCA supports signing serving certs and client-certs,
+// and can be used as an auth mechanism with envtest.
+type TinyCA struct {
+	CA      CertPair
+	orgName string
+
+	nextSerial *big.Int
+}
+
+// newPrivateKey generates a new private key of a relatively sane size (see
+// rsaKeySize).
+func newPrivateKey() (crypto.Signer, error) {
+	return ecdsa.GenerateKey(ellipticCurve, crand.Reader)
+}
+
+// NewTinyCA creates a new a tiny CA utility for provisioning serving certs and client certs FOR TESTING ONLY.
+// Don't use this for anything else!
+func NewTinyCA() (*TinyCA, error) {
+	caPrivateKey, err := newPrivateKey()
+	if err != nil {
+		return nil, fmt.Errorf("unable to generate private key for CA: %v", err)
+	}
+	caCfg := certutil.Config{CommonName: "envtest-environment", Organization: []string{"envtest"}}
+	caCert, err := certutil.NewSelfSignedCACert(caCfg, caPrivateKey)
+	if err != nil {
+		return nil, fmt.Errorf("unable to generate certificate for CA: %v", err)
+	}
+
+	return &TinyCA{
+		CA:         CertPair{Key: caPrivateKey, Cert: caCert},
+		orgName:    "envtest",
+		nextSerial: big.NewInt(1),
+	}, nil
+}
+
+func (c *TinyCA) makeCert(cfg certutil.Config) (CertPair, error) {
+	now := time.Now()
+
+	key, err := newPrivateKey()
+	if err != nil {
+		return CertPair{}, fmt.Errorf("unable to create private key: %v", err)
+	}
+
+	serial := new(big.Int).Set(c.nextSerial)
+	c.nextSerial.Add(c.nextSerial, bigOne)
+
+	template := x509.Certificate{
+		Subject:      pkix.Name{CommonName: cfg.CommonName, Organization: cfg.Organization},
+		DNSNames:     cfg.AltNames.DNSNames,
+		IPAddresses:  cfg.AltNames.IPs,
+		SerialNumber: serial,
+
+		KeyUsage:    x509.KeyUsageKeyEncipherment | x509.KeyUsageDigitalSignature,
+		ExtKeyUsage: cfg.Usages,
+
+		// technically not necessary for testing, but let's set anyway just in case.
+		NotBefore: now.UTC(),
+		// 1 week -- the default for cfssl, and just long enough for a
+		// long-term test, but not too long that anyone would try to use this
+		// seriously.
+		NotAfter: now.Add(168 * time.Hour).UTC(),
+	}
+
+	certRaw, err := x509.CreateCertificate(crand.Reader, &template, c.CA.Cert, key.Public(), c.CA.Key)
+	if err != nil {
+		return CertPair{}, fmt.Errorf("unable to create certificate: %v", err)
+	}
+
+	cert, err := x509.ParseCertificate(certRaw)
+	if err != nil {
+		return CertPair{}, fmt.Errorf("generated invalid certificate, could not parse: %v", err)
+	}
+
+	return CertPair{
+		Key:  key,
+		Cert: cert,
+	}, nil
+}
+
+// NewServingCert returns a new CertPair for a serving HTTPS on localhost (or other specified names).
+func (c *TinyCA) NewServingCert(names ...string) (CertPair, error) {
+	if len(names) == 0 {
+		names = []string{"localhost"}
+	}
+	dnsNames, ips, err := resolveNames(names)
+	if err != nil {
+		return CertPair{}, err
+	}
+
+	return c.makeCert(certutil.Config{
+		CommonName:   "localhost",
+		Organization: []string{c.orgName},
+		AltNames: certutil.AltNames{
+			DNSNames: dnsNames,
+			IPs:      ips,
+		},
+		Usages: []x509.ExtKeyUsage{x509.ExtKeyUsageServerAuth},
+	})
+}
+
+// ClientInfo describes some Kubernetes user for the purposes of creating
+// client certificates.
+type ClientInfo struct {
+	// Name is the user name (embedded as the cert's CommonName)
+	Name string
+	// Groups are the groups to which this user belongs (embedded as the cert's
+	// Organization)
+	Groups []string
+}
+
+// NewClientCert produces a new CertPair suitable for use with Kubernetes
+// client cert auth with an API server validating based on this CA.
+func (c *TinyCA) NewClientCert(user ClientInfo) (CertPair, error) {
+	return c.makeCert(certutil.Config{
+		CommonName:   user.Name,
+		Organization: user.Groups,
+		Usages:       []x509.ExtKeyUsage{x509.ExtKeyUsageClientAuth},
+	})
+}
+
+func resolveNames(names []string) ([]string, []net.IP, error) {
+	dnsNames := []string{}
+	ips := []net.IP{}
+	for _, name := range names {
+		if name == "" {
+			continue
+		}
+		ip := net.ParseIP(name)
+		if ip == nil {
+			dnsNames = append(dnsNames, name)
+			// Also resolve to IPs.
+			nameIPs, err := net.LookupHost(name)
+			if err != nil {
+				return nil, nil, err
+			}
+			for _, nameIP := range nameIPs {
+				ip = net.ParseIP(nameIP)
+				if ip != nil {
+					ips = append(ips, ip)
+				}
+			}
+		} else {
+			ips = append(ips, ip)
+		}
+	}
+	return dnsNames, ips, nil
+}
diff --git a/vendor/sigs.k8s.io/controller-runtime/pkg/internal/testing/controlplane/apiserver.go b/vendor/sigs.k8s.io/controller-runtime/pkg/internal/testing/controlplane/apiserver.go
new file mode 100644
index 000000000..d6a71dc95
--- /dev/null
+++ b/vendor/sigs.k8s.io/controller-runtime/pkg/internal/testing/controlplane/apiserver.go
@@ -0,0 +1,469 @@
+/*
+Copyright 2021 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 controlplane
+
+import (
+	"fmt"
+	"io"
+	"io/ioutil"
+	"net/url"
+	"os"
+	"path/filepath"
+	"strconv"
+	"time"
+
+	"sigs.k8s.io/controller-runtime/pkg/internal/testing/addr"
+	"sigs.k8s.io/controller-runtime/pkg/internal/testing/certs"
+	"sigs.k8s.io/controller-runtime/pkg/internal/testing/process"
+)
+
+const (
+	// saKeyFile is the name of the service account signing private key file.
+	saKeyFile = "sa-signer.key"
+	// saKeyFile is the name of the service account signing public key (cert) file.
+	saCertFile = "sa-signer.crt"
+)
+
+// SecureServing provides/configures how the API server serves on the secure port.
+type SecureServing struct {
+	// ListenAddr contains the host & port to serve on.
+	//
+	// Configurable.  If unset, it will be defaulted.
+	process.ListenAddr
+	// CA contains the CA that signed the API server's serving certificates.
+	//
+	// Read-only.
+	CA []byte
+	// Authn can be used to provision users, and override what type of
+	// authentication is used to provision users.
+	//
+	// Configurable.  If unset, it will be defaulted.
+	Authn
+}
+
+// APIServer knows how to run a kubernetes apiserver.
+type APIServer struct {
+	// URL is the address the ApiServer should listen on for client
+	// connections.
+	//
+	// If set, this will configure the *insecure* serving details.
+	// If unset, it will contain the insecure port if insecure serving is enabled,
+	// and otherwise will contain the secure port.
+	//
+	// If this is not specified, we default to a random free port on localhost.
+	//
+	// Deprecated: use InsecureServing (for the insecure URL) or SecureServing, ideally.
+	URL *url.URL
+
+	// SecurePort is the additional secure port that the APIServer should listen on.
+	//
+	// If set, this will override SecureServing.Port.
+	//
+	// Deprecated: use SecureServing.
+	SecurePort int
+
+	// SecureServing indicates how the API server will serve on the secure port.
+	//
+	// Some parts are configurable.  Will be defaulted if unset.
+	SecureServing
+
+	// InsecureServing indicates how the API server will serve on the insecure port.
+	//
+	// If unset, the insecure port will be disabled.  Set to an empty struct to get
+	// default values.
+	//
+	// Deprecated: does not work with Kubernetes versions 1.20 and above.  Use secure
+	// serving instead.
+	InsecureServing *process.ListenAddr
+
+	// Path is the path to the apiserver binary.
+	//
+	// If this is left as the empty string, we will attempt to locate a binary,
+	// by checking for the TEST_ASSET_KUBE_APISERVER environment variable, and
+	// the default test assets directory. See the "Binaries" section above (in
+	// doc.go) for details.
+	Path string
+
+	// Args is a list of arguments which will passed to the APIServer binary.
+	// Before they are passed on, they will be evaluated as go-template strings.
+	// This means you can use fields which are defined and exported on this
+	// APIServer struct (e.g. "--cert-dir={{ .Dir }}").
+	// Those templates will be evaluated after the defaulting of the APIServer's
+	// fields has already happened and just before the binary actually gets
+	// started. Thus you have access to calculated fields like `URL` and others.
+	//
+	// If not specified, the minimal set of arguments to run the APIServer will
+	// be used.
+	//
+	// They will be loaded into the same argument set as Configure.  Each flag
+	// will be Append-ed to the configured arguments just before launch.
+	//
+	// Deprecated: use Configure instead.
+	Args []string
+
+	// CertDir is a path to a directory containing whatever certificates the
+	// APIServer will need.
+	//
+	// If left unspecified, then the Start() method will create a fresh temporary
+	// directory, and the Stop() method will clean it up.
+	CertDir string
+
+	// EtcdURL is the URL of the Etcd the APIServer should use.
+	//
+	// If this is not specified, the Start() method will return an error.
+	EtcdURL *url.URL
+
+	// StartTimeout, StopTimeout specify the time the APIServer is allowed to
+	// take when starting and stoppping before an error is emitted.
+	//
+	// If not specified, these default to 20 seconds.
+	StartTimeout time.Duration
+	StopTimeout  time.Duration
+
+	// Out, Err specify where APIServer should write its StdOut, StdErr to.
+	//
+	// If not specified, the output will be discarded.
+	Out io.Writer
+	Err io.Writer
+
+	processState *process.State
+
+	// args contains the structured arguments to use for running the API server
+	// Lazily initialized by .Configure(), Defaulted eventually with .defaultArgs()
+	args *process.Arguments
+}
+
+// Configure returns Arguments that may be used to customize the
+// flags used to launch the API server.  A set of defaults will
+// be applied underneath.
+func (s *APIServer) Configure() *process.Arguments {
+	if s.args == nil {
+		s.args = process.EmptyArguments()
+	}
+	return s.args
+}
+
+// Start starts the apiserver, waits for it to come up, and returns an error,
+// if occurred.
+func (s *APIServer) Start() error {
+	if err := s.prepare(); err != nil {
+		return err
+	}
+	return s.processState.Start(s.Out, s.Err)
+}
+
+func (s *APIServer) prepare() error {
+	if err := s.setProcessState(); err != nil {
+		return err
+	}
+	return s.Authn.Start()
+}
+
+// configurePorts configures the serving ports for this API server.
+//
+// Most of this method currently deals with making the deprecated fields
+// take precedence over the new fields.
+func (s *APIServer) configurePorts() error {
+	// prefer the old fields to the new fields if a user set one,
+	// otherwise, default the new fields and populate the old ones.
+
+	// Insecure: URL, InsecureServing
+	if s.URL != nil {
+		s.InsecureServing = &process.ListenAddr{
+			Address: s.URL.Hostname(),
+			Port:    s.URL.Port(),
+		}
+	} else if insec := s.InsecureServing; insec != nil {
+		if insec.Port == "" || insec.Address == "" {
+			port, host, err := addr.Suggest("")
+			if err != nil {
+				return fmt.Errorf("unable to provision unused insecure port: %w", err)
+			}
+			s.InsecureServing.Port = strconv.Itoa(port)
+			s.InsecureServing.Address = host
+		}
+		s.URL = s.InsecureServing.URL("http", "")
+	}
+
+	// Secure: SecurePort, SecureServing
+	if s.SecurePort != 0 {
+		s.SecureServing.Port = strconv.Itoa(s.SecurePort)
+		// if we don't have an address, try the insecure address, and otherwise
+		// default to loopback.
+		if s.SecureServing.Address == "" {
+			if s.InsecureServing != nil {
+				s.SecureServing.Address = s.InsecureServing.Address
+			} else {
+				s.SecureServing.Address = "127.0.0.1"
+			}
+		}
+	} else if s.SecureServing.Port == "" || s.SecureServing.Address == "" {
+		port, host, err := addr.Suggest("")
+		if err != nil {
+			return fmt.Errorf("unable to provision unused secure port: %w", err)
+		}
+		s.SecureServing.Port = strconv.Itoa(port)
+		s.SecureServing.Address = host
+		s.SecurePort = port
+	}
+
+	return nil
+}
+
+func (s *APIServer) setProcessState() error {
+	if s.EtcdURL == nil {
+		return fmt.Errorf("expected EtcdURL to be configured")
+	}
+
+	var err error
+
+	// unconditionally re-set this so we can successfully restart
+	// TODO(directxman12): we supported this in the past, but do we actually
+	// want to support re-using an API server object to restart?  The loss
+	// of provisioned users is surprising to say the least.
+	s.processState = &process.State{
+		Dir:          s.CertDir,
+		Path:         s.Path,
+		StartTimeout: s.StartTimeout,
+		StopTimeout:  s.StopTimeout,
+	}
+	if err := s.processState.Init("kube-apiserver"); err != nil {
+		return err
+	}
+
+	if err := s.configurePorts(); err != nil {
+		return err
+	}
+
+	// the secure port will always be on, so use that
+	s.processState.HealthCheck.URL = *s.SecureServing.URL("https", "/healthz")
+
+	s.CertDir = s.processState.Dir
+	s.Path = s.processState.Path
+	s.StartTimeout = s.processState.StartTimeout
+	s.StopTimeout = s.processState.StopTimeout
+
+	if err := s.populateAPIServerCerts(); err != nil {
+		return err
+	}
+
+	if s.SecureServing.Authn == nil {
+		authn, err := NewCertAuthn()
+		if err != nil {
+			return err
+		}
+		s.SecureServing.Authn = authn
+	}
+
+	if err := s.Authn.Configure(s.CertDir, s.Configure()); err != nil {
+		return err
+	}
+
+	// NB(directxman12): insecure port is a mess:
+	// - 1.19 and below have the `--insecure-port` flag, and require it to be set to zero to
+	//   disable it, otherwise the default will be used and we'll conflict.
+	// - 1.20 requires the flag to be unset or set to zero, and yells at you if you configure it
+	// - 1.24 won't have the flag at all...
+	//
+	// In an effort to automatically do the right thing during this mess, we do feature discovery
+	// on the flags, and hope that we've "parsed" them properly.
+	//
+	// TODO(directxman12): once we support 1.20 as the min version (might be when 1.24 comes out,
+	// might be around 1.25 or 1.26), remove this logic and the corresponding line in API server's
+	// default args.
+	if err := s.discoverFlags(); err != nil {
+		return err
+	}
+
+	s.processState.Args, s.Args, err = process.TemplateAndArguments(s.Args, s.Configure(), process.TemplateDefaults{ //nolint:staticcheck
+		Data:     s,
+		Defaults: s.defaultArgs(),
+		MinimalDefaults: map[string][]string{
+			// as per kubernetes-sigs/controller-runtime#641, we need this (we
+			// probably need other stuff too, but this is the only thing that was
+			// previously considered a "minimal default")
+			"service-cluster-ip-range": {"10.0.0.0/24"},
+
+			// we need *some* authorization mode for health checks on the secure port,
+			// so default to RBAC unless the user set something else (in which case
+			// this'll be ignored due to SliceToArguments using AppendNoDefaults).
+			"authorization-mode": {"RBAC"},
+		},
+	})
+	if err != nil {
+		return err
+	}
+
+	return nil
+}
+
+// discoverFlags checks for certain flags that *must* be set in certain
+// versions, and *must not* be set in others.
+func (s *APIServer) discoverFlags() error {
+	// Present: <1.24, Absent: >= 1.24
+	present, err := s.processState.CheckFlag("insecure-port")
+	if err != nil {
+		return err
+	}
+
+	if !present {
+		s.Configure().Disable("insecure-port")
+	}
+
+	return nil
+}
+
+func (s *APIServer) defaultArgs() map[string][]string {
+	args := map[string][]string{
+		"service-cluster-ip-range": {"10.0.0.0/24"},
+		"allow-privileged":         {"true"},
+		// we're keeping this disabled because if enabled, default SA is
+		// missing which would force all tests to create one in normal
+		// apiserver operation this SA is created by controller, but that is
+		// not run in integration environment
+		"disable-admission-plugins": {"ServiceAccount"},
+		"cert-dir":                  {s.CertDir},
+		"authorization-mode":        {"RBAC"},
+		"secure-port":               {s.SecureServing.Port},
+		// NB(directxman12): previously we didn't set the bind address for the secure
+		// port.  It *shouldn't* make a difference unless people are doing something really
+		// funky, but if you start to get bug reports look here ;-)
+		"bind-address": {s.SecureServing.Address},
+
+		// required on 1.20+, fine to leave on for <1.20
+		"service-account-issuer":           {s.SecureServing.URL("https", "/").String()},
+		"service-account-key-file":         {filepath.Join(s.CertDir, saCertFile)},
+		"service-account-signing-key-file": {filepath.Join(s.CertDir, saKeyFile)},
+	}
+	if s.EtcdURL != nil {
+		args["etcd-servers"] = []string{s.EtcdURL.String()}
+	}
+	if s.URL != nil {
+		args["insecure-port"] = []string{s.URL.Port()}
+		args["insecure-bind-address"] = []string{s.URL.Hostname()}
+	} else {
+		// TODO(directxman12): remove this once 1.21 is the lowest version we support
+		// (this might be a while, but this line'll break as of 1.24, so see the comment
+		// in Start
+		args["insecure-port"] = []string{"0"}
+	}
+	return args
+}
+
+func (s *APIServer) populateAPIServerCerts() error {
+	_, statErr := os.Stat(filepath.Join(s.CertDir, "apiserver.crt"))
+	if !os.IsNotExist(statErr) {
+		return statErr
+	}
+
+	ca, err := certs.NewTinyCA()
+	if err != nil {
+		return err
+	}
+
+	servingCerts, err := ca.NewServingCert()
+	if err != nil {
+		return err
+	}
+
+	certData, keyData, err := servingCerts.AsBytes()
+	if err != nil {
+		return err
+	}
+
+	if err := ioutil.WriteFile(filepath.Join(s.CertDir, "apiserver.crt"), certData, 0640); err != nil { //nolint:gosec
+		return err
+	}
+	if err := ioutil.WriteFile(filepath.Join(s.CertDir, "apiserver.key"), keyData, 0640); err != nil { //nolint:gosec
+		return err
+	}
+
+	s.SecureServing.CA = ca.CA.CertBytes()
+
+	// service account signing files too
+	saCA, err := certs.NewTinyCA()
+	if err != nil {
+		return err
+	}
+
+	saCert, saKey, err := saCA.CA.AsBytes()
+	if err != nil {
+		return err
+	}
+
+	if err := ioutil.WriteFile(filepath.Join(s.CertDir, saCertFile), saCert, 0640); err != nil { //nolint:gosec
+		return err
+	}
+	return ioutil.WriteFile(filepath.Join(s.CertDir, saKeyFile), saKey, 0640) //nolint:gosec
+}
+
+// Stop stops this process gracefully, waits for its termination, and cleans up
+// the CertDir if necessary.
+func (s *APIServer) Stop() error {
+	if s.processState != nil {
+		if s.processState.DirNeedsCleaning {
+			s.CertDir = "" // reset the directory if it was randomly allocated, so that we can safely restart
+		}
+		if err := s.processState.Stop(); err != nil {
+			return err
+		}
+	}
+	return s.Authn.Stop()
+}
+
+// APIServerDefaultArgs exposes the default args for the APIServer so that you
+// can use those to append your own additional arguments.
+//
+// Note that these arguments don't handle newer API servers well to due the more
+// complex feature detection neeeded.  It's recommended that you switch to .Configure
+// as you upgrade API server versions.
+//
+// Deprecated: use APIServer.Configure().
+var APIServerDefaultArgs = []string{
+	"--advertise-address=127.0.0.1",
+	"--etcd-servers={{ if .EtcdURL }}{{ .EtcdURL.String }}{{ end }}",
+	"--cert-dir={{ .CertDir }}",
+	"--insecure-port={{ if .URL }}{{ .URL.Port }}{{else}}0{{ end }}",
+	"{{ if .URL }}--insecure-bind-address={{ .URL.Hostname }}{{ end }}",
+	"--secure-port={{ if .SecurePort }}{{ .SecurePort }}{{ end }}",
+	// we're keeping this disabled because if enabled, default SA is missing which would force all tests to create one
+	// in normal apiserver operation this SA is created by controller, but that is not run in integration environment
+	"--disable-admission-plugins=ServiceAccount",
+	"--service-cluster-ip-range=10.0.0.0/24",
+	"--allow-privileged=true",
+	// NB(directxman12): we also enable RBAC if nothing else was enabled
+}
+
+// PrepareAPIServer is an internal-only (NEVER SHOULD BE EXPOSED)
+// function that sets up the API server just before starting it,
+// without actually starting it.  This saves time on tests.
+//
+// NB(directxman12): do not expose this outside of internal -- it's unsafe to
+// use, because things like port allocation could race even more than they
+// currently do if you later call start!
+func PrepareAPIServer(s *APIServer) error {
+	return s.prepare()
+}
+
+// APIServerArguments is an internal-only (NEVER SHOULD BE EXPOSED)
+// function that sets up the API server just before starting it,
+// without actually starting it.  It's public to make testing easier.
+//
+// NB(directxman12): do not expose this outside of internal.
+func APIServerArguments(s *APIServer) []string {
+	return s.processState.Args
+}
diff --git a/vendor/sigs.k8s.io/controller-runtime/pkg/internal/testing/controlplane/auth.go b/vendor/sigs.k8s.io/controller-runtime/pkg/internal/testing/controlplane/auth.go
new file mode 100644
index 000000000..b2cd4e5e0
--- /dev/null
+++ b/vendor/sigs.k8s.io/controller-runtime/pkg/internal/testing/controlplane/auth.go
@@ -0,0 +1,142 @@
+/*
+Copyright 2021 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 controlplane
+
+import (
+	"fmt"
+	"io/ioutil"
+	"path/filepath"
+
+	"k8s.io/client-go/rest"
+	"sigs.k8s.io/controller-runtime/pkg/internal/testing/certs"
+	"sigs.k8s.io/controller-runtime/pkg/internal/testing/process"
+)
+
+// User represents a Kubernetes user.
+type User struct {
+	// Name is the user's Name.
+	Name string
+	// Groups are the groups to which the user belongs.
+	Groups []string
+}
+
+// Authn knows how to configure an API server for a particular type of authentication,
+// and provision users under that authentication scheme.
+//
+// The methods must be called in the following order (as presented below in the interface
+// for a mnemonic):
+//
+// 1. Configure
+// 2. Start
+// 3. AddUsers (0+ calls)
+// 4. Stop.
+type Authn interface {
+	// Configure provides the working directory to this authenticator,
+	// and configures the given API server arguments to make use of this authenticator.
+	//
+	// Should be called first.
+	Configure(workDir string, args *process.Arguments) error
+	// Start runs this authenticator.  Will be called just before API server start.
+	//
+	// Must be called after Configure.
+	Start() error
+	// AddUser provisions a user, returning a copy of the given base rest.Config
+	// configured to authenticate as that users.
+	//
+	// May only be called while the authenticator is "running".
+	AddUser(user User, baseCfg *rest.Config) (*rest.Config, error)
+	// Stop shuts down this authenticator.
+	Stop() error
+}
+
+// CertAuthn is an authenticator (Authn) that makes use of client certificate authn.
+type CertAuthn struct {
+	// ca is the CA used to sign the client certs
+	ca *certs.TinyCA
+	// certDir is the directory used to write the CA crt file
+	// so that the API server can read it.
+	certDir string
+}
+
+// NewCertAuthn creates a new client-cert-based Authn with a new CA.
+func NewCertAuthn() (*CertAuthn, error) {
+	ca, err := certs.NewTinyCA()
+	if err != nil {
+		return nil, fmt.Errorf("unable to provision client certificate auth CA: %w", err)
+	}
+	return &CertAuthn{
+		ca: ca,
+	}, nil
+}
+
+// AddUser provisions a new user that's authenticated via certificates, with
+// the given uesrname and groups embedded in the certificate as expected by the
+// API server.
+func (c *CertAuthn) AddUser(user User, baseCfg *rest.Config) (*rest.Config, error) {
+	certs, err := c.ca.NewClientCert(certs.ClientInfo{
+		Name:   user.Name,
+		Groups: user.Groups,
+	})
+	if err != nil {
+		return nil, fmt.Errorf("unable to create client certificates for %s: %w", user.Name, err)
+	}
+
+	crt, key, err := certs.AsBytes()
+	if err != nil {
+		return nil, fmt.Errorf("unable to serialize client certificates for %s: %w", user.Name, err)
+	}
+
+	cfg := rest.CopyConfig(baseCfg)
+	cfg.CertData = crt
+	cfg.KeyData = key
+
+	return cfg, nil
+}
+
+// caCrtPath returns the path to the on-disk client-cert CA crt file.
+func (c *CertAuthn) caCrtPath() string {
+	return filepath.Join(c.certDir, "client-cert-auth-ca.crt")
+}
+
+// Configure provides the working directory to this authenticator,
+// and configures the given API server arguments to make use of this authenticator.
+func (c *CertAuthn) Configure(workDir string, args *process.Arguments) error {
+	c.certDir = workDir
+	args.Set("client-ca-file", c.caCrtPath())
+	return nil
+}
+
+// Start runs this authenticator.  Will be called just before API server start.
+//
+// Must be called after Configure.
+func (c *CertAuthn) Start() error {
+	if len(c.certDir) == 0 {
+		return fmt.Errorf("start called before configure")
+	}
+	caCrt := c.ca.CA.CertBytes()
+	if err := ioutil.WriteFile(c.caCrtPath(), caCrt, 0640); err != nil { //nolint:gosec
+		return fmt.Errorf("unable to save the client certificate CA to %s: %w", c.caCrtPath(), err)
+	}
+
+	return nil
+}
+
+// Stop shuts down this authenticator.
+func (c *CertAuthn) Stop() error {
+	// no-op -- our workdir is cleaned up for us automatically
+	return nil
+}
diff --git a/vendor/sigs.k8s.io/controller-runtime/pkg/internal/testing/controlplane/etcd.go b/vendor/sigs.k8s.io/controller-runtime/pkg/internal/testing/controlplane/etcd.go
new file mode 100644
index 000000000..c6d625173
--- /dev/null
+++ b/vendor/sigs.k8s.io/controller-runtime/pkg/internal/testing/controlplane/etcd.go
@@ -0,0 +1,202 @@
+/*
+Copyright 2021 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 controlplane
+
+import (
+	"io"
+	"net"
+	"net/url"
+	"strconv"
+	"time"
+
+	"sigs.k8s.io/controller-runtime/pkg/internal/testing/addr"
+	"sigs.k8s.io/controller-runtime/pkg/internal/testing/process"
+)
+
+// Etcd knows how to run an etcd server.
+type Etcd struct {
+	// URL is the address the Etcd should listen on for client connections.
+	//
+	// If this is not specified, we default to a random free port on localhost.
+	URL *url.URL
+
+	// Path is the path to the etcd binary.
+	//
+	// If this is left as the empty string, we will attempt to locate a binary,
+	// by checking for the TEST_ASSET_ETCD environment variable, and the default
+	// test assets directory. See the "Binaries" section above (in doc.go) for
+	// details.
+	Path string
+
+	// Args is a list of arguments which will passed to the Etcd binary. Before
+	// they are passed on, the`y will be evaluated as go-template strings. This
+	// means you can use fields which are defined and exported on this Etcd
+	// struct (e.g. "--data-dir={{ .Dir }}").
+	// Those templates will be evaluated after the defaulting of the Etcd's
+	// fields has already happened and just before the binary actually gets
+	// started. Thus you have access to calculated fields like `URL` and others.
+	//
+	// If not specified, the minimal set of arguments to run the Etcd will be
+	// used.
+	//
+	// They will be loaded into the same argument set as Configure.  Each flag
+	// will be Append-ed to the configured arguments just before launch.
+	//
+	// Deprecated: use Configure instead.
+	Args []string
+
+	// DataDir is a path to a directory in which etcd can store its state.
+	//
+	// If left unspecified, then the Start() method will create a fresh temporary
+	// directory, and the Stop() method will clean it up.
+	DataDir string
+
+	// StartTimeout, StopTimeout specify the time the Etcd is allowed to
+	// take when starting and stopping before an error is emitted.
+	//
+	// If not specified, these default to 20 seconds.
+	StartTimeout time.Duration
+	StopTimeout  time.Duration
+
+	// Out, Err specify where Etcd should write its StdOut, StdErr to.
+	//
+	// If not specified, the output will be discarded.
+	Out io.Writer
+	Err io.Writer
+
+	// processState contains the actual details about this running process
+	processState *process.State
+
+	// args contains the structured arguments to use for running etcd.
+	// Lazily initialized by .Configure(), Defaulted eventually with .defaultArgs()
+	args *process.Arguments
+
+	// listenPeerURL is the address the Etcd should listen on for peer connections.
+	// It's automatically generated and a random port is picked during execution.
+	listenPeerURL *url.URL
+}
+
+// Start starts the etcd, waits for it to come up, and returns an error, if one
+// occoured.
+func (e *Etcd) Start() error {
+	if err := e.setProcessState(); err != nil {
+		return err
+	}
+	return e.processState.Start(e.Out, e.Err)
+}
+
+func (e *Etcd) setProcessState() error {
+	e.processState = &process.State{
+		Dir:          e.DataDir,
+		Path:         e.Path,
+		StartTimeout: e.StartTimeout,
+		StopTimeout:  e.StopTimeout,
+	}
+
+	// unconditionally re-set this so we can successfully restart
+	// TODO(directxman12): we supported this in the past, but do we actually
+	// want to support re-using an API server object to restart?  The loss
+	// of provisioned users is surprising to say the least.
+	if err := e.processState.Init("etcd"); err != nil {
+		return err
+	}
+
+	// Set the listen url.
+	if e.URL == nil {
+		port, host, err := addr.Suggest("")
+		if err != nil {
+			return err
+		}
+		e.URL = &url.URL{
+			Scheme: "http",
+			Host:   net.JoinHostPort(host, strconv.Itoa(port)),
+		}
+	}
+
+	// Set the listen peer URL.
+	{
+		port, host, err := addr.Suggest("")
+		if err != nil {
+			return err
+		}
+		e.listenPeerURL = &url.URL{
+			Scheme: "http",
+			Host:   net.JoinHostPort(host, strconv.Itoa(port)),
+		}
+	}
+
+	// can use /health as of etcd 3.3.0
+	e.processState.HealthCheck.URL = *e.URL
+	e.processState.HealthCheck.Path = "/health"
+
+	e.DataDir = e.processState.Dir
+	e.Path = e.processState.Path
+	e.StartTimeout = e.processState.StartTimeout
+	e.StopTimeout = e.processState.StopTimeout
+
+	var err error
+	e.processState.Args, e.Args, err = process.TemplateAndArguments(e.Args, e.Configure(), process.TemplateDefaults{ //nolint:staticcheck
+		Data:     e,
+		Defaults: e.defaultArgs(),
+	})
+	return err
+}
+
+// Stop stops this process gracefully, waits for its termination, and cleans up
+// the DataDir if necessary.
+func (e *Etcd) Stop() error {
+	if e.processState.DirNeedsCleaning {
+		e.DataDir = "" // reset the directory if it was randomly allocated, so that we can safely restart
+	}
+	return e.processState.Stop()
+}
+
+func (e *Etcd) defaultArgs() map[string][]string {
+	args := map[string][]string{
+		"listen-peer-urls": {e.listenPeerURL.String()},
+		"data-dir":         {e.DataDir},
+	}
+	if e.URL != nil {
+		args["advertise-client-urls"] = []string{e.URL.String()}
+		args["listen-client-urls"] = []string{e.URL.String()}
+	}
+
+	// Add unsafe no fsync, available from etcd 3.5
+	if ok, _ := e.processState.CheckFlag("unsafe-no-fsync"); ok {
+		args["unsafe-no-fsync"] = []string{"true"}
+	}
+	return args
+}
+
+// Configure returns Arguments that may be used to customize the
+// flags used to launch etcd.  A set of defaults will
+// be applied underneath.
+func (e *Etcd) Configure() *process.Arguments {
+	if e.args == nil {
+		e.args = process.EmptyArguments()
+	}
+	return e.args
+}
+
+// EtcdDefaultArgs exposes the default args for Etcd so that you
+// can use those to append your own additional arguments.
+var EtcdDefaultArgs = []string{
+	"--listen-peer-urls=http://localhost:0",
+	"--advertise-client-urls={{ if .URL }}{{ .URL.String }}{{ end }}",
+	"--listen-client-urls={{ if .URL }}{{ .URL.String }}{{ end }}",
+	"--data-dir={{ .DataDir }}",
+}
diff --git a/vendor/sigs.k8s.io/controller-runtime/pkg/internal/testing/controlplane/kubectl.go b/vendor/sigs.k8s.io/controller-runtime/pkg/internal/testing/controlplane/kubectl.go
new file mode 100644
index 000000000..a27b7a0ff
--- /dev/null
+++ b/vendor/sigs.k8s.io/controller-runtime/pkg/internal/testing/controlplane/kubectl.go
@@ -0,0 +1,119 @@
+/*
+Copyright 2021 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 controlplane
+
+import (
+	"bytes"
+	"fmt"
+	"io"
+	"net/url"
+	"os/exec"
+
+	"k8s.io/client-go/rest"
+	"k8s.io/client-go/tools/clientcmd"
+	kcapi "k8s.io/client-go/tools/clientcmd/api"
+
+	"sigs.k8s.io/controller-runtime/pkg/internal/testing/process"
+)
+
+const (
+	envtestName = "envtest"
+)
+
+// KubeConfigFromREST reverse-engineers a kubeconfig file from a rest.Config.
+// The options are tailored towards the rest.Configs we generate, so they're
+// not broadly applicable.
+//
+// This is not intended to be exposed beyond internal for the above reasons.
+func KubeConfigFromREST(cfg *rest.Config) ([]byte, error) {
+	kubeConfig := kcapi.NewConfig()
+	protocol := "https"
+	if !rest.IsConfigTransportTLS(*cfg) {
+		protocol = "http"
+	}
+
+	// cfg.Host is a URL, so we need to parse it so we can properly append the API path
+	baseURL, err := url.Parse(cfg.Host)
+	if err != nil {
+		return nil, fmt.Errorf("unable to interpret config's host value as a URL: %w", err)
+	}
+
+	kubeConfig.Clusters[envtestName] = &kcapi.Cluster{
+		// TODO(directxman12): if client-go ever decides to expose defaultServerUrlFor(config),
+		// we can just use that.  Note that this is not the same as the public DefaultServerURL,
+		// which requires us to pass a bunch of stuff in manually.
+		Server:                   (&url.URL{Scheme: protocol, Host: baseURL.Host, Path: cfg.APIPath}).String(),
+		CertificateAuthorityData: cfg.CAData,
+	}
+	kubeConfig.AuthInfos[envtestName] = &kcapi.AuthInfo{
+		// try to cover all auth strategies that aren't plugins
+		ClientCertificateData: cfg.CertData,
+		ClientKeyData:         cfg.KeyData,
+		Token:                 cfg.BearerToken,
+		Username:              cfg.Username,
+		Password:              cfg.Password,
+	}
+	kcCtx := kcapi.NewContext()
+	kcCtx.Cluster = envtestName
+	kcCtx.AuthInfo = envtestName
+	kubeConfig.Contexts[envtestName] = kcCtx
+	kubeConfig.CurrentContext = envtestName
+
+	contents, err := clientcmd.Write(*kubeConfig)
+	if err != nil {
+		return nil, fmt.Errorf("unable to serialize kubeconfig file: %w", err)
+	}
+	return contents, nil
+}
+
+// KubeCtl is a wrapper around the kubectl binary.
+type KubeCtl struct {
+	// Path where the kubectl binary can be found.
+	//
+	// If this is left empty, we will attempt to locate a binary, by checking for
+	// the TEST_ASSET_KUBECTL environment variable, and the default test assets
+	// directory. See the "Binaries" section above (in doc.go) for details.
+	Path string
+
+	// Opts can be used to configure additional flags which will be used each
+	// time the wrapped binary is called.
+	//
+	// For example, you might want to use this to set the URL of the APIServer to
+	// connect to.
+	Opts []string
+}
+
+// Run executes the wrapped binary with some preconfigured options and the
+// arguments given to this method. It returns Readers for the stdout and
+// stderr.
+func (k *KubeCtl) Run(args ...string) (stdout, stderr io.Reader, err error) {
+	if k.Path == "" {
+		k.Path = process.BinPathFinder("kubectl", "")
+	}
+
+	stdoutBuffer := &bytes.Buffer{}
+	stderrBuffer := &bytes.Buffer{}
+	allArgs := append(k.Opts, args...)
+
+	cmd := exec.Command(k.Path, allArgs...)
+	cmd.Stdout = stdoutBuffer
+	cmd.Stderr = stderrBuffer
+
+	err = cmd.Run()
+
+	return stdoutBuffer, stderrBuffer, err
+}
diff --git a/vendor/sigs.k8s.io/controller-runtime/pkg/internal/testing/controlplane/plane.go b/vendor/sigs.k8s.io/controller-runtime/pkg/internal/testing/controlplane/plane.go
new file mode 100644
index 000000000..36fd3c630
--- /dev/null
+++ b/vendor/sigs.k8s.io/controller-runtime/pkg/internal/testing/controlplane/plane.go
@@ -0,0 +1,248 @@
+/*
+Copyright 2021 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 controlplane
+
+import (
+	"fmt"
+	"net/url"
+	"os"
+
+	kerrors "k8s.io/apimachinery/pkg/util/errors"
+	"k8s.io/client-go/rest"
+	"sigs.k8s.io/controller-runtime/pkg/internal/testing/certs"
+)
+
+// NewTinyCA creates a new a tiny CA utility for provisioning serving certs and client certs FOR TESTING ONLY.
+// Don't use this for anything else!
+var NewTinyCA = certs.NewTinyCA
+
+// ControlPlane is a struct that knows how to start your test control plane.
+//
+// Right now, that means Etcd and your APIServer. This is likely to increase in
+// future.
+type ControlPlane struct {
+	APIServer *APIServer
+	Etcd      *Etcd
+
+	// Kubectl will override the default asset search path for kubectl
+	KubectlPath string
+
+	// for the deprecated methods (Kubectl, etc)
+	defaultUserCfg     *rest.Config
+	defaultUserKubectl *KubeCtl
+}
+
+// Start will start your control plane processes. To stop them, call Stop().
+func (f *ControlPlane) Start() error {
+	if f.Etcd == nil {
+		f.Etcd = &Etcd{}
+	}
+	if err := f.Etcd.Start(); err != nil {
+		return err
+	}
+
+	if f.APIServer == nil {
+		f.APIServer = &APIServer{}
+	}
+	f.APIServer.EtcdURL = f.Etcd.URL
+	if err := f.APIServer.Start(); err != nil {
+		return err
+	}
+
+	// provision the default user -- can be removed when the related
+	// methods are removed.  The default user has admin permissions to
+	// mimic legacy no-authz setups.
+	user, err := f.AddUser(User{Name: "default", Groups: []string{"system:masters"}}, &rest.Config{})
+	if err != nil {
+		return fmt.Errorf("unable to provision the default (legacy) user: %w", err)
+	}
+	kubectl, err := user.Kubectl()
+	if err != nil {
+		return fmt.Errorf("unable to provision the default (legacy) kubeconfig: %w", err)
+	}
+	f.defaultUserCfg = user.Config()
+	f.defaultUserKubectl = kubectl
+	return nil
+}
+
+// Stop will stop your control plane processes, and clean up their data.
+func (f *ControlPlane) Stop() error {
+	var errList []error
+
+	if f.APIServer != nil {
+		if err := f.APIServer.Stop(); err != nil {
+			errList = append(errList, err)
+		}
+	}
+	if f.Etcd != nil {
+		if err := f.Etcd.Stop(); err != nil {
+			errList = append(errList, err)
+		}
+	}
+
+	return kerrors.NewAggregate(errList)
+}
+
+// APIURL returns the URL you should connect to to talk to your API server.
+//
+// If insecure serving is configured, this will contain the insecure port.
+// Otherwise, it will contain the secure port.
+//
+// Deprecated: use AddUser instead, or APIServer.{Ins|S}ecureServing.URL if
+// you really want just the URL.
+func (f *ControlPlane) APIURL() *url.URL {
+	return f.APIServer.URL
+}
+
+// KubeCtl returns a pre-configured KubeCtl, ready to connect to this
+// ControlPlane.
+//
+// Deprecated: use AddUser & AuthenticatedUser.Kubectl instead.
+func (f *ControlPlane) KubeCtl() *KubeCtl {
+	return f.defaultUserKubectl
+}
+
+// RESTClientConfig returns a pre-configured restconfig, ready to connect to
+// this ControlPlane.
+//
+// Deprecated: use AddUser & AuthenticatedUser.Config instead.
+func (f *ControlPlane) RESTClientConfig() (*rest.Config, error) {
+	return f.defaultUserCfg, nil
+}
+
+// AuthenticatedUser contains access information for an provisioned user,
+// including REST config, kubeconfig contents, and access to a KubeCtl instance.
+//
+// It's not "safe" to use the methods on this till after the API server has been
+// started (due to certificate initialization and such).  The various methods will
+// panic if this is done.
+type AuthenticatedUser struct {
+	// cfg is the rest.Config for connecting to the API server.  It's lazily initialized.
+	cfg *rest.Config
+	// cfgIsComplete indicates the cfg has had late-initialized fields (e.g.
+	// API server CA data) initialized.
+	cfgIsComplete bool
+
+	// apiServer is a handle to the APIServer that's used when finalizing cfg
+	// and producing the kubectl instance.
+	plane *ControlPlane
+
+	// kubectl is our existing, provisioned kubectl.  We don't provision one
+	// till someone actually asks for it.
+	kubectl *KubeCtl
+}
+
+// Config returns the REST config that can be used to connect to the API server
+// as this user.
+//
+// Will panic if used before the API server is started.
+func (u *AuthenticatedUser) Config() *rest.Config {
+	// NB(directxman12): we choose to panic here for ergonomics sake, and because there's
+	// not really much you can do to "handle" this error.  This machinery is intended to be
+	// used in tests anyway, so panicing is not a particularly big deal.
+	if u.cfgIsComplete {
+		return u.cfg
+	}
+	if len(u.plane.APIServer.SecureServing.CA) == 0 {
+		panic("the API server has not yet been started, please do that before accessing connection details")
+	}
+
+	u.cfg.CAData = u.plane.APIServer.SecureServing.CA
+	u.cfg.Host = u.plane.APIServer.SecureServing.URL("https", "/").String()
+	u.cfgIsComplete = true
+	return u.cfg
+}
+
+// KubeConfig returns a KubeConfig that's roughly equivalent to this user's REST config.
+//
+// Will panic if used before the API server is started.
+func (u AuthenticatedUser) KubeConfig() ([]byte, error) {
+	// NB(directxman12): we don't return the actual API object to avoid yet another
+	// piece of kubernetes API in our public API, and also because generally the thing
+	// you want to do with this is just write it out to a file for external debugging
+	// purposes, etc.
+	return KubeConfigFromREST(u.Config())
+}
+
+// Kubectl returns a KubeCtl instance for talking to the API server as this user.  It uses
+// a kubeconfig equivalent to that returned by .KubeConfig.
+//
+// Will panic if used before the API server is started.
+func (u *AuthenticatedUser) Kubectl() (*KubeCtl, error) {
+	if u.kubectl != nil {
+		return u.kubectl, nil
+	}
+	if len(u.plane.APIServer.CertDir) == 0 {
+		panic("the API server has not yet been started, please do that before accessing connection details")
+	}
+
+	// cleaning this up is handled when our tmpDir is deleted
+	out, err := os.CreateTemp(u.plane.APIServer.CertDir, "*.kubecfg")
+	if err != nil {
+		return nil, fmt.Errorf("unable to create file for kubeconfig: %w", err)
+	}
+	defer out.Close()
+	contents, err := KubeConfigFromREST(u.Config())
+	if err != nil {
+		return nil, err
+	}
+	if _, err := out.Write(contents); err != nil {
+		return nil, fmt.Errorf("unable to write kubeconfig to disk at %s: %w", out.Name(), err)
+	}
+	k := &KubeCtl{
+		Path: u.plane.KubectlPath,
+	}
+	k.Opts = append(k.Opts, fmt.Sprintf("--kubeconfig=%s", out.Name()))
+	u.kubectl = k
+	return k, nil
+}
+
+// AddUser provisions a new user in the cluster.  It uses the APIServer's authentication
+// strategy -- see APIServer.SecureServing.Authn.
+//
+// Unlike AddUser, it's safe to pass a nil rest.Config here if you have no
+// particular opinions about the config.
+//
+// The default authentication strategy is not guaranteed to any specific strategy, but it is
+// guaranteed to be callable both before and after Start has been called (but, as noted in the
+// AuthenticatedUser docs, the given user objects are only valid after Start has been called).
+func (f *ControlPlane) AddUser(user User, baseConfig *rest.Config) (*AuthenticatedUser, error) {
+	if f.GetAPIServer().SecureServing.Authn == nil {
+		return nil, fmt.Errorf("no API server authentication is configured yet.  The API server defaults one when Start is called, did you mean to use that?")
+	}
+
+	if baseConfig == nil {
+		baseConfig = &rest.Config{}
+	}
+	cfg, err := f.GetAPIServer().SecureServing.AddUser(user, baseConfig)
+	if err != nil {
+		return nil, err
+	}
+
+	return &AuthenticatedUser{
+		cfg:   cfg,
+		plane: f,
+	}, nil
+}
+
+// GetAPIServer returns this ControlPlane's APIServer, initializing it if necessary.
+func (f *ControlPlane) GetAPIServer() *APIServer {
+	if f.APIServer == nil {
+		f.APIServer = &APIServer{}
+	}
+	return f.APIServer
+}
diff --git a/vendor/sigs.k8s.io/controller-runtime/pkg/internal/testing/process/arguments.go b/vendor/sigs.k8s.io/controller-runtime/pkg/internal/testing/process/arguments.go
new file mode 100644
index 000000000..6c2c91e14
--- /dev/null
+++ b/vendor/sigs.k8s.io/controller-runtime/pkg/internal/testing/process/arguments.go
@@ -0,0 +1,340 @@
+/*
+Copyright 2021 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 process
+
+import (
+	"bytes"
+	"html/template"
+	"sort"
+	"strings"
+)
+
+// RenderTemplates returns an []string to render the templates
+//
+// Deprecated: will be removed in favor of Arguments.
+func RenderTemplates(argTemplates []string, data interface{}) (args []string, err error) {
+	var t *template.Template
+
+	for _, arg := range argTemplates {
+		t, err = template.New(arg).Parse(arg)
+		if err != nil {
+			args = nil
+			return
+		}
+
+		buf := &bytes.Buffer{}
+		err = t.Execute(buf, data)
+		if err != nil {
+			args = nil
+			return
+		}
+		args = append(args, buf.String())
+	}
+
+	return
+}
+
+// SliceToArguments converts a slice of arguments to structured arguments,
+// appending each argument that starts with `--` and contains an `=` to the
+// argument set (ignoring defaults), returning the rest.
+//
+// Deprecated: will be removed when RenderTemplates is removed.
+func SliceToArguments(sliceArgs []string, args *Arguments) []string {
+	var rest []string
+	for i, arg := range sliceArgs {
+		if arg == "--" {
+			rest = append(rest, sliceArgs[i:]...)
+			return rest
+		}
+		// skip non-flag arguments, skip arguments w/o equals because we
+		// can't tell if the next argument should take a value
+		if !strings.HasPrefix(arg, "--") || !strings.Contains(arg, "=") {
+			rest = append(rest, arg)
+			continue
+		}
+
+		parts := strings.SplitN(arg[2:], "=", 2)
+		name := parts[0]
+		val := parts[1]
+
+		args.AppendNoDefaults(name, val)
+	}
+
+	return rest
+}
+
+// TemplateDefaults specifies defaults to be used for joining structured arguments with templates.
+//
+// Deprecated: will be removed when RenderTemplates is removed.
+type TemplateDefaults struct {
+	// Data will be used to render the template.
+	Data interface{}
+	// Defaults will be used to default structured arguments if no template is passed.
+	Defaults map[string][]string
+	// MinimalDefaults will be used to default structured arguments if a template is passed.
+	// Use this for flags which *must* be present.
+	MinimalDefaults map[string][]string // for api server service-cluster-ip-range
+}
+
+// TemplateAndArguments joins structured arguments and non-structured arguments, preserving existing
+// behavior.  Namely:
+//
+// 1. if templ has len > 0, it will be rendered against data
+// 2. the rendered template values that look like `--foo=bar` will be split
+//    and appended to args, the rest will be kept around
+// 3. the given args will be rendered as string form.  If a template is given,
+//    no defaults will be used, otherwise defaults will be used
+// 4. a result of [args..., rest...] will be returned
+//
+// It returns the resulting rendered arguments, plus the arguments that were
+// not transferred to `args` during rendering.
+//
+// Deprecated: will be removed when RenderTemplates is removed.
+func TemplateAndArguments(templ []string, args *Arguments, data TemplateDefaults) (allArgs []string, nonFlagishArgs []string, err error) {
+	if len(templ) == 0 { // 3 & 4 (no template case)
+		return args.AsStrings(data.Defaults), nil, nil
+	}
+
+	// 1: render the template
+	rendered, err := RenderTemplates(templ, data.Data)
+	if err != nil {
+		return nil, nil, err
+	}
+
+	// 2: filter out structured args and add them to args
+	rest := SliceToArguments(rendered, args)
+
+	// 3 (template case): render structured args, no defaults (matching the
+	// legacy case where if Args was specified, no defaults were used)
+	res := args.AsStrings(data.MinimalDefaults)
+
+	// 4: return the rendered structured args + all non-structured args
+	return append(res, rest...), rest, nil
+}
+
+// EmptyArguments constructs an empty set of flags with no defaults.
+func EmptyArguments() *Arguments {
+	return &Arguments{
+		values: make(map[string]Arg),
+	}
+}
+
+// Arguments are structured, overridable arguments.
+// Each Arguments object contains some set of default arguments, which may
+// be appended to, or overridden.
+//
+// When ready, you can serialize them to pass to exec.Command and friends using
+// AsStrings.
+//
+// All flag-setting methods return the *same* instance of Arguments so that you
+// can chain calls.
+type Arguments struct {
+	// values contains the user-set values for the arguments.
+	// `values[key] = dontPass` means "don't pass this flag"
+	// `values[key] = passAsName` means "pass this flag without args like --key`
+	// `values[key] = []string{a, b, c}` means "--key=a --key=b --key=c`
+	// any values not explicitly set here will be copied from defaults on final rendering.
+	values map[string]Arg
+}
+
+// Arg is an argument that has one or more values,
+// and optionally falls back to default values.
+type Arg interface {
+	// Append adds new values to this argument, returning
+	// a new instance contain the new value.  The intermediate
+	// argument should generally be assumed to be consumed.
+	Append(vals ...string) Arg
+	// Get returns the full set of values, optionally including
+	// the passed in defaults.  If it returns nil, this will be
+	// skipped.  If it returns a non-nil empty slice, it'll be
+	// assumed that the argument should be passed as name-only.
+	Get(defaults []string) []string
+}
+
+type userArg []string
+
+func (a userArg) Append(vals ...string) Arg {
+	return userArg(append(a, vals...)) //nolint:unconvert
+}
+func (a userArg) Get(_ []string) []string {
+	return []string(a)
+}
+
+type defaultedArg []string
+
+func (a defaultedArg) Append(vals ...string) Arg {
+	return defaultedArg(append(a, vals...)) //nolint:unconvert
+}
+func (a defaultedArg) Get(defaults []string) []string {
+	res := append([]string(nil), defaults...)
+	return append(res, a...)
+}
+
+type dontPassArg struct{}
+
+func (a dontPassArg) Append(vals ...string) Arg {
+	return userArg(vals)
+}
+func (dontPassArg) Get(_ []string) []string {
+	return nil
+}
+
+type passAsNameArg struct{}
+
+func (a passAsNameArg) Append(_ ...string) Arg {
+	return passAsNameArg{}
+}
+func (passAsNameArg) Get(_ []string) []string {
+	return []string{}
+}
+
+var (
+	// DontPass indicates that the given argument will not actually be
+	// rendered.
+	DontPass Arg = dontPassArg{}
+	// PassAsName indicates that the given flag will be passed as `--key`
+	// without any value.
+	PassAsName Arg = passAsNameArg{}
+)
+
+// AsStrings serializes this set of arguments to a slice of strings appropriate
+// for passing to exec.Command and friends, making use of the given defaults
+// as indicated for each particular argument.
+//
+// - Any flag in defaults that's not in Arguments will be present in the output
+// - Any flag that's present in Arguments will be passed the corresponding
+//   defaults to do with as it will (ignore, append-to, suppress, etc).
+func (a *Arguments) AsStrings(defaults map[string][]string) []string {
+	// sort for deterministic ordering
+	keysInOrder := make([]string, 0, len(defaults)+len(a.values))
+	for key := range defaults {
+		if _, userSet := a.values[key]; userSet {
+			continue
+		}
+		keysInOrder = append(keysInOrder, key)
+	}
+	for key := range a.values {
+		keysInOrder = append(keysInOrder, key)
+	}
+	sort.Strings(keysInOrder)
+
+	var res []string
+	for _, key := range keysInOrder {
+		vals := a.Get(key).Get(defaults[key])
+		switch {
+		case vals == nil: // don't pass
+			continue
+		case len(vals) == 0: // pass as name
+			res = append(res, "--"+key)
+		default:
+			for _, val := range vals {
+				res = append(res, "--"+key+"="+val)
+			}
+		}
+	}
+
+	return res
+}
+
+// Get returns the value of the given flag.  If nil,
+// it will not be passed in AsString, otherwise:
+//
+// len == 0 --> `--key`, len > 0 --> `--key=val1 --key=val2 ...`.
+func (a *Arguments) Get(key string) Arg {
+	if vals, ok := a.values[key]; ok {
+		return vals
+	}
+	return defaultedArg(nil)
+}
+
+// Enable configures the given key to be passed as a "name-only" flag,
+// like, `--key`.
+func (a *Arguments) Enable(key string) *Arguments {
+	a.values[key] = PassAsName
+	return a
+}
+
+// Disable prevents this flag from be passed.
+func (a *Arguments) Disable(key string) *Arguments {
+	a.values[key] = DontPass
+	return a
+}
+
+// Append adds additional values to this flag.  If this flag has
+// yet to be set, initial values will include defaults.  If you want
+// to intentionally ignore defaults/start from scratch, call AppendNoDefaults.
+//
+// Multiple values will look like `--key=value1 --key=value2 ...`.
+func (a *Arguments) Append(key string, values ...string) *Arguments {
+	vals, present := a.values[key]
+	if !present {
+		vals = defaultedArg{}
+	}
+	a.values[key] = vals.Append(values...)
+	return a
+}
+
+// AppendNoDefaults adds additional values to this flag.  However,
+// unlike Append, it will *not* copy values from defaults.
+func (a *Arguments) AppendNoDefaults(key string, values ...string) *Arguments {
+	vals, present := a.values[key]
+	if !present {
+		vals = userArg{}
+	}
+	a.values[key] = vals.Append(values...)
+	return a
+}
+
+// Set resets the given flag to the specified values, ignoring any existing
+// values or defaults.
+func (a *Arguments) Set(key string, values ...string) *Arguments {
+	a.values[key] = userArg(values)
+	return a
+}
+
+// SetRaw sets the given flag to the given Arg value directly.  Use this if
+// you need to do some complicated deferred logic or something.
+//
+// Otherwise behaves like Set.
+func (a *Arguments) SetRaw(key string, val Arg) *Arguments {
+	a.values[key] = val
+	return a
+}
+
+// FuncArg is a basic implementation of Arg that can be used for custom argument logic,
+// like pulling values out of APIServer, or dynamically calculating values just before
+// launch.
+//
+// The given function will be mapped directly to Arg#Get, and will generally be
+// used in conjunction with SetRaw.  For example, to set `--some-flag` to the
+// API server's CertDir, you could do:
+//
+//     server.Configure().SetRaw("--some-flag", FuncArg(func(defaults []string) []string {
+//         return []string{server.CertDir}
+//     }))
+//
+// FuncArg ignores Appends; if you need to support appending values too, consider implementing
+// Arg directly.
+type FuncArg func([]string) []string
+
+// Append is a no-op for FuncArg, and just returns itself.
+func (a FuncArg) Append(vals ...string) Arg { return a }
+
+// Get delegates functionality to the FuncArg function itself.
+func (a FuncArg) Get(defaults []string) []string {
+	return a(defaults)
+}
diff --git a/vendor/sigs.k8s.io/controller-runtime/pkg/internal/testing/process/bin_path_finder.go b/vendor/sigs.k8s.io/controller-runtime/pkg/internal/testing/process/bin_path_finder.go
new file mode 100644
index 000000000..e1428aa6e
--- /dev/null
+++ b/vendor/sigs.k8s.io/controller-runtime/pkg/internal/testing/process/bin_path_finder.go
@@ -0,0 +1,70 @@
+/*
+Copyright 2021 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 process
+
+import (
+	"os"
+	"path/filepath"
+	"regexp"
+	"strings"
+)
+
+const (
+	// EnvAssetsPath is the environment variable that stores the global test
+	// binary location override.
+	EnvAssetsPath = "KUBEBUILDER_ASSETS"
+	// EnvAssetOverridePrefix is the environment variable prefix for per-binary
+	// location overrides.
+	EnvAssetOverridePrefix = "TEST_ASSET_"
+	// AssetsDefaultPath is the default location to look for test binaries in,
+	// if no override was provided.
+	AssetsDefaultPath = "/usr/local/kubebuilder/bin"
+)
+
+// BinPathFinder finds the path to the given named binary, using the following locations
+// in order of precedence (highest first).  Notice that the various env vars only need
+// to be set -- the asset is not checked for existence on the filesystem.
+//
+// 1. TEST_ASSET_{tr/a-z-/A-Z_/} (if set; asset overrides -- EnvAssetOverridePrefix)
+// 1. KUBEBUILDER_ASSETS (if set; global asset path -- EnvAssetsPath)
+// 3. assetDirectory (if set; per-config asset directory)
+// 4. /usr/local/kubebuilder/bin (AssetsDefaultPath).
+func BinPathFinder(symbolicName, assetDirectory string) (binPath string) {
+	punctuationPattern := regexp.MustCompile("[^A-Z0-9]+")
+	sanitizedName := punctuationPattern.ReplaceAllString(strings.ToUpper(symbolicName), "_")
+	leadingNumberPattern := regexp.MustCompile("^[0-9]+")
+	sanitizedName = leadingNumberPattern.ReplaceAllString(sanitizedName, "")
+	envVar := EnvAssetOverridePrefix + sanitizedName
+
+	// TEST_ASSET_XYZ
+	if val, ok := os.LookupEnv(envVar); ok {
+		return val
+	}
+
+	// KUBEBUILDER_ASSETS
+	if val, ok := os.LookupEnv(EnvAssetsPath); ok {
+		return filepath.Join(val, symbolicName)
+	}
+
+	// assetDirectory
+	if assetDirectory != "" {
+		return filepath.Join(assetDirectory, symbolicName)
+	}
+
+	// default path
+	return filepath.Join(AssetsDefaultPath, symbolicName)
+}
diff --git a/vendor/sigs.k8s.io/controller-runtime/pkg/internal/testing/process/process.go b/vendor/sigs.k8s.io/controller-runtime/pkg/internal/testing/process/process.go
new file mode 100644
index 000000000..531021bb2
--- /dev/null
+++ b/vendor/sigs.k8s.io/controller-runtime/pkg/internal/testing/process/process.go
@@ -0,0 +1,277 @@
+/*
+Copyright 2021 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 process
+
+import (
+	"crypto/tls"
+	"fmt"
+	"io"
+	"io/ioutil"
+	"net"
+	"net/http"
+	"net/url"
+	"os"
+	"os/exec"
+	"path"
+	"regexp"
+	"sync"
+	"syscall"
+	"time"
+)
+
+// ListenAddr represents some listening address and port.
+type ListenAddr struct {
+	Address string
+	Port    string
+}
+
+// URL returns a URL for this address with the given scheme and subpath.
+func (l *ListenAddr) URL(scheme string, path string) *url.URL {
+	return &url.URL{
+		Scheme: scheme,
+		Host:   l.HostPort(),
+		Path:   path,
+	}
+}
+
+// HostPort returns the joined host-port pair for this address.
+func (l *ListenAddr) HostPort() string {
+	return net.JoinHostPort(l.Address, l.Port)
+}
+
+// HealthCheck describes the information needed to health-check a process via
+// some health-check URL.
+type HealthCheck struct {
+	url.URL
+
+	// HealthCheckPollInterval is the interval which will be used for polling the
+	// endpoint described by Host, Port, and Path.
+	//
+	// If left empty it will default to 100 Milliseconds.
+	PollInterval time.Duration
+}
+
+// State define the state of the process.
+type State struct {
+	Cmd *exec.Cmd
+
+	// HealthCheck describes how to check if this process is up.  If we get an http.StatusOK,
+	// we assume the process is ready to operate.
+	//
+	// For example, the /healthz endpoint of the k8s API server, or the /health endpoint of etcd.
+	HealthCheck HealthCheck
+
+	Args []string
+
+	StopTimeout  time.Duration
+	StartTimeout time.Duration
+
+	Dir              string
+	DirNeedsCleaning bool
+	Path             string
+
+	// ready holds whether the process is currently in ready state (hit the ready condition) or not.
+	// It will be set to true on a successful `Start()` and set to false on a successful `Stop()`
+	ready bool
+
+	// waitDone is closed when our call to wait finishes up, and indicates that
+	// our process has terminated.
+	waitDone chan struct{}
+	errMu    sync.Mutex
+	exitErr  error
+	exited   bool
+}
+
+// Init sets up this process, configuring binary paths if missing, initializing
+// temporary directories, etc.
+//
+// This defaults all defaultable fields.
+func (ps *State) Init(name string) error {
+	if ps.Path == "" {
+		if name == "" {
+			return fmt.Errorf("must have at least one of name or path")
+		}
+		ps.Path = BinPathFinder(name, "")
+	}
+
+	if ps.Dir == "" {
+		newDir, err := ioutil.TempDir("", "k8s_test_framework_")
+		if err != nil {
+			return err
+		}
+		ps.Dir = newDir
+		ps.DirNeedsCleaning = true
+	}
+
+	if ps.StartTimeout == 0 {
+		ps.StartTimeout = 20 * time.Second
+	}
+
+	if ps.StopTimeout == 0 {
+		ps.StopTimeout = 20 * time.Second
+	}
+	return nil
+}
+
+type stopChannel chan struct{}
+
+// CheckFlag checks the help output of this command for the presence of the given flag, specified
+// without the leading `--` (e.g. `CheckFlag("insecure-port")` checks for `--insecure-port`),
+// returning true if the flag is present.
+func (ps *State) CheckFlag(flag string) (bool, error) {
+	cmd := exec.Command(ps.Path, "--help")
+	outContents, err := cmd.CombinedOutput()
+	if err != nil {
+		return false, fmt.Errorf("unable to run command %q to check for flag %q: %w", ps.Path, flag, err)
+	}
+	pat := `(?m)^\s*--` + flag + `\b` // (m --> multi-line --> ^ matches start of line)
+	matched, err := regexp.Match(pat, outContents)
+	if err != nil {
+		return false, fmt.Errorf("unable to check command %q for flag %q in help output: %w", ps.Path, flag, err)
+	}
+	return matched, nil
+}
+
+// Start starts the apiserver, waits for it to come up, and returns an error,
+// if occurred.
+func (ps *State) Start(stdout, stderr io.Writer) (err error) {
+	if ps.ready {
+		return nil
+	}
+
+	ps.Cmd = exec.Command(ps.Path, ps.Args...)
+	ps.Cmd.Stdout = stdout
+	ps.Cmd.Stderr = stderr
+
+	ready := make(chan bool)
+	timedOut := time.After(ps.StartTimeout)
+	pollerStopCh := make(stopChannel)
+	go pollURLUntilOK(ps.HealthCheck.URL, ps.HealthCheck.PollInterval, ready, pollerStopCh)
+
+	ps.waitDone = make(chan struct{})
+
+	if err := ps.Cmd.Start(); err != nil {
+		ps.errMu.Lock()
+		defer ps.errMu.Unlock()
+		ps.exited = true
+		return err
+	}
+	go func() {
+		defer close(ps.waitDone)
+		err := ps.Cmd.Wait()
+
+		ps.errMu.Lock()
+		defer ps.errMu.Unlock()
+		ps.exitErr = err
+		ps.exited = true
+	}()
+
+	select {
+	case <-ready:
+		ps.ready = true
+		return nil
+	case <-ps.waitDone:
+		if pollerStopCh != nil {
+			close(pollerStopCh)
+		}
+		return fmt.Errorf("timeout waiting for process %s to start successfully "+
+			"(it may have failed to start, or stopped unexpectedly before becoming ready)",
+			path.Base(ps.Path))
+	case <-timedOut:
+		if pollerStopCh != nil {
+			close(pollerStopCh)
+		}
+		if ps.Cmd != nil {
+			// intentionally ignore this -- we might've crashed, failed to start, etc
+			ps.Cmd.Process.Signal(syscall.SIGTERM) //nolint:errcheck
+		}
+		return fmt.Errorf("timeout waiting for process %s to start", path.Base(ps.Path))
+	}
+}
+
+// Exited returns true if the process exited, and may also
+// return an error (as per Cmd.Wait) if the process did not
+// exit with error code 0.
+func (ps *State) Exited() (bool, error) {
+	ps.errMu.Lock()
+	defer ps.errMu.Unlock()
+	return ps.exited, ps.exitErr
+}
+
+func pollURLUntilOK(url url.URL, interval time.Duration, ready chan bool, stopCh stopChannel) {
+	client := &http.Client{
+		Transport: &http.Transport{
+			TLSClientConfig: &tls.Config{
+				// there's probably certs *somewhere*,
+				// but it's fine to just skip validating
+				// them for health checks during testing
+				InsecureSkipVerify: true, //nolint:gosec
+			},
+		},
+	}
+	if interval <= 0 {
+		interval = 100 * time.Millisecond
+	}
+	for {
+		res, err := client.Get(url.String())
+		if err == nil {
+			res.Body.Close()
+			if res.StatusCode == http.StatusOK {
+				ready <- true
+				return
+			}
+		}
+
+		select {
+		case <-stopCh:
+			return
+		default:
+			time.Sleep(interval)
+		}
+	}
+}
+
+// Stop stops this process gracefully, waits for its termination, and cleans up
+// the CertDir if necessary.
+func (ps *State) Stop() error {
+	// Always clear the directory if we need to.
+	defer func() {
+		if ps.DirNeedsCleaning {
+			_ = os.RemoveAll(ps.Dir)
+		}
+	}()
+	if ps.Cmd == nil {
+		return nil
+	}
+	if done, _ := ps.Exited(); done {
+		return nil
+	}
+	if err := ps.Cmd.Process.Signal(syscall.SIGTERM); err != nil {
+		return fmt.Errorf("unable to signal for process %s to stop: %w", ps.Path, err)
+	}
+
+	timedOut := time.After(ps.StopTimeout)
+
+	select {
+	case <-ps.waitDone:
+		break
+	case <-timedOut:
+		return fmt.Errorf("timeout waiting for process %s to stop", path.Base(ps.Path))
+	}
+	ps.ready = false
+	return nil
+}