Repository: karmada-io/karmada

Issue:
When MultiClusterService and MultiClusterIngress are used for the same Service, the repeated reporting of EndpointSlice resources leads to duplicate backend IPs
<!-- 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 user use `CrossCluster` type `MultiClusterService` and `MultiClusterIngress` resource for the same `Service`, the controller will report the repeated `EndpointSlice` resource to the Karmada control-plane, which will lead to duplicate backend IPs.

**What you expected to happen**:

Even when both `MultiClusterService` and `MultiClusterIngress` are use simultaneously for the same `Service`, the controller will not redundantly report `EndpointSlice` resources to the Karmada control-plane.

**How to reproduce it (as minimally and precisely as possible)**:

0. Install ServiceExport CRD on the member clusters.

<details>
<summary>unfold me to see the yaml</summary>

```yaml
# propagate ServiceExport CRD
apiVersion: policy.karmada.io/v1alpha1
kind: ClusterPropagationPolicy
metadata:
  name: serviceexport-policy
spec:
  resourceSelectors:
    - apiVersion: apiextensions.k8s.io/v1
      kind: CustomResourceDefinition
      name: serviceexports.multicluster.x-k8s.io
  placement:
    clusterAffinity:
      clusterNames:
        - member1
        - member2
---
# propagate ServiceImport CRD
apiVersion: policy.karmada.io/v1alpha1
kind: ClusterPropagationPolicy
metadata:
  name: serviceimport-policy
spec:
  resourceSelectors:
    - apiVersion: apiextensions.k8s.io/v1
      kind: CustomResourceDefinition
      name: serviceimports.multicluster.x-k8s.io
  placement:
    clusterAffinity:
      clusterNames:
        - member1
        - member2
```

</details>

1, Apply the deployment and service resource to the `member1` cluster.

<details>
<summary>unfold me to see the yaml</summary>

```yaml
apiVersion: apps/v1
kind: Deployment
metadata:
  name: serve
spec:
  replicas: 1
  selector:
    matchLabels:
      app: serve
  template:
    metadata:
      labels:
        app: serve
    spec:
      containers:
      - name: serve
        image: jeremyot/serve:0a40de8
        args:
        - "--message='hello from cluster member1 (Node: {{env \"NODE_NAME\"}} Pod: {{env \"POD_NAME\"}} Address: {{addr}})'"
        env:
          - name: NODE_NAME
            valueFrom:
              fieldRef:
                fieldPath: spec.nodeName
          - name: POD_NAME
            valueFrom:
              fieldRef:
                fieldPath: metadata.name
---
apiVersion: v1
kind: Service
metadata:
  name: serve
  labels:
    cz: test
spec:
  ports:
  - port: 80
    targetPort: 8080
  selector:
    app: serve
---
apiVersion: policy.karmada.io/v1alpha1
kind: PropagationPolicy
metadata:
  name: mcs-workload
spec:
  resourceSelectors:
    - apiVersion: apps/v1
      kind: Deployment
      name: serve
    - apiVersion: v1
      kind: Service
      name: serve
  placement:
    clusterAffinity:
      clusterNames:
        - member1
```
</details>

2. Apply the MultiClusterService to export the service from member1 to member2:

```yaml
apiVersion: networking.karmada.io/v1alpha1
kind: MultiClusterService
metadata:
  name: serve
spec:
  consumerClusters:
  - name: member2
  providerClusters:
  - name: member1
  types:
  - CrossCluster
```

3. Apply ServiceExport resource to the `member1` cluster to export the target service.

```yaml
kind: ServiceExport
metadata:
  name: serve
---
apiVersion: policy.karmada.io/v1alpha1
kind: PropagationPolicy
metadata:
  name: serve-export-policy
spec:
  resourceSelectors:
    - apiVersion: multicluster.x-k8s.io/v1alpha1
      kind: ServiceExport
      name: serve
  placement:
    clusterAffinity:
      clusterNames:
        - member2
```

And then you can find that the same EndpointSlices are collected to the Karmada control-plane:

```cmd
# kubectl get endpointslices.discovery.k8s.io
NAME                                   ADDRESSTYPE   PORTS     ENDPOINTS    AGE
imported-member1-serve-9q7bv           IPv4          8080      10.10.0.9    56s
imported-member2-member1-serve-9q7bv   IPv4          8080      10.10.0.9    55s
```

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

In the replication steps, it is not necessary to actually create the `MultiClusterIngress` resource. The connflict actually lies in the process of reporting the EndpointSlice resource. Therefore, creating the `ServiceExport` resource to export the target `Service` is sufficient.

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


**Root Cause**

This error is caused by the `service-export-controller` redundantly reporting EndpointSlices on the member clusters.

When the `endpointslice-collect-controller` collects EndpointSlices from the source cluster to the Karmada control plane, and then the `endpointslice-dispatch-controller` distributes them to the destination cluster, the `service-export-controller` also collects EndpointSlices on this destination cluster. However, it does not distinguish whether the EndpointSlices on the cluster are issued by Karmada, ultimately leading to redundancy in these EndpointSlice resources.


Hints from issue comments:
@XiShanYongYe-Chang: We can use the [`endpointslice.kubernetes.io/managed-by`](https://github.com/karmada-io/karmada/blob/efca312a572175da133164b3f8d7246840f195fb/vendor/k8s.io/api/discovery/v1/well_known_labels.go#L22-L27) label to distinguish whether the EndpointSlice resources on the cluster are managed by the Karmada system. The `endpointslice_dispatch_controller` has already set this label when propagating EndpointSlice resources to the target cluster. We also need to make similar settings in the `endpointslice_controller` so that when the controller collects EndpointSlice resources, it can decide whether to collect them by judging the value of this label.@XiShanYongYe-Chang: /assign@RainbowMango: It would be better to have a root cause analysis, such as
after `MultiClusterService`/`ServiceExport` is created, which controller and where collects the Endpoint to the Karmada control plane (to Work), and then which controller exposes the EndpointSlice from Work to the Karmada API Server.@XiShanYongYe-Chang: > It would be better to have a root cause analysis, such as after `MultiClusterService`/`ServiceExport` is created, which controller and where collects the Endpoint to the Karmada control plane (to Work), and then which controller exposes the EndpointSlice from Work to the Karmada API Server.

Thanks, I made additions in the PR description.

Solution patch:
diff --git a/.github/workflows/ci-image-scanning-on-schedule.yml b/.github/workflows/ci-image-scanning-on-schedule.yml
index 1a0ff795a..566a2dc4a 100644
--- a/.github/workflows/ci-image-scanning-on-schedule.yml
+++ b/.github/workflows/ci-image-scanning-on-schedule.yml
@@ -30,7 +30,7 @@ jobs:
         karmada-version: [ release-1.14, release-1.13, release-1.12 ]
     steps:
       - name: checkout code
-        uses: actions/checkout@v4
+        uses: actions/checkout@v5
         with:
           ref: ${{ matrix.karmada-version }}
       - name: install Go
diff --git a/.github/workflows/ci-image-scanning.yaml b/.github/workflows/ci-image-scanning.yaml
index e7b16646f..99ada99ba 100644
--- a/.github/workflows/ci-image-scanning.yaml
+++ b/.github/workflows/ci-image-scanning.yaml
@@ -31,7 +31,7 @@ jobs:
           - karmada-metrics-adapter
     steps:
       - name: checkout code
-        uses: actions/checkout@v4
+        uses: actions/checkout@v5
         with:
          # fetch-depth:
           # 0 indicates all history for all branches and tags.
diff --git a/.github/workflows/ci-schedule-compatibility.yaml b/.github/workflows/ci-schedule-compatibility.yaml
index 02a103956..b71be5cd3 100644
--- a/.github/workflows/ci-schedule-compatibility.yaml
+++ b/.github/workflows/ci-schedule-compatibility.yaml
@@ -38,7 +38,7 @@ jobs:
           docker-images: false
           swap-storage: false
       - name: checkout code
-        uses: actions/checkout@v4
+        uses: actions/checkout@v5
         with:
           # Number of commits to fetch. 0 indicates all history for all branches and tags.
           # We need to guess version via git tags.
diff --git a/.github/workflows/ci-schedule.yml b/.github/workflows/ci-schedule.yml
index abbc89be0..e4b958a67 100644
--- a/.github/workflows/ci-schedule.yml
+++ b/.github/workflows/ci-schedule.yml
@@ -35,7 +35,7 @@ jobs:
           docker-images: false
           swap-storage: false
       - name: checkout code
-        uses: actions/checkout@v4
+        uses: actions/checkout@v5
         with:
           # Number of commits to fetch. 0 indicates all history for all branches and tags.
           # We need to guess version via git tags.
diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index 9cfd42514..65ebb1b56 100644
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -20,7 +20,7 @@ jobs:
     runs-on: ubuntu-22.04
     steps:
       - name: checkout code
-        uses: actions/checkout@v4
+        uses: actions/checkout@v5
       - name: install Go
         uses: actions/setup-go@v5
         with:
@@ -38,7 +38,7 @@ jobs:
     runs-on: ubuntu-22.04
     steps:
       - name: checkout code
-        uses: actions/checkout@v4
+        uses: actions/checkout@v5
       - name: install Go
         uses: actions/setup-go@v5
         with:
@@ -66,7 +66,7 @@ jobs:
     runs-on: ubuntu-22.04
     steps:
       - name: checkout code
-        uses: actions/checkout@v4
+        uses: actions/checkout@v5
         with:
           # Number of commits to fetch. 0 indicates all history for all branches and tags.
           # We need to guess version via git tags.
@@ -85,7 +85,7 @@ jobs:
       GOTESTSUM_ENABLED: enabled
     steps:
       - name: checkout code
-        uses: actions/checkout@v4
+        uses: actions/checkout@v5
       - name: install Go
         uses: actions/setup-go@v5
         with:
@@ -132,7 +132,7 @@ jobs:
           docker-images: false
           swap-storage: false
       - name: checkout code
-        uses: actions/checkout@v4
+        uses: actions/checkout@v5
         with:
           # Number of commits to fetch. 0 indicates all history for all branches and tags.
           # We need to guess version via git tags.
@@ -188,7 +188,7 @@ jobs:
           docker-images: false
           swap-storage: false
       - name: checkout code
-        uses: actions/checkout@v4
+        uses: actions/checkout@v5
         with:
           # Number of commits to fetch. 0 indicates all history for all branches and tags.
           # We need to guess version via git tags.
diff --git a/.github/workflows/dockerhub-latest-chart.yml b/.github/workflows/dockerhub-latest-chart.yml
index 7bbd8bfca..057f6c957 100644
--- a/.github/workflows/dockerhub-latest-chart.yml
+++ b/.github/workflows/dockerhub-latest-chart.yml
@@ -16,7 +16,7 @@ jobs:
     runs-on: ubuntu-22.04
     steps:
       - name: checkout code
-        uses: actions/checkout@v4
+        uses: actions/checkout@v5
         with:
           # fetch-depth:
           # 0 indicates all history for all branches and tags.
diff --git a/.github/workflows/dockerhub-latest-image.yml b/.github/workflows/dockerhub-latest-image.yml
index 9c539a919..cfe9ffb35 100644
--- a/.github/workflows/dockerhub-latest-image.yml
+++ b/.github/workflows/dockerhub-latest-image.yml
@@ -31,7 +31,7 @@ jobs:
     runs-on: ubuntu-22.04
     steps:
       - name: checkout code
-        uses: actions/checkout@v4
+        uses: actions/checkout@v5
         with:
           # fetch-depth:
           # 0 indicates all history for all branches and tags.
diff --git a/.github/workflows/dockerhub-released-chart.yml b/.github/workflows/dockerhub-released-chart.yml
index a08091fb5..5076fb0e1 100644
--- a/.github/workflows/dockerhub-released-chart.yml
+++ b/.github/workflows/dockerhub-released-chart.yml
@@ -15,7 +15,7 @@ jobs:
     if: ${{ github.repository == 'karmada-io/karmada' }}
     steps:
       - name: checkout code
-        uses: actions/checkout@v4
+        uses: actions/checkout@v5
         with:
           # fetch-depth:
           # 0 indicates all history for all branches and tags.
diff --git a/.github/workflows/dockerhub-released-image.yml b/.github/workflows/dockerhub-released-image.yml
index d82e698bf..9518b9f2e 100644
--- a/.github/workflows/dockerhub-released-image.yml
+++ b/.github/workflows/dockerhub-released-image.yml
@@ -31,7 +31,7 @@ jobs:
     runs-on: ubuntu-22.04
     steps:
       - name: checkout code
-        uses: actions/checkout@v4
+        uses: actions/checkout@v5
         with:
           # fetch-depth:
           # 0 indicates all history for all branches and tags.
diff --git a/.github/workflows/fossa.yml b/.github/workflows/fossa.yml
index 829ea2607..cabcc5199 100644
--- a/.github/workflows/fossa.yml
+++ b/.github/workflows/fossa.yml
@@ -7,7 +7,7 @@ on:
       - 'dependabot/**'
 
 permissions:
-  contents: read # Required by actions/checkout@v4 to fetch the repository contents.
+  contents: read # Required by actions/checkout@v5 to fetch the repository contents.
   
 jobs:
   fossa:
@@ -19,7 +19,7 @@ jobs:
     runs-on: ubuntu-22.04
     steps:
       - name: checkout code
-        uses: actions/checkout@v4
+        uses: actions/checkout@v5
       - name: Run FOSSA scan and upload build data
         uses: fossas/fossa-action@v1
         with:
diff --git a/.github/workflows/installation-chart.yaml b/.github/workflows/installation-chart.yaml
index 41be1aabb..803122a2f 100644
--- a/.github/workflows/installation-chart.yaml
+++ b/.github/workflows/installation-chart.yaml
@@ -29,7 +29,7 @@ jobs:
         k8s: [ v1.31.0, v1.32.0, v1.33.0 ]
     steps:
       - name: Checkout
-        uses: actions/checkout@v4
+        uses: actions/checkout@v5
         with:
           fetch-depth: 0
 
diff --git a/.github/workflows/installation-cli.yaml b/.github/workflows/installation-cli.yaml
index a60690681..12a92100c 100644
--- a/.github/workflows/installation-cli.yaml
+++ b/.github/workflows/installation-cli.yaml
@@ -27,7 +27,7 @@ jobs:
         k8s: [ v1.31.0, v1.32.0, v1.33.0 ]
     steps:
       - name: checkout code
-        uses: actions/checkout@v4
+        uses: actions/checkout@v5
         with:
           # Number of commits to fetch. 0 indicates all history for all branches and tags.
           # We need to guess version via git tags.
@@ -73,7 +73,7 @@ jobs:
         k8s: [ v1.31.0, v1.32.0, v1.33.0 ]
     steps:
       - name: checkout code
-        uses: actions/checkout@v4
+        uses: actions/checkout@v5
         with:
           fetch-depth: 0
       - name: install Go
diff --git a/.github/workflows/installation-operator.yaml b/.github/workflows/installation-operator.yaml
index 00fd4cd06..f4070f270 100644
--- a/.github/workflows/installation-operator.yaml
+++ b/.github/workflows/installation-operator.yaml
@@ -40,7 +40,7 @@ jobs:
           docker-images: false
           swap-storage: false
       - name: checkout code
-        uses: actions/checkout@v4
+        uses: actions/checkout@v5
         with:
           # Number of commits to fetch. 0 indicates all history for all branches and tags.
           # We need to guess version via git tags.
diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml
index df6628769..ea8e7ab0d 100644
--- a/.github/workflows/release.yml
+++ b/.github/workflows/release.yml
@@ -23,7 +23,7 @@ jobs:
           - amd64
           - arm64
     steps:
-    - uses: actions/checkout@v4
+    - uses: actions/checkout@v5
     - name: Set up Go
       uses: actions/setup-go@v5
       with:
@@ -84,7 +84,7 @@ jobs:
       hashes: ${{ steps.hash.outputs.hashes }}
     runs-on: ubuntu-22.04
     steps:
-    - uses: actions/checkout@v4
+    - uses: actions/checkout@v5
     - name: Rename the crds directory
       run: |
         mv ./charts/karmada/_crds ./charts/karmada/crds
@@ -126,7 +126,7 @@ jobs:
       hashes: ${{ steps.hash.outputs.hashes }}
     runs-on: ubuntu-22.04
     steps:
-    - uses: actions/checkout@v4
+    - uses: actions/checkout@v5
     - name: Making helm charts
       env:
         VERSION: ${{ github.ref_name }}
@@ -165,7 +165,7 @@ jobs:
       hashes: ${{ steps.sbom-hash.outputs.hashes}}
     runs-on: ubuntu-22.04
     steps:
-    - uses: actions/checkout@v4
+    - uses: actions/checkout@v5
     - name: Generate sbom for karmada file system
       uses: aquasecurity/trivy-action@0.32.0
       with:
@@ -220,7 +220,7 @@ jobs:
         echo "Got the latest tag:$LATEST_TAG"
         echo "event.tag:"${{ github.event.release.tag_name }}
         echo "latestTag=$LATEST_TAG" >> "$GITHUB_OUTPUT"
-    - uses: actions/checkout@v4
+    - uses: actions/checkout@v5
       if: steps.get-latest-tag.outputs.latestTag == github.event.release.tag_name
     - name: Update new version in krew-index
       if: steps.get-latest-tag.outputs.latestTag == github.event.release.tag_name
diff --git a/.github/workflows/swr-latest-image.yml b/.github/workflows/swr-latest-image.yml
index aa344b53f..6dc36210a 100644
--- a/.github/workflows/swr-latest-image.yml
+++ b/.github/workflows/swr-latest-image.yml
@@ -15,7 +15,7 @@ jobs:
     runs-on: ubuntu-22.04
     steps:
       - name: checkout code
-        uses: actions/checkout@v4
+        uses: actions/checkout@v5
         with:
           # fetch-depth:
           # 0 indicates all history for all branches and tags.
diff --git a/.github/workflows/swr-released-image.yml b/.github/workflows/swr-released-image.yml
index 83730446c..d9a1bbbb4 100644
--- a/.github/workflows/swr-released-image.yml
+++ b/.github/workflows/swr-released-image.yml
@@ -15,7 +15,7 @@ jobs:
     runs-on: ubuntu-22.04
     steps:
       - name: checkout code
-        uses: actions/checkout@v4
+        uses: actions/checkout@v5
         with:
           fetch-depth: 0
       - name: install Go
diff --git a/pkg/controllers/mcs/endpointslice_controller.go b/pkg/controllers/mcs/endpointslice_controller.go
index 2381e50db..4b0a32816 100644
--- a/pkg/controllers/mcs/endpointslice_controller.go
+++ b/pkg/controllers/mcs/endpointslice_controller.go
@@ -153,6 +153,7 @@ func (c *EndpointSliceController) collectEndpointSliceFromWork(ctx context.Conte
 		desiredEndpointSlice.Labels = util.DedupeAndMergeLabels(desiredEndpointSlice.Labels, map[string]string{
 			workv1alpha2.WorkPermanentIDLabel: work.Labels[workv1alpha2.WorkPermanentIDLabel],
 			discoveryv1.LabelServiceName:      names.GenerateDerivedServiceName(work.Labels[util.ServiceNameLabel]),
+			discoveryv1.LabelManagedBy:        util.EndpointSliceControllerLabelValue,
 		})
 		desiredEndpointSlice.Annotations = util.DedupeAndMergeAnnotations(desiredEndpointSlice.Annotations, map[string]string{
 			workv1alpha2.WorkNamespaceAnnotation: work.Namespace,
diff --git a/pkg/controllers/mcs/service_export_controller.go b/pkg/controllers/mcs/service_export_controller.go
index d4e060a56..1e03be0b5 100644
--- a/pkg/controllers/mcs/service_export_controller.go
+++ b/pkg/controllers/mcs/service_export_controller.go
@@ -394,6 +394,11 @@ func (c *ServiceExportController) handleEndpointSliceEvent(ctx context.Context,
 		return err
 	}
 
+	// Exclude EndpointSlice resources that are managed by Karmada system to avoid duplicate reporting.
+	if helper.IsEndpointSliceManagedByKarmada(endpointSliceObj.GetLabels()) {
+		return nil
+	}
+
 	if err = c.reportEndpointSliceWithEndpointSliceCreateOrUpdate(ctx, endpointSliceKey.Cluster, endpointSliceObj); err != nil {
 		klog.ErrorS(err, "Failed to handle endpointSlice event", "namespace", endpointSliceKey.Namespace, "name", endpointSliceKey.Name)
 		return err
@@ -440,7 +445,13 @@ func (c *ServiceExportController) reportEndpointSliceWithServiceExportCreate(ctx
 	}
 
 	for index := range endpointSliceObjects {
-		if err = reportEndpointSlice(ctx, c.Client, endpointSliceObjects[index].(*unstructured.Unstructured), serviceExportKey.Cluster); err != nil {
+		endpointSlice := endpointSliceObjects[index].(*unstructured.Unstructured)
+		// Exclude EndpointSlice resources that are managed by Karmada system to avoid duplicate reporting.
+		if helper.IsEndpointSliceManagedByKarmada(endpointSlice.GetLabels()) {
+			continue
+		}
+
+		if err = reportEndpointSlice(ctx, c.Client, endpointSlice, serviceExportKey.Cluster); err != nil {
 			errs = append(errs, err)
 		}
 	}
diff --git a/pkg/controllers/multiclusterservice/endpointslice_collect_controller.go b/pkg/controllers/multiclusterservice/endpointslice_collect_controller.go
index 2f31fb8bd..307cd17ba 100644
--- a/pkg/controllers/multiclusterservice/endpointslice_collect_controller.go
+++ b/pkg/controllers/multiclusterservice/endpointslice_collect_controller.go
@@ -299,7 +299,8 @@ func (c *EndpointSliceCollectController) handleEndpointSliceEvent(ctx context.Co
 		return err
 	}
 
-	if util.GetLabelValue(endpointSliceObj.GetLabels(), discoveryv1.LabelManagedBy) == util.EndpointSliceDispatchControllerLabelValue {
+	// Exclude EndpointSlice resources that are managed by Karmada system to avoid duplicate reporting.
+	if helper.IsEndpointSliceManagedByKarmada(endpointSliceObj.GetLabels()) {
 		return nil
 	}
 
@@ -358,7 +359,8 @@ func (c *EndpointSliceCollectController) collectTargetEndpointSlice(ctx context.
 			klog.ErrorS(err, "Failed to convert object to EndpointSlice")
 			return err
 		}
-		if util.GetLabelValue(eps.GetLabels(), discoveryv1.LabelManagedBy) == util.EndpointSliceDispatchControllerLabelValue {
+		// Exclude EndpointSlice resources that are managed by Karmada system to avoid duplicate reporting.
+		if helper.IsEndpointSliceManagedByKarmada(eps.GetLabels()) {
 			continue
 		}
 		epsUnstructured, err := helper.ToUnstructured(eps)
diff --git a/pkg/resourceinterpreter/default/thirdparty/thirdparty.go b/pkg/resourceinterpreter/default/thirdparty/thirdparty.go
index 11dbff462..bcc9b935d 100644
--- a/pkg/resourceinterpreter/default/thirdparty/thirdparty.go
+++ b/pkg/resourceinterpreter/default/thirdparty/thirdparty.go
@@ -56,6 +56,8 @@ func (p *ConfigurableInterpreter) HookEnabled(kind schema.GroupVersionKind, oper
 		script = customAccessor.GetHealthInterpretationLuaScript()
 	case configv1alpha1.InterpreterOperationInterpretReplica:
 		script = customAccessor.GetReplicaResourceLuaScript()
+	case configv1alpha1.InterpreterOperationInterpretComponent:
+		script = customAccessor.GetComponentResourceLuaScript()
 	case configv1alpha1.InterpreterOperationInterpretStatus:
 		script = customAccessor.GetStatusReflectionLuaScript()
 	case configv1alpha1.InterpreterOperationRetain:
@@ -85,6 +87,25 @@ func (p *ConfigurableInterpreter) GetReplicas(object *unstructured.Unstructured)
 	return
 }
 
+// GetComponents returns the desired components of the object.
+func (p *ConfigurableInterpreter) GetComponents(object *unstructured.Unstructured) (components []workv1alpha2.ComponentRequirements, enabled bool, err error) {
+	klog.V(4).Infof("Get components for object: %v %s/%s with thirdparty configurable interpreter.", object.GroupVersionKind(), object.GetNamespace(), object.GetName())
+
+	customAccessor, enabled := p.getCustomAccessor(object.GroupVersionKind())
+	if !enabled {
+		return
+	}
+
+	script := customAccessor.GetComponentResourceLuaScript()
+	if len(script) == 0 {
+		enabled = false
+		return
+	}
+
+	components, err = p.luaVM.GetComponents(object, script)
+	return
+}
+
 // ReviseReplica revises the replica of the given object.
 func (p *ConfigurableInterpreter) ReviseReplica(object *unstructured.Unstructured, replica int64) (revised *unstructured.Unstructured, enabled bool, err error) {
 	klog.V(4).Infof("Revise replicas for object: %v %s/%s with thirdparty configurable interpreter.", object.GroupVersionKind(), object.GetNamespace(), object.GetName())
diff --git a/pkg/resourceinterpreter/interpreter.go b/pkg/resourceinterpreter/interpreter.go
index 84ec875d5..3130a5240 100644
--- a/pkg/resourceinterpreter/interpreter.go
+++ b/pkg/resourceinterpreter/interpreter.go
@@ -216,6 +216,14 @@ func (i *customResourceInterpreterImpl) GetComponents(object *unstructured.Unstr
 		return components, nil
 	}
 
+	components, hookEnabled, err = i.thirdpartyInterpreter.GetComponents(object)
+	if err != nil {
+		return nil, err
+	}
+	if hookEnabled {
+		return components, nil
+	}
+
 	// TODO(@RainbowMango): Implement GetComponents for extracting per-component replica and resource requirements.
 	// Follow up tracked by: https://github.com/karmada-io/karmada/issues/6641
 	return nil, errors.New("interface GetComponents not implemented yet")
diff --git a/pkg/util/constants.go b/pkg/util/constants.go
index 969f81073..c27599cbb 100644
--- a/pkg/util/constants.go
+++ b/pkg/util/constants.go
@@ -56,9 +56,6 @@ const (
 	// managed by karmada controllers.
 	KarmadaSystemLabel = "karmada.io/system"
 
-	// EndpointSliceDispatchControllerLabelValue indicates the endpointSlice are controlled by Karmada
-	EndpointSliceDispatchControllerLabelValue = "endpointslice-dispatch-controller.karmada.io"
-
 	// RetainReplicasLabel is a reserved label to indicate whether the replicas should be retained. e.g:
 	// resourcetemplate.karmada.io/retain-replicas: true   // with value `true` indicates retain
 	// resourcetemplate.karmada.io/retain-replicas: false  // with value `false` and others, indicates not retain
@@ -71,6 +68,7 @@ const (
 	EndpointSliceWorkManagedByLabel = "endpointslice.karmada.io/managed-by"
 )
 
+// Define label values used by Karmada system.
 const (
 	// ManagedByKarmadaLabelValue indicates that these are workloads in member cluster synchronized by karmada controllers.
 	ManagedByKarmadaLabelValue = "true"
@@ -80,6 +78,12 @@ const (
 
 	// RetainReplicasValue is an optional value of RetainReplicasLabel, indicating retain
 	RetainReplicasValue = "true"
+
+	// EndpointSliceDispatchControllerLabelValue indicates the endpointSlice is controlled by Karmada endpointslice-dispatch-controller
+	EndpointSliceDispatchControllerLabelValue = "endpointslice-dispatch-controller.karmada.io"
+
+	// EndpointSliceControllerLabelValue indicates the endpointSlice is controlled by Karmada endpointslice-controller
+	EndpointSliceControllerLabelValue = "endpointslice-controller.karmada.io"
 )
 
 // Define annotations used by karmada system.
diff --git a/pkg/util/helper/mcs.go b/pkg/util/helper/mcs.go
index 672151a77..70e6eff64 100644
--- a/pkg/util/helper/mcs.go
+++ b/pkg/util/helper/mcs.go
@@ -141,3 +141,12 @@ func GetConsumerClusters(client client.Client, mcs *networkingv1alpha1.MultiClus
 	}
 	return allClusters, nil
 }
+
+// IsEndpointSliceManagedByKarmada checks if the EndpointSlice is managed by Karmada.
+func IsEndpointSliceManagedByKarmada(epsLabels map[string]string) bool {
+	switch util.GetLabelValue(epsLabels, discoveryv1.LabelManagedBy) {
+	case util.EndpointSliceDispatchControllerLabelValue, util.EndpointSliceControllerLabelValue:
+		return true
+	}
+	return false
+}



Test patch:
diff --git a/pkg/util/helper/mcs_test.go b/pkg/util/helper/mcs_test.go
index 8fd874f09..f21bf23a3 100644
--- a/pkg/util/helper/mcs_test.go
+++ b/pkg/util/helper/mcs_test.go
@@ -28,6 +28,7 @@ import (
 	"sigs.k8s.io/controller-runtime/pkg/client"
 	"sigs.k8s.io/controller-runtime/pkg/client/fake"
 
+	"github.com/karmada-io/karmada/pkg/util"
 	"github.com/karmada-io/karmada/pkg/util/gclient"
 )
 
@@ -223,3 +224,50 @@ func TestDeleteEndpointSlice(t *testing.T) {
 		})
 	}
 }
+
+func TestIsEndpointSliceManagedByKarmada(t *testing.T) {
+	tests := []struct {
+		name   string
+		labels map[string]string
+		want   bool
+	}{
+		{
+			name: "managed by endpointslice-dispatch-controller",
+			labels: map[string]string{
+				discoveryv1.LabelManagedBy: util.EndpointSliceDispatchControllerLabelValue,
+			},
+			want: true,
+		},
+		{
+			name: "managed by endpointslice-controller",
+			labels: map[string]string{
+				discoveryv1.LabelManagedBy: util.EndpointSliceControllerLabelValue,
+			},
+			want: true,
+		},
+		{
+			name: "not managed by karmada",
+			labels: map[string]string{
+				discoveryv1.LabelManagedBy: "not-karmada",
+			},
+			want: false,
+		},
+		{
+			name:   "nil labels",
+			labels: nil,
+			want:   false,
+		},
+		{
+			name:   "empty labels",
+			labels: map[string]string{},
+			want:   false,
+		},
+	}
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			if got := IsEndpointSliceManagedByKarmada(tt.labels); got != tt.want {
+				t.Errorf("IsEndpointSliceManagedByKarmada() = %v, want %v", got, tt.want)
+			}
+		})
+	}
+}