Repository: kubernetes-sigs/controller-runtime

Issue:
Proposal: Generics for CustomValidator & CustomDefaulter
[CustomValidator](https://pkg.go.dev/sigs.k8s.io/controller-runtime@v0.17.0/pkg/webhook/admission#CustomValidator) and [CustomDefaulter](https://pkg.go.dev/sigs.k8s.io/controller-runtime@v0.17.0/pkg/webhook/admissionCustomDefaulter) interfaces could use generics support to potentially shorten the handler funcs like this:

```go
func (w *KubernetesPoolWebhook) ValidateUpdate(ctx context.Context, oldObj, newObj runtime.Object) (admission.Warnings, error) {
	oldO, ok := oldObj.(apiv1alpha1.KubernetesPool)
	if err != nil {
		return nil, errors.NewBadRequest(fmt.Sprintf("unexpected type %T", oldObj))
	}
	newO, ok := newObj.(apiv1alpha1.KubernetesPool)
	if err != nil {
		return nil,  errors.NewBadRequest(fmt.Sprintf("unexpected type %T", newObj))
	}
	return nil, validateUpdate(oldO, newO)
}
```
to

```go
func (w *KubernetesPoolWebhook) ValidateUpdate(ctx context.Context, oldObj, newObj apiv1alpha1.KubernetesPool) (admission.Warnings, error) {
	return nil, validateUpdate(oldO, newO)
}
```

Also statements like:
```
return ctrl.NewWebhookManagedBy(mgr).
	For(&apiv1alpha1.KubernetesPool{}).
	WithDefaulter(w).
	WithValidator(w).
	Complete()
```

can probably be inferred with more type safety (right now there's nothing ensures a & b are handlers of the same type in `WithDefaulter(a).WithDefaulter(b)`?). e.g.

```
return ctrl.NewWebhookManagedBy[apiv1alpha1.KubernetesPool](mgr).
	WithDefaulter(w).
	WithValidator(w).
	Complete()
```

/kind proposal
/kind feature

Hints from issue comments:
@k8s-ci-robot: @ahmetb: The label(s) `kind/proposal` cannot be applied, because the repository doesn't have them.

<details>

In response to [this](https://github.com/kubernetes-sigs/controller-runtime/issues/2675):

>[CustomValidator](https://pkg.go.dev/sigs.k8s.io/controller-runtime@v0.17.0/pkg/webhook/admission#CustomValidator) and [CustomDefaulter](https://pkg.go.dev/sigs.k8s.io/controller-runtime@v0.17.0/pkg/webhook/admissionCustomDefaulter) interfaces could use generics support to potentially shorten the handler funcs like this:
>
>```go
>func (w *KubernetesPoolWebhook) ValidateUpdate(ctx context.Context, oldObj, newObj runtime.Object) (admission.Warnings, error) {
>	oldO, ok := oldObj.(apiv1alpha1.KubernetesPool)
>	if err != nil {
>		return nil, errors.NewBadRequest(fmt.Sprintf("unexpected type %T", oldObj))
>	}
>	newO, ok := newObj.(apiv1alpha1.KubernetesPool)
>	if err != nil {
>		return nil,  errors.NewBadRequest(fmt.Sprintf("unexpected type %T", newObj))
>	}
>	return nil, validateUpdate(oldO, newO)
>}
>```
>to
>
>```go
>func (w *KubernetesPoolWebhook) ValidateUpdate(ctx context.Context, oldObj, newObj apiv1alpha1.KubernetesPool) (admission.Warnings, error) {
>	return nil, validateUpdate(oldO, newO)
>}
>```
>
>Also statements like:
>```
>return ctrl.NewWebhookManagedBy(mgr).
>	For(&apiv1alpha1.KubernetesPool{}).
>	WithDefaulter(w).
>	WithValidator(w).
>	Complete()
>```
>
>can probably be inferred with more type safety (right now there's nothing ensures a & b are handlers of the same type in `WithDefaulter(a).WithDefaulter(b)`?).
>
>/kind proposal
>/kind feature


Instructions for interacting with me using PR comments are available [here](https://git.k8s.io/community/contributors/guide/pull-requests.md).  If you have questions or suggestions related to my behavior, please file an issue against the [kubernetes/test-infra](https://github.com/kubernetes/test-infra/issues/new?title=Prow%20issue:) repository.
</details>@alvaroaleman: @ahmetb are you sure this is possible? My understanding is that it isn't due to https://github.com/golang/go/issues/49085, the `WebhookBuilder` would only be able to support one type. I'd love to be wrong on this though.@ahmetb: I will give it a shot. I hit that in porting go-linq to generics as well. @k8s-triage-robot: The Kubernetes project currently lacks enough contributors to adequately respond to all issues.

This bot triages un-triaged issues according to the following rules:
- After 90d of inactivity, `lifecycle/stale` is applied
- After 30d of inactivity since `lifecycle/stale` was applied, `lifecycle/rotten` is applied
- After 30d of inactivity since `lifecycle/rotten` was applied, the issue is closed

You can:
- Mark this issue as fresh with `/remove-lifecycle stale`
- Close this issue with `/close`
- Offer to help out with [Issue Triage][1]

Please send feedback to sig-contributor-experience at [kubernetes/community](https://github.com/kubernetes/community).

/lifecycle stale

[1]: https://www.kubernetes.dev/docs/guide/issue-triage/@k8s-triage-robot: The Kubernetes project currently lacks enough active contributors to adequately respond to all issues.

This bot triages un-triaged issues according to the following rules:
- After 90d of inactivity, `lifecycle/stale` is applied
- After 30d of inactivity since `lifecycle/stale` was applied, `lifecycle/rotten` is applied
- After 30d of inactivity since `lifecycle/rotten` was applied, the issue is closed

You can:
- Mark this issue as fresh with `/remove-lifecycle rotten`
- Close this issue with `/close`
- Offer to help out with [Issue Triage][1]

Please send feedback to sig-contributor-experience at [kubernetes/community](https://github.com/kubernetes/community).

/lifecycle rotten

[1]: https://www.kubernetes.dev/docs/guide/issue-triage/@sbueringer: /remove-lifecycle rotten@k8s-triage-robot: The Kubernetes project currently lacks enough contributors to adequately respond to all issues.

This bot triages un-triaged issues according to the following rules:
- After 90d of inactivity, `lifecycle/stale` is applied
- After 30d of inactivity since `lifecycle/stale` was applied, `lifecycle/rotten` is applied
- After 30d of inactivity since `lifecycle/rotten` was applied, the issue is closed

You can:
- Mark this issue as fresh with `/remove-lifecycle stale`
- Close this issue with `/close`
- Offer to help out with [Issue Triage][1]

Please send feedback to sig-contributor-experience at [kubernetes/community](https://github.com/kubernetes/community).

/lifecycle stale

[1]: https://www.kubernetes.dev/docs/guide/issue-triage/@k8s-triage-robot: The Kubernetes project currently lacks enough active contributors to adequately respond to all issues.

This bot triages un-triaged issues according to the following rules:
- After 90d of inactivity, `lifecycle/stale` is applied
- After 30d of inactivity since `lifecycle/stale` was applied, `lifecycle/rotten` is applied
- After 30d of inactivity since `lifecycle/rotten` was applied, the issue is closed

You can:
- Mark this issue as fresh with `/remove-lifecycle rotten`
- Close this issue with `/close`
- Offer to help out with [Issue Triage][1]

Please send feedback to sig-contributor-experience at [kubernetes/community](https://github.com/kubernetes/community).

/lifecycle rotten

[1]: https://www.kubernetes.dev/docs/guide/issue-triage/@k8s-triage-robot: The Kubernetes project currently lacks enough active contributors to adequately respond to all issues and PRs.

This bot triages issues according to the following rules:
- After 90d of inactivity, `lifecycle/stale` is applied
- After 30d of inactivity since `lifecycle/stale` was applied, `lifecycle/rotten` is applied
- After 30d of inactivity since `lifecycle/rotten` was applied, the issue is closed

You can:
- Reopen this issue with `/reopen`
- Mark this issue as fresh with `/remove-lifecycle rotten`
- Offer to help out with [Issue Triage][1]

Please send feedback to sig-contributor-experience at [kubernetes/community](https://github.com/kubernetes/community).

/close not-planned

[1]: https://www.kubernetes.dev/docs/guide/issue-triage/@k8s-ci-robot: @k8s-triage-robot: Closing this issue, marking it as "Not Planned".

<details>

In response to [this](https://github.com/kubernetes-sigs/controller-runtime/issues/2675#issuecomment-2478560840):

>The Kubernetes project currently lacks enough active contributors to adequately respond to all issues and PRs.
>
>This bot triages issues according to the following rules:
>- After 90d of inactivity, `lifecycle/stale` is applied
>- After 30d of inactivity since `lifecycle/stale` was applied, `lifecycle/rotten` is applied
>- After 30d of inactivity since `lifecycle/rotten` was applied, the issue is closed
>
>You can:
>- Reopen this issue with `/reopen`
>- Mark this issue as fresh with `/remove-lifecycle rotten`
>- Offer to help out with [Issue Triage][1]
>
>Please send feedback to sig-contributor-experience at [kubernetes/community](https://github.com/kubernetes/community).
>
>/close not-planned
>
>[1]: https://www.kubernetes.dev/docs/guide/issue-triage/


Instructions for interacting with me using PR comments are available [here](https://git.k8s.io/community/contributors/guide/pull-requests.md).  If you have questions or suggestions related to my behavior, please file an issue against the [kubernetes-sigs/prow](https://github.com/kubernetes-sigs/prow/issues/new?title=Prow%20issue:) repository.
</details>@alvaroaleman: /reopen
/lifecycle frozen

Currently also running into this and its annoying. I just had a look at all of this and I think this is totally possible, it is just that the typing changes would need to go up until including the webhook builder. Since the webhook builder conceptionally represents a webhook for one type, that seems fine@sbueringer: +1 Makes sense to me. We might have to add new builder methods to keep supporting the Custom* interfaces until they are removed but that's fine for me (I think that depends on if we use the generic type parameter for For or not).

We could also maybe add a type parameter and only use it for defaulter/validator and once the Custom* interfaces are gone we also use it in For

Solution patch:
diff --git a/.github/workflows/ossf-scorecard.yaml b/.github/workflows/ossf-scorecard.yaml
index 24156f49..379fb885 100644
--- a/.github/workflows/ossf-scorecard.yaml
+++ b/.github/workflows/ossf-scorecard.yaml
@@ -43,7 +43,7 @@ jobs:
 
       # Upload the results as artifacts.
       - name: "Upload artifact"
-        uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # tag=v4.6.2
+        uses: actions/upload-artifact@330a01c490aca151604b8cf639adc76d48f6c5d4 # tag=v5.0.0
         with:
           name: SARIF file
           path: results.sarif
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/cache/cache.go b/pkg/cache/cache.go
index a7e49185..107a7f1c 100644
--- a/pkg/cache/cache.go
+++ b/pkg/cache/cache.go
@@ -22,7 +22,6 @@ import (
 	"maps"
 	"net/http"
 	"slices"
-	"sort"
 	"time"
 
 	corev1 "k8s.io/api/core/v1"
@@ -583,7 +582,8 @@ func defaultConfig(toDefault, defaultFrom Config) Config {
 
 func namespaceAllSelector(namespaces []string) []fields.Selector {
 	selectors := make([]fields.Selector, 0, len(namespaces)-1)
-	sort.Strings(namespaces)
+	slices.Sort(namespaces)
+
 	for _, namespace := range namespaces {
 		if namespace != metav1.NamespaceAll {
 			selectors = append(selectors, fields.OneTermNotEqualSelector("metadata.namespace", namespace))
diff --git a/pkg/client/apiutil/errors.go b/pkg/client/apiutil/errors.go
index c216c49d..b00e0712 100644
--- a/pkg/client/apiutil/errors.go
+++ b/pkg/client/apiutil/errors.go
@@ -18,7 +18,7 @@ package apiutil
 
 import (
 	"fmt"
-	"sort"
+	"slices"
 	"strings"
 
 	apierrors "k8s.io/apimachinery/pkg/api/errors"
@@ -38,7 +38,7 @@ func (e *ErrResourceDiscoveryFailed) Error() string {
 	for k, v := range *e {
 		subErrors = append(subErrors, fmt.Sprintf("%s: %v", k, v))
 	}
-	sort.Strings(subErrors)
+	slices.Sort(subErrors)
 	return fmt.Sprintf("unable to retrieve the complete list of server APIs: %s", strings.Join(subErrors, ", "))
 }
 
diff --git a/pkg/client/fake/client.go b/pkg/client/fake/client.go
index 62067cb1..05d71bac 100644
--- a/pkg/client/fake/client.go
+++ b/pkg/client/fake/client.go
@@ -1701,6 +1701,12 @@ func (c *fakeClient) addToSchemeIfUnknownAndUnstructuredOrPartial(obj runtime.Ob
 		return err
 	}
 
+	if isUnstructuredList || isPartialList {
+		if !strings.HasSuffix(gvk.Kind, "List") {
+			gvk.Kind += "List"
+		}
+	}
+
 	if !c.scheme.Recognizes(gvk) {
 		c.scheme.AddKnownTypeWithName(gvk, obj)
 	}
diff --git a/pkg/controller/priorityqueue/priorityqueue.go b/pkg/controller/priorityqueue/priorityqueue.go
index 98df84c5..71363f0d 100644
--- a/pkg/controller/priorityqueue/priorityqueue.go
+++ b/pkg/controller/priorityqueue/priorityqueue.go
@@ -124,8 +124,8 @@ type priorityqueue[T comparable] struct {
 	get chan item[T]
 
 	// waiters is the number of routines blocked in Get, we use it to determine
-	// if we can push items.
-	waiters atomic.Int64
+	// if we can push items. Every manipulation has to be protected with the lock.
+	waiters int64
 
 	// Configurable for testing
 	now  func() time.Time
@@ -269,7 +269,7 @@ func (w *priorityqueue[T]) spin() {
 						}
 					}
 
-					if w.waiters.Load() == 0 {
+					if w.waiters == 0 {
 						// Have to keep iterating here to ensure we update metrics
 						// for further items that became ready and set nextReady.
 						return true
@@ -277,7 +277,7 @@ func (w *priorityqueue[T]) spin() {
 
 					w.metrics.get(item.Key, item.Priority)
 					w.locked.Insert(item.Key)
-					w.waiters.Add(-1)
+					w.waiters--
 					delete(w.items, item.Key)
 					toDelete = append(toDelete, item)
 					w.becameReady.Delete(item.Key)
@@ -316,7 +316,9 @@ func (w *priorityqueue[T]) GetWithPriority() (_ T, priority int, shutdown bool)
 		return zero, 0, true
 	}
 
-	w.waiters.Add(1)
+	w.lock.Lock()
+	w.waiters++
+	w.lock.Unlock()
 
 	w.notifyItemOrWaiterAdded()
 
diff --git a/pkg/healthz/healthz.go b/pkg/healthz/healthz.go
index cfb5dc8d..149b02ec 100644
--- a/pkg/healthz/healthz.go
+++ b/pkg/healthz/healthz.go
@@ -20,7 +20,7 @@ import (
 	"fmt"
 	"net/http"
 	"path"
-	"sort"
+	"slices"
 	"strings"
 
 	"k8s.io/apimachinery/pkg/util/sets"
@@ -75,7 +75,7 @@ func (h *Handler) serveAggregated(resp http.ResponseWriter, req *http.Request) {
 	}
 
 	// ...sort to be consistent...
-	sort.Slice(parts, func(i, j int) bool { return parts[i].name < parts[j].name })
+	slices.SortStableFunc(parts, func(i, j checkStatus) int { return strings.Compare(i.name, j.name) })
 
 	// ...and write out the result
 	// TODO(directxman12): this should also accept a request for JSON content (via a accept header)
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/cache/cache_test.go b/pkg/cache/cache_test.go
index 7748e2e3..c2dae097 100644
--- a/pkg/cache/cache_test.go
+++ b/pkg/cache/cache_test.go
@@ -21,7 +21,7 @@ import (
 	"errors"
 	"fmt"
 	"reflect"
-	"sort"
+	"slices"
 	"strconv"
 	"strings"
 	"time"
@@ -808,8 +808,8 @@ func CacheTest(createCacheFunc func(config *rest.Config, opts cache.Options) (ca
 
 						By("verifying the pointer fields in pod have the same addresses")
 						Expect(outList1.Items).To(HaveLen(len(outList2.Items)))
-						sort.SliceStable(outList1.Items, func(i, j int) bool { return outList1.Items[i].Name <= outList1.Items[j].Name })
-						sort.SliceStable(outList2.Items, func(i, j int) bool { return outList2.Items[i].Name <= outList2.Items[j].Name })
+						slices.SortStableFunc(outList1.Items, func(i, j corev1.Pod) int { return strings.Compare(i.Name, j.Name) })
+						slices.SortStableFunc(outList2.Items, func(i, j corev1.Pod) int { return strings.Compare(i.Name, j.Name) })
 						for i := range outList1.Items {
 							a := &outList1.Items[i]
 							b := &outList2.Items[i]
@@ -1134,8 +1134,8 @@ func CacheTest(createCacheFunc func(config *rest.Config, opts cache.Options) (ca
 
 						By("verifying the pointer fields in pod have the same addresses")
 						Expect(outList1.Items).To(HaveLen(len(outList2.Items)))
-						sort.SliceStable(outList1.Items, func(i, j int) bool { return outList1.Items[i].GetName() <= outList1.Items[j].GetName() })
-						sort.SliceStable(outList2.Items, func(i, j int) bool { return outList2.Items[i].GetName() <= outList2.Items[j].GetName() })
+						slices.SortStableFunc(outList1.Items, func(i, j unstructured.Unstructured) int { return strings.Compare(i.GetName(), j.GetName()) })
+						slices.SortStableFunc(outList2.Items, func(i, j unstructured.Unstructured) int { return strings.Compare(i.GetName(), j.GetName()) })
 						for i := range outList1.Items {
 							a := &outList1.Items[i]
 							b := &outList2.Items[i]
@@ -1519,8 +1519,8 @@ func CacheTest(createCacheFunc func(config *rest.Config, opts cache.Options) (ca
 
 						By("verifying the pointer fields in pod have the same addresses")
 						Expect(outList1.Items).To(HaveLen(len(outList2.Items)))
-						sort.SliceStable(outList1.Items, func(i, j int) bool { return outList1.Items[i].Name <= outList1.Items[j].Name })
-						sort.SliceStable(outList2.Items, func(i, j int) bool { return outList2.Items[i].Name <= outList2.Items[j].Name })
+						slices.SortStableFunc(outList1.Items, func(i, j metav1.PartialObjectMetadata) int { return strings.Compare(i.Name, j.Name) })
+						slices.SortStableFunc(outList2.Items, func(i, j metav1.PartialObjectMetadata) int { return strings.Compare(i.Name, j.Name) })
 						for i := range outList1.Items {
 							a := &outList1.Items[i]
 							b := &outList2.Items[i]
diff --git a/pkg/client/client_test.go b/pkg/client/client_test.go
index 021fbeb0..079458f5 100644
--- a/pkg/client/client_test.go
+++ b/pkg/client/client_test.go
@@ -2516,6 +2516,43 @@ U5wwSivyi7vmegHKmblOzNVKA5qPO8zWzqBC
 				Expect(hasDep).To(BeTrue())
 			})
 
+			It("should fetch unstructured collection of objects when setting a non-list kind", func(ctx SpecContext) {
+				// While it is not ideal to omit the List suffix it can easily happen.
+				// As the client is using TrimSuffix(gvk.Kind,"List") this also works.
+				// As it works it is part of our API and we should test it accordingly.
+				By("create an initial object")
+				_, err := clientset.AppsV1().Deployments(ns).Create(ctx, dep, metav1.CreateOptions{})
+				Expect(err).NotTo(HaveOccurred())
+
+				cl, err := client.New(cfg, client.Options{})
+				Expect(err).NotTo(HaveOccurred())
+
+				By("listing all objects of that type in the cluster")
+				deps := &unstructured.UnstructuredList{}
+				deps.SetGroupVersionKind(schema.GroupVersionKind{
+					Group:   "apps",
+					Kind:    "Deployment",
+					Version: "v1",
+				})
+				err = cl.List(ctx, deps)
+				Expect(err).NotTo(HaveOccurred())
+
+				Expect(deps.Items).NotTo(BeEmpty())
+				hasDep := false
+				for _, item := range deps.Items {
+					Expect(item.GroupVersionKind()).To(Equal(schema.GroupVersionKind{
+						Group:   "apps",
+						Kind:    "Deployment",
+						Version: "v1",
+					}))
+					if item.GetName() == dep.Name && item.GetNamespace() == dep.Namespace {
+						hasDep = true
+						break
+					}
+				}
+				Expect(hasDep).To(BeTrue())
+			})
+
 			It("should fetch unstructured collection of objects, even if scheme is empty", func(ctx SpecContext) {
 				By("create an initial object")
 				_, err := clientset.AppsV1().Deployments(ns).Create(ctx, dep, metav1.CreateOptions{})
diff --git a/pkg/client/fake/client_test.go b/pkg/client/fake/client_test.go
index 6c71d680..1eed4d5a 100644
--- a/pkg/client/fake/client_test.go
+++ b/pkg/client/fake/client_test.go
@@ -2939,6 +2939,40 @@ var _ = Describe("Fake client", func() {
 		Expect(result.Object["spec"]).To(Equal(map[string]any{"other": "data"}))
 	})
 
+	It("supports server-side apply of a custom resource via Apply method after List with a non-list kind", func(ctx SpecContext) {
+		cl := NewClientBuilder().Build()
+
+		// Previously this List call lead to addToSchemeIfUnknownAndUnstructuredOrPartial adding FakeResource as UnstructuredList to the scheme.
+		// This broke the subsequent SSA call as it was trying to create a new FakeResource object as an UnstructuredList.
+		// After a fix this List call leads to addToSchemeIfUnknownAndUnstructuredOrPartial adding FakeResourceList as UnstructuredList to the
+		// scheme even if the UnstructuredList here is missing the List suffix.
+		objList := &unstructured.UnstructuredList{}
+		objList.SetAPIVersion("custom/v1")
+		objList.SetKind("FakeResource")
+		Expect(cl.List(ctx, objList)).To(Succeed())
+
+		obj := &unstructured.Unstructured{}
+		obj.SetAPIVersion("custom/v1")
+		obj.SetKind("FakeResource")
+		obj.SetName("foo")
+		result := obj.DeepCopy()
+
+		Expect(unstructured.SetNestedField(obj.Object, map[string]any{"some": "data"}, "spec")).To(Succeed())
+
+		applyConfig := client.ApplyConfigurationFromUnstructured(obj)
+		Expect(cl.Apply(ctx, applyConfig, &client.ApplyOptions{FieldManager: "test-manager"})).To(Succeed())
+
+		Expect(cl.Get(ctx, client.ObjectKeyFromObject(result), result)).To(Succeed())
+		Expect(result.Object["spec"]).To(Equal(map[string]any{"some": "data"}))
+
+		Expect(unstructured.SetNestedField(obj.Object, map[string]any{"other": "data"}, "spec")).To(Succeed())
+		applyConfig2 := client.ApplyConfigurationFromUnstructured(obj)
+		Expect(cl.Apply(ctx, applyConfig2, &client.ApplyOptions{FieldManager: "test-manager"})).To(Succeed())
+
+		Expect(cl.Get(ctx, client.ObjectKeyFromObject(result), result)).To(Succeed())
+		Expect(result.Object["spec"]).To(Equal(map[string]any{"other": "data"}))
+	})
+
 	It("sets the fieldManager in create, patch and update", func(ctx SpecContext) {
 		owner := "test-owner"
 		cl := client.WithFieldOwner(
diff --git a/pkg/controller/priorityqueue/priorityqueue_test.go b/pkg/controller/priorityqueue/priorityqueue_test.go
index fb186944..9c708e98 100644
--- a/pkg/controller/priorityqueue/priorityqueue_test.go
+++ b/pkg/controller/priorityqueue/priorityqueue_test.go
@@ -441,6 +441,60 @@ func newQueueWithTimeForwarder() (_ *priorityqueue[string], _ *fakeMetricsProvid
 	}
 }
 
+func TestHighPriorityItemsAreReturnedBeforeLowPriorityItemMultipleTimes(t *testing.T) {
+	t.Parallel()
+	synctest.Test(t, func(t *testing.T) {
+		g := NewWithT(t)
+
+		q, metrics := newQueue()
+		defer q.ShutDown()
+
+		const itemsPerPriority = 1000
+		lowPriority := 0
+		lowMiddlePriority := 5
+		middlePriority := 10
+		upperMiddlePriority := 15
+		highPriority := 20
+		for i := range itemsPerPriority {
+			q.AddWithOpts(AddOpts{Priority: &highPriority}, fmt.Sprintf("high-%d", i))
+			q.AddWithOpts(AddOpts{Priority: &upperMiddlePriority}, fmt.Sprintf("upperMiddle-%d", i))
+			q.AddWithOpts(AddOpts{Priority: &middlePriority}, fmt.Sprintf("middle-%d", i))
+			q.AddWithOpts(AddOpts{Priority: &lowMiddlePriority}, fmt.Sprintf("lowMiddle-%d", i))
+			q.AddWithOpts(AddOpts{Priority: &lowPriority}, fmt.Sprintf("low-%d", i))
+		}
+		synctest.Wait()
+
+		for range itemsPerPriority {
+			key, prio, _ := q.GetWithPriority()
+			g.Expect(prio).To(Equal(highPriority))
+			g.Expect(key).To(HavePrefix("high-"))
+		}
+		for range itemsPerPriority {
+			key, prio, _ := q.GetWithPriority()
+			g.Expect(prio).To(Equal(upperMiddlePriority))
+			g.Expect(key).To(HavePrefix("upperMiddle-"))
+		}
+		for range itemsPerPriority {
+			key, prio, _ := q.GetWithPriority()
+			g.Expect(prio).To(Equal(middlePriority))
+			g.Expect(key).To(HavePrefix("middle-"))
+		}
+		for range itemsPerPriority {
+			key, prio, _ := q.GetWithPriority()
+			g.Expect(prio).To(Equal(lowMiddlePriority))
+			g.Expect(key).To(HavePrefix("lowMiddle-"))
+		}
+		for range itemsPerPriority {
+			key, prio, _ := q.GetWithPriority()
+			g.Expect(prio).To(Equal(lowPriority))
+			g.Expect(key).To(HavePrefix("low-"))
+		}
+		g.Expect(metrics.depth["test"]).To(Equal(map[int]int{10: 0, 5: 0, 0: 0, 20: 0, 15: 0}))
+		g.Expect(metrics.adds["test"]).To(Equal(itemsPerPriority * 5))
+		g.Expect(metrics.retries["test"]).To(Equal(0))
+	})
+}
+
 func newQueue() (*priorityqueue[string], *fakeMetricsProvider) {
 	metrics := newFakeMetricsProvider()
 	q := New("test", func(o *Opts[string]) {
diff --git a/pkg/internal/testing/certs/tinyca_test.go b/pkg/internal/testing/certs/tinyca_test.go
index 5d84de56..95429755 100644
--- a/pkg/internal/testing/certs/tinyca_test.go
+++ b/pkg/internal/testing/certs/tinyca_test.go
@@ -21,7 +21,7 @@ import (
 	"encoding/pem"
 	"math/big"
 	"net"
-	"sort"
+	"slices"
 	"time"
 
 	. "github.com/onsi/ginkgo/v2"
@@ -67,10 +67,11 @@ var _ = Describe("TinyCA", func() {
 			secondCerts.Cert.SerialNumber,
 			thirdCerts.Cert.SerialNumber,
 		}
-		// quick uniqueness check of numbers: sort, then you only have to compare sequential entries
-		sort.Slice(serials, func(i, j int) bool {
-			return serials[i].Cmp(serials[j]) == -1
+		// quick uniqueness check of numbers: slices sort, then you only have to compare sequential entries
+		slices.SortStableFunc(serials, func(i, j *big.Int) int {
+			return i.Cmp(j)
 		})
+
 		Expect(serials[1].Cmp(serials[0])).NotTo(Equal(0), "serials shouldn't be equal")
 		Expect(serials[2].Cmp(serials[1])).NotTo(Equal(0), "serials shouldn't be equal")
 	})
diff --git a/pkg/internal/testing/process/arguments.go b/pkg/internal/testing/process/arguments.go
index 391eec1f..1e556e99 100644
--- a/pkg/internal/testing/process/arguments.go
+++ b/pkg/internal/testing/process/arguments.go
@@ -19,7 +19,7 @@ package process
 import (
 	"bytes"
 	"html/template"
-	"sort"
+	"slices"
 	"strings"
 )
 
@@ -230,7 +230,7 @@ func (a *Arguments) AsStrings(defaults map[string][]string) []string {
 	for key := range a.values {
 		keysInOrder = append(keysInOrder, key)
 	}
-	sort.Strings(keysInOrder)
+	slices.Sort(keysInOrder)
 
 	var res []string
 	for _, key := range keysInOrder {
diff --git a/tools/setup-envtest/env/env.go b/tools/setup-envtest/env/env.go
index 6168739e..cfcad350 100644
--- a/tools/setup-envtest/env/env.go
+++ b/tools/setup-envtest/env/env.go
@@ -10,7 +10,7 @@ import (
 	"io"
 	"io/fs"
 	"path/filepath"
-	"sort"
+	"slices"
 	"strings"
 	"text/tabwriter"
 
@@ -121,9 +121,10 @@ func (e *Env) ListVersions(ctx context.Context) {
 		if !e.Version.Matches(set.Version) {
 			continue
 		}
-		sort.Slice(set.Platforms, func(i, j int) bool {
-			return orderPlatforms(set.Platforms[i].Platform, set.Platforms[j].Platform)
+		slices.SortStableFunc(set.Platforms, func(i, j versions.PlatformItem) int {
+			return orderPlatforms(i.Platform, j.Platform)
 		})
+
 		for _, plat := range set.Platforms {
 			if e.Platform.Matches(plat.Platform) {
 				fmt.Fprintf(out, "(available)\tv%s\t%s\n", set.Version, plat)
@@ -246,7 +247,7 @@ func (e *Env) EnsureVersionIsSet(ctx context.Context) {
 	serverVer, platform := e.LatestVersion(ctx)
 
 	// if we're not forcing a download, and we have a newer local version, just use that
-	if !e.ForceDownload && localVer != nil && localVer.NewerThan(serverVer) {
+	if !e.ForceDownload && localVer != nil && localVer.Compare(serverVer) > 0 {
 		e.Platform.Platform = localPlat // update our data with hash
 		e.Version.MakeConcrete(*localVer)
 		return
diff --git a/tools/setup-envtest/env/helpers.go b/tools/setup-envtest/env/helpers.go
index 2c98c88d..85720885 100644
--- a/tools/setup-envtest/env/helpers.go
+++ b/tools/setup-envtest/env/helpers.go
@@ -5,17 +5,18 @@ package env
 
 import (
 	"fmt"
+	"strings"
 
 	"sigs.k8s.io/controller-runtime/tools/setup-envtest/versions"
 )
 
 // orderPlatforms orders platforms by OS then arch.
-func orderPlatforms(first, second versions.Platform) bool {
+func orderPlatforms(first, second versions.Platform) int {
 	// sort by OS, then arch
 	if first.OS != second.OS {
-		return first.OS < second.OS
+		return strings.Compare(first.OS, second.OS)
 	}
-	return first.Arch < second.Arch
+	return strings.Compare(first.Arch, second.Arch)
 }
 
 // PrintFormat indicates how to print out fetch and switch results.
diff --git a/tools/setup-envtest/remote/http_client.go b/tools/setup-envtest/remote/http_client.go
index 0339654a..a87ef1f1 100644
--- a/tools/setup-envtest/remote/http_client.go
+++ b/tools/setup-envtest/remote/http_client.go
@@ -9,7 +9,7 @@ import (
 	"io"
 	"net/http"
 	"net/url"
-	"sort"
+	"slices"
 
 	"github.com/go-logr/logr"
 	"sigs.k8s.io/controller-runtime/tools/setup-envtest/versions"
@@ -87,9 +87,8 @@ func (c *HTTPClient) ListVersions(ctx context.Context) ([]versions.Set, error) {
 		res = append(res, versions.Set{Version: ver, Platforms: details})
 	}
 	// sort in inverse order so that the newest one is first
-	sort.Slice(res, func(i, j int) bool {
-		first, second := res[i].Version, res[j].Version
-		return first.NewerThan(second)
+	slices.SortStableFunc(res, func(i, j versions.Set) int {
+		return i.Version.Compare(j.Version)
 	})
 
 	return res, nil
diff --git a/tools/setup-envtest/store/store.go b/tools/setup-envtest/store/store.go
index bb5a1f7b..1e1d4beb 100644
--- a/tools/setup-envtest/store/store.go
+++ b/tools/setup-envtest/store/store.go
@@ -12,7 +12,8 @@ import (
 	"io"
 	"os"
 	"path/filepath"
-	"sort"
+	"slices"
+	"strings"
 
 	"github.com/go-logr/logr"
 	"github.com/spf13/afero"
@@ -102,11 +103,11 @@ func (s *Store) List(ctx context.Context, matching Filter) ([]Item, error) {
 		return nil, fmt.Errorf("unable to list version-platform pairs in store: %w", err)
 	}
 
-	sort.Slice(res, func(i, j int) bool {
-		if !res[i].Version.Matches(res[j].Version) {
-			return res[i].Version.NewerThan(res[j].Version)
+	slices.SortStableFunc(res, func(i, j Item) int {
+		if !i.Version.Matches(j.Version) {
+			return i.Version.Compare(j.Version)
 		}
-		return orderPlatforms(res[i].Platform, res[j].Platform)
+		return orderPlatforms(i.Platform, j.Platform)
 	})
 
 	return res, nil
@@ -296,10 +297,10 @@ func (s *Store) removeItem(itemDir afero.Fs) error {
 }
 
 // orderPlatforms orders platforms by OS then arch.
-func orderPlatforms(first, second versions.Platform) bool {
+func orderPlatforms(first, second versions.Platform) int {
 	// sort by OS, then arch
 	if first.OS != second.OS {
-		return first.OS < second.OS
+		return strings.Compare(first.OS, second.OS)
 	}
-	return first.Arch < second.Arch
+	return strings.Compare(first.Arch, second.Arch)
 }
diff --git a/tools/setup-envtest/versions/misc_test.go b/tools/setup-envtest/versions/misc_test.go
index a609f4dc..1c0dbb68 100644
--- a/tools/setup-envtest/versions/misc_test.go
+++ b/tools/setup-envtest/versions/misc_test.go
@@ -36,13 +36,13 @@ var _ = Describe("Concrete", func() {
 	Describe("when ordering relative to other versions", func() {
 		ver1163 := Concrete{Major: 1, Minor: 16, Patch: 3}
 		Specify("newer patch should be newer", func() {
-			Expect(ver1163.NewerThan(Concrete{Major: 1, Minor: 16})).To(BeTrue())
+			Expect(ver1163.Compare(Concrete{Major: 1, Minor: 16})).To(Equal(1))
 		})
 		Specify("newer minor should be newer", func() {
-			Expect(ver1163.NewerThan(Concrete{Major: 1, Minor: 15, Patch: 3})).To(BeTrue())
+			Expect(ver1163.Compare(Concrete{Major: 1, Minor: 15, Patch: 3})).To(Equal(1))
 		})
 		Specify("newer major should be newer", func() {
-			Expect(ver1163.NewerThan(Concrete{Major: 0, Minor: 16, Patch: 3})).To(BeTrue())
+			Expect(ver1163.Compare(Concrete{Major: 0, Minor: 16, Patch: 3})).To(Equal(1))
 		})
 	})
 })
diff --git a/tools/setup-envtest/versions/version.go b/tools/setup-envtest/versions/version.go
index 945a9500..bad05c32 100644
--- a/tools/setup-envtest/versions/version.go
+++ b/tools/setup-envtest/versions/version.go
@@ -27,15 +27,24 @@ func (c Concrete) AsConcrete() *Concrete {
 	return &c
 }
 
-// NewerThan checks if the given other version is newer than this one.
-func (c Concrete) NewerThan(other Concrete) bool {
+// Compare checks if the given other version is newer than this one.
+func (c Concrete) Compare(other Concrete) int {
+	IntCompare := func(a, b int) int {
+		if a > b {
+			return 1
+		} else if a < b {
+			return -1
+		}
+		return 0
+	}
+
 	if c.Major != other.Major {
-		return c.Major > other.Major
+		return IntCompare(c.Major, other.Major)
 	}
 	if c.Minor != other.Minor {
-		return c.Minor > other.Minor
+		return IntCompare(c.Minor, other.Minor)
 	}
-	return c.Patch > other.Patch
+	return IntCompare(c.Patch, other.Patch)
 }
 
 // Matches checks if this version is equal to the other one.
diff --git a/tools/setup-envtest/workflows/workflows_test.go b/tools/setup-envtest/workflows/workflows_test.go
index 435ae242..eaa1c8c0 100644
--- a/tools/setup-envtest/workflows/workflows_test.go
+++ b/tools/setup-envtest/workflows/workflows_test.go
@@ -9,7 +9,7 @@ import (
 	"io/fs"
 	"path/filepath"
 	"runtime/debug"
-	"sort"
+	"slices"
 	"strings"
 
 	. "github.com/onsi/ginkgo/v2"
@@ -333,7 +333,7 @@ var _ = Describe("Workflows", func() {
 							archiveNames = append(archiveNames, archiveName)
 						}
 					}
-					sort.Strings(archiveNames)
+					slices.Sort(archiveNames)
 					archiveNamesSet := sets.Set[string]{}.Insert(archiveNames[:7]...)
 					// Delete all other archives
 					for _, release := range remoteHTTPItems.index.Releases {