Repository: kubeclipper/kubeclipper
Pull request #496 (2022-12-26 00:28:40)

feat: kcctl add node-ip-detect parameter
Signed-off-by: qinyer <qy913726062@gmail.com>

<!-- Thanks for sending a pull request! Here are some tips for you:

1. If you want **faster** PR reviews, read how: https://github.com/kubeclipper/community/blob/master/developer-guide/development/the-pr-author-guide-to-getting-through-code-review.md
2. In case you want to know how your PR got reviewed, read: https://github.com/kubeclipper/community/blob/master/developer-guide/development/code-review-guide.md
3. Here are some coding convetions followed by Kubeclipper community: https://github.com/kubeclipper/community/blob/master/developer-guide/development/coding-conventions.md
-->

### What type of PR is this?
<!--
Add one of the following kinds:
/kind bug
/kind cleanup
/kind documentation
/kind feature
/kind design

Optionally add one or more of the following kinds if applicable:
/kind api-change
/kind deprecation
/kind failing-test
/kind flake
/kind regression
-->
/kind feature

### What this PR does / why we need it:

### Which issue(s) this PR fixes:
<!--
Usage: `Fixes #<issue number>`, or `Fixes (paste link of issue)`.
_If PR is about `failing-tests or flakes`, please post the related issues/tests in a comment and do not use `Fixes`_*
-->
Fixes #

### Special notes for reviewers:
```
```

### Does this PR introduced a user-facing change?
<!--
If no, just write "None" in the release-note block below.
If yes, a release note is required:
Enter your extended release note in the block below. If the PR requires additional action from users switching to the new release, include the string "action required".

For more information on release notes see: https://github.com/kubernetes/community/blob/master/contributors/guide/release-notes.md
-->
```release-note

```

### Additional documentation, usage docs, etc.:
<!--
This section can be blank if this pull request does not require a release note.
Please use the following format for linking documentation or pass the
section below:
- [KEP]: <link>
- [Usage]: <link>
- [Other doc]: <link>
-->
```docs

```

kubeclipper-bot: @qinyer: Adding the "do-not-merge/release-note-label-needed" label because no release-note block was detected, please follow our [release note process](https://git.k8s.io/community/contributors/guide/release-notes.md) to remove it.

<details>

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/test-infra](https://github.com/kubernetes/test-infra/issues/new?title=Prow%20issue:) repository.
</details>
kubeclipper-bot: [APPROVALNOTIFIER] This PR is **NOT APPROVED**

This pull-request has been approved by: *<a href="https://github.com/kubeclipper/kubeclipper/pull/496#" title="Author self-approved">qinyer</a>*
To complete the [pull request process](https://git.k8s.io/community/contributors/guide/owners.md#the-code-review-process), please assign **x893675** after the PR has been reviewed.
You can assign the PR to them by writing `/assign @x893675` in a comment when ready.

The full list of commands accepted by this bot can be found [here](https://go.k8s.io/bot-commands?repo=kubeclipper%2Fkubeclipper).

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

- **[OWNERS](https://github.com/kubeclipper/kubeclipper/blob/master/OWNERS)**

Approvers can indicate their approval by writing `/approve` in a comment
Approvers can cancel approval by writing `/approve cancel` in a comment
</details>
<!-- META={"approvers":["x893675"]} -->
codecov-commenter: # [Codecov](https://codecov.io/gh/kubeclipper/kubeclipper/pull/496?src=pr&el=h1&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=kubeclipper) Report
> Merging [#496](https://codecov.io/gh/kubeclipper/kubeclipper/pull/496?src=pr&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=kubeclipper) (6ed4cc4) into [master](https://codecov.io/gh/kubeclipper/kubeclipper/commit/0d165f7ea39e46b6d59dda0a9e49a013c5f94c63?el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=kubeclipper) (0d165f7) will **increase** coverage by `0.20%`.
> The diff coverage is `20.00%`.

[![Impacted file tree graph](https://codecov.io/gh/kubeclipper/kubeclipper/pull/496/graphs/tree.svg?width=650&height=150&src=pr&token=3PWFG2MHTE&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=kubeclipper)](https://codecov.io/gh/kubeclipper/kubeclipper/pull/496?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=kubeclipper)

```diff
@@            Coverage Diff             @@
##           master     #496      +/-   ##
==========================================
+ Coverage   12.99%   13.19%   +0.20%     
==========================================
  Files         121      121              
  Lines       19060    18804     -256     
==========================================
+ Hits         2477     2482       +5     
+ Misses      16303    16042     -261     
  Partials      280      280              
```


| [Impacted Files](https://codecov.io/gh/kubeclipper/kubeclipper/pull/496?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=kubeclipper) | Coverage Δ | |
|---|---|---|
| [pkg/apis/core/v1/cri\_utils.go](https://codecov.io/gh/kubeclipper/kubeclipper/pull/496/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=kubeclipper#diff-cGtnL2FwaXMvY29yZS92MS9jcmlfdXRpbHMuZ28=) | `0.00% <0.00%> (ø)` | |
| [pkg/apis/core/v1/handler.go](https://codecov.io/gh/kubeclipper/kubeclipper/pull/496/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=kubeclipper#diff-cGtnL2FwaXMvY29yZS92MS9oYW5kbGVyLmdv) | `0.34% <0.00%> (-0.01%)` | :arrow_down: |
| [pkg/cli/deploy/deploy.go](https://codecov.io/gh/kubeclipper/kubeclipper/pull/496/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=kubeclipper#diff-cGtnL2NsaS9kZXBsb3kvZGVwbG95Lmdv) | `5.99% <0.00%> (-0.05%)` | :arrow_down: |
| [pkg/component/utils/utils.go](https://codecov.io/gh/kubeclipper/kubeclipper/pull/496/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=kubeclipper#diff-cGtnL2NvbXBvbmVudC91dGlscy91dGlscy5nbw==) | `34.04% <0.00%> (-0.75%)` | :arrow_down: |
| [pkg/scheme/core/v1/k8s/kubeadm.go](https://codecov.io/gh/kubeclipper/kubeclipper/pull/496/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=kubeclipper#diff-cGtnL3NjaGVtZS9jb3JlL3YxL2s4cy9rdWJlYWRtLmdv) | `2.67% <0.00%> (ø)` | |
| [pkg/scheme/core/v1/k8s/kubeadm\_step.go](https://codecov.io/gh/kubeclipper/kubeclipper/pull/496/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=kubeclipper#diff-cGtnL3NjaGVtZS9jb3JlL3YxL2s4cy9rdWJlYWRtX3N0ZXAuZ28=) | `0.00% <0.00%> (ø)` | |
| [pkg/apis/core/v1/utils.go](https://codecov.io/gh/kubeclipper/kubeclipper/pull/496/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=kubeclipper#diff-cGtnL2FwaXMvY29yZS92MS91dGlscy5nbw==) | `52.75% <100.00%> (+0.41%)` | :arrow_up: |
| [pkg/clusteroperation/node.go](https://codecov.io/gh/kubeclipper/kubeclipper/pull/496/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=kubeclipper#diff-cGtnL2NsdXN0ZXJvcGVyYXRpb24vbm9kZS5nbw==) | `65.16% <100.00%> (+0.45%)` | :arrow_up: |
| [pkg/cli/registry/registry.go](https://codecov.io/gh/kubeclipper/kubeclipper/pull/496/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=kubeclipper#diff-cGtnL2NsaS9yZWdpc3RyeS9yZWdpc3RyeS5nbw==) | `0.00% <0.00%> (ø)` | |


Patch:
diff --git a/api/openapi-spec/swagger.json b/api/openapi-spec/swagger.json
index 2275afe..a748391 100644
--- a/api/openapi-spec/swagger.json
+++ b/api/openapi-spec/swagger.json
@@ -3174,6 +3174,38 @@
         }
       }
     },
+    "/api/core.kubeclipper.io/v1/operations/{name}/termination": {
+      "post": {
+        "produces": [
+          "application/json"
+        ],
+        "tags": [
+          "Core-Cluster"
+        ],
+        "summary": "clusters termination operation.",
+        "operationId": "TerminationOperation",
+        "parameters": [
+          {
+            "type": "boolean",
+            "description": "dry run clusters termination operation.",
+            "name": "dryRun",
+            "in": "query"
+          },
+          {
+            "type": "string",
+            "description": "operation name",
+            "name": "name",
+            "in": "path",
+            "required": true
+          }
+        ],
+        "responses": {
+          "200": {
+            "description": "OK"
+          }
+        }
+      }
+    },
     "/api/core.kubeclipper.io/v1/projects/{project}/backuppoints": {
       "get": {
         "produces": [
@@ -4569,6 +4601,45 @@
         }
       }
     },
+    "/api/core.kubeclipper.io/v1/projects/{project}/operations/{name}/termination": {
+      "post": {
+        "produces": [
+          "application/json"
+        ],
+        "tags": [
+          "Core-Cluster"
+        ],
+        "summary": "clusters termination operation.",
+        "operationId": "TerminationOperation",
+        "parameters": [
+          {
+            "type": "string",
+            "description": "project name",
+            "name": "project",
+            "in": "path",
+            "required": true
+          },
+          {
+            "type": "boolean",
+            "description": "dry run clusters termination operation.",
+            "name": "dryRun",
+            "in": "query"
+          },
+          {
+            "type": "string",
+            "description": "operation name",
+            "name": "name",
+            "in": "path",
+            "required": true
+          }
+        ],
+        "responses": {
+          "200": {
+            "description": "OK"
+          }
+        }
+      }
+    },
     "/api/core.kubeclipper.io/v1/projects/{project}/regions": {
       "get": {
         "produces": [
diff --git a/cmd/kcctl/app/options/options.go b/cmd/kcctl/app/options/options.go
index 19b84c3..ea18f4a 100644
--- a/cmd/kcctl/app/options/options.go
+++ b/cmd/kcctl/app/options/options.go
@@ -90,6 +90,40 @@ const (
 	KCServerAltName  = "server.kubeclipper.io"
 )
 
+const IPDetectDescription = `
+To eliminate node specific IP address configuration,the KubeClipper can be configuredto autodetect these IP addresses.
+In many systems, there might be multiple physical interfaces on a host, or possibly multiple IP addresses configured 
+on a physical interface.In these cases, there are multiple addresses to choose from and soautodetection of the correct 
+address can be tricky.
+
+The IP autodetection methods are provided to improve the selection of thecorrect address, by limiting the selection 
+based on suitable criteria for your deployment.
+
+The following sections describe the available IP autodetection methods.
+
+1. first-found
+The first-found option enumerates all interface IP addresses and returns the first valid IP address (based on IP version
+and type of address) on the first valid interface. 
+Certain known “local” interfaces are omitted, such as the docker bridge.The order that both the interfaces and the IP 
+addresses are listed is system dependent.
+
+This is the default detection method. 
+However, since this method only makes a very simplified guess,it is recommended to either configure the node with a 
+specific IP address,or to use one of the other detection methods.
+
+2. interface=INTERFACE-REGEX
+The interface method uses the supplied interface regular expression to enumerate matching interfaces and to return the 
+first IP address on the first matching interface. 
+The order that both the interfaces and the IP addresses are listed is system dependent.
+
+Example with valid IP address on interface eth0, eth1, eth2 etc.:
+interface=eth.*
+
+3. cidr=CIDR
+The cidr method will select any IP address from the node that falls within the given CIDRs.
+Example:
+cidr=10.0.1.0/24,10.0.2.0/24`
+
 var AssumeYes bool
 
 var (
@@ -240,6 +274,7 @@ type DeployConfig struct {
 	Agents             Agents                         `json:"agents" yaml:"agents,omitempty"`
 	Proxys             []string                       `json:"proxys" yaml:"proxys,omitempty"`
 	IPDetect           string                         `json:"ipDetect" yaml:"ipDetect,omitempty"`
+	NodeIPDetect       string                         `json:"nodeIPDetect" yaml:"nodeIPDetect,omitempty"`
 	Debug              bool                           `json:"debug" yaml:"debug,omitempty"`
 	DefaultRegion      string                         `json:"defaultRegion" yaml:"defaultRegion,omitempty"`
 	ServerPort         int                            `json:"serverPort" yaml:"serverPort,omitempty"`
@@ -420,7 +455,8 @@ func getRepoMirror() string {
 
 func (c *DeployConfig) AddFlags(flags *pflag.FlagSet) {
 	flags.StringVarP(&c.Config, "deploy-config", "c", c.Config, "Path to the config file to use for Deploy.")
-	flags.StringVar(&c.IPDetect, "ip-detect", c.IPDetect, "Kc ip detect method.")
+	flags.StringVar(&c.IPDetect, "ip-detect", c.IPDetect, fmt.Sprintf("Kc agent node ip detect method. Used to route between nodes. \n%s", IPDetectDescription))
+	flags.StringVar(&c.NodeIPDetect, "node-ip-detect", c.NodeIPDetect, fmt.Sprintf("Kc agent node ip detect method. Used for routing between nodes in the kubernetes cluster. If not specified, ip-detect is inherited. \n%s", IPDetectDescription))
 	flags.BoolVar(&c.Debug, "debug", c.Debug, "Deploy kc use debug mode")
 	flags.StringVarP(&c.DefaultRegion, "region", "r", c.DefaultRegion, "Kc agent default region")
 	flags.BoolVar(&c.TLS, "tls", c.TLS, "Kc api server  use tls mode")
@@ -549,6 +585,7 @@ func (c *DeployConfig) GetKcAgentConfigTemplateContent(metadata Metadata) (strin
 	data["Region"] = metadata.Region
 	data["FloatIP"] = metadata.FloatIP
 	data["IPDetect"] = c.IPDetect
+	data["NodeIPDetect"] = c.NodeIPDetect
 	data["StaticServerAddress"] = fmt.Sprintf("http://%s:%d", c.ServerIPs[0], c.StaticServerPort)
 	if c.Debug {
 		data["LogLevel"] = "debug"
diff --git a/pkg/agent/agent.go b/pkg/agent/agent.go
index 1377fcd..93b8b50 100644
--- a/pkg/agent/agent.go
+++ b/pkg/agent/agent.go
@@ -44,7 +44,7 @@ func (s *Server) PrepareRun(stopCh <-chan struct{}) error {
 	if err != nil {
 		return errors.WithMessage(err, "config hosts")
 	}
-	s.taskService = task.NewService(s.Config.AgentID, s.Config.Metadata.Region, s.Config.IPDetect, s.Config.RegisterNode, s.Config.MQOptions,
+	s.taskService = task.NewService(s.Config.AgentID, s.Config.Metadata.Region, s.Config.IPDetect, s.Config.NodeIPDetect, s.Config.RegisterNode, s.Config.MQOptions,
 		task.WithNodeStatusUpdateFrequency(s.Config.NodeStatusUpdateFrequency),
 		task.WithLeaseDurationSeconds(240),
 		task.WithOplog(opLog),
diff --git a/pkg/agent/config/config.go b/pkg/agent/config/config.go
index 650e9a2..34d754a 100644
--- a/pkg/agent/config/config.go
+++ b/pkg/agent/config/config.go
@@ -53,6 +53,7 @@ type Config struct {
 	AgentID                   string              `json:"agentID,omitempty" yaml:"agentID"`
 	Metadata                  options.Metadata    `json:"metadata,omitempty" yaml:"metadata"`
 	IPDetect                  string              `json:"ipDetect,omitempty" yaml:"ipDetect"`
+	NodeIPDetect              string              `json:"nodeIPDetect,omitempty" yaml:"nodeIPDetect"`
 	RegisterNode              bool                `json:"registerNode,omitempty" yaml:"registerNode"`
 	NodeStatusUpdateFrequency time.Duration       `json:"nodeStatusUpdateFrequency,omitempty" yaml:"nodeStatusUpdateFrequency"`
 	DownloaderOptions         *downloader.Options `json:"downloader" yaml:"downloader" mapstructure:"downloader"`
diff --git a/pkg/apis/core/v1/cri_utils.go b/pkg/apis/core/v1/cri_utils.go
index 4e06cc0..4686395 100644
--- a/pkg/apis/core/v1/cri_utils.go
+++ b/pkg/apis/core/v1/cri_utils.go
@@ -158,6 +158,7 @@ func criRegistryUpdateStep(cluster *v1.Cluster, registries []v1.RegistrySpec, no
 		allNodes = append(allNodes, v1.StepNode{
 			ID:       node.Name,
 			IPv4:     node.Status.Ipv4DefaultIP,
+			NodeIPv4: node.Status.NodeIpv4DefaultIP,
 			Hostname: node.Labels[common.LabelHostname],
 		})
 	}
diff --git a/pkg/apis/core/v1/handler.go b/pkg/apis/core/v1/handler.go
index 43d5a58..0c042f8 100644
--- a/pkg/apis/core/v1/handler.go
+++ b/pkg/apis/core/v1/handler.go
@@ -31,6 +31,12 @@ import (
 	"strings"
 	"time"
 
+	"github.com/kubeclipper/kubeclipper/pkg/utils/httputil"
+
+	"k8s.io/client-go/util/retry"
+
+	"github.com/kubeclipper/kubeclipper/pkg/controller/cronbackupcontroller"
+
 	"github.com/emicklei/go-restful"
 	"github.com/google/uuid"
 	"github.com/gorilla/websocket"
@@ -57,7 +63,6 @@ import (
 	"github.com/kubeclipper/kubeclipper/pkg/controller"
 	"github.com/kubeclipper/kubeclipper/pkg/controller-runtime/client"
 	"github.com/kubeclipper/kubeclipper/pkg/controller/cloudprovidercontroller"
-	"github.com/kubeclipper/kubeclipper/pkg/controller/cronbackupcontroller"
 	"github.com/kubeclipper/kubeclipper/pkg/logger"
 	"github.com/kubeclipper/kubeclipper/pkg/models/cluster"
 	"github.com/kubeclipper/kubeclipper/pkg/models/core"
@@ -98,6 +103,7 @@ type handler struct {
 	delivery         service.IDelivery
 	tenantOperator   tenant.Operator
 	tokenOperator    auth.TokenManagementInterface
+	terminationChan  *chan struct{}
 }
 
 const (
@@ -114,7 +120,7 @@ var (
 
 func newHandler(conf *generic.ServerRunOptions, clusterOperator cluster.Operator, op operation.Operator, leaseOperator lease.Operator,
 	platform platform.Operator, coreOperator core.Operator, delivery service.IDelivery,
-	tokenOperator auth.TokenManagementInterface, tenantOperator tenant.Operator) *handler {
+	tokenOperator auth.TokenManagementInterface, tenantOperator tenant.Operator, terminationChan *chan struct{}) *handler {
 	return &handler{
 		genericConfig:    conf,
 		clusterOperator:  clusterOperator,
@@ -125,6 +131,7 @@ func newHandler(conf *generic.ServerRunOptions, clusterOperator cluster.Operator
 		coreOperator:     coreOperator,
 		tenantOperator:   tenantOperator,
 		tokenOperator:    tokenOperator,
+		terminationChan:  terminationChan,
 	}
 }
 
@@ -290,7 +297,7 @@ func (h *handler) AddOrRemoveNodes(request *restful.Request, response *restful.R
 				return
 			}
 		}
-		pendingOperation, err := buildPendingOperation(operationType, timeoutSecs, c.ResourceVersion, pn)
+		pendingOperation, err := buildPendingOperation(operationType, buildOperationSponsor(h.genericConfig), timeoutSecs, c.ResourceVersion, pn)
 		if err != nil {
 			restplus.HandleInternalError(response, request, err)
 			return
@@ -406,6 +413,7 @@ func (h *handler) DeleteCluster(request *restful.Request, response *restful.Resp
 	op.Status.Status = v1.OperationStatusRunning
 	op.Labels[common.LabelTimeoutSeconds] = timeoutSecs
 	op.Labels[common.LabelOperationAction] = v1.OperationDeleteCluster
+	op.Labels[common.LabelOperationSponsor] = buildOperationSponsor(h.genericConfig)
 	if !dryRun {
 		c.Status.Phase = v1.ClusterTerminating
 		_, err = h.clusterOperator.UpdateCluster(request.Request.Context(), c)
@@ -529,6 +537,7 @@ func (h *handler) CreateClusters(request *restful.Request, response *restful.Res
 
 	op.Labels[common.LabelTimeoutSeconds] = timeoutSecs
 	op.Labels[common.LabelOperationAction] = v1.OperationCreateCluster
+	op.Labels[common.LabelOperationSponsor] = buildOperationSponsor(h.genericConfig)
 	op.Status.Status = v1.OperationStatusRunning
 	if !dryRun {
 		op, err = h.opOperator.CreateOperation(context.TODO(), op)
@@ -698,9 +707,10 @@ func (h *handler) UpdateClusterCertification(request *restful.Request, response
 
 	op.Name = uuid.New().String()
 	op.Labels = map[string]string{
-		common.LabelClusterName:     c.Name,
-		common.LabelTimeoutSeconds:  v1.DefaultOperationTimeoutSecs,
-		common.LabelOperationAction: v1.OperationUpdateCertification,
+		common.LabelClusterName:      c.Name,
+		common.LabelTimeoutSeconds:   v1.DefaultOperationTimeoutSecs,
+		common.LabelOperationAction:  v1.OperationUpdateCertification,
+		common.LabelOperationSponsor: buildOperationSponsor(h.genericConfig),
 	}
 	op.Status.Status = v1.OperationStatusRunning
 	c.Status.Phase = v1.ClusterUpdating
@@ -1052,6 +1062,57 @@ func (h *handler) ListOperations(request *restful.Request, response *restful.Res
 	}
 }
 
+func (h *handler) TerminationOperation(request *restful.Request, response *restful.Response) {
+	ctx := request.Request.Context()
+	dryRun := query.GetBoolValueWithDefault(request, query.ParamDryRun, false)
+	name := request.PathParameter(query.ParameterName)
+
+	op, err := h.opOperator.GetOperationEx(ctx, name, "0")
+	if err != nil {
+		restplus.HandleInternalError(response, request, err)
+		return
+	}
+	if op.Status.Status != v1.OperationStatusRunning {
+		restplus.HandleBadRequest(response, request, fmt.Errorf("the operation status does not support termination"))
+		return
+	}
+	if !dryRun {
+		// if the kc-server is not the initiator of the operation, the request is forwarded to the specified service
+		if sponsor := op.Labels[common.LabelOperationSponsor]; sponsor != "" && !strings.Contains(sponsor, h.genericConfig.BindAddress) {
+			headers := make(map[string]string)
+			reqURL := fmt.Sprintf("%s%s", parseOperationSponsor(sponsor), request.Request.RequestURI)
+			headers["Authorization"] = request.Request.Header.Get("Authorization")
+			_, code, err := httputil.CommonRequest(reqURL, "POST", headers, nil, nil)
+			if err != nil {
+				restplus.HandleInternalError(response, request, err)
+				return
+			}
+			if code != http.StatusOK {
+				restplus.HandleInternalError(response, request, errors.New("forwarding request error"))
+				return
+			}
+			_ = response.WriteHeaderAndEntity(http.StatusOK, nil)
+			return
+		}
+		// update the data before broadcasting the message
+		// write termination label
+		op.Labels[common.LabelOperationIntent] = common.OperationIntent
+
+		if err = retry.RetryOnConflict(retry.DefaultRetry, func() error {
+			_, err = h.opOperator.UpdateOperation(ctx, op)
+			return err
+		}); err != nil {
+			restplus.HandleInternalError(response, request, err)
+			return
+		}
+		// close channel for broadcast termination message
+		close(*h.terminationChan)
+		// reset channel
+		*h.terminationChan = make(chan struct{})
+	}
+	_ = response.WriteHeaderAndEntity(http.StatusOK, nil)
+}
+
 func (h *handler) watchOperations(req *restful.Request, resp *restful.Response, q *query.Query) {
 	timeout := query.MinTimeoutSeconds * time.Second
 	if q.TimeoutSeconds != nil {
@@ -1125,6 +1186,7 @@ func (h *handler) getNodeInfo(ctx context.Context, nodes v1.WorkerNodeList, skip
 		item := component.Node{
 			ID:       n.Name,
 			IPv4:     n.Status.Ipv4DefaultIP,
+			NodeIPv4: n.Status.NodeIpv4DefaultIP,
 			Region:   n.Labels[common.LabelTopologyRegion],
 			Hostname: n.Labels[common.LabelHostname],
 			Role:     n.Labels[common.LabelNodeRole],
@@ -1545,6 +1607,7 @@ func (h *handler) CreateBackup(request *restful.Request, response *restful.Respo
 	op.Name = uuid.New().String()
 	op.Labels = make(map[string]string)
 	op.Labels[common.LabelOperationAction] = v1.OperationBackupCluster
+	op.Labels[common.LabelOperationSponsor] = buildOperationSponsor(h.genericConfig)
 	op.Labels[common.LabelTimeoutSeconds] = strconv.Itoa(v1.DefaultBackupTimeoutSec)
 	op.Labels[common.LabelClusterName] = c.Name
 	op.Labels[common.LabelBackupName] = backup.Name
@@ -1635,6 +1698,7 @@ func (h *handler) DeleteBackup(request *restful.Request, response *restful.Respo
 	op.Labels = make(map[string]string)
 	op.Labels[common.LabelOperationAction] = v1.OperationDeleteBackup
 	op.Labels[common.LabelTimeoutSeconds] = strconv.Itoa(v1.DefaultBackupTimeoutSec)
+	op.Labels[common.LabelOperationSponsor] = buildOperationSponsor(h.genericConfig)
 	op.Labels[common.LabelClusterName] = c.Name
 	op.Labels[common.LabelBackupName] = b.Name
 	op.Labels[common.LabelTopologyRegion] = c.Masters[0].Labels[common.LabelTopologyRegion]
@@ -1760,7 +1824,8 @@ func (h *handler) RetryCluster(request *restful.Request, response *restful.Respo
 	}
 
 	op = opList.Items[0].(*v1.Operation)
-	if op.Status.Status == v1.OperationStatusSuccessful || op.Status.Status == v1.OperationStatusRunning || op.Name != name {
+	if op.Status.Status == v1.OperationStatusSuccessful || op.Status.Status == v1.OperationStatusRunning ||
+		op.Status.Status == v1.OperationStatusTermination || op.Name != name {
 		restplus.HandleBadRequest(response, request, fmt.Errorf("only the latest faild operation can do a retry"))
 		return
 	}
@@ -1932,6 +1997,7 @@ func (h *handler) CreateRecovery(request *restful.Request, response *restful.Res
 	o.Labels = make(map[string]string)
 	o.Labels[common.LabelOperationAction] = v1.OperationRecoverCluster
 	o.Labels[common.LabelTimeoutSeconds] = strconv.Itoa(v1.DefaultRecoveryTimeoutSec)
+	o.Labels[common.LabelOperationSponsor] = buildOperationSponsor(h.genericConfig)
 	o.Labels[common.LabelClusterName] = c.Name
 	o.Labels[common.LabelRecoveryName] = rName
 	o.Labels[common.LabelTopologyRegion] = c.Masters[0].Labels[common.LabelTopologyRegion]
@@ -2041,6 +2107,7 @@ func (h *handler) InstallOrUninstallPlugins(request *restful.Request, response *
 
 	op.Labels[common.LabelTimeoutSeconds] = timeoutSecs
 	op.Labels[common.LabelOperationAction] = operationAction
+	op.Labels[common.LabelOperationSponsor] = buildOperationSponsor(h.genericConfig)
 	op.Status.Status = v1.OperationStatusRunning
 
 	if !dryRun {
@@ -2179,6 +2246,7 @@ func (h *handler) UpgradeCluster(request *restful.Request, response *restful.Res
 
 	op.Labels[common.LabelTimeoutSeconds] = timeoutSecs
 	op.Labels[common.LabelOperationAction] = v1.OperationUpgradeCluster
+	op.Labels[common.LabelOperationSponsor] = buildOperationSponsor(h.genericConfig)
 	op.Labels[common.LabelUpgradeVersion] = body.Version
 	op.Status.Status = v1.OperationStatusRunning
 	if !dryRun {
@@ -3331,12 +3399,26 @@ func (h *handler) CreateBackupPoint(request *restful.Request, response *restful.
 func (h *handler) DeleteBackupPoint(request *restful.Request, response *restful.Response) {
 	name := request.PathParameter(query.ParameterName)
 	q := query.ParseQueryParameter(request)
+	clusters, err := h.clusterOperator.ListClusters(request.Request.Context(), &query.Query{
+		Pagination:      query.NoPagination(),
+		ResourceVersion: "0",
+		LabelSelector:   common.LabelBackupPoint,
+		FieldSelector:   "",
+	})
+	if err != nil {
+		restplus.HandleInternalError(response, request, err)
+		return
+	}
+	if clus := h.checkBackupPointInUseByCluster(clusters, name); len(clus) > 0 {
+		restplus.HandleInternalError(response, request, fmt.Errorf("backup point is in use by clusters %v, please update cluster first", clus))
+		return
+	}
 	backups, err := h.clusterOperator.ListBackups(request.Request.Context(), q)
 	if err != nil {
 		restplus.HandleInternalError(response, request, err)
 		return
 	}
-	if ok := h.checkBackupPointInUse(backups, name); ok {
+	if ok := h.checkBackupPointInUseByBackup(backups, name); ok {
 		restplus.HandleInternalError(response, request, errors.New("backup point is in use, please delete backup first"))
 		return
 	}
diff --git a/pkg/apis/core/v1/registry.go b/pkg/apis/core/v1/registry.go
index b4c957a..00dc318 100644
--- a/pkg/apis/core/v1/registry.go
+++ b/pkg/apis/core/v1/registry.go
@@ -725,6 +725,28 @@ func SetupWebService(h *handler) *restful.WebService {
 		Returns(http.StatusOK, http.StatusText(http.StatusOK), corev1.Operation{}).
 		Returns(http.StatusNotFound, http.StatusText(http.StatusNotFound), nil))
 
+	webservice.Route(webservice.POST("/operations/{name}/termination").
+		To(h.TerminationOperation).
+		Metadata(restfulspec.KeyOpenAPITags, []string{CoreClusterTag}).
+		Doc("clusters termination operation.").
+		Param(webservice.QueryParameter(query.ParamDryRun, "dry run clusters termination operation.").
+			Required(false).DataType("boolean")).
+		Param(webservice.PathParameter(query.ParameterName, "operation name").
+			Required(true).
+			DataType("string")).
+		Returns(http.StatusOK, http.StatusText(http.StatusOK), nil))
+	webservice.Route(webservice.POST("/projects/{project}/operations/{name}/termination").
+		To(h.TerminationOperation).
+		Metadata(restfulspec.KeyOpenAPITags, []string{CoreClusterTag}).
+		Doc("clusters termination operation.").
+		Param(webservice.PathParameter("project", "project name")).
+		Param(webservice.QueryParameter(query.ParamDryRun, "dry run clusters termination operation.").
+			Required(false).DataType("boolean")).
+		Param(webservice.PathParameter(query.ParameterName, "operation name").
+			Required(true).
+			DataType("string")).
+		Returns(http.StatusOK, http.StatusText(http.StatusOK), nil))
+
 	webservice.Route(webservice.POST("/operations/{name}/retry").
 		To(h.RetryCluster).
 		Metadata(restfulspec.KeyOpenAPITags, []string{CoreClusterTag}).
@@ -1617,8 +1639,8 @@ func AddToContainer(c *restful.Container, clusterOperator cluster.Operator,
 	op operation.Operator, platform platform.Operator, leaseOperator lease.Operator,
 	coreOperator core.Operator, delivery service.IDelivery,
 	tokenOperator auth.TokenManagementInterface, tenantOperator tenant.Operator,
-	conf *generic.ServerRunOptions) error {
-	h := newHandler(conf, clusterOperator, op, leaseOperator, platform, coreOperator, delivery, tokenOperator, tenantOperator)
+	conf *generic.ServerRunOptions, terminationChan *chan struct{}) error {
+	h := newHandler(conf, clusterOperator, op, leaseOperator, platform, coreOperator, delivery, tokenOperator, tenantOperator, terminationChan)
 	webservice := SetupWebService(h)
 	c.Add(webservice)
 	return nil
diff --git a/pkg/apis/core/v1/utils.go b/pkg/apis/core/v1/utils.go
index 29ccb06..83d33bd 100644
--- a/pkg/apis/core/v1/utils.go
+++ b/pkg/apis/core/v1/utils.go
@@ -23,6 +23,9 @@ import (
 	"encoding/json"
 	"fmt"
 	"path/filepath"
+	"strings"
+
+	"github.com/kubeclipper/kubeclipper/pkg/simple/generic"
 
 	"github.com/kubeclipper/kubeclipper/pkg/utils/strutil"
 
@@ -178,6 +181,7 @@ func (h *handler) parseRecoverySteps(c *v1.Cluster, b *v1.Backup, restoreDir str
 			workers = append(workers, component.Node{
 				ID:       node.Name,
 				IPv4:     node.Status.Ipv4DefaultIP,
+				NodeIPv4: node.Status.NodeIpv4DefaultIP,
 				Hostname: node.Status.NodeInfo.Hostname,
 			})
 			continue
@@ -187,6 +191,7 @@ func (h *handler) parseRecoverySteps(c *v1.Cluster, b *v1.Backup, restoreDir str
 		masters = append(masters, component.Node{
 			ID:       node.Name,
 			IPv4:     node.Status.Ipv4DefaultIP,
+			NodeIPv4: node.Status.NodeIpv4DefaultIP,
 			Hostname: node.Status.NodeInfo.Hostname,
 		})
 	}
@@ -307,6 +312,7 @@ func getActBackupStep(c *v1.Cluster, b *v1.Backup, bp *v1.BackupPoint, pNode *v1
 	meta.Masters = []component.Node{{
 		ID:       b.PreferredNode, // the preferred node is used by default
 		IPv4:     pNode.Status.Ipv4DefaultIP,
+		NodeIPv4: pNode.Status.NodeIpv4DefaultIP,
 		Hostname: pNode.Status.NodeInfo.Hostname,
 	}}
 	ctx := component.WithExtraMetadata(context.TODO(), meta)
@@ -344,7 +350,7 @@ func (h *handler) parseUpdateCertOperation(clu *v1.Cluster, extraMetadata *compo
 	return op, nil
 }
 
-func (h *handler) checkBackupPointInUse(backups *v1.BackupList, name string) bool {
+func (h *handler) checkBackupPointInUseByBackup(backups *v1.BackupList, name string) bool {
 	for _, item := range backups.Items {
 		if item.BackupPointName == name {
 			return true
@@ -353,6 +359,17 @@ func (h *handler) checkBackupPointInUse(backups *v1.BackupList, name string) boo
 	return false
 }
 
+func (h *handler) checkBackupPointInUseByCluster(clusters *v1.ClusterList, name string) []string {
+	var clus []string
+	for _, item := range clusters.Items {
+		if item.Labels[common.LabelBackupPoint] == name {
+			clus = append(clus, item.Name)
+			return clus
+		}
+	}
+	return clus
+}
+
 func MarkToOriginNode(ctx context.Context, operator cluster.Operator, kcNodeID string) (*v1.Node, error) {
 	node, err := operator.GetNode(ctx, kcNodeID)
 	if err != nil {
@@ -441,10 +458,11 @@ func CreateBasic(serverURL, clusterName, userName string, caCert []byte) *client
 
 // BuildPendingOperation build PendingOperation struct
 // @param operationType Operation type
+// @param operationSponsor Address of the service that initiates the operation
 // @param timeout The timeout time of the entire operation
 // @param clusterResourceVersion The resource version of the cluster at the time the request was submitted
 // @param extra Additional data necessary to create the operation
-func buildPendingOperation(operationType, timeout, clusterResourceVersion string, extra interface{}) (v1.PendingOperation, error) {
+func buildPendingOperation(operationType, operationSponsor, timeout, clusterResourceVersion string, extra interface{}) (v1.PendingOperation, error) {
 	extraData, err := json.Marshal(extra)
 	if err != nil {
 		return v1.PendingOperation{}, nil
@@ -453,8 +471,30 @@ func buildPendingOperation(operationType, timeout, clusterResourceVersion string
 	return v1.PendingOperation{
 		OperationID:            strutil.GetUUID(),
 		OperationType:          operationType,
+		OperationSponsor:       operationSponsor,
 		Timeout:                timeout,
 		ClusterResourceVersion: clusterResourceVersion,
 		ExtraData:              extraData,
 	}, nil
 }
+
+// buildOperationSponsor build operation sponsor label
+func buildOperationSponsor(cfg *generic.ServerRunOptions) string {
+	scheme := "http"
+	port := cfg.InsecurePort
+	if cfg.SecurePort != 0 {
+		scheme = "https"
+		port = cfg.SecurePort
+	}
+	// FIXME floatIP or domain?
+	return fmt.Sprintf("%s-%s-%d", scheme, cfg.BindAddress, port)
+}
+
+// parseOperationSponsor parse operation sponsor label
+func parseOperationSponsor(sponsor string) string {
+	items := strings.Split(sponsor, "-")
+	if len(items) == 3 {
+		return fmt.Sprintf("%s://%s:%s", items[0], items[1], items[2])
+	}
+	return ""
+}
diff --git a/pkg/cli/config/template.go b/pkg/cli/config/template.go
index f99c3e5..64e0473 100644
--- a/pkg/cli/config/template.go
+++ b/pkg/cli/config/template.go
@@ -295,6 +295,7 @@ mq:
 
 const KcAgentConfigTmpl = `agentID: {{.AgentID}}
 ipDetect: {{.IPDetect}}
+nodeIPDetect: {{.NodeIPDetect}}
 metadata:
   region: {{.Region}}
 {{- if .FloatIP}}
diff --git a/pkg/cli/deploy/deploy.go b/pkg/cli/deploy/deploy.go
index 329b421..c4c8b29 100644
--- a/pkg/cli/deploy/deploy.go
+++ b/pkg/cli/deploy/deploy.go
@@ -105,6 +105,12 @@ const (
   # Deploy env with many agent node which has orderly ip.
   # this will add 10 agent,1.1.1.1, 1.1.1.2, ... 1.1.1.10.
   kcctl deploy --server 192.168.234.3 --agent us-west-1:1.1.1.1-1.1.1.10 --pk-file ~/.ssh/id_rsa --pkg https://oss.kubeclipper.io/release/v1.3.1/kc-amd64.tar.gz
+  
+  # Deploy env with many agent nodes and specify ip detect method for these nodes
+  kcctl deploy --server 192.168.234.3 --agent 192.168.234.3,192.168.234.4 --ip-detect=interface=eth0 --pk-file ~/.ssh/id_rsa --pkg https://oss.kubeclipper.io/release/v1.3.1/kc-amd64.tar.gz
+
+  # Deploy env with many agent nodes and specify node ip detect method for these nodes, used for routing between nodes in the kubernetes cluster
+  kcctl deploy --server 192.168.234.3 --agent 192.168.234.3,192.168.234.4 --node-ip-detect=interface=eth1 --pk-file ~/.ssh/id_rsa --pkg https://oss.kubeclipper.io/release/v1.3.1/kc-amd64.tar.gz
 
   # Deploy from config.
   kcctl deploy --deploy-config deploy-config.yaml
@@ -218,6 +224,11 @@ func (d *DeployOptions) Complete() error {
 		}
 	}
 
+	if d.deployConfig.NodeIPDetect == "" {
+		logger.Infof("node-ip-detect inherits from ip-detect: %s", d.deployConfig.IPDetect)
+		d.deployConfig.NodeIPDetect = d.deployConfig.IPDetect
+	}
+
 	if d.aio {
 		logger.Infof("run in aio mode.")
 	}
@@ -236,6 +247,9 @@ func (d *DeployOptions) ValidateArgs() error {
 	if d.deployConfig.IPDetect != "" && !autodetection.CheckMethod(d.deployConfig.IPDetect) {
 		return fmt.Errorf("invalid ip detect method,suppot [first-found,interface=xxx,cidr=xxx] now")
 	}
+	if d.deployConfig.NodeIPDetect != "" && !autodetection.CheckMethod(d.deployConfig.NodeIPDetect) {
+		return fmt.Errorf("invalid node ip detect method,suppot [first-found,interface=xxx,cidr=xxx] now")
+	}
 	if d.deployConfig.Pkg == "" {
 		return fmt.Errorf("--pkg must be specified")
 	}
diff --git a/pkg/cli/join/join.go b/pkg/cli/join/join.go
index c96b1a8..01fc325 100644
--- a/pkg/cli/join/join.go
+++ b/pkg/cli/join/join.go
@@ -81,10 +81,11 @@ type JoinOptions struct {
 	cliOpts      *options.CliOptions
 	client       *kc.Client
 
-	agents     []string // user input agents,maybe with region,need to parse.
-	floatIPs   []string // format: ip:floatIP,e.g. 192.168.10.11:172.20.149.199
-	ipDetect   string
-	parseAgent options.Agents
+	agents       []string // user input agents,maybe with region,need to parse.
+	floatIPs     []string // format: ip:floatIP,e.g. 192.168.10.11:172.20.149.199
+	ipDetect     string
+	nodeIPDetect string
+	parseAgent   options.Agents
 }
 
 func NewJoinOptions(streams options.IOStreams) *JoinOptions {
@@ -115,7 +116,8 @@ func NewCmdJoin(streams options.IOStreams) *cobra.Command {
 		},
 	}
 	o.cliOpts.AddFlags(cmd.Flags())
-	cmd.Flags().StringVar(&o.ipDetect, "ip-detect", o.ipDetect, "Kc ip detect method.")
+	cmd.Flags().StringVar(&o.ipDetect, "ip-detect", o.ipDetect, fmt.Sprintf("Kc agent node ip detect method. Used to route between nodes. \n%s", options.IPDetectDescription))
+	cmd.Flags().StringVar(&o.nodeIPDetect, "node-ip-detect", o.nodeIPDetect, fmt.Sprintf("Kc agent node ip detect method. Used for routing between nodes in the kubernetes cluster. If not specified, ip-detect is inherited. \n%s", options.IPDetectDescription))
 	cmd.Flags().StringArrayVar(&o.agents, "agent", o.agents, "join agent node.")
 	cmd.Flags().StringArrayVar(&o.floatIPs, "float-ip", o.floatIPs, "Kc agent ip and float ip.")
 	utils.CheckErr(cmd.MarkFlagRequired("agent"))
@@ -157,6 +159,11 @@ func (c *JoinOptions) Complete() error {
 	if c.ipDetect != "" {
 		c.deployConfig.IPDetect = c.ipDetect
 	}
+	if c.nodeIPDetect == "" {
+		logger.Infof("node-ip-detect inherits from ip-detect: %s", c.ipDetect)
+	} else {
+		c.deployConfig.NodeIPDetect = c.nodeIPDetect
+	}
 	return nil
 }
 
@@ -167,6 +174,9 @@ func (c *JoinOptions) ValidateArgs() error {
 	if c.ipDetect != "" && !autodetection.CheckMethod(c.ipDetect) {
 		return fmt.Errorf("invalid ip detect method,suppot [first-found,interface=xxx,cidr=xxx] now")
 	}
+	if c.nodeIPDetect != "" && !autodetection.CheckMethod(c.nodeIPDetect) {
+		return fmt.Errorf("invalid node ip detect method,suppot [first-found,interface=xxx,cidr=xxx] now")
+	}
 	if len(c.agents) == 0 {
 		return fmt.Errorf("must specified at least one agent node")
 	}
@@ -312,6 +322,7 @@ func (c *JoinOptions) getKcAgentConfigTemplateContent(metadata options.Metadata)
 	data["Region"] = metadata.Region
 	data["FloatIP"] = metadata.FloatIP
 	data["IPDetect"] = c.deployConfig.IPDetect
+	data["NodeIPDetect"] = c.deployConfig.NodeIPDetect
 	data["AgentID"] = metadata.AgentID
 	data["StaticServerAddress"] = fmt.Sprintf("http://%s:%d", c.deployConfig.ServerIPs[0], c.deployConfig.StaticServerPort)
 	if c.deployConfig.Debug {
diff --git a/pkg/clusteroperation/node.go b/pkg/clusteroperation/node.go
index 739a502..55a55ea 100644
--- a/pkg/clusteroperation/node.go
+++ b/pkg/clusteroperation/node.go
@@ -58,6 +58,7 @@ func (n *NodeOperation) Builder() (*corev1.Operation, error) {
 	}
 
 	op.Labels[common.LabelTimeoutSeconds] = n.pendingOperation.Timeout
+	op.Labels[common.LabelOperationSponsor] = n.pendingOperation.OperationSponsor
 	op.Status.Status = corev1.OperationStatusPending
 
 	return op, nil
@@ -145,10 +146,12 @@ func (p *PatchNodes) makeWorkerOperation(extra component.ExtraMetadata, cluster
 	// nodes to be added or removed
 	var stepNodes []corev1.StepNode
 	workerIPs := extra.GetWorkerNodeIP()
+	workerClusterIPs := extra.GetWorkerNodeClusterIP()
 	for _, nodeID := range p.Nodes.GetNodeIDs() {
 		stepNode := corev1.StepNode{
 			ID:       nodeID,
 			IPv4:     workerIPs[nodeID],
+			NodeIPv4: workerClusterIPs[nodeID],
 			Hostname: extra.GetWorkerHostname(nodeID),
 		}
 		stepNodes = append(stepNodes, stepNode)
diff --git a/pkg/clusteroperation/operation.go b/pkg/clusteroperation/operation.go
index 2847dc3..acf4ae9 100644
--- a/pkg/clusteroperation/operation.go
+++ b/pkg/clusteroperation/operation.go
@@ -136,7 +136,7 @@ func Retry(op *v1.Operation) (context.Context, *v1.Operation, []v1.Step, error)
 		for _, status := range op.Status.Conditions[failedIndex].Status {
 			if status.Status == v1.StepStatusFailed {
 				// select the nodes whose execution fails
-				if node := findStepNode(op.Steps[0].Nodes, status.Node); node.ID != "" {
+				if node := findStepNode(op.Steps[failedIndex].Nodes, status.Node); node.ID != "" {
 					failedNodes = append(failedNodes, node)
 				}
 				continue
diff --git a/pkg/component/ctx.go b/pkg/component/ctx.go
index b84aee3..84be0a1 100644
--- a/pkg/component/ctx.go
+++ b/pkg/component/ctx.go
@@ -54,6 +54,7 @@ type ExtraMetadata struct {
 type Node struct {
 	ID       string
 	IPv4     string
+	NodeIPv4 string
 	Region   string
 	Hostname string
 	Role     string
@@ -134,6 +135,22 @@ func (e ExtraMetadata) GetWorkerNodeIP() map[string]string {
 	return nodes
 }
 
+func (e ExtraMetadata) GetMasterNodeClusterIP() map[string]string {
+	nodes := make(map[string]string)
+	for _, node := range e.Masters {
+		nodes[node.ID] = node.NodeIPv4
+	}
+	return nodes
+}
+
+func (e ExtraMetadata) GetWorkerNodeClusterIP() map[string]string {
+	nodes := make(map[string]string)
+	for _, node := range e.Workers {
+		nodes[node.ID] = node.NodeIPv4
+	}
+	return nodes
+}
+
 func (e ExtraMetadata) GetAvailableMasterNodes() []string {
 	var nodes []string
 	for _, node := range e.ControlPlaneStatus {
diff --git a/pkg/component/utils/utils.go b/pkg/component/utils/utils.go
index 3d46883..1443c78 100644
--- a/pkg/component/utils/utils.go
+++ b/pkg/component/utils/utils.go
@@ -82,6 +82,7 @@ func UnwrapNodeList(nl component.NodeList) (nodes []v1.StepNode) {
 		nodes = append(nodes, v1.StepNode{
 			ID:       v.ID,
 			IPv4:     v.IPv4,
+			NodeIPv4: v.NodeIPv4,
 			Hostname: v.Hostname,
 		})
 	}
diff --git a/pkg/controller-runtime/manager/manager.go b/pkg/controller-runtime/manager/manager.go
index 5478d9d..b4312f0 100644
--- a/pkg/controller-runtime/manager/manager.go
+++ b/pkg/controller-runtime/manager/manager.go
@@ -112,6 +112,8 @@ type ControllerManager struct {
 
 	// per cluster map storing cluster clientset
 	clusterClientMap *clusterClientMap
+
+	terminationChan *chan struct{}
 }
 
 type workerLoop struct {
@@ -153,7 +155,7 @@ func (s *ControllerManager) GetCmdDelivery() service.CmdDelivery {
 	return s.cmdDelivery
 }
 
-func NewControllerManager(rc *rest.Config, storageFactory registry.SharedStorageFactory, cmdDelivery service.CmdDelivery, setupFunc SetupFunc) (*ControllerManager, error) {
+func NewControllerManager(rc *rest.Config, storageFactory registry.SharedStorageFactory, cmdDelivery service.CmdDelivery, terminationChan *chan struct{}, setupFunc SetupFunc) (*ControllerManager, error) {
 	s := &ControllerManager{
 		leaderStopChan:          make(chan struct{}, 1),
 		defaultWorkerLoopPeriod: time.Second,
@@ -161,6 +163,7 @@ func NewControllerManager(rc *rest.Config, storageFactory registry.SharedStorage
 		cmdDelivery:             cmdDelivery,
 		storageFactory:          storageFactory,
 		setupFunc:               setupFunc,
+		terminationChan:         terminationChan,
 	}
 	s.lock = leaderelect.NewLock(resourceLockNS, resourceLockName, lease.NewLeaseOperator(storageFactory.Leases()), resourcelock.ResourceLockConfig{
 		Identity:      uuid.New().String(),
diff --git a/pkg/controller/cluster_status.go b/pkg/controller/cluster_status.go
index 237df3f..f12d2c5 100644
--- a/pkg/controller/cluster_status.go
+++ b/pkg/controller/cluster_status.go
@@ -289,7 +289,7 @@ func (s *ClusterStatusMon) updateClusterControlPlaneStatus(clu *v1.Cluster) erro
 		}
 		proxyAPIServer := node.Annotations[common.AnnotationMetadataProxyAPIServer]
 		floatIP := node.Annotations[common.AnnotationMetadataFloatIP]
-		apiServer := node.Status.Ipv4DefaultIP + ":6443"
+		apiServer := node.Status.NodeIpv4DefaultIP + ":6443"
 		if floatIP != "" {
 			apiServer = floatIP + ":6443"
 		}
diff --git a/pkg/controller/clustercontroller/controller.go b/pkg/controller/clustercontroller/controller.go
index eadf93c..88d9eae 100644
--- a/pkg/controller/clustercontroller/controller.go
+++ b/pkg/controller/clustercontroller/controller.go
@@ -363,7 +363,7 @@ func (r *ClusterReconciler) getKubeConfig(ctx context.Context, c *v1.Cluster) (s
 	// there is 3 ip maybe used in kubeconfig,sort by priority: proxyServer > floatIP > defaultIP
 	proxyAPIServer := node.Annotations[common.AnnotationMetadataProxyAPIServer]
 	floatIP := node.Annotations[common.AnnotationMetadataFloatIP]
-	apiServer := node.Status.Ipv4DefaultIP + ":6443"
+	apiServer := node.Status.NodeIpv4DefaultIP + ":6443"
 	if floatIP != "" {
 		apiServer = floatIP + ":6443"
 	}
@@ -532,6 +532,7 @@ func criRegistryUpdateStep(cluster *v1.Cluster, registries []v1.RegistrySpec, no
 		allNodes = append(allNodes, v1.StepNode{
 			ID:       node.Name,
 			IPv4:     node.Status.Ipv4DefaultIP,
+			NodeIPv4: node.Status.NodeIpv4DefaultIP,
 			Hostname: node.Labels[common.LabelHostname],
 		})
 	}
diff --git a/pkg/controller/clustercontroller/metadata.go b/pkg/controller/clustercontroller/metadata.go
index 95d9384..31f7f2b 100644
--- a/pkg/controller/clustercontroller/metadata.go
+++ b/pkg/controller/clustercontroller/metadata.go
@@ -47,6 +47,7 @@ func (r *ClusterReconciler) convertNodes(ctx context.Context, nodes v1.WorkerNod
 		item := component.Node{
 			ID:       n.Name,
 			IPv4:     n.Status.Ipv4DefaultIP,
+			NodeIPv4: n.Status.NodeIpv4DefaultIP,
 			Region:   n.Labels[common.LabelTopologyRegion],
 			Hostname: n.Labels[common.LabelHostname],
 			Role:     n.Labels[common.LabelNodeRole],
diff --git a/pkg/controller/operationcontroller/controller.go b/pkg/controller/operationcontroller/controller.go
index 0ffacb2..e234f4c 100644
--- a/pkg/controller/operationcontroller/controller.go
+++ b/pkg/controller/operationcontroller/controller.go
@@ -181,17 +181,17 @@ func (r *OperationReconciler) distributeOperation(ctx context.Context, log logge
 				if _, err = r.ClusterOperator.UpdateCluster(ctx, clu.DeepCopy()); err != nil {
 					return err
 				}
+				// update operation
+				newOp.Status.Status = v1.OperationStatusRunning
+				if _, err = r.OperationWriter.UpdateOperation(ctx, newOp); err != nil {
+					return err
+				}
 				return nil
 			}); err != nil {
-				log.Error("automatic retry, update cluster failed", zap.String("cluster", cluName), zap.String("operation", op.Name), zap.Error(err))
-				return err
-			}
-			// update operation
-			newOp.Status.Status = v1.OperationStatusRunning
-			if _, err = r.OperationWriter.UpdateOperation(ctx, newOp); err != nil {
-				log.Error("retry on conflict, update operation failed", zap.String("cluster", cluName), zap.String("operation", op.Name), zap.Error(err))
+				log.Error("automatic retry, update cluster and operation failed", zap.String("cluster", cluName), zap.String("operation", op.Name), zap.Error(err))
 				return err
 			}
+
 			// process delivery steps
 			newOp.Steps = continueSteps
 			go func() {
diff --git a/pkg/nodestatus/node_status.go b/pkg/nodestatus/node_status.go
index ba5e99e..ab8e472 100644
--- a/pkg/nodestatus/node_status.go
+++ b/pkg/nodestatus/node_status.go
@@ -49,8 +49,8 @@ import (
 // Setters may partially mutate the node before returning an error.
 type Setter func(node *v1.Node) error
 
-func NodeAddress(ipDetectMethod string) Setter {
-	log.Println("ip detect method:", ipDetectMethod)
+func NodeAddress(ipDetectMethod, nodeIPDetectMethod string) Setter {
+	log.Printf("ip detect method: %s, node ip detect methd: %s \n", ipDetectMethod, nodeIPDetectMethod)
 	return func(node *v1.Node) error {
 		var nodeAddress []v1.NodeAddress
 		addresses, err := net.InterfaceAddrs()
@@ -70,11 +70,17 @@ func NodeAddress(ipDetectMethod string) Setter {
 		if err != nil {
 			return err
 		}
+		nodeIP, err := netutil.GetDefaultIP(true, nodeIPDetectMethod)
+		if err != nil {
+			return err
+		}
 		gw, err := netutil.GetDefaultGateway(true)
 		if err != nil {
 			return err
 		}
-		node.Status.Ipv4DefaultIP, node.Status.Ipv4DefaultGw = ip.To4().String(), gw.To4().String()
+		node.Status.Ipv4DefaultIP = ip.To4().String()
+		node.Status.Ipv4DefaultGw = gw.To4().String()
+		node.Status.NodeIpv4DefaultIP = nodeIP.To4().String()
 		return nil
 	}
 }
diff --git a/pkg/scheme/common/well_known_labels.go b/pkg/scheme/common/well_known_labels.go
index d1f92a0..0362972 100644
--- a/pkg/scheme/common/well_known_labels.go
+++ b/pkg/scheme/common/well_known_labels.go
@@ -34,6 +34,8 @@ const (
 	LabelOperationAction   = "kubeclipper.io/operation"
 	LabelOperationName     = "kubeclipper.io/operation-name"
 	LabelOperationRetry    = "kubeclipper.io/operation-retry-times"
+	LabelOperationIntent   = "kubeclipper.io/operation-intent"
+	LabelOperationSponsor  = "kubeclipper.io/operation-sponsor"
 	LabelTimeoutSeconds    = "kubeclipper.io/timeout"
 	LabelRoleTemplate      = "kubeclipper.io/role-template"
 	LabelHidden            = "kubeclipper.io/hidden"
@@ -102,3 +104,5 @@ const (
 	AnnotationOriginNode        = "kubeclipper.io/originNode"        // mark node is from kc or provider,add when join node to provider cluster
 	AnnotationProviderClusterID = "kubeclipper.io/providerClusterID" // match kc cluster to provider cluster
 )
+
+const OperationIntent = "termination"
diff --git a/pkg/scheme/core/v1/k8s/kubeadm.go b/pkg/scheme/core/v1/k8s/kubeadm.go
index aaf2b0c..2492e1d 100644
--- a/pkg/scheme/core/v1/k8s/kubeadm.go
+++ b/pkg/scheme/core/v1/k8s/kubeadm.go
@@ -117,6 +117,7 @@ type KubeadmConfig struct {
 	CACertHashes         string        `json:"caCertHashes,omitempty"`
 	BootstrapToken       string        `json:"bootstrapToken,omitempty"`
 	CertificateKey       string        `json:"certificateKey,omitempty"`
+	AdvertiseAddress     string        `json:"advertiseAddress,omitempty"`
 }
 
 type ControlPlane struct {
@@ -284,6 +285,7 @@ func (stepper KubeadmConfig) Install(ctx context.Context, opts component.Options
 		stepper.BootstrapToken = masterJoinCmd[4]
 		stepper.CACertHashes = masterJoinCmd[6]
 		stepper.CertificateKey = masterJoinCmd[9]
+		// TODO: When adding a master node, can set the `--apiserver-advertise-address` and `--apiserver-bind-port` parameters.
 	} else {
 		workerJoinCmd := strings.Split(cmds[1], " ")
 		if len(workerJoinCmd) < 6 {
@@ -396,7 +398,7 @@ func (stepper *ControlPlane) Install(ctx context.Context, opts component.Options
 	if err != nil {
 		return nil, errors.WithMessage(err, "load agent config")
 	}
-	ipnet, err := netutil.GetDefaultIP(true, agentConfig.IPDetect)
+	ipnet, err := netutil.GetDefaultIP(true, agentConfig.NodeIPDetect)
 	if err != nil {
 		return nil, err
 	}
@@ -680,7 +682,7 @@ func (stepper *ClusterNode) Install(ctx context.Context, opts component.Options)
 		if err != nil {
 			return nil, errors.WithMessage(err, "load agent config")
 		}
-		ipnet, err := netutil.GetDefaultIP(true, agentConfig.IPDetect)
+		ipnet, err := netutil.GetDefaultIP(true, agentConfig.NodeIPDetect)
 		if err != nil {
 			return nil, err
 		}
diff --git a/pkg/scheme/core/v1/k8s/kubeadm_step.go b/pkg/scheme/core/v1/k8s/kubeadm_step.go
index f8f717a..40a856f 100644
--- a/pkg/scheme/core/v1/k8s/kubeadm_step.go
+++ b/pkg/scheme/core/v1/k8s/kubeadm_step.go
@@ -383,7 +383,7 @@ func (stepper *KubeadmConfig) InitStepper(c *v1.Cluster, metadata *component.Ext
 	apiServerDomain := APIServerDomainPrefix + strutil.StringDefaultIfEmpty("cluster.local", c.Networking.DNSDomain)
 	cpEndpoint := fmt.Sprintf("%s:6443", apiServerDomain)
 	if _, ok := c.Labels[common.LabelClusterProviderName]; ok {
-		cpEndpoint = fmt.Sprintf("%s:6443", metadata.Masters[0].IPv4)
+		cpEndpoint = fmt.Sprintf("%s:6443", metadata.Masters[0].NodeIPv4)
 	}
 
 	stepper.ClusterConfigAPIVersion = ""
@@ -398,6 +398,7 @@ func (stepper *KubeadmConfig) InitStepper(c *v1.Cluster, metadata *component.Ext
 	stepper.CertSANs = c.CertSANs
 	stepper.LocalRegistry = c.LocalRegistry
 	stepper.Offline = metadata.Offline
+	stepper.AdvertiseAddress = metadata.Masters[0].NodeIPv4
 
 	return stepper
 }
@@ -535,7 +536,7 @@ func (stepper *ClusterNode) InitStepper(c *v1.Cluster, metadata *component.Extra
 	stepper.Masters = metadata.GetMasterNodeIP()
 	stepper.LocalRegistry = c.LocalRegistry
 	stepper.APIServerDomainName = apiServerDomain
-	stepper.JoinMasterIP = metadata.Masters[0].IPv4
+	stepper.JoinMasterIP = metadata.Masters[0].NodeIPv4
 	stepper.EtcdDataPath = c.Etcd.DataDir
 
 	return stepper
@@ -557,7 +558,7 @@ func GetKubeConfig(ctx context.Context, name string, node component.Node, delive
 		return "", err
 	}
 
-	kubeConfig.Clusters[name].Server = fmt.Sprintf("https://%s:6443", node.IPv4)
+	kubeConfig.Clusters[name].Server = fmt.Sprintf("https://%s:6443", node.NodeIPv4)
 	config, err := clientcmd.Write(kubeConfig)
 	if err != nil {
 		return "", err
@@ -917,7 +918,10 @@ func PatchTaintAndLabelStep(master, workers v1.WorkerNodeList, metadata *compone
 				RetryTimes: 1,
 				Nodes: []v1.StepNode{
 					{
-						ID: metadata.Masters[0].ID, IPv4: metadata.Masters[0].IPv4, Hostname: metadata.GetMasterHostname(metadata.Masters[0].ID),
+						ID:       metadata.Masters[0].ID,
+						IPv4:     metadata.Masters[0].IPv4,
+						NodeIPv4: metadata.Masters[0].NodeIPv4,
+						Hostname: metadata.GetMasterHostname(metadata.Masters[0].ID),
 					},
 				},
 				Commands: shellCommand,
diff --git a/pkg/scheme/core/v1/k8s/template.go b/pkg/scheme/core/v1/k8s/template.go
index c59ee99..3d77b81 100644
--- a/pkg/scheme/core/v1/k8s/template.go
+++ b/pkg/scheme/core/v1/k8s/template.go
@@ -90,6 +90,9 @@ volumeStatsAggPeriod: 1m
 ---
 apiVersion: kubeadm.k8s.io/{{.ClusterConfigAPIVersion}}
 kind: InitConfiguration
+localAPIEndpoint:
+  advertiseAddress: {{.AdvertiseAddress}}
+  bindPort: 6443
 nodeRegistration:
 {{- if eq .ContainerRuntime  "containerd"}}
   criSocket: /run/containerd/containerd.sock
diff --git a/pkg/scheme/core/v1/node_types.go b/pkg/scheme/core/v1/node_types.go
index 005de53..5d9f782 100644
--- a/pkg/scheme/core/v1/node_types.go
+++ b/pkg/scheme/core/v1/node_types.go
@@ -164,8 +164,13 @@ type AttachedVolume struct {
 }
 
 type NodeStatus struct {
+	// Note: It is usually used for routing between kc nodes and belongs to the operation and maintenance management network.
 	Ipv4DefaultIP string `json:"ipv4DefaultIP" description:"node ipv4 default gateway interface ip"`
 	Ipv4DefaultGw string `json:"ipv4DefaultGw" description:"node ipv4 default gateway ip"`
+	// Note: It is commonly used for routing between nodes in a kubernetes cluster.
+	// If the `--node-ip-detect` parameter is not specified at deployment time, it is the same as `Ipv4DefaultIP`;
+	// otherwise, it depends on the ip detect method.
+	NodeIpv4DefaultIP string `json:"nodeIpv4DefaultIP"`
 	// Capacity represents the total resources of a node.
 	// More info: https://kubernetes.io/docs/concepts/storage/persistent-volumes#capacity
 	// +optional
diff --git a/pkg/scheme/core/v1/operation_types.go b/pkg/scheme/core/v1/operation_types.go
index 213641e..ddfe52f 100644
--- a/pkg/scheme/core/v1/operation_types.go
+++ b/pkg/scheme/core/v1/operation_types.go
@@ -64,11 +64,12 @@ const DefaultOperationTimeoutSecs = "5400"
 type OperationStatusType string
 
 const (
-	OperationStatusPending    OperationStatusType = "pending"
-	OperationStatusRunning    OperationStatusType = "running"
-	OperationStatusFailed     OperationStatusType = "failed"
-	OperationStatusUnknown    OperationStatusType = "unknown"
-	OperationStatusSuccessful OperationStatusType = "successful"
+	OperationStatusPending     OperationStatusType = "pending"
+	OperationStatusRunning     OperationStatusType = "running"
+	OperationStatusFailed      OperationStatusType = "failed"
+	OperationStatusTermination OperationStatusType = "termination"
+	OperationStatusUnknown     OperationStatusType = "unknown"
+	OperationStatusSuccessful  OperationStatusType = "successful"
 )
 
 type OperationStatus struct {
@@ -118,6 +119,7 @@ type Step struct {
 type StepNode struct {
 	ID       string `json:"id,omitempty"`
 	IPv4     string `json:"ipv4,omitempty"`
+	NodeIPv4 string `json:"nodeIPv4,omitempty"`
 	Hostname string `json:"hostname,omitempty"`
 }
 
@@ -172,6 +174,7 @@ type StepStatus struct {
 type PendingOperation struct {
 	OperationID            string `json:"operationID"`
 	OperationType          string `json:"operationType"`
+	OperationSponsor       string `json:"operationSponsor"`
 	Timeout                string `json:"timeout"`
 	ClusterResourceVersion string `json:"clusterResourceVersion"`
 	ExtraData              []byte `json:"extraData,omitempty"`
diff --git a/pkg/server/server.go b/pkg/server/server.go
index ed4bfd1..69ebea3 100644
--- a/pkg/server/server.go
+++ b/pkg/server/server.go
@@ -102,12 +102,14 @@ type APIServer struct {
 	databaseAuditBackend  auditing.Backend
 	internalInformerUser  string
 	InternalInformerToken string
+	terminationChan       chan struct{}
 }
 
 func (s *APIServer) PrepareRun(stopCh <-chan struct{}) error {
 	s.internalInformerUser = "system:kc-server"
 	s.InternalInformerToken = uuid.New().String()
 	s.storageFactory = registry.NewSharedStorageFactory(s.RESTOptionsGetter)
+	s.terminationChan = make(chan struct{})
 
 	var err error
 	switch s.Config.CacheOptions.CacheProvider {
@@ -262,7 +264,7 @@ func (s *APIServer) installAPIs(stopCh <-chan struct{}) error {
 		s.storageFactory.GlobalRoleBindings(), s.storageFactory.Tokens(), s.storageFactory.LoginRecords(), s.storageFactory.ProjectRole(), s.storageFactory.ProjectRoleBinding())
 	s.rbacAuthorizer = rbac.NewAuthorizer(iamOperator, clusterOperator)
 
-	deliverySvc := delivery.NewService(s.Config.MQOptions, clusterOperator, leaseOperator, opOperator)
+	deliverySvc := delivery.NewService(s.Config.MQOptions, clusterOperator, leaseOperator, opOperator, &s.terminationChan)
 	s.Services = append(s.Services, deliverySvc)
 
 	platformOperator := platform.NewPlatformOperator(s.storageFactory.PlatformSettings(), s.storageFactory.Events())
@@ -317,7 +319,7 @@ func (s *APIServer) installAPIs(stopCh <-chan struct{}) error {
 		}
 	}
 
-	ctrl, err := manager.NewControllerManager(rc, s.storageFactory, deliverySvc, s.SetupController)
+	ctrl, err := manager.NewControllerManager(rc, s.storageFactory, deliverySvc, &s.terminationChan, s.SetupController)
 	if err != nil {
 		return err
 	}
@@ -325,7 +327,7 @@ func (s *APIServer) installAPIs(stopCh <-chan struct{}) error {
 
 	if err = corev1.AddToContainer(s.container, clusterOperator, opOperator, platformOperator,
 		leaseOperator, coreOperator, deliverySvc, tokenOperator, tenantOperator,
-		s.Config.GenericServerRunOptions); err != nil {
+		s.Config.GenericServerRunOptions, &s.terminationChan); err != nil {
 		return err
 	}
 	if err = proxy.AddToContainer(s.container, clusterOperator); err != nil {
diff --git a/pkg/service/delivery/delivery.go b/pkg/service/delivery/delivery.go
index a79b52e..0914542 100644
--- a/pkg/service/delivery/delivery.go
+++ b/pkg/service/delivery/delivery.go
@@ -71,9 +71,10 @@ type Service struct {
 	leaseOperator     lease.Operator
 	opOperator        operation.Operator
 	stepStatusChan    chan stepStatus
+	terminationChan   *chan struct{}
 }
 
-func NewService(opts *natsio.NatsOptions, clusterOperator cluster.Operator, leaseOperator lease.Operator, opOperator operation.Operator) *Service {
+func NewService(opts *natsio.NatsOptions, clusterOperator cluster.Operator, leaseOperator lease.Operator, opOperator operation.Operator, terminationChan *chan struct{}) *Service {
 	s := &Service{
 		external:          opts.External,
 		client:            natsio.NewNats(opts),
@@ -84,6 +85,7 @@ func NewService(opts *natsio.NatsOptions, clusterOperator cluster.Operator, leas
 		leaseOperator:     leaseOperator,
 		opOperator:        opOperator,
 		stepStatusChan:    make(chan stepStatus, 256),
+		terminationChan:   terminationChan,
 	}
 	s.client.SetReconnectHandler(s.defaultMQReconnectHandler)
 	s.client.SetDisconnectErrHandler(s.defaultMQDisconnectHandler)
@@ -404,6 +406,7 @@ func (s *Service) DeliverTaskOperation(ctx context.Context, operation *v1.Operat
 	errChan := make(chan error, 1)
 	defer close(errChan)
 	operation.Status.Conditions = make([]v1.OperationCondition, len(operation.Steps))
+	var termination bool
 	go func() {
 		for {
 			select {
@@ -412,6 +415,25 @@ func (s *Service) DeliverTaskOperation(ctx context.Context, operation *v1.Operat
 				stepCtxCancel()
 				go s.updateOperationStatus(operation.Name, v1.OperationStatusFailed, opts.DryRun)
 				return
+			case <-*s.terminationChan:
+				if operation.Status.Status == v1.OperationStatusRunning {
+					// get latest edition operation
+					op, err := s.opOperator.GetOperationEx(context.TODO(), operation.Name, "0")
+					if err != nil {
+						logger.Error("get latest edition operation failed", zap.String("op", op.Name), zap.Error(err))
+						continue
+					}
+					logger.Debugf("get latest operation: %v, label intent: %v", operation.Name, op.Labels[common.LabelOperationIntent])
+					if op.Labels[common.LabelOperationIntent] == common.OperationIntent {
+						// termination operation
+						stepCtxCancel()
+						// termination step flag
+						termination = true
+
+						go s.updateOperationStatus(operation.Name, v1.OperationStatusTermination, opts.DryRun)
+						return
+					}
+				}
 			case <-doneChan:
 				// all step done, set operation status successful
 				go s.updateOperationStatus(operation.Name, v1.OperationStatusSuccessful, opts.DryRun)
@@ -425,6 +447,10 @@ func (s *Service) DeliverTaskOperation(ctx context.Context, operation *v1.Operat
 	}()
 	var err error
 	for i, step := range operation.Steps {
+		if termination {
+			logger.Debug("termination delivery task step", zap.String("operation", operation.Name), zap.String("step", step.Name))
+			break
+		}
 		// TODO: add retry steps
 		// TODO: refactor
 		// Notice: 目前只针对 CUSTOM 命令有用，下一步骤依赖上一步骤的输出，比如 K8S 安装时初始化一个 K8S 控制节点后得到 kubeadm join 命令，需要传给其他节点进行执行
diff --git a/pkg/service/task/service.go b/pkg/service/task/service.go
index 78b9836..abcca4b 100644
--- a/pkg/service/task/service.go
+++ b/pkg/service/task/service.go
@@ -57,6 +57,7 @@ type Service struct {
 	NodeReportSubject string
 	AgentID           string
 	IPDetect          string
+	NodeIPDetect      string
 	Region            string
 	AgentSubject      string
 	RegisterNode      bool
@@ -154,7 +155,7 @@ func defaultRepeatedHeartbeatFailure() {
 	logger.Debug("repeated heartbeat failure ...")
 }
 
-func NewService(agentID, region, ipDetectMethod string, registerNode bool, natOpts *natsio.NatsOptions, opts ...ServiceOption) *Service {
+func NewService(agentID, region, ipDetectMethod, nodeIPDetectMethod string, registerNode bool, natOpts *natsio.NatsOptions, opts ...ServiceOption) *Service {
 	nc := natsio.NewNats(natOpts)
 	nc.SetReconnectHandler(defaultMQReconnectHandler)
 	nc.SetDisconnectErrHandler(defaultMQDisconnectHandler)
@@ -165,6 +166,7 @@ func NewService(agentID, region, ipDetectMethod string, registerNode bool, natOp
 		NodeReportSubject:          natOpts.Client.NodeReportSubject,
 		AgentID:                    agentID,
 		IPDetect:                   ipDetectMethod,
+		NodeIPDetect:               nodeIPDetectMethod,
 		Region:                     region,
 		AgentSubject:               fmt.Sprintf(service.MsgSubjectFormat, agentID, natOpts.Client.SubjectSuffix),
 		RegisterNode:               registerNode,
@@ -206,7 +208,7 @@ func (s *Service) defaultNodeStatusFuncs() []func(*v1.Node) error {
 	var setters []func(n *v1.Node) error
 	setters = append(setters,
 		nodestatus.Metadata(),
-		nodestatus.NodeAddress(s.IPDetect),
+		nodestatus.NodeAddress(s.IPDetect, s.NodeIPDetect),
 		nodestatus.MachineInfo(),
 		nodestatus.ReadyCondition(s.clock.Now, TODO, TODO, TODO))
 
diff --git a/tools/doc-gen/main.go b/tools/doc-gen/main.go
index 858921d..ce8bd29 100644
--- a/tools/doc-gen/main.go
+++ b/tools/doc-gen/main.go
@@ -100,7 +100,7 @@ func validateSpec(apiSpec []byte) error {
 func generateSwaggerJSON() []byte {
 
 	container := restful.NewContainer()
-	urlruntime.Must(corev1.AddToContainer(container, nil, nil, nil, nil, nil, nil, nil, nil, nil))
+	urlruntime.Must(corev1.AddToContainer(container, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil))
 	urlruntime.Must(iamv1.AddToContainer(container, nil, nil, nil, nil))
 	urlruntime.Must(tenantv1.AddToContainer(container, nil, nil, nil, nil))
 	urlruntime.Must(configv1.AddToContainer(container, nil, nil))


Test patch:
diff --git a/pkg/apis/core/v1/utils_test.go b/pkg/apis/core/v1/utils_test.go
index 1de9777..17991b6 100644
--- a/pkg/apis/core/v1/utils_test.go
+++ b/pkg/apis/core/v1/utils_test.go
@@ -106,31 +106,36 @@ var (
 	extraMeta = &component.ExtraMetadata{
 		Masters: []component.Node{
 			{
-				ID:     "1e3ea00f-1403-46e5-a486-70e4cb29d541",
-				IPv4:   "192.168.1.1",
-				Region: "default",
+				ID:       "1e3ea00f-1403-46e5-a486-70e4cb29d541",
+				IPv4:     "192.168.1.1",
+				NodeIPv4: "192.168.2.1",
+				Region:   "default",
 			},
 			{
-				ID:     "43ed594a-a76f-4370-a14d-551e7b6153de",
-				IPv4:   "192.168.1.2",
-				Region: "default",
+				ID:       "43ed594a-a76f-4370-a14d-551e7b6153de",
+				IPv4:     "192.168.1.2",
+				NodeIPv4: "192.168.2.2",
+				Region:   "default",
 			},
 			{
-				ID:     "c7a91d86-cd53-4c3f-85b0-fbc657778067",
-				IPv4:   "192.168.1.3",
-				Region: "default",
+				ID:       "c7a91d86-cd53-4c3f-85b0-fbc657778067",
+				IPv4:     "192.168.1.3",
+				NodeIPv4: "192.168.2.3",
+				Region:   "default",
 			},
 		},
 		Workers: []component.Node{
 			{
-				ID:     "4cf1ad74-704c-4290-a523-e524e930245d",
-				IPv4:   "192.168.1.4",
-				Region: "default",
+				ID:       "4cf1ad74-704c-4290-a523-e524e930245d",
+				IPv4:     "192.168.1.4",
+				NodeIPv4: "192.168.2.4",
+				Region:   "default",
 			},
 			{
-				ID:     "ae4ba282-27f9-4a93-8fe9-63f786781d48",
-				IPv4:   "192.168.1.5",
-				Region: "default",
+				ID:       "ae4ba282-27f9-4a93-8fe9-63f786781d48",
+				IPv4:     "192.168.1.5",
+				NodeIPv4: "192.168.2.5",
+				Region:   "default",
 			},
 		},
 	}
@@ -147,7 +152,7 @@ var (
 )
 
 func Test_parseOperationFromCluster(t *testing.T) {
-	h := newHandler(nil, nil, nil, nil, nil, nil, nil, nil, nil)
+	h := newHandler(nil, nil, nil, nil, nil, nil, nil, nil, nil, nil)
 	type args struct {
 		c      *v1.Cluster
 		meta   *component.ExtraMetadata
@@ -195,7 +200,7 @@ func Test_parseOperationFromComponent(t *testing.T) {
 		cluster    *v1.Cluster
 		components []v1.Addon
 	}
-	h := newHandler(nil, nil, nil, nil, nil, nil, nil, nil, nil)
+	h := newHandler(nil, nil, nil, nil, nil, nil, nil, nil, nil, nil)
 	nfs := nfsprovisioner.NFSProvisioner{
 		ManifestsDir:     "/tmp/.nfs",
 		Namespace:        "kube-system",
@@ -406,7 +411,8 @@ func Test_parseActBackupSteps(t *testing.T) {
 			},
 			ProxyIpv4CIDR: "10.0.0.0/32",
 			Status: v1.NodeStatus{
-				Ipv4DefaultIP: "127.0.0.1",
+				Ipv4DefaultIP:     "127.0.0.1",
+				NodeIpv4DefaultIP: "127.0.0.1",
 				NodeInfo: v1.NodeSystemInfo{
 					Hostname: "test",
 				},
diff --git a/pkg/clusteroperation/node_test.go b/pkg/clusteroperation/node_test.go
index e73ee91..a0a1523 100644
--- a/pkg/clusteroperation/node_test.go
+++ b/pkg/clusteroperation/node_test.go
@@ -85,31 +85,36 @@ var (
 	extraMeta = &component.ExtraMetadata{
 		Masters: []component.Node{
 			{
-				ID:     "1e3ea00f-1403-46e5-a486-70e4cb29d541",
-				IPv4:   "192.168.1.1",
-				Region: "default",
+				ID:       "1e3ea00f-1403-46e5-a486-70e4cb29d541",
+				IPv4:     "192.168.1.1",
+				NodeIPv4: "192.168.2.1",
+				Region:   "default",
 			},
 			{
-				ID:     "43ed594a-a76f-4370-a14d-551e7b6153de",
-				IPv4:   "192.168.1.2",
-				Region: "default",
+				ID:       "43ed594a-a76f-4370-a14d-551e7b6153de",
+				IPv4:     "192.168.1.2",
+				NodeIPv4: "192.168.2.2",
+				Region:   "default",
 			},
 			{
-				ID:     "c7a91d86-cd53-4c3f-85b0-fbc657778067",
-				IPv4:   "192.168.1.3",
-				Region: "default",
+				ID:       "c7a91d86-cd53-4c3f-85b0-fbc657778067",
+				IPv4:     "192.168.1.3",
+				NodeIPv4: "192.168.2.3",
+				Region:   "default",
 			},
 		},
 		Workers: []component.Node{
 			{
-				ID:     "4cf1ad74-704c-4290-a523-e524e930245d",
-				IPv4:   "192.168.1.4",
-				Region: "default",
+				ID:       "4cf1ad74-704c-4290-a523-e524e930245d",
+				IPv4:     "192.168.1.4",
+				NodeIPv4: "192.168.2.4",
+				Region:   "default",
 			},
 			{
-				ID:     "ae4ba282-27f9-4a93-8fe9-63f786781d48",
-				IPv4:   "192.168.1.5",
-				Region: "default",
+				ID:       "ae4ba282-27f9-4a93-8fe9-63f786781d48",
+				IPv4:     "192.168.1.5",
+				NodeIPv4: "192.168.2.5",
+				Region:   "default",
 			},
 		},
 	}