Repository: syself/cluster-api-provider-hetzner
Pull request #516 (2022-12-12 14:32:05)

:sparkles: Enable SSA apply for hcloud and bare metal templates
**What type of PR is this?**
/feature

**What this PR does / why we need it**:
Enable SSA apply for HCloudMachineTemplate and HetznerBareMetalMachineTemplate by using admission.CustomValidator. This is the foundation to efficiently use the ClusterClass feature of CAPI.

**TODOs**:
- [ ] squashed commits
- [ ] includes documentation
- [ ] adds unit tests




Patch:
diff --git a/api/v1beta1/hcloudmachinetemplate_webhook.go b/api/v1beta1/hcloudmachinetemplate_webhook.go
index b4be62d0..edb737b7 100644
--- a/api/v1beta1/hcloudmachinetemplate_webhook.go
+++ b/api/v1beta1/hcloudmachinetemplate_webhook.go
@@ -17,63 +17,66 @@ limitations under the License.
 package v1beta1
 
 import (
+	"context"
+	"fmt"
 	"reflect"
 
-	"github.com/syself/cluster-api-provider-hetzner/pkg/utils"
 	apierrors "k8s.io/apimachinery/pkg/api/errors"
 	"k8s.io/apimachinery/pkg/runtime"
+	"k8s.io/apimachinery/pkg/util/validation/field"
+	"sigs.k8s.io/cluster-api/util/topology"
 	ctrl "sigs.k8s.io/controller-runtime"
 	"sigs.k8s.io/controller-runtime/pkg/webhook"
+	"sigs.k8s.io/controller-runtime/pkg/webhook/admission"
 )
 
-// log is for logging in this package.
-var hcloudmachinetemplatelog = utils.GetDefaultLogger("info").WithName("hcloudmachinetemplate-resource")
-
 // SetupWebhookWithManager initializes webhook manager for HetznerMachineTemplate.
-func (r *HCloudMachineTemplate) SetupWebhookWithManager(mgr ctrl.Manager) error {
+func (r *HCloudMachineTemplateWebhook) SetupWebhookWithManager(mgr ctrl.Manager) error {
 	return ctrl.NewWebhookManagedBy(mgr).
-		For(r).
+		For(&HCloudMachineTemplate{}).
+		WithValidator(r).
 		Complete()
 }
 
-// SetupWebhookWithManager initializes webhook manager for HetznerMachineTemplateList.
-func (r *HCloudMachineTemplateList) SetupWebhookWithManager(mgr ctrl.Manager) error {
-	return ctrl.NewWebhookManagedBy(mgr).
-		For(r).
-		Complete()
-}
-
-//+kubebuilder:webhook:path=/mutate-infrastructure-cluster-x-k8s-io-v1beta1-hcloudmachinetemplate,mutating=true,failurePolicy=fail,sideEffects=None,groups=infrastructure.cluster.x-k8s.io,resources=hcloudmachinetemplates,verbs=create;update,versions=v1beta1,name=mutation.hcloudmachinetemplate.infrastructure.x-k8s.io,admissionReviewVersions={v1,v1beta1}
-
-var _ webhook.Defaulter = &HCloudMachineTemplate{}
-
-// Default implements webhook.Defaulter so a webhook will be registered for the type.
-func (r *HCloudMachineTemplate) Default() {}
+// HCloudMachineTemplateWebhook implements a custom validation webhook for HCloudMachineTemplate.
+// +kubebuilder:object:generate=false
+type HCloudMachineTemplateWebhook struct{}
 
-//+kubebuilder:webhook:path=/validate-infrastructure-cluster-x-k8s-io-v1beta1-hcloudmachinetemplate,mutating=false,failurePolicy=fail,sideEffects=None,groups=infrastructure.cluster.x-k8s.io,resources=hcloudmachinetemplates,verbs=create;update,versions=v1beta1,name=validation.hcloudmachinetemplate.infrastructure.x-k8s.io,admissionReviewVersions={v1,v1beta1}
+// +kubebuilder:webhook:path=/validate-infrastructure-cluster-x-k8s-io-v1beta1-hcloudmachinetemplate,mutating=false,sideEffects=None,failurePolicy=fail,sideEffects=None,groups=infrastructure.cluster.x-k8s.io,resources=hcloudmachinetemplates,verbs=create;update,versions=v1beta1,name=validation.hcloudmachinetemplate.infrastructure.x-k8s.io,admissionReviewVersions=v1;v1beta1
 
-var _ webhook.Validator = &HCloudMachineTemplate{}
+var _ webhook.CustomValidator = &HCloudMachineTemplateWebhook{}
 
 // ValidateCreate implements webhook.Validator so a webhook will be registered for the type.
-func (r *HCloudMachineTemplate) ValidateCreate() error {
-	hcloudmachinetemplatelog.V(1).Info("validate create", "name", r.Name)
+func (r *HCloudMachineTemplateWebhook) ValidateCreate(_ context.Context, _ runtime.Object) error {
 	return nil
 }
 
 // ValidateUpdate implements webhook.Validator so a webhook will be registered for the type.
-func (r *HCloudMachineTemplate) ValidateUpdate(old runtime.Object) error {
-	hcloudmachinetemplatelog.V(1).Info("validate update", "name", r.Name)
-	oldHCloudMachineTemplate := old.(*HCloudMachineTemplate)
-	if !reflect.DeepEqual(r.Spec, oldHCloudMachineTemplate.Spec) {
-		hcloudmachinetemplatelog.Info("not equal", "new HcloudMachineTemplateSpec", r.Spec, "old HcloudMachineTemplateSpec", oldHCloudMachineTemplate.Spec)
-		return apierrors.NewBadRequest("HCloudMachineTemplate.Spec is immutable")
+func (r *HCloudMachineTemplateWebhook) ValidateUpdate(ctx context.Context, oldRaw runtime.Object, newRaw runtime.Object) error {
+	newHCloudMachineTemplate, ok := newRaw.(*HCloudMachineTemplate)
+	if !ok {
+		return apierrors.NewBadRequest(fmt.Sprintf("expected a HCloudMachineTemplate but got a %T", newRaw))
+	}
+	oldHCloudMachineTemplate, ok := oldRaw.(*HCloudMachineTemplate)
+	if !ok {
+		return apierrors.NewBadRequest(fmt.Sprintf("expected a HCloudMachineTemplate but got a %T", oldRaw))
 	}
 
-	return nil
+	req, err := admission.RequestFromContext(ctx)
+	if err != nil {
+		return apierrors.NewBadRequest(fmt.Sprintf("expected a admission.Request inside context: %v", err))
+	}
+
+	var allErrs field.ErrorList
+
+	if !topology.ShouldSkipImmutabilityChecks(req, newHCloudMachineTemplate) && !reflect.DeepEqual(newHCloudMachineTemplate.Spec, oldHCloudMachineTemplate.Spec) {
+		allErrs = append(allErrs, field.Invalid(field.NewPath("spec"), newHCloudMachineTemplate, "HCloudMachineTemplate.Spec is immutable"))
+	}
+
+	return aggregateObjErrors(newHCloudMachineTemplate.GroupVersionKind().GroupKind(), newHCloudMachineTemplate.Name, allErrs)
 }
 
 // ValidateDelete implements webhook.Validator so a webhook will be registered for the type.
-func (r *HCloudMachineTemplate) ValidateDelete() error {
-	hcloudmachinetemplatelog.V(1).Info("validate delete", "name", r.Name)
+func (r *HCloudMachineTemplateWebhook) ValidateDelete(_ context.Context, _ runtime.Object) error {
 	return nil
 }
diff --git a/api/v1beta1/hetznerbaremetalmachinetemplate_webhook.go b/api/v1beta1/hetznerbaremetalmachinetemplate_webhook.go
index 13631ec9..56b5d336 100644
--- a/api/v1beta1/hetznerbaremetalmachinetemplate_webhook.go
+++ b/api/v1beta1/hetznerbaremetalmachinetemplate_webhook.go
@@ -17,49 +17,66 @@ limitations under the License.
 package v1beta1
 
 import (
+	"context"
+	"fmt"
 	"reflect"
 
 	apierrors "k8s.io/apimachinery/pkg/api/errors"
 	"k8s.io/apimachinery/pkg/runtime"
+	"k8s.io/apimachinery/pkg/util/validation/field"
+	"sigs.k8s.io/cluster-api/util/topology"
 	ctrl "sigs.k8s.io/controller-runtime"
 	"sigs.k8s.io/controller-runtime/pkg/webhook"
+	"sigs.k8s.io/controller-runtime/pkg/webhook/admission"
 )
 
 // SetupWebhookWithManager initializes webhook manager for HetznerBareMetalMachineTemplate.
-func (r *HetznerBareMetalMachineTemplate) SetupWebhookWithManager(mgr ctrl.Manager) error {
+func (r *HetznerBareMetalMachineTemplateWebhook) SetupWebhookWithManager(mgr ctrl.Manager) error {
 	return ctrl.NewWebhookManagedBy(mgr).
-		For(r).
+		For(&HetznerBareMetalMachineTemplate{}).
+		WithValidator(r).
 		Complete()
 }
 
-//+kubebuilder:webhook:path=/mutate-infrastructure-cluster-x-k8s-io-v1beta1-hetznerbaremetalmachinetemplate,mutating=true,failurePolicy=fail,sideEffects=None,groups=infrastructure.cluster.x-k8s.io,resources=hetznerbaremetalmachinetemplates,verbs=create;update,versions=v1beta1,name=mutation.hetznerbaremetalmachinetemplate.infrastructure.cluster.x-k8s.io,admissionReviewVersions={v1,v1beta1}
+// HetznerBareMetalMachineTemplateWebhook implements a custom validation webhook for HetznerBareMetalMachineTemplate.
+// +kubebuilder:object:generate=false
+type HetznerBareMetalMachineTemplateWebhook struct{}
 
-var _ webhook.Defaulter = &HetznerBareMetalMachineTemplate{}
+//+kubebuilder:webhook:path=/validate-infrastructure-cluster-x-k8s-io-v1beta1-hetznerbaremetalmachinetemplate,mutating=false,sideEffects=None,failurePolicy=fail,sideEffects=None,groups=infrastructure.cluster.x-k8s.io,resources=hetznerbaremetalmachinetemplates,verbs=create;update,versions=v1beta1,name=validation.hetznerbaremetalmachinetemplate.infrastructure.cluster.x-k8s.io,admissionReviewVersions={v1,v1beta1}
 
-// Default implements webhook.Defaulter so a webhook will be registered for the type.
-func (r *HetznerBareMetalMachineTemplate) Default() {
-}
-
-//+kubebuilder:webhook:path=/validate-infrastructure-cluster-x-k8s-io-v1beta1-hetznerbaremetalmachinetemplate,mutating=false,failurePolicy=fail,sideEffects=None,groups=infrastructure.cluster.x-k8s.io,resources=hetznerbaremetalmachinetemplates,verbs=create;update,versions=v1beta1,name=validation.hetznerbaremetalmachinetemplate.infrastructure.cluster.x-k8s.io,admissionReviewVersions={v1,v1beta1}
-
-var _ webhook.Validator = &HetznerBareMetalMachineTemplate{}
+var _ webhook.CustomValidator = &HetznerBareMetalMachineTemplateWebhook{}
 
 // ValidateCreate implements webhook.Validator so a webhook will be registered for the type.
-func (r *HetznerBareMetalMachineTemplate) ValidateCreate() error {
+func (r *HetznerBareMetalMachineTemplateWebhook) ValidateCreate(_ context.Context, _ runtime.Object) error {
 	return nil
 }
 
 // ValidateUpdate implements webhook.Validator so a webhook will be registered for the type.
-func (r *HetznerBareMetalMachineTemplate) ValidateUpdate(old runtime.Object) error {
-	oldHetznerBareMetalMachineTemplate := old.(*HetznerBareMetalMachineTemplate)
-	if !reflect.DeepEqual(r.Spec, oldHetznerBareMetalMachineTemplate.Spec) {
-		hcloudmachinetemplatelog.Info("not equal", "new HetznerBareMetalMachineTemplateSpec", r.Spec, "old HetznerBareMetalMachineTemplateSpec", oldHetznerBareMetalMachineTemplate.Spec)
-		return apierrors.NewBadRequest("HetznerBareMetalMachineTemplate.Spec is immutable")
+func (r *HetznerBareMetalMachineTemplateWebhook) ValidateUpdate(ctx context.Context, oldRaw runtime.Object, newRaw runtime.Object) error {
+	newHetznerBareMetalMachineTemplate, ok := newRaw.(*HetznerBareMetalMachineTemplate)
+	if !ok {
+		return apierrors.NewBadRequest(fmt.Sprintf("expected a HetznerBareMetalMachineTemplate but got a %T", newRaw))
 	}
-	return nil
+	oldHetznerBareMetalMachineTemplate, ok := oldRaw.(*HetznerBareMetalMachineTemplate)
+	if !ok {
+		return apierrors.NewBadRequest(fmt.Sprintf("expected a HetznerBareMetalMachineTemplate but got a %T", oldRaw))
+	}
+
+	req, err := admission.RequestFromContext(ctx)
+	if err != nil {
+		return apierrors.NewBadRequest(fmt.Sprintf("expected a admission.Request inside context: %v", err))
+	}
+
+	var allErrs field.ErrorList
+
+	if !topology.ShouldSkipImmutabilityChecks(req, newHetznerBareMetalMachineTemplate) && !reflect.DeepEqual(newHetznerBareMetalMachineTemplate.Spec, oldHetznerBareMetalMachineTemplate.Spec) {
+		allErrs = append(allErrs, field.Invalid(field.NewPath("spec"), newHetznerBareMetalMachineTemplate, "HetznerBareMetalMachineTemplate.Spec is immutable"))
+	}
+
+	return aggregateObjErrors(newHetznerBareMetalMachineTemplate.GroupVersionKind().GroupKind(), newHetznerBareMetalMachineTemplate.Name, allErrs)
 }
 
 // ValidateDelete implements webhook.Validator so a webhook will be registered for the type.
-func (r *HetznerBareMetalMachineTemplate) ValidateDelete() error {
+func (r *HetznerBareMetalMachineTemplateWebhook) ValidateDelete(_ context.Context, _ runtime.Object) error {
 	return nil
 }
diff --git a/api/v1beta1/hetznercluster_types.go b/api/v1beta1/hetznercluster_types.go
index 0151fabe..46019655 100644
--- a/api/v1beta1/hetznercluster_types.go
+++ b/api/v1beta1/hetznercluster_types.go
@@ -37,8 +37,6 @@ type HetznerClusterSpec struct {
 	// ControlPlaneRegion consists of a list of HCloud Regions (fsn, nbg, hel). Because HCloud Networks
 	// have a very low latency we could assume in some use-cases that a region is behaving like a zone
 	// https://kubernetes.io/docs/reference/labels-annotations-taints/#topologykubernetesiozone
-	// +kubebuilder:validation:MinItems=1
-	// +kubebuilder:default={fsn1}
 	ControlPlaneRegions []Region `json:"controlPlaneRegions"`
 
 	// SSHKeys are cluster wide. Valid values are a valid SSH key name.
diff --git a/api/v1beta1/hetznercluster_webhook.go b/api/v1beta1/hetznercluster_webhook.go
index e3c2a435..0db99eeb 100644
--- a/api/v1beta1/hetznercluster_webhook.go
+++ b/api/v1beta1/hetznercluster_webhook.go
@@ -70,6 +70,34 @@ func (r *HetznerCluster) ValidateCreate() error {
 	hetznerclusterlog.V(1).Info("validate create", "name", r.Name)
 	var allErrs field.ErrorList
 
+	if len(r.Spec.ControlPlaneRegions) == 0 {
+		allErrs = append(allErrs, field.Invalid(
+			field.NewPath("spec", "controlPlaneRegions"),
+			r.Spec.ControlPlaneRegions,
+			"control plane regions must not be empty",
+		))
+	}
+
+	for _, region := range r.Spec.ControlPlaneRegions {
+		if _, ok := regionNetworkZoneMap[string(region)]; !ok {
+			allErrs = append(allErrs, field.Invalid(
+				field.NewPath("spec", "controlPlaneRegions"),
+				r.Spec.ControlPlaneRegions,
+				"wrong control plane region. Should be fsn1, nbg1, hel1, or ash",
+			))
+		}
+	}
+
+	if r.Spec.ControlPlaneLoadBalancer.Enabled {
+		if r.Spec.ControlPlaneLoadBalancer.Region == Region("") {
+			allErrs = append(allErrs, field.Invalid(
+				field.NewPath("spec", "controlPlaneLoadBalancer", "region"),
+				r.Spec.ControlPlaneLoadBalancer.Region,
+				"region should not be empty if load balancer is enabled"),
+			)
+		}
+	}
+
 	// Check whether regions are all in same network zone
 	if !r.Spec.HCloudNetwork.Enabled {
 		if err := isNetworkZoneSameForAllRegions(r.Spec.ControlPlaneRegions, nil); err != nil {
diff --git a/api/v1beta1/types.go b/api/v1beta1/types.go
index 4cf8bf40..28cb8e5c 100644
--- a/api/v1beta1/types.go
+++ b/api/v1beta1/types.go
@@ -157,7 +157,7 @@ type LoadBalancerSpec struct {
 	ExtraServices []LoadBalancerServiceSpec `json:"extraServices,omitempty"`
 
 	// Region contains the name of the HCloud location the load balancer is running.
-	Region Region `json:"region"`
+	Region Region `json:"region,omitempty"`
 }
 
 // LoadBalancerServiceSpec defines a Loadbalancer Target.
diff --git a/config/crd/bases/infrastructure.cluster.x-k8s.io_hetznerclusters.yaml b/config/crd/bases/infrastructure.cluster.x-k8s.io_hetznerclusters.yaml
index ee3141d9..d071f58c 100644
--- a/config/crd/bases/infrastructure.cluster.x-k8s.io_hetznerclusters.yaml
+++ b/config/crd/bases/infrastructure.cluster.x-k8s.io_hetznerclusters.yaml
@@ -150,12 +150,8 @@ spec:
                     - lb21
                     - lb31
                     type: string
-                required:
-                - region
                 type: object
               controlPlaneRegions:
-                default:
-                - fsn1
                 description: ControlPlaneRegion consists of a list of HCloud Regions
                   (fsn, nbg, hel). Because HCloud Networks have a very low latency
                   we could assume in some use-cases that a region is behaving like
@@ -168,7 +164,6 @@ spec:
                   - nbg1
                   - ash
                   type: string
-                minItems: 1
                 type: array
               hcloudNetwork:
                 description: HCloudNetworkSpec defines the Network for Hetzner Cloud.
diff --git a/config/crd/bases/infrastructure.cluster.x-k8s.io_hetznerclustertemplates.yaml b/config/crd/bases/infrastructure.cluster.x-k8s.io_hetznerclustertemplates.yaml
index 97e230d5..d5668e41 100644
--- a/config/crd/bases/infrastructure.cluster.x-k8s.io_hetznerclustertemplates.yaml
+++ b/config/crd/bases/infrastructure.cluster.x-k8s.io_hetznerclustertemplates.yaml
@@ -178,12 +178,8 @@ spec:
                             - lb21
                             - lb31
                             type: string
-                        required:
-                        - region
                         type: object
                       controlPlaneRegions:
-                        default:
-                        - fsn1
                         description: ControlPlaneRegion consists of a list of HCloud
                           Regions (fsn, nbg, hel). Because HCloud Networks have a
                           very low latency we could assume in some use-cases that
@@ -196,7 +192,6 @@ spec:
                           - nbg1
                           - ash
                           type: string
-                        minItems: 1
                         type: array
                       hcloudNetwork:
                         description: HCloudNetworkSpec defines the Network for Hetzner
diff --git a/config/webhook/manifests.yaml b/config/webhook/manifests.yaml
index b2cac2f2..8fa4b114 100644
--- a/config/webhook/manifests.yaml
+++ b/config/webhook/manifests.yaml
@@ -26,27 +26,6 @@ webhooks:
     resources:
     - hcloudmachines
   sideEffects: None
-- admissionReviewVersions:
-  - v1
-  - v1beta1
-  clientConfig:
-    service:
-      name: webhook-service
-      namespace: system
-      path: /mutate-infrastructure-cluster-x-k8s-io-v1beta1-hcloudmachinetemplate
-  failurePolicy: Fail
-  name: mutation.hcloudmachinetemplate.infrastructure.x-k8s.io
-  rules:
-  - apiGroups:
-    - infrastructure.cluster.x-k8s.io
-    apiVersions:
-    - v1beta1
-    operations:
-    - CREATE
-    - UPDATE
-    resources:
-    - hcloudmachinetemplates
-  sideEffects: None
 - admissionReviewVersions:
   - v1
   - v1beta1
@@ -89,27 +68,6 @@ webhooks:
     resources:
     - hetznerbaremetalmachines
   sideEffects: None
-- admissionReviewVersions:
-  - v1
-  - v1beta1
-  clientConfig:
-    service:
-      name: webhook-service
-      namespace: system
-      path: /mutate-infrastructure-cluster-x-k8s-io-v1beta1-hetznerbaremetalmachinetemplate
-  failurePolicy: Fail
-  name: mutation.hetznerbaremetalmachinetemplate.infrastructure.cluster.x-k8s.io
-  rules:
-  - apiGroups:
-    - infrastructure.cluster.x-k8s.io
-    apiVersions:
-    - v1beta1
-    operations:
-    - CREATE
-    - UPDATE
-    resources:
-    - hetznerbaremetalmachinetemplates
-  sideEffects: None
 - admissionReviewVersions:
   - v1
   - v1beta1
diff --git a/main.go b/main.go
index 4eb4e732..2fe029ab 100644
--- a/main.go
+++ b/main.go
@@ -206,7 +206,7 @@ func setUpWebhookWithManager(mgr ctrl.Manager) {
 		setupLog.Error(err, "unable to create webhook", "webhook", "HCloudMachine")
 		os.Exit(1)
 	}
-	if err := (&infrastructurev1beta1.HCloudMachineTemplate{}).SetupWebhookWithManager(mgr); err != nil {
+	if err := (&infrastructurev1beta1.HCloudMachineTemplateWebhook{}).SetupWebhookWithManager(mgr); err != nil {
 		setupLog.Error(err, "unable to create webhook", "webhook", "HCloudMachineTemplate")
 		os.Exit(1)
 	}
@@ -218,7 +218,7 @@ func setUpWebhookWithManager(mgr ctrl.Manager) {
 		setupLog.Error(err, "unable to create webhook", "webhook", "HetznerBareMetalMachine")
 		os.Exit(1)
 	}
-	if err := (&infrastructurev1beta1.HetznerBareMetalMachineTemplate{}).SetupWebhookWithManager(mgr); err != nil {
+	if err := (&infrastructurev1beta1.HetznerBareMetalMachineTemplateWebhook{}).SetupWebhookWithManager(mgr); err != nil {
 		setupLog.Error(err, "unable to create webhook", "webhook", "HetznerBareMetalMachineTemplate")
 		os.Exit(1)
 	}
diff --git a/templates/cluster-templates/cluster-class-topology-example.yaml b/templates/cluster-templates/cluster-class-topology-example.yaml
new file mode 100644
index 00000000..19eb776b
--- /dev/null
+++ b/templates/cluster-templates/cluster-class-topology-example.yaml
@@ -0,0 +1,95 @@
+apiVersion: cluster.x-k8s.io/v1beta1
+kind: Cluster
+metadata:
+  name: "test"
+  namespace: default
+spec:
+  clusterNetwork:
+    services:
+      cidrBlocks: ["10.128.0.0/12"]
+    pods:
+      cidrBlocks: ["192.168.0.0/16"]
+    serviceDomain: "cluster.local"
+  topology:
+    class: quick-start
+    controlPlane:
+      metadata: {}
+      replicas: 3
+    variables:
+      # All Variables:
+      # - name: clusterEndpointHost
+      #   value: ""
+      # - name: clusterEndpointPort
+      #   value: 443
+      # - name: clusterLoadBalancerType
+      #   value: "lb11"
+      # - name: clusterLoadBalancerExtraServices
+      #   value:
+      #     - protocol: tcp
+      #       listenPort: 8132
+      #       destinationPort: 8132
+      - name: hcloudPlacementGroups
+        value:
+          - name: control-plane
+            type: spread
+          - name: md-0
+            type: spread
+          - name: md-1
+            type: spread
+      # - name: hcloudNetwork
+      #   value:
+      #     cidrBlock: 10.0.0.0/16
+      #     enabled: false
+      #     networkZone: eu-central
+      #     subnetCidrBlock: 10.0.0.0/24
+      - name: hcloudSSHKeyName
+        value:
+          - name: k8s-internal
+      - name: region
+        value: hel1
+      # - name: hcloudControlPlaneMachineType
+      #   value: cpx31
+      # - name: hcloudControlPlaneMachineImageName
+      #   value: ubuntu-22.04
+      - name: hcloudControlPlanePlacementGroupName
+        value: control-plane
+    version: v1.25.3
+    workers:
+      machineDeployments:
+        - class: hcloud-worker
+          name: md-0
+          replicas: 1
+          failureDomain: hel1
+          variables:
+            overrides:
+              - name: hcloudWorkerMachineType
+                value: cx21
+              # - name: hcloudWorkerMachineImageName
+              #   value: ubuntu-22.04
+              - name: hcloudWorkerMachinePlacementGroupName
+                value: md-0
+        - class: hcloud-worker
+          name: md-1
+          replicas: 1
+          failureDomain: fsn1
+          variables:
+            overrides:
+              - name: hcloudWorkerMachineType
+                value: cx31
+              # - name: hcloudWorkerMachineImageName
+              #   value: ubuntu-22.04
+              - name: hcloudWorkerMachinePlacementGroupName
+                value: md-1
+        - class: baremetal-worker
+          name: md-2
+          replicas: 3
+          variables:
+            overrides:
+              - name: bareMetalWorkerHostSelector
+                value:
+                  matchLabels:
+                    cluster: test
+              - name: bareMetalWorkerRaidEnabled
+                value: true
+              - name: bareMetalWorkerRaidLevel
+                value: 1
diff --git a/templates/cluster-templates/cluster-class.yaml b/templates/cluster-templates/cluster-class.yaml
new file mode 100644
index 00000000..6a23e8e9
--- /dev/null
+++ b/templates/cluster-templates/cluster-class.yaml
@@ -0,0 +1,854 @@
+apiVersion: cluster.x-k8s.io/v1beta1
+kind: ClusterClass
+metadata:
+  name: quick-start
+spec:
+  controlPlane:
+    ref:
+      apiVersion: controlplane.cluster.x-k8s.io/v1beta1
+      kind: KubeadmControlPlaneTemplate
+      name: quick-start-control-plane
+    machineInfrastructure:
+      ref:
+        kind: HCloudMachineTemplate
+        apiVersion: infrastructure.cluster.x-k8s.io/v1beta1
+        name: quick-start-hcloud-machinetemplate
+    machineHealthCheck:
+      maxUnhealthy: 33%
+      nodeStartupTimeout: 15m
+      unhealthyConditions:
+        - type: Ready
+          status: Unknown
+          timeout: 300s
+        - type: Ready
+          status: "False"
+          timeout: 300s
+  infrastructure:
+    ref:
+      apiVersion: infrastructure.cluster.x-k8s.io/v1beta1
+      kind: HetznerClusterTemplate
+      name: quick-start-cluster
+  workers:
+    machineDeployments:
+      - class: hcloud-worker
+        template:
+          bootstrap:
+            ref:
+              apiVersion: bootstrap.cluster.x-k8s.io/v1beta1
+              kind: KubeadmConfigTemplate
+              name: quick-start-hcloud-worker-bootstraptemplate
+          infrastructure:
+            ref:
+              apiVersion: infrastructure.cluster.x-k8s.io/v1beta1
+              kind: HCloudMachineTemplate
+              name: quick-start-hcloud-machinetemplate
+        machineHealthCheck:
+          unhealthyRange: "[0-2]"
+          nodeStartupTimeout: 10m
+          unhealthyConditions:
+            - type: Ready
+              status: Unknown
+              timeout: 300s
+            - type: Ready
+              status: "False"
+              timeout: 300s
+      - class: baremetal-worker
+        template:
+          bootstrap:
+            ref:
+              apiVersion: bootstrap.cluster.x-k8s.io/v1beta1
+              kind: KubeadmConfigTemplate
+              name: quick-start-baremetal-worker-bootstraptemplate
+          infrastructure:
+            ref:
+              apiVersion: infrastructure.cluster.x-k8s.io/v1beta1
+              kind: HetznerBareMetalMachineTemplate
+              name: quick-start-baremetal-machinetemplate
+        machineHealthCheck:
+          maxUnhealthy: 1
+          nodeStartupTimeout: 15m
+          remediationTemplate:
+            apiVersion: infrastructure.cluster.x-k8s.io/v1beta1
+            kind: HetznerBareMetalRemediationTemplate
+            name: quick-start-worker-remediation-request
+            namespace: default
+          unhealthyConditions:
+            - type: Ready
+              status: Unknown
+              timeout: 300s
+            - type: Ready
+              status: "False"
+              timeout: 300s
+  variables:
+    - name: clusterEndpointHost
+      required: false
+      schema:
+        openAPIV3Schema:
+          type: string
+          default: ""
+    - name: clusterEndpointPort
+      required: false
+      schema:
+        openAPIV3Schema:
+          type: integer
+          default: 443
+    - name: clusterLoadBalancerType
+      required: false
+      schema:
+        openAPIV3Schema:
+          type: string
+          default: "lb11"
+    - name: clusterLoadBalancerExtraServices
+      required: false
+      schema:
+        openAPIV3Schema:
+          type: array
+          default: []
+          items:
+            type: object
+            properties:
+              protocol:
+                type: string
+              listenPort:
+                type: integer
+              destinationPort:
+                type: integer
+    - name: hcloudPlacementGroups
+      required: false
+      schema:
+        openAPIV3Schema:
+          type: array
+          items:
+            type: object
+            properties:
+              name:
+                type: string
+              type:
+                type: string
+    - name: hcloudNetwork
+      required: false
+      schema:
+        openAPIV3Schema:
+          type: object
+          properties:
+            enabled:
+              type: boolean
+            cidrBlock:
+              type: string
+            subnetCidrBlock:
+              type: string
+            networkZone:
+              type: string
+    - name: hcloudSSHKeyName
+      required: true
+      schema:
+        openAPIV3Schema:
+          type: array
+          items:
+            type: object
+            properties:
+              name:
+                type: string
+    - name: region
+      required: true
+      schema:
+        openAPIV3Schema:
+          type: string
+          default: fsn1
+    - name: hcloudControlPlaneMachineType
+      required: true
+      schema:
+        openAPIV3Schema:
+          type: string
+          default: cx31
+    - name: hcloudControlPlaneMachineImageName
+      required: true
+      schema:
+        openAPIV3Schema:
+          type: string
+          default: ubuntu-22.04
+    - name: hcloudControlPlanePlacementGroupName
+      required: false
+      schema:
+        openAPIV3Schema:
+          type: string
+    - name: hcloudWorkerMachineType
+      required: true
+      schema:
+        openAPIV3Schema:
+          type: string
+          default: cx21
+    - name: hcloudWorkerMachineImageName
+      required: true
+      schema:
+        openAPIV3Schema:
+          type: string
+          default: ubuntu-22.04
+    - name: hcloudWorkerMachinePlacementGroupName
+      required: false
+      schema:
+        openAPIV3Schema:
+          type: string
+    - name: bareMetalWorkerHostSelector
+      required: false
+      schema:
+        openAPIV3Schema:
+          type: object
+          properties:
+            matchExpressions:
+              type: array
+              items:
+                type: object
+                properties:
+                  key:
+                    type: string
+                  operator:
+                    type: string
+                  values:
+                    type: array
+                    items:
+                      type: string
+            matchLabels:
+              type: object
+              x-kubernetes-preserve-unknown-fields: true
+    - name: bareMetalWorkerRaidEnabled
+      required: false
+      schema:
+        openAPIV3Schema:
+          type: boolean
+    - name: bareMetalWorkerRaidLevel
+      required: false
+      schema:
+        openAPIV3Schema:
+          type: integer
+          default: 1
+  patches:
+    - name: HetznerClusterTemplateGeneral
+      definitions:
+        - selector:
+            apiVersion: infrastructure.cluster.x-k8s.io/v1beta1
+            kind: HetznerClusterTemplate
+            matchResources:
+              infrastructureCluster: true
+          jsonPatches:
+            - op: replace
+              path: "/spec/template/spec/controlPlaneEndpoint/host"
+              valueFrom:
+                variable: clusterEndpointHost
+            - op: replace
+              path: "/spec/template/spec/controlPlaneEndpoint/port"
+              valueFrom:
+                variable: clusterEndpointPort
+            - op: add
+              path: "/spec/template/spec/controlPlaneLoadBalancer/type"
+              valueFrom:
+                variable: clusterLoadBalancerType
+            - op: add
+              path: "/spec/template/spec/controlPlaneLoadBalancer/extraServices"
+              valueFrom:
+                variable: clusterLoadBalancerExtraServices
+            - op: add
+              path: "/spec/template/spec/hcloudPlacementGroups"
+              valueFrom:
+                variable: hcloudPlacementGroups
+            - op: add
+              path: "/spec/template/spec/sshKeys/hcloud"
+              valueFrom:
+                variable: hcloudSSHKeyName
+            - op: add
+              path: "/spec/template/spec/controlPlaneRegions"
+              valueFrom:
+                template: "[{{ .region | quote }}]"
+            - op: add
+              path: "/spec/template/spec/controlPlaneLoadBalancer/region"
+              valueFrom:
+                variable: region
+    - name: HetznerClusterTemplateNetwork
+      enabledIf: "{{ if .hcloudNetwork.enabled }}true{{end}}"
+      definitions:
+        - selector:
+            apiVersion: infrastructure.cluster.x-k8s.io/v1beta1
+            kind: HetznerClusterTemplate
+            matchResources:
+              infrastructureCluster: true
+          jsonPatches:
+            - op: add
+              path: "/spec/template/spec/hcloudNetwork"
+              valueFrom:
+                variable: hcloudNetwork
+    - name: HCloudMachineTemplateControlPlane
+      definitions:
+        - selector:
+            apiVersion: infrastructure.cluster.x-k8s.io/v1beta1
+            kind: HCloudMachineTemplate
+            matchResources:
+              controlPlane: true
+          jsonPatches:
+            - op: replace
+              path: "/spec/template/spec/type"
+              valueFrom:
+                variable: hcloudControlPlaneMachineType
+            - op: replace
+              path: "/spec/template/spec/imageName"
+              valueFrom:
+                variable: hcloudControlPlaneMachineImageName
+            - op: add
+              path: "/spec/template/spec/placementGroupName"
+              valueFrom:
+                variable: hcloudControlPlanePlacementGroupName
+    - name: HCloudMachineTemplateWorker
+      definitions:
+        - selector:
+            apiVersion: infrastructure.cluster.x-k8s.io/v1beta1
+            kind: HCloudMachineTemplate
+            matchResources:
+              machineDeploymentClass:
+                names:
+                  - hcloud-worker
+          jsonPatches:
+            - op: replace
+              path: "/spec/template/spec/type"
+              valueFrom:
+                variable: hcloudWorkerMachineType
+            - op: replace
+              path: "/spec/template/spec/imageName"
+              valueFrom:
+                variable: hcloudWorkerMachineImageName
+            - op: add
+              path: "/spec/template/spec/placementGroupName"
+              valueFrom:
+                variable: hcloudWorkerMachinePlacementGroupName
+    - name: HetznerBareMetalMachineTemplateWorker
+      definitions:
+        - selector:
+            apiVersion: infrastructure.cluster.x-k8s.io/v1beta1
+            kind: HetznerBareMetalMachineTemplate
+            matchResources:
+              machineDeploymentClass:
+                names:
+                  - baremetal-worker
+          jsonPatches:
+            - op: add
+              path: "/spec/template/spec/hostSelector"
+              valueFrom:
+                variable: bareMetalWorkerHostSelector
+            - op: replace
+              path: "/spec/template/spec/installImage/swraid"
+              valueFrom:
+                template: "{{ if .bareMetalWorkerRaidEnabled }}1{{else}}0{{end}}"
+            - op: replace
+              path: "/spec/template/spec/installImage/swraidLevel"
+              valueFrom:
+                variable: bareMetalWorkerRaidLevel
+---
+apiVersion: infrastructure.cluster.x-k8s.io/v1beta1
+kind: HetznerClusterTemplate
+metadata:
+  name: quick-start-cluster
+spec:
+  template:
+    spec:
+      controlPlaneEndpoint:
+        host: ""
+        port: 443
+      controlPlaneLoadBalancer:
+        region: fsn1
+      controlPlaneRegions: []
+      hcloudNetwork:
+        enabled: false
+      hetznerSecretRef:
+        key:
+          hcloudToken: hcloud
+          hetznerRobotPassword: robot-password
+          hetznerRobotUser: robot-user
+        name: hetzner
+      sshKeys:
+        hcloud: []
+        robotRescueSecretRef:
+          key:
+            name: sshkey-name
+            privateKey: ssh-privatekey
+            publicKey: ssh-publickey
+          name: robot-ssh
+---
+apiVersion: infrastructure.cluster.x-k8s.io/v1beta1
+kind: HCloudMachineTemplate
+metadata:
+  name: quick-start-hcloud-machinetemplate
+spec:
+  template:
+    spec:
+      imageName: ubuntu-22.04
+      type: cx21
+---
+apiVersion: controlplane.cluster.x-k8s.io/v1beta1
+kind: KubeadmControlPlaneTemplate
+metadata:
+  name: quick-start-control-plane
+spec:
+  template:
+    spec:
+      kubeadmConfigSpec:
+        clusterConfiguration:
+          apiServer:
+            extraArgs:
+              authorization-mode: Node,RBAC
+              client-ca-file: /etc/kubernetes/pki/ca.crt
+              cloud-provider: external
+              default-not-ready-toleration-seconds: "45"
+              default-unreachable-toleration-seconds: "45"
+              enable-aggregator-routing: "true"
+              enable-bootstrap-token-auth: "true"
+              encryption-provider-config: /etc/kubernetes/encryption-provider.yaml
+              etcd-cafile: /etc/kubernetes/pki/etcd/ca.crt
+              etcd-certfile: /etc/kubernetes/pki/etcd/server.crt
+              etcd-keyfile: /etc/kubernetes/pki/etcd/server.key
+              kubelet-client-certificate: /etc/kubernetes/pki/apiserver-kubelet-client.crt
+              kubelet-client-key: /etc/kubernetes/pki/apiserver-kubelet-client.key
+              kubelet-preferred-address-types: ExternalIP,Hostname,InternalDNS,ExternalDNS
+              profiling: "false"
+              proxy-client-cert-file: /etc/kubernetes/pki/front-proxy-client.crt
+              proxy-client-key-file: /etc/kubernetes/pki/front-proxy-client.key
+              requestheader-allowed-names: front-proxy-client
+              requestheader-client-ca-file: /etc/kubernetes/pki/front-proxy-ca.crt
+              requestheader-extra-headers-prefix: X-Remote-Extra-
+              requestheader-group-headers: X-Remote-Group
+              requestheader-username-headers: X-Remote-User
+              service-account-key-file: /etc/kubernetes/pki/sa.pub
+              service-account-lookup: "true"
+              tls-cert-file: /etc/kubernetes/pki/apiserver.crt
+              tls-cipher-suites: TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256,TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256,TLS_ECDHE_ECDSA_WITH_CHACHA20_POLY1305,TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384,TLS_ECDHE_RSA_WITH_CHACHA20_POLY1305,TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384,TLS_RSA_WITH_AES_256_GCM_SHA384,TLS_RSA_WITH_AES_128_GCM_SHA256
+              tls-private-key-file: /etc/kubernetes/pki/apiserver.key
+            extraVolumes:
+              - hostPath: /etc/kubernetes/encryption-provider.yaml
+                mountPath: /etc/kubernetes/encryption-provider.yaml
+                name: encryption-provider
+          controllerManager:
+            extraArgs:
+              allocate-node-cidrs: "true"
+              authentication-kubeconfig: /etc/kubernetes/controller-manager.conf
+              authorization-kubeconfig: /etc/kubernetes/controller-manager.conf
+              bind-address: 0.0.0.0
+              cloud-provider: external
+              cluster-signing-cert-file: /etc/kubernetes/pki/ca.crt
+              cluster-signing-duration: 6h0m0s
+              cluster-signing-key-file: /etc/kubernetes/pki/ca.key
+              kubeconfig: /etc/kubernetes/controller-manager.conf
+              pod-eviction-timeout: 2m
+              profiling: "false"
+              requestheader-client-ca-file: /etc/kubernetes/pki/front-proxy-ca.crt
+              root-ca-file: /etc/kubernetes/pki/ca.crt
+              secure-port: "10257"
+              service-account-private-key-file: /etc/kubernetes/pki/sa.key
+              terminated-pod-gc-threshold: "10"
+              use-service-account-credentials: "true"
+          etcd:
+            local:
+              dataDir: /var/lib/etcd
+              extraArgs:
+                auto-tls: "false"
+                cert-file: /etc/kubernetes/pki/etcd/server.crt
+                cipher-suites: TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256,TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256,TLS_ECDHE_ECDSA_WITH_CHACHA20_POLY1305,TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384,TLS_ECDHE_RSA_WITH_CHACHA20_POLY1305,TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384,TLS_RSA_WITH_AES_256_GCM_SHA384,TLS_RSA_WITH_AES_128_GCM_SHA256
+                client-cert-auth: "true"
+                key-file: /etc/kubernetes/pki/etcd/server.key
+                peer-auto-tls: "false"
+                peer-client-cert-auth: "true"
+                trusted-ca-file: /etc/kubernetes/pki/etcd/ca.crt
+          scheduler:
+            extraArgs:
+              bind-address: 0.0.0.0
+              kubeconfig: /etc/kubernetes/scheduler.conf
+              profiling: "false"
+              secure-port: "10259"
+        files:
+          - content: |
+              apiVersion: apiserver.config.k8s.io/v1
+              kind: EncryptionConfiguration
+              resources:
+                - resources:
+                  - secrets
+                  providers:
+                  - aescbc:
+                      keys:
+                      - name: key1
+                        secret: 8d7iAcg3/NwN9aijhtEXj5kL2NOHIgokGFjbIBfL6X0=
+                  - identity: {}
+            owner: root:root
+            path: /etc/kubernetes/encryption-provider.yaml
+            permissions: "0600"
+          - content: |
+              net.ipv4.conf.lxc*.rp_filter = 0
+            owner: root:root
+            path: /etc/sysctl.d/99-cilium.conf
+            permissions: "0744"
+          - content: |
+              overlay
+              br_netfilter
+            owner: root:root
+            path: /etc/modules-load.d/crio.conf
+            permissions: "0744"
+          - content: |
+              version = 2
+              [plugins."io.containerd.grpc.v1.cri".containerd.runtimes.runc]
+                runtime_type = "io.containerd.runc.v2"
+              [plugins."io.containerd.grpc.v1.cri".containerd.runtimes.runc.options]
+                SystemdCgroup = true
+              [plugins."io.containerd.grpc.v1.cri".containerd.runtimes.crun]
+                runtime_type = "io.containerd.runc.v2"
+              [plugins."io.containerd.grpc.v1.cri".containerd.runtimes.crun.options]
+                BinaryName = "crun"
+                Root = "/usr/local/sbin"
+                SystemdCgroup = true
+              [plugins."io.containerd.grpc.v1.cri".containerd]
+                default_runtime_name = "crun"
+              [plugins."io.containerd.runtime.v1.linux"]
+                runtime = "crun"
+                runtime_root = "/usr/local/sbin"
+            owner: root:root
+            path: /etc/containerd/config.toml
+            permissions: "0744"
+          - content: |
+              net.bridge.bridge-nf-call-iptables  = 1
+              net.bridge.bridge-nf-call-ip6tables = 1
+              net.ipv4.ip_forward                 = 1
+            owner: root:root
+            path: /etc/sysctl.d/99-kubernetes-cri.conf
+            permissions: "0744"
+          - content: |
+              vm.overcommit_memory=1
+              kernel.panic=10
+              kernel.panic_on_oops=1
+            owner: root:root
+            path: /etc/sysctl.d/99-kubelet.conf
+            permissions: "0744"
+          - content: |
+              nameserver 1.1.1.1
+              nameserver 1.0.0.1
+              nameserver 2606:4700:4700::1111
+            owner: root:root
+            path: /etc/kubernetes/resolv.conf
+            permissions: "0744"
+        initConfiguration:
+          nodeRegistration:
+            kubeletExtraArgs:
+              anonymous-auth: "false"
+              authentication-token-webhook: "true"
+              authorization-mode: Webhook
+              cloud-provider: external
+              event-qps: "5"
+              kubeconfig: /etc/kubernetes/kubelet.conf
+              max-pods: "120"
+              read-only-port: "0"
+              resolv-conf: /etc/kubernetes/resolv.conf
+              rotate-server-certificates: "true"
+              tls-cipher-suites: TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256,TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256,TLS_ECDHE_ECDSA_WITH_CHACHA20_POLY1305,TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384,TLS_ECDHE_RSA_WITH_CHACHA20_POLY1305,TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384,TLS_RSA_WITH_AES_256_GCM_SHA384,TLS_RSA_WITH_AES_128_GCM_SHA256
+        joinConfiguration:
+          nodeRegistration:
+            kubeletExtraArgs:
+              anonymous-auth: "false"
+              authentication-token-webhook: "true"
+              authorization-mode: Webhook
+              cloud-provider: external
+              event-qps: "5"
+              kubeconfig: /etc/kubernetes/kubelet.conf
+              max-pods: "120"
+              read-only-port: "0"
+              resolv-conf: /etc/kubernetes/resolv.conf
+              rotate-server-certificates: "true"
+              tls-cipher-suites: TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256,TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256,TLS_ECDHE_ECDSA_WITH_CHACHA20_POLY1305,TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384,TLS_ECDHE_RSA_WITH_CHACHA20_POLY1305,TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384,TLS_RSA_WITH_AES_256_GCM_SHA384,TLS_RSA_WITH_AES_128_GCM_SHA256
+        preKubeadmCommands:
+          - export CRUN=1.6
+          - export CONTAINERD=1.6.8
+          - export KUBERNETES_VERSION=1.25.3
+          - localectl set-locale LANG=en_US.UTF-8
+          - localectl set-locale LANGUAGE=en_US.UTF-8
+          - apt-get update -y
+          - apt-get -y install at jq unzip wget socat mtr logrotate apt-transport-https
+          - sed -i '/swap/d' /etc/fstab
+          - swapoff -a
+          - modprobe overlay && modprobe br_netfilter && sysctl --system
+          - wget https://github.com/containerd/containerd/releases/download/v$CONTAINERD/cri-containerd-cni-$CONTAINERD-linux-amd64.tar.gz
+          - wget https://github.com/containerd/containerd/releases/download/v$CONTAINERD/cri-containerd-cni-$CONTAINERD-linux-amd64.tar.gz.sha256sum
+          - sha256sum --check cri-containerd-cni-$CONTAINERD-linux-amd64.tar.gz.sha256sum
+          - tar --no-overwrite-dir -C / -xzf cri-containerd-cni-$CONTAINERD-linux-amd64.tar.gz
+          - rm -f cri-containerd-cni-$CONTAINERD-linux-amd64.tar.gz cri-containerd-cni-$CONTAINERD-linux-amd64.tar.gz.sha256sum
+          - wget https://github.com/containers/crun/releases/download/$CRUN/crun-$CRUN-linux-amd64
+            -O /usr/local/sbin/crun && chmod +x /usr/local/sbin/crun
+          - rm -f /etc/cni/net.d/10-containerd-net.conflist
+          - chmod -R 644 /etc/cni && chown -R root:root /etc/cni
+          - systemctl daemon-reload && systemctl enable containerd && systemctl start containerd
+          - curl -s https://packages.cloud.google.com/apt/doc/apt-key.gpg | sudo apt-key
+            add -
+          - echo "deb https://apt.kubernetes.io/ kubernetes-xenial main" | sudo tee -a /etc/apt/sources.list.d/kubernetes.list
+          - apt-get update
+          - apt-get install -y kubelet=$KUBERNETES_VERSION-00 kubeadm=$KUBERNETES_VERSION-00
+            kubectl=$KUBERNETES_VERSION-00  bash-completion && apt-mark hold kubelet kubectl
+            kubeadm && systemctl enable kubelet
+          - kubeadm config images pull --kubernetes-version $KUBERNETES_VERSION
+          - echo 'source <(kubectl completion bash)' >>~/.bashrc
+          - echo 'export KUBECONFIG=/etc/kubernetes/admin.conf' >>~/.bashrc
+          - apt-get -y autoremove && apt-get -y clean all
+---
+apiVersion: bootstrap.cluster.x-k8s.io/v1beta1
+kind: KubeadmConfigTemplate
+metadata:
+  name: quick-start-hcloud-worker-bootstraptemplate
+spec:
+  template:
+    spec:
+      files:
+        - content: |
+            net.ipv4.conf.lxc*.rp_filter = 0
+          owner: root:root
+          path: /etc/sysctl.d/99-cilium.conf
+          permissions: "0744"
+        - content: |
+            overlay
+            br_netfilter
+          owner: root:root
+          path: /etc/modules-load.d/crio.conf
+          permissions: "0744"
+        - content: |
+            version = 2
+            [plugins."io.containerd.grpc.v1.cri".containerd.runtimes.runc]
+              runtime_type = "io.containerd.runc.v2"
+            [plugins."io.containerd.grpc.v1.cri".containerd.runtimes.runc.options]
+              SystemdCgroup = true
+            [plugins."io.containerd.grpc.v1.cri".containerd.runtimes.crun]
+              runtime_type = "io.containerd.runc.v2"
+            [plugins."io.containerd.grpc.v1.cri".containerd.runtimes.crun.options]
+              BinaryName = "crun"
+              Root = "/usr/local/sbin"
+              SystemdCgroup = true
+            [plugins."io.containerd.grpc.v1.cri".containerd]
+              default_runtime_name = "crun"
+            [plugins."io.containerd.runtime.v1.linux"]
+              runtime = "crun"
+              runtime_root = "/usr/local/sbin"
+          owner: root:root
+          path: /etc/containerd/config.toml
+          permissions: "0744"
+        - content: |
+            net.bridge.bridge-nf-call-iptables  = 1
+            net.bridge.bridge-nf-call-ip6tables = 1
+            net.ipv4.ip_forward                 = 1
+          owner: root:root
+          path: /etc/sysctl.d/99-kubernetes-cri.conf
+          permissions: "0744"
+        - content: |
+            vm.overcommit_memory=1
+            kernel.panic=10
+            kernel.panic_on_oops=1
+          owner: root:root
+          path: /etc/sysctl.d/99-kubelet.conf
+          permissions: "0744"
+        - content: |
+            nameserver 1.1.1.1
+            nameserver 1.0.0.1
+            nameserver 2606:4700:4700::1111
+          owner: root:root
+          path: /etc/kubernetes/resolv.conf
+          permissions: "0744"
+      joinConfiguration:
+        nodeRegistration:
+          kubeletExtraArgs:
+            anonymous-auth: "false"
+            authentication-token-webhook: "true"
+            authorization-mode: Webhook
+            cloud-provider: external
+            event-qps: "5"
+            kubeconfig: /etc/kubernetes/kubelet.conf
+            max-pods: "220"
+            read-only-port: "0"
+            resolv-conf: /etc/kubernetes/resolv.conf
+            rotate-server-certificates: "true"
+            tls-cipher-suites: TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256,TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256,TLS_ECDHE_ECDSA_WITH_CHACHA20_POLY1305,TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384,TLS_ECDHE_RSA_WITH_CHACHA20_POLY1305,TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384,TLS_RSA_WITH_AES_256_GCM_SHA384,TLS_RSA_WITH_AES_128_GCM_SHA256
+      preKubeadmCommands:
+        - export CRUN=1.7.2
+        - export CONTAINERD=1.6.10
+        - export KUBERNETES_VERSION=1.25.3
+        - localectl set-locale LANG=en_US.UTF-8
+        - localectl set-locale LANGUAGE=en_US.UTF-8
+        - apt-get update -y
+        - apt-get -y install at jq unzip wget socat mtr logrotate apt-transport-https
+        - sed -i '/swap/d' /etc/fstab
+        - swapoff -a
+        - modprobe overlay && modprobe br_netfilter && sysctl --system
+        - wget https://github.com/containerd/containerd/releases/download/v$CONTAINERD/cri-containerd-cni-$CONTAINERD-linux-amd64.tar.gz
+        - wget https://github.com/containerd/containerd/releases/download/v$CONTAINERD/cri-containerd-cni-$CONTAINERD-linux-amd64.tar.gz.sha256sum
+        - sha256sum --check cri-containerd-cni-$CONTAINERD-linux-amd64.tar.gz.sha256sum
+        - tar --no-overwrite-dir -C / -xzf cri-containerd-cni-$CONTAINERD-linux-amd64.tar.gz
+        - rm -f cri-containerd-cni-$CONTAINERD-linux-amd64.tar.gz cri-containerd-cni-$CONTAINERD-linux-amd64.tar.gz.sha256sum
+        - wget https://github.com/containers/crun/releases/download/$CRUN/crun-$CRUN-linux-amd64
+          -O /usr/local/sbin/crun && chmod +x /usr/local/sbin/crun
+        - rm -f /etc/cni/net.d/10-containerd-net.conflist
+        - chmod -R 644 /etc/cni && chown -R root:root /etc/cni
+        - systemctl daemon-reload && systemctl enable containerd && systemctl start
+          containerd
+        - curl -s https://packages.cloud.google.com/apt/doc/apt-key.gpg | sudo apt-key
+          add -
+        - echo "deb https://apt.kubernetes.io/ kubernetes-xenial main" | sudo tee -a
+          /etc/apt/sources.list.d/kubernetes.list
+        - apt-get update
+        - apt-get install -y kubelet=$KUBERNETES_VERSION-00 kubeadm=$KUBERNETES_VERSION-00
+          kubectl=$KUBERNETES_VERSION-00  bash-completion && apt-mark hold kubelet kubectl
+          kubeadm && systemctl enable kubelet
+        - kubeadm config images pull --kubernetes-version $KUBERNETES_VERSION
+        - echo 'source <(kubectl completion bash)' >>~/.bashrc
+        - echo 'export KUBECONFIG=/etc/kubernetes/admin.conf' >>~/.bashrc
+        - apt-get -y autoremove && apt-get -y clean all
+---
+apiVersion: infrastructure.cluster.x-k8s.io/v1beta1
+kind: HetznerBareMetalRemediationTemplate
+metadata:
+  name: quick-start-worker-remediation-request
+spec:
+  template:
+    spec:
+      strategy:
+        retryLimit: 2
+        timeout: 300s
+        type: Reboot
+---
+apiVersion: infrastructure.cluster.x-k8s.io/v1beta1
+kind: HetznerBareMetalMachineTemplate
+metadata:
+  name: quick-start-baremetal-machinetemplate
+spec:
+  template:
+    spec:
+      installImage:
+        swraid: 0
+        swraidLevel: 1
+        image:
+          path: /root/.oldroot/nfs/install/../images/Ubuntu-2004-focal-64-minimal-hwe.tar.gz
+        partitions:
+          - fileSystem: ext4
+            mount: /boot
+            size: 1024M
+          - fileSystem: ext4
+            mount: /
+            size: all
+        postInstallScript: |
+          #!/bin/bash
+          mkdir -p /etc/cloud/cloud.cfg.d && touch /etc/cloud/cloud.cfg.d/99-custom-networking.cfg
+          echo "network: { config: disabled }" > /etc/cloud/cloud.cfg.d/99-custom-networking.cfg
+          apt-get update && apt-get install -y cloud-init apparmor apparmor-utils
+      sshSpec:
+        portAfterCloudInit: 22
+        portAfterInstallImage: 22
+        secretRef:
+          key:
+            name: sshkey-name
+            privateKey: ssh-privatekey
+            publicKey: ssh-publickey
+          name: robot-ssh
+---
+apiVersion: bootstrap.cluster.x-k8s.io/v1beta1
+kind: KubeadmConfigTemplate
+metadata:
+  name: quick-start-baremetal-worker-bootstraptemplate
+spec:
+  template:
+    spec:
+      files:
+        - content: |
+            net.ipv4.conf.lxc*.rp_filter = 0
+          owner: root:root
+          path: /etc/sysctl.d/99-cilium.conf
+          permissions: "0744"
+        - content: |
+            overlay
+            br_netfilter
+          owner: root:root
+          path: /etc/modules-load.d/crio.conf
+          permissions: "0744"
+        - content: |
+            version = 2
+            [plugins."io.containerd.grpc.v1.cri".containerd.runtimes.runc]
+              runtime_type = "io.containerd.runc.v2"
+            [plugins."io.containerd.grpc.v1.cri".containerd.runtimes.runc.options]
+              SystemdCgroup = true
+            [plugins."io.containerd.grpc.v1.cri".containerd.runtimes.crun]
+              runtime_type = "io.containerd.runc.v2"
+            [plugins."io.containerd.grpc.v1.cri".containerd.runtimes.crun.options]
+              BinaryName = "crun"
+              Root = "/usr/local/sbin"
+              SystemdCgroup = true
+            [plugins."io.containerd.grpc.v1.cri".containerd]
+              default_runtime_name = "crun"
+            [plugins."io.containerd.runtime.v1.linux"]
+              runtime = "crun"
+              runtime_root = "/usr/local/sbin"
+          owner: root:root
+          path: /etc/containerd/config.toml
+          permissions: "0744"
+        - content: |
+            net.bridge.bridge-nf-call-iptables  = 1
+            net.bridge.bridge-nf-call-ip6tables = 1
+            net.ipv4.ip_forward                 = 1
+          owner: root:root
+          path: /etc/sysctl.d/99-kubernetes-cri.conf
+          permissions: "0744"
+        - content: |
+            vm.overcommit_memory=1
+            kernel.panic=10
+            kernel.panic_on_oops=1
+          owner: root:root
+          path: /etc/sysctl.d/99-kubelet.conf
+          permissions: "0744"
+        - content: |
+            nameserver 1.1.1.1
+            nameserver 1.0.0.1
+            nameserver 2606:4700:4700::1111
+          owner: root:root
+          path: /etc/kubernetes/resolv.conf
+          permissions: "0744"
+      joinConfiguration:
+        nodeRegistration:
+          kubeletExtraArgs:
+            anonymous-auth: "false"
+            authentication-token-webhook: "true"
+            authorization-mode: Webhook
+            cloud-provider: external
+            event-qps: "5"
+            kubeconfig: /etc/kubernetes/kubelet.conf
+            max-pods: "220"
+            read-only-port: "0"
+            resolv-conf: /etc/kubernetes/resolv.conf
+            rotate-server-certificates: "true"
+            tls-cipher-suites: TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256,TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256,TLS_ECDHE_ECDSA_WITH_CHACHA20_POLY1305,TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384,TLS_ECDHE_RSA_WITH_CHACHA20_POLY1305,TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384,TLS_RSA_WITH_AES_256_GCM_SHA384,TLS_RSA_WITH_AES_128_GCM_SHA256
+      preKubeadmCommands:
+        - export CRUN=1.7.2
+        - export CONTAINERD=1.6.10
+        - export KUBERNETES_VERSION=1.25.3
+        - localectl set-locale LANG=en_US.UTF-8
+        - localectl set-locale LANGUAGE=en_US.UTF-8
+        - apt-get update -y
+        - apt-get -y install at jq unzip wget socat mtr logrotate apt-transport-https
+        - sed -i '/swap/d' /etc/fstab
+        - swapoff -a
+        - modprobe overlay && modprobe br_netfilter && sysctl --system
+        - wget https://github.com/containerd/containerd/releases/download/v$CONTAINERD/cri-containerd-cni-$CONTAINERD-linux-amd64.tar.gz
+        - wget https://github.com/containerd/containerd/releases/download/v$CONTAINERD/cri-containerd-cni-$CONTAINERD-linux-amd64.tar.gz.sha256sum
+        - sha256sum --check cri-containerd-cni-$CONTAINERD-linux-amd64.tar.gz.sha256sum
+        - tar --no-overwrite-dir -C / -xzf cri-containerd-cni-$CONTAINERD-linux-amd64.tar.gz
+        - rm -f cri-containerd-cni-$CONTAINERD-linux-amd64.tar.gz cri-containerd-cni-$CONTAINERD-linux-amd64.tar.gz.sha256sum
+        - wget https://github.com/containers/crun/releases/download/$CRUN/crun-$CRUN-linux-amd64
+          -O /usr/local/sbin/crun && chmod +x /usr/local/sbin/crun
+        - rm -f /etc/cni/net.d/10-containerd-net.conflist
+        - chmod -R 644 /etc/cni && chown -R root:root /etc/cni
+        - systemctl daemon-reload && systemctl enable containerd && systemctl start
+          containerd
+        - curl -s https://packages.cloud.google.com/apt/doc/apt-key.gpg | sudo apt-key
+          add -
+        - echo "deb https://apt.kubernetes.io/ kubernetes-xenial main" | sudo tee -a
+          /etc/apt/sources.list.d/kubernetes.list
+        - apt-get update
+        - apt-get install -y kubelet=$KUBERNETES_VERSION-00 kubeadm=$KUBERNETES_VERSION-00
+          kubectl=$KUBERNETES_VERSION-00  bash-completion && apt-mark hold kubelet kubectl
+          kubeadm && systemctl enable kubelet
+        - kubeadm config images pull --kubernetes-version $KUBERNETES_VERSION
+        - echo 'source <(kubectl completion bash)' >>~/.bashrc
+        - echo 'export KUBECONFIG=/etc/kubernetes/admin.conf' >>~/.bashrc
+        - apt-get -y autoremove && apt-get -y clean all


Test patch:
diff --git a/test/helpers/envtest.go b/test/helpers/envtest.go
index ef1e26c6..7693e056 100644
--- a/test/helpers/envtest.go
+++ b/test/helpers/envtest.go
@@ -150,13 +150,13 @@ func NewTestEnvironment() *TestEnvironment {
 	if err := (&infrav1.HCloudMachine{}).SetupWebhookWithManager(mgr); err != nil {
 		klog.Fatalf("failed to set up webhook with manager for HCloudMachine: %s", err)
 	}
-	if err := (&infrav1.HCloudMachineTemplate{}).SetupWebhookWithManager(mgr); err != nil {
+	if err := (&infrav1.HCloudMachineTemplateWebhook{}).SetupWebhookWithManager(mgr); err != nil {
 		klog.Fatalf("failed to set up webhook with manager for HCloudMachineTemplate: %s", err)
 	}
 	if err := (&infrav1.HetznerBareMetalMachine{}).SetupWebhookWithManager(mgr); err != nil {
 		klog.Fatalf("failed to set up webhook with manager for HetznerBareMetalMachine: %s", err)
 	}
-	if err := (&infrav1.HetznerBareMetalMachineTemplate{}).SetupWebhookWithManager(mgr); err != nil {
+	if err := (&infrav1.HetznerBareMetalMachineTemplateWebhook{}).SetupWebhookWithManager(mgr); err != nil {
 		klog.Fatalf("failed to set up webhook with manager for HetznerBareMetalMachineTemplate: %s", err)
 	}
 	if err := (&infrav1.HetznerBareMetalHost{}).SetupWebhookWithManager(mgr); err != nil {