Repository: karmada-io/karmada

Issue:
The default resource interpreter should not fill clusterName to Hostname field when aggregating status
<!-- 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 we were using karmada to distribute `Service` resources recently, we found that when the lb entry of a loadBalancer type Service is of ip type, karmada's default resourceinterpreter would write the name of the member cluster in the `Hostname` field. The original intention was obviously to make it easier to identify when the member cluster returns two different lb ips, but the `Hostname` field clearly indicates that it should be a legal domain name that can be queried by dns. So we think that filling in the member cluster name is inappropriate.

In actual situations, it does have an impact. Since our karmada will connect to multiple provider of multiple AZs, some provider can provide domain names as lb entries , while others provide ip. When these two services are in the same list, it causes interference in UI display and program identification.

**What you expected to happen**:

When aggregate status for Service and Ingress, default resourceInterpreter should not fill `Hostname` field with cluster name.

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

- run a local up karmada
- propagate a service or ingress resource to member clusters
- use `kubectl edit --subresource=status` to update the resource status in the member cluster, manually fill in the ingress ip
- print the service/ingress in karmada cluster.

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

At present, we have found 3 places with this behavior: the default interpreter of service, ingress and pdb. Since there may be pods with the same name in multiple clusters, I guess we can only temporarily not modify pdb. So I proposed a pr #6249 to remove the behavior of adding clusterName for service and ingress. If users still want to identify the lb domain name and ip for each member cluster, I think they should read the `ResourceBinding`.

But removing clusterName will expose another problem. Most cloud providers can provide the same lb entry for services with the same name in multiple clusters. It probably doesn't make sense to repeat the same information multiple times in the status, so I wrote another PR #6252 to deduplicate it, but the code implementation is very verbose, so I would very much welcome reviews that can help optimize the implementation.

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


Hints from issue comments:
@zhzhuang-zju: As additional information:
Deploy a service of type LoadBalancer to the member cluster. The status of the service in the member cluster is as follows:
```yaml
status:
  loadBalancer:
    ingress:
    - ip: 0.0.0.0
      ipMode: VIP
    - hostname: user.example.com
      ip: 8.8.8.8
      ipMode: VIP
```
The status of the service on the control plane after the aggregation is: 
```yaml
status:
  loadBalancer:
    ingress:
    - hostname: member1
      ip: 0.0.0.0
      ipMode: VIP
    - hostname: user.example.com
      ip: 8.8.8.8
      ipMode: VIP
```@XiShanYongYe-Chang: @zach593 Thanks a lot for this issue!

> he original intention was obviously to make it easier to identify when the member cluster returns two different lb ips

That's really what we're trying to do. But for now, it seems a little mishandled.

> In actual situations, it does have an impact. Since our karmada will connect to multiple provider of multiple AZs, some provider can provide domain names as lb entries , while others provide ip. When these two services are in the same list, it causes interference in UI display and program identification.

Thank you for your feedback on your real application scenarios.

> If users still want to identify the lb domain name and ip for each member cluster, I think they should read the ResourceBinding.

For native services/ingress resources, there is no place to carry cluster information. 

I want to know, do you not need cluster information in the process of using it?@XiShanYongYe-Chang: In favor of #6249 
/assign @zach593 @XiShanYongYe-Chang: /assign @zach593 

/reopen
We still have one pr #6252@karmada-bot: @XiShanYongYe-Chang: Reopened this issue.

<details>

In response to [this](https://github.com/karmada-io/karmada/issues/6248#issuecomment-2768427129):

>/assign @zach593 
>
>/reopen
>We still have one pr #6252


Instructions for interacting with me using PR comments are available [here](https://git.k8s.io/community/contributors/guide/pull-requests.md).  If you have questions or suggestions related to my behavior, please file an issue against the [kubernetes-sigs/prow](https://github.com/kubernetes-sigs/prow/issues/new?title=Prow%20issue:) repository.
</details>@zach593: > I want to know, do you not need cluster information in the process of using it?

Since multiple cloud providers in our environment support cross-cluster load balancer entry, member cluster information is indeed not needed in our environment.

But I think this cannot be assumed for everyone. This issue mainly depends on whether the cloud provider supports cross-cluster load balancer and whether the user will use this LB. Although I think this should be the case in most cases.
If the user chooses to use a separate LB for each member cluster service, then cluster information is still necessary, but because there is no suitable field to carry member cluster information, I think the user needs to read `ResourceBinding` in this case.@XiShanYongYe-Chang: /close@karmada-bot: @XiShanYongYe-Chang: Closing this issue.

<details>

In response to [this](https://github.com/karmada-io/karmada/issues/6248#issuecomment-2795711607):

>/close


Instructions for interacting with me using PR comments are available [here](https://git.k8s.io/community/contributors/guide/pull-requests.md).  If you have questions or suggestions related to my behavior, please file an issue against the [kubernetes-sigs/prow](https://github.com/kubernetes-sigs/prow/issues/new?title=Prow%20issue:) repository.
</details>

Solution patch:
diff --git a/.go-version b/.go-version
index 2d27ccba1..82bfa5ce3 100644
--- a/.go-version
+++ b/.go-version
@@ -1 +1 @@
-1.23.7
+1.23.8
diff --git a/README.md b/README.md
index a76bf2eed..271b596a4 100644
--- a/README.md
+++ b/README.md
@@ -186,12 +186,12 @@ nginx   2/2     2            2           20s
 
 ## Kubernetes compatibility
 
-|                       | Kubernetes 1.16 | Kubernetes 1.17 | Kubernetes 1.18 | Kubernetes 1.19 | Kubernetes 1.20 | Kubernetes 1.21 | Kubernetes 1.22 | Kubernetes 1.23 | Kubernetes 1.24 | Kubernetes 1.25 | Kubernetes 1.26 | Kubernetes 1.27 | Kubernetes 1.28 | Kubernetes 1.29 |
-|-----------------------|-----------------|-----------------|-----------------|-----------------|-----------------|-----------------|-----------------|-----------------|-----------------|-----------------|-----------------|-----------------|-----------------|-----------------|
-| Karmada v1.7          | ✓               | ✓               | ✓               | ✓               | ✓               | ✓               | ✓               | ✓               | ✓               | ✓               | ✓               | ✓               | ✓               | ✓               |
-| Karmada v1.8          | ✓               | ✓               | ✓               | ✓               | ✓               | ✓               | ✓               | ✓               | ✓               | ✓               | ✓               | ✓               | ✓               | ✓               |
-| Karmada v1.9          | ✓               | ✓               | ✓               | ✓               | ✓               | ✓               | ✓               | ✓               | ✓               | ✓               | ✓               | ✓               | ✓               | ✓               |
-| Karmada HEAD (master) | ✓               | ✓               | ✓               | ✓               | ✓               | ✓               | ✓               | ✓               | ✓               | ✓               | ✓               | ✓               | ✓               | ✓               |
+|                       | Kubernetes 1.16 | Kubernetes 1.17 | Kubernetes 1.18 | Kubernetes 1.19 | Kubernetes 1.20 | Kubernetes 1.21 | Kubernetes 1.22 | Kubernetes 1.23 | Kubernetes 1.24 | Kubernetes 1.25 | Kubernetes 1.26 | Kubernetes 1.27 | Kubernetes 1.28 | Kubernetes 1.29 | Kubernetes 1.30 | Kubernetes 1.31 | Kubernetes 1.32 |
+|-----------------------|-----------------|-----------------|-----------------|-----------------|-----------------|-----------------|-----------------|-----------------|-----------------|-----------------|-----------------|-----------------|-----------------|-----------------|-----------------|-----------------|-----------------|
+| Karmada v1.11         | ✓               | ✓               | ✓               | ✓               | ✓               | ✓               | ✓               | ✓               | ✓               | ✓               | ✓               | ✓               | ✓               | ✓               | ✓               | ✓               | ✓               |
+| Karmada v1.12         | ✓               | ✓               | ✓               | ✓               | ✓               | ✓               | ✓               | ✓               | ✓               | ✓               | ✓               | ✓               | ✓               | ✓               | ✓               | ✓               | ✓               |
+| Karmada v1.13         | ✓               | ✓               | ✓               | ✓               | ✓               | ✓               | ✓               | ✓               | ✓               | ✓               | ✓               | ✓               | ✓               | ✓               | ✓               | ✓               | ✓               |
+| Karmada HEAD (master) | ✓               | ✓               | ✓               | ✓               | ✓               | ✓               | ✓               | ✓               | ✓               | ✓               | ✓               | ✓               | ✓               | ✓               | ✓               | ✓               | ✓               |
 
 Key:
 * `✓` Karmada and the Kubernetes version are exactly compatible.
diff --git a/cmd/agent/app/agent.go b/cmd/agent/app/agent.go
index fa67234f0..14de9b4a9 100644
--- a/cmd/agent/app/agent.go
+++ b/cmd/agent/app/agent.go
@@ -36,7 +36,7 @@ import (
 	"sigs.k8s.io/controller-runtime/pkg/cache"
 	"sigs.k8s.io/controller-runtime/pkg/config"
 	"sigs.k8s.io/controller-runtime/pkg/healthz"
-	crtlmetrics "sigs.k8s.io/controller-runtime/pkg/metrics"
+	ctrlmetrics "sigs.k8s.io/controller-runtime/pkg/metrics"
 	metricsserver "sigs.k8s.io/controller-runtime/pkg/metrics/server"
 
 	"github.com/karmada-io/karmada/cmd/agent/app/options"
@@ -52,6 +52,7 @@ import (
 	karmadaclientset "github.com/karmada-io/karmada/pkg/generated/clientset/versioned"
 	"github.com/karmada-io/karmada/pkg/karmadactl/util/apiclient"
 	"github.com/karmada-io/karmada/pkg/metrics"
+	versionmetrics "github.com/karmada-io/karmada/pkg/metrics"
 	"github.com/karmada-io/karmada/pkg/resourceinterpreter"
 	"github.com/karmada-io/karmada/pkg/sharedcli"
 	"github.com/karmada-io/karmada/pkg/sharedcli/klogflag"
@@ -228,9 +229,10 @@ func run(ctx context.Context, opts *options.Options) error {
 		return err
 	}
 
-	crtlmetrics.Registry.MustRegister(metrics.ClusterCollectors()...)
-	crtlmetrics.Registry.MustRegister(metrics.ResourceCollectorsForAgent()...)
-	crtlmetrics.Registry.MustRegister(metrics.PoolCollectors()...)
+	ctrlmetrics.Registry.MustRegister(metrics.ClusterCollectors()...)
+	ctrlmetrics.Registry.MustRegister(metrics.ResourceCollectorsForAgent()...)
+	ctrlmetrics.Registry.MustRegister(metrics.PoolCollectors()...)
+	ctrlmetrics.Registry.MustRegister(versionmetrics.NewBuildInfoCollector())
 
 	if err = setupControllers(controllerManager, opts, ctx.Done()); err != nil {
 		return err
diff --git a/cmd/controller-manager/app/controllermanager.go b/cmd/controller-manager/app/controllermanager.go
index 9f705290c..d17de7f9b 100644
--- a/cmd/controller-manager/app/controllermanager.go
+++ b/cmd/controller-manager/app/controllermanager.go
@@ -41,7 +41,7 @@ import (
 	"sigs.k8s.io/controller-runtime/pkg/config"
 	"sigs.k8s.io/controller-runtime/pkg/event"
 	"sigs.k8s.io/controller-runtime/pkg/healthz"
-	crtlmetrics "sigs.k8s.io/controller-runtime/pkg/metrics"
+	ctrlmetrics "sigs.k8s.io/controller-runtime/pkg/metrics"
 	metricsserver "sigs.k8s.io/controller-runtime/pkg/metrics/server"
 	"sigs.k8s.io/controller-runtime/pkg/predicate"
 
@@ -75,6 +75,7 @@ import (
 	"github.com/karmada-io/karmada/pkg/features"
 	"github.com/karmada-io/karmada/pkg/karmadactl/util/apiclient"
 	"github.com/karmada-io/karmada/pkg/metrics"
+	versionmetrics "github.com/karmada-io/karmada/pkg/metrics"
 	"github.com/karmada-io/karmada/pkg/resourceinterpreter"
 	"github.com/karmada-io/karmada/pkg/sharedcli"
 	"github.com/karmada-io/karmada/pkg/sharedcli/klogflag"
@@ -189,9 +190,10 @@ func Run(ctx context.Context, opts *options.Options) error {
 		return err
 	}
 
-	crtlmetrics.Registry.MustRegister(metrics.ClusterCollectors()...)
-	crtlmetrics.Registry.MustRegister(metrics.ResourceCollectors()...)
-	crtlmetrics.Registry.MustRegister(metrics.PoolCollectors()...)
+	ctrlmetrics.Registry.MustRegister(metrics.ClusterCollectors()...)
+	ctrlmetrics.Registry.MustRegister(metrics.ResourceCollectors()...)
+	ctrlmetrics.Registry.MustRegister(metrics.PoolCollectors()...)
+	ctrlmetrics.Registry.MustRegister(versionmetrics.NewBuildInfoCollector())
 
 	if err := helper.IndexWork(ctx, controllerManager); err != nil {
 		klog.Fatalf("Failed to index Work: %v", err)
diff --git a/cmd/descheduler/app/descheduler.go b/cmd/descheduler/app/descheduler.go
index 9f3db2ea9..5c4e3bc07 100644
--- a/cmd/descheduler/app/descheduler.go
+++ b/cmd/descheduler/app/descheduler.go
@@ -40,6 +40,7 @@ import (
 	"github.com/karmada-io/karmada/cmd/descheduler/app/options"
 	"github.com/karmada-io/karmada/pkg/descheduler"
 	karmadaclientset "github.com/karmada-io/karmada/pkg/generated/clientset/versioned"
+	versionmetrics "github.com/karmada-io/karmada/pkg/metrics"
 	"github.com/karmada-io/karmada/pkg/sharedcli"
 	"github.com/karmada-io/karmada/pkg/sharedcli/klogflag"
 	"github.com/karmada-io/karmada/pkg/sharedcli/profileflag"
@@ -127,6 +128,9 @@ karmada-scheduler-estimator to get replica status.`,
 func run(opts *options.Options, stopChan <-chan struct{}) error {
 	klog.Infof("karmada-descheduler version: %s", version.Get())
 	klog.Infof("Please make sure the karmada-scheduler-estimator of all member clusters has been deployed")
+
+	ctrlmetrics.Registry.MustRegister(versionmetrics.NewBuildInfoCollector())
+
 	serveHealthzAndMetrics(opts.HealthProbeBindAddress, opts.MetricsBindAddress)
 
 	profileflag.ListenAndServe(opts.ProfileOpts)
diff --git a/cmd/metrics-adapter/app/options/options.go b/cmd/metrics-adapter/app/options/options.go
index 62560fba6..45a305b30 100755
--- a/cmd/metrics-adapter/app/options/options.go
+++ b/cmd/metrics-adapter/app/options/options.go
@@ -38,6 +38,7 @@ import (
 	karmadaclientset "github.com/karmada-io/karmada/pkg/generated/clientset/versioned"
 	informerfactory "github.com/karmada-io/karmada/pkg/generated/informers/externalversions"
 	generatedopenapi "github.com/karmada-io/karmada/pkg/generated/openapi"
+	versionmetrics "github.com/karmada-io/karmada/pkg/metrics"
 	"github.com/karmada-io/karmada/pkg/metricsadapter"
 	"github.com/karmada-io/karmada/pkg/sharedcli/profileflag"
 	"github.com/karmada-io/karmada/pkg/util"
@@ -178,6 +179,7 @@ func (o *Options) Config(stopCh <-chan struct{}) (*metricsadapter.MetricsServer,
 // Run runs the metrics-adapter with options. This should never exit.
 func (o *Options) Run(ctx context.Context) error {
 	klog.Infof("karmada-metrics-adapter version: %s", version.Get())
+	legacyregistry.RawMustRegister(versionmetrics.NewBuildInfoCollector())
 	if o.MetricsBindAddress != "0" {
 		go serveMetrics(o.MetricsBindAddress)
 	}
diff --git a/cmd/scheduler-estimator/app/scheduler-estimator.go b/cmd/scheduler-estimator/app/scheduler-estimator.go
index 97fc9ad32..df6d24e2c 100644
--- a/cmd/scheduler-estimator/app/scheduler-estimator.go
+++ b/cmd/scheduler-estimator/app/scheduler-estimator.go
@@ -37,6 +37,7 @@ import (
 
 	"github.com/karmada-io/karmada/cmd/scheduler-estimator/app/options"
 	"github.com/karmada-io/karmada/pkg/estimator/server"
+	versionmetrics "github.com/karmada-io/karmada/pkg/metrics"
 	"github.com/karmada-io/karmada/pkg/sharedcli"
 	"github.com/karmada-io/karmada/pkg/sharedcli/klogflag"
 	"github.com/karmada-io/karmada/pkg/sharedcli/profileflag"
@@ -114,6 +115,9 @@ provides the scheduler with more accurate cluster resource information.`,
 
 func run(ctx context.Context, opts *options.Options) error {
 	klog.Infof("karmada-scheduler-estimator version: %s", version.Get())
+
+	ctrlmetrics.Registry.MustRegister(versionmetrics.NewBuildInfoCollector())
+
 	serveHealthzAndMetrics(opts.HealthProbeBindAddress, opts.MetricsBindAddress)
 
 	profileflag.ListenAndServe(opts.ProfileOpts)
diff --git a/cmd/scheduler/app/scheduler.go b/cmd/scheduler/app/scheduler.go
index 6b23a12c2..cb793dd2a 100644
--- a/cmd/scheduler/app/scheduler.go
+++ b/cmd/scheduler/app/scheduler.go
@@ -40,6 +40,7 @@ import (
 
 	"github.com/karmada-io/karmada/cmd/scheduler/app/options"
 	karmadaclientset "github.com/karmada-io/karmada/pkg/generated/clientset/versioned"
+	versionmetrics "github.com/karmada-io/karmada/pkg/metrics"
 	"github.com/karmada-io/karmada/pkg/scheduler"
 	"github.com/karmada-io/karmada/pkg/scheduler/framework/runtime"
 	"github.com/karmada-io/karmada/pkg/sharedcli"
@@ -139,6 +140,8 @@ the most suitable cluster.`,
 
 func run(opts *options.Options, stopChan <-chan struct{}, registryOptions ...Option) error {
 	klog.Infof("karmada-scheduler version: %s", version.Get())
+
+	ctrlmetrics.Registry.MustRegister(versionmetrics.NewBuildInfoCollector())
 	serveHealthzAndMetrics(opts.HealthProbeBindAddress, opts.MetricsBindAddress)
 
 	profileflag.ListenAndServe(opts.ProfileOpts)
diff --git a/cmd/webhook/app/webhook.go b/cmd/webhook/app/webhook.go
index 843287599..e50228230 100644
--- a/cmd/webhook/app/webhook.go
+++ b/cmd/webhook/app/webhook.go
@@ -30,12 +30,14 @@ import (
 	"k8s.io/klog/v2"
 	controllerruntime "sigs.k8s.io/controller-runtime"
 	"sigs.k8s.io/controller-runtime/pkg/healthz"
+	ctrlmetrics "sigs.k8s.io/controller-runtime/pkg/metrics"
 	metricsserver "sigs.k8s.io/controller-runtime/pkg/metrics/server"
 	"sigs.k8s.io/controller-runtime/pkg/webhook"
 	"sigs.k8s.io/controller-runtime/pkg/webhook/admission"
 	"sigs.k8s.io/controller-runtime/pkg/webhook/conversion"
 
 	"github.com/karmada-io/karmada/cmd/webhook/app/options"
+	versionmetrics "github.com/karmada-io/karmada/pkg/metrics"
 	"github.com/karmada-io/karmada/pkg/sharedcli"
 	"github.com/karmada-io/karmada/pkg/sharedcli/klogflag"
 	"github.com/karmada-io/karmada/pkg/sharedcli/profileflag"
@@ -184,6 +186,8 @@ func Run(ctx context.Context, opts *options.Options) error {
 	hookServer.Register("/mutate-clusterresourcebinding", &webhook.Admission{Handler: &clusterresourcebinding.MutatingAdmission{Decoder: decoder}})
 	hookServer.WebhookMux().Handle("/readyz/", http.StripPrefix("/readyz/", &healthz.Handler{}))
 
+	ctrlmetrics.Registry.MustRegister(versionmetrics.NewBuildInfoCollector())
+
 	// blocks until the context is done.
 	if err := hookManager.Start(ctx); err != nil {
 		klog.Errorf("webhook server exits unexpectedly: %v", err)
diff --git a/docs/CHANGELOG/CHANGELOG-1.11.md b/docs/CHANGELOG/CHANGELOG-1.11.md
index 890acf4fc..aa9f2c925 100644
--- a/docs/CHANGELOG/CHANGELOG-1.11.md
+++ b/docs/CHANGELOG/CHANGELOG-1.11.md
@@ -2,49 +2,55 @@
 <!-- DON'T EDIT THIS SECTION, INSTEAD RE-RUN doctoc TO UPDATE -->
 **Table of Contents**  *generated with [DocToc](https://github.com/thlorenz/doctoc)*
 
-- [v1.11.7](#v1117)
-  - [Downloads for v1.11.7](#downloads-for-v1117)
-  - [Changelog since v1.11.6](#changelog-since-v1116)
+- [v1.11.8](#v1118)
+  - [Downloads for v1.11.8](#downloads-for-v1118)
+  - [Changelog since v1.11.7](#changelog-since-v1117)
     - [Changes by Kind](#changes-by-kind)
       - [Bug Fixes](#bug-fixes)
       - [Others](#others)
-- [v1.11.6](#v1116)
-  - [Downloads for v1.11.6](#downloads-for-v1116)
-  - [Changelog since v1.11.5](#changelog-since-v1115)
+- [v1.11.7](#v1117)
+  - [Downloads for v1.11.7](#downloads-for-v1117)
+  - [Changelog since v1.11.6](#changelog-since-v1116)
     - [Changes by Kind](#changes-by-kind-1)
       - [Bug Fixes](#bug-fixes-1)
       - [Others](#others-1)
-- [v1.11.5](#v1115)
-  - [Downloads for v1.11.5](#downloads-for-v1115)
-  - [Changelog since v1.11.4](#changelog-since-v1114)
+- [v1.11.6](#v1116)
+  - [Downloads for v1.11.6](#downloads-for-v1116)
+  - [Changelog since v1.11.5](#changelog-since-v1115)
     - [Changes by Kind](#changes-by-kind-2)
       - [Bug Fixes](#bug-fixes-2)
       - [Others](#others-2)
+- [v1.11.5](#v1115)
+  - [Downloads for v1.11.5](#downloads-for-v1115)
+  - [Changelog since v1.11.4](#changelog-since-v1114)
+    - [Changes by Kind](#changes-by-kind-3)
+      - [Bug Fixes](#bug-fixes-3)
+      - [Others](#others-3)
 - [v1.11.4](#v1114)
   - [Downloads for v1.11.4](#downloads-for-v1114)
   - [Changelog since v1.11.3](#changelog-since-v1113)
-    - [Changes by Kind](#changes-by-kind-3)
+    - [Changes by Kind](#changes-by-kind-4)
       - [Urgent Upgrade Notes](#urgent-upgrade-notes)
-      - [Bug Fixes](#bug-fixes-3)
-      - [Others](#others-3)
+      - [Bug Fixes](#bug-fixes-4)
+      - [Others](#others-4)
 - [v1.11.3](#v1113)
   - [Downloads for v1.11.3](#downloads-for-v1113)
   - [Changelog since v1.11.2](#changelog-since-v1112)
-    - [Changes by Kind](#changes-by-kind-4)
-      - [Bug Fixes](#bug-fixes-4)
-      - [Others](#others-4)
-- [v1.11.2](#v1112)
-  - [Downloads for v1.11.2](#downloads-for-v1112)
-  - [Changelog since v1.11.1](#changelog-since-v1111)
     - [Changes by Kind](#changes-by-kind-5)
       - [Bug Fixes](#bug-fixes-5)
       - [Others](#others-5)
-- [v1.11.1](#v1111)
-  - [Downloads for v1.11.1](#downloads-for-v1111)
-  - [Changelog since v1.11.0](#changelog-since-v1110)
+- [v1.11.2](#v1112)
+  - [Downloads for v1.11.2](#downloads-for-v1112)
+  - [Changelog since v1.11.1](#changelog-since-v1111)
     - [Changes by Kind](#changes-by-kind-6)
       - [Bug Fixes](#bug-fixes-6)
       - [Others](#others-6)
+- [v1.11.1](#v1111)
+  - [Downloads for v1.11.1](#downloads-for-v1111)
+  - [Changelog since v1.11.0](#changelog-since-v1110)
+    - [Changes by Kind](#changes-by-kind-7)
+      - [Bug Fixes](#bug-fixes-7)
+      - [Others](#others-7)
 - [v1.11.0](#v1110)
   - [Downloads for v1.11.0](#downloads-for-v1110)
   - [What's New](#whats-new)
@@ -55,7 +61,7 @@
   - [Other Notable Changes](#other-notable-changes)
     - [API Changes](#api-changes)
     - [Deprecation](#deprecation)
-    - [Bug Fixes](#bug-fixes-7)
+    - [Bug Fixes](#bug-fixes-8)
     - [Security](#security)
     - [Features & Enhancements](#features--enhancements)
   - [Other](#other)
@@ -66,6 +72,20 @@
 
 <!-- END doctoc generated TOC please keep comment here to allow auto update -->
 
+# v1.11.8
+## Downloads for v1.11.8
+
+Download v1.11.8 in the [v1.11.8 release page](https://github.com/karmada-io/karmada/releases/tag/v1.11.8).
+
+## Changelog since v1.11.7
+### Changes by Kind
+#### Bug Fixes
+- `karmada-agent`: Fixed a panic issue where the agent does not need to report secret when registering cluster. ([#6224](https://github.com/karmada-io/karmada/pull/6224), @jabellard)
+- `karmada-controller-manager`: Fixed the issue that the gracefulEvictionTask of ResourceBinding can not be cleared in case of schedule fails. ([#6236](https://github.com/karmada-io/karmada/pull/6236), @XiShanYongYe-Chang)
+
+#### Others
+None.
+
 # v1.11.7
 ## Downloads for v1.11.7
 
diff --git a/docs/CHANGELOG/CHANGELOG-1.12.md b/docs/CHANGELOG/CHANGELOG-1.12.md
index ba088fe90..ac6bd6f7a 100644
--- a/docs/CHANGELOG/CHANGELOG-1.12.md
+++ b/docs/CHANGELOG/CHANGELOG-1.12.md
@@ -2,30 +2,36 @@
 <!-- DON'T EDIT THIS SECTION, INSTEAD RE-RUN doctoc TO UPDATE -->
 **Table of Contents**  *generated with [DocToc](https://github.com/thlorenz/doctoc)*
 
-- [v1.12.4](#v1124)
-  - [Downloads for v1.12.4](#downloads-for-v1124)
-  - [Changelog since v1.12.3](#changelog-since-v1123)
+- [v1.12.5](#v1125)
+  - [Downloads for v1.12.5](#downloads-for-v1125)
+  - [Changelog since v1.12.4](#changelog-since-v1124)
     - [Changes by Kind](#changes-by-kind)
       - [Bug Fixes](#bug-fixes)
       - [Others](#others)
-- [v1.12.3](#v1123)
-  - [Downloads for v1.12.3](#downloads-for-v1123)
-  - [Changelog since v1.12.2](#changelog-since-v1122)
+- [v1.12.4](#v1124)
+  - [Downloads for v1.12.4](#downloads-for-v1124)
+  - [Changelog since v1.12.3](#changelog-since-v1123)
     - [Changes by Kind](#changes-by-kind-1)
       - [Bug Fixes](#bug-fixes-1)
       - [Others](#others-1)
-- [v1.12.2](#v1122)
-  - [Downloads for v1.12.2](#downloads-for-v1122)
-  - [Changelog since v1.12.1](#changelog-since-v1121)
+- [v1.12.3](#v1123)
+  - [Downloads for v1.12.3](#downloads-for-v1123)
+  - [Changelog since v1.12.2](#changelog-since-v1122)
     - [Changes by Kind](#changes-by-kind-2)
       - [Bug Fixes](#bug-fixes-2)
       - [Others](#others-2)
-- [v1.12.1](#v1121)
-  - [Downloads for v1.12.1](#downloads-for-v1121)
-  - [Changelog since v1.12.0](#changelog-since-v1120)
+- [v1.12.2](#v1122)
+  - [Downloads for v1.12.2](#downloads-for-v1122)
+  - [Changelog since v1.12.1](#changelog-since-v1121)
     - [Changes by Kind](#changes-by-kind-3)
       - [Bug Fixes](#bug-fixes-3)
       - [Others](#others-3)
+- [v1.12.1](#v1121)
+  - [Downloads for v1.12.1](#downloads-for-v1121)
+  - [Changelog since v1.12.0](#changelog-since-v1120)
+    - [Changes by Kind](#changes-by-kind-4)
+      - [Bug Fixes](#bug-fixes-4)
+      - [Others](#others-4)
 - [v1.12.0](#v1120)
   - [Downloads for v1.12.0](#downloads-for-v1120)
   - [What's New](#whats-new)
@@ -36,7 +42,7 @@
   - [Other Notable Changes](#other-notable-changes)
     - [API Changes](#api-changes)
     - [Deprecation](#deprecation)
-    - [Bug Fixes](#bug-fixes-4)
+    - [Bug Fixes](#bug-fixes-5)
     - [Security](#security)
     - [Features & Enhancements](#features--enhancements)
   - [Other](#other)
@@ -48,11 +54,11 @@
   - [Downloads for v1.12.0-beta.0](#downloads-for-v1120-beta0)
   - [Changelog since v1.12.0-alpha.1](#changelog-since-v1120-alpha1)
   - [Urgent Update Notes](#urgent-update-notes)
-  - [Changes by Kind](#changes-by-kind-4)
+  - [Changes by Kind](#changes-by-kind-5)
     - [API Changes](#api-changes-1)
     - [Features & Enhancements](#features--enhancements-1)
     - [Deprecation](#deprecation-1)
-    - [Bug Fixes](#bug-fixes-5)
+    - [Bug Fixes](#bug-fixes-6)
     - [Security](#security-1)
   - [Other](#other-1)
     - [Dependencies](#dependencies-1)
@@ -62,11 +68,11 @@
   - [Downloads for v1.12.0-alpha.1](#downloads-for-v1120-alpha1)
   - [Changelog since v1.11.0](#changelog-since-v1110)
   - [Urgent Update Notes](#urgent-update-notes-1)
-  - [Changes by Kind](#changes-by-kind-5)
+  - [Changes by Kind](#changes-by-kind-6)
     - [API Changes](#api-changes-2)
     - [Features & Enhancements](#features--enhancements-2)
     - [Deprecation](#deprecation-2)
-    - [Bug Fixes](#bug-fixes-6)
+    - [Bug Fixes](#bug-fixes-7)
     - [Security](#security-2)
   - [Other](#other-2)
     - [Dependencies](#dependencies-2)
@@ -75,6 +81,21 @@
 
 <!-- END doctoc generated TOC please keep comment here to allow auto update -->
 
+# v1.12.5
+## Downloads for v1.12.5
+
+Download v1.12.5 in the [v1.12.5 release page](https://github.com/karmada-io/karmada/releases/tag/v1.12.5).
+
+## Changelog since v1.12.4
+### Changes by Kind
+#### Bug Fixes
+- `karmada-agent`: Fixed a panic issue where the agent does not need to report secret when registering cluster. ([#6223](https://github.com/karmada-io/karmada/pull/6223), @jabellard)
+- `karmada-controller-manager`: Fixed the issue that the gracefulEvictionTask of ResourceBinding can not be cleared in case of schedule fails. ([#6235](https://github.com/karmada-io/karmada/pull/6235), @XiShanYongYe-Chang)
+- `helm`: Fixed the issue where the required ServiceAccount was missing when the certificate mode was set to custom. ([#6241](https://github.com/karmada-io/karmada/pull/6241), @seanlaii)
+
+#### Others
+None.
+
 # v1.12.4
 ## Downloads for v1.12.4
 
diff --git a/docs/CHANGELOG/CHANGELOG-1.13.md b/docs/CHANGELOG/CHANGELOG-1.13.md
index 21d789333..3f06e1bc9 100644
--- a/docs/CHANGELOG/CHANGELOG-1.13.md
+++ b/docs/CHANGELOG/CHANGELOG-1.13.md
@@ -2,6 +2,12 @@
 <!-- DON'T EDIT THIS SECTION, INSTEAD RE-RUN doctoc TO UPDATE -->
 **Table of Contents**  *generated with [DocToc](https://github.com/thlorenz/doctoc)*
 
+- [v1.13.1](#v1131)
+  - [Downloads for v1.13.1](#downloads-for-v1131)
+  - [Changelog since v1.13.0](#changelog-since-v1130)
+    - [Changes by Kind](#changes-by-kind)
+      - [Bug Fixes](#bug-fixes)
+      - [Others](#others)
 - [v1.13.0](#v1130)
   - [Downloads for v1.13.0](#downloads-for-v1130)
   - [Urgent Update Notes](#urgent-update-notes)
@@ -14,7 +20,7 @@
   - [Other Notable Changes](#other-notable-changes)
     - [API Changes](#api-changes)
     - [Deprecation](#deprecation)
-    - [Bug Fixes](#bug-fixes)
+    - [Bug Fixes](#bug-fixes-1)
     - [Security](#security)
     - [Features & Enhancements](#features--enhancements)
   - [Other](#other)
@@ -26,11 +32,11 @@
   - [Downloads for v1.13.0-rc.0](#downloads-for-v1130-rc0)
   - [Changelog since v1.13.0-beta.0](#changelog-since-v1130-beta0)
   - [Urgent Update Notes](#urgent-update-notes-1)
-  - [Changes by Kind](#changes-by-kind)
+  - [Changes by Kind](#changes-by-kind-1)
     - [API Changes](#api-changes-1)
     - [Features & Enhancements](#features--enhancements-1)
     - [Deprecation](#deprecation-1)
-    - [Bug Fixes](#bug-fixes-1)
+    - [Bug Fixes](#bug-fixes-2)
     - [Security](#security-1)
   - [Other](#other-1)
     - [Dependencies](#dependencies-1)
@@ -40,11 +46,11 @@
   - [Downloads for v1.13.0-beta.0](#downloads-for-v1130-beta0)
   - [Changelog since v1.13.0-alpha.2](#changelog-since-v1130-alpha2)
   - [Urgent Update Notes](#urgent-update-notes-2)
-  - [Changes by Kind](#changes-by-kind-1)
+  - [Changes by Kind](#changes-by-kind-2)
     - [API Changes](#api-changes-2)
     - [Features & Enhancements](#features--enhancements-2)
     - [Deprecation](#deprecation-2)
-    - [Bug Fixes](#bug-fixes-2)
+    - [Bug Fixes](#bug-fixes-3)
     - [Security](#security-2)
   - [Other](#other-2)
     - [Dependencies](#dependencies-2)
@@ -54,11 +60,11 @@
   - [Downloads for v1.13.0-alpha.2](#downloads-for-v1130-alpha2)
   - [Changelog since v1.13.0-alpha.1](#changelog-since-v1130-alpha1)
   - [Urgent Update Notes](#urgent-update-notes-3)
-  - [Changes by Kind](#changes-by-kind-2)
+  - [Changes by Kind](#changes-by-kind-3)
     - [API Changes](#api-changes-3)
     - [Features & Enhancements](#features--enhancements-3)
     - [Deprecation](#deprecation-3)
-    - [Bug Fixes](#bug-fixes-3)
+    - [Bug Fixes](#bug-fixes-4)
     - [Security](#security-3)
   - [Other](#other-3)
     - [Dependencies](#dependencies-3)
@@ -68,11 +74,11 @@
   - [Downloads for v1.13.0-alpha.1](#downloads-for-v1130-alpha1)
   - [Changelog since v1.12.0](#changelog-since-v1120)
   - [Urgent Update Notes](#urgent-update-notes-4)
-  - [Changes by Kind](#changes-by-kind-3)
+  - [Changes by Kind](#changes-by-kind-4)
     - [API Changes](#api-changes-4)
     - [Features & Enhancements](#features--enhancements-4)
     - [Deprecation](#deprecation-4)
-    - [Bug Fixes](#bug-fixes-4)
+    - [Bug Fixes](#bug-fixes-5)
     - [Security](#security-4)
   - [Other](#other-4)
     - [Dependencies](#dependencies-4)
@@ -81,6 +87,22 @@
 
 <!-- END doctoc generated TOC please keep comment here to allow auto update -->
 
+# v1.13.1
+## Downloads for v1.13.1
+
+Download v1.13.1 in the [v1.13.1 release page](https://github.com/karmada-io/karmada/releases/tag/v1.13.1).
+
+## Changelog since v1.13.0
+### Changes by Kind
+#### Bug Fixes
+- `karmada-operator`: The `karmada-app` label key previously used for control plane components and for the components of the operator itself has been changed to the more idiomatic `app.kubernetes.io/name` label key. ([#6200](https://github.com/karmada-io/karmada/pull/6200), @jabellard)
+- `karmada-agent`: Fixed a panic issue where the agent does not need to report secret when registering cluster. ([#6222](https://github.com/karmada-io/karmada/pull/6222), @jabellard)
+- `karmada-controller-manager`: Fixed the issue that the gracefulEvictionTask of ResourceBinding can not be cleared in case of schedule fails. ([#6234](https://github.com/karmada-io/karmada/pull/6234), @XiShanYongYe-Chang)
+- `helm`: Fixed the issue where the required ServiceAccount was missing when the certificate mode was set to custom. ([#6242](https://github.com/karmada-io/karmada/pull/6242), @seanlaii)
+
+#### Others
+None.
+
 # v1.13.0
 ## Downloads for v1.13.0
 
diff --git a/docs/CHANGELOG/CHANGELOG-1.14.md b/docs/CHANGELOG/CHANGELOG-1.14.md
new file mode 100644
index 000000000..2e8b0c929
--- /dev/null
+++ b/docs/CHANGELOG/CHANGELOG-1.14.md
@@ -0,0 +1,66 @@
+<!-- START doctoc generated TOC please keep comment here to allow auto update -->
+<!-- DON'T EDIT THIS SECTION, INSTEAD RE-RUN doctoc TO UPDATE -->
+**Table of Contents**  *generated with [DocToc](https://github.com/thlorenz/doctoc)*
+
+- [v1.14.0-alpha.1](#v1140-alpha1)
+  - [Downloads for v1.14.0-alpha.1](#downloads-for-v1140-alpha1)
+  - [Changelog since v1.13.0](#changelog-since-v1130)
+  - [Urgent Update Notes](#urgent-update-notes)
+  - [Changes by Kind](#changes-by-kind)
+    - [API Changes](#api-changes)
+    - [Features & Enhancements](#features--enhancements)
+    - [Deprecation](#deprecation)
+    - [Bug Fixes](#bug-fixes)
+    - [Security](#security)
+  - [Other](#other)
+    - [Dependencies](#dependencies)
+    - [Helm Charts](#helm-charts)
+    - [Instrumentation](#instrumentation)
+
+<!-- END doctoc generated TOC please keep comment here to allow auto update -->
+
+# v1.14.0-alpha.1
+## Downloads for v1.14.0-alpha.1
+
+Download v1.14.0-alpha.1 in the [v1.14.0-alpha.1 release page](https://github.com/karmada-io/karmada/releases/tag/v1.14.0-alpha.1).
+
+## Changelog since v1.13.0
+
+## Urgent Update Notes
+
+## Changes by Kind
+
+### API Changes
+- Introduced `spec.customCertificate.leafCertValidityDays` field in `Karmada` API to specify a custom validity period in days for control plane leaf certificates (e.g., API server certificate). When not explicitly set, the default validity period of 1 year is used. ([#6193](https://github.com/karmada-io/karmada/pull/6193), @jabellard)
+
+### Features & Enhancements
+- `karmada-controller-manager`: All controller reconcile frequency will honor a unified rate limiter configuration(--rate-limiter-*). ([#6145](https://github.com/karmada-io/karmada/pull/6145), @CharlesQQ)
+- `karmada-agent`: All controller reconcile frequency will honor a unified rate limiter configuration(--rate-limiter-*). ([#6145](https://github.com/karmada-io/karmada/pull/6145), @CharlesQQ)
+
+### Deprecation
+- `karmadactl`: The flag `--ca-cert-path` of `register`, which has been deprecated in release-1.13, now has been removed. ([#6191](https://github.com/karmada-io/karmada/pull/6191), @husnialhamdani)
+- The label `propagation.karmada.io/instruction` now has been deprecated in favor of the field(`.spec.suspendDispatching`) in Work API, the label will be removed in future releases. ([#6043](https://github.com/karmada-io/karmada/pull/6043), @vie-serendipity)
+
+### Bug Fixes
+- `karmada-agent`: Fixed a panic issue where the agent does not need to report secret when registering cluster. ([#6214](https://github.com/karmada-io/karmada/pull/6214), @jabellard)
+- `karmada-operator`: The `karmada-app` label key previously used for control plane components and for the components of the operator itself has been changed to the more idiomatic `app.kubernetes.io/name` label key. ([#6180](https://github.com/karmada-io/karmada/pull/6180), @jabellard)
+- `karmada-controller-manager`: Fixed the issue that the gracefulEvictionTask of ResourceBinding can not be cleared in case of schedule fails. ([#6227](https://github.com/karmada-io/karmada/pull/6227), @XiShanYongYe-Chang)
+- `helm`: Fixed the issue where the required ServiceAccount was missing when the certificate mode was set to custom. ([#6188](https://github.com/karmada-io/karmada/pull/6188), @seanlaii)
+- Unify the rate limiter for different clients in each component to access karmada-apiserver. Access may be restricted in large-scale environments compared to before the modification. Administrators can avoid this situation by adjusting upward the rate limit parameters 'kube-api-qps' and 'kube-api-burst' for each component to access karmada-apiserver, and adjusting 'cluster-api-qps' and 'cluster-api-burst' for scheduler-estimator to access member cluster apiserver. ([#6095](https://github.com/karmada-io/karmada/pull/6095), @zach593)
+
+### Security
+None.
+
+## Other
+### Dependencies
+- Karmada is now built with Golang v1.23.7. ([#6218](https://github.com/karmada-io/karmada/pull/6218), @seanlaii)
+- Bump golang.org/x/net to 0.37.0. ([#6225](https://github.com/karmada-io/karmada/pull/6225), @seanlaii)
+- Bump mockery to v2.53.3 to remove the hashicorp packages. ([#6212](https://github.com/karmada-io/karmada/pull/6212), @seanlaii)
+
+### Helm Charts
+- `HELM Chart`: OpenID Connect based auth can be configured for the Karmada API server when installing via Helm. ([#6159](https://github.com/karmada-io/karmada/pull/6159), @tw-mnewman)
+- `Helm chart`: Added helm index for 1.13 release. ([#6196](https://github.com/karmada-io/karmada/pull/6196), @zhzhuang-zju)
+
+### Instrumentation
+- `karmada-controller-manager`: metric `recreate_resource_to_cluster` has been merged into `create_resource_to_cluster`. ([#6148](https://github.com/karmada-io/karmada/pull/6148), @zach593)
+- `karmada-operator`: Introduced `karmada_build_info` metrics to emit the build info, as well as a bunch of Go runtime metrics. ([#6044](https://github.com/karmada-io/karmada/pull/6044), @dongjiang1989)
diff --git a/go.mod b/go.mod
index a18296cd4..daf780b6f 100644
--- a/go.mod
+++ b/go.mod
@@ -1,6 +1,6 @@
 module github.com/karmada-io/karmada
 
-go 1.23.7 // keep in sync with .go-version
+go 1.23.8 // keep in sync with .go-version
 
 require (
 	github.com/adhocore/gronx v1.6.3
diff --git a/operator/cmd/operator/app/operator.go b/operator/cmd/operator/app/operator.go
index 0fc8fd200..068202037 100644
--- a/operator/cmd/operator/app/operator.go
+++ b/operator/cmd/operator/app/operator.go
@@ -112,7 +112,6 @@ func Run(ctx context.Context, o *options.Options) error {
 		return err
 	}
 
-	// `karmada_operator_build_info` metrics for operator version upgrade
 	ctrlmetrics.Registry.MustRegister(versionmetrics.NewBuildInfoCollector())
 
 	controllerCtx := ctrlctx.Context{
diff --git a/pkg/controllers/multiclusterservice/mcs_controller.go b/pkg/controllers/multiclusterservice/mcs_controller.go
index 438902783..edc8fc5b4 100644
--- a/pkg/controllers/multiclusterservice/mcs_controller.go
+++ b/pkg/controllers/multiclusterservice/mcs_controller.go
@@ -149,7 +149,7 @@ func (c *MCSController) retrieveMultiClusterService(ctx context.Context, mcs *ne
 			continue
 		}
 
-		if providerClusters.Has(clusterName) && c.IsClusterReady(ctx, clusterName) {
+		if providerClusters.Has(clusterName) {
 			continue
 		}
 
@@ -524,17 +524,6 @@ func (c *MCSController) updateMultiClusterServiceStatus(ctx context.Context, mcs
 	})
 }
 
-// IsClusterReady checks whether the cluster is ready.
-func (c *MCSController) IsClusterReady(ctx context.Context, clusterName string) bool {
-	cluster := &clusterv1alpha1.Cluster{}
-	if err := c.Client.Get(ctx, types.NamespacedName{Name: clusterName}, cluster); err != nil {
-		klog.Errorf("Failed to get cluster object(%s):%v", clusterName, err)
-		return false
-	}
-
-	return util.IsClusterReady(&cluster.Status)
-}
-
 // SetupWithManager creates a controller and register to controller manager.
 func (c *MCSController) SetupWithManager(mgr controllerruntime.Manager) error {
 	mcsPredicateFunc := predicate.Funcs{
diff --git a/pkg/karmadactl/addons/utils/helpers.go b/pkg/karmadactl/addons/utils/helpers.go
index bb0a79b45..c67b84fcd 100644
--- a/pkg/karmadactl/addons/utils/helpers.go
+++ b/pkg/karmadactl/addons/utils/helpers.go
@@ -19,6 +19,7 @@ package utils
 import (
 	"context"
 	"fmt"
+	"time"
 
 	appsv1 "k8s.io/api/apps/v1"
 	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
@@ -35,7 +36,7 @@ var (
 	// This blocks until the Deployment's observed generation and ready replicas match the desired state,
 	// ensuring it is fully rolled out.
 	WaitForDeploymentRollout = func(c clientset.Interface, dep *appsv1.Deployment, timeoutSeconds int) error {
-		return cmdutil.WaitForDeploymentRollout(c, dep, timeoutSeconds)
+		return cmdutil.WaitForDeploymentRollout(c, dep, time.Duration(timeoutSeconds)*time.Second)
 	}
 )
 
diff --git a/pkg/karmadactl/cmdinit/utils/deploy.go b/pkg/karmadactl/cmdinit/utils/deploy.go
index 2c5194c1f..2143c98e0 100644
--- a/pkg/karmadactl/cmdinit/utils/deploy.go
+++ b/pkg/karmadactl/cmdinit/utils/deploy.go
@@ -18,6 +18,7 @@ package utils
 
 import (
 	"context"
+	"time"
 
 	appsv1 "k8s.io/api/apps/v1"
 	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
@@ -32,5 +33,5 @@ func CreateDeployAndWait(kubeClientSet kubernetes.Interface, deployment *appsv1.
 	if _, err := kubeClientSet.AppsV1().Deployments(deployment.GetNamespace()).Create(context.TODO(), deployment, metav1.CreateOptions{}); err != nil {
 		klog.Warning(err)
 	}
-	return util.WaitForDeploymentRollout(kubeClientSet, deployment, waitComponentReadyTimeout)
+	return util.WaitForDeploymentRollout(kubeClientSet, deployment, time.Duration(waitComponentReadyTimeout)*time.Second)
 }
diff --git a/pkg/karmadactl/register/register.go b/pkg/karmadactl/register/register.go
index 6c33ce831..b8dbfe714 100644
--- a/pkg/karmadactl/register/register.go
+++ b/pkg/karmadactl/register/register.go
@@ -437,7 +437,7 @@ func (o *CommandRegisterOption) EnsureNecessaryResourcesExistInMemberCluster(boo
 		return err
 	}
 
-	if err = cmdutil.WaitForDeploymentRollout(o.memberClusterClient, KarmadaAgentDeployment, int(o.Timeout)); err != nil {
+	if err = cmdutil.WaitForDeploymentRollout(o.memberClusterClient, KarmadaAgentDeployment, o.Timeout); err != nil {
 		return err
 	}
 
diff --git a/pkg/karmadactl/util/check.go b/pkg/karmadactl/util/check.go
index fedf901ac..8c8be3a29 100644
--- a/pkg/karmadactl/util/check.go
+++ b/pkg/karmadactl/util/check.go
@@ -59,10 +59,10 @@ func WaitForStatefulSetRollout(c kubernetes.Interface, sts *appsv1.StatefulSet,
 	return nil
 }
 
-// WaitForDeploymentRollout  wait for Deployment reaches the ready state or timeout.
-func WaitForDeploymentRollout(c kubernetes.Interface, dep *appsv1.Deployment, timeoutSeconds int) error {
+// WaitForDeploymentRollout wait for Deployment reaches the ready state or timeout.
+func WaitForDeploymentRollout(c kubernetes.Interface, dep *appsv1.Deployment, timeout time.Duration) error {
 	var lastErr error
-	pollError := wait.PollUntilContextTimeout(context.TODO(), time.Second, time.Duration(timeoutSeconds)*time.Second, true, func(ctx context.Context) (bool, error) {
+	pollError := wait.PollUntilContextTimeout(context.TODO(), time.Second, timeout, true, func(ctx context.Context) (bool, error) {
 		d, err := c.AppsV1().Deployments(dep.GetNamespace()).Get(ctx, dep.GetName(), metav1.GetOptions{})
 		if err != nil {
 			lastErr = err
diff --git a/pkg/util/cluster.go b/pkg/util/cluster.go
index a57e39b58..f524f9305 100644
--- a/pkg/util/cluster.go
+++ b/pkg/util/cluster.go
@@ -94,10 +94,6 @@ func (r *ClusterRegisterOption) Validate(karmadaClient karmadaclientset.Interfac
 		return err
 	}
 	clusterIDUsed, clusterNameUsed, sameCluster := r.validateCluster(clusterList)
-	if err != nil {
-		return err
-	}
-
 	if isAgent && sameCluster {
 		return nil
 	}
diff --git a/pkg/util/helper/ingress.go b/pkg/util/helper/ingress.go
new file mode 100644
index 000000000..110d0af4d
--- /dev/null
+++ b/pkg/util/helper/ingress.go
@@ -0,0 +1,104 @@
+/*
+Copyright 2025 The Karmada 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 helper
+
+import (
+	"fmt"
+	"sort"
+
+	networkingv1 "k8s.io/api/networking/v1"
+	"k8s.io/apimachinery/pkg/util/sets"
+)
+
+// DedupeAndSortIngressLoadBalancerIngress dedupes and sorts the IngressLoadBalancerIngress slice.
+func DedupeAndSortIngressLoadBalancerIngress(ingresses []networkingv1.IngressLoadBalancerIngress) []networkingv1.IngressLoadBalancerIngress {
+	errMap := make(map[string]*string)
+	for _, ingress := range ingresses {
+		for j, port := range ingress.Ports {
+			if port.Error != nil {
+				// let same error use same ptr, to let *Error be comparable
+				if errMap[*port.Error] == nil {
+					errMap[*port.Error] = port.Error
+				} else {
+					ingress.Ports[j].Error = errMap[*port.Error]
+				}
+			}
+		}
+	}
+	ingressMap := make(map[string]*networkingv1.IngressLoadBalancerIngress)
+	portStatusMap := make(map[string]sets.Set[networkingv1.IngressPortStatus])
+	for _, ingress := range ingresses {
+		key := fmt.Sprintf("%s/%s", ingress.Hostname, ingress.IP)
+		if portStatusMap[key] == nil {
+			portStatusMap[key] = sets.New[networkingv1.IngressPortStatus]()
+		}
+		portStatusMap[key].Insert(ingress.Ports...)
+		ingress.Ports = nil
+		ingressMap[key] = &ingress
+	}
+	for key, portStatus := range portStatusMap {
+		for port := range portStatus {
+			ingressMap[key].Ports = append(ingressMap[key].Ports, port)
+		}
+
+		sortIngressPortStatus(ingressMap[key].Ports)
+	}
+	out := make([]networkingv1.IngressLoadBalancerIngress, 0, len(ingressMap))
+	for _, v := range ingressMap {
+		out = append(out, *v)
+	}
+
+	sortIngressLoadBalancerIngress(out)
+
+	return out
+}
+
+func sortIngressPortStatus(ports []networkingv1.IngressPortStatus) {
+	sort.Slice(ports, func(i, j int) bool {
+		if ports[i].Port != ports[j].Port {
+			return ports[i].Port < ports[j].Port
+		}
+
+		if ports[i].Protocol != ports[j].Protocol {
+			return ports[i].Protocol < ports[j].Protocol
+		}
+
+		if ports[i].Error == nil {
+			return true
+		}
+		if ports[j].Error == nil {
+			return false
+		}
+		return *ports[i].Error < *ports[j].Error
+	})
+}
+
+func sortIngressLoadBalancerIngress(ingresses []networkingv1.IngressLoadBalancerIngress) {
+	sort.Slice(ingresses, func(i, j int) bool {
+		if ingresses[i].Hostname != ingresses[j].Hostname {
+			if ingresses[j].Hostname == "" {
+				return true
+			}
+			if ingresses[i].Hostname == "" {
+				return false
+			}
+			return ingresses[i].Hostname < ingresses[j].Hostname
+		}
+
+		return ingresses[i].IP < ingresses[j].IP
+	})
+}
diff --git a/pkg/util/helper/service.go b/pkg/util/helper/service.go
new file mode 100644
index 000000000..01af38cd8
--- /dev/null
+++ b/pkg/util/helper/service.go
@@ -0,0 +1,123 @@
+/*
+Copyright 2025 The Karmada 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 helper
+
+import (
+	"fmt"
+	"sort"
+
+	corev1 "k8s.io/api/core/v1"
+	"k8s.io/apimachinery/pkg/util/sets"
+)
+
+// DedupeAndSortServiceLoadBalancerIngress dedupes and sorts the ServiceLoadBalancerIngress slice.
+func DedupeAndSortServiceLoadBalancerIngress(ingresses []corev1.LoadBalancerIngress) []corev1.LoadBalancerIngress {
+	ipModeMap := make(map[corev1.LoadBalancerIPMode]*corev1.LoadBalancerIPMode)
+	errMap := make(map[string]*string)
+	for i, ingress := range ingresses {
+		if ingress.IPMode != nil {
+			// let same IPMode use same ptr, to let *IPMode be comparable
+			if ipModeMap[*ingress.IPMode] == nil {
+				ipModeMap[*ingress.IPMode] = ingress.IPMode
+			} else {
+				ingresses[i].IPMode = ipModeMap[*ingress.IPMode]
+			}
+		}
+		for j, port := range ingress.Ports {
+			if port.Error != nil {
+				// let same error use same ptr, to let *Error be comparable
+				if errMap[*port.Error] == nil {
+					errMap[*port.Error] = port.Error
+				} else {
+					ingress.Ports[j].Error = errMap[*port.Error]
+				}
+			}
+		}
+	}
+	ingressMap := make(map[string]*corev1.LoadBalancerIngress)
+	portStatusMap := make(map[string]sets.Set[corev1.PortStatus])
+	for _, ingress := range ingresses {
+		key := fmt.Sprintf("%s/%s/%p", ingress.Hostname, ingress.IP, ingress.IPMode)
+		if portStatusMap[key] == nil {
+			portStatusMap[key] = sets.New[corev1.PortStatus]()
+		}
+		portStatusMap[key].Insert(ingress.Ports...)
+		ingress.Ports = nil
+		ingressMap[key] = &ingress
+	}
+	for key, portStatus := range portStatusMap {
+		for port := range portStatus {
+			ingressMap[key].Ports = append(ingressMap[key].Ports, port)
+		}
+
+		sortServicePortStatus(ingressMap[key].Ports)
+	}
+	out := make([]corev1.LoadBalancerIngress, 0, len(ingressMap))
+	for _, v := range ingressMap {
+		out = append(out, *v)
+	}
+
+	sortServiceLoadBalancerIngress(out)
+
+	return out
+}
+
+func sortServicePortStatus(ports []corev1.PortStatus) {
+	sort.Slice(ports, func(i, j int) bool {
+		if ports[i].Port != ports[j].Port {
+			return ports[i].Port < ports[j].Port
+		}
+
+		if ports[i].Protocol != ports[j].Protocol {
+			return ports[i].Protocol < ports[j].Protocol
+		}
+
+		if ports[i].Error == nil {
+			return true
+		}
+		if ports[j].Error == nil {
+			return false
+		}
+		return *ports[i].Error < *ports[j].Error
+	})
+}
+
+func sortServiceLoadBalancerIngress(ingresses []corev1.LoadBalancerIngress) {
+	sort.Slice(ingresses, func(i, j int) bool {
+		if ingresses[i].Hostname != ingresses[j].Hostname {
+			if ingresses[j].Hostname == "" {
+				return true
+			}
+			if ingresses[i].Hostname == "" {
+				return false
+			}
+			return ingresses[i].Hostname < ingresses[j].Hostname
+		}
+
+		if ingresses[i].IP != ingresses[j].IP {
+			return ingresses[i].IP < ingresses[j].IP
+		}
+
+		if ingresses[j].IPMode == nil {
+			return true
+		}
+		if ingresses[i].IPMode == nil {
+			return false
+		}
+		return *ingresses[i].IPMode < *ingresses[j].IPMode
+	})
+}



Test patch:
diff --git a/pkg/controllers/multiclusterservice/mcs_controller_test.go b/pkg/controllers/multiclusterservice/mcs_controller_test.go
index 0008277a0..5a80f263f 100644
--- a/pkg/controllers/multiclusterservice/mcs_controller_test.go
+++ b/pkg/controllers/multiclusterservice/mcs_controller_test.go
@@ -727,72 +727,6 @@ func TestClaimMultiClusterServiceForService(t *testing.T) {
 	}
 }
 
-func TestIsClusterReady(t *testing.T) {
-	tests := []struct {
-		name          string
-		clusterName   string
-		clusterObj    *clusterv1alpha1.Cluster
-		expectedReady bool
-	}{
-		{
-			name:        "cluster is ready",
-			clusterName: "ready-cluster",
-			clusterObj: &clusterv1alpha1.Cluster{
-				ObjectMeta: metav1.ObjectMeta{
-					Name: "ready-cluster",
-				},
-				Status: clusterv1alpha1.ClusterStatus{
-					Conditions: []metav1.Condition{
-						{
-							Type:   clusterv1alpha1.ClusterConditionReady,
-							Status: metav1.ConditionTrue,
-						},
-					},
-				},
-			},
-			expectedReady: true,
-		},
-		{
-			name:        "cluster is not ready",
-			clusterName: "not-ready-cluster",
-			clusterObj: &clusterv1alpha1.Cluster{
-				ObjectMeta: metav1.ObjectMeta{
-					Name: "not-ready-cluster",
-				},
-				Status: clusterv1alpha1.ClusterStatus{
-					Conditions: []metav1.Condition{
-						{
-							Type:   clusterv1alpha1.ClusterConditionReady,
-							Status: metav1.ConditionFalse,
-						},
-					},
-				},
-			},
-			expectedReady: false,
-		},
-		{
-			name:          "cluster does not exist",
-			clusterName:   "non-existent-cluster",
-			clusterObj:    nil,
-			expectedReady: false,
-		},
-	}
-
-	for _, tt := range tests {
-		t.Run(tt.name, func(t *testing.T) {
-			var objects []runtime.Object
-			if tt.clusterObj != nil {
-				objects = append(objects, tt.clusterObj)
-			}
-
-			controller := newFakeController(objects...)
-
-			ready := controller.IsClusterReady(context.Background(), tt.clusterName)
-			assert.Equal(t, tt.expectedReady, ready, "IsClusterReady() result does not match expected")
-		})
-	}
-}
-
 func TestServiceHasCrossClusterMultiClusterService(t *testing.T) {
 	tests := []struct {
 		name     string
diff --git a/pkg/resourceinterpreter/default/native/aggregatestatus.go b/pkg/resourceinterpreter/default/native/aggregatestatus.go
index a9e5101cb..48d4ee877 100644
--- a/pkg/resourceinterpreter/default/native/aggregatestatus.go
+++ b/pkg/resourceinterpreter/default/native/aggregatestatus.go
@@ -147,6 +147,7 @@ func aggregateServiceStatus(object *unstructured.Unstructured, aggregatedStatusI
 
 		newStatus.LoadBalancer.Ingress = append(newStatus.LoadBalancer.Ingress, temp.LoadBalancer.Ingress...)
 	}
+	newStatus.LoadBalancer.Ingress = helper.DedupeAndSortServiceLoadBalancerIngress(newStatus.LoadBalancer.Ingress)
 
 	if reflect.DeepEqual(service.Status, *newStatus) {
 		klog.V(3).Infof("Ignore update service(%s/%s) status as up to date", service.Namespace, service.Name)
@@ -179,6 +180,7 @@ func aggregateIngressStatus(object *unstructured.Unstructured, aggregatedStatusI
 
 		newStatus.LoadBalancer.Ingress = append(newStatus.LoadBalancer.Ingress, temp.LoadBalancer.Ingress...)
 	}
+	newStatus.LoadBalancer.Ingress = helper.DedupeAndSortIngressLoadBalancerIngress(newStatus.LoadBalancer.Ingress)
 
 	if reflect.DeepEqual(ingress.Status, *newStatus) {
 		klog.V(3).Infof("Ignore update ingress(%s/%s) status as up to date", ingress.Namespace, ingress.Name)
diff --git a/pkg/util/helper/ingress_test.go b/pkg/util/helper/ingress_test.go
new file mode 100644
index 000000000..1b1480523
--- /dev/null
+++ b/pkg/util/helper/ingress_test.go
@@ -0,0 +1,151 @@
+/*
+Copyright 2025 The Karmada 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 helper
+
+import (
+	"testing"
+
+	"github.com/stretchr/testify/assert"
+	networkingv1 "k8s.io/api/networking/v1"
+	"k8s.io/utils/ptr"
+)
+
+func TestDedupeAndSortIngressLoadBalancerIngress(t *testing.T) {
+	type args struct {
+		ingresses []networkingv1.IngressLoadBalancerIngress
+	}
+	tests := []struct {
+		name string
+		args args
+		want []networkingv1.IngressLoadBalancerIngress
+	}{
+		{
+			name: "sort hostname",
+			args: args{
+				ingresses: []networkingv1.IngressLoadBalancerIngress{
+					{Hostname: "hostname-2"},
+					{Hostname: "hostname-1"},
+				},
+			},
+			want: []networkingv1.IngressLoadBalancerIngress{
+				{Hostname: "hostname-1"},
+				{Hostname: "hostname-2"},
+			},
+		},
+		{
+			name: "sort ip",
+			args: args{
+				ingresses: []networkingv1.IngressLoadBalancerIngress{
+					{IP: "2.2.2.2"},
+					{IP: "1.1.1.1"},
+				},
+			},
+			want: []networkingv1.IngressLoadBalancerIngress{
+				{IP: "1.1.1.1"},
+				{IP: "2.2.2.2"},
+			},
+		},
+		{
+			name: "hostname should in front of ip",
+			args: args{
+				ingresses: []networkingv1.IngressLoadBalancerIngress{
+					{IP: "1.1.1.1"},
+					{Hostname: "hostname-1"},
+				},
+			},
+			want: []networkingv1.IngressLoadBalancerIngress{
+				{Hostname: "hostname-1"},
+				{IP: "1.1.1.1"},
+			},
+		},
+		{
+			name: "merge hostname and ip",
+			args: args{
+				ingresses: []networkingv1.IngressLoadBalancerIngress{
+					{IP: "1.1.1.1"},
+					{Hostname: "hostname-1"},
+					{IP: "1.1.1.1"},
+					{Hostname: "hostname-1"},
+					{IP: "1.1.1.1"},
+					{Hostname: "hostname-1"},
+				},
+			},
+			want: []networkingv1.IngressLoadBalancerIngress{
+				{Hostname: "hostname-1"},
+				{IP: "1.1.1.1"},
+			},
+		},
+		{
+			name: "merge and sort ports",
+			args: args{
+				ingresses: []networkingv1.IngressLoadBalancerIngress{
+					{Hostname: "hostname-1", Ports: []networkingv1.IngressPortStatus{
+						{Port: 80, Protocol: "TCP"},
+					}},
+					{Hostname: "hostname-1", Ports: []networkingv1.IngressPortStatus{
+						{Port: 81, Protocol: "TCP"},
+					}},
+					{Hostname: "hostname-1", Ports: []networkingv1.IngressPortStatus{
+						{Port: 80, Protocol: "TCP"},
+					}},
+				},
+			},
+			want: []networkingv1.IngressLoadBalancerIngress{
+				{Hostname: "hostname-1", Ports: []networkingv1.IngressPortStatus{
+					{Port: 80, Protocol: "TCP"},
+					{Port: 81, Protocol: "TCP"},
+				}},
+			},
+		},
+		{
+			name: "merge and sort errors",
+			args: args{
+				ingresses: []networkingv1.IngressLoadBalancerIngress{
+					{Hostname: "hostname-1", Ports: []networkingv1.IngressPortStatus{
+						{Port: 80, Protocol: "TCP", Error: ptr.To("error-1")},
+					}},
+					{Hostname: "hostname-1", Ports: []networkingv1.IngressPortStatus{
+						{Port: 80, Protocol: "TCP", Error: ptr.To("error-1")},
+					}},
+					{Hostname: "hostname-1", Ports: []networkingv1.IngressPortStatus{
+						{Port: 80, Protocol: "TCP", Error: ptr.To("error-2")},
+					}},
+					{Hostname: "hostname-1", Ports: []networkingv1.IngressPortStatus{
+						{Port: 80, Protocol: "TCP"},
+					}},
+				},
+			},
+			want: []networkingv1.IngressLoadBalancerIngress{
+				{Hostname: "hostname-1", Ports: []networkingv1.IngressPortStatus{
+					{Port: 80, Protocol: "TCP"},
+					{Port: 80, Protocol: "TCP", Error: ptr.To("error-1")},
+					{Port: 80, Protocol: "TCP", Error: ptr.To("error-2")},
+				}},
+			},
+		},
+	}
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			for i := 0; i < 1000; i++ { // eliminate randomness in sorting
+				pass := assert.Equalf(t, tt.want, DedupeAndSortIngressLoadBalancerIngress(tt.args.ingresses), "DedupeAndSortIngressLoadBalancerIngress(%v)", tt.args.ingresses)
+				if !pass {
+					break
+				}
+			}
+		})
+	}
+}
diff --git a/pkg/util/helper/service_test.go b/pkg/util/helper/service_test.go
new file mode 100644
index 000000000..10d89f585
--- /dev/null
+++ b/pkg/util/helper/service_test.go
@@ -0,0 +1,176 @@
+/*
+Copyright 2025 The Karmada 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 helper
+
+import (
+	"testing"
+
+	"github.com/stretchr/testify/assert"
+	corev1 "k8s.io/api/core/v1"
+	"k8s.io/utils/ptr"
+)
+
+func TestDedupeAndSortServiceLoadBalancerIngress(t *testing.T) {
+	type args struct {
+		ingresses []corev1.LoadBalancerIngress
+	}
+	tests := []struct {
+		name string
+		args args
+		want []corev1.LoadBalancerIngress
+	}{
+		{
+			name: "sort hostname",
+			args: args{
+				ingresses: []corev1.LoadBalancerIngress{
+					{Hostname: "hostname-2"},
+					{Hostname: "hostname-1"},
+				},
+			},
+			want: []corev1.LoadBalancerIngress{
+				{Hostname: "hostname-1"},
+				{Hostname: "hostname-2"},
+			},
+		},
+		{
+			name: "sort ip",
+			args: args{
+				ingresses: []corev1.LoadBalancerIngress{
+					{IP: "2.2.2.2"},
+					{IP: "1.1.1.1"},
+				},
+			},
+			want: []corev1.LoadBalancerIngress{
+				{IP: "1.1.1.1"},
+				{IP: "2.2.2.2"},
+			},
+		},
+		{
+			name: "hostname should in front of ip",
+			args: args{
+				ingresses: []corev1.LoadBalancerIngress{
+					{IP: "1.1.1.1"},
+					{Hostname: "hostname-1"},
+				},
+			},
+			want: []corev1.LoadBalancerIngress{
+				{Hostname: "hostname-1"},
+				{IP: "1.1.1.1"},
+			},
+		},
+		{
+			name: "sort ipMode",
+			args: args{
+				ingresses: []corev1.LoadBalancerIngress{
+					{IP: "1.1.1.1", IPMode: ptr.To(corev1.LoadBalancerIPModeProxy)},
+					{IP: "1.1.1.1", IPMode: ptr.To(corev1.LoadBalancerIPModeVIP)},
+				},
+			},
+			want: []corev1.LoadBalancerIngress{
+				{IP: "1.1.1.1", IPMode: ptr.To(corev1.LoadBalancerIPModeProxy)},
+				{IP: "1.1.1.1", IPMode: ptr.To(corev1.LoadBalancerIPModeVIP)},
+			},
+		},
+		{
+			name: "merge ipMode",
+			args: args{
+				ingresses: []corev1.LoadBalancerIngress{
+					{IP: "1.1.1.1", IPMode: ptr.To(corev1.LoadBalancerIPModeVIP)},
+					{IP: "1.1.1.1", IPMode: ptr.To(corev1.LoadBalancerIPModeVIP)},
+				},
+			},
+			want: []corev1.LoadBalancerIngress{
+				{IP: "1.1.1.1", IPMode: ptr.To(corev1.LoadBalancerIPModeVIP)},
+			},
+		},
+		{
+			name: "merge hostname and ip",
+			args: args{
+				ingresses: []corev1.LoadBalancerIngress{
+					{IP: "1.1.1.1"},
+					{Hostname: "hostname-1"},
+					{IP: "1.1.1.1"},
+					{Hostname: "hostname-1"},
+					{IP: "1.1.1.1"},
+					{Hostname: "hostname-1"},
+				},
+			},
+			want: []corev1.LoadBalancerIngress{
+				{Hostname: "hostname-1"},
+				{IP: "1.1.1.1"},
+			},
+		},
+		{
+			name: "merge and sort ports",
+			args: args{
+				ingresses: []corev1.LoadBalancerIngress{
+					{Hostname: "hostname-1", Ports: []corev1.PortStatus{
+						{Port: 80, Protocol: "TCP"},
+					}},
+					{Hostname: "hostname-1", Ports: []corev1.PortStatus{
+						{Port: 81, Protocol: "TCP"},
+					}},
+					{Hostname: "hostname-1", Ports: []corev1.PortStatus{
+						{Port: 80, Protocol: "TCP"},
+					}},
+				},
+			},
+			want: []corev1.LoadBalancerIngress{
+				{Hostname: "hostname-1", Ports: []corev1.PortStatus{
+					{Port: 80, Protocol: "TCP"},
+					{Port: 81, Protocol: "TCP"},
+				}},
+			},
+		},
+		{
+			name: "merge and sort errors",
+			args: args{
+				ingresses: []corev1.LoadBalancerIngress{
+					{Hostname: "hostname-1", Ports: []corev1.PortStatus{
+						{Port: 80, Protocol: "TCP", Error: ptr.To("error-1")},
+					}},
+					{Hostname: "hostname-1", Ports: []corev1.PortStatus{
+						{Port: 80, Protocol: "TCP", Error: ptr.To("error-1")},
+					}},
+					{Hostname: "hostname-1", Ports: []corev1.PortStatus{
+						{Port: 80, Protocol: "TCP", Error: ptr.To("error-2")},
+					}},
+					{Hostname: "hostname-1", Ports: []corev1.PortStatus{
+						{Port: 80, Protocol: "TCP"},
+					}},
+				},
+			},
+			want: []corev1.LoadBalancerIngress{
+				{Hostname: "hostname-1", Ports: []corev1.PortStatus{
+					{Port: 80, Protocol: "TCP"},
+					{Port: 80, Protocol: "TCP", Error: ptr.To("error-1")},
+					{Port: 80, Protocol: "TCP", Error: ptr.To("error-2")},
+				}},
+			},
+		},
+	}
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			for i := 0; i < 1000; i++ { // eliminate randomness in sorting
+				pass := assert.Equalf(t, tt.want, DedupeAndSortServiceLoadBalancerIngress(tt.args.ingresses), "DedupeAndSortServiceLoadBalancerIngress(%v)", tt.args.ingresses)
+				if !pass {
+					break
+				}
+			}
+		})
+	}
+}
diff --git a/test/e2e/suites/base/karmadactl_test.go b/test/e2e/suites/base/karmadactl_test.go
index 9db5ef4cd..6c3d89ab4 100644
--- a/test/e2e/suites/base/karmadactl_test.go
+++ b/test/e2e/suites/base/karmadactl_test.go
@@ -1542,6 +1542,148 @@ func WriteYamlToFile(obj interface{}, filePath string) error {
 	return nil
 }
 
+var _ = framework.SerialDescribe("Karmadactl register testing", ginkgo.Ordered, ginkgo.Labels{NeedCreateCluster}, func() {
+	var (
+		newClusterName, clusterContext                 string
+		homeDir, kubeConfigPath, controlPlane          string
+		karmadaAPIEndpoint, karmadaAPIEndpointExpected string
+		token, discoveryTokenCACertHash                string
+	)
+
+	ginkgo.BeforeAll(func() {
+		ginkgo.By("Initialize dependencies", func() {
+			// Initialize member cluster variables.
+			newClusterName = "member-e2e-" + rand.String(RandomStrLength)
+			homeDir = os.Getenv("HOME")
+			kubeConfigPath = fmt.Sprintf("%s/.kube/%s.config", homeDir, newClusterName)
+			controlPlane = fmt.Sprintf("%s-control-plane", newClusterName)
+			clusterContext = fmt.Sprintf("kind-%s", newClusterName)
+		})
+
+		ginkgo.By(fmt.Sprintf("Creating cluster: %s", newClusterName), func() {
+			err := createCluster(newClusterName, kubeConfigPath, controlPlane, clusterContext)
+			gomega.Expect(err).ShouldNot(gomega.HaveOccurred())
+		})
+
+		ginkgo.By("Extract Karmada API server endpoint", func() {
+			var err error
+			karmadaAPIEndpointExpected, err = extractAPIServerEndpoint(kubeconfig, "karmada-apiserver")
+			gomega.Expect(err).ShouldNot(gomega.HaveOccurred())
+		})
+
+		ginkgo.By("Generate token and discovery token CA cert hash", func() {
+			cmd := framework.NewKarmadactlCommand(
+				kubeconfig, karmadaContext, karmadactlPath, "", karmadactlTimeout,
+				"token", "create", "--print-register-command="+"true",
+			)
+			output, err := cmd.ExecOrDie()
+			gomega.Expect(err).ShouldNot(gomega.HaveOccurred())
+
+			// Extract the endpoint for Karmada APIServer.
+			endpointRegex := regexp.MustCompile(`(\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}):(\d{1,5})`)
+			karmadaAPIEndpoint = endpointRegex.FindString(output)
+			gomega.Expect(karmadaAPIEndpoint).Should(gomega.Equal(karmadaAPIEndpointExpected))
+
+			// Extract token.
+			tokenRegex := regexp.MustCompile(`--token\s+(\S+)`)
+			tokenMatches := tokenRegex.FindStringSubmatch(output)
+			gomega.Expect(len(tokenMatches)).Should(gomega.BeNumerically(">", 1))
+			token = tokenMatches[1]
+
+			// Extract discovery token CA cert hash.
+			hashRegex := regexp.MustCompile(`--discovery-token-ca-cert-hash\s+(\S+)`)
+			hashMatches := hashRegex.FindStringSubmatch(output)
+			gomega.Expect(len(hashMatches)).Should(gomega.BeNumerically(">", 1))
+			discoveryTokenCACertHash = hashMatches[1]
+		})
+	})
+
+	ginkgo.AfterEach(func() {
+		ginkgo.By(fmt.Sprintf("Unregistering cluster: %s", newClusterName), func() {
+			cmd := framework.NewKarmadactlCommand(
+				kubeconfig, karmadaContext, karmadactlPath, "", 5*options.DefaultKarmadactlCommandDuration,
+				"unregister", "--cluster-kubeconfig", kubeConfigPath, newClusterName,
+			)
+			_, err := cmd.ExecOrDie()
+			gomega.Expect(err).ShouldNot(gomega.HaveOccurred())
+		})
+	})
+
+	ginkgo.AfterAll(func() {
+		ginkgo.By(fmt.Sprintf("Deleting clusters: %s", newClusterName), func() {
+			err := deleteCluster(newClusterName, kubeConfigPath)
+			gomega.Expect(err).ShouldNot(gomega.HaveOccurred())
+			_ = os.Remove(kubeConfigPath)
+		})
+	})
+
+	ginkgo.It("should register a cluster without CA verification", func() {
+		ginkgo.By("Register the new cluster in Karmada without CA verification", func() {
+			cmd := framework.NewKarmadactlCommand(
+				"", karmadaContext, karmadactlPath, "", karmadactlTimeout*5, "register", karmadaAPIEndpoint, "--token", token,
+				"--discovery-token-unsafe-skip-ca-verification="+"true", "--kubeconfig="+kubeConfigPath,
+				"--cluster-name", newClusterName, "--karmada-agent-image", "docker.io/karmada/karmada-agent:latest",
+			)
+			output, err := cmd.ExecOrDie()
+			gomega.Expect(err).ShouldNot(gomega.HaveOccurred())
+			gomega.Expect(output).Should(gomega.ContainSubstring(fmt.Sprintf("cluster(%s) is joined successfully", newClusterName)))
+		})
+
+		ginkgo.By("Wait for the new cluster to be ready", func() {
+			framework.WaitClusterFitWith(controlPlaneClient, newClusterName, func(cluster *clusterv1alpha1.Cluster) bool {
+				return meta.IsStatusConditionPresentAndEqual(cluster.Status.Conditions, clusterv1alpha1.ClusterConditionReady, metav1.ConditionTrue)
+			})
+		})
+	})
+
+	ginkgo.It("should register a cluster with CA verification", func() {
+		ginkgo.By("Register the new cluster in Karmada with CA verification", func() {
+			cmd := framework.NewKarmadactlCommand(
+				"", karmadaContext, karmadactlPath, "", karmadactlTimeout*5, "register", karmadaAPIEndpoint,
+				"--token", token, "--discovery-token-ca-cert-hash", discoveryTokenCACertHash,
+				"--kubeconfig="+kubeConfigPath, "--cluster-name", newClusterName,
+				"--karmada-agent-image", "docker.io/karmada/karmada-agent:latest",
+			)
+			output, err := cmd.ExecOrDie()
+			gomega.Expect(err).ShouldNot(gomega.HaveOccurred())
+			gomega.Expect(output).Should(gomega.ContainSubstring(fmt.Sprintf("cluster(%s) is joined successfully", newClusterName)))
+		})
+
+		ginkgo.By("Wait for the new cluster to be ready", func() {
+			framework.WaitClusterFitWith(controlPlaneClient, newClusterName, func(cluster *clusterv1alpha1.Cluster) bool {
+				return meta.IsStatusConditionPresentAndEqual(cluster.Status.Conditions, clusterv1alpha1.ClusterConditionReady, metav1.ConditionTrue)
+			})
+		})
+	})
+})
+
+// extractAPIServerEndpoint extracts the given clusterName API endpoint from a kubeconfig file.
+//
+// Parameters:
+// - kubeConfigPath: The file path to the kubeconfig file.
+//
+// Returns:
+// - string: The extracted API server endpoint without the "https://" prefix.
+// - error: An error if:
+//   - the kubeconfig file cannot be read
+//   - the YAML format is invalid
+//   - the given clusterName is not found
+func extractAPIServerEndpoint(kubeConfigPath string, clusterName string) (string, error) {
+	config, err := clientcmd.LoadFromFile(kubeConfigPath)
+	if err != nil {
+		return "", err
+	}
+
+	for name, cluster := range config.Clusters {
+		if name == clusterName {
+			endpointWithoutPrefix := strings.TrimPrefix(cluster.Server, "https://")
+			return endpointWithoutPrefix, nil
+		}
+	}
+
+	return "", fmt.Errorf("%s endpoint not found in kubeconfig", clusterName)
+}
+
 // extractPropagationPolicyName extracts the propagation policy name from the input string.
 //
 // Parameters: