Repository: kurator-dev/kurator
Pull request #334 (2023-06-15 09:29:14)

customcluster: support kubeversion to v1.26.5
**What type of PR is this?**


/kind feature


**What this PR does / why we need it**:

The previously supported kubeversion ranged from 'v1.22.0 - v1.24.6', and it has now been extended to include 'v1.22.0 - v1.26.5'.


part of #218 

**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 | c10157e942775b11945d52afde4d6df092aba3ed |
|<span aria-hidden="true">🔍</span> Latest deploy log | https://app.netlify.com/sites/kurator-dev/deploys/648ad9ec6156100008b65865 |
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"]} -->

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/docs/cluster-operator/vms-cluster-lifecycle.md b/docs/content/en/docs/cluster-operator/vms-cluster-lifecycle.md
index b1ab1e1..2daab64 100644
--- a/docs/content/en/docs/cluster-operator/vms-cluster-lifecycle.md
+++ b/docs/content/en/docs/cluster-operator/vms-cluster-lifecycle.md
@@ -389,7 +389,7 @@ spec:
       maxSurge: 1
     type: RollingUpdate
   # edit the version to desired upgrading version
-  version: v1.24.6
+  version: v1.26.5
 status:
   conditions:
   ...
diff --git a/docs/content/en/references/cluster_v1alpha1_types.html b/docs/content/en/references/cluster_v1alpha1_types.html
index d1bc471..00c83c7 100644
--- a/docs/content/en/references/cluster_v1alpha1_types.html
+++ b/docs/content/en/references/cluster_v1alpha1_types.html
@@ -8,8 +8,98 @@
 <p>Package v1alpha1 contains API Schema definitions for the cluster v1alpha1 API group</p>
 Resource Types:
 <ul class="simple"><li>
+<a href="#cluster.kurator.dev/v1alpha1.AttachedCluster">AttachedCluster</a>
+</li><li>
 <a href="#cluster.kurator.dev/v1alpha1.Cluster">Cluster</a>
 </li></ul>
+<h3 id="cluster.kurator.dev/v1alpha1.AttachedCluster">AttachedCluster
+</h3>
+<p>AttachedCluster is the schema for the external cluster that are not created by kurator.</p>
+<div class="md-typeset__scrollwrap">
+<div class="md-typeset__table td-content">
+<table>
+<thead>
+<tr>
+<th>Field</th>
+<th>Description</th>
+</tr>
+</thead>
+<tbody>
+<tr>
+<td>
+<code>apiVersion</code><br>
+string</td>
+<td>
+<code>cluster.kurator.dev/v1alpha1</code>
+</td>
+</tr>
+<tr>
+<td>
+<code>kind</code><br>
+string
+</td>
+<td>
+<code>AttachedCluster</code>
+</td>
+</tr>
+<tr>
+<td>
+<code>metadata</code><br>
+<em>
+<a href="https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.25/#objectmeta-v1-meta">
+Kubernetes meta/v1.ObjectMeta
+</a>
+</em>
+</td>
+<td>
+Refer to the Kubernetes API documentation for the fields of the
+<code>metadata</code> field.
+</td>
+</tr>
+<tr>
+<td>
+<code>spec</code><br>
+<em>
+<a href="#cluster.kurator.dev/v1alpha1.AttachedClusterSpec">
+AttachedClusterSpec
+</a>
+</em>
+</td>
+<td>
+<table>
+<tr>
+<td>
+<code>kubeconfig</code><br>
+<em>
+<a href="#cluster.kurator.dev/v1alpha1.SecretKeyRef">
+SecretKeyRef
+</a>
+</em>
+</td>
+<td>
+<em>(Optional)</em>
+<p>Kubeconfig represents the secret that contains the credential to access this cluster.</p>
+</td>
+</tr>
+</table>
+</td>
+</tr>
+<tr>
+<td>
+<code>status</code><br>
+<em>
+<a href="#cluster.kurator.dev/v1alpha1.AttachedClusterStatus">
+AttachedClusterStatus
+</a>
+</em>
+</td>
+<td>
+</td>
+</tr>
+</tbody>
+</table>
+</div>
+</div>
 <h3 id="cluster.kurator.dev/v1alpha1.Cluster">Cluster
 </h3>
 <p>Cluster is the schema for the cluster&rsquo;s API</p>
@@ -201,77 +291,6 @@ ClusterStatus
 </table>
 </div>
 </div>
-<h3 id="cluster.kurator.dev/v1alpha1.AttachedCluster">AttachedCluster
-</h3>
-<p>AttachedCluster is the schema for the external cluster that are not created by kurator.</p>
-<div class="md-typeset__scrollwrap">
-<div class="md-typeset__table td-content">
-<table>
-<thead>
-<tr>
-<th>Field</th>
-<th>Description</th>
-</tr>
-</thead>
-<tbody>
-<tr>
-<td>
-<code>metadata</code><br>
-<em>
-<a href="https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.25/#objectmeta-v1-meta">
-Kubernetes meta/v1.ObjectMeta
-</a>
-</em>
-</td>
-<td>
-Refer to the Kubernetes API documentation for the fields of the
-<code>metadata</code> field.
-</td>
-</tr>
-<tr>
-<td>
-<code>spec</code><br>
-<em>
-<a href="#cluster.kurator.dev/v1alpha1.AttachedClusterSpec">
-AttachedClusterSpec
-</a>
-</em>
-</td>
-<td>
-<table>
-<tr>
-<td>
-<code>kubeconfig</code><br>
-<em>
-<a href="#cluster.kurator.dev/v1alpha1.SecretKeyRef">
-SecretKeyRef
-</a>
-</em>
-</td>
-<td>
-<em>(Optional)</em>
-<p>Kubeconfig represents the secret that contains the credential to access this cluster.</p>
-</td>
-</tr>
-</table>
-</td>
-</tr>
-<tr>
-<td>
-<code>status</code><br>
-<em>
-<a href="#cluster.kurator.dev/v1alpha1.AttachedClusterStatus">
-AttachedClusterStatus
-</a>
-</em>
-</td>
-<td>
-</td>
-</tr>
-</tbody>
-</table>
-</div>
-</div>
 <h3 id="cluster.kurator.dev/v1alpha1.AttachedClusterSpec">AttachedClusterSpec
 </h3>
 <p>
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/examples/infra/customcluster/cc-kcp.yaml b/examples/infra/customcluster/cc-kcp.yaml
index 52e3dae..f7480ea 100644
--- a/examples/infra/customcluster/cc-kcp.yaml
+++ b/examples/infra/customcluster/cc-kcp.yaml
@@ -19,4 +19,4 @@ spec:
       kind: customMachine
       name: cc-custommachine
       namespace: default
-  version: v1.24.6
+  version: v1.26.5
diff --git a/manifests/charts/cluster-operator/crds/cluster.kurator.dev_attachedclusters.yaml b/manifests/charts/cluster-operator/crds/cluster.kurator.dev_attachedclusters.yaml
index c0a8156..ad3c81a 100644
--- a/manifests/charts/cluster-operator/crds/cluster.kurator.dev_attachedclusters.yaml
+++ b/manifests/charts/cluster-operator/crds/cluster.kurator.dev_attachedclusters.yaml
@@ -17,7 +17,12 @@ spec:
     singular: attachedcluster
   scope: Namespaced
   versions:
-  - name: v1alpha1
+  - additionalPrinterColumns:
+    - description: Indicates if the AttachedClusters are ready
+      jsonPath: .status.ready
+      name: Ready
+      type: boolean
+    name: v1alpha1
     schema:
       openAPIV3Schema:
         description: AttachedCluster is the schema for the external cluster that are
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 c1f7abc..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
@@ -18,7 +18,12 @@ spec:
     singular: customcluster
   scope: Namespaced
   versions:
-  - name: v1alpha1
+  - additionalPrinterColumns:
+    - description: Represents the current phase of customCluster actuation.
+      jsonPath: .status.phase
+      name: Ready
+      type: string
+    name: v1alpha1
     schema:
       openAPIV3Schema:
         description: CustomCluster represents the parameters for a cluster in supplement
@@ -43,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
@@ -53,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/manifests/charts/fleet-manager/templates/rbac.yaml b/manifests/charts/fleet-manager/templates/rbac.yaml
index 7b763a1..b2b13f9 100644
--- a/manifests/charts/fleet-manager/templates/rbac.yaml
+++ b/manifests/charts/fleet-manager/templates/rbac.yaml
@@ -34,6 +34,7 @@ rules:
       - patch
       - update
       - watch
+      - delete
   - apiGroups:
       - cluster.kurator.dev
     resources:
diff --git a/pkg/apis/cluster/v1alpha1/attached_cluster_types.go b/pkg/apis/cluster/v1alpha1/attached_cluster_types.go
index a9b40a8..c9d8245 100644
--- a/pkg/apis/cluster/v1alpha1/attached_cluster_types.go
+++ b/pkg/apis/cluster/v1alpha1/attached_cluster_types.go
@@ -26,6 +26,8 @@ import (
 // +kubebuilder:object:root=true
 // +kubebuilder:resource:scope=Namespaced,categories=kurator-dev
 // +kubebuilder:subresource:status
+// +kubebuilder:printcolumn:name="Ready",type="boolean",JSONPath=".status.ready",description="Indicates if the AttachedClusters are ready"
+
 // AttachedCluster is the schema for the external cluster that are not created by kurator.
 type AttachedCluster struct {
 	metav1.TypeMeta   `json:",inline"`
diff --git a/pkg/apis/infra/v1alpha1/cluster.go b/pkg/apis/infra/v1alpha1/cluster.go
index def9259..f093bdb 100644
--- a/pkg/apis/infra/v1alpha1/cluster.go
+++ b/pkg/apis/infra/v1alpha1/cluster.go
@@ -27,12 +27,11 @@ import (
 // +kubebuilder:object:root=true
 // +kubebuilder:resource:path=customclusters,shortName=cc
 // +kubebuilder:subresource:status
+// +kubebuilder:printcolumn:name="Ready",type="string",JSONPath=".status.phase",description="Represents the current phase of customCluster actuation."
 
 // 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.
@@ -47,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/cluster-operator/customcluster_controller.go b/pkg/cluster-operator/customcluster_controller.go
index 800ac61..a33ba25 100644
--- a/pkg/cluster-operator/customcluster_controller.go
+++ b/pkg/cluster-operator/customcluster_controller.go
@@ -97,13 +97,6 @@ const (
 
 	// KubeVersionPrefix is the prefix string of version of kubernetes
 	KubeVersionPrefix = "kube_version: "
-	// DefaultKubesprayVersion is the version of Kubespray used by the customCluster manager worker pod.
-	DefaultKubesprayVersion = "v2.20.0"
-	// DefaultMaxVersion is the max version of Kubernetes that is supported by DefaultKubesprayVersion.
-	DefaultMaxVersion = "v1.24.6"
-	// DefaultMinVersion is the min version of Kubernetes that is supported by DefaultKubesprayVersion.
-	DefaultMinVersion = "v1.22.0"
-	// TODO: support more version to remove limited DefaultMaxVersion and DefaultMinVersion
 )
 
 // SetupWithManager sets up the controller with the Manager.
@@ -270,11 +263,6 @@ func (r *CustomClusterController) reconcile(ctx context.Context, customCluster *
 	desiredClusterInfo := getDesiredClusterInfo(customMachine, kcp)
 	// desiredVersion is the one recorded in kcp.version.
 	desiredVersion := desiredClusterInfo.KubeVersion
-	// If the desired version is not within a specified range of versions, return directly.
-	if !isSupportedVersion(desiredVersion, DefaultMinVersion, DefaultMaxVersion) {
-		log.Error(fmt.Errorf("the kubespray image version is %s. Then the minimum supported version of Kubernetes is %s, and the maximum is %s. However, the desired version of Kubernetes is %s ", DefaultKubesprayVersion, DefaultMinVersion, DefaultMaxVersion, desiredVersion), "")
-		return ctrl.Result{}, nil
-	}
 
 	// Handle cluster provision.
 	if phase == v1alpha1.PendingPhase || phase == v1alpha1.ProvisionFailedPhase || phase == v1alpha1.ProvisioningPhase {
@@ -304,10 +292,10 @@ func (r *CustomClusterController) reconcile(ctx context.Context, customCluster *
 	// Handle worker nodes scaling.
 	// By comparing desiredClusterInfo.WorkerNodes and provisionedClusterInfo.WorkerNodes to decide whether to proceed reconcileScaleUp or reconcileScaleDown.
 	if len(scaleUpWorkerNodes) != 0 {
-		return r.reconcileScaleUp(ctx, customCluster, scaleUpWorkerNodes)
+		return r.reconcileScaleUp(ctx, customCluster, scaleUpWorkerNodes, kcp)
 	}
 	if len(scaleDownWorkerNodes) != 0 {
-		return r.reconcileScaleDown(ctx, customCluster, customMachine, scaleDownWorkerNodes)
+		return r.reconcileScaleDown(ctx, customCluster, customMachine, scaleDownWorkerNodes, kcp)
 	}
 
 	// Handle cluster upgrade.
@@ -341,7 +329,7 @@ func (r *CustomClusterController) reconcileProvision(ctx context.Context, custom
 	}
 
 	// Create init worker pod to handle the provisioning process.
-	initWorker, err3 := r.ensureWorkerPodCreated(ctx, customCluster, CustomClusterInitAction, KubesprayInitCMD, generateClusterHostsName(customCluster), generateClusterConfigName(customCluster))
+	initWorker, err3 := r.ensureWorkerPodCreated(ctx, customCluster, CustomClusterInitAction, KubesprayInitCMD, generateClusterHostsName(customCluster), generateClusterConfigName(customCluster), kcp.Spec.Version)
 	if err3 != nil {
 		conditions.MarkFalse(customCluster, v1alpha1.ReadyCondition, v1alpha1.FailedCreateInitWorker,
 			clusterv1.ConditionSeverityWarning, "init worker is failed to create %s/%s", customCluster.Namespace, customCluster.Name)
@@ -398,7 +386,7 @@ func (r *CustomClusterController) reconcileDelete(ctx context.Context, customClu
 	}
 
 	// Create the termination worker to handle the cluster deletion.
-	terminateWorker, err1 := r.ensureWorkerPodCreated(ctx, customCluster, CustomClusterTerminateAction, KubesprayTerminateCMD, generateClusterHostsName(customCluster), generateClusterConfigName(customCluster))
+	terminateWorker, err1 := r.ensureWorkerPodCreated(ctx, customCluster, CustomClusterTerminateAction, KubesprayTerminateCMD, generateClusterHostsName(customCluster), generateClusterConfigName(customCluster), kcp.Spec.Version)
 	if err1 != nil {
 		conditions.MarkFalse(customCluster, v1alpha1.TerminatedCondition, v1alpha1.FailedCreateTerminateWorker,
 			clusterv1.ConditionSeverityWarning, "terminate worker is failed to create %s/%s.", customCluster.Namespace, customCluster.Name)
diff --git a/pkg/cluster-operator/customcluster_helper.go b/pkg/cluster-operator/customcluster_helper.go
index 6873235..2306072 100644
--- a/pkg/cluster-operator/customcluster_helper.go
+++ b/pkg/cluster-operator/customcluster_helper.go
@@ -24,6 +24,7 @@ import (
 	"strings"
 	"text/template"
 
+	"github.com/coreos/go-semver/semver"
 	"github.com/pkg/sftp"
 	"golang.org/x/crypto/ssh"
 	corev1 "k8s.io/api/core/v1"
@@ -32,6 +33,7 @@ import (
 	"k8s.io/apiserver/pkg/storage/names"
 	clusterv1 "sigs.k8s.io/cluster-api/api/v1beta1"
 	controlplanev1 "sigs.k8s.io/cluster-api/controlplane/kubeadm/api/v1beta1"
+	ctrl "sigs.k8s.io/controller-runtime"
 	"sigs.k8s.io/controller-runtime/pkg/client"
 	"sigs.k8s.io/controller-runtime/pkg/controller/controllerutil"
 
@@ -39,12 +41,11 @@ import (
 )
 
 // generateClusterManageWorker generate a kubespray manage worker pod from configmap.
-func generateClusterManageWorker(customCluster *v1alpha1.CustomCluster, manageAction customClusterManageAction, manageCMD customClusterManageCMD, hostsName, configName string) *corev1.Pod {
+func generateClusterManageWorker(customCluster *v1alpha1.CustomCluster, manageAction customClusterManageAction, manageCMD customClusterManageCMD, hostsName, configName, kubesprayImage string) *corev1.Pod {
 	basePodName := customCluster.Name + "-" + string(manageAction)
 	podName := names.SimpleNameGenerator.GenerateName(basePodName + "-")
 	namespace := customCluster.Namespace
 	defaultMode := int32(0o600)
-	kubesprayImage := getKubesprayImage(DefaultKubesprayVersion)
 	managerWorker := &corev1.Pod{
 		ObjectMeta: metav1.ObjectMeta{
 			Namespace: namespace,
@@ -400,7 +401,7 @@ func (r *CustomClusterController) ensureWorkerPodDeleted(ctx context.Context, cu
 }
 
 // ensureWorkerPodCreated ensure that the worker pod is created.
-func (r *CustomClusterController) ensureWorkerPodCreated(ctx context.Context, customCluster *v1alpha1.CustomCluster, manageAction customClusterManageAction, manageCMD customClusterManageCMD, hostName, configName string) (*corev1.Pod, error) {
+func (r *CustomClusterController) ensureWorkerPodCreated(ctx context.Context, customCluster *v1alpha1.CustomCluster, manageAction customClusterManageAction, manageCMD customClusterManageCMD, hostName, configName, kubeVersion string) (*corev1.Pod, error) {
 	workerPod, err := r.findManageWorkerPod(ctx, customCluster, manageAction)
 
 	if err != nil {
@@ -410,7 +411,9 @@ func (r *CustomClusterController) ensureWorkerPodCreated(ctx context.Context, cu
 		return workerPod, nil
 	}
 
-	newWorkerPod := generateClusterManageWorker(customCluster, manageAction, manageCMD, hostName, configName)
+	kubesprayImage := getKubesprayImage(ctx, kubeVersion)
+
+	newWorkerPod := generateClusterManageWorker(customCluster, manageAction, manageCMD, hostName, configName, kubesprayImage)
 	newWorkerPod.OwnerReferences = []metav1.OwnerReference{generateOwnerRefFromCustomCluster(customCluster)}
 	if err := r.Client.Create(ctx, newWorkerPod); err != nil {
 		return nil, fmt.Errorf("failed to create customCluster manager worker pod: %v", err)
@@ -441,9 +444,36 @@ func (r *CustomClusterController) findManageWorkerPod(ctx context.Context, custo
 	return nil, nil
 }
 
-// getKubesprayImage take in kubesprayVersion return the kubespray image url of this version.
-func getKubesprayImage(kubesprayVersion string) string {
+// getKubesprayImage takes a Kubernetes version string (in the format "vX.Y.Z")
+// and returns the corresponding Kubespray image URL for that version.
+// The function supports Kubernetes versions from 1.22.0 to 1.26.5.
+// Kubespray v2.20.0 supports Kubernetes versions from 1.22.0 to 1.24.6,
+// while Kubespray v2.22.1 supports Kubernetes versions from 1.24.0 to 1.26.5.
+// This function returns one of these two Kubespray versions based on the input Kubernetes version.
+func getKubesprayImage(ctx context.Context, kubeVersion string) string {
+	log := ctrl.LoggerFrom(ctx)
+	var kubesprayVersion string
+
+	kubeVersion = strings.TrimPrefix(kubeVersion, "v")
+
+	targetVersion, err := semver.NewVersion(kubeVersion)
+	// should not happen, if we have validation on kubeVersion
+	if err != nil {
+		log.Error(err, "unexpected kube version", "targetVersion", targetVersion)
+		return ""
+	}
+
+	midVersion, _ := semver.NewVersion("1.24.0")
+
+	// Determine the Kubespray version based on the Kubernetes version.
+	if targetVersion.Compare(*midVersion) >= 0 {
+		kubesprayVersion = "v2.22.1"
+	} else {
+		kubesprayVersion = "v2.20.0"
+	}
+
 	imagePath := "quay.io/kubespray/kubespray:" + kubesprayVersion
+
 	return imagePath
 }
 
diff --git a/pkg/cluster-operator/customcluster_scale.go b/pkg/cluster-operator/customcluster_scale.go
index 4bca656..7c7975e 100644
--- a/pkg/cluster-operator/customcluster_scale.go
+++ b/pkg/cluster-operator/customcluster_scale.go
@@ -26,6 +26,7 @@ import (
 	apierrors "k8s.io/apimachinery/pkg/api/errors"
 	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
 	clusterv1 "sigs.k8s.io/cluster-api/api/v1beta1"
+	controlplanev1 "sigs.k8s.io/cluster-api/controlplane/kubeadm/api/v1beta1"
 	"sigs.k8s.io/cluster-api/util/conditions"
 	ctrl "sigs.k8s.io/controller-runtime"
 	"sigs.k8s.io/controller-runtime/pkg/client"
@@ -34,7 +35,7 @@ import (
 )
 
 // reconcileScaleUp is responsible for handling the customCluster reconciliation process when worker nodes need to be scaled up.
-func (r *CustomClusterController) reconcileScaleUp(ctx context.Context, customCluster *v1alpha1.CustomCluster, scaleUpWorkerNodes []NodeInfo) (ctrl.Result, error) {
+func (r *CustomClusterController) reconcileScaleUp(ctx context.Context, customCluster *v1alpha1.CustomCluster, scaleUpWorkerNodes []NodeInfo, kcp *controlplanev1.KubeadmControlPlane) (ctrl.Result, error) {
 	log := ctrl.LoggerFrom(ctx)
 
 	// Create a temporary configmap that represents the desired state to create the scaleUp pod.
@@ -44,7 +45,7 @@ func (r *CustomClusterController) reconcileScaleUp(ctx context.Context, customCl
 	}
 
 	// Create the scaleUp pod.
-	workerPod, err1 := r.ensureWorkerPodCreated(ctx, customCluster, CustomClusterScaleUpAction, KubesprayScaleUpCMD, generateScaleUpHostsName(customCluster), generateClusterConfigName(customCluster))
+	workerPod, err1 := r.ensureWorkerPodCreated(ctx, customCluster, CustomClusterScaleUpAction, KubesprayScaleUpCMD, generateScaleUpHostsName(customCluster), generateClusterConfigName(customCluster), kcp.Spec.Version)
 	if err1 != nil {
 		conditions.MarkFalse(customCluster, v1alpha1.ScaledUpCondition, v1alpha1.FailedCreateScaleUpWorker,
 			clusterv1.ConditionSeverityWarning, "scale up worker is failed to create %s/%s.", customCluster.Namespace, customCluster.Name)
@@ -98,11 +99,11 @@ func (r *CustomClusterController) reconcileScaleUp(ctx context.Context, customCl
 }
 
 // reconcileScaleDown is responsible for handling the customCluster reconciliation process when worker nodes need to be scaled down.
-func (r *CustomClusterController) reconcileScaleDown(ctx context.Context, customCluster *v1alpha1.CustomCluster, customMachine *v1alpha1.CustomMachine, scaleDownWorkerNodes []NodeInfo) (ctrl.Result, error) {
+func (r *CustomClusterController) reconcileScaleDown(ctx context.Context, customCluster *v1alpha1.CustomCluster, customMachine *v1alpha1.CustomMachine, scaleDownWorkerNodes []NodeInfo, kcp *controlplanev1.KubeadmControlPlane) (ctrl.Result, error) {
 	log := ctrl.LoggerFrom(ctx)
 
 	// Checks whether the worker node for scaling down already exists. If it does not exist, the function creates it.
-	workerPod, err1 := r.ensureWorkerPodCreated(ctx, customCluster, CustomClusterScaleDownAction, generateScaleDownManageCMD(scaleDownWorkerNodes), generateClusterHostsName(customCluster), generateClusterConfigName(customCluster))
+	workerPod, err1 := r.ensureWorkerPodCreated(ctx, customCluster, CustomClusterScaleDownAction, generateScaleDownManageCMD(scaleDownWorkerNodes), generateClusterHostsName(customCluster), generateClusterConfigName(customCluster), kcp.Spec.Version)
 	if err1 != nil {
 		conditions.MarkFalse(customCluster, v1alpha1.ScaledDownCondition, v1alpha1.FailedCreateScaleDownWorker,
 			clusterv1.ConditionSeverityWarning, "scale down worker is failed to create %s/%s.", customCluster.Namespace, customCluster.Name)
diff --git a/pkg/cluster-operator/customcluster_upgrade.go b/pkg/cluster-operator/customcluster_upgrade.go
index 9298452..1adc9f1 100644
--- a/pkg/cluster-operator/customcluster_upgrade.go
+++ b/pkg/cluster-operator/customcluster_upgrade.go
@@ -36,7 +36,7 @@ func (r *CustomClusterController) reconcileUpgrade(ctx context.Context, customCl
 
 	cmd := generateUpgradeManageCMD(targetVersion)
 	// Checks whether the worker node for upgrading already exists. If it does not exist, then create it.
-	workerPod, err1 := r.ensureWorkerPodCreated(ctx, customCluster, CustomClusterUpgradeAction, cmd, generateClusterHostsName(customCluster), generateClusterConfigName(customCluster))
+	workerPod, err1 := r.ensureWorkerPodCreated(ctx, customCluster, CustomClusterUpgradeAction, cmd, generateClusterHostsName(customCluster), generateClusterConfigName(customCluster), targetVersion)
 	if err1 != nil {
 		conditions.MarkFalse(customCluster, v1alpha1.UpgradeCondition, v1alpha1.UpgradeWorkerCreateFailed,
 			clusterv1.ConditionSeverityWarning, "upgrade worker is failed to create %s/%s.", customCluster.Namespace, customCluster.Name)
diff --git a/pkg/fleet-manager/application_controller.go b/pkg/fleet-manager/application_controller.go
index 3c302c2..6c4f615 100644
--- a/pkg/fleet-manager/application_controller.go
+++ b/pkg/fleet-manager/application_controller.go
@@ -38,7 +38,6 @@ import (
 	"sigs.k8s.io/controller-runtime/pkg/source"
 
 	applicationapi "kurator.dev/kurator/pkg/apis/apps/v1alpha1"
-	clusterv1alpha1 "kurator.dev/kurator/pkg/apis/cluster/v1alpha1"
 	fleetapi "kurator.dev/kurator/pkg/apis/fleet/v1alpha1"
 )
 
@@ -60,10 +59,6 @@ type ApplicationManager struct {
 	Scheme *runtime.Scheme
 }
 
-var fleetToApplicationMap = map[string][]string{}
-var clusterToApplicationMap = map[string][]string{}
-var attachedClusterToApplicationMap = map[string][]string{}
-
 // SetupWithManager sets up the controller with the Manager.
 func (a *ApplicationManager) SetupWithManager(ctx context.Context, mgr ctrl.Manager, options controller.Options) error {
 	c, err := ctrl.NewControllerManagedBy(mgr).
@@ -73,28 +68,6 @@ func (a *ApplicationManager) SetupWithManager(ctx context.Context, mgr ctrl.Mana
 		return err
 	}
 
-	// Set up watches for the updates to application's resource.
-	if err := c.Watch(
-		&source.Kind{Type: &fleetapi.Fleet{}},
-		handler.EnqueueRequestsFromMapFunc(a.fleetToApplicationFunc),
-	); err != nil {
-		return fmt.Errorf("failed to add a Watch for Fleet: %v", err)
-	}
-
-	if err := c.Watch(
-		&source.Kind{Type: &clusterv1alpha1.Cluster{}},
-		handler.EnqueueRequestsFromMapFunc(a.clusterToApplicationFunc),
-	); err != nil {
-		return fmt.Errorf("failed to add a Watch for Cluster: %v", err)
-	}
-
-	if err := c.Watch(
-		&source.Kind{Type: &clusterv1alpha1.AttachedCluster{}},
-		handler.EnqueueRequestsFromMapFunc(a.attachedClusterToApplicationFunc),
-	); err != nil {
-		return fmt.Errorf("failed to add a Watch for AttachedCluster: %v", err)
-	}
-
 	// Set up watches for the updates to application's status.
 	if err := c.Watch(
 		&source.Kind{Type: &sourcev1beta2.GitRepository{}},
@@ -164,17 +137,8 @@ func (a *ApplicationManager) Reconcile(ctx context.Context, req ctrl.Request) (_
 		return ctrl.Result{}, nil
 	}
 
-	var fleetName string
-	if app.Spec.Destination != nil {
-		fleetName = app.Spec.Destination.Fleet
-	} else {
-		fleetName = app.Spec.SyncPolicies[0].Destination.Fleet
-	}
 	// there only one fleet, so pre-fetch it here.
-	fleetKey := client.ObjectKey{
-		Namespace: app.Namespace,
-		Name:      fleetName,
-	}
+	fleetKey := generateFleetKey(app)
 	fleet := &fleetapi.Fleet{}
 	// Retrieve fleet object based on the defined fleet key
 	if err := a.Client.Get(ctx, fleetKey, fleet); err != nil {
@@ -187,15 +151,9 @@ func (a *ApplicationManager) Reconcile(ctx context.Context, req ctrl.Request) (_
 		return ctrl.Result{}, err
 	}
 
-	// Add this relation to fleetToApplicationMap
-	if fleetToApplicationMap[fleet.Name] == nil {
-		fleetToApplicationMap[fleet.Name] = make([]string, 0)
-	}
-	fleetToApplicationMap[fleet.Name] = append(fleetToApplicationMap[fleet.Name], app.Name)
-
 	// Handle deletion reconciliation loop.
 	if app.DeletionTimestamp != nil {
-		return a.reconcileDelete(ctx, app, fleet)
+		return a.reconcileDelete(app)
 	}
 
 	// Handle normal loop.
@@ -205,168 +163,142 @@ func (a *ApplicationManager) Reconcile(ctx context.Context, req ctrl.Request) (_
 func (a *ApplicationManager) reconcile(ctx context.Context, app *applicationapi.Application, fleet *fleetapi.Fleet) (ctrl.Result, error) {
 	log := ctrl.LoggerFrom(ctx)
 
-	if result, err := a.reconcileInitializeParameters(ctx, app); err != nil || result.RequeueAfter > 0 {
-		log.Error(err, "failed to reconcileInitializeParameters")
-		return result, err
-	}
-
-	if result, err := a.reconcileApplicationResources(ctx, app, fleet); err != nil || result.RequeueAfter > 0 {
+	result, err := a.reconcileApplicationResources(ctx, app, fleet)
+	if err != nil {
 		log.Error(err, "failed to reconcileSyncResources")
-
-		return result, err
 	}
-
-	if result, err := a.reconcileStatus(ctx, app); err != nil || result.RequeueAfter > 0 {
-		log.Error(err, "failed to reconcileSyncStatus")
-
+	if err != nil || result.RequeueAfter > 0 {
 		return result, err
 	}
 
-	return ctrl.Result{}, nil
-}
-
-// reconcileInitializeParameters initializes the parameters for the given application.
-func (a *ApplicationManager) reconcileInitializeParameters(ctx context.Context, app *applicationapi.Application) (ctrl.Result, error) {
-	log := ctrl.LoggerFrom(ctx)
-
-	sourceKind := findSourceKind(app)
-
-	// Iterate over all sync policies.
-	for _, policy := range app.Spec.SyncPolicies {
-		// Currently, only two pairs of source types are supported: 'gitRepo + kustomization' and 'helmRepo + helmRelease'.
-		// TODO: support other pairs
-		if sourceKind == GitRepoKind && policy.Kustomization == nil {
-			log.Error(fmt.Errorf("source kind is GitRepoKind, but policy.Kustomization is nil"), "policyName", policy.Name)
-			return ctrl.Result{}, nil
-		}
-
-		if sourceKind == HelmRepoKind && policy.Helm == nil {
-			log.Error(fmt.Errorf("source kind is HelmRepoKind, but policy.policy.Helm is nil"), "policyName", policy.Name)
-			return ctrl.Result{}, nil
-		}
-	}
-
-	// If the application's source status is nil, set it to an empty ApplicationSourceStatus object.
-	if app.Status.SourceStatus == nil {
-		app.Status.SourceStatus = &applicationapi.ApplicationSourceStatus{}
+	if err := a.reconcileStatus(ctx, app); err != nil {
+		log.Error(err, "failed to reconcile status")
+		return ctrl.Result{}, err
 	}
 
 	return ctrl.Result{}, nil
 }
 
-// reconcileSyncResources handles the synchronization of resources associated with the current Application resource.
+// reconcileApplicationResources handles the synchronization of resources associated with the current Application resource.
 // The associated resources are categorized as 'source' and 'policy'.
 // 'source' could be one of gitRepo, helmRepo, or ociRepo while 'policy' can be either kustomizations or helmReleases.
 // Any change in Application configuration could potentially lead to creation, deletion, or modification of associated resources in the Kubernetes cluster.
 func (a *ApplicationManager) reconcileApplicationResources(ctx context.Context, app *applicationapi.Application, fleet *fleetapi.Fleet) (ctrl.Result, error) {
-	log := ctrl.LoggerFrom(ctx)
 	// Synchronize source resource based on application configuration
-	if err := a.syncSourceResource(ctx, app); err != nil {
-		return ctrl.Result{}, err
+	if result, err := a.syncSourceResource(ctx, app); err != nil || result.RequeueAfter > 0 {
+		return result, err
 	}
 
-	// todo: if we need to delete the kustomization/helmReleases when the cluster is removed from the fleet?
-	// todo: if we need to delete the kustomization/helmReleases when the fleet is removed from the application?
-
 	// Iterate over each policy in the application's spec.SyncPolicy
 	for index, policy := range app.Spec.SyncPolicies {
 		policyName := generatePolicyName(app, index)
 		// A policy has a fleet, and a fleet has many clusters. Therefore, a policy may need to create or update multiple kustomizations/helmReleases for each cluster.
 		// Synchronize policy resource based on current application, fleet, and policy configuration
-		if err := a.syncPolicyResource(ctx, app, fleet, policy, policyName); err != nil {
-			log.Error(err, "failed to sync policy resource", "fleet", fleet.Name)
-			return ctrl.Result{}, err
+		if result, err := a.syncPolicyResource(ctx, app, fleet, policy, policyName); err != nil || result.RequeueAfter > 0 {
+			return result, err
 		}
 	}
 	return ctrl.Result{}, nil
 }
 
-// reconcileSyncStatus updates the status of resources associated with the current Application resource.
+// reconcileStatus updates the status of resources associated with the current Application resource.
 // It does this by fetching the current status of the source (either GitRepoKind or HelmRepoKind) and the sync policy from the API server,
 // and updating the Application's status to reflect these current statuses.
-func (a *ApplicationManager) reconcileStatus(ctx context.Context, app *applicationapi.Application) (ctrl.Result, error) {
+func (a *ApplicationManager) reconcileStatus(ctx context.Context, app *applicationapi.Application) error {
+	if err := a.reconcileSourceStatus(ctx, app); err != nil {
+		return err
+	}
+
+	if err := a.reconcileSyncStatus(ctx, app); err != nil {
+		return err
+	}
+
+	return nil
+}
+
+// reconcileSourceStatus reconciles the source status of the given application by fetching the status of the source resource (e.g. GitRepository, HelmRepository)
+func (a *ApplicationManager) reconcileSourceStatus(ctx context.Context, app *applicationapi.Application) error {
 	log := ctrl.LoggerFrom(ctx)
 
-	// reconcile Sync source Status
 	sourceKey := client.ObjectKey{
 		Name:      generateSourceName(app),
 		Namespace: app.GetNamespace(),
 	}
 
+	if app.Status.SourceStatus == nil {
+		app.Status.SourceStatus = &applicationapi.ApplicationSourceStatus{}
+	}
+
 	sourceKind := findSourceKind(app)
 	// Depending on source kind in application specifications, fetch resource status and update application's source status
 	switch sourceKind {
 	case GitRepoKind:
 		currentResource := &sourcev1beta2.GitRepository{}
-		if err := a.Client.Get(ctx, sourceKey, currentResource); err != nil {
+		err := a.Client.Get(ctx, sourceKey, currentResource)
+		if err != nil && !apierrors.IsNotFound(err) {
 			log.Error(err, "failed to get GitRepository from the API server when reconciling status")
-			return ctrl.Result{}, nil
+			return err
+		}
+		// if not found, return directly. new created GitRepository will be watched in subsequent loop
+		if apierrors.IsNotFound(err) {
+			return nil
 		}
 		app.Status.SourceStatus.GitRepoStatus = &currentResource.Status
 
 	case HelmRepoKind:
 		currentResource := &sourcev1beta2.HelmRepository{}
-		if err := a.Client.Get(ctx, sourceKey, currentResource); err != nil {
+		err := a.Client.Get(ctx, sourceKey, currentResource)
+		if err != nil && !apierrors.IsNotFound(err) {
 			log.Error(err, "failed to get HelmRepository from the API server when reconciling status")
-			return ctrl.Result{}, err
+			return err
+		}
+		// if not found, return directly. new created HelmRepository will be watched in subsequent loop
+		if apierrors.IsNotFound(err) {
+			return nil
 		}
 		app.Status.SourceStatus.HelmRepoStatus = &currentResource.Status
 	}
+	return nil
+}
 
-	// Depending on source kind in application specifications, fetch associated resources and update application's sync status
-	switch sourceKind {
-	case GitRepoKind:
-		var kustomizationList kustomizev1beta2.KustomizationList
-		if err := a.Client.List(ctx, &kustomizationList, client.InNamespace(app.Namespace), client.MatchingLabels{ApplicationLabel: app.Name}); err != nil {
-			return ctrl.Result{}, err
-		}
-
-		var syncStatus []*applicationapi.ApplicationSyncStatus
-		for _, kustomization := range kustomizationList.Items {
-			currentStatus := &applicationapi.ApplicationSyncStatus{
-				Name:                kustomization.Name,
-				KustomizationStatus: &kustomization.Status,
-			}
-			syncStatus = append(syncStatus, currentStatus)
-		}
-		app.Status.SyncStatus = syncStatus
+// reconcileSyncStatus reconciles the sync status of the given application by finding all Kustomizations and HelmReleases associated with it,
+// and updating the sync status of each resource in the application's SyncStatus field.
+func (a *ApplicationManager) reconcileSyncStatus(ctx context.Context, app *applicationapi.Application) error {
+	var syncStatus []*applicationapi.ApplicationSyncStatus
 
-	case HelmRepoKind:
-		var helmReleaseList helmv2b1.HelmReleaseList
-		if err := a.Client.List(ctx, &helmReleaseList, client.InNamespace(app.Namespace), client.MatchingLabels{ApplicationLabel: app.Name}); err != nil {
-			return ctrl.Result{}, err
+	// find all kustomization
+	kustomizationList, err := a.getKustomizationList(ctx, app)
+	if err != nil {
+		return nil
+	}
+	// sync all kustomization status
+	for _, kustomization := range kustomizationList.Items {
+		kustomizationStatus := &applicationapi.ApplicationSyncStatus{
+			Name:                kustomization.Name,
+			KustomizationStatus: &kustomization.Status,
 		}
+		syncStatus = append(syncStatus, kustomizationStatus)
+	}
 
-		var syncStatus []*applicationapi.ApplicationSyncStatus
-		for _, helmRelease := range helmReleaseList.Items {
-			currentStatus := &applicationapi.ApplicationSyncStatus{
-				Name:              helmRelease.Name,
-				HelmReleaseStatus: &helmRelease.Status,
-			}
-			syncStatus = append(syncStatus, currentStatus)
+	// find all helmRelease
+	helmReleaseList, err := a.getHelmReleaseList(ctx, app)
+	if err != nil {
+		return err
+	}
+	// sync all helmRelease status
+	for _, helmRelease := range helmReleaseList.Items {
+		helmReleaseStatus := &applicationapi.ApplicationSyncStatus{
+			Name:              helmRelease.Name,
+			HelmReleaseStatus: &helmRelease.Status,
 		}
-		app.Status.SyncStatus = syncStatus
+		syncStatus = append(syncStatus, helmReleaseStatus)
 	}
 
-	return ctrl.Result{}, nil
+	app.Status.SyncStatus = syncStatus
+	return nil
 }
 
-func (a *ApplicationManager) reconcileDelete(ctx context.Context, app *applicationapi.Application, fleet *fleetapi.Fleet) (ctrl.Result, error) {
-	log := ctrl.LoggerFrom(ctx)
-
-	if _, ok := fleetToApplicationMap[fleet.Name]; !ok {
-		log.Info("current fleet is not recorded in fleetToApplicationMap")
-	} else {
-		// remove current mapping of fleetToApplicationMap
-		applications := fleetToApplicationMap[fleet.Name]
-		for i, application := range applications {
-			if application == app.Name {
-				// delete it
-				fleetToApplicationMap[fleet.Name] = append(applications[:i], applications[i+1:]...)
-			}
-		}
-	}
-
+func (a *ApplicationManager) reconcileDelete(app *applicationapi.Application) (ctrl.Result, error) {
 	controllerutil.RemoveFinalizer(app, ApplicationFinalizer)
 
 	return ctrl.Result{}, nil
@@ -387,57 +319,3 @@ func (a *ApplicationManager) objectToApplicationFunc(o client.Object) []ctrl.Req
 
 	return nil
 }
-
-func (a *ApplicationManager) fleetToApplicationFunc(o client.Object) []ctrl.Request {
-	c, ok := o.(*fleetapi.Fleet)
-	if !ok {
-		panic(fmt.Sprintf("Expected a Fleet but got a %T", o))
-	}
-	var result []ctrl.Request
-
-	applicationNames, ok := fleetToApplicationMap[c.Name]
-
-	if ok {
-		for _, applicationName := range applicationNames {
-			result = append(result, ctrl.Request{NamespacedName: client.ObjectKey{Namespace: c.GetNamespace(), Name: applicationName}})
-		}
-	}
-
-	return result
-}
-
-func (a *ApplicationManager) clusterToApplicationFunc(o client.Object) []ctrl.Request {
-	c, ok := o.(*clusterv1alpha1.Cluster)
-	if !ok {
-		panic(fmt.Sprintf("Expected a Fleet but got a %T", o))
-	}
-	var result []ctrl.Request
-
-	applicationNames, ok := clusterToApplicationMap[c.Name]
-
-	if ok {
-		for _, applicationName := range applicationNames {
-			result = append(result, ctrl.Request{NamespacedName: client.ObjectKey{Namespace: c.GetNamespace(), Name: applicationName}})
-		}
-	}
-
-	return result
-}
-
-func (a *ApplicationManager) attachedClusterToApplicationFunc(o client.Object) []ctrl.Request {
-	c, ok := o.(*clusterv1alpha1.AttachedCluster)
-	if !ok {
-		panic(fmt.Sprintf("Expected a Fleet but got a %T", o))
-	}
-	var result []ctrl.Request
-
-	applicationNames, ok := attachedClusterToApplicationMap[c.Name]
-
-	if ok {
-		for _, applicationName := range applicationNames {
-			result = append(result, ctrl.Request{NamespacedName: client.ObjectKey{Namespace: c.GetNamespace(), Name: applicationName}})
-		}
-	}
-
-	return result
-}
diff --git a/pkg/fleet-manager/application_helper.go b/pkg/fleet-manager/application_helper.go
index 2e7d6b5..04383f3 100644
--- a/pkg/fleet-manager/application_helper.go
+++ b/pkg/fleet-manager/application_helper.go
@@ -26,6 +26,7 @@ import (
 	kustomizev1beta2 "github.com/fluxcd/kustomize-controller/api/v1beta2"
 	fluxmeta "github.com/fluxcd/pkg/apis/meta"
 	sourcev1beta2 "github.com/fluxcd/source-controller/api/v1beta2"
+	"github.com/pkg/errors"
 	apierrors "k8s.io/apimachinery/pkg/api/errors"
 	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
 	ctrl "sigs.k8s.io/controller-runtime"
@@ -37,101 +38,126 @@ import (
 )
 
 // syncPolicyResource synchronizes the sync policy resources for a given application.
-func (a *ApplicationManager) syncPolicyResource(ctx context.Context, app *applicationapi.Application, fleet *fleetapi.Fleet, syncPolicy *applicationapi.ApplicationSyncPolicy, policyName string) error {
-	log := ctrl.LoggerFrom(ctx)
-	sourceKind := findSourceKind(app)
+func (a *ApplicationManager) syncPolicyResource(ctx context.Context, app *applicationapi.Application, fleet *fleetapi.Fleet, syncPolicy *applicationapi.ApplicationSyncPolicy, policyName string) (ctrl.Result, error) {
+	policyKind := getSyncPolicyKind(syncPolicy)
 
-	// Merge the list of clusters and attached clusters into a single list. also mapping relation will be added here
-	fleetClusterList, err := a.generateFleetCluster(ctx, fleet, app)
-	if err != nil {
-		return err
+	// fetch fleet cluster list that recorded in fleet.
+	fleetClusterList, result, err := a.fetchFleetClusterList(ctx, fleet)
+	if err != nil || result.RequeueAfter > 0 {
+		return result, err
 	}
-
-	if len(fleetClusterList) == 0 {
-		log.Info("no cluster is found in current fleet", "fleet", fleet.Name)
-		return nil
-	}
-
-	// Handle each cluster in current fleet.
+	// Iterate through all clusters, and create/update kustomization/helmRelease for each of them.
 	for _, currentFleetCluster := range fleetClusterList {
 		// fetch kubeconfig for each cluster.
-		kubeConfig := a.generateKubeConfig(currentFleetCluster)
-
-		// handle gitRepo + kustomization
-		if sourceKind == GitRepoKind {
-			kustomization := syncPolicy.Kustomization
-			kustomizationName := generateKustomizationName(app, currentFleetCluster.GetObject().GetObjectKind().GroupVersionKind().Kind, currentFleetCluster.GetObject().GetName(), policyName)
+		kubeconfig := a.generateKubeConfig(currentFleetCluster)
 
-			// create flux kustomization using kubeconfig and source.
-			if err := a.syncKustomizationForCluster(ctx, app, kustomization, kubeConfig, kustomizationName); err != nil {
-				log.Error(err, "failed to syncKustomizationForCluster", "kustomizationName", kustomizationName)
-				return err
-			}
-			return nil
+		if result, err1 := a.handleSyncPolicyByKind(ctx, app, policyKind, syncPolicy, policyName, currentFleetCluster, kubeconfig); err1 != nil || result.RequeueAfter > 0 {
+			return result, errors.Wrapf(err1, "failed to handleSyncPolicyByKind currentFleetCluster=%s", currentFleetCluster.GetObject().GetName())
 		}
+	}
 
-		// handle helmRepo + helmRelease
-		if sourceKind == HelmRepoKind {
-			helmRelease := syncPolicy.Helm
-			helmReleaseName := generateHelmReleaseName(app, currentFleetCluster.GetObject().GetObjectKind().GroupVersionKind().Kind, currentFleetCluster.GetObject().GetName(), policyName)
+	return ctrl.Result{}, nil
+}
 
-			// create flux helmRelease using kubeconfig and source.
-			if err := a.syncHelmReleaseForCluster(ctx, app, helmRelease, kubeConfig, helmReleaseName); err != nil {
-				log.Error(err, "failed to syncHelmReleaseForCluster", "helmReleaseName", helmReleaseName)
-				return err
-			}
-			return nil
-		}
+// fetchFleetClusterList fetch fleet cluster list that recorded in fleet.
+func (a *ApplicationManager) fetchFleetClusterList(ctx context.Context, fleet *fleetapi.Fleet) ([]ClusterInterface, ctrl.Result, error) {
+	log := ctrl.LoggerFrom(ctx)
+	var fleetClusterList []ClusterInterface
 
-		// todo: what if kind is ociRepo
-		if sourceKind != GitRepoKind && sourceKind != HelmRepoKind {
-			return fmt.Errorf("current source kind is %s, this kind is unsupported", sourceKind)
+	for _, cluster := range fleet.Spec.Clusters {
+		// cluster.kind cluster.name that recorded in fleet must be valid
+		kind := cluster.Kind
+		name := cluster.Name
+		if kind == ClusterKind {
+			cluster := &clusterv1alpha1.Cluster{}
+			key := client.ObjectKey{
+				Name:      name,
+				Namespace: fleet.Namespace,
+			}
+			err := a.Client.Get(ctx, key, cluster)
+			if apierrors.IsNotFound(err) {
+				return nil, ctrl.Result{RequeueAfter: RequeueAfter}, nil
+			}
+			if err != nil {
+				return nil, ctrl.Result{}, err
+			}
+			fleetClusterList = append(fleetClusterList, cluster)
+		} else if kind == AttachedClusterKind {
+			attachedCluster := &clusterv1alpha1.AttachedCluster{}
+			key := client.ObjectKey{
+				Name:      name,
+				Namespace: fleet.Namespace,
+			}
+			err := a.Client.Get(ctx, key, attachedCluster)
+			if apierrors.IsNotFound(err) {
+				return nil, ctrl.Result{RequeueAfter: RequeueAfter}, nil
+			}
+			if err != nil {
+				return nil, ctrl.Result{}, err
+			}
+			fleetClusterList = append(fleetClusterList, attachedCluster)
+		} else {
+			log.Info("kind of cluster in fleet is not support, skip this cluster", "fleet", fleet.Name, "kind", kind)
 		}
 	}
-	return nil
+	return fleetClusterList, ctrl.Result{}, nil
 }
 
-// mergeClusterLists merges the lists of Clusters and AttachedClusters associated with the specified Fleet.
-func (a *ApplicationManager) generateFleetCluster(ctx context.Context, fleet *fleetapi.Fleet, app *applicationapi.Application) ([]ClusterInterface, error) {
-	log := ctrl.LoggerFrom(ctx)
-
-	var clusterList clusterv1alpha1.ClusterList
-	var attachedClusterList clusterv1alpha1.AttachedClusterList
-
-	if err := a.Client.List(ctx, &clusterList,
-		client.InNamespace(fleet.Namespace),
-		client.MatchingLabels{FleetLabel: fleet.Name}); err != nil {
-		log.Error(err, "failed to fetch clusterList for fleet", "fleet", fleet.Name)
-		return nil, err
+// getKustomizationList returns a list of kustomizations associated with the given application.
+func (a *ApplicationManager) getKustomizationList(ctx context.Context, app *applicationapi.Application) (*kustomizev1beta2.KustomizationList, error) {
+	kustomizationList := &kustomizev1beta2.KustomizationList{}
+	err := a.Client.List(ctx, kustomizationList,
+		client.InNamespace(app.Namespace),
+		client.MatchingLabels{ApplicationLabel: app.Name})
+	if err != nil && !apierrors.IsNotFound(err) {
+		return nil, fmt.Errorf("failed to fetch kustomizationList for application: %w", err)
 	}
+	return kustomizationList, nil
+}
 
-	if err := a.Client.List(ctx, &attachedClusterList,
-		client.InNamespace(fleet.Namespace),
-		client.MatchingLabels{FleetLabel: fleet.Name}); err != nil {
-		log.Error(err, "failed to fetch attachedClusterList for fleet", "fleet", fleet.Name)
-		return nil, err
+// getHelmReleaseList returns a list of helmReleases associated with the given application.
+func (a *ApplicationManager) getHelmReleaseList(ctx context.Context, app *applicationapi.Application) (*helmv2b1.HelmReleaseList, error) {
+	helmReleaseList := &helmv2b1.HelmReleaseList{}
+	err := a.Client.List(ctx, helmReleaseList,
+		client.InNamespace(app.Namespace),
+		client.MatchingLabels{ApplicationLabel: app.Name})
+	if err != nil && !apierrors.IsNotFound(err) {
+		return nil, fmt.Errorf("failed to fetch kustomizationList for application: %w", err)
 	}
+	return helmReleaseList, nil
+}
 
-	var fleetClusterList []ClusterInterface
-	for _, cluster := range clusterList.Items {
-		// add map item
-		if clusterToApplicationMap[cluster.Name] == nil {
-			clusterToApplicationMap[cluster.Name] = make([]string, 0)
+// handleSyncPolicyByKind handles syncing for a given policy kind (either kustomization or Helm release) based on the provided sync policy.
+func (a *ApplicationManager) handleSyncPolicyByKind(
+	ctx context.Context,
+	app *applicationapi.Application,
+	policyKind string,
+	syncPolicy *applicationapi.ApplicationSyncPolicy,
+	policyName string,
+	fleetCluster ClusterInterface,
+	kubeConfig *fluxmeta.KubeConfigReference,
+) (ctrl.Result, error) {
+	policyResourceName := generatePolicyResourceName(policyName, fleetCluster.GetObject().GetObjectKind().GroupVersionKind().Kind, fleetCluster.GetObject().GetName())
+	// handle kustomization
+	if policyKind == KustomizationKind {
+		kustomization := syncPolicy.Kustomization
+		// sync kustomization using the provided kubeconfig and source.
+		if result, err := a.syncKustomizationForCluster(ctx, app, kustomization, kubeConfig, policyResourceName); err != nil || result.RequeueAfter > 0 {
+			return result, err
 		}
-		clusterToApplicationMap[cluster.Name] = append(clusterToApplicationMap[cluster.Name], app.Name)
-		// merge fleetClusterList
-		fleetClusterList = append(fleetClusterList, &cluster)
-	}
-	for _, attachedCluster := range attachedClusterList.Items {
-		// add map item
-		if clusterToApplicationMap[attachedCluster.Name] == nil {
-			clusterToApplicationMap[attachedCluster.Name] = make([]string, 0)
+		return ctrl.Result{}, nil
+	}
+
+	// handle helmRelease
+	if policyKind == HelmReleaseKind {
+		helmRelease := syncPolicy.Helm
+		// sync helmRelease using the provided kubeconfig and source.
+		if result, err := a.syncHelmReleaseForCluster(ctx, app, helmRelease, kubeConfig, policyResourceName); err != nil || result.RequeueAfter > 0 {
+			return result, err
 		}
-		clusterToApplicationMap[attachedCluster.Name] = append(clusterToApplicationMap[attachedCluster.Name], app.Name)
-		// merge fleetClusterList
-		fleetClusterList = append(fleetClusterList, &attachedCluster)
+		return ctrl.Result{}, nil
 	}
-	return fleetClusterList, nil
+	return ctrl.Result{}, nil
 }
 
 // generateKubeConfig generates the kubeconfig reference for a cluster within a Fleet.
@@ -147,7 +173,7 @@ func (a *ApplicationManager) generateKubeConfig(fleetCluster ClusterInterface) *
 }
 
 // syncKustomizationForCluster ensures that the Kustomization object is in sync with Flux's requirements for the object.
-func (a *ApplicationManager) syncKustomizationForCluster(ctx context.Context, app *applicationapi.Application, kustomization *applicationapi.Kustomization, kubeConfig *fluxmeta.KubeConfigReference, kustomizationName string) error {
+func (a *ApplicationManager) syncKustomizationForCluster(ctx context.Context, app *applicationapi.Application, kustomization *applicationapi.Kustomization, kubeConfig *fluxmeta.KubeConfigReference, kustomizationName string) (ctrl.Result, error) {
 	// Create a target Kustomization object with details extracted from the provided application's Kustomization spec
 	targetKustomization := &kustomizev1beta2.Kustomization{
 		ObjectMeta: metav1.ObjectMeta{
@@ -170,7 +196,7 @@ func (a *ApplicationManager) syncKustomizationForCluster(ctx context.Context, ap
 			Patches:       kustomization.Patches,
 			Images:        kustomization.Images,
 			SourceRef: kustomizev1beta2.CrossNamespaceSourceReference{
-				Kind: GitRepoKind,
+				Kind: findSourceKind(app),
 				Name: generateSourceName(app),
 			},
 			Suspend:         kustomization.Suspend,
@@ -194,11 +220,11 @@ func (a *ApplicationManager) syncKustomizationForCluster(ctx context.Context, ap
 }
 
 // syncHelmReleaseForCluster ensures that the HelmRelease object is in sync with Flux's requirements for the object.
-func (a *ApplicationManager) syncHelmReleaseForCluster(ctx context.Context, app *applicationapi.Application, helmRelease *applicationapi.HelmRelease, kubeConfig *fluxmeta.KubeConfigReference, kustomizationName string) error {
+func (a *ApplicationManager) syncHelmReleaseForCluster(ctx context.Context, app *applicationapi.Application, helmRelease *applicationapi.HelmRelease, kubeConfig *fluxmeta.KubeConfigReference, helmReleaseName string) (ctrl.Result, error) {
 	// Create a target HelmRelease object with details extracted from the provided application's HelmRelease spec
 	targetHelmRelease := &helmv2b1.HelmRelease{
 		ObjectMeta: metav1.ObjectMeta{
-			Name:      kustomizationName,
+			Name:      helmReleaseName,
 			Namespace: app.Namespace,
 			Labels: map[string]string{
 				ApplicationLabel: app.Name,
@@ -241,7 +267,7 @@ func (a *ApplicationManager) syncHelmReleaseForCluster(ctx context.Context, app
 		Chart:   charSpec.Chart,
 		Version: charSpec.Version,
 		SourceRef: helmv2b1.CrossNamespaceObjectReference{
-			Kind: HelmRepoKind,
+			Kind: findSourceKind(app),
 			Name: generateSourceName(app),
 		},
 		Interval:          charSpec.Interval,
@@ -254,7 +280,7 @@ func (a *ApplicationManager) syncHelmReleaseForCluster(ctx context.Context, app
 }
 
 // syncSourceResource synchronizes the source resource based on the application's source specification.
-func (a *ApplicationManager) syncSourceResource(ctx context.Context, app *applicationapi.Application) error {
+func (a *ApplicationManager) syncSourceResource(ctx context.Context, app *applicationapi.Application) (ctrl.Result, error) {
 	kind := findSourceKind(app)
 	// Based on the source kind, create the appropriate source object and synchronize it with the Kubernetes API server
 	switch kind {
@@ -298,7 +324,7 @@ func (a *ApplicationManager) syncSourceResource(ctx context.Context, app *applic
 		}
 		return a.syncResource(ctx, targetSource, OCIRepoKind)
 	}
-	return nil
+	return ctrl.Result{}, nil
 }
 
 // createEmptyObject generates an uninitialized instance of the specified resource type.
@@ -326,7 +352,7 @@ func createEmptyObject(resourceKind string) client.Object {
 // If the resource already exists, it is updated with the contents of the `targetSource` object.
 // If the resource does not exist, it is created using the contents of the `targetSource` object.
 // Returns an error if the synchronization or creation of the resource fails.
-func (a *ApplicationManager) syncResource(ctx context.Context, targetSource client.Object, resourceKind string) error {
+func (a *ApplicationManager) syncResource(ctx context.Context, targetSource client.Object, resourceKind string) (ctrl.Result, error) {
 	log := ctrl.LoggerFrom(ctx)
 
 	// try to get the current resource from the API server
@@ -338,17 +364,18 @@ func (a *ApplicationManager) syncResource(ctx context.Context, targetSource clie
 	err := a.Client.Get(ctx, resourceKey, currentResource)
 	if err != nil && !apierrors.IsNotFound(err) {
 		log.Error(err, fmt.Sprintf("failed to get %s", resourceKind), resourceKind, resourceKey)
-		return err
+		return ctrl.Result{}, err
 	}
 
 	// if not found, create it
 	if apierrors.IsNotFound(err) {
 		if err := a.Client.Create(ctx, targetSource); err != nil {
-			log.Error(err, fmt.Sprintf("failed to get %s", resourceKind), resourceKind, resourceKey)
-			return err
+			if !apierrors.IsAlreadyExists(err) {
+				log.Error(err, fmt.Sprintf("failed to create %s", resourceKind), resourceKind, resourceKey)
+				return ctrl.Result{}, err
+			}
 		}
-		log.Info(fmt.Sprintf("create %s successful", resourceKind), resourceKind, resourceKey)
-		return nil
+		return ctrl.Result{}, nil
 	}
 
 	// if already exist, update it
@@ -366,14 +393,19 @@ func (a *ApplicationManager) syncResource(ctx context.Context, targetSource clie
 		err = a.updateHelmRelease(ctx, currentResource.(*helmv2b1.HelmRelease), targetSource.(*helmv2b1.HelmRelease))
 	default:
 		log.Error(err, fmt.Sprintf("resource type %s is not supported", resourceKind))
-		return nil
+		return ctrl.Result{}, nil
+	}
+	// If there is a conflict during the update, it indicates that the resource may have been updated by the Flux controller.
+	// In this case, the handler should requeue the resource and wait for the next reconciliation.
+	if apierrors.IsConflict(err) {
+		return ctrl.Result{RequeueAfter: RequeueAfter}, nil
 	}
-	if err != nil {
+	if err != nil && !apierrors.IsConflict(err) {
 		log.Error(err, fmt.Sprintf("failed to update %s", resourceKind), resourceKind, resourceKey)
-		return err
+		return ctrl.Result{}, err
 	}
 
-	return nil
+	return ctrl.Result{}, nil
 }
 
 // updateGitRepository updates the state of a current GitRepository resource to match the provided target GitRepository resource.
@@ -442,23 +474,21 @@ func findSourceKind(app *applicationapi.Application) string {
 	return ""
 }
 
-// generateKustomizationName constructs a unique name for Kustomization based on the provided application,
-// synchronization policy, cluster kind and cluster name. The resulting name is formatted to be lower-case,
-// and is truncated to a maximum of 63 characters if needed.
-
-// generateKustomizationName constructs a unique name for Kustomization based on the provided application,
-func generateKustomizationName(app *applicationapi.Application, clusterKind, clusterName, policyName string) string {
-	name := app.Name + "-" + policyName + "-" + clusterKind + "-" + clusterName
-	name = strings.ToLower(name)
-	if len(name) > 63 {
-		name = name[:63]
+// getSyncPolicyKind get the type of the application's syncPolicy.
+func getSyncPolicyKind(syncPolicy *applicationapi.ApplicationSyncPolicy) string {
+	if syncPolicy.Kustomization != nil {
+		return KustomizationKind
 	}
-	return name
+	if syncPolicy.Helm != nil {
+		return HelmReleaseKind
+	}
+	return ""
 }
 
-// generateHelmReleaseName constructs a unique name for HelmRelease based on the provided application,
-func generateHelmReleaseName(app *applicationapi.Application, clusterKind, clusterName, policyName string) string {
-	name := app.Name + "-" + policyName + "-" + clusterKind + "-" + clusterName
+// generatePolicyResourceName creates a unique name for a policy resource (such as helmRelease or kustomization)
+// based on the provided application, cluster kind, and cluster name.
+func generatePolicyResourceName(policyName, clusterKind, clusterName string) string {
+	name := policyName + "-" + clusterKind + "-" + clusterName
 	name = strings.ToLower(name)
 	if len(name) > 63 {
 		name = name[:63]
@@ -492,3 +522,22 @@ func generatePolicyName(app *applicationapi.Application, index int) string {
 
 	return app.Spec.SyncPolicies[index].Name
 }
+
+func generateFleetKey(app *applicationapi.Application) client.ObjectKey {
+	var fleetName string
+	// if destination of SyncPolicies is not set, we use the destination of application
+	if app.Spec.SyncPolicies[0].Destination == nil || len(app.Spec.SyncPolicies[0].Destination.Fleet) == 0 {
+		// if destination is not set in both SyncPolicies and application, just return ""
+		if app.Spec.Destination == nil {
+			fleetName = ""
+		} else {
+			fleetName = app.Spec.Destination.Fleet
+		}
+	} else {
+		fleetName = app.Spec.SyncPolicies[0].Destination.Fleet
+	}
+	return client.ObjectKey{
+		Namespace: app.Namespace,
+		Name:      fleetName,
+	}
+}
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