Repository: kubernetes-sigs/cluster-api-provider-vsphere
Pull request #2296 (2023-08-29 07:26:37)

:seedling: Move webhooks out of API packages
**What this PR does / why we need it**:
Address #2159 to move the webhooks to `internal/webhooks` pkg.

**Which issue(s) this PR fixes** *(optional, in `fixes #<issue number>(, fixes #<issue_number>, ...)` format, will close the issue(s) when PR gets merged)*:



k8s-ci-robot: [APPROVALNOTIFIER] This PR is **NOT APPROVED**

This pull-request has been approved by:
**Once this PR has been reviewed and has the lgtm label**, please assign [neolit123](https://github.com/neolit123) for approval. For more information see [the Kubernetes Code Review Process](https://git.k8s.io/community/contributors/guide/owners.md#the-code-review-process).

The full list of commands accepted by this bot can be found [here](https://go.k8s.io/bot-commands?repo=kubernetes-sigs%2Fcluster-api-provider-vsphere).

<details open>
Needs approval from an approver in each of these files:

- **[OWNERS](https://github.com/kubernetes-sigs/cluster-api-provider-vsphere/blob/main/OWNERS)**

Approvers can indicate their approval by writing `/approve` in a comment
Approvers can cancel approval by writing `/approve cancel` in a comment
</details>
<!-- META={"approvers":["neolit123"]} -->
k8s-ci-robot: Hi @laozc. Thanks for your PR.

I'm waiting for a [kubernetes-sigs](https://github.com/orgs/kubernetes-sigs/people) member to verify that this patch is reasonable to test. If it is, they should reply with `/ok-to-test` on its own line. Until that is done, I will not automatically test new commits in this PR, but the usual testing commands by org members will still work. Regular contributors should [join the org](https://git.k8s.io/community/community-membership.md#member) to skip this step.

Once the patch is verified, the new status will be reflected by the `ok-to-test` label.

I understand the commands that are listed [here](https://go.k8s.io/bot-commands?repo=kubernetes-sigs%2Fcluster-api-provider-vsphere).

<details>

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>

sbueringer: /ok-to-test
sbueringer: Let me know if you want a first review (I usually ignore WIP/Draft PRs except if folks ask for reviews)
k8s-ci-robot: @laozc: The following test **failed**, say `/retest` to rerun all failed tests or `/retest-required` to rerun all mandatory failed tests:

Test name | Commit | Details | Required | Rerun command
--- | --- | --- | --- | ---
pull-cluster-api-provider-vsphere-apidiff-main | 4c959c2f81952f32e6c9ae18f6703a3c00c4089e | [link](https://prow.k8s.io/view/gs/kubernetes-jenkins/pr-logs/pull/kubernetes-sigs_cluster-api-provider-vsphere/2296/pull-cluster-api-provider-vsphere-apidiff-main/1696460828145881088) | false | `/test pull-cluster-api-provider-vsphere-apidiff-main`

[Full PR test history](https://prow.k8s.io/pr-history?org=kubernetes-sigs&repo=cluster-api-provider-vsphere&pr=2296). [Your PR dashboard](https://prow.k8s.io/pr?query=is%3Apr%20state%3Aopen%20author%3Alaozc). Please help us cut down on flakes by [linking to](https://git.k8s.io/community/contributors/devel/sig-testing/flaky-tests.md#filing-issues-for-flaky-tests) an [open issue](https://github.com/kubernetes-sigs/cluster-api-provider-vsphere/issues?q=is:issue+is:open) when you hit one in your PR.

<details>

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. I understand the commands that are listed [here](https://go.k8s.io/bot-commands).
</details>
<!-- test report -->
k8s-ci-robot: @laozc: The following test **failed**, say `/retest` to rerun all failed tests or `/retest-required` to rerun all mandatory failed tests:

Test name | Commit | Details | Required | Rerun command
--- | --- | --- | --- | ---
pull-cluster-api-provider-vsphere-apidiff-main | 4c959c2f81952f32e6c9ae18f6703a3c00c4089e | [link](https://prow.k8s.io/view/gs/kubernetes-jenkins/pr-logs/pull/kubernetes-sigs_cluster-api-provider-vsphere/2296/pull-cluster-api-provider-vsphere-apidiff-main/1696499280837087232) | false | `/test pull-cluster-api-provider-vsphere-apidiff-main`

[Full PR test history](https://prow.k8s.io/pr-history?org=kubernetes-sigs&repo=cluster-api-provider-vsphere&pr=2296). [Your PR dashboard](https://prow.k8s.io/pr?query=is%3Apr%20state%3Aopen%20author%3Alaozc). Please help us cut down on flakes by [linking to](https://git.k8s.io/community/contributors/devel/sig-testing/flaky-tests.md#filing-issues-for-flaky-tests) an [open issue](https://github.com/kubernetes-sigs/cluster-api-provider-vsphere/issues?q=is:issue+is:open) when you hit one in your PR.

<details>

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. I understand the commands that are listed [here](https://go.k8s.io/bot-commands).
</details>
<!-- test report -->
k8s-ci-robot: @laozc: The following test **failed**, say `/retest` to rerun all failed tests or `/retest-required` to rerun all mandatory failed tests:

Test name | Commit | Details | Required | Rerun command
--- | --- | --- | --- | ---
pull-cluster-api-provider-vsphere-apidiff-main | 46f2b6b9493df143a2153c7762b6cb9676c6aef0 | [link](https://prow.k8s.io/view/gs/kubernetes-jenkins/pr-logs/pull/kubernetes-sigs_cluster-api-provider-vsphere/2296/pull-cluster-api-provider-vsphere-apidiff-main/1697569122050641920) | false | `/test pull-cluster-api-provider-vsphere-apidiff-main`

[Full PR test history](https://prow.k8s.io/pr-history?org=kubernetes-sigs&repo=cluster-api-provider-vsphere&pr=2296). [Your PR dashboard](https://prow.k8s.io/pr?query=is%3Apr%20state%3Aopen%20author%3Alaozc). Please help us cut down on flakes by [linking to](https://git.k8s.io/community/contributors/devel/sig-testing/flaky-tests.md#filing-issues-for-flaky-tests) an [open issue](https://github.com/kubernetes-sigs/cluster-api-provider-vsphere/issues?q=is:issue+is:open) when you hit one in your PR.

<details>

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. I understand the commands that are listed [here](https://go.k8s.io/bot-commands).
</details>
<!-- test report -->
k8s-ci-robot: @laozc: The following test **failed**, say `/retest` to rerun all failed tests or `/retest-required` to rerun all mandatory failed tests:

Test name | Commit | Details | Required | Rerun command
--- | --- | --- | --- | ---
pull-cluster-api-provider-vsphere-apidiff-main | c09e6420de03dfaa6e2b3206dd50efb58f0054c4 | [link](https://prow.k8s.io/view/gs/kubernetes-jenkins/pr-logs/pull/kubernetes-sigs_cluster-api-provider-vsphere/2296/pull-cluster-api-provider-vsphere-apidiff-main/1698513488227340288) | false | `/test pull-cluster-api-provider-vsphere-apidiff-main`

[Full PR test history](https://prow.k8s.io/pr-history?org=kubernetes-sigs&repo=cluster-api-provider-vsphere&pr=2296). [Your PR dashboard](https://prow.k8s.io/pr?query=is%3Apr%20state%3Aopen%20author%3Alaozc). Please help us cut down on flakes by [linking to](https://git.k8s.io/community/contributors/devel/sig-testing/flaky-tests.md#filing-issues-for-flaky-tests) an [open issue](https://github.com/kubernetes-sigs/cluster-api-provider-vsphere/issues?q=is:issue+is:open) when you hit one in your PR.

<details>

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. I understand the commands that are listed [here](https://go.k8s.io/bot-commands).
</details>
<!-- test report -->

Patch:
diff --git a/Makefile b/Makefile
index d76bca7e..3b108a3a 100644
--- a/Makefile
+++ b/Makefile
@@ -246,6 +246,7 @@ generate-manifests: $(CONTROLLER_GEN) ## Generate manifests e.g. CRD, RBAC etc.
 		paths=./apis/v1alpha3 \
 		paths=./apis/v1alpha4 \
 		paths=./apis/v1beta1 \
+		paths=./internal/webhooks \
 		crd:crdVersions=v1 \
 		output:crd:dir=$(CRD_ROOT) \
 		output:webhook:dir=$(WEBHOOK_ROOT) \
@@ -455,7 +456,7 @@ setup-envtest: $(SETUP_ENVTEST) ## Set up envtest (download kubebuilder assets)
 
 .PHONY: test
 test: $(SETUP_ENVTEST) $(GOVC) ## Run unit tests
-	KUBEBUILDER_ASSETS="$(KUBEBUILDER_ASSETS)" GOVC_BIN_PATH=$(GOVC) go test -v ./apis/... ./controllers/... ./pkg/... $(TEST_ARGS)
+	KUBEBUILDER_ASSETS="$(KUBEBUILDER_ASSETS)" GOVC_BIN_PATH=$(GOVC) go test -v ./apis/... ./controllers/... ./pkg/... ./internal/... $(TEST_ARGS)
 
 .PHONY: test-verbose
 test-verbose: ## Run unit tests with verbose flag
@@ -463,7 +464,7 @@ test-verbose: ## Run unit tests with verbose flag
 
 .PHONY: test-junit
 test-junit: $(SETUP_ENVTEST) $(GOTESTSUM) $(GOVC) ## Run unit tests
-	set +o errexit; (KUBEBUILDER_ASSETS="$(KUBEBUILDER_ASSETS)" GOVC_BIN_PATH=$(GOVC) go test -json ./apis/... ./controllers/... ./pkg/... $(TEST_ARGS); echo $$? > $(ARTIFACTS)/junit.exitcode) | tee $(ARTIFACTS)/junit.stdout
+	set +o errexit; (KUBEBUILDER_ASSETS="$(KUBEBUILDER_ASSETS)" GOVC_BIN_PATH=$(GOVC) go test -json ./apis/... ./controllers/... ./pkg/... ./internal/... $(TEST_ARGS); echo $$? > $(ARTIFACTS)/junit.exitcode) | tee $(ARTIFACTS)/junit.stdout
 	$(GOTESTSUM) --junitfile $(ARTIFACTS)/junit.xml --raw-command cat $(ARTIFACTS)/junit.stdout
 	exit $$(cat $(ARTIFACTS)/junit.exitcode)
 
diff --git a/apis/v1beta1/vspherefailuredomain_webhook.go b/apis/v1beta1/vspherefailuredomain_webhook.go
deleted file mode 100644
index 06a6a953..00000000
--- a/apis/v1beta1/vspherefailuredomain_webhook.go
+++ /dev/null
@@ -1,94 +0,0 @@
-/*
-Copyright 2021 The Kubernetes Authors.
-
-Licensed under the Apache License, Version 2.0 (the "License");
-you may not use this file except in compliance with the License.
-You may obtain a copy of the License at
-
-    http://www.apache.org/licenses/LICENSE-2.0
-
-Unless required by applicable law or agreed to in writing, software
-distributed under the License is distributed on an "AS IS" BASIS,
-WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-See the License for the specific language governing permissions and
-limitations under the License.
-*/
-
-package v1beta1
-
-import (
-	"fmt"
-	"reflect"
-
-	"k8s.io/apimachinery/pkg/runtime"
-	"k8s.io/apimachinery/pkg/util/validation/field"
-	"k8s.io/utils/pointer"
-	ctrl "sigs.k8s.io/controller-runtime"
-	"sigs.k8s.io/controller-runtime/pkg/webhook"
-	"sigs.k8s.io/controller-runtime/pkg/webhook/admission"
-)
-
-func (r *VSphereFailureDomain) SetupWebhookWithManager(mgr ctrl.Manager) error {
-	return ctrl.NewWebhookManagedBy(mgr).
-		For(r).
-		Complete()
-}
-
-//+kubebuilder:webhook:verbs=create;update,path=/validate-infrastructure-cluster-x-k8s-io-v1beta1-vspherefailuredomain,mutating=false,failurePolicy=fail,matchPolicy=Equivalent,groups=infrastructure.cluster.x-k8s.io,resources=vspherefailuredomains,versions=v1beta1,name=validation.vspherefailuredomain.infrastructure.cluster.x-k8s.io,sideEffects=None,admissionReviewVersions=v1beta1
-//+kubebuilder:webhook:path=/mutate-infrastructure-cluster-x-k8s-io-v1beta1-vspherefailuredomain,mutating=true,failurePolicy=fail,matchPolicy=Equivalent,groups=infrastructure.cluster.x-k8s.io,resources=vspherefailuredomains,verbs=create;update,versions=v1beta1,name=default.vspherefailuredomain.infrastructure.cluster.x-k8s.io,sideEffects=None,admissionReviewVersions=v1beta1
-
-var _ webhook.Validator = &VSphereFailureDomain{}
-
-var _ webhook.Defaulter = &VSphereFailureDomain{}
-
-// ValidateCreate implements webhook.Validator so a webhook will be registered for the type.
-func (r *VSphereFailureDomain) ValidateCreate() (admission.Warnings, error) {
-	var allErrs field.ErrorList
-
-	if r.Spec.Topology.ComputeCluster == nil && r.Spec.Topology.Hosts != nil {
-		allErrs = append(allErrs, field.Forbidden(field.NewPath("spec", "Topology", "ComputeCluster"), "cannot be empty if Hosts is not empty"))
-	}
-
-	if r.Spec.Region.Type == HostGroupFailureDomain {
-		allErrs = append(allErrs, field.Forbidden(field.NewPath("spec", "Region", "Type"), fmt.Sprintf("region's Failure Domain type cannot be %s", r.Spec.Region.Type)))
-	}
-
-	if r.Spec.Zone.Type == HostGroupFailureDomain && r.Spec.Topology.Hosts == nil {
-		allErrs = append(allErrs, field.Forbidden(field.NewPath("spec", "Topology", "Hosts"), fmt.Sprintf("cannot be nil if zone's Failure Domain type is %s", r.Spec.Zone.Type)))
-	}
-
-	if r.Spec.Region.Type == ComputeClusterFailureDomain && r.Spec.Topology.ComputeCluster == nil {
-		allErrs = append(allErrs, field.Forbidden(field.NewPath("spec", "Topology", "ComputeCluster"), fmt.Sprintf("cannot be nil if region's Failure Domain type is %s", r.Spec.Region.Type)))
-	}
-
-	if r.Spec.Zone.Type == ComputeClusterFailureDomain && r.Spec.Topology.ComputeCluster == nil {
-		allErrs = append(allErrs, field.Forbidden(field.NewPath("spec", "Topology", "ComputeCluster"), fmt.Sprintf("cannot be nil if zone's Failure Domain type is %s", r.Spec.Zone.Type)))
-	}
-
-	return nil, aggregateObjErrors(r.GroupVersionKind().GroupKind(), r.Name, allErrs)
-}
-
-// ValidateUpdate implements webhook.Validator so a webhook will be registered for the type.
-func (r *VSphereFailureDomain) ValidateUpdate(old runtime.Object) (admission.Warnings, error) {
-	oldVSphereFailureDomain, ok := old.(*VSphereFailureDomain)
-	if !ok || !reflect.DeepEqual(r.Spec, oldVSphereFailureDomain.Spec) {
-		return nil, field.Forbidden(field.NewPath("spec"), "VSphereFailureDomainSpec is immutable")
-	}
-	return nil, nil
-}
-
-// ValidateDelete implements webhook.Validator so a webhook will be registered for the type.
-func (r *VSphereFailureDomain) ValidateDelete() (admission.Warnings, error) {
-	return nil, nil
-}
-
-// Default implements webhook.Defaulter so a webhook will be registered for the type.
-func (r *VSphereFailureDomain) Default() {
-	if r.Spec.Zone.AutoConfigure == nil {
-		r.Spec.Zone.AutoConfigure = pointer.Bool(false)
-	}
-
-	if r.Spec.Region.AutoConfigure == nil {
-		r.Spec.Region.AutoConfigure = pointer.Bool(false)
-	}
-}
diff --git a/internal/webhooks/doc.go b/internal/webhooks/doc.go
new file mode 100644
index 00000000..5745a103
--- /dev/null
+++ b/internal/webhooks/doc.go
@@ -0,0 +1,18 @@
+/*
+Copyright 2023 The Kubernetes Authors.
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+    http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+// Package webhooks contains webhooks for the infrastructure v1beta1 API group.
+package webhooks
diff --git a/apis/v1beta1/webhooks.go b/internal/webhooks/util.go
similarity index 98%
rename from apis/v1beta1/webhooks.go
rename to internal/webhooks/util.go
index ba5c4907..cc485089 100644
--- a/apis/v1beta1/webhooks.go
+++ b/internal/webhooks/util.go
@@ -14,7 +14,7 @@ See the License for the specific language governing permissions and
 limitations under the License.
 */
 
-package v1beta1
+package webhooks
 
 import (
 	apierrors "k8s.io/apimachinery/pkg/api/errors"
diff --git a/apis/v1beta1/vsphereclustertemplate_webhook.go b/internal/webhooks/vsphereclustertemplate.go
similarity index 55%
rename from apis/v1beta1/vsphereclustertemplate_webhook.go
rename to internal/webhooks/vsphereclustertemplate.go
index 828159f8..d9e83721 100644
--- a/apis/v1beta1/vsphereclustertemplate_webhook.go
+++ b/internal/webhooks/vsphereclustertemplate.go
@@ -14,43 +14,59 @@ See the License for the specific language governing permissions and
 limitations under the License.
 */
 
-package v1beta1
+package webhooks
 
 import (
+	"context"
+	"fmt"
 	"reflect"
 
+	apierrors "k8s.io/apimachinery/pkg/api/errors"
 	"k8s.io/apimachinery/pkg/runtime"
 	"k8s.io/apimachinery/pkg/util/validation/field"
 	ctrl "sigs.k8s.io/controller-runtime"
 	"sigs.k8s.io/controller-runtime/pkg/webhook"
 	"sigs.k8s.io/controller-runtime/pkg/webhook/admission"
+
+	infrav1 "sigs.k8s.io/cluster-api-provider-vsphere/apis/v1beta1"
 )
 
-func (r *VSphereClusterTemplate) SetupWebhookWithManager(mgr ctrl.Manager) error {
+// +kubebuilder:webhook:verbs=create;update,path=/validate-infrastructure-cluster-x-k8s-io-v1beta1-vsphereclustertemplate,mutating=false,failurePolicy=fail,matchPolicy=Equivalent,groups=infrastructure.cluster.x-k8s.io,resources=vsphereclustertemplates,versions=v1beta1,name=validation.vsphereclustertemplate.infrastructure.x-k8s.io,sideEffects=None,admissionReviewVersions=v1beta1
+
+// VSphereClusterTemplateWebhook implements a validation and defaulting webhook for VSphereClusterTemplate.
+type VSphereClusterTemplateWebhook struct{}
+
+var _ webhook.CustomValidator = &VSphereClusterTemplateWebhook{}
+
+func (webhook *VSphereClusterTemplateWebhook) SetupWebhookWithManager(mgr ctrl.Manager) error {
 	return ctrl.NewWebhookManagedBy(mgr).
-		For(r).
+		For(&infrav1.VSphereClusterTemplate{}).
+		WithValidator(webhook).
 		Complete()
 }
 
-// +kubebuilder:webhook:verbs=create;update,path=/validate-infrastructure-cluster-x-k8s-io-v1beta1-vsphereclustertemplate,mutating=false,failurePolicy=fail,matchPolicy=Equivalent,groups=infrastructure.cluster.x-k8s.io,resources=vsphereclustertemplates,versions=v1beta1,name=validation.vsphereclustertemplate.infrastructure.x-k8s.io,sideEffects=None,admissionReviewVersions=v1beta1
-
-var _ webhook.Validator = &VSphereClusterTemplate{}
-
 // ValidateCreate implements webhook.Validator so a webhook will be registered for the type.
-func (r *VSphereClusterTemplate) ValidateCreate() (admission.Warnings, error) {
+func (webhook *VSphereClusterTemplateWebhook) ValidateCreate(_ context.Context, _ runtime.Object) (admission.Warnings, error) {
 	return nil, nil
 }
 
 // ValidateUpdate implements webhook.Validator so a webhook will be registered for the type.
-func (r *VSphereClusterTemplate) ValidateUpdate(oldRaw runtime.Object) (admission.Warnings, error) {
-	old := oldRaw.(*VSphereClusterTemplate)
-	if !reflect.DeepEqual(r.Spec.Template.Spec, old.Spec.Template.Spec) {
+func (webhook *VSphereClusterTemplateWebhook) ValidateUpdate(_ context.Context, oldRaw runtime.Object, newRaw runtime.Object) (admission.Warnings, error) {
+	oldTyped, ok := oldRaw.(*infrav1.VSphereClusterTemplate)
+	if !ok {
+		return nil, apierrors.NewBadRequest(fmt.Sprintf("expected a VSphereClusterTemplate but got a %T", oldRaw))
+	}
+	newTyped, ok := newRaw.(*infrav1.VSphereClusterTemplate)
+	if !ok {
+		return nil, apierrors.NewBadRequest(fmt.Sprintf("expected a VSphereClusterTemplate but got a %T", newRaw))
+	}
+	if !reflect.DeepEqual(newTyped.Spec.Template.Spec, oldTyped.Spec.Template.Spec) {
 		return nil, field.Forbidden(field.NewPath("spec", "template", "spec"), "VSphereClusterTemplate spec is immutable")
 	}
 	return nil, nil
 }
 
 // ValidateDelete implements webhook.Validator so a webhook will be registered for the type.
-func (r *VSphereClusterTemplate) ValidateDelete() (admission.Warnings, error) {
+func (webhook *VSphereClusterTemplateWebhook) ValidateDelete(_ context.Context, _ runtime.Object) (admission.Warnings, error) {
 	return nil, nil
 }
diff --git a/apis/v1beta1/vspheredeploymentzone_webhook.go b/internal/webhooks/vspheredeploymentzone.go
similarity index 56%
rename from apis/v1beta1/vspheredeploymentzone_webhook.go
rename to internal/webhooks/vspheredeploymentzone.go
index c98e10cb..38051368 100644
--- a/apis/v1beta1/vspheredeploymentzone_webhook.go
+++ b/internal/webhooks/vspheredeploymentzone.go
@@ -14,27 +14,44 @@ See the License for the specific language governing permissions and
 limitations under the License.
 */
 
-package v1beta1
+package webhooks
 
 import (
+	"context"
+	"fmt"
+
+	apierrors "k8s.io/apimachinery/pkg/api/errors"
+	"k8s.io/apimachinery/pkg/runtime"
 	"k8s.io/utils/pointer"
 	ctrl "sigs.k8s.io/controller-runtime"
 	"sigs.k8s.io/controller-runtime/pkg/webhook"
+
+	infrav1 "sigs.k8s.io/cluster-api-provider-vsphere/apis/v1beta1"
 )
 
-func (z *VSphereDeploymentZone) SetupWebhookWithManager(mgr ctrl.Manager) error {
+// +kubebuilder:webhook:verbs=create;update,path=/mutate-infrastructure-cluster-x-k8s-io-v1beta1-vspheredeploymentzone,mutating=true,failurePolicy=fail,matchPolicy=Equivalent,groups=infrastructure.cluster.x-k8s.io,resources=vspheredeploymentzones,versions=v1beta1,name=default.vspheredeploymentzone.infrastructure.cluster.x-k8s.io,sideEffects=None,admissionReviewVersions=v1beta1
+
+// VSphereDeploymentZoneWebhook implements a validation and defaulting webhook for VSphereDeploymentZone.
+type VSphereDeploymentZoneWebhook struct{}
+
+var _ webhook.CustomDefaulter = &VSphereDeploymentZoneWebhook{}
+
+func (webhook *VSphereDeploymentZoneWebhook) SetupWebhookWithManager(mgr ctrl.Manager) error {
 	return ctrl.NewWebhookManagedBy(mgr).
-		For(z).
+		For(&infrav1.VSphereDeploymentZone{}).
+		WithDefaulter(webhook).
 		Complete()
 }
 
-// +kubebuilder:webhook:verbs=create;update,path=/mutate-infrastructure-cluster-x-k8s-io-v1beta1-vspheredeploymentzone,mutating=true,failurePolicy=fail,matchPolicy=Equivalent,groups=infrastructure.cluster.x-k8s.io,resources=vspheredeploymentzones,versions=v1beta1,name=default.vspheredeploymentzone.infrastructure.cluster.x-k8s.io,sideEffects=None,admissionReviewVersions=v1beta1
-
-var _ webhook.Defaulter = &VSphereDeploymentZone{}
-
 // Default implements webhook.Defaulter so a webhook will be registered for the type.
-func (z *VSphereDeploymentZone) Default() {
-	if z.Spec.ControlPlane == nil {
-		z.Spec.ControlPlane = pointer.Bool(true)
+func (webhook *VSphereDeploymentZoneWebhook) Default(_ context.Context, obj runtime.Object) error {
+	typedObj, ok := obj.(*infrav1.VSphereDeploymentZone)
+	if !ok {
+		return apierrors.NewBadRequest(fmt.Sprintf("expected a VSphereDeploymentZone but got a %T", obj))
 	}
+	if typedObj.Spec.ControlPlane == nil {
+		typedObj.Spec.ControlPlane = pointer.Bool(true)
+	}
+
+	return nil
 }
diff --git a/internal/webhooks/vspherefailuredomain.go b/internal/webhooks/vspherefailuredomain.go
new file mode 100644
index 00000000..cdab6929
--- /dev/null
+++ b/internal/webhooks/vspherefailuredomain.go
@@ -0,0 +1,119 @@
+/*
+Copyright 2021 The Kubernetes Authors.
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+    http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+package webhooks
+
+import (
+	"context"
+	"fmt"
+	"reflect"
+
+	apierrors "k8s.io/apimachinery/pkg/api/errors"
+	"k8s.io/apimachinery/pkg/runtime"
+	"k8s.io/apimachinery/pkg/util/validation/field"
+	"k8s.io/utils/pointer"
+	ctrl "sigs.k8s.io/controller-runtime"
+	"sigs.k8s.io/controller-runtime/pkg/webhook"
+	"sigs.k8s.io/controller-runtime/pkg/webhook/admission"
+
+	infrav1 "sigs.k8s.io/cluster-api-provider-vsphere/apis/v1beta1"
+)
+
+// +kubebuilder:webhook:verbs=create;update,path=/validate-infrastructure-cluster-x-k8s-io-v1beta1-vspherefailuredomain,mutating=false,failurePolicy=fail,matchPolicy=Equivalent,groups=infrastructure.cluster.x-k8s.io,resources=vspherefailuredomains,versions=v1beta1,name=validation.vspherefailuredomain.infrastructure.cluster.x-k8s.io,sideEffects=None,admissionReviewVersions=v1beta1
+// +kubebuilder:webhook:path=/mutate-infrastructure-cluster-x-k8s-io-v1beta1-vspherefailuredomain,mutating=true,failurePolicy=fail,matchPolicy=Equivalent,groups=infrastructure.cluster.x-k8s.io,resources=vspherefailuredomains,verbs=create;update,versions=v1beta1,name=default.vspherefailuredomain.infrastructure.cluster.x-k8s.io,sideEffects=None,admissionReviewVersions=v1beta1
+
+// VSphereFailureDomainWebhook implements a validation and defaulting webhook for VSphereFailureDomain.
+type VSphereFailureDomainWebhook struct{}
+
+var _ webhook.CustomValidator = &VSphereFailureDomainWebhook{}
+var _ webhook.CustomDefaulter = &VSphereFailureDomainWebhook{}
+
+func (webhook *VSphereFailureDomainWebhook) SetupWebhookWithManager(mgr ctrl.Manager) error {
+	return ctrl.NewWebhookManagedBy(mgr).
+		For(&infrav1.VSphereFailureDomain{}).
+		WithValidator(webhook).
+		WithDefaulter(webhook).
+		Complete()
+}
+
+// ValidateCreate implements webhook.Validator so a webhook will be registered for the type.
+func (webhook *VSphereFailureDomainWebhook) ValidateCreate(_ context.Context, raw runtime.Object) (admission.Warnings, error) {
+	var allErrs field.ErrorList
+
+	obj, ok := raw.(*infrav1.VSphereFailureDomain)
+	if !ok {
+		return nil, apierrors.NewBadRequest(fmt.Sprintf("expected a VSphereFailureDomain but got a %T", raw))
+	}
+	if obj.Spec.Topology.ComputeCluster == nil && obj.Spec.Topology.Hosts != nil {
+		allErrs = append(allErrs, field.Forbidden(field.NewPath("spec", "Topology", "ComputeCluster"), "cannot be empty if Hosts is not empty"))
+	}
+
+	if obj.Spec.Region.Type == infrav1.HostGroupFailureDomain {
+		allErrs = append(allErrs, field.Forbidden(field.NewPath("spec", "Region", "Type"), fmt.Sprintf("region's Failure Domain type cannot be %s", obj.Spec.Region.Type)))
+	}
+
+	if obj.Spec.Zone.Type == infrav1.HostGroupFailureDomain && obj.Spec.Topology.Hosts == nil {
+		allErrs = append(allErrs, field.Forbidden(field.NewPath("spec", "Topology", "Hosts"), fmt.Sprintf("cannot be nil if zone's Failure Domain type is %s", obj.Spec.Zone.Type)))
+	}
+
+	if obj.Spec.Region.Type == infrav1.ComputeClusterFailureDomain && obj.Spec.Topology.ComputeCluster == nil {
+		allErrs = append(allErrs, field.Forbidden(field.NewPath("spec", "Topology", "ComputeCluster"), fmt.Sprintf("cannot be nil if region's Failure Domain type is %s", obj.Spec.Region.Type)))
+	}
+
+	if obj.Spec.Zone.Type == infrav1.ComputeClusterFailureDomain && obj.Spec.Topology.ComputeCluster == nil {
+		allErrs = append(allErrs, field.Forbidden(field.NewPath("spec", "Topology", "ComputeCluster"), fmt.Sprintf("cannot be nil if zone's Failure Domain type is %s", obj.Spec.Zone.Type)))
+	}
+
+	return nil, aggregateObjErrors(obj.GroupVersionKind().GroupKind(), obj.Name, allErrs)
+}
+
+// ValidateUpdate implements webhook.Validator so a webhook will be registered for the type.
+func (webhook *VSphereFailureDomainWebhook) ValidateUpdate(_ context.Context, oldRaw runtime.Object, newRaw runtime.Object) (admission.Warnings, error) {
+	oldTyped, ok := oldRaw.(*infrav1.VSphereFailureDomain)
+	if !ok {
+		return nil, apierrors.NewBadRequest(fmt.Sprintf("expected a VSphereFailureDomain but got a %T", oldRaw))
+	}
+	newTyped, ok := newRaw.(*infrav1.VSphereFailureDomain)
+	if !ok {
+		return nil, apierrors.NewBadRequest(fmt.Sprintf("expected a VSphereFailureDomain but got a %T", newRaw))
+	}
+	if !reflect.DeepEqual(newTyped.Spec, oldTyped.Spec) {
+		return nil, field.Forbidden(field.NewPath("spec"), "VSphereFailureDomainSpec is immutable")
+	}
+	return nil, nil
+}
+
+// ValidateDelete implements webhook.Validator so a webhook will be registered for the type.
+func (webhook *VSphereFailureDomainWebhook) ValidateDelete(_ context.Context, _ runtime.Object) (admission.Warnings, error) {
+	return nil, nil
+}
+
+// Default implements webhook.Defaulter so a webhook will be registered for the type.
+func (webhook *VSphereFailureDomainWebhook) Default(_ context.Context, obj runtime.Object) error {
+	typedObj, ok := obj.(*infrav1.VSphereFailureDomain)
+	if !ok {
+		return apierrors.NewBadRequest(fmt.Sprintf("expected a VSphereFailureDomain but got a %T", obj))
+	}
+	if typedObj.Spec.Zone.AutoConfigure == nil { //nolint:staticcheck
+		typedObj.Spec.Zone.AutoConfigure = pointer.Bool(false) //nolint:staticcheck
+	}
+
+	if typedObj.Spec.Region.AutoConfigure == nil { //nolint:staticcheck
+		typedObj.Spec.Region.AutoConfigure = pointer.Bool(false) //nolint:staticcheck
+	}
+
+	return nil
+}
diff --git a/apis/v1beta1/vspheremachine_webhook.go b/internal/webhooks/vspheremachine.go
similarity index 66%
rename from apis/v1beta1/vspheremachine_webhook.go
rename to internal/webhooks/vspheremachine.go
index dca4031c..745e5200 100644
--- a/apis/v1beta1/vspheremachine_webhook.go
+++ b/internal/webhooks/vspheremachine.go
@@ -14,9 +14,10 @@ See the License for the specific language governing permissions and
 limitations under the License.
 */
 
-package v1beta1
+package webhooks
 
 import (
+	"context"
 	"fmt"
 	"net"
 	"reflect"
@@ -28,31 +29,48 @@ import (
 	ctrl "sigs.k8s.io/controller-runtime"
 	"sigs.k8s.io/controller-runtime/pkg/webhook"
 	"sigs.k8s.io/controller-runtime/pkg/webhook/admission"
-)
 
-func (m *VSphereMachine) SetupWebhookWithManager(mgr ctrl.Manager) error {
-	return ctrl.NewWebhookManagedBy(mgr).
-		For(m).
-		Complete()
-}
+	infrav1 "sigs.k8s.io/cluster-api-provider-vsphere/apis/v1beta1"
+)
 
 // +kubebuilder:webhook:verbs=create;update,path=/validate-infrastructure-cluster-x-k8s-io-v1beta1-vspheremachine,mutating=false,failurePolicy=fail,matchPolicy=Equivalent,groups=infrastructure.cluster.x-k8s.io,resources=vspheremachines,versions=v1beta1,name=validation.vspheremachine.infrastructure.x-k8s.io,sideEffects=None,admissionReviewVersions=v1beta1
 // +kubebuilder:webhook:verbs=create;update,path=/mutate-infrastructure-cluster-x-k8s-io-v1beta1-vspheremachine,mutating=true,failurePolicy=fail,matchPolicy=Equivalent,groups=infrastructure.cluster.x-k8s.io,resources=vspheremachines,versions=v1beta1,name=default.vspheremachine.infrastructure.cluster.x-k8s.io,sideEffects=None,admissionReviewVersions=v1beta1
 
-var _ webhook.Validator = &VSphereMachine{}
+// VSphereMachineWebhook implements a validation and defaulting webhook for VSphereMachine.
+type VSphereMachineWebhook struct{}
+
+var _ webhook.CustomValidator = &VSphereMachineWebhook{}
+var _ webhook.CustomDefaulter = &VSphereMachineWebhook{}
 
-var _ webhook.Defaulter = &VSphereMachine{}
+func (webhook *VSphereMachineWebhook) SetupWebhookWithManager(mgr ctrl.Manager) error {
+	return ctrl.NewWebhookManagedBy(mgr).
+		For(&infrav1.VSphereMachine{}).
+		WithValidator(webhook).
+		WithDefaulter(webhook).
+		Complete()
+}
 
-func (m *VSphereMachine) Default() {
-	if m.Spec.Datacenter == "" {
-		m.Spec.Datacenter = "*"
+// Default implements webhook.Defaulter so a webhook will be registered for the type.
+func (webhook *VSphereMachineWebhook) Default(_ context.Context, obj runtime.Object) error {
+	objValue, ok := obj.(*infrav1.VSphereMachine)
+	if !ok {
+		return apierrors.NewBadRequest(fmt.Sprintf("expected a VSphereMachine but got a %T", obj))
 	}
+	if objValue.Spec.Datacenter == "" {
+		objValue.Spec.Datacenter = "*"
+	}
+	return nil
 }
 
 // ValidateCreate implements webhook.Validator so a webhook will be registered for the type.
-func (m *VSphereMachine) ValidateCreate() (admission.Warnings, error) {
+func (webhook *VSphereMachineWebhook) ValidateCreate(_ context.Context, raw runtime.Object) (admission.Warnings, error) {
 	var allErrs field.ErrorList
-	spec := m.Spec
+
+	obj, ok := raw.(*infrav1.VSphereMachine)
+	if !ok {
+		return nil, apierrors.NewBadRequest(fmt.Sprintf("expected a VSphereMachine but got a %T", raw))
+	}
+	spec := obj.Spec
 
 	if spec.Network.PreferredAPIServerCIDR != "" {
 		allErrs = append(allErrs, field.Invalid(field.NewPath("spec", "PreferredAPIServerCIDR"), spec.Network.PreferredAPIServerCIDR, "cannot be set, as it will be removed and is no longer used"))
@@ -67,7 +85,7 @@ func (m *VSphereMachine) ValidateCreate() (admission.Warnings, error) {
 	}
 
 	if spec.GuestSoftPowerOffTimeout != nil {
-		if spec.PowerOffMode != VirtualMachinePowerOpModeTrySoft {
+		if spec.PowerOffMode != infrav1.VirtualMachinePowerOpModeTrySoft {
 			allErrs = append(allErrs, field.Invalid(field.NewPath("spec", "guestSoftPowerOffTimeout"), spec.GuestSoftPowerOffTimeout, "should not be set in templates unless the powerOffMode is trySoft"))
 		}
 		if spec.GuestSoftPowerOffTimeout.Duration <= 0 {
@@ -75,27 +93,32 @@ func (m *VSphereMachine) ValidateCreate() (admission.Warnings, error) {
 		}
 	}
 
-	return nil, aggregateObjErrors(m.GroupVersionKind().GroupKind(), m.Name, allErrs)
+	return nil, aggregateObjErrors(obj.GroupVersionKind().GroupKind(), obj.Name, allErrs)
 }
 
 // ValidateUpdate implements webhook.Validator so a webhook will be registered for the type.
-func (m *VSphereMachine) ValidateUpdate(old runtime.Object) (admission.Warnings, error) {
+func (webhook *VSphereMachineWebhook) ValidateUpdate(_ context.Context, oldRaw runtime.Object, newRaw runtime.Object) (admission.Warnings, error) {
 	var allErrs field.ErrorList
-	if m.Spec.GuestSoftPowerOffTimeout != nil {
-		if m.Spec.PowerOffMode != VirtualMachinePowerOpModeTrySoft {
-			allErrs = append(allErrs, field.Invalid(field.NewPath("spec", "guestSoftPowerOffTimeout"), m.Spec.GuestSoftPowerOffTimeout, "should not be set in templates unless the powerOffMode is trySoft"))
+
+	newTyped, ok := newRaw.(*infrav1.VSphereMachine)
+	if !ok {
+		return nil, apierrors.NewBadRequest(fmt.Sprintf("expected a VSphereMachine but got a %T", newRaw))
+	}
+	if newTyped.Spec.GuestSoftPowerOffTimeout != nil {
+		if newTyped.Spec.PowerOffMode != infrav1.VirtualMachinePowerOpModeTrySoft {
+			allErrs = append(allErrs, field.Invalid(field.NewPath("spec", "guestSoftPowerOffTimeout"), newTyped.Spec.GuestSoftPowerOffTimeout, "should not be set in templates unless the powerOffMode is trySoft"))
 		}
-		if m.Spec.GuestSoftPowerOffTimeout.Duration <= 0 {
-			allErrs = append(allErrs, field.Invalid(field.NewPath("spec", "guestSoftPowerOffTimeout"), m.Spec.GuestSoftPowerOffTimeout, "should be greater than 0"))
+		if newTyped.Spec.GuestSoftPowerOffTimeout.Duration <= 0 {
+			allErrs = append(allErrs, field.Invalid(field.NewPath("spec", "guestSoftPowerOffTimeout"), newTyped.Spec.GuestSoftPowerOffTimeout, "should be greater than 0"))
 		}
 	}
 
-	newVSphereMachine, err := runtime.DefaultUnstructuredConverter.ToUnstructured(m)
+	newVSphereMachine, err := runtime.DefaultUnstructuredConverter.ToUnstructured(newRaw)
 	if err != nil {
 		return nil, apierrors.NewInternalError(errors.Wrap(err, "failed to convert new VSphereMachine to unstructured object"))
 	}
 
-	oldVSphereMachine, err := runtime.DefaultUnstructuredConverter.ToUnstructured(old)
+	oldVSphereMachine, err := runtime.DefaultUnstructuredConverter.ToUnstructured(oldRaw)
 	if err != nil {
 		return nil, apierrors.NewInternalError(errors.Wrap(err, "failed to convert old VSphereMachine to unstructured object"))
 	}
@@ -117,7 +140,7 @@ func (m *VSphereMachine) ValidateUpdate(old runtime.Object) (admission.Warnings,
 	delete(newVSphereMachineNetwork, "devices")
 
 	// validate that IPAddrs in updaterequest are valid.
-	spec := m.Spec
+	spec := newTyped.Spec
 	for i, device := range spec.Network.Devices {
 		for j, ip := range device.IPAddrs {
 			if _, _, err := net.ParseCIDR(ip); err != nil {
@@ -130,10 +153,10 @@ func (m *VSphereMachine) ValidateUpdate(old runtime.Object) (admission.Warnings,
 		allErrs = append(allErrs, field.Forbidden(field.NewPath("spec"), "cannot be modified"))
 	}
 
-	return nil, aggregateObjErrors(m.GroupVersionKind().GroupKind(), m.Name, allErrs)
+	return nil, aggregateObjErrors(newTyped.GroupVersionKind().GroupKind(), newTyped.Name, allErrs)
 }
 
 // ValidateDelete implements webhook.Validator so a webhook will be registered for the type.
-func (m *VSphereMachine) ValidateDelete() (admission.Warnings, error) {
+func (webhook *VSphereMachineWebhook) ValidateDelete(_ context.Context, _ runtime.Object) (admission.Warnings, error) {
 	return nil, nil
 }
diff --git a/apis/v1beta1/vspheremachinetemplate_webhook.go b/internal/webhooks/vspheremachinetemplate.go
similarity index 76%
rename from apis/v1beta1/vspheremachinetemplate_webhook.go
rename to internal/webhooks/vspheremachinetemplate.go
index 9f0cb47a..55024869 100644
--- a/apis/v1beta1/vspheremachinetemplate_webhook.go
+++ b/internal/webhooks/vspheremachinetemplate.go
@@ -14,7 +14,7 @@ See the License for the specific language governing permissions and
 limitations under the License.
 */
 
-package v1beta1
+package webhooks
 
 import (
 	"context"
@@ -29,28 +29,29 @@ import (
 	ctrl "sigs.k8s.io/controller-runtime"
 	"sigs.k8s.io/controller-runtime/pkg/webhook"
 	"sigs.k8s.io/controller-runtime/pkg/webhook/admission"
+
+	infrav1 "sigs.k8s.io/cluster-api-provider-vsphere/apis/v1beta1"
 )
 
 const machineTemplateImmutableMsg = "VSphereMachineTemplate spec.template.spec field is immutable. Please create a new resource instead."
 
-func (v *VSphereMachineTemplateWebhook) SetupWebhookWithManager(mgr ctrl.Manager) error {
-	return ctrl.NewWebhookManagedBy(mgr).
-		For(&VSphereMachineTemplate{}).
-		WithValidator(v).
-		Complete()
-}
-
 // +kubebuilder:webhook:verbs=create;update,path=/validate-infrastructure-cluster-x-k8s-io-v1beta1-vspheremachinetemplate,mutating=false,failurePolicy=fail,matchPolicy=Equivalent,groups=infrastructure.cluster.x-k8s.io,resources=vspheremachinetemplates,versions=v1beta1,name=validation.vspheremachinetemplate.infrastructure.x-k8s.io,sideEffects=None,admissionReviewVersions=v1beta1
 
-// VSphereMachineTemplateWebhook implements a custom validation webhook for DockerMachineTemplate.
-// +kubebuilder:object:generate=false
+// VSphereMachineTemplateWebhook implements a validation and defaulting webhook for VSphereMachineTemplate.
 type VSphereMachineTemplateWebhook struct{}
 
 var _ webhook.CustomValidator = &VSphereMachineTemplateWebhook{}
 
+func (webhook *VSphereMachineTemplateWebhook) SetupWebhookWithManager(mgr ctrl.Manager) error {
+	return ctrl.NewWebhookManagedBy(mgr).
+		For(&infrav1.VSphereMachineTemplate{}).
+		WithValidator(webhook).
+		Complete()
+}
+
 // ValidateCreate implements webhook.Validator so a webhook will be registered for the type.
-func (v *VSphereMachineTemplateWebhook) ValidateCreate(_ context.Context, raw runtime.Object) (admission.Warnings, error) {
-	obj, ok := raw.(*VSphereMachineTemplate)
+func (webhook *VSphereMachineTemplateWebhook) ValidateCreate(_ context.Context, raw runtime.Object) (admission.Warnings, error) {
+	obj, ok := raw.(*infrav1.VSphereMachineTemplate)
 	if !ok {
 		return nil, apierrors.NewBadRequest(fmt.Sprintf("expected a VSphereMachineTemplate but got a %T", raw))
 	}
@@ -76,7 +77,7 @@ func (v *VSphereMachineTemplateWebhook) ValidateCreate(_ context.Context, raw ru
 		}
 	}
 	if spec.GuestSoftPowerOffTimeout != nil {
-		if spec.PowerOffMode != VirtualMachinePowerOpModeTrySoft {
+		if spec.PowerOffMode != infrav1.VirtualMachinePowerOpModeTrySoft {
 			allErrs = append(allErrs, field.Invalid(field.NewPath("spec", "template", "spec", "guestSoftPowerOffTimeout"), spec.GuestSoftPowerOffTimeout, "should not be set in templates unless the powerOffMode is trySoft"))
 		}
 		if spec.GuestSoftPowerOffTimeout.Duration <= 0 {
@@ -87,14 +88,14 @@ func (v *VSphereMachineTemplateWebhook) ValidateCreate(_ context.Context, raw ru
 }
 
 // ValidateUpdate implements webhook.Validator so a webhook will be registered for the type.
-func (v *VSphereMachineTemplateWebhook) ValidateUpdate(ctx context.Context, oldRaw runtime.Object, newRaw runtime.Object) (admission.Warnings, error) {
-	newObj, ok := newRaw.(*VSphereMachineTemplate)
+func (webhook *VSphereMachineTemplateWebhook) ValidateUpdate(ctx context.Context, oldRaw runtime.Object, newRaw runtime.Object) (admission.Warnings, error) {
+	oldTyped, ok := oldRaw.(*infrav1.VSphereMachineTemplate)
 	if !ok {
-		return nil, apierrors.NewBadRequest(fmt.Sprintf("expected a VSphereMachineTemplate but got a %T", newRaw))
+		return nil, apierrors.NewBadRequest(fmt.Sprintf("expected a VSphereMachineTemplate but got a %T", oldRaw))
 	}
-	oldObj, ok := oldRaw.(*VSphereMachineTemplate)
+	newTyped, ok := newRaw.(*infrav1.VSphereMachineTemplate)
 	if !ok {
-		return nil, apierrors.NewBadRequest(fmt.Sprintf("expected a VSphereMachineTemplate but got a %T", oldRaw))
+		return nil, apierrors.NewBadRequest(fmt.Sprintf("expected a VSphereMachineTemplate but got a %T", newRaw))
 	}
 
 	req, err := admission.RequestFromContext(ctx)
@@ -103,14 +104,14 @@ func (v *VSphereMachineTemplateWebhook) ValidateUpdate(ctx context.Context, oldR
 	}
 
 	var allErrs field.ErrorList
-	if !topology.ShouldSkipImmutabilityChecks(req, newObj) &&
-		!reflect.DeepEqual(newObj.Spec.Template.Spec, oldObj.Spec.Template.Spec) {
-		allErrs = append(allErrs, field.Invalid(field.NewPath("spec", "template", "spec"), newObj, machineTemplateImmutableMsg))
+	if !topology.ShouldSkipImmutabilityChecks(req, newTyped) &&
+		!reflect.DeepEqual(newTyped.Spec.Template.Spec, oldTyped.Spec.Template.Spec) {
+		allErrs = append(allErrs, field.Invalid(field.NewPath("spec", "template", "spec"), newTyped, machineTemplateImmutableMsg))
 	}
-	return nil, aggregateObjErrors(newObj.GroupVersionKind().GroupKind(), newObj.Name, allErrs)
+	return nil, aggregateObjErrors(newTyped.GroupVersionKind().GroupKind(), newTyped.Name, allErrs)
 }
 
 // ValidateDelete implements webhook.Validator so a webhook will be registered for the type.
-func (v *VSphereMachineTemplateWebhook) ValidateDelete(_ context.Context, _ runtime.Object) (admission.Warnings, error) {
+func (webhook *VSphereMachineTemplateWebhook) ValidateDelete(_ context.Context, _ runtime.Object) (admission.Warnings, error) {
 	return nil, nil
 }
diff --git a/apis/v1beta1/vspherevm_webhook.go b/internal/webhooks/vspherevm.go
similarity index 62%
rename from apis/v1beta1/vspherevm_webhook.go
rename to internal/webhooks/vspherevm.go
index 11c11050..47585516 100644
--- a/apis/v1beta1/vspherevm_webhook.go
+++ b/internal/webhooks/vspherevm.go
@@ -14,9 +14,10 @@ See the License for the specific language governing permissions and
 limitations under the License.
 */
 
-package v1beta1
+package webhooks
 
 import (
+	"context"
 	"fmt"
 	"net"
 	"reflect"
@@ -26,30 +27,50 @@ import (
 	"k8s.io/apimachinery/pkg/runtime"
 	"k8s.io/apimachinery/pkg/util/validation/field"
 	ctrl "sigs.k8s.io/controller-runtime"
+	"sigs.k8s.io/controller-runtime/pkg/webhook"
 	"sigs.k8s.io/controller-runtime/pkg/webhook/admission"
+
+	infrav1 "sigs.k8s.io/cluster-api-provider-vsphere/apis/v1beta1"
 )
 
-func (r *VSphereVM) SetupWebhookWithManager(mgr ctrl.Manager) error {
+// +kubebuilder:webhook:verbs=create;update,path=/validate-infrastructure-cluster-x-k8s-io-v1beta1-vspherevm,mutating=false,failurePolicy=fail,matchPolicy=Equivalent,groups=infrastructure.cluster.x-k8s.io,resources=vspherevms,versions=v1beta1,name=validation.vspherevm.infrastructure.x-k8s.io,sideEffects=None,admissionReviewVersions=v1beta1
+// +kubebuilder:webhook:verbs=create;update,path=/mutate-infrastructure-cluster-x-k8s-io-v1beta1-vspherevm,mutating=true,failurePolicy=fail,matchPolicy=Equivalent,groups=infrastructure.cluster.x-k8s.io,resources=vspherevms,versions=v1beta1,name=default.vspherevm.infrastructure.x-k8s.io,sideEffects=None,admissionReviewVersions=v1beta1
+
+// VSphereVMWebhook implements a validation and defaulting webhook for VSphereVM.
+type VSphereVMWebhook struct{}
+
+var _ webhook.CustomValidator = &VSphereVMWebhook{}
+var _ webhook.CustomDefaulter = &VSphereVMWebhook{}
+
+func (webhook *VSphereVMWebhook) SetupWebhookWithManager(mgr ctrl.Manager) error {
 	return ctrl.NewWebhookManagedBy(mgr).
-		For(r).
+		For(&infrav1.VSphereVM{}).
+		WithValidator(webhook).
+		WithDefaulter(webhook).
 		Complete()
 }
 
-// +kubebuilder:webhook:verbs=create;update,path=/validate-infrastructure-cluster-x-k8s-io-v1beta1-vspherevm,mutating=false,failurePolicy=fail,matchPolicy=Equivalent,groups=infrastructure.cluster.x-k8s.io,resources=vspherevms,versions=v1beta1,name=validation.vspherevm.infrastructure.x-k8s.io,sideEffects=None,admissionReviewVersions=v1beta1
-// +kubebuilder:webhook:verbs=create;update,path=/mutate-infrastructure-cluster-x-k8s-io-v1beta1-vspherevm,mutating=true,failurePolicy=fail,matchPolicy=Equivalent,groups=infrastructure.cluster.x-k8s.io,resources=vspherevms,versions=v1beta1,name=default.vspherevm.infrastructure.x-k8s.io,sideEffects=None,admissionReviewVersions=v1beta1
-
 // Default implements webhook.Defaulter so a webhook will be registered for the type.
-func (r *VSphereVM) Default() {
+func (webhook *VSphereVMWebhook) Default(_ context.Context, obj runtime.Object) error {
+	typedObj, ok := obj.(*infrav1.VSphereVM)
+	if !ok {
+		return apierrors.NewBadRequest(fmt.Sprintf("expected a VSphereVM but got a %T", obj))
+	}
 	// Set Linux as default OS value
-	if r.Spec.OS == "" {
-		r.Spec.OS = Linux
+	if typedObj.Spec.OS == "" {
+		typedObj.Spec.OS = infrav1.Linux
 	}
+	return nil
 }
 
 // ValidateCreate implements webhook.Validator so a webhook will be registered for the type.
-func (r *VSphereVM) ValidateCreate() (admission.Warnings, error) {
+func (webhook *VSphereVMWebhook) ValidateCreate(_ context.Context, raw runtime.Object) (admission.Warnings, error) {
 	var allErrs field.ErrorList
-	spec := r.Spec
+	objValue, ok := raw.(*infrav1.VSphereVM)
+	if !ok {
+		return nil, apierrors.NewBadRequest(fmt.Sprintf("expected a VSphereVM but got a %T", raw))
+	}
+	spec := objValue.Spec
 
 	if spec.Network.PreferredAPIServerCIDR != "" {
 		allErrs = append(allErrs, field.Invalid(field.NewPath("spec", "PreferredAPIServerCIDR"), spec.Network.PreferredAPIServerCIDR, "cannot be set, as it will be removed and is no longer used"))
@@ -63,46 +84,50 @@ func (r *VSphereVM) ValidateCreate() (admission.Warnings, error) {
 		}
 	}
 
-	if r.Spec.OS == Windows && len(r.Name) > 15 {
-		allErrs = append(allErrs, field.Invalid(field.NewPath("name"), r.Name, "name has to be less than 16 characters for Windows VM"))
+	if objValue.Spec.OS == infrav1.Windows && len(objValue.Name) > 15 {
+		allErrs = append(allErrs, field.Invalid(field.NewPath("name"), objValue.Name, "name has to be less than 16 characters for Windows VM"))
 	}
 	if spec.GuestSoftPowerOffTimeout != nil {
-		if spec.PowerOffMode != VirtualMachinePowerOpModeTrySoft {
+		if spec.PowerOffMode != infrav1.VirtualMachinePowerOpModeTrySoft {
 			allErrs = append(allErrs, field.Invalid(field.NewPath("spec", "guestSoftPowerOffTimeout"), spec.GuestSoftPowerOffTimeout, "should not be set in templates unless the powerOffMode is trySoft"))
 		}
 		if spec.GuestSoftPowerOffTimeout.Duration <= 0 {
 			allErrs = append(allErrs, field.Invalid(field.NewPath("spec", "guestSoftPowerOffTimeout"), spec.GuestSoftPowerOffTimeout, "should be greater than 0"))
 		}
 	}
-	return nil, aggregateObjErrors(r.GroupVersionKind().GroupKind(), r.Name, allErrs)
+	return nil, aggregateObjErrors(objValue.GroupVersionKind().GroupKind(), objValue.Name, allErrs)
 }
 
 // ValidateUpdate implements webhook.Validator so a webhook will be registered for the type.
-func (r *VSphereVM) ValidateUpdate(old runtime.Object) (admission.Warnings, error) {
+func (webhook *VSphereVMWebhook) ValidateUpdate(_ context.Context, oldRaw runtime.Object, newRaw runtime.Object) (admission.Warnings, error) {
 	var allErrs field.ErrorList
-	if r.Spec.GuestSoftPowerOffTimeout != nil {
-		if r.Spec.PowerOffMode != VirtualMachinePowerOpModeTrySoft {
-			allErrs = append(allErrs, field.Invalid(field.NewPath("spec", "guestSoftPowerOffTimeout"), r.Spec.GuestSoftPowerOffTimeout, "should not be set in templates unless the powerOffMode is trySoft"))
+
+	oldTyped, ok := oldRaw.(*infrav1.VSphereVM)
+	if !ok {
+		return nil, apierrors.NewBadRequest(fmt.Sprintf("expected a VSphereVM but got a %T", oldRaw))
+	}
+	newTyped, ok := newRaw.(*infrav1.VSphereVM)
+	if !ok {
+		return nil, apierrors.NewBadRequest(fmt.Sprintf("expected a VSphereVM but got a %T", newRaw))
+	}
+	if newTyped.Spec.GuestSoftPowerOffTimeout != nil {
+		if newTyped.Spec.PowerOffMode != infrav1.VirtualMachinePowerOpModeTrySoft {
+			allErrs = append(allErrs, field.Invalid(field.NewPath("spec", "guestSoftPowerOffTimeout"), newTyped.Spec.GuestSoftPowerOffTimeout, "should not be set in templates unless the powerOffMode is trySoft"))
 		}
-		if r.Spec.GuestSoftPowerOffTimeout.Duration <= 0 {
-			allErrs = append(allErrs, field.Invalid(field.NewPath("spec", "guestSoftPowerOffTimeout"), r.Spec.GuestSoftPowerOffTimeout, "should be greater than 0"))
+		if newTyped.Spec.GuestSoftPowerOffTimeout.Duration <= 0 {
+			allErrs = append(allErrs, field.Invalid(field.NewPath("spec", "guestSoftPowerOffTimeout"), newTyped.Spec.GuestSoftPowerOffTimeout, "should be greater than 0"))
 		}
 	}
 
-	newVSphereVM, err := runtime.DefaultUnstructuredConverter.ToUnstructured(r)
+	newVSphereVM, err := runtime.DefaultUnstructuredConverter.ToUnstructured(newTyped)
 	if err != nil {
 		return nil, apierrors.NewInternalError(errors.Wrap(err, "failed to convert new VSphereVM to unstructured object"))
 	}
-	oldVSphereVM, err := runtime.DefaultUnstructuredConverter.ToUnstructured(old)
+	oldVSphereVM, err := runtime.DefaultUnstructuredConverter.ToUnstructured(oldTyped)
 	if err != nil {
 		return nil, apierrors.NewInternalError(errors.Wrap(err, "failed to convert old VSphereVM to unstructured object"))
 	}
 
-	oldTyped, ok := old.(*VSphereVM)
-	if !ok {
-		return nil, apierrors.NewInternalError(fmt.Errorf("failed to typecast old runtime object to VSphereVM"))
-	}
-
 	newVSphereVMSpec := newVSphereVM["spec"].(map[string]interface{})
 	oldVSphereVMSpec := oldVSphereVM["spec"].(map[string]interface{})
 
@@ -116,30 +141,30 @@ func (r *VSphereVM) ValidateUpdate(old runtime.Object) (admission.Warnings, erro
 	if oldTyped.Spec.BiosUUID == "" {
 		keys = append(keys, "biosUUID")
 	}
-	r.deleteSpecKeys(oldVSphereVMSpec, keys)
-	r.deleteSpecKeys(newVSphereVMSpec, keys)
+	webhook.deleteSpecKeys(oldVSphereVMSpec, keys)
+	webhook.deleteSpecKeys(newVSphereVMSpec, keys)
 
 	newVSphereVMNetwork := newVSphereVMSpec["network"].(map[string]interface{})
 	oldVSphereVMNetwork := oldVSphereVMSpec["network"].(map[string]interface{})
 
 	// allow changes to the network devices
 	networkKeys := []string{"devices"}
-	r.deleteSpecKeys(oldVSphereVMNetwork, networkKeys)
-	r.deleteSpecKeys(newVSphereVMNetwork, networkKeys)
+	webhook.deleteSpecKeys(oldVSphereVMNetwork, networkKeys)
+	webhook.deleteSpecKeys(newVSphereVMNetwork, networkKeys)
 
 	if !reflect.DeepEqual(oldVSphereVMSpec, newVSphereVMSpec) {
 		allErrs = append(allErrs, field.Forbidden(field.NewPath("spec"), "cannot be modified"))
 	}
 
-	return nil, aggregateObjErrors(r.GroupVersionKind().GroupKind(), r.Name, allErrs)
+	return nil, aggregateObjErrors(newTyped.GroupVersionKind().GroupKind(), newTyped.Name, allErrs)
 }
 
 // ValidateDelete implements webhook.Validator so a webhook will be registered for the type.
-func (r *VSphereVM) ValidateDelete() (admission.Warnings, error) {
+func (webhook *VSphereVMWebhook) ValidateDelete(_ context.Context, _ runtime.Object) (admission.Warnings, error) {
 	return nil, nil
 }
 
-func (r *VSphereVM) deleteSpecKeys(spec map[string]interface{}, keys []string) {
+func (webhook *VSphereVMWebhook) deleteSpecKeys(spec map[string]interface{}, keys []string) {
 	if len(spec) == 0 || len(keys) == 0 {
 		return
 	}
diff --git a/main.go b/main.go
index 5e6a02fc..d7cd3387 100644
--- a/main.go
+++ b/main.go
@@ -50,6 +50,7 @@ import (
 	vmwarev1 "sigs.k8s.io/cluster-api-provider-vsphere/apis/vmware/v1beta1"
 	"sigs.k8s.io/cluster-api-provider-vsphere/controllers"
 	"sigs.k8s.io/cluster-api-provider-vsphere/feature"
+	"sigs.k8s.io/cluster-api-provider-vsphere/internal/webhooks"
 	"sigs.k8s.io/cluster-api-provider-vsphere/pkg/constants"
 	"sigs.k8s.io/cluster-api-provider-vsphere/pkg/context"
 	"sigs.k8s.io/cluster-api-provider-vsphere/pkg/manager"
@@ -330,27 +331,27 @@ func main() {
 }
 
 func setupVAPIControllers(ctx *context.ControllerManagerContext, mgr ctrlmgr.Manager, tracker *remote.ClusterCacheTracker) error {
-	if err := (&infrav1.VSphereClusterTemplate{}).SetupWebhookWithManager(mgr); err != nil {
+	if err := (&webhooks.VSphereClusterTemplateWebhook{}).SetupWebhookWithManager(mgr); err != nil {
 		return err
 	}
 
-	if err := (&infrav1.VSphereMachine{}).SetupWebhookWithManager(mgr); err != nil {
+	if err := (&webhooks.VSphereMachineWebhook{}).SetupWebhookWithManager(mgr); err != nil {
 		return err
 	}
 
-	if err := (&infrav1.VSphereMachineTemplateWebhook{}).SetupWebhookWithManager(mgr); err != nil {
+	if err := (&webhooks.VSphereMachineTemplateWebhook{}).SetupWebhookWithManager(mgr); err != nil {
 		return err
 	}
 
-	if err := (&infrav1.VSphereVM{}).SetupWebhookWithManager(mgr); err != nil {
+	if err := (&webhooks.VSphereVMWebhook{}).SetupWebhookWithManager(mgr); err != nil {
 		return err
 	}
 
-	if err := (&infrav1.VSphereDeploymentZone{}).SetupWebhookWithManager(mgr); err != nil {
+	if err := (&webhooks.VSphereDeploymentZoneWebhook{}).SetupWebhookWithManager(mgr); err != nil {
 		return err
 	}
 
-	if err := (&infrav1.VSphereFailureDomain{}).SetupWebhookWithManager(mgr); err != nil {
+	if err := (&webhooks.VSphereFailureDomainWebhook{}).SetupWebhookWithManager(mgr); err != nil {
 		return err
 	}
 


Test patch:
diff --git a/apis/v1beta1/vspheredeploymentzone_webhook_test.go b/internal/webhooks/vspheredeploymentzone_test.go
similarity index 81%
rename from apis/v1beta1/vspheredeploymentzone_webhook_test.go
rename to internal/webhooks/vspheredeploymentzone_test.go
index ebcd2b65..2980b44c 100644
--- a/apis/v1beta1/vspheredeploymentzone_webhook_test.go
+++ b/internal/webhooks/vspheredeploymentzone_test.go
@@ -14,13 +14,16 @@ See the License for the specific language governing permissions and
 limitations under the License.
 */
 
-package v1beta1
+package webhooks
 
 import (
+	"context"
 	"testing"
 
 	. "github.com/onsi/gomega"
 	"k8s.io/utils/pointer"
+
+	infrav1 "sigs.k8s.io/cluster-api-provider-vsphere/apis/v1beta1"
 )
 
 func TestVSphereDeploymentZone_Default(t *testing.T) {
@@ -47,12 +50,13 @@ func TestVSphereDeploymentZone_Default(t *testing.T) {
 		// Need to reinit the test variable
 		tt := tt
 		t.Run(tt.name, func(t *testing.T) {
-			vdz := VSphereDeploymentZone{
-				Spec: VSphereDeploymentZoneSpec{
+			vdz := infrav1.VSphereDeploymentZone{
+				Spec: infrav1.VSphereDeploymentZoneSpec{
 					ControlPlane: tt.boolPtr,
 				},
 			}
-			vdz.Default()
+			webhook := VSphereDeploymentZoneWebhook{}
+			g.Expect(webhook.Default(context.Background(), &vdz)).NotTo(HaveOccurred())
 			g.Expect(vdz.Spec.ControlPlane).NotTo(BeNil())
 			g.Expect(*vdz.Spec.ControlPlane).To(Equal(tt.expectedVal))
 		})
diff --git a/apis/v1beta1/vspherefailuredomain_webhook_test.go b/internal/webhooks/vspherefailuredomain_test.go
similarity index 57%
rename from apis/v1beta1/vspherefailuredomain_webhook_test.go
rename to internal/webhooks/vspherefailuredomain_test.go
index b34b153a..fa08f1fe 100644
--- a/apis/v1beta1/vspherefailuredomain_webhook_test.go
+++ b/internal/webhooks/vspherefailuredomain_test.go
@@ -14,24 +14,28 @@ See the License for the specific language governing permissions and
 limitations under the License.
 */
 
-package v1beta1
+package webhooks
 
 import (
+	"context"
 	"testing"
 
 	. "github.com/onsi/gomega"
 	"k8s.io/utils/pointer"
+
+	infrav1 "sigs.k8s.io/cluster-api-provider-vsphere/apis/v1beta1"
 )
 
 func TestVsphereFailureDomain_Default(t *testing.T) {
 	g := NewWithT(t)
-	m := &VSphereFailureDomain{
-		Spec: VSphereFailureDomainSpec{},
+	m := &infrav1.VSphereFailureDomain{
+		Spec: infrav1.VSphereFailureDomainSpec{},
 	}
-	m.Default()
+	webhook := &VSphereFailureDomainWebhook{}
+	g.Expect(webhook.Default(context.Background(), m)).ToNot(HaveOccurred())
 
-	g.Expect(*m.Spec.Zone.AutoConfigure).To(BeFalse())
-	g.Expect(*m.Spec.Region.AutoConfigure).To(BeFalse())
+	g.Expect(*m.Spec.Zone.AutoConfigure).To(BeFalse())   //nolint:staticcheck
+	g.Expect(*m.Spec.Region.AutoConfigure).To(BeFalse()) //nolint:staticcheck
 }
 
 func TestVSphereFailureDomain_ValidateCreate(t *testing.T) {
@@ -40,14 +44,14 @@ func TestVSphereFailureDomain_ValidateCreate(t *testing.T) {
 	tests := []struct {
 		name          string
 		errExpected   *bool
-		failureDomain VSphereFailureDomain
+		failureDomain infrav1.VSphereFailureDomain
 	}{
 		{
 			name: "region failureDomain type is hostGroup",
-			failureDomain: VSphereFailureDomain{Spec: VSphereFailureDomainSpec{
-				Region: FailureDomain{
+			failureDomain: infrav1.VSphereFailureDomain{Spec: infrav1.VSphereFailureDomainSpec{
+				Region: infrav1.FailureDomain{
 					Name:          "foo",
-					Type:          HostGroupFailureDomain,
+					Type:          infrav1.HostGroupFailureDomain,
 					TagCategory:   "k8s-bar",
 					AutoConfigure: pointer.Bool(true),
 				},
@@ -55,11 +59,11 @@ func TestVSphereFailureDomain_ValidateCreate(t *testing.T) {
 		},
 		{
 			name: "hostGroup failureDomain set but compute Cluster is empty",
-			failureDomain: VSphereFailureDomain{Spec: VSphereFailureDomainSpec{
-				Topology: Topology{
+			failureDomain: infrav1.VSphereFailureDomain{Spec: infrav1.VSphereFailureDomainSpec{
+				Topology: infrav1.Topology{
 					Datacenter:     "/blah",
 					ComputeCluster: nil,
-					Hosts: &FailureDomainHosts{
+					Hosts: &infrav1.FailureDomainHosts{
 						VMGroupName:   "vm-foo",
 						HostGroupName: "host-foo",
 					},
@@ -68,18 +72,18 @@ func TestVSphereFailureDomain_ValidateCreate(t *testing.T) {
 		},
 		{
 			name: "type of zone failure domain is Hostgroup but topology's hostgroup is not set",
-			failureDomain: VSphereFailureDomain{Spec: VSphereFailureDomainSpec{
-				Region: FailureDomain{
+			failureDomain: infrav1.VSphereFailureDomain{Spec: infrav1.VSphereFailureDomainSpec{
+				Region: infrav1.FailureDomain{
 					Name:        "foo",
-					Type:        ComputeClusterFailureDomain,
+					Type:        infrav1.ComputeClusterFailureDomain,
 					TagCategory: "k8s-bar",
 				},
-				Zone: FailureDomain{
+				Zone: infrav1.FailureDomain{
 					Name:        "foo",
-					Type:        HostGroupFailureDomain,
+					Type:        infrav1.HostGroupFailureDomain,
 					TagCategory: "k8s-bar",
 				},
-				Topology: Topology{
+				Topology: infrav1.Topology{
 					Datacenter:     "/blah",
 					ComputeCluster: pointer.String("blah2"),
 					Hosts:          nil,
@@ -88,21 +92,21 @@ func TestVSphereFailureDomain_ValidateCreate(t *testing.T) {
 		},
 		{
 			name: "type of region failure domain is Compute Cluster but topology is not set",
-			failureDomain: VSphereFailureDomain{Spec: VSphereFailureDomainSpec{
-				Region: FailureDomain{
+			failureDomain: infrav1.VSphereFailureDomain{Spec: infrav1.VSphereFailureDomainSpec{
+				Region: infrav1.FailureDomain{
 					Name:        "foo",
-					Type:        ComputeClusterFailureDomain,
+					Type:        infrav1.ComputeClusterFailureDomain,
 					TagCategory: "k8s-bar",
 				},
-				Zone: FailureDomain{
+				Zone: infrav1.FailureDomain{
 					Name:        "foo",
-					Type:        HostGroupFailureDomain,
+					Type:        infrav1.HostGroupFailureDomain,
 					TagCategory: "k8s-bar",
 				},
-				Topology: Topology{
+				Topology: infrav1.Topology{
 					Datacenter:     "/blah",
 					ComputeCluster: nil,
-					Hosts: &FailureDomainHosts{
+					Hosts: &infrav1.FailureDomainHosts{
 						VMGroupName:   "vm-foo",
 						HostGroupName: "host-foo",
 					},
@@ -111,18 +115,18 @@ func TestVSphereFailureDomain_ValidateCreate(t *testing.T) {
 		},
 		{
 			name: "type of zone failure domain is Compute Cluster but topology is not set",
-			failureDomain: VSphereFailureDomain{Spec: VSphereFailureDomainSpec{
-				Region: FailureDomain{
+			failureDomain: infrav1.VSphereFailureDomain{Spec: infrav1.VSphereFailureDomainSpec{
+				Region: infrav1.FailureDomain{
 					Name:        "foo",
-					Type:        DatacenterFailureDomain,
+					Type:        infrav1.DatacenterFailureDomain,
 					TagCategory: "k8s-bar",
 				},
-				Zone: FailureDomain{
+				Zone: infrav1.FailureDomain{
 					Name:        "foo",
-					Type:        ComputeClusterFailureDomain,
+					Type:        infrav1.ComputeClusterFailureDomain,
 					TagCategory: "k8s-bar",
 				},
-				Topology: Topology{
+				Topology: infrav1.Topology{
 					Datacenter:     "/blah",
 					ComputeCluster: nil,
 				},
@@ -134,7 +138,8 @@ func TestVSphereFailureDomain_ValidateCreate(t *testing.T) {
 		// Need to reinit the test variable
 		tt := tt
 		t.Run(tt.name, func(t *testing.T) {
-			_, err := tt.failureDomain.ValidateCreate()
+			webhook := &VSphereFailureDomainWebhook{}
+			_, err := webhook.ValidateCreate(context.Background(), &tt.failureDomain)
 			if tt.errExpected == nil || !*tt.errExpected {
 				g.Expect(err).To(HaveOccurred())
 			} else {
diff --git a/apis/v1beta1/vspheremachine_webhook_test.go b/internal/webhooks/vspheremachine_test.go
similarity index 60%
rename from apis/v1beta1/vspheremachine_webhook_test.go
rename to internal/webhooks/vspheremachine_test.go
index 0d288c55..46c37ae0 100644
--- a/apis/v1beta1/vspheremachine_webhook_test.go
+++ b/internal/webhooks/vspheremachine_test.go
@@ -14,23 +14,27 @@ See the License for the specific language governing permissions and
 limitations under the License.
 */
 
-package v1beta1
+package webhooks
 
 import (
+	"context"
 	"testing"
 
 	. "github.com/onsi/gomega"
 	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
+
+	infrav1 "sigs.k8s.io/cluster-api-provider-vsphere/apis/v1beta1"
 )
 
 var someProviderID = "vsphere://42305f0b-dad7-1d3d-5727-0eaffffffffc"
 
 func TestVsphereMachine_Default(t *testing.T) {
 	g := NewWithT(t)
-	m := &VSphereMachine{
-		Spec: VSphereMachineSpec{},
+	m := &infrav1.VSphereMachine{
+		Spec: infrav1.VSphereMachineSpec{},
 	}
-	m.Default()
+	webhook := &VSphereMachineWebhook{}
+	g.Expect(webhook.Default(context.Background(), m)).ToNot(HaveOccurred())
 
 	g.Expect(m.Spec.Datacenter).To(Equal("*"))
 }
@@ -39,63 +43,64 @@ func TestVSphereMachine_ValidateCreate(t *testing.T) {
 	g := NewWithT(t)
 	tests := []struct {
 		name           string
-		vsphereMachine *VSphereMachine
+		vsphereMachine *infrav1.VSphereMachine
 		wantErr        bool
 	}{
 		{
 			name:           "preferredAPIServerCIDR set on creation ",
-			vsphereMachine: createVSphereMachine("foo.com", nil, "192.168.0.1/32", []string{}, VirtualMachinePowerOpModeTrySoft, nil),
+			vsphereMachine: createVSphereMachine("foo.com", nil, "192.168.0.1/32", []string{}, infrav1.VirtualMachinePowerOpModeTrySoft, nil),
 			wantErr:        true,
 		},
 		{
 			name:           "IPs are not in CIDR format",
-			vsphereMachine: createVSphereMachine("foo.com", nil, "", []string{"192.168.0.1/32", "192.168.0.3"}, VirtualMachinePowerOpModeTrySoft, nil),
+			vsphereMachine: createVSphereMachine("foo.com", nil, "", []string{"192.168.0.1/32", "192.168.0.3"}, infrav1.VirtualMachinePowerOpModeTrySoft, nil),
 			wantErr:        true,
 		},
 		{
 			name:           "IPs are not valid IPs in CIDR format",
-			vsphereMachine: createVSphereMachine("foo.com", nil, "", []string{"<nil>/32", "192.168.0.644/33"}, VirtualMachinePowerOpModeTrySoft, nil),
+			vsphereMachine: createVSphereMachine("foo.com", nil, "", []string{"<nil>/32", "192.168.0.644/33"}, infrav1.VirtualMachinePowerOpModeTrySoft, nil),
 			wantErr:        true,
 		},
 		{
 			name:           "guestSoftPowerOffTimeout should not be set with powerOffMode set to hard",
-			vsphereMachine: createVSphereMachine("foo.com", nil, "", []string{"192.168.0.1/32", "192.168.0.3/32"}, VirtualMachinePowerOpModeHard, &metav1.Duration{Duration: GuestSoftPowerOffDefaultTimeout}),
+			vsphereMachine: createVSphereMachine("foo.com", nil, "", []string{"192.168.0.1/32", "192.168.0.3/32"}, infrav1.VirtualMachinePowerOpModeHard, &metav1.Duration{Duration: infrav1.GuestSoftPowerOffDefaultTimeout}),
 			wantErr:        true,
 		},
 		{
 			name:           "guestSoftPowerOffTimeout should not be set with powerOffMode set to soft",
-			vsphereMachine: createVSphereMachine("foo.com", nil, "", []string{"192.168.0.1/32", "192.168.0.3/32"}, VirtualMachinePowerOpModeSoft, &metav1.Duration{Duration: GuestSoftPowerOffDefaultTimeout}),
+			vsphereMachine: createVSphereMachine("foo.com", nil, "", []string{"192.168.0.1/32", "192.168.0.3/32"}, infrav1.VirtualMachinePowerOpModeSoft, &metav1.Duration{Duration: infrav1.GuestSoftPowerOffDefaultTimeout}),
 			wantErr:        true,
 		},
 		{
 			name:           "guestSoftPowerOffTimeout should not be negative",
-			vsphereMachine: createVSphereMachine("foo.com", nil, "", []string{"192.168.0.1/32", "192.168.0.3/32"}, VirtualMachinePowerOpModeHard, &metav1.Duration{Duration: -1234}),
+			vsphereMachine: createVSphereMachine("foo.com", nil, "", []string{"192.168.0.1/32", "192.168.0.3/32"}, infrav1.VirtualMachinePowerOpModeHard, &metav1.Duration{Duration: -1234}),
 			wantErr:        true,
 		},
 		{
 			name:           "successful VSphereMachine creation",
-			vsphereMachine: createVSphereMachine("foo.com", nil, "", []string{"192.168.0.1/32", "192.168.0.3/32"}, VirtualMachinePowerOpModeTrySoft, nil),
+			vsphereMachine: createVSphereMachine("foo.com", nil, "", []string{"192.168.0.1/32", "192.168.0.3/32"}, infrav1.VirtualMachinePowerOpModeTrySoft, nil),
 			wantErr:        false,
 		},
 		{
 			name:           "successful VSphereMachine creation with powerOffMode set to hard",
-			vsphereMachine: createVSphereMachine("foo.com", nil, "", []string{"192.168.0.1/32", "192.168.0.3/32"}, VirtualMachinePowerOpModeHard, nil),
+			vsphereMachine: createVSphereMachine("foo.com", nil, "", []string{"192.168.0.1/32", "192.168.0.3/32"}, infrav1.VirtualMachinePowerOpModeHard, nil),
 			wantErr:        false,
 		},
 		{
 			name:           "successful VSphereMachine creation with powerOffMode set to soft",
-			vsphereMachine: createVSphereMachine("foo.com", nil, "", []string{"192.168.0.1/32", "192.168.0.3/32"}, VirtualMachinePowerOpModeSoft, nil),
+			vsphereMachine: createVSphereMachine("foo.com", nil, "", []string{"192.168.0.1/32", "192.168.0.3/32"}, infrav1.VirtualMachinePowerOpModeSoft, nil),
 			wantErr:        false,
 		},
 		{
 			name:           "successful VSphereMachine creation with powerOffMode set to trySoft and non-default timeout",
-			vsphereMachine: createVSphereMachine("foo.com", nil, "", []string{"192.168.0.1/32", "192.168.0.3/32"}, VirtualMachinePowerOpModeTrySoft, &metav1.Duration{Duration: 1234}),
+			vsphereMachine: createVSphereMachine("foo.com", nil, "", []string{"192.168.0.1/32", "192.168.0.3/32"}, infrav1.VirtualMachinePowerOpModeTrySoft, &metav1.Duration{Duration: 1234}),
 			wantErr:        false,
 		},
 	}
 	for _, tc := range tests {
 		t.Run(tc.name, func(t *testing.T) {
-			_, err := tc.vsphereMachine.ValidateCreate()
+			webhook := &VSphereMachineWebhook{}
+			_, err := webhook.ValidateCreate(context.Background(), tc.vsphereMachine)
 			if tc.wantErr {
 				g.Expect(err).To(HaveOccurred())
 			} else {
@@ -110,62 +115,63 @@ func TestVSphereMachine_ValidateUpdate(t *testing.T) {
 
 	tests := []struct {
 		name              string
-		oldVSphereMachine *VSphereMachine
-		vsphereMachine    *VSphereMachine
+		oldVSphereMachine *infrav1.VSphereMachine
+		vsphereMachine    *infrav1.VSphereMachine
 		wantErr           bool
 	}{
 		{
 			name:              "ProviderID can be updated",
-			oldVSphereMachine: createVSphereMachine("foo.com", nil, "", []string{"192.168.0.1/32"}, VirtualMachinePowerOpModeSoft, nil),
-			vsphereMachine:    createVSphereMachine("foo.com", &someProviderID, "", []string{"192.168.0.1/32"}, VirtualMachinePowerOpModeSoft, nil),
+			oldVSphereMachine: createVSphereMachine("foo.com", nil, "", []string{"192.168.0.1/32"}, infrav1.VirtualMachinePowerOpModeSoft, nil),
+			vsphereMachine:    createVSphereMachine("foo.com", &someProviderID, "", []string{"192.168.0.1/32"}, infrav1.VirtualMachinePowerOpModeSoft, nil),
 			wantErr:           false,
 		},
 		{
 			name:              "updating ips can be done",
-			oldVSphereMachine: createVSphereMachine("foo.com", nil, "", []string{"192.168.0.1/32"}, VirtualMachinePowerOpModeSoft, nil),
-			vsphereMachine:    createVSphereMachine("foo.com", &someProviderID, "", []string{"192.168.0.1/32", "192.168.0.10/32"}, VirtualMachinePowerOpModeSoft, nil),
+			oldVSphereMachine: createVSphereMachine("foo.com", nil, "", []string{"192.168.0.1/32"}, infrav1.VirtualMachinePowerOpModeSoft, nil),
+			vsphereMachine:    createVSphereMachine("foo.com", &someProviderID, "", []string{"192.168.0.1/32", "192.168.0.10/32"}, infrav1.VirtualMachinePowerOpModeSoft, nil),
 			wantErr:           false,
 		},
 		{
 			name:              "updating non-existing IP with invalid ips can not be done",
-			oldVSphereMachine: createVSphereMachine("foo.com", nil, "", nil, VirtualMachinePowerOpModeSoft, nil),
-			vsphereMachine:    createVSphereMachine("foo.com", &someProviderID, "", []string{"<nil>/32", "192.168.0.10/33"}, VirtualMachinePowerOpModeSoft, nil),
+			oldVSphereMachine: createVSphereMachine("foo.com", nil, "", nil, infrav1.VirtualMachinePowerOpModeSoft, nil),
+			vsphereMachine:    createVSphereMachine("foo.com", &someProviderID, "", []string{"<nil>/32", "192.168.0.10/33"}, infrav1.VirtualMachinePowerOpModeSoft, nil),
 			wantErr:           true,
 		},
 		{
 			name:              "updating existing IP with invalid ips can not be done",
-			oldVSphereMachine: createVSphereMachine("foo.com", nil, "", []string{"192.168.0.1/32"}, VirtualMachinePowerOpModeSoft, nil),
-			vsphereMachine:    createVSphereMachine("foo.com", &someProviderID, "", []string{"<nil>/32", "192.168.0.10/33"}, VirtualMachinePowerOpModeSoft, nil),
+			oldVSphereMachine: createVSphereMachine("foo.com", nil, "", []string{"192.168.0.1/32"}, infrav1.VirtualMachinePowerOpModeSoft, nil),
+			vsphereMachine:    createVSphereMachine("foo.com", &someProviderID, "", []string{"<nil>/32", "192.168.0.10/33"}, infrav1.VirtualMachinePowerOpModeSoft, nil),
 			wantErr:           true,
 		},
 		{
 			name:              "updating server cannot be done",
-			oldVSphereMachine: createVSphereMachine("foo.com", nil, "", []string{"192.168.0.1/32"}, VirtualMachinePowerOpModeSoft, nil),
-			vsphereMachine:    createVSphereMachine("bar.com", &someProviderID, "", []string{"192.168.0.1/32", "192.168.0.10/32"}, VirtualMachinePowerOpModeSoft, nil),
+			oldVSphereMachine: createVSphereMachine("foo.com", nil, "", []string{"192.168.0.1/32"}, infrav1.VirtualMachinePowerOpModeSoft, nil),
+			vsphereMachine:    createVSphereMachine("bar.com", &someProviderID, "", []string{"192.168.0.1/32", "192.168.0.10/32"}, infrav1.VirtualMachinePowerOpModeSoft, nil),
 			wantErr:           true,
 		},
 		{
 			name:              "powerOffMode cannot be updated when new powerOffMode is not valid",
-			oldVSphereMachine: createVSphereMachine("foo.com", &someProviderID, "", []string{"192.168.0.1/32"}, VirtualMachinePowerOpModeTrySoft, nil),
-			vsphereMachine:    createVSphereMachine("foo.com", &someProviderID, "", []string{"192.168.0.1/32"}, VirtualMachinePowerOpModeHard, &metav1.Duration{Duration: GuestSoftPowerOffDefaultTimeout}),
+			oldVSphereMachine: createVSphereMachine("foo.com", &someProviderID, "", []string{"192.168.0.1/32"}, infrav1.VirtualMachinePowerOpModeTrySoft, nil),
+			vsphereMachine:    createVSphereMachine("foo.com", &someProviderID, "", []string{"192.168.0.1/32"}, infrav1.VirtualMachinePowerOpModeHard, &metav1.Duration{Duration: infrav1.GuestSoftPowerOffDefaultTimeout}),
 			wantErr:           true,
 		},
 		{
 			name:              "powerOffMode can be updated to hard",
-			oldVSphereMachine: createVSphereMachine("foo.com", &someProviderID, "", []string{"192.168.0.1/32"}, VirtualMachinePowerOpModeTrySoft, &metav1.Duration{Duration: GuestSoftPowerOffDefaultTimeout}),
-			vsphereMachine:    createVSphereMachine("foo.com", &someProviderID, "", []string{"192.168.0.1/32"}, VirtualMachinePowerOpModeHard, nil),
+			oldVSphereMachine: createVSphereMachine("foo.com", &someProviderID, "", []string{"192.168.0.1/32"}, infrav1.VirtualMachinePowerOpModeTrySoft, &metav1.Duration{Duration: infrav1.GuestSoftPowerOffDefaultTimeout}),
+			vsphereMachine:    createVSphereMachine("foo.com", &someProviderID, "", []string{"192.168.0.1/32"}, infrav1.VirtualMachinePowerOpModeHard, nil),
 			wantErr:           false,
 		},
 		{
 			name:              "powerOffMode can be updated to soft",
-			oldVSphereMachine: createVSphereMachine("foo.com", &someProviderID, "", []string{"192.168.0.1/32"}, VirtualMachinePowerOpModeTrySoft, &metav1.Duration{Duration: GuestSoftPowerOffDefaultTimeout}),
-			vsphereMachine:    createVSphereMachine("foo.com", &someProviderID, "", []string{"192.168.0.1/32"}, VirtualMachinePowerOpModeSoft, nil),
+			oldVSphereMachine: createVSphereMachine("foo.com", &someProviderID, "", []string{"192.168.0.1/32"}, infrav1.VirtualMachinePowerOpModeTrySoft, &metav1.Duration{Duration: infrav1.GuestSoftPowerOffDefaultTimeout}),
+			vsphereMachine:    createVSphereMachine("foo.com", &someProviderID, "", []string{"192.168.0.1/32"}, infrav1.VirtualMachinePowerOpModeSoft, nil),
 			wantErr:           false,
 		},
 	}
 	for _, tc := range tests {
 		t.Run(tc.name, func(t *testing.T) {
-			_, err := tc.vsphereMachine.ValidateUpdate(tc.oldVSphereMachine)
+			webhook := &VSphereMachineWebhook{}
+			_, err := webhook.ValidateUpdate(context.Background(), tc.oldVSphereMachine, tc.vsphereMachine)
 			if tc.wantErr {
 				g.Expect(err).To(HaveOccurred())
 			} else {
@@ -175,14 +181,14 @@ func TestVSphereMachine_ValidateUpdate(t *testing.T) {
 	}
 }
 
-func createVSphereMachine(server string, providerID *string, preferredAPIServerCIDR string, ips []string, powerOffMode VirtualMachinePowerOpMode, guestSoftPowerOffTimeout *metav1.Duration) *VSphereMachine {
-	VSphereMachine := &VSphereMachine{
-		Spec: VSphereMachineSpec{
-			VirtualMachineCloneSpec: VirtualMachineCloneSpec{
+func createVSphereMachine(server string, providerID *string, preferredAPIServerCIDR string, ips []string, powerOffMode infrav1.VirtualMachinePowerOpMode, guestSoftPowerOffTimeout *metav1.Duration) *infrav1.VSphereMachine {
+	VSphereMachine := &infrav1.VSphereMachine{
+		Spec: infrav1.VSphereMachineSpec{
+			VirtualMachineCloneSpec: infrav1.VirtualMachineCloneSpec{
 				Server: server,
-				Network: NetworkSpec{
+				Network: infrav1.NetworkSpec{
 					PreferredAPIServerCIDR: preferredAPIServerCIDR,
-					Devices:                []NetworkDeviceSpec{},
+					Devices:                []infrav1.NetworkDeviceSpec{},
 				},
 			},
 			ProviderID:               providerID,
@@ -191,7 +197,7 @@ func createVSphereMachine(server string, providerID *string, preferredAPIServerC
 		},
 	}
 	for _, ip := range ips {
-		VSphereMachine.Spec.Network.Devices = append(VSphereMachine.Spec.Network.Devices, NetworkDeviceSpec{
+		VSphereMachine.Spec.Network.Devices = append(VSphereMachine.Spec.Network.Devices, infrav1.NetworkDeviceSpec{
 			IPAddrs: []string{ip},
 		})
 	}
diff --git a/apis/v1beta1/vspheremachinetemplate_webhooks_test.go b/internal/webhooks/vspheremachinetemplate_test.go
similarity index 89%
rename from apis/v1beta1/vspheremachinetemplate_webhooks_test.go
rename to internal/webhooks/vspheremachinetemplate_test.go
index 523aa6d3..faad6a4e 100644
--- a/apis/v1beta1/vspheremachinetemplate_webhooks_test.go
+++ b/internal/webhooks/vspheremachinetemplate_test.go
@@ -14,7 +14,7 @@ See the License for the specific language governing permissions and
 limitations under the License.
 */
 
-package v1beta1
+package webhooks
 
 import (
 	"context"
@@ -24,13 +24,15 @@ import (
 	admissionv1 "k8s.io/api/admission/v1"
 	"k8s.io/utils/pointer"
 	"sigs.k8s.io/controller-runtime/pkg/webhook/admission"
+
+	infrav1 "sigs.k8s.io/cluster-api-provider-vsphere/apis/v1beta1"
 )
 
 func TestVSphereMachineTemplate_ValidateCreate(t *testing.T) {
 	g := NewWithT(t)
 	tests := []struct {
 		name           string
-		vsphereMachine *VSphereMachineTemplate
+		vsphereMachine *infrav1.VSphereMachineTemplate
 		wantErr        bool
 	}{
 		{
@@ -85,8 +87,8 @@ func TestVSphereMachineTemplate_ValidateUpdate(t *testing.T) {
 	g := NewWithT(t)
 	tests := []struct {
 		name              string
-		oldVSphereMachine *VSphereMachineTemplate
-		vsphereMachine    *VSphereMachineTemplate
+		oldVSphereMachine *infrav1.VSphereMachineTemplate
+		vsphereMachine    *infrav1.VSphereMachineTemplate
 		req               *admission.Request
 		wantErr           bool
 	}{
@@ -143,17 +145,17 @@ func TestVSphereMachineTemplate_ValidateUpdate(t *testing.T) {
 	}
 }
 
-func createVSphereMachineTemplate(server, hwVersion string, providerID *string, preferredAPIServerCIDR string, ips []string) *VSphereMachineTemplate {
-	vsphereMachineTemplate := &VSphereMachineTemplate{
-		Spec: VSphereMachineTemplateSpec{
-			Template: VSphereMachineTemplateResource{
-				Spec: VSphereMachineSpec{
+func createVSphereMachineTemplate(server, hwVersion string, providerID *string, preferredAPIServerCIDR string, ips []string) *infrav1.VSphereMachineTemplate {
+	vsphereMachineTemplate := &infrav1.VSphereMachineTemplate{
+		Spec: infrav1.VSphereMachineTemplateSpec{
+			Template: infrav1.VSphereMachineTemplateResource{
+				Spec: infrav1.VSphereMachineSpec{
 					ProviderID: providerID,
-					VirtualMachineCloneSpec: VirtualMachineCloneSpec{
+					VirtualMachineCloneSpec: infrav1.VirtualMachineCloneSpec{
 						Server: server,
-						Network: NetworkSpec{
+						Network: infrav1.NetworkSpec{
 							PreferredAPIServerCIDR: preferredAPIServerCIDR,
-							Devices:                []NetworkDeviceSpec{},
+							Devices:                []infrav1.NetworkDeviceSpec{},
 						},
 						HardwareVersion: hwVersion,
 					},
@@ -162,7 +164,7 @@ func createVSphereMachineTemplate(server, hwVersion string, providerID *string,
 		},
 	}
 	for _, ip := range ips {
-		vsphereMachineTemplate.Spec.Template.Spec.Network.Devices = append(vsphereMachineTemplate.Spec.Template.Spec.Network.Devices, NetworkDeviceSpec{
+		vsphereMachineTemplate.Spec.Template.Spec.Network.Devices = append(vsphereMachineTemplate.Spec.Template.Spec.Network.Devices, infrav1.NetworkDeviceSpec{
 			IPAddrs: []string{ip},
 		})
 	}
diff --git a/apis/v1beta1/vspherevm_webhook_test.go b/internal/webhooks/vspherevm_test.go
similarity index 55%
rename from apis/v1beta1/vspherevm_webhook_test.go
rename to internal/webhooks/vspherevm_test.go
index 0a50929d..4b297960 100644
--- a/apis/v1beta1/vspherevm_webhook_test.go
+++ b/internal/webhooks/vspherevm_test.go
@@ -14,14 +14,17 @@ See the License for the specific language governing permissions and
 limitations under the License.
 */
 
-package v1beta1
+package webhooks
 
 import (
+	"context"
 	"testing"
 
 	. "github.com/onsi/gomega"
 	corev1 "k8s.io/api/core/v1"
 	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
+
+	infrav1 "sigs.k8s.io/cluster-api-provider-vsphere/apis/v1beta1"
 )
 
 const (
@@ -33,17 +36,19 @@ const (
 func TestVSphereVM_Default(t *testing.T) {
 	g := NewWithT(t)
 
-	WindowsVM := createVSphereVM(windowsVMName, "foo.com", "", "", "", []string{"192.168.0.1/32", "192.168.0.3/32"}, nil, Windows, VirtualMachinePowerOpModeTrySoft, nil)
-	LinuxVM := createVSphereVM(linuxVMName, "foo.com", "", "", "", []string{"192.168.0.1/32", "192.168.0.3/32"}, nil, Linux, VirtualMachinePowerOpModeTrySoft, nil)
-	NoOSVM := createVSphereVM(linuxVMName, "foo.com", "", "", "", []string{"192.168.0.1/32", "192.168.0.3/32"}, nil, "", VirtualMachinePowerOpModeTrySoft, nil)
+	WindowsVM := createVSphereVM(windowsVMName, "foo.com", "", "", "", []string{"192.168.0.1/32", "192.168.0.3/32"}, nil, infrav1.Windows, infrav1.VirtualMachinePowerOpModeTrySoft, nil)
+	LinuxVM := createVSphereVM(linuxVMName, "foo.com", "", "", "", []string{"192.168.0.1/32", "192.168.0.3/32"}, nil, infrav1.Linux, infrav1.VirtualMachinePowerOpModeTrySoft, nil)
+	NoOSVM := createVSphereVM(linuxVMName, "foo.com", "", "", "", []string{"192.168.0.1/32", "192.168.0.3/32"}, nil, "", infrav1.VirtualMachinePowerOpModeTrySoft, nil)
 
-	WindowsVM.Default()
-	LinuxVM.Default()
-	NoOSVM.Default()
+	ctx := context.Background()
+	webhook := &VSphereVMWebhook{}
+	g.Expect(webhook.Default(ctx, WindowsVM)).ToNot(HaveOccurred())
+	g.Expect(webhook.Default(ctx, LinuxVM)).ToNot(HaveOccurred())
+	g.Expect(webhook.Default(ctx, NoOSVM)).ToNot(HaveOccurred())
 
-	g.Expect(LinuxVM.Spec.OS).To(Equal(Linux))
-	g.Expect(WindowsVM.Spec.OS).To(Equal(Windows))
-	g.Expect(NoOSVM.Spec.OS).To(Equal(Linux))
+	g.Expect(LinuxVM.Spec.OS).To(Equal(infrav1.Linux))
+	g.Expect(WindowsVM.Spec.OS).To(Equal(infrav1.Windows))
+	g.Expect(NoOSVM.Spec.OS).To(Equal(infrav1.Linux))
 }
 
 func TestVSphereVM_ValidateCreate(t *testing.T) {
@@ -51,68 +56,69 @@ func TestVSphereVM_ValidateCreate(t *testing.T) {
 
 	tests := []struct {
 		name      string
-		vSphereVM *VSphereVM
+		vSphereVM *infrav1.VSphereVM
 		wantErr   bool
 	}{
 		{
 			name:      "preferredAPIServerCIDR set on creation ",
-			vSphereVM: createVSphereVM("vsphere-vm-1", "foo.com", "", "192.168.0.1/32", "", []string{}, nil, Linux, VirtualMachinePowerOpModeTrySoft, nil),
+			vSphereVM: createVSphereVM("vsphere-vm-1", "foo.com", "", "192.168.0.1/32", "", []string{}, nil, infrav1.Linux, infrav1.VirtualMachinePowerOpModeTrySoft, nil),
 			wantErr:   true,
 		},
 		{
 			name:      "IPs are not in CIDR format",
-			vSphereVM: createVSphereVM("vsphere-vm-1", "foo.com", "", "", "", []string{"192.168.0.1/32", "192.168.0.3"}, nil, Linux, VirtualMachinePowerOpModeTrySoft, nil),
+			vSphereVM: createVSphereVM("vsphere-vm-1", "foo.com", "", "", "", []string{"192.168.0.1/32", "192.168.0.3"}, nil, infrav1.Linux, infrav1.VirtualMachinePowerOpModeTrySoft, nil),
 			wantErr:   true,
 		},
 		{
 			name:      "successful VSphereVM creation",
-			vSphereVM: createVSphereVM("vsphere-vm-1", "foo.com", "", "", "", []string{"192.168.0.1/32", "192.168.0.3/32"}, nil, Linux, VirtualMachinePowerOpModeTrySoft, nil),
+			vSphereVM: createVSphereVM("vsphere-vm-1", "foo.com", "", "", "", []string{"192.168.0.1/32", "192.168.0.3/32"}, nil, infrav1.Linux, infrav1.VirtualMachinePowerOpModeTrySoft, nil),
 			wantErr:   false,
 		},
 		{
 			name:      "successful VSphereVM creation with powerOffMode set to hard",
-			vSphereVM: createVSphereVM("vsphere-vm-1", "foo.com", "", "", "", []string{"192.168.0.1/32", "192.168.0.3/32"}, nil, Linux, VirtualMachinePowerOpModeHard, nil),
+			vSphereVM: createVSphereVM("vsphere-vm-1", "foo.com", "", "", "", []string{"192.168.0.1/32", "192.168.0.3/32"}, nil, infrav1.Linux, infrav1.VirtualMachinePowerOpModeHard, nil),
 			wantErr:   false,
 		},
 		{
 			name:      "successful VSphereVM creation with powerOffMode set to soft",
-			vSphereVM: createVSphereVM("vsphere-vm-1", "foo.com", "", "", "", []string{"192.168.0.1/32", "192.168.0.3/32"}, nil, Linux, VirtualMachinePowerOpModeSoft, nil),
+			vSphereVM: createVSphereVM("vsphere-vm-1", "foo.com", "", "", "", []string{"192.168.0.1/32", "192.168.0.3/32"}, nil, infrav1.Linux, infrav1.VirtualMachinePowerOpModeSoft, nil),
 			wantErr:   false,
 		},
 		{
 			name:      "successful VSphereVM creation with powerOffMode set to trySoft and non-default timeout",
-			vSphereVM: createVSphereVM("vsphere-vm-1", "foo.com", "", "", "", []string{"192.168.0.1/32", "192.168.0.3/32"}, nil, Linux, VirtualMachinePowerOpModeTrySoft, &metav1.Duration{Duration: 1234}),
+			vSphereVM: createVSphereVM("vsphere-vm-1", "foo.com", "", "", "", []string{"192.168.0.1/32", "192.168.0.3/32"}, nil, infrav1.Linux, infrav1.VirtualMachinePowerOpModeTrySoft, &metav1.Duration{Duration: 1234}),
 			wantErr:   false,
 		},
 		{
 			name:      "name too long for Windows VM",
-			vSphereVM: createVSphereVM(windowsVMName, "foo.com", "", "", "", []string{"192.168.0.1/32", "192.168.0.3/32"}, nil, Windows, VirtualMachinePowerOpModeTrySoft, nil),
+			vSphereVM: createVSphereVM(windowsVMName, "foo.com", "", "", "", []string{"192.168.0.1/32", "192.168.0.3/32"}, nil, infrav1.Windows, infrav1.VirtualMachinePowerOpModeTrySoft, nil),
 			wantErr:   true,
 		},
 		{
 			name:      "no error with name too long for Linux VM",
-			vSphereVM: createVSphereVM(linuxVMName, "foo.com", "", "", "", []string{"192.168.0.1/32", "192.168.0.3/32"}, nil, Linux, VirtualMachinePowerOpModeTrySoft, nil),
+			vSphereVM: createVSphereVM(linuxVMName, "foo.com", "", "", "", []string{"192.168.0.1/32", "192.168.0.3/32"}, nil, infrav1.Linux, infrav1.VirtualMachinePowerOpModeTrySoft, nil),
 			wantErr:   false,
 		},
 		{
 			name:      "guestSoftPowerOffTimeout should not be set with powerOffMode set to hard",
-			vSphereVM: createVSphereVM(linuxVMName, "foo.com", "", "", "", []string{"192.168.0.1/32", "192.168.0.3/32"}, nil, Linux, VirtualMachinePowerOpModeHard, &metav1.Duration{Duration: GuestSoftPowerOffDefaultTimeout}),
+			vSphereVM: createVSphereVM(linuxVMName, "foo.com", "", "", "", []string{"192.168.0.1/32", "192.168.0.3/32"}, nil, infrav1.Linux, infrav1.VirtualMachinePowerOpModeHard, &metav1.Duration{Duration: infrav1.GuestSoftPowerOffDefaultTimeout}),
 			wantErr:   true,
 		},
 		{
 			name:      "guestSoftPowerOffTimeout should not be set with powerOffMode set to soft",
-			vSphereVM: createVSphereVM(linuxVMName, "foo.com", "", "", "", []string{"192.168.0.1/32", "192.168.0.3/32"}, nil, Linux, VirtualMachinePowerOpModeSoft, &metav1.Duration{Duration: GuestSoftPowerOffDefaultTimeout}),
+			vSphereVM: createVSphereVM(linuxVMName, "foo.com", "", "", "", []string{"192.168.0.1/32", "192.168.0.3/32"}, nil, infrav1.Linux, infrav1.VirtualMachinePowerOpModeSoft, &metav1.Duration{Duration: infrav1.GuestSoftPowerOffDefaultTimeout}),
 			wantErr:   true,
 		},
 		{
 			name:      "guestSoftPowerOffTimeout should not be negative",
-			vSphereVM: createVSphereVM(linuxVMName, "foo.com", "", "", "", []string{"192.168.0.1/32", "192.168.0.3/32"}, nil, Linux, VirtualMachinePowerOpModeTrySoft, &metav1.Duration{Duration: -1234}),
+			vSphereVM: createVSphereVM(linuxVMName, "foo.com", "", "", "", []string{"192.168.0.1/32", "192.168.0.3/32"}, nil, infrav1.Linux, infrav1.VirtualMachinePowerOpModeTrySoft, &metav1.Duration{Duration: -1234}),
 			wantErr:   true,
 		},
 	}
 	for _, tc := range tests {
 		t.Run(tc.name, func(t *testing.T) {
-			_, err := tc.vSphereVM.ValidateCreate()
+			webhook := &VSphereVMWebhook{}
+			_, err := webhook.ValidateCreate(context.Background(), tc.vSphereVM)
 			if tc.wantErr {
 				g.Expect(err).To(HaveOccurred())
 			} else {
@@ -127,86 +133,87 @@ func TestVSphereVM_ValidateUpdate(t *testing.T) {
 
 	tests := []struct {
 		name         string
-		oldVSphereVM *VSphereVM
-		vSphereVM    *VSphereVM
+		oldVSphereVM *infrav1.VSphereVM
+		vSphereVM    *infrav1.VSphereVM
 		wantErr      bool
 	}{
 		{
 			name:         "ProviderID can be updated",
-			oldVSphereVM: createVSphereVM("vsphere-vm-1", "foo.com", "", "", "", []string{"192.168.0.1/32"}, nil, Linux, VirtualMachinePowerOpModeTrySoft, nil),
-			vSphereVM:    createVSphereVM("vsphere-vm-1", "foo.com", biosUUID, "", "", []string{"192.168.0.1/32"}, nil, Linux, VirtualMachinePowerOpModeTrySoft, nil),
+			oldVSphereVM: createVSphereVM("vsphere-vm-1", "foo.com", "", "", "", []string{"192.168.0.1/32"}, nil, infrav1.Linux, infrav1.VirtualMachinePowerOpModeTrySoft, nil),
+			vSphereVM:    createVSphereVM("vsphere-vm-1", "foo.com", biosUUID, "", "", []string{"192.168.0.1/32"}, nil, infrav1.Linux, infrav1.VirtualMachinePowerOpModeTrySoft, nil),
 			wantErr:      false,
 		},
 		{
 			name:         "updating ips can be done",
-			oldVSphereVM: createVSphereVM("vsphere-vm-1", "foo.com", "", "", "", []string{"192.168.0.1/32"}, nil, Linux, VirtualMachinePowerOpModeTrySoft, nil),
-			vSphereVM:    createVSphereVM("vsphere-vm-1", "foo.com", biosUUID, "", "", []string{"192.168.0.1/32", "192.168.0.10/32"}, nil, Linux, VirtualMachinePowerOpModeTrySoft, nil),
+			oldVSphereVM: createVSphereVM("vsphere-vm-1", "foo.com", "", "", "", []string{"192.168.0.1/32"}, nil, infrav1.Linux, infrav1.VirtualMachinePowerOpModeTrySoft, nil),
+			vSphereVM:    createVSphereVM("vsphere-vm-1", "foo.com", biosUUID, "", "", []string{"192.168.0.1/32", "192.168.0.10/32"}, nil, infrav1.Linux, infrav1.VirtualMachinePowerOpModeTrySoft, nil),
 			wantErr:      false,
 		},
 		{
 			name:         "updating bootstrapRef can be done",
-			oldVSphereVM: createVSphereVM("vsphere-vm-1", "foo.com", "", "", "", []string{"192.168.0.1/32"}, nil, Linux, VirtualMachinePowerOpModeTrySoft, nil),
-			vSphereVM:    createVSphereVM("vsphere-vm-1", "foo.com", biosUUID, "", "", []string{"192.168.0.1/32", "192.168.0.10/32"}, &corev1.ObjectReference{}, Linux, VirtualMachinePowerOpModeTrySoft, nil),
+			oldVSphereVM: createVSphereVM("vsphere-vm-1", "foo.com", "", "", "", []string{"192.168.0.1/32"}, nil, infrav1.Linux, infrav1.VirtualMachinePowerOpModeTrySoft, nil),
+			vSphereVM:    createVSphereVM("vsphere-vm-1", "foo.com", biosUUID, "", "", []string{"192.168.0.1/32", "192.168.0.10/32"}, &corev1.ObjectReference{}, infrav1.Linux, infrav1.VirtualMachinePowerOpModeTrySoft, nil),
 			wantErr:      false,
 		},
 		{
 			name:         "updating server cannot be done",
-			oldVSphereVM: createVSphereVM("vsphere-vm-1", "foo.com", "", "", "", []string{"192.168.0.1/32"}, nil, Linux, VirtualMachinePowerOpModeTrySoft, nil),
-			vSphereVM:    createVSphereVM("vsphere-vm-1", "bar.com", biosUUID, "", "", []string{"192.168.0.1/32", "192.168.0.10/32"}, nil, Linux, VirtualMachinePowerOpModeTrySoft, nil),
+			oldVSphereVM: createVSphereVM("vsphere-vm-1", "foo.com", "", "", "", []string{"192.168.0.1/32"}, nil, infrav1.Linux, infrav1.VirtualMachinePowerOpModeTrySoft, nil),
+			vSphereVM:    createVSphereVM("vsphere-vm-1", "bar.com", biosUUID, "", "", []string{"192.168.0.1/32", "192.168.0.10/32"}, nil, infrav1.Linux, infrav1.VirtualMachinePowerOpModeTrySoft, nil),
 			wantErr:      true,
 		},
 		{
 			name:         "updating OS can be done only when empty",
-			oldVSphereVM: createVSphereVM("vsphere-vm-1-os", "foo.com", "", "", "", []string{"192.168.0.1/32"}, nil, "", VirtualMachinePowerOpModeTrySoft, nil),
-			vSphereVM:    createVSphereVM("vsphere-vm-1-os", "foo.com", "", "", "", []string{"192.168.0.1/32"}, nil, Linux, VirtualMachinePowerOpModeTrySoft, nil),
+			oldVSphereVM: createVSphereVM("vsphere-vm-1-os", "foo.com", "", "", "", []string{"192.168.0.1/32"}, nil, "", infrav1.VirtualMachinePowerOpModeTrySoft, nil),
+			vSphereVM:    createVSphereVM("vsphere-vm-1-os", "foo.com", "", "", "", []string{"192.168.0.1/32"}, nil, infrav1.Linux, infrav1.VirtualMachinePowerOpModeTrySoft, nil),
 			wantErr:      false,
 		},
 		{
 			name:         "updating OS cannot be done when alreadySet",
-			oldVSphereVM: createVSphereVM("vsphere-vm-1-os", "foo.com", "", "", "", []string{"192.168.0.1/32"}, nil, Windows, VirtualMachinePowerOpModeTrySoft, nil),
-			vSphereVM:    createVSphereVM("vsphere-vm-1-os", "foo.com", "", "", "", []string{"192.168.0.1/32"}, nil, Linux, VirtualMachinePowerOpModeTrySoft, nil),
+			oldVSphereVM: createVSphereVM("vsphere-vm-1-os", "foo.com", "", "", "", []string{"192.168.0.1/32"}, nil, infrav1.Windows, infrav1.VirtualMachinePowerOpModeTrySoft, nil),
+			vSphereVM:    createVSphereVM("vsphere-vm-1-os", "foo.com", "", "", "", []string{"192.168.0.1/32"}, nil, infrav1.Linux, infrav1.VirtualMachinePowerOpModeTrySoft, nil),
 			wantErr:      true,
 		},
 		{
 			name:         "updating thumbprint can be updated",
-			oldVSphereVM: createVSphereVM("vsphere-vm-1", "foo.com", "", "", "AA:BB:CC:DD:EE", []string{"192.168.0.1/32"}, nil, Linux, VirtualMachinePowerOpModeTrySoft, nil),
-			vSphereVM:    createVSphereVM("vsphere-vm-1", "foo.com", biosUUID, "", "BB:CC:DD:EE:FF", []string{"192.168.0.1/32"}, nil, Linux, VirtualMachinePowerOpModeTrySoft, nil),
+			oldVSphereVM: createVSphereVM("vsphere-vm-1", "foo.com", "", "", "AA:BB:CC:DD:EE", []string{"192.168.0.1/32"}, nil, infrav1.Linux, infrav1.VirtualMachinePowerOpModeTrySoft, nil),
+			vSphereVM:    createVSphereVM("vsphere-vm-1", "foo.com", biosUUID, "", "BB:CC:DD:EE:FF", []string{"192.168.0.1/32"}, nil, infrav1.Linux, infrav1.VirtualMachinePowerOpModeTrySoft, nil),
 			wantErr:      false,
 		},
 		{
 			name:         "powerOffMode cannot be updated when new powerOffMode is not valid",
-			oldVSphereVM: createVSphereVM("vsphere-vm-1", "foo.com", "", "", "AA:BB:CC:DD:EE", []string{"192.168.0.1/32"}, nil, Linux, VirtualMachinePowerOpModeTrySoft, nil),
-			vSphereVM:    createVSphereVM("vsphere-vm-1", "foo.com", biosUUID, "", "BB:CC:DD:EE:FF", []string{"192.168.0.1/32"}, nil, Linux, VirtualMachinePowerOpModeSoft, &metav1.Duration{Duration: GuestSoftPowerOffDefaultTimeout}),
+			oldVSphereVM: createVSphereVM("vsphere-vm-1", "foo.com", "", "", "AA:BB:CC:DD:EE", []string{"192.168.0.1/32"}, nil, infrav1.Linux, infrav1.VirtualMachinePowerOpModeTrySoft, nil),
+			vSphereVM:    createVSphereVM("vsphere-vm-1", "foo.com", biosUUID, "", "BB:CC:DD:EE:FF", []string{"192.168.0.1/32"}, nil, infrav1.Linux, infrav1.VirtualMachinePowerOpModeSoft, &metav1.Duration{Duration: infrav1.GuestSoftPowerOffDefaultTimeout}),
 			wantErr:      true,
 		},
 		{
 			name:         "powerOffMode can be updated to hard",
-			oldVSphereVM: createVSphereVM("vsphere-vm-1", "foo.com", "", "", "AA:BB:CC:DD:EE", []string{"192.168.0.1/32"}, nil, Linux, VirtualMachinePowerOpModeTrySoft, &metav1.Duration{Duration: GuestSoftPowerOffDefaultTimeout}),
-			vSphereVM:    createVSphereVM("vsphere-vm-1", "foo.com", biosUUID, "", "BB:CC:DD:EE:FF", []string{"192.168.0.1/32"}, nil, Linux, VirtualMachinePowerOpModeHard, nil),
+			oldVSphereVM: createVSphereVM("vsphere-vm-1", "foo.com", "", "", "AA:BB:CC:DD:EE", []string{"192.168.0.1/32"}, nil, infrav1.Linux, infrav1.VirtualMachinePowerOpModeTrySoft, &metav1.Duration{Duration: infrav1.GuestSoftPowerOffDefaultTimeout}),
+			vSphereVM:    createVSphereVM("vsphere-vm-1", "foo.com", biosUUID, "", "BB:CC:DD:EE:FF", []string{"192.168.0.1/32"}, nil, infrav1.Linux, infrav1.VirtualMachinePowerOpModeHard, nil),
 			wantErr:      false,
 		},
 		{
 			name:         "powerOffMode can be updated to soft",
-			oldVSphereVM: createVSphereVM("vsphere-vm-1", "foo.com", "", "", "AA:BB:CC:DD:EE", []string{"192.168.0.1/32"}, nil, Linux, VirtualMachinePowerOpModeTrySoft, &metav1.Duration{Duration: GuestSoftPowerOffDefaultTimeout}),
-			vSphereVM:    createVSphereVM("vsphere-vm-1", "foo.com", biosUUID, "", "BB:CC:DD:EE:FF", []string{"192.168.0.1/32"}, nil, Linux, VirtualMachinePowerOpModeSoft, nil),
+			oldVSphereVM: createVSphereVM("vsphere-vm-1", "foo.com", "", "", "AA:BB:CC:DD:EE", []string{"192.168.0.1/32"}, nil, infrav1.Linux, infrav1.VirtualMachinePowerOpModeTrySoft, &metav1.Duration{Duration: infrav1.GuestSoftPowerOffDefaultTimeout}),
+			vSphereVM:    createVSphereVM("vsphere-vm-1", "foo.com", biosUUID, "", "BB:CC:DD:EE:FF", []string{"192.168.0.1/32"}, nil, infrav1.Linux, infrav1.VirtualMachinePowerOpModeSoft, nil),
 			wantErr:      false,
 		},
 		{
 			name:         "biosUUID can be set to a value",
-			oldVSphereVM: createVSphereVM("vsphere-vm-1", "foo.com", "", "", "AA:BB:CC:DD:EE", []string{"192.168.0.1/32"}, nil, Linux, VirtualMachinePowerOpModeTrySoft, nil),
-			vSphereVM:    createVSphereVM("vsphere-vm-1", "foo.com", biosUUID, "", "AA:BB:CC:DD:EE", []string{"192.168.0.1/32"}, nil, Linux, VirtualMachinePowerOpModeTrySoft, nil),
+			oldVSphereVM: createVSphereVM("vsphere-vm-1", "foo.com", "", "", "AA:BB:CC:DD:EE", []string{"192.168.0.1/32"}, nil, infrav1.Linux, infrav1.VirtualMachinePowerOpModeTrySoft, nil),
+			vSphereVM:    createVSphereVM("vsphere-vm-1", "foo.com", biosUUID, "", "AA:BB:CC:DD:EE", []string{"192.168.0.1/32"}, nil, infrav1.Linux, infrav1.VirtualMachinePowerOpModeTrySoft, nil),
 			wantErr:      false,
 		},
 		{
 			name:         "biosUUID cannot be updated to a different value",
-			oldVSphereVM: createVSphereVM("vsphere-vm-1", "foo.com", "old-uuid", "", "AA:BB:CC:DD:EE", []string{"192.168.0.1/32"}, nil, Linux, VirtualMachinePowerOpModeTrySoft, nil),
-			vSphereVM:    createVSphereVM("vsphere-vm-1", "foo.com", biosUUID, "", "AA:BB:CC:DD:EE", []string{"192.168.0.1/32"}, nil, Linux, VirtualMachinePowerOpModeTrySoft, nil),
+			oldVSphereVM: createVSphereVM("vsphere-vm-1", "foo.com", "old-uuid", "", "AA:BB:CC:DD:EE", []string{"192.168.0.1/32"}, nil, infrav1.Linux, infrav1.VirtualMachinePowerOpModeTrySoft, nil),
+			vSphereVM:    createVSphereVM("vsphere-vm-1", "foo.com", biosUUID, "", "AA:BB:CC:DD:EE", []string{"192.168.0.1/32"}, nil, infrav1.Linux, infrav1.VirtualMachinePowerOpModeTrySoft, nil),
 			wantErr:      true,
 		},
 	}
 	for _, tc := range tests {
 		t.Run(tc.name, func(t *testing.T) {
-			_, err := tc.vSphereVM.ValidateUpdate(tc.oldVSphereVM)
+			webhook := &VSphereVMWebhook{}
+			_, err := webhook.ValidateUpdate(context.Background(), tc.oldVSphereVM, tc.vSphereVM)
 			if tc.wantErr {
 				g.Expect(err).To(HaveOccurred())
 			} else {
@@ -216,17 +223,17 @@ func TestVSphereVM_ValidateUpdate(t *testing.T) {
 	}
 }
 
-func createVSphereVM(name, server, biosUUID, preferredAPIServerCIDR, thumbprint string, ips []string, bootstrapRef *corev1.ObjectReference, os OS, powerOffMode VirtualMachinePowerOpMode, guestSoftPowerOffTimeout *metav1.Duration) *VSphereVM {
-	VSphereVM := &VSphereVM{
+func createVSphereVM(name, server, biosUUID, preferredAPIServerCIDR, thumbprint string, ips []string, bootstrapRef *corev1.ObjectReference, os infrav1.OS, powerOffMode infrav1.VirtualMachinePowerOpMode, guestSoftPowerOffTimeout *metav1.Duration) *infrav1.VSphereVM {
+	VSphereVM := &infrav1.VSphereVM{
 		ObjectMeta: metav1.ObjectMeta{
 			Name: name,
 		},
-		Spec: VSphereVMSpec{
-			VirtualMachineCloneSpec: VirtualMachineCloneSpec{
+		Spec: infrav1.VSphereVMSpec{
+			VirtualMachineCloneSpec: infrav1.VirtualMachineCloneSpec{
 				Server: server,
-				Network: NetworkSpec{
+				Network: infrav1.NetworkSpec{
 					PreferredAPIServerCIDR: preferredAPIServerCIDR,
-					Devices:                []NetworkDeviceSpec{},
+					Devices:                []infrav1.NetworkDeviceSpec{},
 				},
 				Thumbprint: thumbprint,
 			},
@@ -241,7 +248,7 @@ func createVSphereVM(name, server, biosUUID, preferredAPIServerCIDR, thumbprint
 		VSphereVM.Spec.OS = os
 	}
 	for _, ip := range ips {
-		VSphereVM.Spec.Network.Devices = append(VSphereVM.Spec.Network.Devices, NetworkDeviceSpec{
+		VSphereVM.Spec.Network.Devices = append(VSphereVM.Spec.Network.Devices, infrav1.NetworkDeviceSpec{
 			IPAddrs: []string{ip},
 		})
 	}
diff --git a/test/helpers/envtest.go b/test/helpers/envtest.go
index b4f11e83..0d3b2084 100644
--- a/test/helpers/envtest.go
+++ b/test/helpers/envtest.go
@@ -48,6 +48,7 @@ import (
 	infrav1alpha3 "sigs.k8s.io/cluster-api-provider-vsphere/apis/v1alpha3"
 	infrav1alpha4 "sigs.k8s.io/cluster-api-provider-vsphere/apis/v1alpha4"
 	infrav1 "sigs.k8s.io/cluster-api-provider-vsphere/apis/v1beta1"
+	"sigs.k8s.io/cluster-api-provider-vsphere/internal/webhooks"
 	"sigs.k8s.io/cluster-api-provider-vsphere/pkg/context"
 	"sigs.k8s.io/cluster-api-provider-vsphere/pkg/manager"
 	"sigs.k8s.io/cluster-api-provider-vsphere/test/helpers/vcsim"
@@ -141,23 +142,27 @@ func NewTestEnvironment() *TestEnvironment {
 		Password:   simr.Password(),
 	}
 	managerOpts.AddToManager = func(ctx *context.ControllerManagerContext, mgr ctrlmgr.Manager) error {
-		if err := (&infrav1.VSphereMachine{}).SetupWebhookWithManager(mgr); err != nil {
+		if err := (&webhooks.VSphereClusterTemplateWebhook{}).SetupWebhookWithManager(mgr); err != nil {
 			return err
 		}
 
-		if err := (&infrav1.VSphereMachineTemplateWebhook{}).SetupWebhookWithManager(mgr); err != nil {
+		if err := (&webhooks.VSphereMachineWebhook{}).SetupWebhookWithManager(mgr); err != nil {
 			return err
 		}
 
-		if err := (&infrav1.VSphereVM{}).SetupWebhookWithManager(mgr); err != nil {
+		if err := (&webhooks.VSphereMachineTemplateWebhook{}).SetupWebhookWithManager(mgr); err != nil {
 			return err
 		}
 
-		if err := (&infrav1.VSphereDeploymentZone{}).SetupWebhookWithManager(mgr); err != nil {
+		if err := (&webhooks.VSphereVMWebhook{}).SetupWebhookWithManager(mgr); err != nil {
 			return err
 		}
 
-		return (&infrav1.VSphereFailureDomain{}).SetupWebhookWithManager(mgr)
+		if err := (&webhooks.VSphereDeploymentZoneWebhook{}).SetupWebhookWithManager(mgr); err != nil {
+			return err
+		}
+
+		return (&webhooks.VSphereFailureDomainWebhook{}).SetupWebhookWithManager(mgr)
 	}
 
 	mgr, err := manager.New(managerOpts)