Repository: kubernetes-sigs/controller-runtime

Issue:
envtest: Not respecting custom apiserver/etcd paths
It appears if I provide custom paths to envtest.Environment struct it ignores them. Minimal repro:

```
func TestNewEnvironment(t *testing.T) {
	testEnv := &envtest.Environment{
		ControlPlane: envtest.ControlPlane{
			APIServer: &envtest.APIServer{
				Path: "/Users/abalkan/workspace/my-project/bin/kubebuilder_1.30.3_darwin_amd64/kube-apiserver",
			},
			Etcd: &envtest.Etcd{
				Path: "/Users/abalkan/workspace/my-project/bin/kubebuilder_1.30.3_darwin_amd64/etcd",
			},
		},
	}

	// Start the control plane
	cfg, err := testEnv.Start()
	if err != nil {
		t.Fatalf("Failed to start control plane: %v", err)
	}
}
```

Gives error:

```
Failed to start control plane: unable to start control plane itself: failed to start the controlplane. retried 5 times: fork/exec /usr/local/kubebuilder/bin/etcd: no such file or directory
```

so it's looking at the wrong path despite the provided path override.

Currently one has to do `t.Setenv("TEST_ASSET_KUBE_APISERVER", "...")` to make this work I think, which is a little less than ideal.

Discussion:
@alvaroaleman: Fixed in #3372

Solution patch:
diff --git a/.golangci.yml b/.golangci.yml
index 5f8edd56..88fa3535 100644
--- a/.golangci.yml
+++ b/.golangci.yml
@@ -11,6 +11,7 @@ linters:
     - bidichk
     - bodyclose
     - copyloopvar
+    - depguard
     - dogsled
     - dupl
     - errcheck
@@ -42,6 +43,12 @@ linters:
     - unused
     - whitespace
   settings:
+    depguard:
+      rules:
+        forbid-pkg-errors:
+          deny:
+            - pkg: sort
+              desc: Should be replaced with slices package
     forbidigo:
         forbid:
           - pattern: context.Background
diff --git a/alias.go b/alias.go
index 01ba012d..dde2c5b9 100644
--- a/alias.go
+++ b/alias.go
@@ -18,6 +18,7 @@ package controllerruntime
 
 import (
 	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
+	"k8s.io/apimachinery/pkg/runtime"
 	"k8s.io/apimachinery/pkg/runtime/schema"
 	"sigs.k8s.io/controller-runtime/pkg/builder"
 	"sigs.k8s.io/controller-runtime/pkg/client/config"
@@ -104,9 +105,6 @@ var (
 	// NewControllerManagedBy returns a new controller builder that will be started by the provided Manager.
 	NewControllerManagedBy = builder.ControllerManagedBy
 
-	// NewWebhookManagedBy returns a new webhook builder that will be started by the provided Manager.
-	NewWebhookManagedBy = builder.WebhookManagedBy
-
 	// NewManager returns a new Manager for creating Controllers.
 	// Note that if ContentType in the given config is not set, "application/vnd.kubernetes.protobuf"
 	// will be used for all built-in resources of Kubernetes, and "application/json" is for other types
@@ -155,3 +153,8 @@ var (
 	// SetLogger sets a concrete logging implementation for all deferred Loggers.
 	SetLogger = log.SetLogger
 )
+
+// NewWebhookManagedBy returns a new webhook builder for the provided type T.
+func NewWebhookManagedBy[T runtime.Object](mgr manager.Manager, obj T) *builder.WebhookBuilder[T] {
+	return builder.WebhookManagedBy(mgr, obj)
+}
diff --git a/examples/builtins/main.go b/examples/builtins/main.go
index 3a47814d..f30c6525 100644
--- a/examples/builtins/main.go
+++ b/examples/builtins/main.go
@@ -60,8 +60,7 @@ func main() {
 		os.Exit(1)
 	}
 
-	if err := ctrl.NewWebhookManagedBy(mgr).
-		For(&corev1.Pod{}).
+	if err := ctrl.NewWebhookManagedBy(mgr, &corev1.Pod{}).
 		WithDefaulter(&podAnnotator{}).
 		WithValidator(&podValidator{}).
 		Complete(); err != nil {
diff --git a/examples/builtins/mutatingwebhook.go b/examples/builtins/mutatingwebhook.go
index a588eba8..0f150c9b 100644
--- a/examples/builtins/mutatingwebhook.go
+++ b/examples/builtins/mutatingwebhook.go
@@ -18,10 +18,8 @@ package main
 
 import (
 	"context"
-	"fmt"
 
 	corev1 "k8s.io/api/core/v1"
-	"k8s.io/apimachinery/pkg/runtime"
 
 	logf "sigs.k8s.io/controller-runtime/pkg/log"
 )
@@ -31,12 +29,8 @@ import (
 // podAnnotator annotates Pods
 type podAnnotator struct{}
 
-func (a *podAnnotator) Default(ctx context.Context, obj runtime.Object) error {
+func (a *podAnnotator) Default(ctx context.Context, pod *corev1.Pod) error {
 	log := logf.FromContext(ctx)
-	pod, ok := obj.(*corev1.Pod)
-	if !ok {
-		return fmt.Errorf("expected a Pod but got a %T", obj)
-	}
 
 	if pod.Annotations == nil {
 		pod.Annotations = map[string]string{}
diff --git a/examples/builtins/validatingwebhook.go b/examples/builtins/validatingwebhook.go
index 1bee7f7c..eb081596 100644
--- a/examples/builtins/validatingwebhook.go
+++ b/examples/builtins/validatingwebhook.go
@@ -21,7 +21,6 @@ import (
 	"fmt"
 
 	corev1 "k8s.io/api/core/v1"
-	"k8s.io/apimachinery/pkg/runtime"
 
 	logf "sigs.k8s.io/controller-runtime/pkg/log"
 	"sigs.k8s.io/controller-runtime/pkg/webhook/admission"
@@ -33,12 +32,8 @@ import (
 type podValidator struct{}
 
 // validate admits a pod if a specific annotation exists.
-func (v *podValidator) validate(ctx context.Context, obj runtime.Object) (admission.Warnings, error) {
+func (v *podValidator) validate(ctx context.Context, pod *corev1.Pod) (admission.Warnings, error) {
 	log := logf.FromContext(ctx)
-	pod, ok := obj.(*corev1.Pod)
-	if !ok {
-		return nil, fmt.Errorf("expected a Pod but got a %T", obj)
-	}
 
 	log.Info("Validating Pod")
 	key := "example-mutating-admission-webhook"
@@ -53,14 +48,14 @@ func (v *podValidator) validate(ctx context.Context, obj runtime.Object) (admiss
 	return nil, nil
 }
 
-func (v *podValidator) ValidateCreate(ctx context.Context, obj runtime.Object) (admission.Warnings, error) {
+func (v *podValidator) ValidateCreate(ctx context.Context, obj *corev1.Pod) (admission.Warnings, error) {
 	return v.validate(ctx, obj)
 }
 
-func (v *podValidator) ValidateUpdate(ctx context.Context, oldObj, newObj runtime.Object) (admission.Warnings, error) {
+func (v *podValidator) ValidateUpdate(ctx context.Context, oldObj, newObj *corev1.Pod) (admission.Warnings, error) {
 	return v.validate(ctx, newObj)
 }
 
-func (v *podValidator) ValidateDelete(ctx context.Context, obj runtime.Object) (admission.Warnings, error) {
+func (v *podValidator) ValidateDelete(ctx context.Context, obj *corev1.Pod) (admission.Warnings, error) {
 	return v.validate(ctx, obj)
 }
diff --git a/examples/crd/main.go b/examples/crd/main.go
index 0bf65c98..eec58abc 100644
--- a/examples/crd/main.go
+++ b/examples/crd/main.go
@@ -129,8 +129,7 @@ func main() {
 		os.Exit(1)
 	}
 
-	err = ctrl.NewWebhookManagedBy(mgr).
-		For(&api.ChaosPod{}).
+	err = ctrl.NewWebhookManagedBy(mgr, &api.ChaosPod{}).
 		Complete()
 	if err != nil {
 		setupLog.Error(err, "unable to create webhook")
diff --git a/pkg/builder/webhook.go b/pkg/builder/webhook.go
index bb5b6deb..428100a6 100644
--- a/pkg/builder/webhook.go
+++ b/pkg/builder/webhook.go
@@ -37,11 +37,13 @@ import (
 )
 
 // WebhookBuilder builds a Webhook.
-type WebhookBuilder struct {
+type WebhookBuilder[T runtime.Object] struct {
 	apiType                   runtime.Object
 	customDefaulter           admission.CustomDefaulter
+	defaulter                 admission.Defaulter[T]
 	customDefaulterOpts       []admission.DefaulterOption
 	customValidator           admission.CustomValidator
+	validator                 admission.Validator[T]
 	customPath                string
 	customValidatorCustomPath string
 	customDefaulterCustomPath string
@@ -56,59 +58,61 @@ type WebhookBuilder struct {
 }
 
 // WebhookManagedBy returns a new webhook builder.
-func WebhookManagedBy(m manager.Manager) *WebhookBuilder {
-	return &WebhookBuilder{mgr: m}
+func WebhookManagedBy[T runtime.Object](m manager.Manager, object T) *WebhookBuilder[T] {
+	return &WebhookBuilder[T]{mgr: m, apiType: object}
 }
 
-// TODO(droot): update the GoDoc for conversion.
-
-// For takes a runtime.Object which should be a CR.
-// If the given object implements the admission.Defaulter interface, a MutatingWebhook will be wired for this type.
-// If the given object implements the admission.Validator interface, a ValidatingWebhook will be wired for this type.
-func (blder *WebhookBuilder) For(apiType runtime.Object) *WebhookBuilder {
-	if blder.apiType != nil {
-		blder.err = errors.New("For(...) should only be called once, could not assign multiple objects for webhook registration")
-	}
-	blder.apiType = apiType
+// WithCustomDefaulter takes an admission.CustomDefaulter interface, a MutatingWebhook with the provided opts (admission.DefaulterOption)
+// will be wired for this type.
+// Deprecated: Use WithDefaulter instead.
+func (blder *WebhookBuilder[T]) WithCustomDefaulter(defaulter admission.CustomDefaulter, opts ...admission.DefaulterOption) *WebhookBuilder[T] {
+	blder.customDefaulter = defaulter
+	blder.customDefaulterOpts = opts
 	return blder
 }
 
-// WithDefaulter takes an admission.CustomDefaulter interface, a MutatingWebhook with the provided opts (admission.DefaulterOption)
-// will be wired for this type.
-func (blder *WebhookBuilder) WithDefaulter(defaulter admission.CustomDefaulter, opts ...admission.DefaulterOption) *WebhookBuilder {
-	blder.customDefaulter = defaulter
+// WithDefaulter sets up the provided admission.Defaulter in a defaulting webhook.
+func (blder *WebhookBuilder[T]) WithDefaulter(defaulter admission.Defaulter[T], opts ...admission.DefaulterOption) *WebhookBuilder[T] {
+	blder.defaulter = defaulter
 	blder.customDefaulterOpts = opts
 	return blder
 }
 
-// WithValidator takes a admission.CustomValidator interface, a ValidatingWebhook will be wired for this type.
-func (blder *WebhookBuilder) WithValidator(validator admission.CustomValidator) *WebhookBuilder {
+// WithCustomValidator takes a admission.CustomValidator interface, a ValidatingWebhook will be wired for this type.
+// Deprecated: Use WithValidator instead.
+func (blder *WebhookBuilder[T]) WithCustomValidator(validator admission.CustomValidator) *WebhookBuilder[T] {
 	blder.customValidator = validator
 	return blder
 }
 
+// WithValidator sets up the provided admission.Validator in a validating webhook.
+func (blder *WebhookBuilder[T]) WithValidator(validator admission.Validator[T]) *WebhookBuilder[T] {
+	blder.validator = validator
+	return blder
+}
+
 // WithConverter takes a func that constructs a converter.Converter.
-// The Converter will then be used by the conversion endpoint for the type passed into For().
-func (blder *WebhookBuilder) WithConverter(converterConstructor func(*runtime.Scheme) (conversion.Converter, error)) *WebhookBuilder {
+// The Converter will then be used by the conversion endpoint for the type passed into NewWebhookManagedBy()
+func (blder *WebhookBuilder[T]) WithConverter(converterConstructor func(*runtime.Scheme) (conversion.Converter, error)) *WebhookBuilder[T] {
 	blder.converterConstructor = converterConstructor
 	return blder
 }
 
 // WithLogConstructor overrides the webhook's LogConstructor.
-func (blder *WebhookBuilder) WithLogConstructor(logConstructor func(base logr.Logger, req *admission.Request) logr.Logger) *WebhookBuilder {
+func (blder *WebhookBuilder[T]) WithLogConstructor(logConstructor func(base logr.Logger, req *admission.Request) logr.Logger) *WebhookBuilder[T] {
 	blder.logConstructor = logConstructor
 	return blder
 }
 
 // WithContextFunc overrides the webhook's WithContextFunc.
-func (blder *WebhookBuilder) WithContextFunc(contextFunc func(context.Context, *http.Request) context.Context) *WebhookBuilder {
+func (blder *WebhookBuilder[T]) WithContextFunc(contextFunc func(context.Context, *http.Request) context.Context) *WebhookBuilder[T] {
 	blder.contextFunc = contextFunc
 	return blder
 }
 
 // RecoverPanic indicates whether panics caused by the webhook should be recovered.
 // Defaults to true.
-func (blder *WebhookBuilder) RecoverPanic(recoverPanic bool) *WebhookBuilder {
+func (blder *WebhookBuilder[T]) RecoverPanic(recoverPanic bool) *WebhookBuilder[T] {
 	blder.recoverPanic = &recoverPanic
 	return blder
 }
@@ -117,25 +121,25 @@ func (blder *WebhookBuilder) RecoverPanic(recoverPanic bool) *WebhookBuilder {
 //
 // Deprecated: WithCustomPath should not be used anymore.
 // Please use WithValidatorCustomPath or WithDefaulterCustomPath instead.
-func (blder *WebhookBuilder) WithCustomPath(customPath string) *WebhookBuilder {
+func (blder *WebhookBuilder[T]) WithCustomPath(customPath string) *WebhookBuilder[T] {
 	blder.customPath = customPath
 	return blder
 }
 
 // WithValidatorCustomPath overrides the path of the Validator.
-func (blder *WebhookBuilder) WithValidatorCustomPath(customPath string) *WebhookBuilder {
+func (blder *WebhookBuilder[T]) WithValidatorCustomPath(customPath string) *WebhookBuilder[T] {
 	blder.customValidatorCustomPath = customPath
 	return blder
 }
 
 // WithDefaulterCustomPath overrides the path of the Defaulter.
-func (blder *WebhookBuilder) WithDefaulterCustomPath(customPath string) *WebhookBuilder {
+func (blder *WebhookBuilder[T]) WithDefaulterCustomPath(customPath string) *WebhookBuilder[T] {
 	blder.customDefaulterCustomPath = customPath
 	return blder
 }
 
 // Complete builds the webhook.
-func (blder *WebhookBuilder) Complete() error {
+func (blder *WebhookBuilder[T]) Complete() error {
 	// Set the Config
 	blder.loadRestConfig()
 
@@ -146,13 +150,13 @@ func (blder *WebhookBuilder) Complete() error {
 	return blder.registerWebhooks()
 }
 
-func (blder *WebhookBuilder) loadRestConfig() {
+func (blder *WebhookBuilder[T]) loadRestConfig() {
 	if blder.config == nil {
 		blder.config = blder.mgr.GetConfig()
 	}
 }
 
-func (blder *WebhookBuilder) setLogConstructor() {
+func (blder *WebhookBuilder[T]) setLogConstructor() {
 	if blder.logConstructor == nil {
 		blder.logConstructor = func(base logr.Logger, req *admission.Request) logr.Logger {
 			log := base.WithValues(
@@ -172,11 +176,11 @@ func (blder *WebhookBuilder) setLogConstructor() {
 	}
 }
 
-func (blder *WebhookBuilder) isThereCustomPathConflict() bool {
+func (blder *WebhookBuilder[T]) isThereCustomPathConflict() bool {
 	return (blder.customPath != "" && blder.customDefaulter != nil && blder.customValidator != nil) || (blder.customPath != "" && blder.customDefaulterCustomPath != "") || (blder.customPath != "" && blder.customValidatorCustomPath != "")
 }
 
-func (blder *WebhookBuilder) registerWebhooks() error {
+func (blder *WebhookBuilder[T]) registerWebhooks() error {
 	typ, err := blder.getType()
 	if err != nil {
 		return err
@@ -217,8 +221,11 @@ func (blder *WebhookBuilder) registerWebhooks() error {
 }
 
 // registerDefaultingWebhook registers a defaulting webhook if necessary.
-func (blder *WebhookBuilder) registerDefaultingWebhook() error {
-	mwh := blder.getDefaultingWebhook()
+func (blder *WebhookBuilder[T]) registerDefaultingWebhook() error {
+	mwh, err := blder.getDefaultingWebhook()
+	if err != nil {
+		return err
+	}
 	if mwh != nil {
 		mwh.LogConstructor = blder.logConstructor
 		mwh.WithContextFunc = blder.contextFunc
@@ -244,20 +251,28 @@ func (blder *WebhookBuilder) registerDefaultingWebhook() error {
 	return nil
 }
 
-func (blder *WebhookBuilder) getDefaultingWebhook() *admission.Webhook {
-	if defaulter := blder.customDefaulter; defaulter != nil {
-		w := admission.WithCustomDefaulter(blder.mgr.GetScheme(), blder.apiType, defaulter, blder.customDefaulterOpts...)
-		if blder.recoverPanic != nil {
-			w = w.WithRecoverPanic(*blder.recoverPanic)
+func (blder *WebhookBuilder[T]) getDefaultingWebhook() (*admission.Webhook, error) {
+	var w *admission.Webhook
+	if blder.defaulter != nil {
+		if blder.customDefaulter != nil {
+			return nil, errors.New("only one of Defaulter or CustomDefaulter can be set")
 		}
-		return w
+		w = admission.WithDefaulter(blder.mgr.GetScheme(), blder.defaulter, blder.customDefaulterOpts...)
+	} else if blder.customDefaulter != nil {
+		w = admission.WithCustomDefaulter(blder.mgr.GetScheme(), blder.apiType, blder.customDefaulter, blder.customDefaulterOpts...)
 	}
-	return nil
+	if w != nil && blder.recoverPanic != nil {
+		w = w.WithRecoverPanic(*blder.recoverPanic)
+	}
+	return w, nil
 }
 
 // registerValidatingWebhook registers a validating webhook if necessary.
-func (blder *WebhookBuilder) registerValidatingWebhook() error {
-	vwh := blder.getValidatingWebhook()
+func (blder *WebhookBuilder[T]) registerValidatingWebhook() error {
+	vwh, err := blder.getValidatingWebhook()
+	if err != nil {
+		return err
+	}
 	if vwh != nil {
 		vwh.LogConstructor = blder.logConstructor
 		vwh.WithContextFunc = blder.contextFunc
@@ -283,18 +298,23 @@ func (blder *WebhookBuilder) registerValidatingWebhook() error {
 	return nil
 }
 
-func (blder *WebhookBuilder) getValidatingWebhook() *admission.Webhook {
-	if validator := blder.customValidator; validator != nil {
-		w := admission.WithCustomValidator(blder.mgr.GetScheme(), blder.apiType, validator)
-		if blder.recoverPanic != nil {
-			w = w.WithRecoverPanic(*blder.recoverPanic)
+func (blder *WebhookBuilder[T]) getValidatingWebhook() (*admission.Webhook, error) {
+	var w *admission.Webhook
+	if blder.validator != nil {
+		if blder.customValidator != nil {
+			return nil, errors.New("only one of Validator or CustomValidator can be set")
 		}
-		return w
+		w = admission.WithValidator(blder.mgr.GetScheme(), blder.validator)
+	} else if blder.customValidator != nil {
+		w = admission.WithCustomValidator(blder.mgr.GetScheme(), blder.apiType, blder.customValidator)
 	}
-	return nil
+	if w != nil && blder.recoverPanic != nil {
+		w = w.WithRecoverPanic(*blder.recoverPanic)
+	}
+	return w, nil
 }
 
-func (blder *WebhookBuilder) registerConversionWebhook() error {
+func (blder *WebhookBuilder[T]) registerConversionWebhook() error {
 	if blder.converterConstructor != nil {
 		converter, err := blder.converterConstructor(blder.mgr.GetScheme())
 		if err != nil {
@@ -323,14 +343,14 @@ func (blder *WebhookBuilder) registerConversionWebhook() error {
 	return nil
 }
 
-func (blder *WebhookBuilder) getType() (runtime.Object, error) {
+func (blder *WebhookBuilder[T]) getType() (runtime.Object, error) {
 	if blder.apiType != nil {
 		return blder.apiType, nil
 	}
-	return nil, errors.New("For() must be called with a valid object")
+	return nil, errors.New("NewWebhookManagedBy() must be called with a valid object")
 }
 
-func (blder *WebhookBuilder) isAlreadyHandled(path string) bool {
+func (blder *WebhookBuilder[T]) isAlreadyHandled(path string) bool {
 	if blder.mgr.GetWebhookServer().WebhookMux() == nil {
 		return false
 	}
diff --git a/pkg/webhook/admission/defaulter_custom.go b/pkg/webhook/admission/defaulter_custom.go
index a703cbd2..1dc8af10 100644
--- a/pkg/webhook/admission/defaulter_custom.go
+++ b/pkg/webhook/admission/defaulter_custom.go
@@ -21,6 +21,7 @@ import (
 	"encoding/json"
 	"errors"
 	"net/http"
+	"reflect"
 	"slices"
 
 	"gomodules.xyz/jsonpatch/v2"
@@ -31,11 +32,15 @@ import (
 	"k8s.io/apimachinery/pkg/util/sets"
 )
 
-// CustomDefaulter defines functions for setting defaults on resources.
-type CustomDefaulter interface {
-	Default(ctx context.Context, obj runtime.Object) error
+// Defaulter defines functions for setting defaults on resources.
+type Defaulter[T runtime.Object] interface {
+	Default(ctx context.Context, obj T) error
 }
 
+// CustomDefaulter defines functions for setting defaults on resources.
+// Deprecated: CustomDefaulter is deprecated, use Defaulter instead
+type CustomDefaulter = Defaulter[runtime.Object]
+
 type defaulterOptions struct {
 	removeUnknownOrOmitableFields bool
 }
@@ -50,6 +55,29 @@ func DefaulterRemoveUnknownOrOmitableFields(o *defaulterOptions) {
 	o.removeUnknownOrOmitableFields = true
 }
 
+// WithDefaulter creates a new Webhook for a Defaulter interface.
+func WithDefaulter[T runtime.Object](scheme *runtime.Scheme, defaulter Defaulter[T], opts ...DefaulterOption) *Webhook {
+	options := &defaulterOptions{}
+	for _, o := range opts {
+		o(options)
+	}
+	return &Webhook{
+		Handler: &defaulterForType[T]{
+			defaulter:                     defaulter,
+			decoder:                       NewDecoder(scheme),
+			removeUnknownOrOmitableFields: options.removeUnknownOrOmitableFields,
+			new: func() T {
+				var zero T
+				typ := reflect.TypeOf(zero)
+				if typ.Kind() == reflect.Ptr {
+					return reflect.New(typ.Elem()).Interface().(T)
+				}
+				return zero
+			},
+		},
+	}
+}
+
 // WithCustomDefaulter creates a new Webhook for a CustomDefaulter interface.
 func WithCustomDefaulter(scheme *runtime.Scheme, obj runtime.Object, defaulter CustomDefaulter, opts ...DefaulterOption) *Webhook {
 	options := &defaulterOptions{}
@@ -57,33 +85,30 @@ func WithCustomDefaulter(scheme *runtime.Scheme, obj runtime.Object, defaulter C
 		o(options)
 	}
 	return &Webhook{
-		Handler: &defaulterForType{
-			object:                        obj,
+		Handler: &defaulterForType[runtime.Object]{
 			defaulter:                     defaulter,
 			decoder:                       NewDecoder(scheme),
 			removeUnknownOrOmitableFields: options.removeUnknownOrOmitableFields,
+			new:                           func() runtime.Object { return obj.DeepCopyObject() },
 		},
 	}
 }
 
-type defaulterForType struct {
-	defaulter                     CustomDefaulter
-	object                        runtime.Object
+type defaulterForType[T runtime.Object] struct {
+	defaulter                     Defaulter[T]
 	decoder                       Decoder
 	removeUnknownOrOmitableFields bool
+	new                           func() T
 }
 
 // Handle handles admission requests.
-func (h *defaulterForType) Handle(ctx context.Context, req Request) Response {
+func (h *defaulterForType[T]) Handle(ctx context.Context, req Request) Response {
 	if h.decoder == nil {
 		panic("decoder should never be nil")
 	}
 	if h.defaulter == nil {
 		panic("defaulter should never be nil")
 	}
-	if h.object == nil {
-		panic("object should never be nil")
-	}
 
 	// Always skip when a DELETE operation received in custom mutation handler.
 	if req.Operation == admissionv1.Delete {
@@ -98,15 +123,15 @@ func (h *defaulterForType) Handle(ctx context.Context, req Request) Response {
 	ctx = NewContextWithRequest(ctx, req)
 
 	// Get the object in the request
-	obj := h.object.DeepCopyObject()
+	obj := h.new()
 	if err := h.decoder.Decode(req, obj); err != nil {
 		return Errored(http.StatusBadRequest, err)
 	}
 
 	// Keep a copy of the object if needed
-	var originalObj runtime.Object
+	var originalObj T
 	if !h.removeUnknownOrOmitableFields {
-		originalObj = obj.DeepCopyObject()
+		originalObj = obj.DeepCopyObject().(T)
 	}
 
 	// Default the object
@@ -131,7 +156,7 @@ func (h *defaulterForType) Handle(ctx context.Context, req Request) Response {
 	return handlerResponse
 }
 
-func (h *defaulterForType) dropSchemeRemovals(r Response, original runtime.Object, raw []byte) Response {
+func (h *defaulterForType[T]) dropSchemeRemovals(r Response, original T, raw []byte) Response {
 	const opRemove = "remove"
 	if !r.Allowed || r.PatchType == nil {
 		return r
diff --git a/pkg/webhook/admission/validator_custom.go b/pkg/webhook/admission/validator_custom.go
index ef1be52a..abd68e88 100644
--- a/pkg/webhook/admission/validator_custom.go
+++ b/pkg/webhook/admission/validator_custom.go
@@ -21,6 +21,7 @@ import (
 	"errors"
 	"fmt"
 	"net/http"
+	"reflect"
 
 	v1 "k8s.io/api/admission/v1"
 	apierrors "k8s.io/apimachinery/pkg/api/errors"
@@ -30,54 +31,77 @@ import (
 // Warnings represents warning messages.
 type Warnings []string
 
-// CustomValidator defines functions for validating an operation.
+// Validator defines functions for validating an operation.
 // The object to be validated is passed into methods as a parameter.
-type CustomValidator interface {
+type Validator[T runtime.Object] interface {
 	// ValidateCreate validates the object on creation.
 	// The optional warnings will be added to the response as warning messages.
 	// Return an error if the object is invalid.
-	ValidateCreate(ctx context.Context, obj runtime.Object) (warnings Warnings, err error)
+	ValidateCreate(ctx context.Context, obj T) (warnings Warnings, err error)
 
 	// ValidateUpdate validates the object on update.
 	// The optional warnings will be added to the response as warning messages.
 	// Return an error if the object is invalid.
-	ValidateUpdate(ctx context.Context, oldObj, newObj runtime.Object) (warnings Warnings, err error)
+	ValidateUpdate(ctx context.Context, oldObj, newObj T) (warnings Warnings, err error)
 
 	// ValidateDelete validates the object on deletion.
 	// The optional warnings will be added to the response as warning messages.
 	// Return an error if the object is invalid.
-	ValidateDelete(ctx context.Context, obj runtime.Object) (warnings Warnings, err error)
+	ValidateDelete(ctx context.Context, obj T) (warnings Warnings, err error)
+}
+
+// CustomValidator defines functions for validating an operation.
+// Deprecated: CustomValidator is deprecated, use Validator instead
+type CustomValidator = Validator[runtime.Object]
+
+// WithValidator creates a new Webhook for validating the provided type.
+func WithValidator[T runtime.Object](scheme *runtime.Scheme, validator Validator[T]) *Webhook {
+	return &Webhook{
+		Handler: &validatorForType[T]{
+			validator: validator,
+			decoder:   NewDecoder(scheme),
+			new: func() T {
+				var zero T
+				typ := reflect.TypeOf(zero)
+				if typ.Kind() == reflect.Ptr {
+					return reflect.New(typ.Elem()).Interface().(T)
+				}
+				return zero
+			},
+		},
+	}
 }
 
-// WithCustomValidator creates a new Webhook for validating the provided type.
+// WithCustomValidator creates a new Webhook for a CustomValidator.
+// Deprecated: WithCustomValidator is deprecated, use WithValidator instead
 func WithCustomValidator(scheme *runtime.Scheme, obj runtime.Object, validator CustomValidator) *Webhook {
 	return &Webhook{
-		Handler: &validatorForType{object: obj, validator: validator, decoder: NewDecoder(scheme)},
+		Handler: &validatorForType[runtime.Object]{
+			validator: validator,
+			decoder:   NewDecoder(scheme),
+			new:       func() runtime.Object { return obj.DeepCopyObject() },
+		},
 	}
 }
 
-type validatorForType struct {
-	validator CustomValidator
-	object    runtime.Object
+type validatorForType[T runtime.Object] struct {
+	validator Validator[T]
 	decoder   Decoder
+	new       func() T
 }
 
 // Handle handles admission requests.
-func (h *validatorForType) Handle(ctx context.Context, req Request) Response {
+func (h *validatorForType[T]) Handle(ctx context.Context, req Request) Response {
 	if h.decoder == nil {
 		panic("decoder should never be nil")
 	}
 	if h.validator == nil {
 		panic("validator should never be nil")
 	}
-	if h.object == nil {
-		panic("object should never be nil")
-	}
 
 	ctx = NewContextWithRequest(ctx, req)
 
-	// Get the object in the request
-	obj := h.object.DeepCopyObject()
+	obj := h.new()
 
 	var err error
 	var warnings []string
@@ -93,7 +117,7 @@ func (h *validatorForType) Handle(ctx context.Context, req Request) Response {
 
 		warnings, err = h.validator.ValidateCreate(ctx, obj)
 	case v1.Update:
-		oldObj := obj.DeepCopyObject()
+		oldObj := h.new()
 		if err := h.decoder.DecodeRaw(req.Object, obj); err != nil {
 			return Errored(http.StatusBadRequest, err)
 		}
diff --git a/pkg/webhook/alias.go b/pkg/webhook/alias.go
index 2882e7ba..b4f16a3f 100644
--- a/pkg/webhook/alias.go
+++ b/pkg/webhook/alias.go
@@ -24,9 +24,11 @@ import (
 // define some aliases for common bits of the webhook functionality
 
 // CustomDefaulter defines functions for setting defaults on resources.
+// Deprecated: Use admission.Defaulter instead.
 type CustomDefaulter = admission.CustomDefaulter
 
 // CustomValidator defines functions for validating an operation.
+// Deprecated: Use admission.Validator instead.
 type CustomValidator = admission.CustomValidator
 
 // AdmissionRequest defines the input for an admission handler.



Test patch:
diff --git a/pkg/builder/example_webhook_test.go b/pkg/builder/example_webhook_test.go
index c26eba8a..133da472 100644
--- a/pkg/builder/example_webhook_test.go
+++ b/pkg/builder/example_webhook_test.go
@@ -40,8 +40,7 @@ func ExampleWebhookBuilder() {
 	}
 
 	err = builder.
-		WebhookManagedBy(mgr).         // Create the WebhookManagedBy
-		For(&examplegroup.ChaosPod{}). // ChaosPod is a CRD.
+		WebhookManagedBy(mgr, &examplegroup.ChaosPod{}). // Create the WebhookManagedBy
 		Complete()
 	if err != nil {
 		log.Error(err, "could not create webhook")
diff --git a/pkg/builder/webhook_test.go b/pkg/builder/webhook_test.go
index 72538ef7..e10e693a 100644
--- a/pkg/builder/webhook_test.go
+++ b/pkg/builder/webhook_test.go
@@ -85,35 +85,36 @@ func runTests(admissionReviewVersion string) {
 		close(stop)
 	})
 
-	It("should scaffold a custom defaulting webhook", func(specCtx SpecContext) {
-		By("creating a controller manager")
-		m, err := manager.New(cfg, manager.Options{})
-		ExpectWithOffset(1, err).NotTo(HaveOccurred())
+	DescribeTable("scaffold a defaulting webhook",
+		func(specCtx SpecContext, build func(*WebhookBuilder[*TestDefaulterObject])) {
+			By("creating a controller manager")
+			m, err := manager.New(cfg, manager.Options{})
+			ExpectWithOffset(1, err).NotTo(HaveOccurred())
 
-		By("registering the type in the Scheme")
-		builder := scheme.Builder{GroupVersion: testDefaulterGVK.GroupVersion()}
-		builder.Register(&TestDefaulter{}, &TestDefaulterList{})
-		err = builder.AddToScheme(m.GetScheme())
-		ExpectWithOffset(1, err).NotTo(HaveOccurred())
+			By("registering the type in the Scheme")
+			builder := scheme.Builder{GroupVersion: testDefaulterGVK.GroupVersion()}
+			builder.Register(&TestDefaulterObject{}, &TestDefaulterList{})
+			err = builder.AddToScheme(m.GetScheme())
+			ExpectWithOffset(1, err).NotTo(HaveOccurred())
 
-		err = WebhookManagedBy(m).
-			For(&TestDefaulter{}).
-			WithDefaulter(&TestCustomDefaulter{}).
-			WithLogConstructor(func(base logr.Logger, req *admission.Request) logr.Logger {
-				return admission.DefaultLogConstructor(testingLogger, req)
-			}).
-			Complete()
-		ExpectWithOffset(1, err).NotTo(HaveOccurred())
-		svr := m.GetWebhookServer()
-		ExpectWithOffset(1, svr).NotTo(BeNil())
+			webhookBuilder := WebhookManagedBy(m, &TestDefaulterObject{})
+			build(webhookBuilder)
+			err = webhookBuilder.
+				WithLogConstructor(func(base logr.Logger, req *admission.Request) logr.Logger {
+					return admission.DefaultLogConstructor(testingLogger, req)
+				}).
+				Complete()
+			ExpectWithOffset(1, err).NotTo(HaveOccurred())
+			svr := m.GetWebhookServer()
+			ExpectWithOffset(1, svr).NotTo(BeNil())
 
-		reader := strings.NewReader(admissionReviewGV + admissionReviewVersion + `",
+			reader := strings.NewReader(admissionReviewGV + admissionReviewVersion + `",
   "request":{
     "uid":"07e52e8d-4513-11e9-a716-42010a800270",
     "kind":{
       "group":"foo.test.org",
       "version":"v1",
-      "kind":"TestDefaulter"
+      "kind":"TestDefaulterObject"
     },
     "resource":{
       "group":"foo.test.org",
@@ -130,68 +131,76 @@ func runTests(admissionReviewVersion string) {
   }
 }`)
 
-		ctx, cancel := context.WithCancel(specCtx)
-		cancel()
-		err = svr.Start(ctx)
-		if err != nil && !os.IsNotExist(err) {
+			ctx, cancel := context.WithCancel(specCtx)
+			cancel()
+			err = svr.Start(ctx)
+			if err != nil && !os.IsNotExist(err) {
+				ExpectWithOffset(1, err).NotTo(HaveOccurred())
+			}
+
+			By("sending a request to a mutating webhook path")
+			path := generateMutatePath(testDefaulterGVK)
+			req := httptest.NewRequest("POST", svcBaseAddr+path, reader)
+			req.Header.Add("Content-Type", "application/json")
+			w := httptest.NewRecorder()
+			svr.WebhookMux().ServeHTTP(w, req)
+			ExpectWithOffset(1, w.Code).To(Equal(http.StatusOK))
+			By("sanity checking the response contains reasonable fields")
+			ExpectWithOffset(1, w.Body.String()).To(ContainSubstring(`"allowed":true`))
+			ExpectWithOffset(1, w.Body.String()).To(ContainSubstring(`"patch":`))
+			ExpectWithOffset(1, w.Body.String()).To(ContainSubstring(`"code":200`))
+			EventuallyWithOffset(1, logBuffer).Should(gbytes.Say(`"msg":"Defaulting object","object":{"name":"foo","namespace":"default"},"namespace":"default","name":"foo","resource":{"group":"foo.test.org","version":"v1","resource":"testdefaulter"},"user":"","requestID":"07e52e8d-4513-11e9-a716-42010a800270"`))
+
+			By("sending a request to a validating webhook path that doesn't exist")
+			path = generateValidatePath(testDefaulterGVK)
+			_, err = reader.Seek(0, 0)
 			ExpectWithOffset(1, err).NotTo(HaveOccurred())
-		}
-
-		By("sending a request to a mutating webhook path")
-		path := generateMutatePath(testDefaulterGVK)
-		req := httptest.NewRequest("POST", svcBaseAddr+path, reader)
-		req.Header.Add("Content-Type", "application/json")
-		w := httptest.NewRecorder()
-		svr.WebhookMux().ServeHTTP(w, req)
-		ExpectWithOffset(1, w.Code).To(Equal(http.StatusOK))
-		By("sanity checking the response contains reasonable fields")
-		ExpectWithOffset(1, w.Body).To(ContainSubstring(`"allowed":true`))
-		ExpectWithOffset(1, w.Body).To(ContainSubstring(`"patch":`))
-		ExpectWithOffset(1, w.Body).To(ContainSubstring(`"code":200`))
-		EventuallyWithOffset(1, logBuffer).Should(gbytes.Say(`"msg":"Defaulting object","object":{"name":"foo","namespace":"default"},"namespace":"default","name":"foo","resource":{"group":"foo.test.org","version":"v1","resource":"testdefaulter"},"user":"","requestID":"07e52e8d-4513-11e9-a716-42010a800270"`))
-
-		By("sending a request to a validating webhook path that doesn't exist")
-		path = generateValidatePath(testDefaulterGVK)
-		_, err = reader.Seek(0, 0)
-		ExpectWithOffset(1, err).NotTo(HaveOccurred())
-		req = httptest.NewRequest("POST", svcBaseAddr+path, reader)
-		req.Header.Add("Content-Type", "application/json")
-		w = httptest.NewRecorder()
-		svr.WebhookMux().ServeHTTP(w, req)
-		ExpectWithOffset(1, w.Code).To(Equal(http.StatusNotFound))
-	})
+			req = httptest.NewRequest("POST", svcBaseAddr+path, reader)
+			req.Header.Add("Content-Type", "application/json")
+			w = httptest.NewRecorder()
+			svr.WebhookMux().ServeHTTP(w, req)
+			ExpectWithOffset(1, w.Code).To(Equal(http.StatusNotFound))
+		},
+		Entry("Custom Defaulter", func(b *WebhookBuilder[*TestDefaulterObject]) {
+			b.WithCustomDefaulter(&TestCustomDefaulter{})
+		}),
+		Entry("Defaulter", func(b *WebhookBuilder[*TestDefaulterObject]) {
+			b.WithDefaulter(&testDefaulter{})
+		}),
+	)
 
-	It("should scaffold a custom defaulting webhook with a custom path", func(specCtx SpecContext) {
-		By("creating a controller manager")
-		m, err := manager.New(cfg, manager.Options{})
-		ExpectWithOffset(1, err).NotTo(HaveOccurred())
+	DescribeTable("should scaffold a custom defaulting webhook with a custom path",
+		func(specCtx SpecContext, build func(*WebhookBuilder[*TestDefaulterObject])) {
+			By("creating a controller manager")
+			m, err := manager.New(cfg, manager.Options{})
+			ExpectWithOffset(1, err).NotTo(HaveOccurred())
 
-		By("registering the type in the Scheme")
-		builder := scheme.Builder{GroupVersion: testDefaulterGVK.GroupVersion()}
-		builder.Register(&TestDefaulter{}, &TestDefaulterList{})
-		err = builder.AddToScheme(m.GetScheme())
-		ExpectWithOffset(1, err).NotTo(HaveOccurred())
+			By("registering the type in the Scheme")
+			builder := scheme.Builder{GroupVersion: testDefaulterGVK.GroupVersion()}
+			builder.Register(&TestDefaulterObject{}, &TestDefaulterList{})
+			err = builder.AddToScheme(m.GetScheme())
+			ExpectWithOffset(1, err).NotTo(HaveOccurred())
 
-		customPath := "/custom-defaulting-path"
-		err = WebhookManagedBy(m).
-			For(&TestDefaulter{}).
-			WithDefaulter(&TestCustomDefaulter{}).
-			WithLogConstructor(func(base logr.Logger, req *admission.Request) logr.Logger {
-				return admission.DefaultLogConstructor(testingLogger, req)
-			}).
-			WithDefaulterCustomPath(customPath).
-			Complete()
-		ExpectWithOffset(1, err).NotTo(HaveOccurred())
-		svr := m.GetWebhookServer()
-		ExpectWithOffset(1, svr).NotTo(BeNil())
+			customPath := "/custom-defaulting-path"
+			webhookBuilder := WebhookManagedBy(m, &TestDefaulterObject{})
+			build(webhookBuilder)
+			err = webhookBuilder.
+				WithLogConstructor(func(base logr.Logger, req *admission.Request) logr.Logger {
+					return admission.DefaultLogConstructor(testingLogger, req)
+				}).
+				WithDefaulterCustomPath(customPath).
+				Complete()
+			ExpectWithOffset(1, err).NotTo(HaveOccurred())
+			svr := m.GetWebhookServer()
+			ExpectWithOffset(1, svr).NotTo(BeNil())
 
-		reader := strings.NewReader(admissionReviewGV + admissionReviewVersion + `",
+			reader := strings.NewReader(admissionReviewGV + admissionReviewVersion + `",
   "request":{
     "uid":"07e52e8d-4513-11e9-a716-42010a800270",
     "kind":{
       "group":"foo.test.org",
       "version":"v1",
-      "kind":"TestDefaulter"
+      "kind":"TestDefaulterObject"
     },
     "resource":{
       "group":"foo.test.org",
@@ -208,66 +217,73 @@ func runTests(admissionReviewVersion string) {
   }
 }`)
 
-		ctx, cancel := context.WithCancel(specCtx)
-		cancel()
-		err = svr.Start(ctx)
-		if err != nil && !os.IsNotExist(err) {
-			ExpectWithOffset(1, err).NotTo(HaveOccurred())
-		}
+			ctx, cancel := context.WithCancel(specCtx)
+			cancel()
+			err = svr.Start(ctx)
+			if err != nil && !os.IsNotExist(err) {
+				ExpectWithOffset(1, err).NotTo(HaveOccurred())
+			}
 
-		By("sending a request to a mutating webhook path that have been overriten by a custom path")
-		path, err := generateCustomPath(customPath)
-		ExpectWithOffset(1, err).NotTo(HaveOccurred())
-		req := httptest.NewRequest("POST", svcBaseAddr+path, reader)
-		req.Header.Add("Content-Type", "application/json")
-		w := httptest.NewRecorder()
-		svr.WebhookMux().ServeHTTP(w, req)
-		ExpectWithOffset(1, w.Code).To(Equal(http.StatusOK))
-		By("sanity checking the response contains reasonable fields")
-		ExpectWithOffset(1, w.Body).To(ContainSubstring(`"allowed":true`))
-		ExpectWithOffset(1, w.Body).To(ContainSubstring(`"patch":`))
-		ExpectWithOffset(1, w.Body).To(ContainSubstring(`"code":200`))
-		EventuallyWithOffset(1, logBuffer).Should(gbytes.Say(`"msg":"Defaulting object","object":{"name":"foo","namespace":"default"},"namespace":"default","name":"foo","resource":{"group":"foo.test.org","version":"v1","resource":"testdefaulter"},"user":"","requestID":"07e52e8d-4513-11e9-a716-42010a800270"`))
-
-		By("sending a request to a mutating webhook path")
-		path = generateMutatePath(testDefaulterGVK)
-		_, err = reader.Seek(0, 0)
-		ExpectWithOffset(1, err).NotTo(HaveOccurred())
-		req = httptest.NewRequest("POST", svcBaseAddr+path, reader)
-		req.Header.Add("Content-Type", "application/json")
-		w = httptest.NewRecorder()
-		svr.WebhookMux().ServeHTTP(w, req)
-		ExpectWithOffset(1, w.Code).To(Equal(http.StatusNotFound))
-	})
+			By("sending a request to a mutating webhook path that have been overriten by a custom path")
+			path, err := generateCustomPath(customPath)
+			ExpectWithOffset(1, err).NotTo(HaveOccurred())
+			req := httptest.NewRequest("POST", svcBaseAddr+path, reader)
+			req.Header.Add("Content-Type", "application/json")
+			w := httptest.NewRecorder()
+			svr.WebhookMux().ServeHTTP(w, req)
+			ExpectWithOffset(1, w.Code).To(Equal(http.StatusOK))
+			By("sanity checking the response contains reasonable fields")
+			ExpectWithOffset(1, w.Body.String()).To(ContainSubstring(`"allowed":true`))
+			ExpectWithOffset(1, w.Body.String()).To(ContainSubstring(`"patch":`))
+			ExpectWithOffset(1, w.Body.String()).To(ContainSubstring(`"code":200`))
+			EventuallyWithOffset(1, logBuffer).Should(gbytes.Say(`"msg":"Defaulting object","object":{"name":"foo","namespace":"default"},"namespace":"default","name":"foo","resource":{"group":"foo.test.org","version":"v1","resource":"testdefaulter"},"user":"","requestID":"07e52e8d-4513-11e9-a716-42010a800270"`))
+
+			By("sending a request to a mutating webhook path")
+			path = generateMutatePath(testDefaulterGVK)
+			_, err = reader.Seek(0, 0)
+			ExpectWithOffset(1, err).NotTo(HaveOccurred())
+			req = httptest.NewRequest("POST", svcBaseAddr+path, reader)
+			req.Header.Add("Content-Type", "application/json")
+			w = httptest.NewRecorder()
+			svr.WebhookMux().ServeHTTP(w, req)
+			ExpectWithOffset(1, w.Code).To(Equal(http.StatusNotFound))
+		},
+		Entry("Custom Defaulter", func(b *WebhookBuilder[*TestDefaulterObject]) {
+			b.WithCustomDefaulter(&TestCustomDefaulter{})
+		}),
+		Entry("Defaulter", func(b *WebhookBuilder[*TestDefaulterObject]) {
+			b.WithDefaulter(&testDefaulter{})
+		}),
+	)
 
-	It("should scaffold a custom defaulting webhook which recovers from panics", func(specCtx SpecContext) {
-		By("creating a controller manager")
-		m, err := manager.New(cfg, manager.Options{})
-		ExpectWithOffset(1, err).NotTo(HaveOccurred())
+	DescribeTable("should scaffold a custom defaulting webhook which recovers from panics",
+		func(specCtx SpecContext, build func(*WebhookBuilder[*TestDefaulterObject])) {
+			By("creating a controller manager")
+			m, err := manager.New(cfg, manager.Options{})
+			ExpectWithOffset(1, err).NotTo(HaveOccurred())
 
-		By("registering the type in the Scheme")
-		builder := scheme.Builder{GroupVersion: testDefaulterGVK.GroupVersion()}
-		builder.Register(&TestDefaulter{}, &TestDefaulterList{})
-		err = builder.AddToScheme(m.GetScheme())
-		ExpectWithOffset(1, err).NotTo(HaveOccurred())
+			By("registering the type in the Scheme")
+			builder := scheme.Builder{GroupVersion: testDefaulterGVK.GroupVersion()}
+			builder.Register(&TestDefaulterObject{}, &TestDefaulterList{})
+			err = builder.AddToScheme(m.GetScheme())
+			ExpectWithOffset(1, err).NotTo(HaveOccurred())
 
-		err = WebhookManagedBy(m).
-			For(&TestDefaulter{}).
-			WithDefaulter(&TestCustomDefaulter{}).
-			RecoverPanic(true).
-			// RecoverPanic defaults to true.
-			Complete()
-		ExpectWithOffset(1, err).NotTo(HaveOccurred())
-		svr := m.GetWebhookServer()
-		ExpectWithOffset(1, svr).NotTo(BeNil())
+			webhookBuilder := WebhookManagedBy(m, &TestDefaulterObject{})
+			build(webhookBuilder)
+			err = webhookBuilder.
+				RecoverPanic(true).
+				Complete()
+			ExpectWithOffset(1, err).NotTo(HaveOccurred())
+			svr := m.GetWebhookServer()
+			ExpectWithOffset(1, svr).NotTo(BeNil())
 
-		reader := strings.NewReader(admissionReviewGV + admissionReviewVersion + `",
+			reader := strings.NewReader(admissionReviewGV + admissionReviewVersion + `",
   "request":{
     "uid":"07e52e8d-4513-11e9-a716-42010a800270",
     "kind":{
       "group":"",
       "version":"v1",
-      "kind":"TestDefaulter"
+      "kind":"TestDefaulterObject"
     },
     "resource":{
       "group":"",
@@ -284,58 +300,65 @@ func runTests(admissionReviewVersion string) {
   }
 }`)
 
-		ctx, cancel := context.WithCancel(specCtx)
-		cancel()
-		err = svr.Start(ctx)
-		if err != nil && !os.IsNotExist(err) {
-			ExpectWithOffset(1, err).NotTo(HaveOccurred())
-		}
-
-		By("sending a request to a mutating webhook path")
-		path := generateMutatePath(testDefaulterGVK)
-		req := httptest.NewRequest("POST", svcBaseAddr+path, reader)
-		req.Header.Add("Content-Type", "application/json")
-		w := httptest.NewRecorder()
-		svr.WebhookMux().ServeHTTP(w, req)
-		ExpectWithOffset(1, w.Code).To(Equal(http.StatusOK))
-		By("sanity checking the response contains reasonable fields")
-		ExpectWithOffset(1, w.Body).To(ContainSubstring(`"allowed":false`))
-		ExpectWithOffset(1, w.Body).To(ContainSubstring(`"code":500`))
-		ExpectWithOffset(1, w.Body).To(ContainSubstring(`"message":"panic: fake panic test [recovered]`))
-	})
+			ctx, cancel := context.WithCancel(specCtx)
+			cancel()
+			err = svr.Start(ctx)
+			if err != nil && !os.IsNotExist(err) {
+				ExpectWithOffset(1, err).NotTo(HaveOccurred())
+			}
+
+			By("sending a request to a mutating webhook path")
+			path := generateMutatePath(testDefaulterGVK)
+			req := httptest.NewRequest("POST", svcBaseAddr+path, reader)
+			req.Header.Add("Content-Type", "application/json")
+			w := httptest.NewRecorder()
+			svr.WebhookMux().ServeHTTP(w, req)
+			ExpectWithOffset(1, w.Code).To(Equal(http.StatusOK))
+			By("sanity checking the response contains reasonable fields")
+			ExpectWithOffset(1, w.Body).To(ContainSubstring(`"allowed":false`))
+			ExpectWithOffset(1, w.Body).To(ContainSubstring(`"code":500`))
+			ExpectWithOffset(1, w.Body).To(ContainSubstring(`"message":"panic: fake panic test [recovered]`))
+		},
+		Entry("CustomDefaulter", func(b *WebhookBuilder[*TestDefaulterObject]) {
+			b.WithCustomDefaulter(&TestCustomDefaulter{})
+		}),
+		Entry("Defaulter", func(b *WebhookBuilder[*TestDefaulterObject]) {
+			b.WithDefaulter(&testDefaulter{})
+		}),
+	)
 
-	It("should scaffold a custom validating webhook", func(specCtx SpecContext) {
-		By("creating a controller manager")
-		m, err := manager.New(cfg, manager.Options{})
-		ExpectWithOffset(1, err).NotTo(HaveOccurred())
+	DescribeTable("should scaffold a custom validating webhook",
+		func(specCtx SpecContext, build func(*WebhookBuilder[*TestValidatorObject])) {
+			By("creating a controller manager")
+			m, err := manager.New(cfg, manager.Options{})
+			ExpectWithOffset(1, err).NotTo(HaveOccurred())
 
-		By("registering the type in the Scheme")
-		builder := scheme.Builder{GroupVersion: testValidatorGVK.GroupVersion()}
-		builder.Register(&TestValidator{}, &TestValidatorList{})
-		err = builder.AddToScheme(m.GetScheme())
-		ExpectWithOffset(1, err).NotTo(HaveOccurred())
+			By("registering the type in the Scheme")
+			builder := scheme.Builder{GroupVersion: testValidatorGVK.GroupVersion()}
+			builder.Register(&TestValidatorObject{}, &TestValidatorList{})
+			err = builder.AddToScheme(m.GetScheme())
+			ExpectWithOffset(1, err).NotTo(HaveOccurred())
 
-		err = WebhookManagedBy(m).
-			For(&TestValidator{}).
-			WithValidator(&TestCustomValidator{}).
-			WithLogConstructor(func(base logr.Logger, req *admission.Request) logr.Logger {
+			webhook := WebhookManagedBy(m, &TestValidatorObject{})
+			build(webhook)
+			err = webhook.WithLogConstructor(func(base logr.Logger, req *admission.Request) logr.Logger {
 				return admission.DefaultLogConstructor(testingLogger, req)
 			}).
-			WithContextFunc(func(ctx context.Context, request *http.Request) context.Context {
-				return context.WithValue(ctx, userAgentCtxKey, request.Header.Get(userAgentHeader))
-			}).
-			Complete()
-		ExpectWithOffset(1, err).NotTo(HaveOccurred())
-		svr := m.GetWebhookServer()
-		ExpectWithOffset(1, svr).NotTo(BeNil())
+				WithContextFunc(func(ctx context.Context, request *http.Request) context.Context {
+					return context.WithValue(ctx, userAgentCtxKey, request.Header.Get(userAgentHeader))
+				}).
+				Complete()
+			ExpectWithOffset(1, err).NotTo(HaveOccurred())
+			svr := m.GetWebhookServer()
+			ExpectWithOffset(1, svr).NotTo(BeNil())
 
-		reader := strings.NewReader(admissionReviewGV + admissionReviewVersion + `",
+			reader := strings.NewReader(admissionReviewGV + admissionReviewVersion + `",
   "request":{
     "uid":"07e52e8d-4513-11e9-a716-42010a800270",
     "kind":{
       "group":"foo.test.org",
       "version":"v1",
-      "kind":"TestValidator"
+      "kind":"TestValidatorObject"
     },
     "resource":{
       "group":"foo.test.org",
@@ -353,13 +376,13 @@ func runTests(admissionReviewVersion string) {
     }
   }
 }`)
-		readerWithCxt := strings.NewReader(admissionReviewGV + admissionReviewVersion + `",
+			readerWithCxt := strings.NewReader(admissionReviewGV + admissionReviewVersion + `",
   "request":{
     "uid":"07e52e8d-4513-11e9-a716-42010a800270",
     "kind":{
       "group":"foo.test.org",
       "version":"v1",
-      "kind":"TestValidator"
+      "kind":"TestValidatorObject"
     },
     "resource":{
       "group":"foo.test.org",
@@ -378,81 +401,88 @@ func runTests(admissionReviewVersion string) {
   }
 }`)
 
-		ctx, cancel := context.WithCancel(specCtx)
-		cancel()
-		err = svr.Start(ctx)
-		if err != nil && !os.IsNotExist(err) {
+			ctx, cancel := context.WithCancel(specCtx)
+			cancel()
+			err = svr.Start(ctx)
+			if err != nil && !os.IsNotExist(err) {
+				ExpectWithOffset(1, err).NotTo(HaveOccurred())
+			}
+
+			By("sending a request to a mutating webhook path that doesn't exist")
+			path := generateMutatePath(testValidatorGVK)
+			req := httptest.NewRequest("POST", svcBaseAddr+path, reader)
+			req.Header.Add("Content-Type", "application/json")
+			w := httptest.NewRecorder()
+			svr.WebhookMux().ServeHTTP(w, req)
+			ExpectWithOffset(1, w.Code).To(Equal(http.StatusNotFound))
+
+			By("sending a request to a validating webhook path")
+			path = generateValidatePath(testValidatorGVK)
+			_, err = reader.Seek(0, 0)
 			ExpectWithOffset(1, err).NotTo(HaveOccurred())
-		}
-
-		By("sending a request to a mutating webhook path that doesn't exist")
-		path := generateMutatePath(testValidatorGVK)
-		req := httptest.NewRequest("POST", svcBaseAddr+path, reader)
-		req.Header.Add("Content-Type", "application/json")
-		w := httptest.NewRecorder()
-		svr.WebhookMux().ServeHTTP(w, req)
-		ExpectWithOffset(1, w.Code).To(Equal(http.StatusNotFound))
-
-		By("sending a request to a validating webhook path")
-		path = generateValidatePath(testValidatorGVK)
-		_, err = reader.Seek(0, 0)
-		ExpectWithOffset(1, err).NotTo(HaveOccurred())
-		req = httptest.NewRequest("POST", svcBaseAddr+path, reader)
-		req.Header.Add("Content-Type", "application/json")
-		w = httptest.NewRecorder()
-		svr.WebhookMux().ServeHTTP(w, req)
-		ExpectWithOffset(1, w.Code).To(Equal(http.StatusOK))
-		By("sanity checking the response contains reasonable field")
-		ExpectWithOffset(1, w.Body).To(ContainSubstring(`"allowed":false`))
-		ExpectWithOffset(1, w.Body).To(ContainSubstring(`"code":403`))
-		EventuallyWithOffset(1, logBuffer).Should(gbytes.Say(`"msg":"Validating object","object":{"name":"foo","namespace":"default"},"namespace":"default","name":"foo","resource":{"group":"foo.test.org","version":"v1","resource":"testvalidator"},"user":"","requestID":"07e52e8d-4513-11e9-a716-42010a800270"`))
-
-		By("sending a request to a validating webhook with context header validation")
-		path = generateValidatePath(testValidatorGVK)
-		_, err = readerWithCxt.Seek(0, 0)
-		ExpectWithOffset(1, err).NotTo(HaveOccurred())
-		req = httptest.NewRequest("POST", svcBaseAddr+path, readerWithCxt)
-		req.Header.Add("Content-Type", "application/json")
-		req.Header.Add(userAgentHeader, userAgentValue)
-		w = httptest.NewRecorder()
-		svr.WebhookMux().ServeHTTP(w, req)
-		ExpectWithOffset(1, w.Code).To(Equal(http.StatusOK))
-		By("sanity checking the response contains reasonable field")
-		ExpectWithOffset(1, w.Body).To(ContainSubstring(`"allowed":true`))
-		ExpectWithOffset(1, w.Body).To(ContainSubstring(`"code":200`))
-	})
+			req = httptest.NewRequest("POST", svcBaseAddr+path, reader)
+			req.Header.Add("Content-Type", "application/json")
+			w = httptest.NewRecorder()
+			svr.WebhookMux().ServeHTTP(w, req)
+			ExpectWithOffset(1, w.Code).To(Equal(http.StatusOK))
+			By("sanity checking the response contains reasonable field")
+			ExpectWithOffset(1, w.Body.String()).To(ContainSubstring(`"allowed":false`))
+			ExpectWithOffset(1, w.Body.String()).To(ContainSubstring(`"code":403`))
+			EventuallyWithOffset(1, logBuffer).Should(gbytes.Say(`"msg":"Validating object","object":{"name":"foo","namespace":"default"},"namespace":"default","name":"foo","resource":{"group":"foo.test.org","version":"v1","resource":"testvalidator"},"user":"","requestID":"07e52e8d-4513-11e9-a716-42010a800270"`))
+
+			By("sending a request to a validating webhook with context header validation")
+			path = generateValidatePath(testValidatorGVK)
+			_, err = readerWithCxt.Seek(0, 0)
+			ExpectWithOffset(1, err).NotTo(HaveOccurred())
+			req = httptest.NewRequest("POST", svcBaseAddr+path, readerWithCxt)
+			req.Header.Add("Content-Type", "application/json")
+			req.Header.Add(userAgentHeader, userAgentValue)
+			w = httptest.NewRecorder()
+			svr.WebhookMux().ServeHTTP(w, req)
+			ExpectWithOffset(1, w.Code).To(Equal(http.StatusOK))
+			By("sanity checking the response contains reasonable field")
+			ExpectWithOffset(1, w.Body.String()).To(ContainSubstring(`"allowed":true`))
+			ExpectWithOffset(1, w.Body.String()).To(ContainSubstring(`"code":200`))
+		},
+		Entry("CustomValidator", func(b *WebhookBuilder[*TestValidatorObject]) {
+			b.WithCustomValidator(&TestCustomValidator{})
+		}),
+		Entry("Validator", func(b *WebhookBuilder[*TestValidatorObject]) {
+			b.WithValidator(&testValidator{})
+		}),
+	)
 
-	It("should scaffold a custom validating webhook with a custom path", func(specCtx SpecContext) {
-		By("creating a controller manager")
-		m, err := manager.New(cfg, manager.Options{})
-		ExpectWithOffset(1, err).NotTo(HaveOccurred())
+	DescribeTable("should scaffold a custom validating webhook with a custom path",
+		func(specCtx SpecContext, build func(*WebhookBuilder[*TestValidatorObject])) {
+			By("creating a controller manager")
+			m, err := manager.New(cfg, manager.Options{})
+			ExpectWithOffset(1, err).NotTo(HaveOccurred())
 
-		By("registering the type in the Scheme")
-		builder := scheme.Builder{GroupVersion: testValidatorGVK.GroupVersion()}
-		builder.Register(&TestValidator{}, &TestValidatorList{})
-		err = builder.AddToScheme(m.GetScheme())
-		ExpectWithOffset(1, err).NotTo(HaveOccurred())
+			By("registering the type in the Scheme")
+			builder := scheme.Builder{GroupVersion: testValidatorGVK.GroupVersion()}
+			builder.Register(&TestValidatorObject{}, &TestValidatorList{})
+			err = builder.AddToScheme(m.GetScheme())
+			ExpectWithOffset(1, err).NotTo(HaveOccurred())
 
-		customPath := "/custom-validating-path"
-		err = WebhookManagedBy(m).
-			For(&TestValidator{}).
-			WithValidator(&TestCustomValidator{}).
-			WithLogConstructor(func(base logr.Logger, req *admission.Request) logr.Logger {
+			customPath := "/custom-validating-path"
+			webhookBuilder := WebhookManagedBy(m, &TestValidatorObject{})
+			build(webhookBuilder)
+			err = webhookBuilder.WithLogConstructor(func(base logr.Logger, req *admission.Request) logr.Logger {
 				return admission.DefaultLogConstructor(testingLogger, req)
 			}).
-			WithValidatorCustomPath(customPath).
-			Complete()
-		ExpectWithOffset(1, err).NotTo(HaveOccurred())
-		svr := m.GetWebhookServer()
-		ExpectWithOffset(1, svr).NotTo(BeNil())
+				WithValidatorCustomPath(customPath).
+				Complete()
+			ExpectWithOffset(1, err).NotTo(HaveOccurred())
+			svr := m.GetWebhookServer()
+			ExpectWithOffset(1, svr).NotTo(BeNil())
 
-		reader := strings.NewReader(admissionReviewGV + admissionReviewVersion + `",
+			reader := strings.NewReader(admissionReviewGV + admissionReviewVersion + `",
   "request":{
     "uid":"07e52e8d-4513-11e9-a716-42010a800270",
     "kind":{
       "group":"foo.test.org",
       "version":"v1",
-      "kind":"TestValidator"
+      "kind":"TestValidatorObject"
     },
     "resource":{
       "group":"foo.test.org",
@@ -471,64 +501,71 @@ func runTests(admissionReviewVersion string) {
   }
 }`)
 
-		ctx, cancel := context.WithCancel(specCtx)
-		cancel()
-		err = svr.Start(ctx)
-		if err != nil && !os.IsNotExist(err) {
-			ExpectWithOffset(1, err).NotTo(HaveOccurred())
-		}
+			ctx, cancel := context.WithCancel(specCtx)
+			cancel()
+			err = svr.Start(ctx)
+			if err != nil && !os.IsNotExist(err) {
+				ExpectWithOffset(1, err).NotTo(HaveOccurred())
+			}
 
-		By("sending a request to a valiting webhook path that have been overriten by a custom path")
-		path, err := generateCustomPath(customPath)
-		ExpectWithOffset(1, err).NotTo(HaveOccurred())
-		_, err = reader.Seek(0, 0)
-		ExpectWithOffset(1, err).NotTo(HaveOccurred())
-		req := httptest.NewRequest("POST", svcBaseAddr+path, reader)
-		req.Header.Add("Content-Type", "application/json")
-		w := httptest.NewRecorder()
-		svr.WebhookMux().ServeHTTP(w, req)
-		ExpectWithOffset(1, w.Code).To(Equal(http.StatusOK))
-		By("sanity checking the response contains reasonable field")
-		ExpectWithOffset(1, w.Body).To(ContainSubstring(`"allowed":false`))
-		ExpectWithOffset(1, w.Body).To(ContainSubstring(`"code":403`))
-		EventuallyWithOffset(1, logBuffer).Should(gbytes.Say(`"msg":"Validating object","object":{"name":"foo","namespace":"default"},"namespace":"default","name":"foo","resource":{"group":"foo.test.org","version":"v1","resource":"testvalidator"},"user":"","requestID":"07e52e8d-4513-11e9-a716-42010a800270"`))
-
-		By("sending a request to a validating webhook path")
-		path = generateValidatePath(testValidatorGVK)
-		req = httptest.NewRequest("POST", svcBaseAddr+path, reader)
-		req.Header.Add("Content-Type", "application/json")
-		w = httptest.NewRecorder()
-		svr.WebhookMux().ServeHTTP(w, req)
-		ExpectWithOffset(1, w.Code).To(Equal(http.StatusNotFound))
-	})
+			By("sending a request to a valiting webhook path that have been overriten by a custom path")
+			path, err := generateCustomPath(customPath)
+			ExpectWithOffset(1, err).NotTo(HaveOccurred())
+			_, err = reader.Seek(0, 0)
+			ExpectWithOffset(1, err).NotTo(HaveOccurred())
+			req := httptest.NewRequest("POST", svcBaseAddr+path, reader)
+			req.Header.Add("Content-Type", "application/json")
+			w := httptest.NewRecorder()
+			svr.WebhookMux().ServeHTTP(w, req)
+			ExpectWithOffset(1, w.Code).To(Equal(http.StatusOK))
+			By("sanity checking the response contains reasonable field")
+			ExpectWithOffset(1, w.Body.String()).To(ContainSubstring(`"allowed":false`))
+			ExpectWithOffset(1, w.Body.String()).To(ContainSubstring(`"code":403`))
+			EventuallyWithOffset(1, logBuffer).Should(gbytes.Say(`"msg":"Validating object","object":{"name":"foo","namespace":"default"},"namespace":"default","name":"foo","resource":{"group":"foo.test.org","version":"v1","resource":"testvalidator"},"user":"","requestID":"07e52e8d-4513-11e9-a716-42010a800270"`))
+
+			By("sending a request to a validating webhook path")
+			path = generateValidatePath(testValidatorGVK)
+			req = httptest.NewRequest("POST", svcBaseAddr+path, reader)
+			req.Header.Add("Content-Type", "application/json")
+			w = httptest.NewRecorder()
+			svr.WebhookMux().ServeHTTP(w, req)
+			ExpectWithOffset(1, w.Code).To(Equal(http.StatusNotFound))
+		},
+		Entry("CustomValidator", func(b *WebhookBuilder[*TestValidatorObject]) {
+			b.WithCustomValidator(&TestCustomValidator{})
+		}),
+		Entry("Validator", func(b *WebhookBuilder[*TestValidatorObject]) {
+			b.WithValidator(&testValidator{})
+		}),
+	)
 
-	It("should scaffold a custom validating webhook which recovers from panics", func(specCtx SpecContext) {
-		By("creating a controller manager")
-		m, err := manager.New(cfg, manager.Options{})
-		ExpectWithOffset(1, err).NotTo(HaveOccurred())
+	DescribeTable("should scaffold a custom validating webhook which recovers from panics",
+		func(specCtx SpecContext, build func(*WebhookBuilder[*TestValidatorObject])) {
+			By("creating a controller manager")
+			m, err := manager.New(cfg, manager.Options{})
+			ExpectWithOffset(1, err).NotTo(HaveOccurred())
 
-		By("registering the type in the Scheme")
-		builder := scheme.Builder{GroupVersion: testValidatorGVK.GroupVersion()}
-		builder.Register(&TestValidator{}, &TestValidatorList{})
-		err = builder.AddToScheme(m.GetScheme())
-		ExpectWithOffset(1, err).NotTo(HaveOccurred())
+			By("registering the type in the Scheme")
+			builder := scheme.Builder{GroupVersion: testValidatorGVK.GroupVersion()}
+			builder.Register(&TestValidatorObject{}, &TestValidatorList{})
+			err = builder.AddToScheme(m.GetScheme())
+			ExpectWithOffset(1, err).NotTo(HaveOccurred())
 
-		err = WebhookManagedBy(m).
-			For(&TestValidator{}).
-			WithValidator(&TestCustomValidator{}).
-			RecoverPanic(true).
-			Complete()
-		ExpectWithOffset(1, err).NotTo(HaveOccurred())
-		svr := m.GetWebhookServer()
-		ExpectWithOffset(1, svr).NotTo(BeNil())
+			webhookBuilder := WebhookManagedBy(m, &TestValidatorObject{})
+			build(webhookBuilder)
+			err = webhookBuilder.RecoverPanic(true).
+				Complete()
+			ExpectWithOffset(1, err).NotTo(HaveOccurred())
+			svr := m.GetWebhookServer()
+			ExpectWithOffset(1, svr).NotTo(BeNil())
 
-		reader := strings.NewReader(admissionReviewGV + admissionReviewVersion + `",
+			reader := strings.NewReader(admissionReviewGV + admissionReviewVersion + `",
   "request":{
     "uid":"07e52e8d-4513-11e9-a716-42010a800270",
     "kind":{
       "group":"",
       "version":"v1",
-      "kind":"TestValidator"
+      "kind":"TestValidatorObject"
     },
     "resource":{
       "group":"",
@@ -544,56 +581,63 @@ func runTests(admissionReviewVersion string) {
   }
 }`)
 
-		ctx, cancel := context.WithCancel(specCtx)
-		cancel()
-		err = svr.Start(ctx)
-		if err != nil && !os.IsNotExist(err) {
-			ExpectWithOffset(1, err).NotTo(HaveOccurred())
-		}
+			ctx, cancel := context.WithCancel(specCtx)
+			cancel()
+			err = svr.Start(ctx)
+			if err != nil && !os.IsNotExist(err) {
+				ExpectWithOffset(1, err).NotTo(HaveOccurred())
+			}
 
-		By("sending a request to a validating webhook path")
-		path := generateValidatePath(testValidatorGVK)
-		_, err = reader.Seek(0, 0)
-		ExpectWithOffset(1, err).NotTo(HaveOccurred())
-		req := httptest.NewRequest("POST", svcBaseAddr+path, reader)
-		req.Header.Add("Content-Type", "application/json")
-		w := httptest.NewRecorder()
-		svr.WebhookMux().ServeHTTP(w, req)
-		ExpectWithOffset(1, w.Code).To(Equal(http.StatusOK))
-		By("sanity checking the response contains reasonable field")
-		ExpectWithOffset(1, w.Body).To(ContainSubstring(`"allowed":false`))
-		ExpectWithOffset(1, w.Body).To(ContainSubstring(`"code":500`))
-		ExpectWithOffset(1, w.Body).To(ContainSubstring(`"message":"panic: fake panic test [recovered]`))
-	})
+			By("sending a request to a validating webhook path")
+			path := generateValidatePath(testValidatorGVK)
+			_, err = reader.Seek(0, 0)
+			ExpectWithOffset(1, err).NotTo(HaveOccurred())
+			req := httptest.NewRequest("POST", svcBaseAddr+path, reader)
+			req.Header.Add("Content-Type", "application/json")
+			w := httptest.NewRecorder()
+			svr.WebhookMux().ServeHTTP(w, req)
+			ExpectWithOffset(1, w.Code).To(Equal(http.StatusOK))
+			By("sanity checking the response contains reasonable field")
+			ExpectWithOffset(1, w.Body).To(ContainSubstring(`"allowed":false`))
+			ExpectWithOffset(1, w.Body).To(ContainSubstring(`"code":500`))
+			ExpectWithOffset(1, w.Body).To(ContainSubstring(`"message":"panic: fake panic test [recovered]`))
+		},
+		Entry("CustomValidator", func(b *WebhookBuilder[*TestValidatorObject]) {
+			b.WithCustomValidator(&TestCustomValidator{})
+		}),
+		Entry("Validator", func(b *WebhookBuilder[*TestValidatorObject]) {
+			b.WithValidator(&testValidator{})
+		}),
+	)
 
-	It("should scaffold a custom validating webhook to validate deletes", func(specCtx SpecContext) {
-		By("creating a controller manager")
-		ctx, cancel := context.WithCancel(specCtx)
+	DescribeTable("should scaffold a custom validating webhook to validate deletes",
+		func(specCtx SpecContext, build func(*WebhookBuilder[*TestValidatorObject])) {
+			By("creating a controller manager")
+			ctx, cancel := context.WithCancel(specCtx)
 
-		m, err := manager.New(cfg, manager.Options{})
-		ExpectWithOffset(1, err).NotTo(HaveOccurred())
+			m, err := manager.New(cfg, manager.Options{})
+			ExpectWithOffset(1, err).NotTo(HaveOccurred())
 
-		By("registering the type in the Scheme")
-		builder := scheme.Builder{GroupVersion: testValidatorGVK.GroupVersion()}
-		builder.Register(&TestValidator{}, &TestValidatorList{})
-		err = builder.AddToScheme(m.GetScheme())
-		ExpectWithOffset(1, err).NotTo(HaveOccurred())
+			By("registering the type in the Scheme")
+			builder := scheme.Builder{GroupVersion: testValidatorGVK.GroupVersion()}
+			builder.Register(&TestValidatorObject{}, &TestValidatorList{})
+			err = builder.AddToScheme(m.GetScheme())
+			ExpectWithOffset(1, err).NotTo(HaveOccurred())
 
-		err = WebhookManagedBy(m).
-			For(&TestValidator{}).
-			WithValidator(&TestCustomValidator{}).
-			Complete()
-		ExpectWithOffset(1, err).NotTo(HaveOccurred())
-		svr := m.GetWebhookServer()
-		ExpectWithOffset(1, svr).NotTo(BeNil())
+			webhookBuilder := WebhookManagedBy(m, &TestValidatorObject{})
+			build(webhookBuilder)
+			err = webhookBuilder.Complete()
+			ExpectWithOffset(1, err).NotTo(HaveOccurred())
+			svr := m.GetWebhookServer()
+			ExpectWithOffset(1, svr).NotTo(BeNil())
 
-		reader := strings.NewReader(admissionReviewGV + admissionReviewVersion + `",
+			reader := strings.NewReader(admissionReviewGV + admissionReviewVersion + `",
   "request":{
     "uid":"07e52e8d-4513-11e9-a716-42010a800270",
     "kind":{
       "group":"",
       "version":"v1",
-      "kind":"TestValidator"
+      "kind":"TestValidatorObject"
     },
     "resource":{
       "group":"",
@@ -609,30 +653,30 @@ func runTests(admissionReviewVersion string) {
   }
 }`)
 
-		cancel()
-		err = svr.Start(ctx)
-		if err != nil && !os.IsNotExist(err) {
-			ExpectWithOffset(1, err).NotTo(HaveOccurred())
-		}
-
-		By("sending a request to a validating webhook path to check for failed delete")
-		path := generateValidatePath(testValidatorGVK)
-		req := httptest.NewRequest("POST", svcBaseAddr+path, reader)
-		req.Header.Add("Content-Type", "application/json")
-		w := httptest.NewRecorder()
-		svr.WebhookMux().ServeHTTP(w, req)
-		ExpectWithOffset(1, w.Code).To(Equal(http.StatusOK))
-		By("sanity checking the response contains reasonable field")
-		ExpectWithOffset(1, w.Body).To(ContainSubstring(`"allowed":false`))
-		ExpectWithOffset(1, w.Body).To(ContainSubstring(`"code":403`))
-
-		reader = strings.NewReader(admissionReviewGV + admissionReviewVersion + `",
+			cancel()
+			err = svr.Start(ctx)
+			if err != nil && !os.IsNotExist(err) {
+				ExpectWithOffset(1, err).NotTo(HaveOccurred())
+			}
+
+			By("sending a request to a validating webhook path to check for failed delete")
+			path := generateValidatePath(testValidatorGVK)
+			req := httptest.NewRequest("POST", svcBaseAddr+path, reader)
+			req.Header.Add("Content-Type", "application/json")
+			w := httptest.NewRecorder()
+			svr.WebhookMux().ServeHTTP(w, req)
+			ExpectWithOffset(1, w.Code).To(Equal(http.StatusOK))
+			By("sanity checking the response contains reasonable field")
+			ExpectWithOffset(1, w.Body).To(ContainSubstring(`"allowed":false`))
+			ExpectWithOffset(1, w.Body).To(ContainSubstring(`"code":403`))
+
+			reader = strings.NewReader(admissionReviewGV + admissionReviewVersion + `",
   "request":{
     "uid":"07e52e8d-4513-11e9-a716-42010a800270",
     "kind":{
       "group":"",
       "version":"v1",
-      "kind":"TestValidator"
+      "kind":"TestValidatorObject"
     },
     "resource":{
       "group":"",
@@ -647,60 +691,49 @@ func runTests(admissionReviewVersion string) {
     }
   }
 }`)
-		By("sending a request to a validating webhook path with correct request")
-		path = generateValidatePath(testValidatorGVK)
-		req = httptest.NewRequest("POST", svcBaseAddr+path, reader)
-		req.Header.Add("Content-Type", "application/json")
-		w = httptest.NewRecorder()
-		svr.WebhookMux().ServeHTTP(w, req)
-		ExpectWithOffset(1, w.Code).To(Equal(http.StatusOK))
-		By("sanity checking the response contains reasonable field")
-		ExpectWithOffset(1, w.Body).To(ContainSubstring(`"allowed":true`))
-		ExpectWithOffset(1, w.Body).To(ContainSubstring(`"code":200`))
-	})
-
-	It("should send an error when trying to register a webhook with more than one For", func() {
-		By("creating a controller manager")
-		m, err := manager.New(cfg, manager.Options{})
-		ExpectWithOffset(1, err).NotTo(HaveOccurred())
-
-		By("registering the type in the Scheme")
-		builder := scheme.Builder{GroupVersion: testDefaultValidatorGVK.GroupVersion()}
-		builder.Register(&TestDefaulter{}, &TestDefaulterList{})
-		err = builder.AddToScheme(m.GetScheme())
-		ExpectWithOffset(1, err).NotTo(HaveOccurred())
-
-		err = WebhookManagedBy(m).
-			For(&TestDefaulter{}).
-			For(&TestDefaulter{}).
-			Complete()
-		Expect(err).To(HaveOccurred())
-	})
+			By("sending a request to a validating webhook path with correct request")
+			path = generateValidatePath(testValidatorGVK)
+			req = httptest.NewRequest("POST", svcBaseAddr+path, reader)
+			req.Header.Add("Content-Type", "application/json")
+			w = httptest.NewRecorder()
+			svr.WebhookMux().ServeHTTP(w, req)
+			ExpectWithOffset(1, w.Code).To(Equal(http.StatusOK))
+			By("sanity checking the response contains reasonable field")
+			ExpectWithOffset(1, w.Body).To(ContainSubstring(`"allowed":true`))
+			ExpectWithOffset(1, w.Body).To(ContainSubstring(`"code":200`))
+		},
+		Entry("CustomValidator", func(b *WebhookBuilder[*TestValidatorObject]) {
+			b.WithCustomValidator(&TestCustomValidator{})
+		}),
+		Entry("Validator", func(b *WebhookBuilder[*TestValidatorObject]) {
+			b.WithValidator(&testValidator{})
+		}),
+	)
 
-	It("should scaffold a custom defaulting and validating webhook", func(specCtx SpecContext) {
-		By("creating a controller manager")
-		m, err := manager.New(cfg, manager.Options{})
-		ExpectWithOffset(1, err).NotTo(HaveOccurred())
+	DescribeTable("should scaffold a custom defaulting and validating webhook",
+		func(specCtx SpecContext, build func(*WebhookBuilder[*TestDefaultValidator])) {
+			By("creating a controller manager")
+			m, err := manager.New(cfg, manager.Options{})
+			ExpectWithOffset(1, err).NotTo(HaveOccurred())
 
-		By("registering the type in the Scheme")
-		builder := scheme.Builder{GroupVersion: testValidatorGVK.GroupVersion()}
-		builder.Register(&TestDefaultValidator{}, &TestDefaultValidatorList{})
-		err = builder.AddToScheme(m.GetScheme())
-		ExpectWithOffset(1, err).NotTo(HaveOccurred())
+			By("registering the type in the Scheme")
+			builder := scheme.Builder{GroupVersion: testValidatorGVK.GroupVersion()}
+			builder.Register(&TestDefaultValidator{}, &TestDefaultValidatorList{})
+			err = builder.AddToScheme(m.GetScheme())
+			ExpectWithOffset(1, err).NotTo(HaveOccurred())
 
-		err = WebhookManagedBy(m).
-			For(&TestDefaultValidator{}).
-			WithDefaulter(&TestCustomDefaultValidator{}).
-			WithValidator(&TestCustomDefaultValidator{}).
-			WithLogConstructor(func(base logr.Logger, req *admission.Request) logr.Logger {
-				return admission.DefaultLogConstructor(testingLogger, req)
-			}).
-			Complete()
-		ExpectWithOffset(1, err).NotTo(HaveOccurred())
-		svr := m.GetWebhookServer()
-		ExpectWithOffset(1, svr).NotTo(BeNil())
+			webhookBuilder := WebhookManagedBy(m, &TestDefaultValidator{})
+			build(webhookBuilder)
+			err = webhookBuilder.
+				WithLogConstructor(func(base logr.Logger, req *admission.Request) logr.Logger {
+					return admission.DefaultLogConstructor(testingLogger, req)
+				}).
+				Complete()
+			ExpectWithOffset(1, err).NotTo(HaveOccurred())
+			svr := m.GetWebhookServer()
+			ExpectWithOffset(1, svr).NotTo(BeNil())
 
-		reader := strings.NewReader(admissionReviewGV + admissionReviewVersion + `",
+			reader := strings.NewReader(admissionReviewGV + admissionReviewVersion + `",
   "request":{
     "uid":"07e52e8d-4513-11e9-a716-42010a800270",
     "kind":{
@@ -725,69 +758,86 @@ func runTests(admissionReviewVersion string) {
   }
 }`)
 
-		ctx, cancel := context.WithCancel(specCtx)
-		cancel()
-		err = svr.Start(ctx)
-		if err != nil && !os.IsNotExist(err) {
+			ctx, cancel := context.WithCancel(specCtx)
+			cancel()
+			err = svr.Start(ctx)
+			if err != nil && !os.IsNotExist(err) {
+				ExpectWithOffset(1, err).NotTo(HaveOccurred())
+			}
+
+			By("sending a request to a mutating webhook path")
+			path := generateMutatePath(testDefaultValidatorGVK)
+			req := httptest.NewRequest("POST", svcBaseAddr+path, reader)
+			req.Header.Add("Content-Type", "application/json")
+			w := httptest.NewRecorder()
+			svr.WebhookMux().ServeHTTP(w, req)
+			ExpectWithOffset(1, w.Code).To(Equal(http.StatusOK))
+			By("sanity checking the response contains reasonable fields")
+			ExpectWithOffset(1, w.Body).To(ContainSubstring(`"allowed":true`))
+			ExpectWithOffset(1, w.Body).To(ContainSubstring(`"patch":`))
+			ExpectWithOffset(1, w.Body).To(ContainSubstring(`"code":200`))
+			EventuallyWithOffset(1, logBuffer).Should(gbytes.Say(`"msg":"Defaulting object","object":{"name":"foo","namespace":"default"},"namespace":"default","name":"foo","resource":{"group":"foo.test.org","version":"v1","resource":"testdefaultvalidator"},"user":"","requestID":"07e52e8d-4513-11e9-a716-42010a800270"`))
+
+			By("sending a request to a validating webhook path")
+			path = generateValidatePath(testDefaultValidatorGVK)
+			_, err = reader.Seek(0, 0)
 			ExpectWithOffset(1, err).NotTo(HaveOccurred())
-		}
-
-		By("sending a request to a mutating webhook path")
-		path := generateMutatePath(testDefaultValidatorGVK)
-		req := httptest.NewRequest("POST", svcBaseAddr+path, reader)
-		req.Header.Add("Content-Type", "application/json")
-		w := httptest.NewRecorder()
-		svr.WebhookMux().ServeHTTP(w, req)
-		ExpectWithOffset(1, w.Code).To(Equal(http.StatusOK))
-		By("sanity checking the response contains reasonable fields")
-		ExpectWithOffset(1, w.Body).To(ContainSubstring(`"allowed":true`))
-		ExpectWithOffset(1, w.Body).To(ContainSubstring(`"patch":`))
-		ExpectWithOffset(1, w.Body).To(ContainSubstring(`"code":200`))
-		EventuallyWithOffset(1, logBuffer).Should(gbytes.Say(`"msg":"Defaulting object","object":{"name":"foo","namespace":"default"},"namespace":"default","name":"foo","resource":{"group":"foo.test.org","version":"v1","resource":"testdefaultvalidator"},"user":"","requestID":"07e52e8d-4513-11e9-a716-42010a800270"`))
-
-		By("sending a request to a validating webhook path")
-		path = generateValidatePath(testDefaultValidatorGVK)
-		_, err = reader.Seek(0, 0)
-		ExpectWithOffset(1, err).NotTo(HaveOccurred())
-		req = httptest.NewRequest("POST", svcBaseAddr+path, reader)
-		req.Header.Add("Content-Type", "application/json")
-		w = httptest.NewRecorder()
-		svr.WebhookMux().ServeHTTP(w, req)
-		ExpectWithOffset(1, w.Code).To(Equal(http.StatusOK))
-		By("sanity checking the response contains reasonable field")
-		ExpectWithOffset(1, w.Body).To(ContainSubstring(`"allowed":false`))
-		ExpectWithOffset(1, w.Body).To(ContainSubstring(`"code":403`))
-		EventuallyWithOffset(1, logBuffer).Should(gbytes.Say(`"msg":"Validating object","object":{"name":"foo","namespace":"default"},"namespace":"default","name":"foo","resource":{"group":"foo.test.org","version":"v1","resource":"testdefaultvalidator"},"user":"","requestID":"07e52e8d-4513-11e9-a716-42010a800270"`))
-	})
+			req = httptest.NewRequest("POST", svcBaseAddr+path, reader)
+			req.Header.Add("Content-Type", "application/json")
+			w = httptest.NewRecorder()
+			svr.WebhookMux().ServeHTTP(w, req)
+			ExpectWithOffset(1, w.Code).To(Equal(http.StatusOK))
+			By("sanity checking the response contains reasonable field")
+			ExpectWithOffset(1, w.Body).To(ContainSubstring(`"allowed":false`))
+			ExpectWithOffset(1, w.Body).To(ContainSubstring(`"code":403`))
+			EventuallyWithOffset(1, logBuffer).Should(gbytes.Say(`"msg":"Validating object","object":{"name":"foo","namespace":"default"},"namespace":"default","name":"foo","resource":{"group":"foo.test.org","version":"v1","resource":"testdefaultvalidator"},"user":"","requestID":"07e52e8d-4513-11e9-a716-42010a800270"`))
+		},
+		Entry("CustomDefaulter + CustomValidator", func(b *WebhookBuilder[*TestDefaultValidator]) {
+			b.WithCustomDefaulter(&TestCustomDefaultValidator{})
+			b.WithCustomValidator(&TestCustomDefaultValidator{})
+		}),
+		Entry("CustomDefaulter + Validator", func(b *WebhookBuilder[*TestDefaultValidator]) {
+			b.WithCustomDefaulter(&TestCustomDefaultValidator{})
+			b.WithValidator(&testDefaultValidatorValidator{})
+		}),
+		Entry("Defaulter + CustomValidator", func(b *WebhookBuilder[*TestDefaultValidator]) {
+			b.WithDefaulter(&testValidatorDefaulter{})
+			b.WithCustomValidator(&TestCustomDefaultValidator{})
+		}),
+		Entry("Defaulter + Validator", func(b *WebhookBuilder[*TestDefaultValidator]) {
+			b.WithDefaulter(&testValidatorDefaulter{})
+			b.WithValidator(&testDefaultValidatorValidator{})
+		}),
+	)
 
-	It("should scaffold a custom defaulting and validating webhook with a custom path for each of them", func(specCtx SpecContext) {
-		By("creating a controller manager")
-		m, err := manager.New(cfg, manager.Options{})
-		ExpectWithOffset(1, err).NotTo(HaveOccurred())
+	DescribeTable("should scaffold a custom defaulting and validating webhook with a custom path for each of them",
+		func(specCtx SpecContext, build func(*WebhookBuilder[*TestDefaultValidator])) {
+			By("creating a controller manager")
+			m, err := manager.New(cfg, manager.Options{})
+			ExpectWithOffset(1, err).NotTo(HaveOccurred())
 
-		By("registering the type in the Scheme")
-		builder := scheme.Builder{GroupVersion: testValidatorGVK.GroupVersion()}
-		builder.Register(&TestDefaultValidator{}, &TestDefaultValidatorList{})
-		err = builder.AddToScheme(m.GetScheme())
-		ExpectWithOffset(1, err).NotTo(HaveOccurred())
+			By("registering the type in the Scheme")
+			builder := scheme.Builder{GroupVersion: testValidatorGVK.GroupVersion()}
+			builder.Register(&TestDefaultValidator{}, &TestDefaultValidatorList{})
+			err = builder.AddToScheme(m.GetScheme())
+			ExpectWithOffset(1, err).NotTo(HaveOccurred())
 
-		validatingCustomPath := "/custom-validating-path"
-		defaultingCustomPath := "/custom-defaulting-path"
-		err = WebhookManagedBy(m).
-			For(&TestDefaultValidator{}).
-			WithDefaulter(&TestCustomDefaultValidator{}).
-			WithValidator(&TestCustomDefaultValidator{}).
-			WithLogConstructor(func(base logr.Logger, req *admission.Request) logr.Logger {
-				return admission.DefaultLogConstructor(testingLogger, req)
-			}).
-			WithValidatorCustomPath(validatingCustomPath).
-			WithDefaulterCustomPath(defaultingCustomPath).
-			Complete()
-		ExpectWithOffset(1, err).NotTo(HaveOccurred())
-		svr := m.GetWebhookServer()
-		ExpectWithOffset(1, svr).NotTo(BeNil())
+			validatingCustomPath := "/custom-validating-path"
+			defaultingCustomPath := "/custom-defaulting-path"
+			webhookBuilder := WebhookManagedBy(m, &TestDefaultValidator{})
+			build(webhookBuilder)
+			err = webhookBuilder.
+				WithLogConstructor(func(base logr.Logger, req *admission.Request) logr.Logger {
+					return admission.DefaultLogConstructor(testingLogger, req)
+				}).
+				WithValidatorCustomPath(validatingCustomPath).
+				WithDefaulterCustomPath(defaultingCustomPath).
+				Complete()
+			ExpectWithOffset(1, err).NotTo(HaveOccurred())
+			svr := m.GetWebhookServer()
+			ExpectWithOffset(1, svr).NotTo(BeNil())
 
-		reader := strings.NewReader(admissionReviewGV + admissionReviewVersion + `",
+			reader := strings.NewReader(admissionReviewGV + admissionReviewVersion + `",
   "request":{
     "uid":"07e52e8d-4513-11e9-a716-42010a800270",
     "kind":{
@@ -812,60 +862,77 @@ func runTests(admissionReviewVersion string) {
   }
 }`)
 
-		ctx, cancel := context.WithCancel(specCtx)
-		cancel()
-		err = svr.Start(ctx)
-		if err != nil && !os.IsNotExist(err) {
-			ExpectWithOffset(1, err).NotTo(HaveOccurred())
-		}
+			ctx, cancel := context.WithCancel(specCtx)
+			cancel()
+			err = svr.Start(ctx)
+			if err != nil && !os.IsNotExist(err) {
+				ExpectWithOffset(1, err).NotTo(HaveOccurred())
+			}
 
-		By("sending a request to a mutating webhook path that have been overriten by the custom path")
-		path, err := generateCustomPath(defaultingCustomPath)
-		ExpectWithOffset(1, err).NotTo(HaveOccurred())
-		req := httptest.NewRequest("POST", svcBaseAddr+path, reader)
-		req.Header.Add("Content-Type", "application/json")
-		w := httptest.NewRecorder()
-		svr.WebhookMux().ServeHTTP(w, req)
-		ExpectWithOffset(1, w.Code).To(Equal(http.StatusOK))
-		By("sanity checking the response contains reasonable fields")
-		ExpectWithOffset(1, w.Body).To(ContainSubstring(`"allowed":true`))
-		ExpectWithOffset(1, w.Body).To(ContainSubstring(`"patch":`))
-		ExpectWithOffset(1, w.Body).To(ContainSubstring(`"code":200`))
-		EventuallyWithOffset(1, logBuffer).Should(gbytes.Say(`"msg":"Defaulting object","object":{"name":"foo","namespace":"default"},"namespace":"default","name":"foo","resource":{"group":"foo.test.org","version":"v1","resource":"testdefaultvalidator"},"user":"","requestID":"07e52e8d-4513-11e9-a716-42010a800270"`))
-
-		By("sending a request to a mutating webhook path")
-		path = generateMutatePath(testDefaultValidatorGVK)
-		_, err = reader.Seek(0, 0)
-		ExpectWithOffset(1, err).NotTo(HaveOccurred())
-		req = httptest.NewRequest("POST", svcBaseAddr+path, reader)
-		req.Header.Add("Content-Type", "application/json")
-		w = httptest.NewRecorder()
-		svr.WebhookMux().ServeHTTP(w, req)
-		ExpectWithOffset(1, w.Code).To(Equal(http.StatusNotFound))
-
-		By("sending a request to a valiting webhook path that have been overriten by a custom path")
-		path, err = generateCustomPath(validatingCustomPath)
-		ExpectWithOffset(1, err).NotTo(HaveOccurred())
-		_, err = reader.Seek(0, 0)
-		ExpectWithOffset(1, err).NotTo(HaveOccurred())
-		req = httptest.NewRequest("POST", svcBaseAddr+path, reader)
-		req.Header.Add("Content-Type", "application/json")
-		w = httptest.NewRecorder()
-		svr.WebhookMux().ServeHTTP(w, req)
-		ExpectWithOffset(1, w.Code).To(Equal(http.StatusOK))
-		By("sanity checking the response contains reasonable field")
-		ExpectWithOffset(1, w.Body).To(ContainSubstring(`"allowed":false`))
-		ExpectWithOffset(1, w.Body).To(ContainSubstring(`"code":403`))
-		EventuallyWithOffset(1, logBuffer).Should(gbytes.Say(`"msg":"Validating object","object":{"name":"foo","namespace":"default"},"namespace":"default","name":"foo","resource":{"group":"foo.test.org","version":"v1","resource":"testdefaultvalidator"},"user":"","requestID":"07e52e8d-4513-11e9-a716-42010a800270"`))
-
-		By("sending a request to a validating webhook path")
-		path = generateValidatePath(testValidatorGVK)
-		req = httptest.NewRequest("POST", svcBaseAddr+path, reader)
-		req.Header.Add("Content-Type", "application/json")
-		w = httptest.NewRecorder()
-		svr.WebhookMux().ServeHTTP(w, req)
-		ExpectWithOffset(1, w.Code).To(Equal(http.StatusNotFound))
-	})
+			By("sending a request to a mutating webhook path that have been overriten by the custom path")
+			path, err := generateCustomPath(defaultingCustomPath)
+			ExpectWithOffset(1, err).NotTo(HaveOccurred())
+			req := httptest.NewRequest("POST", svcBaseAddr+path, reader)
+			req.Header.Add("Content-Type", "application/json")
+			w := httptest.NewRecorder()
+			svr.WebhookMux().ServeHTTP(w, req)
+			ExpectWithOffset(1, w.Code).To(Equal(http.StatusOK))
+			By("sanity checking the response contains reasonable fields")
+			ExpectWithOffset(1, w.Body).To(ContainSubstring(`"allowed":true`))
+			ExpectWithOffset(1, w.Body).To(ContainSubstring(`"patch":`))
+			ExpectWithOffset(1, w.Body).To(ContainSubstring(`"code":200`))
+			EventuallyWithOffset(1, logBuffer).Should(gbytes.Say(`"msg":"Defaulting object","object":{"name":"foo","namespace":"default"},"namespace":"default","name":"foo","resource":{"group":"foo.test.org","version":"v1","resource":"testdefaultvalidator"},"user":"","requestID":"07e52e8d-4513-11e9-a716-42010a800270"`))
+
+			By("sending a request to a mutating webhook path")
+			path = generateMutatePath(testDefaultValidatorGVK)
+			_, err = reader.Seek(0, 0)
+			ExpectWithOffset(1, err).NotTo(HaveOccurred())
+			req = httptest.NewRequest("POST", svcBaseAddr+path, reader)
+			req.Header.Add("Content-Type", "application/json")
+			w = httptest.NewRecorder()
+			svr.WebhookMux().ServeHTTP(w, req)
+			ExpectWithOffset(1, w.Code).To(Equal(http.StatusNotFound))
+
+			By("sending a request to a valiting webhook path that have been overriten by a custom path")
+			path, err = generateCustomPath(validatingCustomPath)
+			ExpectWithOffset(1, err).NotTo(HaveOccurred())
+			_, err = reader.Seek(0, 0)
+			ExpectWithOffset(1, err).NotTo(HaveOccurred())
+			req = httptest.NewRequest("POST", svcBaseAddr+path, reader)
+			req.Header.Add("Content-Type", "application/json")
+			w = httptest.NewRecorder()
+			svr.WebhookMux().ServeHTTP(w, req)
+			ExpectWithOffset(1, w.Code).To(Equal(http.StatusOK))
+			By("sanity checking the response contains reasonable field")
+			ExpectWithOffset(1, w.Body).To(ContainSubstring(`"allowed":false`))
+			ExpectWithOffset(1, w.Body).To(ContainSubstring(`"code":403`))
+			EventuallyWithOffset(1, logBuffer).Should(gbytes.Say(`"msg":"Validating object","object":{"name":"foo","namespace":"default"},"namespace":"default","name":"foo","resource":{"group":"foo.test.org","version":"v1","resource":"testdefaultvalidator"},"user":"","requestID":"07e52e8d-4513-11e9-a716-42010a800270"`))
+
+			By("sending a request to a validating webhook path")
+			path = generateValidatePath(testValidatorGVK)
+			req = httptest.NewRequest("POST", svcBaseAddr+path, reader)
+			req.Header.Add("Content-Type", "application/json")
+			w = httptest.NewRecorder()
+			svr.WebhookMux().ServeHTTP(w, req)
+			ExpectWithOffset(1, w.Code).To(Equal(http.StatusNotFound))
+		},
+		Entry("CustomDefaulter + CustomValidator", func(b *WebhookBuilder[*TestDefaultValidator]) {
+			b.WithCustomDefaulter(&TestCustomDefaultValidator{})
+			b.WithCustomValidator(&TestCustomDefaultValidator{})
+		}),
+		Entry("CustomDefaulter + Validator", func(b *WebhookBuilder[*TestDefaultValidator]) {
+			b.WithCustomDefaulter(&TestCustomDefaultValidator{})
+			b.WithValidator(&testDefaultValidatorValidator{})
+		}),
+		Entry("Defaulter + CustomValidator", func(b *WebhookBuilder[*TestDefaultValidator]) {
+			b.WithDefaulter(&testValidatorDefaulter{})
+			b.WithCustomValidator(&TestCustomDefaultValidator{})
+		}),
+		Entry("Defaulter + Validator", func(b *WebhookBuilder[*TestDefaultValidator]) {
+			b.WithDefaulter(&testValidatorDefaulter{})
+			b.WithValidator(&testDefaultValidatorValidator{})
+		}),
+	)
 
 	It("should not scaffold a custom defaulting and a custom validating webhook with the same custom path", func() {
 		By("creating a controller manager")
@@ -878,10 +945,9 @@ func runTests(admissionReviewVersion string) {
 		err = builder.AddToScheme(m.GetScheme())
 		ExpectWithOffset(1, err).NotTo(HaveOccurred())
 
-		err = WebhookManagedBy(m).
-			For(&TestDefaultValidator{}).
-			WithDefaulter(&TestCustomDefaultValidator{}).
-			WithValidator(&TestCustomDefaultValidator{}).
+		err = WebhookManagedBy(m, &TestDefaultValidator{}).
+			WithCustomDefaulter(&TestCustomDefaultValidator{}).
+			WithCustomValidator(&TestCustomDefaultValidator{}).
 			WithLogConstructor(func(base logr.Logger, req *admission.Request) logr.Logger {
 				return admission.DefaultLogConstructor(testingLogger, req)
 			}).
@@ -890,77 +956,126 @@ func runTests(admissionReviewVersion string) {
 		ExpectWithOffset(1, err).To(HaveOccurred())
 	})
 
-	It("should not scaffold a custom defaulting when setting a custom path and a defaulting custom path", func() {
-		By("creating a controller manager")
+	DescribeTable("should not scaffold a custom defaulting when setting a custom path and a defaulting custom path",
+		func(build func(*WebhookBuilder[*TestDefaulterObject])) {
+			By("creating a controller manager")
+			m, err := manager.New(cfg, manager.Options{})
+			ExpectWithOffset(1, err).NotTo(HaveOccurred())
+
+			By("registering the type in the Scheme")
+			builder := scheme.Builder{GroupVersion: testDefaulterGVK.GroupVersion()}
+			builder.Register(&TestDefaulterObject{}, &TestDefaulterList{})
+			err = builder.AddToScheme(m.GetScheme())
+			ExpectWithOffset(1, err).NotTo(HaveOccurred())
+
+			webhookBuilder := WebhookManagedBy(m, &TestDefaulterObject{})
+			build(webhookBuilder)
+			err = webhookBuilder.
+				WithLogConstructor(func(base logr.Logger, req *admission.Request) logr.Logger {
+					return admission.DefaultLogConstructor(testingLogger, req)
+				}).
+				WithDefaulterCustomPath(customPath).
+				WithCustomPath(customPath).
+				Complete()
+			ExpectWithOffset(1, err).To(HaveOccurred())
+		},
+		Entry("CustomDefaulter", func(b *WebhookBuilder[*TestDefaulterObject]) {
+			b.WithCustomDefaulter(&TestCustomDefaulter{})
+		}),
+		Entry("Defaulter", func(b *WebhookBuilder[*TestDefaulterObject]) {
+			b.WithDefaulter(&testDefaulter{})
+		}),
+	)
+
+	DescribeTable("should not scaffold a custom validating when setting a custom path and a validating custom path",
+		func(build func(*WebhookBuilder[*TestValidatorObject])) {
+			By("creating a controller manager")
+			m, err := manager.New(cfg, manager.Options{})
+			ExpectWithOffset(1, err).NotTo(HaveOccurred())
+
+			By("registering the type in the Scheme")
+			builder := scheme.Builder{GroupVersion: testValidatorGVK.GroupVersion()}
+			builder.Register(&TestValidatorObject{}, &TestValidatorList{})
+			err = builder.AddToScheme(m.GetScheme())
+			ExpectWithOffset(1, err).NotTo(HaveOccurred())
+
+			webhookBuilder := WebhookManagedBy(m, &TestValidatorObject{})
+			build(webhookBuilder)
+			err = webhookBuilder.
+				WithLogConstructor(func(base logr.Logger, req *admission.Request) logr.Logger {
+					return admission.DefaultLogConstructor(testingLogger, req)
+				}).
+				WithValidatorCustomPath(customPath).
+				WithCustomPath(customPath).
+				Complete()
+			ExpectWithOffset(1, err).To(HaveOccurred())
+		},
+		Entry("CustomValidator", func(b *WebhookBuilder[*TestValidatorObject]) {
+			b.WithCustomValidator(&TestCustomValidator{})
+		}),
+		Entry("Validator", func(b *WebhookBuilder[*TestValidatorObject]) {
+			b.WithValidator(&testValidator{})
+		}),
+	)
+
+	It("should error if both a defaulter and a custom defaulter are set", func() {
 		m, err := manager.New(cfg, manager.Options{})
 		ExpectWithOffset(1, err).NotTo(HaveOccurred())
 
-		By("registering the type in the Scheme")
-		builder := scheme.Builder{GroupVersion: testValidatorGVK.GroupVersion()}
-		builder.Register(&TestDefaultValidator{}, &TestDefaultValidatorList{})
+		builder := scheme.Builder{GroupVersion: testDefaulterGVK.GroupVersion()}
+		builder.Register(&TestDefaulterObject{}, &TestDefaulterList{})
 		err = builder.AddToScheme(m.GetScheme())
 		ExpectWithOffset(1, err).NotTo(HaveOccurred())
 
-		err = WebhookManagedBy(m).
-			For(&TestDefaulter{}).
-			WithDefaulter(&TestCustomDefaulter{}).
-			WithLogConstructor(func(base logr.Logger, req *admission.Request) logr.Logger {
-				return admission.DefaultLogConstructor(testingLogger, req)
-			}).
-			WithDefaulterCustomPath(customPath).
-			WithCustomPath(customPath).
+		err = WebhookManagedBy(m, &TestDefaulterObject{}).
+			WithDefaulter(&testDefaulter{}).
+			WithCustomDefaulter(&TestCustomDefaulter{}).
 			Complete()
 		ExpectWithOffset(1, err).To(HaveOccurred())
+		Expect(err.Error()).To(ContainSubstring("only one of Defaulter or CustomDefaulter can be set"))
 	})
-
-	It("should not scaffold a custom defaulting when setting a custom path and a validating custom path", func() {
-		By("creating a controller manager")
+	It("should error if both a validator and a custom validator are set", func() {
 		m, err := manager.New(cfg, manager.Options{})
 		ExpectWithOffset(1, err).NotTo(HaveOccurred())
 
-		By("registering the type in the Scheme")
 		builder := scheme.Builder{GroupVersion: testValidatorGVK.GroupVersion()}
-		builder.Register(&TestDefaultValidator{}, &TestDefaultValidatorList{})
+		builder.Register(&TestValidatorObject{}, &TestValidatorList{})
 		err = builder.AddToScheme(m.GetScheme())
 		ExpectWithOffset(1, err).NotTo(HaveOccurred())
 
-		err = WebhookManagedBy(m).
-			For(&TestValidator{}).
-			WithValidator(&TestCustomValidator{}).
-			WithLogConstructor(func(base logr.Logger, req *admission.Request) logr.Logger {
-				return admission.DefaultLogConstructor(testingLogger, req)
-			}).
-			WithDefaulterCustomPath(customPath).
-			WithCustomPath(customPath).
+		err = WebhookManagedBy(m, &TestValidatorObject{}).
+			WithValidator(&testValidator{}).
+			WithCustomValidator(&TestCustomValidator{}).
 			Complete()
 		ExpectWithOffset(1, err).To(HaveOccurred())
+		Expect(err.Error()).To(ContainSubstring("only one of Validator or CustomValidator can be set"))
 	})
 }
 
 // TestDefaulter.
-var _ runtime.Object = &TestDefaulter{}
+var _ runtime.Object = &TestDefaulterObject{}
 
-const testDefaulterKind = "TestDefaulter"
+const testDefaulterKind = "TestDefaulterObject"
 
-type TestDefaulter struct {
+type TestDefaulterObject struct {
 	Replica int  `json:"replica,omitempty"`
 	Panic   bool `json:"panic,omitempty"`
 }
 
 var testDefaulterGVK = schema.GroupVersionKind{Group: "foo.test.org", Version: "v1", Kind: testDefaulterKind}
 
-func (d *TestDefaulter) GetObjectKind() schema.ObjectKind { return d }
-func (d *TestDefaulter) DeepCopyObject() runtime.Object {
-	return &TestDefaulter{
+func (d *TestDefaulterObject) GetObjectKind() schema.ObjectKind { return d }
+func (d *TestDefaulterObject) DeepCopyObject() runtime.Object {
+	return &TestDefaulterObject{
 		Replica: d.Replica,
 	}
 }
 
-func (d *TestDefaulter) GroupVersionKind() schema.GroupVersionKind {
+func (d *TestDefaulterObject) GroupVersionKind() schema.GroupVersionKind {
 	return testDefaulterGVK
 }
 
-func (d *TestDefaulter) SetGroupVersionKind(gvk schema.GroupVersionKind) {}
+func (d *TestDefaulterObject) SetGroupVersionKind(gvk schema.GroupVersionKind) {}
 
 var _ runtime.Object = &TestDefaulterList{}
 
@@ -970,29 +1085,29 @@ func (*TestDefaulterList) GetObjectKind() schema.ObjectKind { return nil }
 func (*TestDefaulterList) DeepCopyObject() runtime.Object   { return nil }
 
 // TestValidator.
-var _ runtime.Object = &TestValidator{}
+var _ runtime.Object = &TestValidatorObject{}
 
-const testValidatorKind = "TestValidator"
+const testValidatorKind = "TestValidatorObject"
 
-type TestValidator struct {
+type TestValidatorObject struct {
 	Replica int  `json:"replica,omitempty"`
 	Panic   bool `json:"panic,omitempty"`
 }
 
 var testValidatorGVK = schema.GroupVersionKind{Group: "foo.test.org", Version: "v1", Kind: testValidatorKind}
 
-func (v *TestValidator) GetObjectKind() schema.ObjectKind { return v }
-func (v *TestValidator) DeepCopyObject() runtime.Object {
-	return &TestValidator{
+func (v *TestValidatorObject) GetObjectKind() schema.ObjectKind { return v }
+func (v *TestValidatorObject) DeepCopyObject() runtime.Object {
+	return &TestValidatorObject{
 		Replica: v.Replica,
 	}
 }
 
-func (v *TestValidator) GroupVersionKind() schema.GroupVersionKind {
+func (v *TestValidatorObject) GroupVersionKind() schema.GroupVersionKind {
 	return testValidatorGVK
 }
 
-func (v *TestValidator) SetGroupVersionKind(gvk schema.GroupVersionKind) {}
+func (v *TestValidatorObject) SetGroupVersionKind(gvk schema.GroupVersionKind) {}
 
 var _ runtime.Object = &TestValidatorList{}
 
@@ -1035,10 +1150,16 @@ type TestDefaultValidatorList struct{}
 func (*TestDefaultValidatorList) GetObjectKind() schema.ObjectKind { return nil }
 func (*TestDefaultValidatorList) DeepCopyObject() runtime.Object   { return nil }
 
-// TestCustomDefaulter.
 type TestCustomDefaulter struct{}
 
 func (*TestCustomDefaulter) Default(ctx context.Context, obj runtime.Object) error {
+	d := obj.(*TestDefaulterObject) //nolint:ifshort
+	return (&testDefaulter{}).Default(ctx, d)
+}
+
+type testDefaulter struct{}
+
+func (*testDefaulter) Default(ctx context.Context, obj *TestDefaulterObject) error {
 	logf.FromContext(ctx).Info("Defaulting object")
 	req, err := admission.RequestFromContext(ctx)
 	if err != nil {
@@ -1048,13 +1169,12 @@ func (*TestCustomDefaulter) Default(ctx context.Context, obj runtime.Object) err
 		return fmt.Errorf("expected Kind TestDefaulter got %q", req.Kind.Kind)
 	}
 
-	d := obj.(*TestDefaulter) //nolint:ifshort
-	if d.Panic {
+	if obj.Panic {
 		panic("fake panic test")
 	}
 
-	if d.Replica < 2 {
-		d.Replica = 2
+	if obj.Replica < 2 {
+		obj.Replica = 2
 	}
 
 	return nil
@@ -1062,11 +1182,27 @@ func (*TestCustomDefaulter) Default(ctx context.Context, obj runtime.Object) err
 
 var _ admission.CustomDefaulter = &TestCustomDefaulter{}
 
-// TestCustomValidator.
-
 type TestCustomValidator struct{}
 
 func (*TestCustomValidator) ValidateCreate(ctx context.Context, obj runtime.Object) (admission.Warnings, error) {
+	v := obj.(*TestValidatorObject) //nolint:ifshort
+	return (&testValidator{}).ValidateCreate(ctx, v)
+}
+
+func (*TestCustomValidator) ValidateUpdate(ctx context.Context, oldObj, newObj runtime.Object) (admission.Warnings, error) {
+	v := newObj.(*TestValidatorObject)
+	old := oldObj.(*TestValidatorObject)
+	return (&testValidator{}).ValidateUpdate(ctx, old, v)
+}
+
+func (*TestCustomValidator) ValidateDelete(ctx context.Context, obj runtime.Object) (admission.Warnings, error) {
+	v := obj.(*TestValidatorObject) //nolint:ifshort
+	return (&testValidator{}).ValidateDelete(ctx, v)
+}
+
+type testValidator struct{}
+
+func (*testValidator) ValidateCreate(ctx context.Context, obj *TestValidatorObject) (admission.Warnings, error) {
 	logf.FromContext(ctx).Info("Validating object")
 	req, err := admission.RequestFromContext(ctx)
 	if err != nil {
@@ -1076,18 +1212,17 @@ func (*TestCustomValidator) ValidateCreate(ctx context.Context, obj runtime.Obje
 		return nil, fmt.Errorf("expected Kind TestValidator got %q", req.Kind.Kind)
 	}
 
-	v := obj.(*TestValidator) //nolint:ifshort
-	if v.Panic {
+	if obj.Panic {
 		panic("fake panic test")
 	}
-	if v.Replica < 0 {
+	if obj.Replica < 0 {
 		return nil, errors.New("number of replica should be greater than or equal to 0")
 	}
 
 	return nil, nil
 }
 
-func (*TestCustomValidator) ValidateUpdate(ctx context.Context, oldObj, newObj runtime.Object) (admission.Warnings, error) {
+func (*testValidator) ValidateUpdate(ctx context.Context, oldObj, newObj *TestValidatorObject) (admission.Warnings, error) {
 	logf.FromContext(ctx).Info("Validating object")
 	req, err := admission.RequestFromContext(ctx)
 	if err != nil {
@@ -1097,13 +1232,11 @@ func (*TestCustomValidator) ValidateUpdate(ctx context.Context, oldObj, newObj r
 		return nil, fmt.Errorf("expected Kind TestValidator got %q", req.Kind.Kind)
 	}
 
-	v := newObj.(*TestValidator)
-	old := oldObj.(*TestValidator)
-	if v.Replica < 0 {
+	if newObj.Replica < 0 {
 		return nil, errors.New("number of replica should be greater than or equal to 0")
 	}
-	if v.Replica < old.Replica {
-		return nil, fmt.Errorf("new replica %v should not be fewer than old replica %v", v.Replica, old.Replica)
+	if newObj.Replica < oldObj.Replica {
+		return nil, fmt.Errorf("new replica %v should not be fewer than old replica %v", newObj.Replica, oldObj.Replica)
 	}
 
 	userAgent, ok := ctx.Value(userAgentCtxKey).(string)
@@ -1114,7 +1247,7 @@ func (*TestCustomValidator) ValidateUpdate(ctx context.Context, oldObj, newObj r
 	return nil, nil
 }
 
-func (*TestCustomValidator) ValidateDelete(ctx context.Context, obj runtime.Object) (admission.Warnings, error) {
+func (*testValidator) ValidateDelete(ctx context.Context, obj *TestValidatorObject) (admission.Warnings, error) {
 	logf.FromContext(ctx).Info("Validating object")
 	req, err := admission.RequestFromContext(ctx)
 	if err != nil {
@@ -1124,8 +1257,7 @@ func (*TestCustomValidator) ValidateDelete(ctx context.Context, obj runtime.Obje
 		return nil, fmt.Errorf("expected Kind TestValidator got %q", req.Kind.Kind)
 	}
 
-	v := obj.(*TestValidator) //nolint:ifshort
-	if v.Replica > 0 {
+	if obj.Replica > 0 {
 		return nil, errors.New("number of replica should be less than or equal to 0 to delete")
 	}
 	return nil, nil
@@ -1214,3 +1346,23 @@ func (*TestCustomDefaultValidator) ValidateDelete(ctx context.Context, obj runti
 }
 
 var _ admission.CustomValidator = &TestCustomValidator{}
+
+type testValidatorDefaulter struct{}
+
+func (*testValidatorDefaulter) Default(ctx context.Context, obj *TestDefaultValidator) error {
+	return (&TestCustomDefaultValidator{}).Default(ctx, obj)
+}
+
+type testDefaultValidatorValidator struct{}
+
+func (*testDefaultValidatorValidator) ValidateCreate(ctx context.Context, obj *TestDefaultValidator) (admission.Warnings, error) {
+	return (&TestCustomDefaultValidator{}).ValidateCreate(ctx, obj)
+}
+
+func (*testDefaultValidatorValidator) ValidateUpdate(ctx context.Context, oldObj, newObj *TestDefaultValidator) (admission.Warnings, error) {
+	return (&TestCustomDefaultValidator{}).ValidateUpdate(ctx, oldObj, newObj)
+}
+
+func (*testDefaultValidatorValidator) ValidateDelete(ctx context.Context, obj *TestDefaultValidator) (admission.Warnings, error) {
+	return (&TestCustomDefaultValidator{}).ValidateDelete(ctx, obj)
+}
diff --git a/pkg/envtest/envtest_test.go b/pkg/envtest/envtest_test.go
index ce3e9a4d..806c9f43 100644
--- a/pkg/envtest/envtest_test.go
+++ b/pkg/envtest/envtest_test.go
@@ -963,4 +963,56 @@ var _ = Describe("Test", func() {
 			Expect(env.WebhookInstallOptions.LocalServingCertDir).ShouldNot(BeADirectory())
 		})
 	})
+
+	Describe("Binary Path Handling", func() {
+		It("should respect pre-configured binary paths when not downloading", func() {
+			// Setup custom paths
+			customAPIServerPath := "/custom/path/to/kube-apiserver"
+			customEtcdPath := "/custom/path/to/etcd"
+			customKubectlPath := "/custom/path/to/kubectl"
+
+			// Create an environment with pre-configured paths
+			testEnv := &Environment{}
+			testEnv.ControlPlane.GetAPIServer().Path = customAPIServerPath
+			testEnv.ControlPlane.Etcd = &Etcd{}
+			testEnv.ControlPlane.Etcd.Path = customEtcdPath
+			testEnv.ControlPlane.KubectlPath = customKubectlPath
+
+			// Set BinaryAssetsDirectory to ensure it's not using defaults
+			testEnv.BinaryAssetsDirectory = "/should/not/be/used"
+			testEnv.DownloadBinaryAssets = false
+
+			// Call configureBinaryPaths to test the path configuration logic
+			err := testEnv.configureBinaryPaths()
+			Expect(err).NotTo(HaveOccurred())
+
+			// Verify paths were preserved (not overwritten)
+			apiServer := testEnv.ControlPlane.GetAPIServer()
+			Expect(apiServer.Path).To(Equal(customAPIServerPath))
+			Expect(testEnv.ControlPlane.Etcd.Path).To(Equal(customEtcdPath))
+			Expect(testEnv.ControlPlane.KubectlPath).To(Equal(customKubectlPath))
+		})
+
+		It("should auto-configure binary paths when not pre-configured", func() {
+			// Create an environment without pre-configured paths
+			testEnv := &Environment{}
+			testEnv.BinaryAssetsDirectory = "/test/assets"
+			testEnv.DownloadBinaryAssets = false
+
+			// Call configureBinaryPaths
+			err := testEnv.configureBinaryPaths()
+			Expect(err).NotTo(HaveOccurred())
+
+			// Verify paths were set using BinPathFinder
+			apiServer := testEnv.ControlPlane.GetAPIServer()
+			Expect(apiServer.Path).NotTo(BeEmpty())
+			Expect(testEnv.ControlPlane.Etcd.Path).NotTo(BeEmpty())
+			Expect(testEnv.ControlPlane.KubectlPath).NotTo(BeEmpty())
+
+			// Verify the paths contain the binary names
+			Expect(apiServer.Path).To(ContainSubstring("kube-apiserver"))
+			Expect(testEnv.ControlPlane.Etcd.Path).To(ContainSubstring("etcd"))
+			Expect(testEnv.ControlPlane.KubectlPath).To(ContainSubstring("kubectl"))
+		})
+	})
 })
diff --git a/pkg/envtest/server.go b/pkg/envtest/server.go
index 9bb81ed2..c9f19da9 100644
--- a/pkg/envtest/server.go
+++ b/pkg/envtest/server.go
@@ -109,7 +109,11 @@ var (
 // 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 is the ControlPlane including the apiserver and etcd.
+	// Binary paths (APIServer.Path, Etcd.Path, KubectlPath) can be pre-configured in ControlPlane.
+	// If DownloadBinaryAssets is true, the downloaded paths will always be used.
+	// If DownloadBinaryAssets is false and paths are not pre-configured (default is empty), they will be
+	// automatically resolved using BinaryAssetsDirectory.
 	ControlPlane controlplane.ControlPlane
 
 	// Scheme is used to determine if conversion webhooks should be enabled
@@ -211,6 +215,40 @@ func (te *Environment) Stop() error {
 	return te.ControlPlane.Stop()
 }
 
+// configureBinaryPaths configures the binary paths for the API server, etcd, and kubectl.
+// If DownloadBinaryAssets is true, it downloads and uses those paths.
+// If DownloadBinaryAssets is false, it only sets paths that are not already configured (empty).
+func (te *Environment) configureBinaryPaths() error {
+	apiServer := te.ControlPlane.GetAPIServer()
+
+	if te.ControlPlane.Etcd == nil {
+		te.ControlPlane.Etcd = &controlplane.Etcd{}
+	}
+
+	if te.DownloadBinaryAssets {
+		apiServerPath, etcdPath, kubectlPath, err := downloadBinaryAssets(context.TODO(),
+			te.BinaryAssetsDirectory, te.DownloadBinaryAssetsVersion, te.DownloadBinaryAssetsIndexURL)
+		if err != nil {
+			return err
+		}
+
+		apiServer.Path = apiServerPath
+		te.ControlPlane.Etcd.Path = etcdPath
+		te.ControlPlane.KubectlPath = kubectlPath
+	} else {
+		if apiServer.Path == "" {
+			apiServer.Path = process.BinPathFinder("kube-apiserver", te.BinaryAssetsDirectory)
+		}
+		if te.ControlPlane.Etcd.Path == "" {
+			te.ControlPlane.Etcd.Path = process.BinPathFinder("etcd", te.BinaryAssetsDirectory)
+		}
+		if te.ControlPlane.KubectlPath == "" {
+			te.ControlPlane.KubectlPath = process.BinPathFinder("kubectl", te.BinaryAssetsDirectory)
+		}
+	}
+	return nil
+}
+
 // 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() {
@@ -229,10 +267,6 @@ func (te *Environment) Start() (*rest.Config, error) {
 	} else {
 		apiServer := te.ControlPlane.GetAPIServer()
 
-		if te.ControlPlane.Etcd == nil {
-			te.ControlPlane.Etcd = &controlplane.Etcd{}
-		}
-
 		if os.Getenv(envAttachOutput) == "true" {
 			te.AttachControlPlaneOutput = true
 		}
@@ -243,6 +277,9 @@ func (te *Environment) Start() (*rest.Config, error) {
 			if apiServer.Err == nil {
 				apiServer.Err = os.Stderr
 			}
+			if te.ControlPlane.Etcd == nil {
+				te.ControlPlane.Etcd = &controlplane.Etcd{}
+			}
 			if te.ControlPlane.Etcd.Out == nil {
 				te.ControlPlane.Etcd.Out = os.Stdout
 			}
@@ -251,20 +288,8 @@ func (te *Environment) Start() (*rest.Config, error) {
 			}
 		}
 
-		if te.DownloadBinaryAssets {
-			apiServerPath, etcdPath, kubectlPath, err := downloadBinaryAssets(context.TODO(),
-				te.BinaryAssetsDirectory, te.DownloadBinaryAssetsVersion, te.DownloadBinaryAssetsIndexURL)
-			if err != nil {
-				return nil, err
-			}
-
-			apiServer.Path = apiServerPath
-			te.ControlPlane.Etcd.Path = etcdPath
-			te.ControlPlane.KubectlPath = kubectlPath
-		} else {
-			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.configureBinaryPaths(); err != nil {
+			return nil, fmt.Errorf("failed to configure binary paths: %w", err)
 		}
 
 		if err := te.defaultTimeouts(); err != nil {
diff --git a/pkg/internal/testing/addr/manager.go b/pkg/internal/testing/addr/manager.go
index ffa33a88..2e2e4132 100644
--- a/pkg/internal/testing/addr/manager.go
+++ b/pkg/internal/testing/addr/manager.go
@@ -124,14 +124,16 @@ func suggest(listenHost string) (*net.TCPListener, int, string, error) {
 // 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.
+// allocated within 2 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 err := listener.Close(); err != nil {
+			return -1, "", err
+		}
 		if ok, err := cache.add(port); ok {
 			return port, resolvedHost, nil
 		} else if err != nil {