Repository: kurator-dev/kurator
Pull request #338 (2023-06-20 12:33:23)

customcluster: init webhook
**What type of PR is this?**


/kind api-change


/kind feature


**What this PR does / why we need it**:

 init validator webhook for customcluster

part of #336 


**Which issue(s) this PR fixes**:
Fixes #

**Special notes for your reviewer**:

**Does this PR introduce a user-facing change?**:
<!--
If no, just write "NONE" in the release-note block below.
If yes, a release note is required.
-->
```release-note

```



netlify[bot]: ### <span aria-hidden="true">👷</span> Deploy Preview for *kurator-dev* processing.


|  Name | Link |
|:-:|------------------------|
|<span aria-hidden="true">🔨</span> Latest commit | 82966d0a53a79af4207df114aad02bd7082862f9 |
|<span aria-hidden="true">🔍</span> Latest deploy log | https://app.netlify.com/sites/kurator-dev/deploys/64919c952334bc0007b7eb54 |
kurator-bot: [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 kevin-wangzefeng 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=kurator-dev%2Fkurator).

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

- **[OWNERS](https://github.com/kurator-dev/kurator/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":["kevin-wangzefeng"]} -->
zirain: IMO, you should seperate this PR into two, one for API changes, another for the webhook.
Xieql: > IMO, you should seperate this PR into two, one for API changes, another for the webhook.

Alright, I will submit separate PRs later. The API changes I made this time are just a, so I think it should be ok?


Xieql: /kind api-change

/kind feature

Patch:
diff --git a/cmd/cluster-operator/customcluster/customcluster.go b/cmd/cluster-operator/customcluster/customcluster.go
index 94274c1..47d1c9a 100644
--- a/cmd/cluster-operator/customcluster/customcluster.go
+++ b/cmd/cluster-operator/customcluster/customcluster.go
@@ -24,6 +24,7 @@ import (
 
 	"kurator.dev/kurator/cmd/cluster-operator/options"
 	clusteroperator "kurator.dev/kurator/pkg/cluster-operator"
+	"kurator.dev/kurator/pkg/webhooks"
 )
 
 var log = ctrl.Log.WithName("custom_cluster")
@@ -47,5 +48,12 @@ func InitControllers(ctx context.Context, opts *options.Options, mgr ctrl.Manage
 		return err
 	}
 
+	if err := (&webhooks.CustomClusterWebhook{
+		Client: mgr.GetClient(),
+	}).SetupWebhookWithManager(mgr); err != nil {
+		log.Error(err, "unable to create CustomCluster webhook", "Webhook", "CustomCluster")
+		return err
+	}
+
 	return nil
 }
diff --git a/docs/content/en/references/infra_v1alpha1_types.html b/docs/content/en/references/infra_v1alpha1_types.html
index 142d8b0..e26f4c5 100644
--- a/docs/content/en/references/infra_v1alpha1_types.html
+++ b/docs/content/en/references/infra_v1alpha1_types.html
@@ -52,7 +52,6 @@ Kubernetes meta/v1.ObjectMeta
 </em>
 </td>
 <td>
-<em>(Optional)</em>
 Refer to the Kubernetes API documentation for the fields of the
 <code>metadata</code> field.
 </td>
@@ -79,7 +78,6 @@ Kubernetes core/v1.ObjectReference
 </em>
 </td>
 <td>
-<em>(Optional)</em>
 <p>MachineRef is the reference of nodes for provisioning a kurator cluster.</p>
 </td>
 </tr>
@@ -106,6 +104,7 @@ ControlPlaneConfig
 </em>
 </td>
 <td>
+<em>(Optional)</em>
 <p>ControlPlaneConfig contains control plane configuration.</p>
 </td>
 </tr>
@@ -254,7 +253,7 @@ string
 </em>
 </td>
 <td>
-<p>Type is the type of CNI. The default value is calico and can be [&ldquo;calico&rdquo;, &ldquo;cilium&rdquo;, &ldquo;canal&rdquo;, &ldquo;flannel&rdquo;]</p>
+<p>Type is the type of CNI. The supported types are: calico, canal, cilium, flannel, kube-ovn, kube-router, macvlan, weave</p>
 </td>
 </tr>
 </tbody>
@@ -285,7 +284,7 @@ string
 </em>
 </td>
 <td>
-<p>same as <code>ControlPlaneEndpoint</code></p>
+<p>same as <code>ControlPlaneEndpoint</code> (e.g. 192.x.x.0).</p>
 </td>
 </tr>
 <tr>
@@ -297,7 +296,7 @@ string
 </td>
 <td>
 <em>(Optional)</em>
-<p>CertSANs sets extra Subject Alternative Names for the API Server signing cert.</p>
+<p>CertSANs sets extra Subject Alternative Names for the API Server signing cert (e.g.[200.x.x.1,200.x.x.2,kubernetes.default.svc.cluster.local], [200.x.x.1], etc&hellip;).</p>
 </td>
 </tr>
 <tr>
@@ -308,6 +307,7 @@ string
 </em>
 </td>
 <td>
+<em>(Optional)</em>
 <p>LoadBalancerDomainName is a variable used to set the endpoint for a Kubernetes cluster
 when a load balancer (LB) is enabled. The default value is ControlPlaneConfig.Address.</p>
 </td>
@@ -349,7 +349,6 @@ Kubernetes core/v1.ObjectReference
 </em>
 </td>
 <td>
-<em>(Optional)</em>
 <p>MachineRef is the reference of nodes for provisioning a kurator cluster.</p>
 </td>
 </tr>
@@ -376,6 +375,7 @@ ControlPlaneConfig
 </em>
 </td>
 <td>
+<em>(Optional)</em>
 <p>ControlPlaneConfig contains control plane configuration.</p>
 </td>
 </tr>
diff --git a/manifests/charts/cluster-operator/crds/infrastructure.cluster.x-k8s.io_customclusters.yaml b/manifests/charts/cluster-operator/crds/infrastructure.cluster.x-k8s.io_customclusters.yaml
index b31605b..124e877 100644
--- a/manifests/charts/cluster-operator/crds/infrastructure.cluster.x-k8s.io_customclusters.yaml
+++ b/manifests/charts/cluster-operator/crds/infrastructure.cluster.x-k8s.io_customclusters.yaml
@@ -48,8 +48,9 @@ spec:
                 description: CNIConfig is the configuration for the CNI of the cluster.
                 properties:
                   type:
-                    description: Type is the type of CNI. The default value is calico
-                      and can be ["calico", "cilium", "canal", "flannel"]
+                    description: 'Type is the type of CNI. The supported types are:
+                      calico, canal, cilium, flannel, kube-ovn, kube-router, macvlan,
+                      weave'
                     type: string
                 required:
                 - type
@@ -58,11 +59,12 @@ spec:
                 description: ControlPlaneConfig contains control plane configuration.
                 properties:
                   address:
-                    description: same as `ControlPlaneEndpoint`
+                    description: same as `ControlPlaneEndpoint` (e.g. 192.x.x.0).
                     type: string
                   certSANs:
                     description: CertSANs sets extra Subject Alternative Names for
-                      the API Server signing cert.
+                      the API Server signing cert (e.g.[200.x.x.1,200.x.x.2,kubernetes.default.svc.cluster.local],
+                      [200.x.x.1], etc...).
                     items:
                       type: string
                     type: array
diff --git a/manifests/charts/cluster-operator/templates/webhooks.yaml b/manifests/charts/cluster-operator/templates/webhooks.yaml
index 0ea1e45..1fb0710 100644
--- a/manifests/charts/cluster-operator/templates/webhooks.yaml
+++ b/manifests/charts/cluster-operator/templates/webhooks.yaml
@@ -28,3 +28,25 @@ webhooks:
     resources:
     - clusters
   sideEffects: None
+- admissionReviewVersions:
+  - v1
+  - v1beta1
+  clientConfig:
+    service:
+      name: kurator-webhook-service
+      namespace: {{ .Release.Namespace }}
+      path: /validate-infrastructure-cluster-x-k8s-io-v1alpha1-customcluster # get this path from webhook service log
+  failurePolicy: Fail
+  matchPolicy: Equivalent
+  name: validation.customcluster.infrastructure.cluster.x-k8s.io
+  rules:
+  - apiGroups:
+    - infrastructure.cluster.x-k8s.io
+    apiVersions:
+    - v1alpha1
+    operations:
+    - CREATE
+    - UPDATE
+    resources:
+    - customclusters
+  sideEffects: None
diff --git a/pkg/apis/infra/v1alpha1/cluster.go b/pkg/apis/infra/v1alpha1/cluster.go
index 7427260..f093bdb 100644
--- a/pkg/apis/infra/v1alpha1/cluster.go
+++ b/pkg/apis/infra/v1alpha1/cluster.go
@@ -31,9 +31,7 @@ import (
 
 // CustomCluster represents the parameters for a cluster in supplement of Cluster API.
 type CustomCluster struct {
-	metav1.TypeMeta `json:",inline"`
-
-	// +optional
+	metav1.TypeMeta   `json:",inline"`
 	metav1.ObjectMeta `json:"metadata,omitempty"`
 
 	// Specification of the desired behavior of the kurator cluster.
@@ -48,29 +46,30 @@ type CustomClusterSpec struct {
 	// TODO: any other configurations that does not exist in upstream cluster api
 
 	// MachineRef is the reference of nodes for provisioning a kurator cluster.
-	// +optional
 	MachineRef *corev1.ObjectReference `json:"machineRef,omitempty"`
 
 	// CNIConfig is the configuration for the CNI of the cluster.
 	CNI CNIConfig `json:"cni"`
 
 	// ControlPlaneConfig contains control plane configuration.
-	ControlPlaneConfig ControlPlaneConfig `json:"controlPlaneConfig,omitempty"`
+	// +optional
+	ControlPlaneConfig *ControlPlaneConfig `json:"controlPlaneConfig,omitempty"`
 }
 
 type ControlPlaneConfig struct {
-	// same as `ControlPlaneEndpoint`
+	// same as `ControlPlaneEndpoint` (e.g. 192.x.x.0).
 	Address string `json:"address"`
-	// CertSANs sets extra Subject Alternative Names for the API Server signing cert.
+	// CertSANs sets extra Subject Alternative Names for the API Server signing cert (e.g.[200.x.x.1,200.x.x.2,kubernetes.default.svc.cluster.local], [200.x.x.1], etc...).
 	// +optional
 	CertSANs []string `json:"certSANs,omitempty"`
 	// LoadBalancerDomainName is a variable used to set the endpoint for a Kubernetes cluster
 	// when a load balancer (LB) is enabled. The default value is ControlPlaneConfig.Address.
+	// +optional
 	LoadBalancerDomainName string `json:"loadBalancerDomainName,omitempty"`
 }
 
 type CNIConfig struct {
-	// Type is the type of CNI. The default value is calico and can be ["calico", "cilium", "canal", "flannel"]
+	// Type is the type of CNI. The supported types are: calico, canal, cilium, flannel, kube-ovn, kube-router, macvlan, weave
 	Type string `json:"type"`
 }
 
diff --git a/pkg/apis/infra/v1alpha1/zz_generated.deepcopy.go b/pkg/apis/infra/v1alpha1/zz_generated.deepcopy.go
index 7086bd6..29e3c4b 100644
--- a/pkg/apis/infra/v1alpha1/zz_generated.deepcopy.go
+++ b/pkg/apis/infra/v1alpha1/zz_generated.deepcopy.go
@@ -134,7 +134,11 @@ func (in *CustomClusterSpec) DeepCopyInto(out *CustomClusterSpec) {
 		**out = **in
 	}
 	out.CNI = in.CNI
-	in.ControlPlaneConfig.DeepCopyInto(&out.ControlPlaneConfig)
+	if in.ControlPlaneConfig != nil {
+		in, out := &in.ControlPlaneConfig, &out.ControlPlaneConfig
+		*out = new(ControlPlaneConfig)
+		(*in).DeepCopyInto(*out)
+	}
 	return
 }
 
diff --git a/pkg/webhooks/customcluster_webhook.go b/pkg/webhooks/customcluster_webhook.go
new file mode 100644
index 0000000..eb42a89
--- /dev/null
+++ b/pkg/webhooks/customcluster_webhook.go
@@ -0,0 +1,147 @@
+/*
+Copyright Kurator 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"
+
+	corev1 "k8s.io/api/core/v1"
+	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/client"
+	"sigs.k8s.io/controller-runtime/pkg/webhook"
+
+	"kurator.dev/kurator/pkg/apis/infra/v1alpha1"
+)
+
+var _ webhook.CustomValidator = &CustomClusterWebhook{}
+
+type CustomClusterWebhook struct {
+	Client client.Reader
+}
+
+func (wh *CustomClusterWebhook) SetupWebhookWithManager(mgr ctrl.Manager) error {
+	return ctrl.NewWebhookManagedBy(mgr).
+		For(&v1alpha1.CustomCluster{}).
+		WithValidator(wh).
+		Complete()
+}
+
+func (wh *CustomClusterWebhook) ValidateCreate(_ context.Context, obj runtime.Object) error {
+	in, ok := obj.(*v1alpha1.CustomCluster)
+	if !ok {
+		return apierrors.NewBadRequest(fmt.Sprintf("expected a CustomCluster but got a %T", obj))
+	}
+
+	return wh.validate(in)
+}
+
+func (wh *CustomClusterWebhook) validate(in *v1alpha1.CustomCluster) error {
+	var allErrs field.ErrorList
+
+	allErrs = append(allErrs, validateCNI(in)...)
+	if in.Spec.MachineRef != nil {
+		allErrs = append(allErrs, validateMachineRef(in.Spec.MachineRef)...)
+	}
+	if in.Spec.ControlPlaneConfig != nil {
+		allErrs = append(allErrs, validateControlPlaneConfig(in.Spec.ControlPlaneConfig)...)
+	}
+
+	if len(allErrs) > 0 {
+		return apierrors.NewInvalid(v1alpha1.SchemeGroupVersion.WithKind("CustomCluster").GroupKind(), in.Name, allErrs)
+	}
+
+	return nil
+}
+
+var validCNIs = []string{"calico", "canal", "cilium", "flannel", "kube-ovn", "kube-router", "macvlan", "weave"}
+
+func IsValidCNI(value string) bool {
+	for _, v := range validCNIs {
+		if value == v {
+			return true
+		}
+	}
+	return false
+}
+
+func validateCNI(in *v1alpha1.CustomCluster) field.ErrorList {
+	var allErrs field.ErrorList
+	if !IsValidCNI(in.Spec.CNI.Type) {
+		allErrs = append(allErrs, field.Invalid(field.NewPath("spec", "cni", "type"), in.Spec.CNI.Type,
+			fmt.Sprintf("invalid CNI type: %v ,it should be one of %v ", in.Spec.CNI.Type, validCNIs)))
+	}
+
+	return allErrs
+}
+
+func validateMachineRef(machineRef *corev1.ObjectReference) field.ErrorList {
+	var allErrs field.ErrorList
+
+	if machineRef.Kind == "" {
+		allErrs = append(allErrs, field.Required(field.NewPath("spec", "machineRef", "kind"), "must be set"))
+	}
+	if machineRef.APIVersion == "" {
+		allErrs = append(allErrs, field.Required(field.NewPath("spec", "machineRef", "apiVersion"), "must be set"))
+	}
+	if machineRef.Name == "" {
+		allErrs = append(allErrs, field.Required(field.NewPath("spec", "machineRef", "name"), "must be set"))
+	} else {
+		allErrs = append(allErrs, validateDNS1123Domain(machineRef.Name, field.NewPath("spec", "machineRef", "name"))...)
+	}
+	if machineRef.Namespace == "" {
+		allErrs = append(allErrs, field.Required(field.NewPath("spec", "machineRef", "namespace"), "must be set"))
+	} else {
+		allErrs = append(allErrs, validateDNS1123Label(machineRef.Namespace, field.NewPath("spec", "machineRef", "namespace"))...)
+	}
+
+	return allErrs
+}
+
+func validateControlPlaneConfig(in *v1alpha1.ControlPlaneConfig) field.ErrorList {
+	var allErrs field.ErrorList
+
+	if len(in.Address) != 0 {
+		allErrs = append(allErrs, validateIP(in.Address, field.NewPath("spec", "controlPlaneConfig", "address"))...)
+	}
+
+	return allErrs
+}
+
+// ValidateUpdate is not checking for changes in parameters such as cni.type, api address, certSANs, and so on.
+// These parameters are set during cluster initialization and are not expected to change during the lifecycle of the cluster.
+// Altering these values does not impact the system because these parameters are not re-checked after cluster creation.
+func (wh *CustomClusterWebhook) ValidateUpdate(_ context.Context, oldObj, newObj runtime.Object) error {
+	_, ok := oldObj.(*v1alpha1.CustomCluster)
+	if !ok {
+		return apierrors.NewBadRequest(fmt.Sprintf("expected a CustomCluster but got a %T", oldObj))
+	}
+
+	newCustomCluster, ok := newObj.(*v1alpha1.CustomCluster)
+	if !ok {
+		return apierrors.NewBadRequest(fmt.Sprintf("expected a CustomCluster but got a %T", newObj))
+	}
+
+	return wh.validate(newCustomCluster)
+}
+
+func (wh *CustomClusterWebhook) ValidateDelete(_ context.Context, obj runtime.Object) error {
+	return nil
+}
diff --git a/pkg/webhooks/validation_utils.go b/pkg/webhooks/validation_utils.go
new file mode 100644
index 0000000..3e59271
--- /dev/null
+++ b/pkg/webhooks/validation_utils.go
@@ -0,0 +1,58 @@
+/*
+Copyright Kurator 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 (
+	"fmt"
+	"net"
+	"strings"
+
+	"k8s.io/apimachinery/pkg/util/validation"
+	"k8s.io/apimachinery/pkg/util/validation/field"
+)
+
+func validateIP(IP string, fldPath *field.Path) field.ErrorList {
+	var allErrs field.ErrorList
+	if net.ParseIP(IP) == nil {
+		allErrs = append(allErrs, field.Invalid(fldPath, IP, fmt.Sprintf("invalid ip address: %s", IP)))
+	}
+	return allErrs
+}
+
+// validateDNS1123Label checks if a string is a valid DNS1123 label.
+// A lowercase RFC 1123 label must consist of lower case alphanumeric characters or '-',
+// must start and end with an alphanumeric character,
+// and must have a maximum length of 63 characters.
+func validateDNS1123Label(label string, fldPath *field.Path) field.ErrorList {
+	var allErrs field.ErrorList
+	if errs := validation.IsDNS1123Label(label); len(errs) > 0 {
+		allErrs = append(allErrs, field.Invalid(fldPath, label, strings.Join(errs, "; ")))
+	}
+	return allErrs
+}
+
+// validateDNS1123Domain checks if a string is a valid DNS1123 domain.
+// A lowercase RFC 1123 label must consist of lower case alphanumeric characters or '-',
+// must start and end with an alphanumeric character,
+// and must have a maximum length of 253 characters.
+func validateDNS1123Domain(domain string, fldPath *field.Path) field.ErrorList {
+	var allErrs field.ErrorList
+	if errs := validation.IsDNS1123Subdomain(domain); len(errs) > 0 {
+		allErrs = append(allErrs, field.Invalid(fldPath, domain, strings.Join(errs, "; ")))
+	}
+	return allErrs
+}


Test patch:
diff --git a/pkg/webhooks/customcluster_webhook_test.go b/pkg/webhooks/customcluster_webhook_test.go
new file mode 100644
index 0000000..5dff976
--- /dev/null
+++ b/pkg/webhooks/customcluster_webhook_test.go
@@ -0,0 +1,90 @@
+/*
+Copyright Kurator 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 (
+	"io/fs"
+	"os"
+	"path"
+	"path/filepath"
+	"testing"
+
+	. "github.com/onsi/gomega"
+	"github.com/stretchr/testify/assert"
+	"sigs.k8s.io/yaml"
+
+	"kurator.dev/kurator/pkg/apis/infra/v1alpha1"
+)
+
+func TestCustomClusterValidation(t *testing.T) {
+	// read configuration from examples directory
+	caseNames := []string{"../../examples/infra/customcluster/cc-customcluster.yaml"}
+
+	wh := &CustomClusterWebhook{}
+	for _, tt := range caseNames {
+		t.Run(tt, func(t *testing.T) {
+			g := NewWithT(t)
+			c, err := readCustomCluster(tt)
+			g.Expect(err).NotTo(HaveOccurred())
+
+			err = wh.validate(c)
+			g.Expect(err).NotTo(HaveOccurred())
+		})
+	}
+}
+
+func TestInvalidCustomClusterValidation(t *testing.T) {
+	r := path.Join("testdata", "customcluster")
+	caseNames := make([]string, 0)
+	err := filepath.WalkDir(r, func(path string, d fs.DirEntry, err error) error {
+		if d.IsDir() {
+			return nil
+		}
+
+		caseNames = append(caseNames, path)
+
+		return nil
+	})
+	assert.NoError(t, err)
+
+	wh := &CustomClusterWebhook{}
+	for _, tt := range caseNames {
+		t.Run(tt, func(t *testing.T) {
+			g := NewWithT(t)
+			c, err := readCustomCluster(tt)
+			g.Expect(err).NotTo(HaveOccurred())
+
+			err = wh.validate(c)
+			g.Expect(err).To(HaveOccurred())
+			t.Logf("%v", err)
+		})
+	}
+}
+
+func readCustomCluster(filename string) (*v1alpha1.CustomCluster, error) {
+	b, err := os.ReadFile(filename)
+	if err != nil {
+		return nil, err
+	}
+
+	c := &v1alpha1.CustomCluster{}
+	if err := yaml.Unmarshal(b, c); err != nil {
+		return nil, err
+	}
+
+	return c, nil
+}
diff --git a/pkg/webhooks/testdata/customcluster/invalid-address.yaml b/pkg/webhooks/testdata/customcluster/invalid-address.yaml
new file mode 100644
index 0000000..fb684eb
--- /dev/null
+++ b/pkg/webhooks/testdata/customcluster/invalid-address.yaml
@@ -0,0 +1,15 @@
+apiVersion: infrastructure.cluster.x-k8s.io/v1alpha1
+kind: CustomCluster
+metadata:
+  name: cc-customcluster
+  namespace: default
+spec:
+  cni:
+    type: cilium
+  machineRef:
+    apiVersion: cluster.kurator.dev/v1alpha1
+    kind: CustomMachine
+    name: cc-custommachine
+    namespace: default
+  controlPlaneConfig:
+    address: 192.1.1.256
diff --git a/pkg/webhooks/testdata/customcluster/invalid-cni-type.yaml b/pkg/webhooks/testdata/customcluster/invalid-cni-type.yaml
new file mode 100644
index 0000000..0560e9c
--- /dev/null
+++ b/pkg/webhooks/testdata/customcluster/invalid-cni-type.yaml
@@ -0,0 +1,13 @@
+apiVersion: infrastructure.cluster.x-k8s.io/v1alpha1
+kind: CustomCluster
+metadata:
+  name: cc-customcluster
+  namespace: default
+spec:
+  cni:
+    type: ciilium
+  machineRef:
+    apiVersion: cluster.kurator.dev/v1alpha1
+    kind: CustomMachine
+    name: cc-custommachine
+    namespace: default
diff --git a/pkg/webhooks/testdata/customcluster/invalid-machineref-name.yaml b/pkg/webhooks/testdata/customcluster/invalid-machineref-name.yaml
new file mode 100644
index 0000000..8857193
--- /dev/null
+++ b/pkg/webhooks/testdata/customcluster/invalid-machineref-name.yaml
@@ -0,0 +1,14 @@
+apiVersion: infrastructure.cluster.x-k8s.io/v1alpha1
+kind: CustomCluster
+metadata:
+  name: cc-customcluster
+  namespace: default
+spec:
+  cni:
+    type: cilium
+  machineRef:
+    apiVersion: cluster.kurator.dev/v1alpha1
+    kind: CustomMachine
+    name: cc_custommachine
+    namespace: default
+
diff --git a/pkg/webhooks/testdata/customcluster/invalid-machineref-namespace.yaml b/pkg/webhooks/testdata/customcluster/invalid-machineref-namespace.yaml
new file mode 100644
index 0000000..c895d40
--- /dev/null
+++ b/pkg/webhooks/testdata/customcluster/invalid-machineref-namespace.yaml
@@ -0,0 +1,14 @@
+apiVersion: infrastructure.cluster.x-k8s.io/v1alpha1
+kind: CustomCluster
+metadata:
+  name: cc-customcluster
+  namespace: default
+spec:
+  cni:
+    type: cilium
+  machineRef:
+    apiVersion: cluster.kurator.dev/v1alpha1
+    kind: CustomMachine
+    name: cc-custommachine
+    namespace: defaultdefaultdefaultdefaultdefaultdefaultdefaultdefaultdefaultt
+
diff --git a/pkg/webhooks/testdata/customcluster/miss-cni-type.yaml b/pkg/webhooks/testdata/customcluster/miss-cni-type.yaml
new file mode 100644
index 0000000..4b17c72
--- /dev/null
+++ b/pkg/webhooks/testdata/customcluster/miss-cni-type.yaml
@@ -0,0 +1,12 @@
+apiVersion: infrastructure.cluster.x-k8s.io/v1alpha1
+kind: CustomCluster
+metadata:
+  name: cc-customcluster
+  namespace: default
+spec:
+  cni:
+  machineRef:
+    apiVersion: cluster.kurator.dev/v1alpha1
+    kind: CustomMachine
+    name: cc-custommachine
+    namespace: default
diff --git a/pkg/webhooks/testdata/customcluster/miss-machineref-all-value.yaml b/pkg/webhooks/testdata/customcluster/miss-machineref-all-value.yaml
new file mode 100644
index 0000000..9d3401d
--- /dev/null
+++ b/pkg/webhooks/testdata/customcluster/miss-machineref-all-value.yaml
@@ -0,0 +1,10 @@
+apiVersion: infrastructure.cluster.x-k8s.io/v1alpha1
+kind: CustomCluster
+metadata:
+  name: cc-customcluster
+  namespace: default
+spec:
+  cni:
+    type: cilium
+  machineRef:
+    name:
diff --git a/pkg/webhooks/testdata/customcluster/miss-machineref-name.yaml b/pkg/webhooks/testdata/customcluster/miss-machineref-name.yaml
new file mode 100644
index 0000000..931e257
--- /dev/null
+++ b/pkg/webhooks/testdata/customcluster/miss-machineref-name.yaml
@@ -0,0 +1,12 @@
+apiVersion: infrastructure.cluster.x-k8s.io/v1alpha1
+kind: CustomCluster
+metadata:
+  name: cc-customcluster
+  namespace: default
+spec:
+  cni:
+    type: cilium
+  machineRef:
+    apiVersion: cluster.kurator.dev/v1alpha1
+    kind: CustomMachine
+    namespace: default