Repository: spidernet-io/spiderpool
Pull request #226 (2022-05-11 02:25:56)

k8s:Add webhook test suite
Signed-off-by: iiiceoo <ziqian.xue@daocloud.io>

sonarcloud[bot]: Kudos, SonarCloud Quality Gate passed!&nbsp; &nbsp; [![Quality Gate passed](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/QualityGateBadge/passed-16px.png 'Quality Gate passed')](https://sonarcloud.io/dashboard?id=spidernet-io_spiderpool&pullRequest=226)

[![Bug](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/bug-16px.png 'Bug')](https://sonarcloud.io/project/issues?id=spidernet-io_spiderpool&pullRequest=226&resolved=false&types=BUG) [![A](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/RatingBadge/A-16px.png 'A')](https://sonarcloud.io/project/issues?id=spidernet-io_spiderpool&pullRequest=226&resolved=false&types=BUG) [0 Bugs](https://sonarcloud.io/project/issues?id=spidernet-io_spiderpool&pullRequest=226&resolved=false&types=BUG)  
[![Vulnerability](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/vulnerability-16px.png 'Vulnerability')](https://sonarcloud.io/project/issues?id=spidernet-io_spiderpool&pullRequest=226&resolved=false&types=VULNERABILITY) [![A](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/RatingBadge/A-16px.png 'A')](https://sonarcloud.io/project/issues?id=spidernet-io_spiderpool&pullRequest=226&resolved=false&types=VULNERABILITY) [0 Vulnerabilities](https://sonarcloud.io/project/issues?id=spidernet-io_spiderpool&pullRequest=226&resolved=false&types=VULNERABILITY)  
[![Security Hotspot](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/security_hotspot-16px.png 'Security Hotspot')](https://sonarcloud.io/project/security_hotspots?id=spidernet-io_spiderpool&pullRequest=226&resolved=false&types=SECURITY_HOTSPOT) [![A](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/RatingBadge/A-16px.png 'A')](https://sonarcloud.io/project/security_hotspots?id=spidernet-io_spiderpool&pullRequest=226&resolved=false&types=SECURITY_HOTSPOT) [0 Security Hotspots](https://sonarcloud.io/project/security_hotspots?id=spidernet-io_spiderpool&pullRequest=226&resolved=false&types=SECURITY_HOTSPOT)  
[![Code Smell](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/code_smell-16px.png 'Code Smell')](https://sonarcloud.io/project/issues?id=spidernet-io_spiderpool&pullRequest=226&resolved=false&types=CODE_SMELL) [![A](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/RatingBadge/A-16px.png 'A')](https://sonarcloud.io/project/issues?id=spidernet-io_spiderpool&pullRequest=226&resolved=false&types=CODE_SMELL) [1 Code Smell](https://sonarcloud.io/project/issues?id=spidernet-io_spiderpool&pullRequest=226&resolved=false&types=CODE_SMELL)

[![No Coverage information](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/CoverageChart/NoCoverageInfo-16px.png 'No Coverage information')](https://sonarcloud.io/component_measures?id=spidernet-io_spiderpool&pullRequest=226) No Coverage information  
[![1.0%](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/Duplications/3-16px.png '1.0%')](https://sonarcloud.io/component_measures?id=spidernet-io_spiderpool&pullRequest=226&metric=new_duplicated_lines_density&view=list) [1.0% Duplication](https://sonarcloud.io/component_measures?id=spidernet-io_spiderpool&pullRequest=226&metric=new_duplicated_lines_density&view=list)


codecov[bot]: # [Codecov](https://codecov.io/gh/spidernet-io/spiderpool/pull/226?src=pr&el=h1&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=spidernet-io) Report
> Merging [#226](https://codecov.io/gh/spidernet-io/spiderpool/pull/226?src=pr&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=spidernet-io) (3c25e55) into [main](https://codecov.io/gh/spidernet-io/spiderpool/commit/9369124dc97f941593077779a2e3bba45fb80c1d?el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=spidernet-io) (9369124) will **decrease** coverage by `5.02%`.
> The diff coverage is `n/a`.

[![Impacted file tree graph](https://codecov.io/gh/spidernet-io/spiderpool/pull/226/graphs/tree.svg?width=650&height=150&src=pr&token=YKXY2E4Q8G&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=spidernet-io)](https://codecov.io/gh/spidernet-io/spiderpool/pull/226?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=spidernet-io)

```diff
@@            Coverage Diff             @@
##             main     #226      +/-   ##
==========================================
- Coverage   73.95%   68.93%   -5.03%     
==========================================
  Files           4        5       +1     
  Lines         192      206      +14     
==========================================
  Hits          142      142              
- Misses         37       51      +14     
  Partials       13       13              
```

| Flag | Coverage Δ | |
|---|---|---|
| unittests | `68.93% <ø> (-5.03%)` | :arrow_down: |

Flags with carried forward coverage won't be shown. [Click here](https://docs.codecov.io/docs/carryforward-flags?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=spidernet-io#carryforward-flags-in-the-pull-request-comment) to find out more.

| [Impacted Files](https://codecov.io/gh/spidernet-io/spiderpool/pull/226?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=spidernet-io) | Coverage Δ | |
|---|---|---|
| [pkg/webhook/ippool\_webhook.go](https://codecov.io/gh/spidernet-io/spiderpool/pull/226/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=spidernet-io#diff-cGtnL3dlYmhvb2svaXBwb29sX3dlYmhvb2suZ28=) | `0.00% <0.00%> (ø)` | |

sonarcloud[bot]: Kudos, SonarCloud Quality Gate passed!&nbsp; &nbsp; [![Quality Gate passed](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/QualityGateBadge/passed-16px.png 'Quality Gate passed')](https://sonarcloud.io/dashboard?id=spidernet-io_spiderpool&pullRequest=226)

[![Bug](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/bug-16px.png 'Bug')](https://sonarcloud.io/project/issues?id=spidernet-io_spiderpool&pullRequest=226&resolved=false&types=BUG) [![A](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/RatingBadge/A-16px.png 'A')](https://sonarcloud.io/project/issues?id=spidernet-io_spiderpool&pullRequest=226&resolved=false&types=BUG) [0 Bugs](https://sonarcloud.io/project/issues?id=spidernet-io_spiderpool&pullRequest=226&resolved=false&types=BUG)  
[![Vulnerability](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/vulnerability-16px.png 'Vulnerability')](https://sonarcloud.io/project/issues?id=spidernet-io_spiderpool&pullRequest=226&resolved=false&types=VULNERABILITY) [![A](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/RatingBadge/A-16px.png 'A')](https://sonarcloud.io/project/issues?id=spidernet-io_spiderpool&pullRequest=226&resolved=false&types=VULNERABILITY) [0 Vulnerabilities](https://sonarcloud.io/project/issues?id=spidernet-io_spiderpool&pullRequest=226&resolved=false&types=VULNERABILITY)  
[![Security Hotspot](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/security_hotspot-16px.png 'Security Hotspot')](https://sonarcloud.io/project/security_hotspots?id=spidernet-io_spiderpool&pullRequest=226&resolved=false&types=SECURITY_HOTSPOT) [![A](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/RatingBadge/A-16px.png 'A')](https://sonarcloud.io/project/security_hotspots?id=spidernet-io_spiderpool&pullRequest=226&resolved=false&types=SECURITY_HOTSPOT) [0 Security Hotspots](https://sonarcloud.io/project/security_hotspots?id=spidernet-io_spiderpool&pullRequest=226&resolved=false&types=SECURITY_HOTSPOT)  
[![Code Smell](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/code_smell-16px.png 'Code Smell')](https://sonarcloud.io/project/issues?id=spidernet-io_spiderpool&pullRequest=226&resolved=false&types=CODE_SMELL) [![A](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/RatingBadge/A-16px.png 'A')](https://sonarcloud.io/project/issues?id=spidernet-io_spiderpool&pullRequest=226&resolved=false&types=CODE_SMELL) [1 Code Smell](https://sonarcloud.io/project/issues?id=spidernet-io_spiderpool&pullRequest=226&resolved=false&types=CODE_SMELL)

[![No Coverage information](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/CoverageChart/NoCoverageInfo-16px.png 'No Coverage information')](https://sonarcloud.io/component_measures?id=spidernet-io_spiderpool&pullRequest=226) No Coverage information  
[![1.0%](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/Duplications/3-16px.png '1.0%')](https://sonarcloud.io/component_measures?id=spidernet-io_spiderpool&pullRequest=226&metric=new_duplicated_lines_density&view=list) [1.0% Duplication](https://sonarcloud.io/component_measures?id=spidernet-io_spiderpool&pullRequest=226&metric=new_duplicated_lines_density&view=list)



Patch:
diff --git a/cmd/spiderpool-agent/cmd/daemon.go b/cmd/spiderpool-agent/cmd/daemon.go
index a51490b45..7cf599e0f 100644
--- a/cmd/spiderpool-agent/cmd/daemon.go
+++ b/cmd/spiderpool-agent/cmd/daemon.go
@@ -20,7 +20,7 @@ func DaemonMain() {
 	signal.Notify(sigCh, syscall.SIGTERM, syscall.SIGINT)
 	go WatchSignal(sigCh)
 
-	logger.Info("Begin to initialize spiderpool-agent controller manager.")
+	logger.Info("Begin to initialize spiderpool-agent Controller Manager")
 	mgr, err := newControllerManager()
 	if nil != err {
 		logger.Fatal(err.Error())
@@ -28,7 +28,7 @@ func DaemonMain() {
 	agentContext.ControllerManagerCtx, agentContext.ControllerManagerCancel = context.WithCancel(context.Background())
 
 	go func() {
-		logger.Info("Starting spiderpool-agent controller manager.")
+		logger.Info("Starting spiderpool-agent Controller Manager")
 		if err := mgr.Start(agentContext.ControllerManagerCtx); err != nil {
 			logger.Fatal(err.Error())
 		}
@@ -84,7 +84,8 @@ func WatchSignal(sigCh chan os.Signal) {
 
 		// TODO: filter some signals
 
-		// TODO
+		// Cancel the context of Controller Manager.
+		// This stops things like the CRD's Informer, Webhook, Controller, etc.
 		if agentContext.ControllerManagerCancel != nil {
 			agentContext.ControllerManagerCancel()
 		}
diff --git a/cmd/spiderpool-controller/cmd/controller_manager.go b/cmd/spiderpool-controller/cmd/controller_manager.go
index e54d27d84..1a81edd4e 100644
--- a/cmd/spiderpool-controller/cmd/controller_manager.go
+++ b/cmd/spiderpool-controller/cmd/controller_manager.go
@@ -26,7 +26,8 @@ func init() {
 
 func newControllerManager() (ctrl.Manager, error) {
 	mgr, err := ctrl.NewManager(ctrl.GetConfigOrDie(), ctrl.Options{
-		Scheme:                 scheme,
+		Scheme: scheme,
+		// TODO: Add port...
 		MetricsBindAddress:     "0",
 		HealthProbeBindAddress: "0",
 	})
diff --git a/cmd/spiderpool-controller/cmd/daemon.go b/cmd/spiderpool-controller/cmd/daemon.go
index eaa133c51..6baa93c81 100644
--- a/cmd/spiderpool-controller/cmd/daemon.go
+++ b/cmd/spiderpool-controller/cmd/daemon.go
@@ -19,7 +19,7 @@ func DaemonMain() {
 	signal.Notify(sigCh, syscall.SIGTERM, syscall.SIGINT)
 	go WatchSignal(sigCh)
 
-	logger.Info("Begin to initialize spiderpool-controller controller manager.")
+	logger.Info("Begin to initialize spiderpool-controller Controller Manager")
 	mgr, err := newControllerManager()
 	if nil != err {
 		logger.Fatal(err.Error())
@@ -34,7 +34,7 @@ func DaemonMain() {
 	controllerContext.HttpServer = srv
 
 	go func() {
-		logger.Info("Starting spiderpool-agent controller manager.")
+		logger.Info("Starting spiderpool-agent Controller Manager")
 		if err := mgr.Start(controllerContext.ControllerManagerCtx); err != nil {
 			logger.Fatal(err.Error())
 		}
@@ -62,7 +62,8 @@ func WatchSignal(sigCh chan os.Signal) {
 
 		// TODO: filter some signals
 
-		// TODO
+		// Cancel the context of Controller Manager.
+		// This stops things like the CRD's Informer, Webhook, Controller, etc.
 		if controllerContext.ControllerManagerCancel != nil {
 			controllerContext.ControllerManagerCancel()
 		}
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 0d5e906fa..d5b002e75 100644
--- a/vendor/modules.txt
+++ b/vendor/modules.txt
@@ -597,6 +597,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
@@ -775,6 +779,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
@@ -826,15 +831,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/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
+}