Repository: karmada-io/karmada

Issue:
ClaimMetadata not properly cleaned on deletion of Lazy activationPreference propagationpolicy
<!-- Please use this template while reporting a bug and provide as much info as possible. Not doing so may result in your bug not being addressed in a timely manner. Thanks!
-->


**What happened**:
When I first deploy a Deployment and then deploy a PropagationPolicy with `Lazy` activationPreference, no corresponding ResourceBinding is generated. This is normal. 
However, if I then delete this PropagationPolicy, the Deployment still retains the PropagationPolicy-related labels and metadata. Subsequently, when I deploy a new PropagationPolicy with a non-`Lazy` activationPreference, the Deployment fails to match this newly created PropagationPolicy.

**What you expected to happen**:
When a PropagationPolicy with `Lazy` activationPreference is deleted, the ClaimMetadata should be properly cleaned up so that it does not prevent the Resource Template from binding to a new PropagationPolicy.

**How to reproduce it (as minimally and precisely as possible)**:
1. Set up a Karmada environment:
```bash
$ hack/local-up-karmada.sh
```

2. Deploy a Deployment:
```yaml
apiVersion: apps/v1
kind: Deployment
metadata:
  name: nginx
  labels:
    app: nginx
spec:
  replicas: 2
  selector:
    matchLabels:
      app: nginx
  template:
    metadata:
      labels:
        app: nginx
    spec:
      containers:
      - image: nginx
        name: nginx
```

3. Deploy a PropagationPolicy with `Lazy` activationPreference:
```yaml
apiVersion: policy.karmada.io/v1alpha1
kind: PropagationPolicy
metadata:
  name: nginx-propagation
spec:
  activationPreference: Lazy
  resourceSelectors:
  - apiVersion: apps/v1
    kind: Deployment
  placement:
    clusterAffinity:
      clusterNames:
      - member1
      - member2
    replicaScheduling:
      replicaDivisionPreference: Weighted
      replicaSchedulingType: Divided
      weightPreference:
        staticWeightList:
        - targetCluster:
            clusterNames:
            - member1
          weight: 1
        - targetCluster:
            clusterNames:
            - member2
          weight: 1
```
At this point, no ResourceBinding is generated, and the Deployment already has PropagationPolicy-related ClaimMetadata:
```yaml
  annotations:
    propagationpolicy.karmada.io/name: nginx-propagation
    propagationpolicy.karmada.io/namespace: default
  labels:
    app: nginx
    propagationpolicy.karmada.io/permanent-id: 537816cb-e95b-45ca-96c8-e7d5cfadae6e
```

4. Delete the PropagationPolicy with `Lazy` activationPreference:
The Deployment still retains the PropagationPolicy-related ClaimMetadata.

5. Deploy a new PropagationPolicy with non-`Lazy` activationPreference:
```yaml
apiVersion: policy.karmada.io/v1alpha1
kind: PropagationPolicy
metadata:
  name: nginx-propagation-new
spec:
  resourceSelectors:
  - apiVersion: apps/v1
    kind: Deployment
  placement:
    clusterAffinity:
      clusterNames:
      - member1
      - member2
    replicaScheduling:
      replicaDivisionPreference: Weighted
      replicaSchedulingType: Divided
      weightPreference:
        staticWeightList:
        - targetCluster:
            clusterNames:
            - member1
          weight: 1
        - targetCluster:
            clusterNames:
            - member2
          weight: 1
```
At this point, the Deployment does not bind to the new PropagationPolicy to generate a ResourceBinding.

**Anything else we need to know?**:

- **Note**: The issue described above only occurs when you first deploy a Deployment and then deploy a PropagationPolicy with `Lazy` activationPreference.

- **Root Cause**: When a PropagationPolicy is deleted, the detector uses the PropagationPolicy-related labels to list ResourceBindings first. It then looks up the ResourceTemplate based on `resourcebindingSpec.Resource` to clean up the PropagationPolicy-related ClaimMetadata on both the ResourceTemplate and ResourceBinding. The process is illustrated below:

<img src="https://github.com/user-attachments/assets/c568f1c2-6c04-43ce-9496-c1918d08493d" width="300">

  However, if you first deploy a Deployment and then a PropagationPolicy with `Lazy` activationPreference, no ResourceBinding is generated. As a result, during the cleanup process for PropagationPolicy-related ClaimMetadata, the ResourceTemplate is overlooked, leading to residual metadata.

**Environment**:
- Karmada version:
- kubectl-karmada or karmadactl version (the result of `kubectl-karmada version` or `karmadactl version`):
- Others:


Hints from issue comments:

Solution patch:
diff --git a/pkg/detector/detector.go b/pkg/detector/detector.go
index 4d9f04655..bba2303cb 100644
--- a/pkg/detector/detector.go
+++ b/pkg/detector/detector.go
@@ -690,6 +690,64 @@ func (d *ResourceDetector) GetUnstructuredObject(objectKey keys.ClusterWideKey)
 	return unstructuredObj.DeepCopy(), nil
 }
 
+// fetchResourceBinding fetches a ResourceBinding from the client or dynamic client.
+func (d *ResourceDetector) fetchResourceBinding(ctx context.Context, rbNamespace, rbName string) (*workv1alpha2.ResourceBinding, error) {
+	// First try to get ResourceBinding using cached client
+	rb := &workv1alpha2.ResourceBinding{}
+	err := d.Client.Get(ctx, client.ObjectKey{Namespace: rbNamespace, Name: rbName}, rb)
+	if err != nil {
+		if apierrors.IsNotFound(err) {
+			// If not found with client, try using dynamic client
+			gvr := workv1alpha2.SchemeGroupVersion.WithResource(workv1alpha2.ResourcePluralResourceBinding)
+			unstructuredRB, dynamicErr := d.DynamicClient.Resource(gvr).Namespace(rbNamespace).Get(ctx, rbName, metav1.GetOptions{})
+			if dynamicErr != nil {
+				return nil, dynamicErr
+			}
+
+			// Convert unstructured to ResourceBinding
+			if err = helper.ConvertToTypedObject(unstructuredRB, rb); err != nil {
+				klog.Errorf("Failed to convert unstructured to ResourceBinding(%s/%s): %v", rbNamespace, rbName, err)
+				return nil, err
+			}
+
+			return rb, nil
+		}
+		klog.Errorf("Failed to get ResourceBinding(%s/%s): %v", rbNamespace, rbName, err)
+		return nil, err
+	}
+
+	return rb, nil
+}
+
+// fetchClusterResourceBinding fetches a ClusterResourceBinding from the client or dynamic client.
+func (d *ResourceDetector) fetchClusterResourceBinding(ctx context.Context, crbName string) (*workv1alpha2.ClusterResourceBinding, error) {
+	// First try to get ClusterResourceBinding using cached client
+	crb := &workv1alpha2.ClusterResourceBinding{}
+	err := d.Client.Get(ctx, client.ObjectKey{Name: crbName}, crb)
+	if err != nil {
+		if apierrors.IsNotFound(err) {
+			// If not found with client, try using dynamic client
+			gvr := workv1alpha2.SchemeGroupVersion.WithResource(workv1alpha2.ResourcePluralClusterResourceBinding)
+			unstructuredRB, dynamicErr := d.DynamicClient.Resource(gvr).Get(ctx, crbName, metav1.GetOptions{})
+			if dynamicErr != nil {
+				return nil, dynamicErr
+			}
+
+			// Convert unstructured to ClusterResourceBinding
+			if err = helper.ConvertToTypedObject(unstructuredRB, crb); err != nil {
+				klog.Errorf("Failed to convert unstructured to ClusterResourceBinding(%s): %v", crbName, err)
+				return nil, err
+			}
+
+			return crb, nil
+		}
+		klog.Errorf("Failed to get ClusterResourceBinding(%s): %v", crbName, err)
+		return nil, err
+	}
+
+	return crb, nil
+}
+
 // ClaimPolicyForObject set policy identifier which the object associated with.
 // It will add policy labels and annotations to the object, and update the obj with the content returned by the Server, ensuring that the obj used when generating the binding is the latest.
 func (d *ResourceDetector) ClaimPolicyForObject(object *unstructured.Unstructured, policy *policyv1alpha1.PropagationPolicy) (string, error) {
@@ -940,7 +998,7 @@ func (d *ResourceDetector) ReconcilePropagationPolicy(key util.QueueKey) error {
 
 	if !propagationObject.DeletionTimestamp.IsZero() {
 		klog.Infof("PropagationPolicy(%s) is being deleted.", nkey.NamespaceKey())
-		if err = d.HandlePropagationPolicyDeletion(propagationObject.Labels[policyv1alpha1.PropagationPolicyPermanentIDLabel]); err != nil {
+		if err = d.HandlePropagationPolicyDeletion(propagationObject.Labels[policyv1alpha1.PropagationPolicyPermanentIDLabel], propagationObject.Spec.ResourceSelectors); err != nil {
 			return err
 		}
 		if controllerutil.RemoveFinalizer(propagationObject, util.PropagationPolicyControllerFinalizer) {
@@ -1012,7 +1070,7 @@ func (d *ResourceDetector) ReconcileClusterPropagationPolicy(key util.QueueKey)
 
 	if !propagationObject.DeletionTimestamp.IsZero() {
 		klog.Infof("ClusterPropagationPolicy(%s) is being deleted.", nkey.NamespaceKey())
-		if err = d.HandleClusterPropagationPolicyDeletion(propagationObject.Labels[policyv1alpha1.ClusterPropagationPolicyPermanentIDLabel]); err != nil {
+		if err = d.HandleClusterPropagationPolicyDeletion(propagationObject.Labels[policyv1alpha1.ClusterPropagationPolicyPermanentIDLabel], propagationObject.Spec.ResourceSelectors); err != nil {
 			return err
 		}
 		if controllerutil.RemoveFinalizer(propagationObject, util.ClusterPropagationPolicyControllerFinalizer) {
@@ -1033,32 +1091,26 @@ func (d *ResourceDetector) ReconcileClusterPropagationPolicy(key util.QueueKey)
 // the resource template a change to match another policy).
 //
 // Note: The relevant ResourceBinding will continue to exist until the resource template is gone.
-func (d *ResourceDetector) HandlePropagationPolicyDeletion(policyID string) error {
+func (d *ResourceDetector) HandlePropagationPolicyDeletion(policyID string, resources []policyv1alpha1.ResourceSelector) error {
 	claimMetadata := labels.Set{policyv1alpha1.PropagationPolicyPermanentIDLabel: policyID}
-	rbs, err := helper.GetResourceBindings(d.Client, claimMetadata)
-	if err != nil {
-		klog.Errorf("Failed to list propagation bindings with policy permanentID(%s): %v", policyID, err)
-		return err
-	}
-
 	var errs []error
-	for index, binding := range rbs.Items {
-		// Must remove the claim metadata, such as labels and annotations, from the resource template ahead of ResourceBinding,
-		// otherwise might lose the chance to do that in a retry loop (in particular, the claim metadata was successfully removed
-		// from ResourceBinding, but resource template not), since the ResourceBinding will not be listed again.
-		if err := d.CleanupResourceTemplateClaimMetadata(binding.Spec.Resource, claimMetadata, CleanupPPClaimMetadata); err != nil {
-			klog.Errorf("Failed to clean up claim metadata from resource(%s-%s/%s) when propagationPolicy removed, error: %v",
-				binding.Spec.Resource.Kind, binding.Spec.Resource.Namespace, binding.Spec.Resource.Name, err)
-			errs = append(errs, err)
-			// Skip cleaning up policy labels and annotations from ResourceBinding, give a chance to do that in a retry loop.
-			continue
+	for _, resource := range util.ExtractUniqueNamespacedSelectors(resources) {
+		objRef := workv1alpha2.ObjectReference{
+			APIVersion: resource.APIVersion,
+			Kind:       resource.Kind,
+			Namespace:  resource.Namespace,
 		}
 
-		// Clean up the claim metadata from the reference binding so that the karmada scheduler won't reschedule the binding.
-		if err := d.CleanupResourceBindingClaimMetadata(&rbs.Items[index], claimMetadata, CleanupPPClaimMetadata); err != nil {
-			klog.Errorf("Failed to clean up claim metadata from resource binding(%s/%s) when propagationPolicy removed, error: %v",
-				binding.Namespace, binding.Name, err)
+		rawObjects, err := helper.FetchResourceTemplatesByLabelSelector(d.DynamicClient, d.InformerManager, d.RESTMapper, objRef, labels.SelectorFromSet(claimMetadata))
+		if err != nil {
 			errs = append(errs, err)
+			continue
+		}
+		for _, rawObject := range rawObjects {
+			err := d.handleResourceTemplateAndBindingCleanup(rawObject, objRef, claimMetadata, CleanupPPClaimMetadata)
+			if err != nil {
+				errs = append(errs, err)
+			}
 		}
 	}
 	return errors.NewAggregate(errs)
@@ -1069,66 +1121,64 @@ func (d *ResourceDetector) HandlePropagationPolicyDeletion(policyID string) erro
 // the resource template a change to match another policy).
 //
 // Note: The relevant ClusterResourceBinding or ResourceBinding will continue to exist until the resource template is gone.
-func (d *ResourceDetector) HandleClusterPropagationPolicyDeletion(policyID string) error {
+func (d *ResourceDetector) HandleClusterPropagationPolicyDeletion(policyID string, resources []policyv1alpha1.ResourceSelector) error {
 	var errs []error
-	labelSet := labels.Set{
-		policyv1alpha1.ClusterPropagationPolicyPermanentIDLabel: policyID,
-	}
+	claimMetadata := labels.Set{policyv1alpha1.ClusterPropagationPolicyPermanentIDLabel: policyID}
 
-	// load the ClusterResourceBindings which labeled with current policy
-	crbs, err := helper.GetClusterResourceBindings(d.Client, labelSet)
-	if err != nil {
-		klog.Errorf("Failed to list clusterResourceBindings with clusterPropagationPolicy permanentID(%s), error: %v", policyID, err)
-		errs = append(errs, err)
-	} else if len(crbs.Items) > 0 {
-		for index, binding := range crbs.Items {
-			// Must remove the claim metadata, such as labels and annotations, from the resource template ahead of
-			// ClusterResourceBinding, otherwise might lose the chance to do that in a retry loop (in particular, the
-			// claim metadata was successfully removed from ClusterResourceBinding, but resource template not), since the
-			// ClusterResourceBinding will not be listed again.
-			if err := d.CleanupResourceTemplateClaimMetadata(binding.Spec.Resource, labelSet, CleanupCPPClaimMetadata); err != nil {
-				klog.Errorf("Failed to clean up claim metadata from resource(%s-%s) when clusterPropagationPolicy removed, error: %v",
-					binding.Spec.Resource.Kind, binding.Spec.Resource.Name, err)
-				// Skip cleaning up policy labels and annotations from ClusterResourceBinding, give a chance to do that in a retry loop.
-				continue
-			}
+	for _, resource := range util.ExtractUniqueNamespacedSelectors(resources) {
+		objRef := workv1alpha2.ObjectReference{
+			APIVersion: resource.APIVersion,
+			Kind:       resource.Kind,
+			Namespace:  resource.Namespace,
+		}
 
-			// Clean up the claim metadata from the reference binding so that the Karmada scheduler won't reschedule the binding.
-			if err := d.CleanupClusterResourceBindingClaimMetadata(&crbs.Items[index], labelSet); err != nil {
-				klog.Errorf("Failed to clean up claim metadata from clusterResourceBinding(%s) when clusterPropagationPolicy removed, error: %v",
-					binding.Name, err)
+		rawObjects, err := helper.FetchResourceTemplatesByLabelSelector(d.DynamicClient, d.InformerManager, d.RESTMapper, objRef, labels.SelectorFromSet(claimMetadata))
+		if err != nil {
+			errs = append(errs, err)
+			continue
+		}
+		for _, rawObject := range rawObjects {
+			err := d.handleResourceTemplateAndBindingCleanup(rawObject, objRef, claimMetadata, CleanupCPPClaimMetadata)
+			if err != nil {
 				errs = append(errs, err)
 			}
 		}
 	}
 
-	// load the ResourceBindings which labeled with current policy
-	rbs, err := helper.GetResourceBindings(d.Client, labelSet)
-	if err != nil {
-		klog.Errorf("Failed to list resourceBindings with clusterPropagationPolicy permanentID(%s), error: %v", policyID, err)
-		errs = append(errs, err)
-	} else if len(rbs.Items) > 0 {
-		for index, binding := range rbs.Items {
-			// Must remove the claim metadata, such as labels and annotations, from the resource template ahead of ResourceBinding,
-			// otherwise might lose the chance to do that in a retry loop (in particular, the label was successfully
-			// removed from ResourceBinding, but resource template not), since the ResourceBinding will not be listed again.
-			if err := d.CleanupResourceTemplateClaimMetadata(binding.Spec.Resource, labelSet, CleanupCPPClaimMetadata); err != nil {
-				klog.Errorf("Failed to clean up claim metadata from resource(%s-%s/%s) when clusterPropagationPolicy removed, error: %v",
-					binding.Spec.Resource.Kind, binding.Spec.Resource.Namespace, binding.Spec.Resource.Name, err)
-				errs = append(errs, err)
-				// Skip cleaning up policy labels and annotations from ResourceBinding, give a chance to do that in a retry loop.
-				continue
-			}
+	return errors.NewAggregate(errs)
+}
 
-			// Clean up the claim metadata from the reference binding so that the Karmada scheduler won't reschedule the binding.
-			if err := d.CleanupResourceBindingClaimMetadata(&rbs.Items[index], labelSet, CleanupCPPClaimMetadata); err != nil {
-				klog.Errorf("Failed to clean up claim metadata from resourceBinding(%s/%s) when clusterPropagationPolicy removed, error: %v",
-					binding.Namespace, binding.Name, err)
-				errs = append(errs, err)
-			}
+func (d *ResourceDetector) handleResourceTemplateAndBindingCleanup(template *unstructured.Unstructured, objRef workv1alpha2.ObjectReference, targetClaimMetadata map[string]string, cleanupFunc func(obj metav1.Object)) error {
+	bindingName := names.GenerateBindingName(template.GetKind(), template.GetName())
+	if template.GetNamespace() != "" {
+		// Clean up the claim metadata from the reference binding so that the karmada scheduler won't reschedule the binding.
+		// Must remove the claim metadata, such as labels and annotations, from the ResourceBinding ahead of resource template,
+		// otherwise might lose the chance to do that in a retry loop (in particular, the claim metadata was successfully removed
+		// from resource template, but ResourceBinding not), since the resource template will not be listed again.
+		if err := d.CleanupResourceBindingClaimMetadata(template.GetNamespace(), bindingName, targetClaimMetadata, cleanupFunc); err != nil {
+			klog.Errorf("Failed to clean up claim metadata from ResourceBinding(%s/%s), error: %v",
+				objRef.Namespace, bindingName, err)
+			return err
+		}
+	} else {
+		// Clean up the claim metadata from the reference binding so that the Karmada scheduler won't reschedule the binding.
+		// Must remove the claim metadata, such as labels and annotations, from the ClusterResourceBinding ahead of resource template,
+		// otherwise might lose the chance to do that in a retry loop (in particular, the claim metadata was successfully removed
+		// from resource template, but ClusterResourceBinding not), since the resource template will not be listed again.
+		if err := d.CleanupClusterResourceBindingClaimMetadata(bindingName, targetClaimMetadata, cleanupFunc); err != nil {
+			klog.Errorf("Failed to clean up claim metadata from ClusterResourceBinding(%s), error: %v",
+				bindingName, err)
+			return err
 		}
 	}
-	return errors.NewAggregate(errs)
+
+	if err := d.CleanupResourceTemplateClaimMetadata(template, objRef, targetClaimMetadata, cleanupFunc); err != nil {
+		klog.Errorf("Failed to clean up claim metadata from resource(%s-%s/%s) when propagationPolicy removed, error: %v",
+			template.GetKind(), template.GetNamespace(), template.GetName(), err)
+		return err
+	}
+
+	return nil
 }
 
 // HandlePropagationPolicyCreationOrUpdate handles PropagationPolicy add and update event.
@@ -1263,24 +1313,14 @@ func (d *ResourceDetector) HandleClusterPropagationPolicyCreationOrUpdate(policy
 }
 
 // CleanupResourceTemplateClaimMetadata removes claim metadata, such as labels and annotations, from object referencing by objRef.
-func (d *ResourceDetector) CleanupResourceTemplateClaimMetadata(objRef workv1alpha2.ObjectReference, targetClaimMetadata map[string]string, cleanupFunc func(obj metav1.Object)) error {
+func (d *ResourceDetector) CleanupResourceTemplateClaimMetadata(obj *unstructured.Unstructured, objRef workv1alpha2.ObjectReference, targetClaimMetadata map[string]string, cleanupFunc func(obj metav1.Object)) error {
 	gvr, err := restmapper.GetGroupVersionResource(d.RESTMapper, schema.FromAPIVersionAndKind(objRef.APIVersion, objRef.Kind))
 	if err != nil {
 		klog.Errorf("Failed to convert GVR from GVK(%s/%s), err: %v", objRef.APIVersion, objRef.Kind, err)
 		return err
 	}
-
+	workload := obj.DeepCopy()
 	return retry.RetryOnConflict(retry.DefaultRetry, func() (err error) {
-		workload, err := d.DynamicClient.Resource(gvr).Namespace(objRef.Namespace).Get(context.TODO(), objRef.Name, metav1.GetOptions{})
-		if err != nil {
-			// do nothing if resource template not exist, it might have been removed.
-			if apierrors.IsNotFound(err) {
-				return nil
-			}
-			klog.Errorf("Failed to fetch resource(kind=%s, %s/%s): err is %v", objRef.Kind, objRef.Namespace, objRef.Name, err)
-			return err
-		}
-
 		if !NeedCleanupClaimMetadata(workload, targetClaimMetadata) {
 			klog.Infof("No need to clean up the claim metadata on resource(kind=%s, %s/%s) since they have changed", workload.GetKind(), workload.GetNamespace(), workload.GetName())
 			return nil
@@ -1291,15 +1331,37 @@ func (d *ResourceDetector) CleanupResourceTemplateClaimMetadata(objRef workv1alp
 		_, err = d.DynamicClient.Resource(gvr).Namespace(workload.GetNamespace()).Update(context.TODO(), workload, metav1.UpdateOptions{})
 		if err != nil {
 			klog.Errorf("Failed to update resource(kind=%s, %s/%s): err is %v", workload.GetKind(), workload.GetNamespace(), workload.GetName(), err)
+			if apierrors.IsConflict(err) {
+				newWorkload, getErr := d.DynamicClient.Resource(gvr).Namespace(workload.GetNamespace()).Get(context.TODO(), workload.GetName(), metav1.GetOptions{})
+				if getErr != nil {
+					// do nothing if resource template not exist, it might have been removed.
+					if apierrors.IsNotFound(getErr) {
+						return nil
+					}
+					return getErr
+				}
+				workload = newWorkload
+			}
 			return err
 		}
-		klog.V(2).Infof("Updated resource template(kind=%s, %s/%s) successfully", workload.GetKind(), workload.GetNamespace(), workload.GetName())
+		klog.V(2).Infof("Clean claimed label for resource template(kind=%s, %s/%s) successfully", workload.GetKind(), workload.GetNamespace(), workload.GetName())
 		return nil
 	})
 }
 
 // CleanupResourceBindingClaimMetadata removes claim metadata, such as labels and annotations, from resource binding.
-func (d *ResourceDetector) CleanupResourceBindingClaimMetadata(rb *workv1alpha2.ResourceBinding, targetClaimMetadata map[string]string, cleanupFunc func(obj metav1.Object)) error {
+func (d *ResourceDetector) CleanupResourceBindingClaimMetadata(rbNamespace, rbName string, targetClaimMetadata map[string]string, cleanupFunc func(obj metav1.Object)) error {
+	var rb *workv1alpha2.ResourceBinding
+	var err error
+
+	rb, err = d.fetchResourceBinding(context.TODO(), rbNamespace, rbName)
+	if err != nil {
+		if apierrors.IsNotFound(err) {
+			return nil // do nothing if resource binding not exist, it might have been removed.
+		}
+		return err
+	}
+
 	return retry.RetryOnConflict(retry.DefaultRetry, func() (err error) {
 		if !NeedCleanupClaimMetadata(rb, targetClaimMetadata) {
 			klog.Infof("No need to clean up the claim metadata on ResourceBinding(%s/%s) since they have changed", rb.GetNamespace(), rb.GetName())
@@ -1311,35 +1373,72 @@ func (d *ResourceDetector) CleanupResourceBindingClaimMetadata(rb *workv1alpha2.
 			return nil
 		}
 
-		updated := &workv1alpha2.ResourceBinding{}
-		if err = d.Client.Get(context.TODO(), client.ObjectKey{Namespace: rb.GetNamespace(), Name: rb.GetName()}, updated); err == nil {
-			rb = updated.DeepCopy()
-		} else {
-			klog.Errorf("Failed to get updated ResourceBinding(%s/%s): %v", rb.GetNamespace(), rb.GetName(), err)
+		if apierrors.IsConflict(updateErr) {
+			updated := &workv1alpha2.ResourceBinding{}
+			gvr := workv1alpha2.SchemeGroupVersion.WithResource(workv1alpha2.ResourcePluralResourceBinding)
+			if unstructuredRB, dynamicErr := d.DynamicClient.Resource(gvr).Namespace(rbNamespace).Get(context.TODO(), rbName, metav1.GetOptions{}); dynamicErr == nil {
+				// Convert unstructured to ResourceBinding
+				if convertErr := helper.ConvertToTypedObject(unstructuredRB, updated); convertErr != nil {
+					klog.Errorf("Failed to convert unstructured to ResourceBinding(%s/%s): %v", rbNamespace, rbName, convertErr)
+					return convertErr
+				}
+				rb = updated
+			} else {
+				if apierrors.IsNotFound(dynamicErr) {
+					return nil // do nothing if resource binding not exist, it might have been removed.
+				}
+				klog.Errorf("Failed to get updated ResourceBinding(%s/%s): %v", rbNamespace, rbName, dynamicErr)
+				return dynamicErr
+			}
 		}
+
 		return updateErr
 	})
 }
 
 // CleanupClusterResourceBindingClaimMetadata removes claim metadata, such as labels and annotations, from cluster resource binding.
-func (d *ResourceDetector) CleanupClusterResourceBindingClaimMetadata(crb *workv1alpha2.ClusterResourceBinding, targetClaimMetadata map[string]string) error {
+func (d *ResourceDetector) CleanupClusterResourceBindingClaimMetadata(crbName string, targetClaimMetadata map[string]string, cleanupFunc func(obj metav1.Object)) error {
+	var crb *workv1alpha2.ClusterResourceBinding
+	var err error
+
+	crb, err = d.fetchClusterResourceBinding(context.TODO(), crbName)
+	if err != nil {
+		if apierrors.IsNotFound(err) {
+			return nil // do nothing if resource binding not exist, it might have been removed.
+		}
+		return err
+	}
+
 	return retry.RetryOnConflict(retry.DefaultRetry, func() (err error) {
 		if !NeedCleanupClaimMetadata(crb, targetClaimMetadata) {
 			klog.Infof("No need to clean up the claim metadata on ClusterResourceBinding(%s) since they have changed", crb.GetName())
 			return nil
 		}
-		CleanupCPPClaimMetadata(crb)
+		cleanupFunc(crb)
 		updateErr := d.Client.Update(context.TODO(), crb)
 		if updateErr == nil {
 			return nil
 		}
 
-		updated := &workv1alpha2.ClusterResourceBinding{}
-		if err = d.Client.Get(context.TODO(), client.ObjectKey{Name: crb.GetName()}, updated); err == nil {
-			crb = updated.DeepCopy()
-		} else {
-			klog.Errorf("Failed to get updated ClusterResourceBinding(%s): %v", crb.GetName(), err)
+		if apierrors.IsConflict(updateErr) {
+			updated := &workv1alpha2.ClusterResourceBinding{}
+			gvr := workv1alpha2.SchemeGroupVersion.WithResource(workv1alpha2.ResourcePluralClusterResourceBinding)
+			if unstructuredRB, dynamicErr := d.DynamicClient.Resource(gvr).Get(context.TODO(), crbName, metav1.GetOptions{}); dynamicErr == nil {
+				// Convert unstructured to ClusterResourceBinding
+				if convertErr := helper.ConvertToTypedObject(unstructuredRB, updated); convertErr != nil {
+					klog.Errorf("Failed to convert unstructured to ClusterResourceBinding(%s): %v", crbName, convertErr)
+					return convertErr
+				}
+				crb = updated
+			} else {
+				if apierrors.IsNotFound(dynamicErr) {
+					return nil // do nothing if resource binding not exist, it might have been removed.
+				}
+				klog.Errorf("Failed to get updated ClusterResourceBinding(%s): %v", crbName, dynamicErr)
+				return dynamicErr
+			}
 		}
+
 		return updateErr
 	})
 }
diff --git a/pkg/detector/policy.go b/pkg/detector/policy.go
index a46c0b098..755433c40 100644
--- a/pkg/detector/policy.go
+++ b/pkg/detector/policy.go
@@ -110,14 +110,33 @@ func (d *ResourceDetector) getAndApplyPolicy(object *unstructured.Unstructured,
 	resourceChangeByKarmada bool, policyNamespace, policyName, claimedID string) error {
 	matchedPropagationPolicy := &policyv1alpha1.PropagationPolicy{}
 	err := d.Client.Get(context.TODO(), client.ObjectKey{Namespace: policyNamespace, Name: policyName}, matchedPropagationPolicy)
-	if err != nil {
-		if apierrors.IsNotFound(err) {
-			klog.V(4).Infof("PropagationPolicy(%s/%s) has been removed.", policyNamespace, policyName)
-			return d.HandlePropagationPolicyDeletion(claimedID)
+	if apierrors.IsNotFound(err) {
+		// The expected PropagationPolicy has been removed, indicating the cleanup process has completed.
+		// However, if the claimed data still exists in the resource template, it likely means the cleanup
+		// for the resource template was missed. This can happen if the claimed data was added to the resource
+		// template and then the PropagationPolicy was removed very shortly after.
+		// When cleaning up resource templates based on PropagationPolicy deletion, the cache may not have received
+		// the resource template update in time, resulting in a missed cleanup.
+		klog.V(4).Infof("PropagationPolicy(%s/%s) has been removed.", policyNamespace, policyName)
+		claimMetadata := labels.Set{policyv1alpha1.PropagationPolicyPermanentIDLabel: claimedID}
+		objRef := workv1alpha2.ObjectReference{
+			APIVersion: object.GetAPIVersion(),
+			Kind:       object.GetKind(),
+			Namespace:  object.GetNamespace(),
+			Name:       object.GetName(),
 		}
+		return d.handleResourceTemplateAndBindingCleanup(object, objRef, claimMetadata, CleanupPPClaimMetadata)
+	}
+	if err != nil {
 		klog.Errorf("Failed to get claimed policy(%s/%s),: %v", policyNamespace, policyName, err)
 		return err
 	}
+
+	// If the policy is being deleted, we should not apply it. Instead, waiting for the next reconcile to clean up metadata.
+	if !matchedPropagationPolicy.DeletionTimestamp.IsZero() {
+		return fmt.Errorf("policy(%s/%s) is being deleted", policyNamespace, policyName)
+	}
+
 	// Some resources are available in more than one group in the same kubernetes version.
 	// Therefore, the following scenarios occurs:
 	// In v1.21 kubernetes cluster, Ingress are available in both networking.k8s.io and extensions groups.
@@ -142,15 +161,33 @@ func (d *ResourceDetector) getAndApplyClusterPolicy(object *unstructured.Unstruc
 	resourceChangeByKarmada bool, policyName, policyID string) error {
 	matchedClusterPropagationPolicy := &policyv1alpha1.ClusterPropagationPolicy{}
 	err := d.Client.Get(context.TODO(), client.ObjectKey{Name: policyName}, matchedClusterPropagationPolicy)
-	if err != nil {
-		if apierrors.IsNotFound(err) {
-			klog.V(4).Infof("ClusterPropagationPolicy(%s) has been removed.", policyName)
-			return d.HandleClusterPropagationPolicyDeletion(policyID)
+	if apierrors.IsNotFound(err) {
+		// The expected ClusterPropagationPolicy has been removed, indicating the cleanup process has completed.
+		// However, if the claimed data still exists in the resource template, it likely means the cleanup
+		// for the resource template was missed. This can happen if the claimed data was added to the resource
+		// template and then the ClusterPropagationPolicy was removed very shortly after.
+		// When cleaning up resource templates based on ClusterPropagationPolicy deletion, the cache may not have received
+		// the resource template update in time, resulting in a missed cleanup.
+		klog.V(4).Infof("ClusterPropagationPolicy(%s) has been removed.", policyName)
+		claimMetadata := labels.Set{policyv1alpha1.ClusterPropagationPolicyPermanentIDLabel: policyID}
+		objRef := workv1alpha2.ObjectReference{
+			APIVersion: object.GetAPIVersion(),
+			Kind:       object.GetKind(),
+			Namespace:  object.GetNamespace(),
+			Name:       object.GetName(),
 		}
-
+		return d.handleResourceTemplateAndBindingCleanup(object, objRef, claimMetadata, CleanupCPPClaimMetadata)
+	}
+	if err != nil {
 		klog.Errorf("Failed to get claimed policy(%s),: %v", policyName, err)
 		return err
 	}
+
+	if !matchedClusterPropagationPolicy.DeletionTimestamp.IsZero() {
+		// If the cluster policy is being deleted, we should not apply it. Instead, waiting for the next reconcile to clean up metadata.
+		return fmt.Errorf("cluster policy(%s) is being deleted", policyName)
+	}
+
 	// Some resources are available in more than one group in the same kubernetes version.
 	// Therefore, the following scenarios occurs:
 	// In v1.21 kubernetes cluster, Ingress are available in both networking.k8s.io and extensions groups.
diff --git a/pkg/util/selector.go b/pkg/util/selector.go
index 83a152747..adb5a77c7 100644
--- a/pkg/util/selector.go
+++ b/pkg/util/selector.go
@@ -239,3 +239,24 @@ func matchZones(zoneMatchExpression *corev1.NodeSelectorRequirement, zones []str
 		return false
 	}
 }
+
+// ExtractUniqueNamespacedSelectors returns a new slice of ResourceSelector deduplicated by
+// APIVersion, Kind and Namespace. The returned selectors only contain APIVersion, Kind and Namespace;
+// other fields (e.g. Name, LabelSelector) are intentionally discarded.
+func ExtractUniqueNamespacedSelectors(selectors []policyv1alpha1.ResourceSelector) []policyv1alpha1.ResourceSelector {
+	var results []policyv1alpha1.ResourceSelector
+	handled := make(map[string]bool)
+	for _, selector := range selectors {
+		key := selector.APIVersion + "|" + selector.Kind + "|" + selector.Namespace
+		if handled[key] {
+			continue
+		}
+		results = append(results, policyv1alpha1.ResourceSelector{
+			APIVersion: selector.APIVersion,
+			Kind:       selector.Kind,
+			Namespace:  selector.Namespace,
+		})
+		handled[key] = true
+	}
+	return results
+}



Test patch:
diff --git a/pkg/util/selector_test.go b/pkg/util/selector_test.go
index a9194e492..15cad2020 100644
--- a/pkg/util/selector_test.go
+++ b/pkg/util/selector_test.go
@@ -1087,3 +1087,149 @@ func Test_matchZones(t *testing.T) {
 		})
 	}
 }
+
+func TestExtractUniqueNamespacedSelectors(t *testing.T) {
+	tests := []struct {
+		name      string
+		selectors []policyv1alpha1.ResourceSelector
+		want      []policyv1alpha1.ResourceSelector
+	}{
+		{
+			name:      "empty selectors",
+			selectors: nil,
+			want:      nil,
+		},
+		{
+			name: "single selector",
+			selectors: []policyv1alpha1.ResourceSelector{
+				{
+					APIVersion: "v1",
+					Kind:       "Pod",
+				},
+			},
+			want: []policyv1alpha1.ResourceSelector{
+				{
+					APIVersion: "v1",
+					Kind:       "Pod",
+				},
+			},
+		},
+		{
+			name: "multiple different selectors",
+			selectors: []policyv1alpha1.ResourceSelector{
+				{
+					APIVersion: "v1",
+					Kind:       "Pod",
+				},
+				{
+					APIVersion: "apps/v1",
+					Kind:       "Deployment",
+				},
+				{
+					APIVersion: "v1",
+					Kind:       "Service",
+				},
+			},
+			want: []policyv1alpha1.ResourceSelector{
+				{
+					APIVersion: "v1",
+					Kind:       "Pod",
+				},
+				{
+					APIVersion: "apps/v1",
+					Kind:       "Deployment",
+				},
+				{
+					APIVersion: "v1",
+					Kind:       "Service",
+				},
+			},
+		},
+		{
+			name: "duplicate selectors should be deduplicated",
+			selectors: []policyv1alpha1.ResourceSelector{
+				{
+					APIVersion: "v1",
+					Kind:       "Pod",
+				},
+				{
+					APIVersion: "apps/v1",
+					Kind:       "Deployment",
+				},
+				{
+					APIVersion: "v1",
+					Kind:       "Pod", // duplicate
+				},
+				{
+					APIVersion: "apps/v1",
+					Kind:       "Deployment", // duplicate
+				},
+			},
+			want: []policyv1alpha1.ResourceSelector{
+				{
+					APIVersion: "v1",
+					Kind:       "Pod",
+				},
+				{
+					APIVersion: "apps/v1",
+					Kind:       "Deployment",
+				},
+			},
+		},
+		{
+			name: "selectors with same kind but different API versions",
+			selectors: []policyv1alpha1.ResourceSelector{
+				{
+					APIVersion: "extensions/v1beta1",
+					Kind:       "Deployment",
+				},
+				{
+					APIVersion: "apps/v1",
+					Kind:       "Deployment",
+				},
+			},
+			want: []policyv1alpha1.ResourceSelector{
+				{
+					APIVersion: "extensions/v1beta1",
+					Kind:       "Deployment",
+				},
+				{
+					APIVersion: "apps/v1",
+					Kind:       "Deployment",
+				},
+			},
+		},
+		{
+			name: "selectors with custom resources",
+			selectors: []policyv1alpha1.ResourceSelector{
+				{
+					APIVersion: "custom.io/v1alpha1",
+					Kind:       "CustomResource",
+				},
+				{
+					APIVersion: "example.com/v1beta1",
+					Kind:       "ExampleResource",
+				},
+			},
+			want: []policyv1alpha1.ResourceSelector{
+				{
+					APIVersion: "custom.io/v1alpha1",
+					Kind:       "CustomResource",
+				},
+				{
+					APIVersion: "example.com/v1beta1",
+					Kind:       "ExampleResource",
+				},
+			},
+		},
+	}
+
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			got := ExtractUniqueNamespacedSelectors(tt.selectors)
+			if !reflect.DeepEqual(got, tt.want) {
+				t.Errorf("ExtractUniqueNamespacedSelectors() = %v, want %v", got, tt.want)
+			}
+		})
+	}
+}