Repository: kubeclipper/kubeclipper
Pull request #750 (2024-08-14 09:18:23)

Check nodes and cluster health with kube clientset
### What type of PR is this?

/kind feature

### What this PR does / why we need it:

Check nodes and cluster health with kube clientset after deploying.

### Does this PR introduced a user-facing change?

```release-note
NONE
```

kubeclipper-bot: @crazytaxii: The label(s) `kind/optimization` cannot be applied, because the repository doesn't have them.

<details>

In response to [this](https://github.com/kubeclipper/kubeclipper/pull/750):

>### What type of PR is this?
>
>/kind optimization
>
>### What this PR does / why we need it:
>
>Check nodes and cluster health with kube clientset after deploying.
>
>### Does this PR introduced a user-facing change?
>
>```release-note
>NONE
>```


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/750#" title="Author self-approved">crazytaxii</a>*
To complete the [pull request process](https://git.k8s.io/community/contributors/guide/owners.md#the-code-review-process), please assign **wu-wenxiang** after the PR has been reviewed.
You can assign the PR to them by writing `/assign @wu-wenxiang` 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":["wu-wenxiang"]} -->
crazytaxii: /rerun-all
crazytaxii: /assign @lixd
lixd: @crazytaxii 检测没过
crazytaxii: > @crazytaxii 检测没过

有没有权限重跑？
lixd: > > @crazytaxii 检测没过
> 
> 有没有权限重跑？

还是不行，有个 test 没过，> > @crazytaxii 检测没过
> 
> 有没有权限重跑？

https://github.com/kubeclipper/kubeclipper/actions/runs/10384969333/job/28806111144?pr=750#step:4:204
crazytaxii: > > > @crazytaxii 检测没过
> > 
> > 
> > 有没有权限重跑？
> 
> 还是不行，有个 test 没过，> > @crazytaxii 检测没过
> 
> > 有没有权限重跑？
> 
> https://github.com/kubeclipper/kubeclipper/actions/runs/10384969333/job/28806111144?pr=750#step:4:204

应该测试代码本身有问题；CI 写的不好，用 make 错误被隐藏了
kubeclipper-bot: [APPROVALNOTIFIER] This PR is **NOT APPROVED**

This pull-request has been approved by: *<a href="https://github.com/kubeclipper/kubeclipper/pull/750#" title="Author self-approved">crazytaxii</a>*
To complete the [pull request process](https://git.k8s.io/community/contributors/guide/owners.md#the-code-review-process), please ask for approval from **lixd** after the PR has been reviewed.

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":["lixd"]} -->
codecov[bot]: ## [Codecov](https://app.codecov.io/gh/kubeclipper/kubeclipper/pull/750?dropdown=coverage&src=pr&el=h1&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=kubeclipper) Report
Attention: Patch coverage is `0%` with `55 lines` in your changes missing coverage. Please review.
> Project coverage is 14.59%. Comparing base [(`42cfd9c`)](https://app.codecov.io/gh/kubeclipper/kubeclipper/commit/42cfd9c52f3865068fa2b3eac80ddb402a1e4b7b?dropdown=coverage&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=kubeclipper) to head [(`e074598`)](https://app.codecov.io/gh/kubeclipper/kubeclipper/commit/e074598725d17edde0553f55dd08f048d808ad72?dropdown=coverage&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=kubeclipper).
> Report is 13 commits behind head on master.

| [Files](https://app.codecov.io/gh/kubeclipper/kubeclipper/pull/750?dropdown=coverage&src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=kubeclipper) | Patch % | Lines |
|---|---|---|
| [pkg/scheme/core/v1/k8s/kubeadm.go](https://app.codecov.io/gh/kubeclipper/kubeclipper/pull/750?src=pr&el=tree&filepath=pkg%2Fscheme%2Fcore%2Fv1%2Fk8s%2Fkubeadm.go&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=kubeclipper#diff-cGtnL3NjaGVtZS9jb3JlL3YxL2s4cy9rdWJlYWRtLmdv) | 0.00% | [32 Missing :warning: ](https://app.codecov.io/gh/kubeclipper/kubeclipper/pull/750?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=kubeclipper) |
| [pkg/scheme/core/v1/k8s/kubeadm\_step.go](https://app.codecov.io/gh/kubeclipper/kubeclipper/pull/750?src=pr&el=tree&filepath=pkg%2Fscheme%2Fcore%2Fv1%2Fk8s%2Fkubeadm_step.go&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=kubeclipper#diff-cGtnL3NjaGVtZS9jb3JlL3YxL2s4cy9rdWJlYWRtX3N0ZXAuZ28=) | 0.00% | [12 Missing :warning: ](https://app.codecov.io/gh/kubeclipper/kubeclipper/pull/750?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=kubeclipper) |
| [pkg/component/utils/utils.go](https://app.codecov.io/gh/kubeclipper/kubeclipper/pull/750?src=pr&el=tree&filepath=pkg%2Fcomponent%2Futils%2Futils.go&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=kubeclipper#diff-cGtnL2NvbXBvbmVudC91dGlscy91dGlscy5nbw==) | 0.00% | [7 Missing :warning: ](https://app.codecov.io/gh/kubeclipper/kubeclipper/pull/750?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=kubeclipper) |
| [pkg/scheme/core/v1/k8s/node.go](https://app.codecov.io/gh/kubeclipper/kubeclipper/pull/750?src=pr&el=tree&filepath=pkg%2Fscheme%2Fcore%2Fv1%2Fk8s%2Fnode.go&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=kubeclipper#diff-cGtnL3NjaGVtZS9jb3JlL3YxL2s4cy9ub2RlLmdv) | 0.00% | [4 Missing :warning: ](https://app.codecov.io/gh/kubeclipper/kubeclipper/pull/750?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=kubeclipper) |

<details><summary>Additional details and impacted files</summary>


[![Impacted file tree graph](https://app.codecov.io/gh/kubeclipper/kubeclipper/pull/750/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://app.codecov.io/gh/kubeclipper/kubeclipper/pull/750?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=kubeclipper)

```diff
@@            Coverage Diff             @@
##           master     #750      +/-   ##
==========================================
+ Coverage   14.45%   14.59%   +0.13%     
==========================================
  Files         142      144       +2     
  Lines       16745    16881     +136     
==========================================
+ Hits         2421     2463      +42     
- Misses      14032    14115      +83     
- Partials      292      303      +11     
```

| [Files](https://app.codecov.io/gh/kubeclipper/kubeclipper/pull/750?dropdown=coverage&src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=kubeclipper) | Coverage Δ | |
|---|---|---|
| [pkg/scheme/core/v1/k8s/node.go](https://app.codecov.io/gh/kubeclipper/kubeclipper/pull/750?src=pr&el=tree&filepath=pkg%2Fscheme%2Fcore%2Fv1%2Fk8s%2Fnode.go&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=kubeclipper#diff-cGtnL3NjaGVtZS9jb3JlL3YxL2s4cy9ub2RlLmdv) | `0.39% <0.00%> (-0.01%)` | :arrow_down: |
| [pkg/component/utils/utils.go](https://app.codecov.io/gh/kubeclipper/kubeclipper/pull/750?src=pr&el=tree&filepath=pkg%2Fcomponent%2Futils%2Futils.go&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=kubeclipper#diff-cGtnL2NvbXBvbmVudC91dGlscy91dGlscy5nbw==) | `20.51% <0.00%> (-4.49%)` | :arrow_down: |
| [pkg/scheme/core/v1/k8s/kubeadm\_step.go](https://app.codecov.io/gh/kubeclipper/kubeclipper/pull/750?src=pr&el=tree&filepath=pkg%2Fscheme%2Fcore%2Fv1%2Fk8s%2Fkubeadm_step.go&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=kubeclipper#diff-cGtnL3NjaGVtZS9jb3JlL3YxL2s4cy9rdWJlYWRtX3N0ZXAuZ28=) | `0.44% <0.00%> (-0.01%)` | :arrow_down: |
| [pkg/scheme/core/v1/k8s/kubeadm.go](https://app.codecov.io/gh/kubeclipper/kubeclipper/pull/750?src=pr&el=tree&filepath=pkg%2Fscheme%2Fcore%2Fv1%2Fk8s%2Fkubeadm.go&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=kubeclipper#diff-cGtnL3NjaGVtZS9jb3JlL3YxL2s4cy9rdWJlYWRtLmdv) | `2.85% <0.00%> (-0.13%)` | :arrow_down: |

</details>
kubeclipper-bot: LGTM label has been added.  <details>Git tree hash: 4cf17dcc8b03df7ad954659935a8e04cf70c2f12</details>
kubeclipper-bot: [APPROVALNOTIFIER] This PR is **APPROVED**

This pull-request has been approved by: *<a href="https://github.com/kubeclipper/kubeclipper/pull/750#" title="Author self-approved">crazytaxii</a>*, *<a href="https://github.com/kubeclipper/kubeclipper/pull/750#pullrequestreview-2241661082" title="Approved">lixd</a>*

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

The pull request process is described [here](https://git.k8s.io/community/contributors/guide/owners.md#the-code-review-process)

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

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

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":[]} -->

Patch:
diff --git a/pkg/component/utils/utils.go b/pkg/component/utils/utils.go
index 48217d1..1453fe5 100644
--- a/pkg/component/utils/utils.go
+++ b/pkg/component/utils/utils.go
@@ -20,12 +20,15 @@ package utils
 
 import (
 	"context"
+	"fmt"
 	"time"
 
-	"github.com/kubeclipper/kubeclipper/pkg/component"
-	v1 "github.com/kubeclipper/kubeclipper/pkg/scheme/core/v1"
+	"k8s.io/client-go/kubernetes"
+	"k8s.io/client-go/tools/clientcmd"
 
+	"github.com/kubeclipper/kubeclipper/pkg/component"
 	"github.com/kubeclipper/kubeclipper/pkg/logger"
+	v1 "github.com/kubeclipper/kubeclipper/pkg/scheme/core/v1"
 	"github.com/kubeclipper/kubeclipper/pkg/utils/cmdutil"
 )
 
@@ -87,3 +90,16 @@ func UnwrapNodeList(nl component.NodeList) (nodes []v1.StepNode) {
 	}
 	return nodes
 }
+
+// NamespacedKey returns ${namespace}/${name} string(namespaced key) of a Kubernetes object.
+func NamespacedKey(namespace, name string) string {
+	return fmt.Sprintf("%s/%s", namespace, name)
+}
+
+func BuildKubeClientset(kubeconfigPath string) (*kubernetes.Clientset, error) {
+	kubeCfg, err := clientcmd.BuildConfigFromFlags("", kubeconfigPath)
+	if err != nil {
+		return nil, fmt.Errorf("failed to build kubeconfig: %v", err)
+	}
+	return kubernetes.NewForConfig(kubeCfg)
+}
diff --git a/pkg/scheme/core/v1/k8s/kubeadm.go b/pkg/scheme/core/v1/k8s/kubeadm.go
index 321bf3f..b38df53 100644
--- a/pkg/scheme/core/v1/k8s/kubeadm.go
+++ b/pkg/scheme/core/v1/k8s/kubeadm.go
@@ -30,11 +30,15 @@ import (
 	"strings"
 	"time"
 
-	"github.com/kubeclipper/kubeclipper/pkg/agent/config"
 	"github.com/pkg/errors"
 	"github.com/txn2/txeh"
 	"go.uber.org/zap"
+	corev1 "k8s.io/api/core/v1"
+	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
+	"k8s.io/client-go/kubernetes"
+	"k8s.io/client-go/util/homedir"
 
+	"github.com/kubeclipper/kubeclipper/pkg/agent/config"
 	"github.com/kubeclipper/kubeclipper/pkg/component"
 	"github.com/kubeclipper/kubeclipper/pkg/component/utils"
 	"github.com/kubeclipper/kubeclipper/pkg/logger"
@@ -86,6 +90,8 @@ const (
 	kubeletSystemdUnitName = "kubelet.service"
 )
 
+var DefaultKubeConfigPath = filepath.Join(homedir.HomeDir(), ".kube/config")
+
 var (
 	_ component.StepRunnable   = (*Package)(nil)
 	_ component.TemplateRender = (*KubeadmConfig)(nil)
@@ -154,6 +160,7 @@ type ClusterNode struct {
 
 type Health struct {
 	KubernetesVersion string
+	*kubernetes.Clientset
 }
 
 type Certification struct{}
@@ -846,15 +853,20 @@ func (stepper *Health) NewInstance() component.ObjectMeta {
 	return &Health{}
 }
 
-func (stepper *Health) Install(ctx context.Context, opts component.Options) ([]byte, error) {
-	if err := utils.RetryFunc(ctx, opts, 10*time.Second, "allNodeReady", stepper.allNodeReady); err != nil {
-		return nil, err
+func (stepper *Health) Install(ctx context.Context, opts component.Options) (b []byte, err error) {
+	if stepper.Clientset == nil {
+		if stepper.Clientset, err = utils.BuildKubeClientset(DefaultKubeConfigPath); err != nil {
+			return nil, fmt.Errorf("failed to create clientset: %v", err)
+		}
 	}
-	if err := utils.RetryFunc(ctx, opts, 10*time.Second, "checkPodStatus", stepper.checkPodStatus); err != nil {
-		return nil, err
+
+	for _, fn := range []func(ctx context.Context, opts component.Options) error{stepper.allNodesReady, stepper.kubeSystemPodsReady} {
+		if err = utils.RetryFunc(ctx, opts, 10*time.Second, "allNodeReady", fn); err != nil {
+			return
+		}
 	}
 
-	return nil, nil
+	return
 }
 
 func (stepper *Health) Uninstall(ctx context.Context, opts component.Options) ([]byte, error) {
@@ -865,31 +877,42 @@ func (stepper *Health) Uninstall(ctx context.Context, opts component.Options) ([
 	return nil, nil
 }
 
-// the status of all nodes in the cluster is ready
-func (stepper *Health) allNodeReady(ctx context.Context, opts component.Options) error {
-	ec, err := cmdutil.RunCmdWithContext(ctx, opts.DryRun, "bash", "-c", `kubectl get node | grep NotReady`)
+func (stepper *Health) allNodesReady(ctx context.Context, opts component.Options) (err error) {
+	logger.Info("check status of all nodes in the cluster")
+	nodes, err := stepper.Clientset.CoreV1().Nodes().List(ctx, metav1.ListOptions{})
 	if err != nil {
-		logger.Warn("run kubectl get node error", zap.Error(err))
-	}
-	if ec.StdOut() == "" {
-		logger.Info("all nodes are ready")
-		return nil
+		return fmt.Errorf("failed to get nodes: %v", err)
+	}
+	for _, node := range nodes.Items {
+		for _, condition := range node.Status.Conditions {
+			if condition.Type == corev1.NodeReady && condition.Status != corev1.ConditionTrue {
+				err = fmt.Errorf("node %s is not ready", node.Name)
+				logger.Warn(err.Error())
+				return
+			}
+		}
 	}
-	logger.Warn("some nodes are not ready", zap.String("node", ec.StdOut()))
-	return fmt.Errorf("some nodes are not ready")
+
+	logger.Info("all nodes in the cluster are ready")
+	return
 }
 
-// k8s own component(kube-systemd) pod status is running
-func (stepper *Health) checkPodStatus(ctx context.Context, opts component.Options) error {
-	ec, err := cmdutil.RunCmdWithContext(ctx, opts.DryRun, "bash", "-c", `kubectl get po -n kube-system | grep -v Running`)
+func (stepper *Health) kubeSystemPodsReady(ctx context.Context, opts component.Options) (err error) {
+	logger.Info("check status of all Pods in kube-system namespace")
+	pods, err := stepper.Clientset.CoreV1().Pods("kube-system").List(ctx, metav1.ListOptions{})
 	if err != nil {
-		logger.Warn("run 'kubectl get po -n kube-system | grep -v Running' error", zap.Error(err))
+		return fmt.Errorf("failed to get pods: %v", err)
 	}
-
-	if len(strings.Split(ec.StdOut(), "\n")) > 2 {
-		return fmt.Errorf("there are no running pods: %s", strings.Join(ec.Args[1:], ","))
+	for _, pod := range pods.Items {
+		if pod.Status.Phase != corev1.PodRunning {
+			err = fmt.Errorf("pod %s is not running", utils.NamespacedKey(pod.Namespace, pod.Name))
+			logger.Warn(err.Error())
+			return
+		}
 	}
-	return err
+
+	logger.Info("all Pods in kube-system namespace are ready")
+	return
 }
 
 func (stepper *Health) getRegisterServiceAccountCommands() ([]v1.Command, error) {
diff --git a/pkg/scheme/core/v1/k8s/kubeadm_step.go b/pkg/scheme/core/v1/k8s/kubeadm_step.go
index a6a4eee..f911169 100644
--- a/pkg/scheme/core/v1/k8s/kubeadm_step.go
+++ b/pkg/scheme/core/v1/k8s/kubeadm_step.go
@@ -248,8 +248,11 @@ func (runnable *Runnable) makeInstallSteps(metadata *component.ExtraMetadata) ([
 	}
 	installSteps = append(installSteps, steps...)
 
-	heal := Health{}
-	steps, err = heal.InitStepper(c.KubernetesVersion).InstallSteps([]v1.StepNode{masters[0]})
+	heal := &Health{}
+	if err := heal.InitStepper(c.KubernetesVersion, DefaultKubeConfigPath); err != nil {
+		return nil, err
+	}
+	steps, err = heal.InstallSteps([]v1.StepNode{masters[0]})
 	if err != nil {
 		return nil, err
 	}
@@ -333,8 +336,11 @@ func (runnable *Runnable) makeUninstallSteps(metadata *component.ExtraMetadata)
 	}
 	uninstallSteps = append(uninstallSteps, steps...)
 
-	heal := Health{}
-	steps, err = heal.InitStepper(c.KubernetesVersion).UninstallSteps(&runnable.Networking, nodes...)
+	heal := &Health{}
+	if err := heal.InitStepper(c.KubernetesVersion, DefaultKubeConfigPath); err != nil {
+		return nil, err
+	}
+	steps, err = heal.UninstallSteps(&runnable.Networking, nodes...)
 	if err != nil {
 		return nil, err
 	}
@@ -647,9 +653,12 @@ func (stepper *ClusterNode) UninstallSteps(nodes []v1.StepNode) ([]v1.Step, erro
 	return nil, nil
 }
 
-func (stepper *Health) InitStepper(version string) *Health {
+func (stepper *Health) InitStepper(version, kubeConfigPath string) (err error) {
 	stepper.KubernetesVersion = version
-	return stepper
+	if stepper.Clientset, err = utils.BuildKubeClientset(kubeConfigPath); err != nil {
+		return fmt.Errorf("failed to create clientset: %v", err)
+	}
+	return
 }
 
 func (stepper *Health) InstallSteps(nodes []v1.StepNode) ([]v1.Step, error) {
diff --git a/pkg/scheme/core/v1/k8s/node.go b/pkg/scheme/core/v1/k8s/node.go
index b49f003..3933dff 100644
--- a/pkg/scheme/core/v1/k8s/node.go
+++ b/pkg/scheme/core/v1/k8s/node.go
@@ -25,6 +25,9 @@ import (
 	"strings"
 	"time"
 
+	"go.uber.org/zap"
+	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
+
 	"github.com/kubeclipper/kubeclipper/pkg/component"
 	"github.com/kubeclipper/kubeclipper/pkg/component/utils"
 	"github.com/kubeclipper/kubeclipper/pkg/logger"
@@ -32,8 +35,6 @@ import (
 	"github.com/kubeclipper/kubeclipper/pkg/scheme/core/v1/cni"
 	"github.com/kubeclipper/kubeclipper/pkg/utils/cmdutil"
 	"github.com/kubeclipper/kubeclipper/pkg/utils/strutil"
-	"go.uber.org/zap"
-	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
 )
 
 var (
@@ -216,8 +217,12 @@ func (stepper *GenNode) MakeUninstallSteps(metadata *component.ExtraMetadata, pa
 			doCommandRemoveStep("removeKubeletDataDir", patchNodes, KubeletDefaultDataDir),
 			doCommandRemoveStep("removeDockershimDataDir", patchNodes, DockershimDefaultDataDir),
 		)
-		heal := Health{}
-		steps, err = heal.InitStepper(metadata.KubeVersion).UninstallSteps(&stepper.Cluster.Networking, patchNodes...)
+
+		heal := &Health{}
+		if err := heal.InitStepper(metadata.KubeVersion, DefaultKubeConfigPath); err != nil {
+			return err
+		}
+		steps, err = heal.UninstallSteps(&stepper.Cluster.Networking, patchNodes...)
 		if err != nil {
 			return err
 		}
diff --git a/tools/licfmt/licfmt.go b/tools/licfmt/licfmt.go
index b426036..0853543 100644
--- a/tools/licfmt/licfmt.go
+++ b/tools/licfmt/licfmt.go
@@ -186,12 +186,8 @@ func addLicenseHeader(path, license string, fmode os.FileMode) (bool, error) {
 	}
 	defer os.Remove(tmp.Name())
 
-	stat, err := f.Stat()
-	if err != nil {
-		return false, err
-	}
 	// Copy the file mode.
-	if err := tmp.Chmod(stat.Mode()); err != nil {
+	if err := tmp.Chmod(fmode); err != nil {
 		return false, err
 	}
 


Test patch:
diff --git a/pkg/apis/core/v1/utils_test.go b/pkg/apis/core/v1/utils_test.go
index 08755a3..f7fe555 100644
--- a/pkg/apis/core/v1/utils_test.go
+++ b/pkg/apis/core/v1/utils_test.go
@@ -21,22 +21,23 @@ package v1
 import (
 	"context"
 	"encoding/json"
+	"errors"
+	"os"
+	"path/filepath"
 	"strings"
 	"testing"
 
-	"github.com/kubeclipper/kubeclipper/pkg/constatns"
-
 	"github.com/golang/mock/gomock"
-
-	mock_cluster "github.com/kubeclipper/kubeclipper/pkg/models/cluster/mock"
-	"github.com/kubeclipper/kubeclipper/pkg/scheme/common"
-
 	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
 	"k8s.io/apimachinery/pkg/runtime"
 
 	"github.com/kubeclipper/kubeclipper/pkg/component"
 	nfsprovisioner "github.com/kubeclipper/kubeclipper/pkg/component/nfs"
+	"github.com/kubeclipper/kubeclipper/pkg/constatns"
+	mock_cluster "github.com/kubeclipper/kubeclipper/pkg/models/cluster/mock"
+	"github.com/kubeclipper/kubeclipper/pkg/scheme/common"
 	v1 "github.com/kubeclipper/kubeclipper/pkg/scheme/core/v1"
+	"github.com/kubeclipper/kubeclipper/pkg/scheme/core/v1/k8s"
 )
 
 var (
@@ -152,7 +153,48 @@ var (
 	}
 )
 
+const fakeKubeConfig = `
+apiVersion: v1
+clusters:
+- cluster:
+    certificate-authority-data:
+    server: https://foo.bar.baz:6443
+  name: fake-cluster
+contexts:
+- context:
+    cluster: fake-cluster
+    user: kubernetes-admin
+  name: kubernetes-admin@fake-cluster
+current-context: kubernetes-admin@fake-cluster
+kind: Config
+preferences: {}
+users:
+- name: kubernetes-admin
+  user:
+    client-certificate-data:
+    client-key-data:
+`
+
+// mockKubeConfig generates a fake kubeconfig file
+func mockKubeConfig() (yes bool, err error) {
+	if _, err = os.Stat(k8s.DefaultKubeConfigPath); err != nil && errors.Is(err, os.ErrNotExist) {
+		_ = os.MkdirAll(filepath.Dir(k8s.DefaultKubeConfigPath), 0755)
+		return true, os.WriteFile(k8s.DefaultKubeConfigPath, []byte(fakeKubeConfig), 0644)
+	}
+	return
+}
+
 func Test_parseOperationFromCluster(t *testing.T) {
+	yes, err := mockKubeConfig()
+	if err != nil {
+		t.Errorf("mock kubeconfig error: %v", err)
+		return
+	}
+	if yes {
+		// clean fake kubeconfig file
+		defer os.RemoveAll(filepath.Dir(k8s.DefaultKubeConfigPath))
+	}
+
 	h := newHandler(nil, nil, nil, nil, nil, nil, nil, nil, nil)
 	type args struct {
 		c      *v1.Cluster