Repository: kubeclipper/kubeclipper
Pull request #654 (2023-08-22 05:58:33)

Support new k8s version
1. support k8s v1.26,v1.27
2. support calico v3.26.1

<!-- 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
none
```

### 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: Thanks for your pull request. Before we can look at it, you'll need to add a 'DCO signoff' to your commits.

:memo: **Please follow instructions in the [contributing guide](https://github.com/kubeclipper/kubeclipper/blob/master/CONTRIBUTING.md) to update your commits with the DCO**

Full details of the Developer Certificate of Origin can be found at [developercertificate.org](https://developercertificate.org/).

**The list of commits missing DCO signoff**:

- [ee1bf9e](https://github.com/kubeclipper/kubeclipper/commits/ee1bf9e87af5384eb69576707dc3da1567becea2) Support new k8s version

<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. I understand the commands that are listed [here](https://go.k8s.io/bot-commands).
</details>

kubeclipper-bot: [APPROVALNOTIFIER] This PR is **APPROVED**

This pull-request has been approved by: *<a href="https://github.com/kubeclipper/kubeclipper/pull/654#" title="Author self-approved">x893675</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)~~ [x893675]

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":[]} -->
codecov-commenter: ## [Codecov](https://app.codecov.io/gh/kubeclipper/kubeclipper/pull/654?src=pr&el=h1&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=kubeclipper) Report
> Merging [#654](https://app.codecov.io/gh/kubeclipper/kubeclipper/pull/654?src=pr&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=kubeclipper) (5bad251) into [master](https://app.codecov.io/gh/kubeclipper/kubeclipper/commit/eb1bf9901c833045a6040b46d43a37f993671009?el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=kubeclipper) (eb1bf99) will **decrease** coverage by `0.02%`.
> The diff coverage is `15.12%`.

:exclamation: Your organization is not using the GitHub App Integration. As a result you may experience degraded service beginning May 15th. Please [install the Github App Integration](https://github.com/apps/codecov) for your organization. [Read more](https://about.codecov.io/blog/codecov-is-updating-its-github-integration/?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=kubeclipper).

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

```diff
@@            Coverage Diff             @@
##           master     #654      +/-   ##
==========================================
- Coverage   13.96%   13.95%   -0.02%     
==========================================
  Files         139      139              
  Lines       19619    19712      +93     
==========================================
+ Hits         2739     2750      +11     
- Misses      16583    16666      +83     
+ Partials      297      296       -1     
```


| [Files Changed](https://app.codecov.io/gh/kubeclipper/kubeclipper/pull/654?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/cni/cni.go](https://app.codecov.io/gh/kubeclipper/kubeclipper/pull/654?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=kubeclipper#diff-cGtnL3NjaGVtZS9jb3JlL3YxL2NuaS9jbmkuZ28=) | `4.65% <0.00%> (-0.23%)` | :arrow_down: |
| [pkg/scheme/core/v1/cri/cri.go](https://app.codecov.io/gh/kubeclipper/kubeclipper/pull/654?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=kubeclipper#diff-cGtnL3NjaGVtZS9jb3JlL3YxL2NyaS9jcmkuZ28=) | `52.94% <ø> (ø)` | |
| [pkg/scheme/core/v1/k8s/extension.go](https://app.codecov.io/gh/kubeclipper/kubeclipper/pull/654?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=kubeclipper#diff-cGtnL3NjaGVtZS9jb3JlL3YxL2s4cy9leHRlbnNpb24uZ28=) | `1.16% <0.00%> (ø)` | |
| [pkg/scheme/core/v1/k8s/kubeadm\_step.go](https://app.codecov.io/gh/kubeclipper/kubeclipper/pull/654?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/scheme/core/v1/cni/calico.go](https://app.codecov.io/gh/kubeclipper/kubeclipper/pull/654?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=kubeclipper#diff-cGtnL3NjaGVtZS9jb3JlL3YxL2NuaS9jYWxpY28uZ28=) | `7.90% <5.12%> (-2.02%)` | :arrow_down: |
| [pkg/scheme/core/v1/cni/utils.go](https://app.codecov.io/gh/kubeclipper/kubeclipper/pull/654?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=kubeclipper#diff-cGtnL3NjaGVtZS9jb3JlL3YxL2NuaS91dGlscy5nbw==) | `5.73% <12.96%> (+5.73%)` | :arrow_up: |
| [pkg/component/utils/utils.go](https://app.codecov.io/gh/kubeclipper/kubeclipper/pull/654?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=kubeclipper#diff-cGtnL2NvbXBvbmVudC91dGlscy91dGlscy5nbw==) | `41.66% <100.00%> (+7.62%)` | :arrow_up: |


Patch:
diff --git a/pkg/clustermanage/kubeadm/provider.go b/pkg/clustermanage/kubeadm/provider.go
index d83a236..e3704cd 100644
--- a/pkg/clustermanage/kubeadm/provider.go
+++ b/pkg/clustermanage/kubeadm/provider.go
@@ -807,7 +807,7 @@ func (r *Kubeadm) kubectlTerminal(ctx context.Context, node KubeNode, action v1.
 			// docker load -i xxx/images.tar
 			loadImage = fmt.Sprintf("docker load -i %s", exImage)
 		case v1.CRIContainerd:
-			loadImage = fmt.Sprintf("ctr --namespace k8s.io image import --all-platforms %s", exImage)
+			loadImage = fmt.Sprintf("nerdctl -n k8s.io load -i %s", exImage)
 		default:
 			logger.Warnf("unsupported cri types: ", node.cri)
 		}
diff --git a/pkg/component/common/chart.go b/pkg/component/common/chart.go
new file mode 100644
index 0000000..a76e04d
--- /dev/null
+++ b/pkg/component/common/chart.go
@@ -0,0 +1,145 @@
+package common
+
+import (
+	"context"
+	"encoding/json"
+	"fmt"
+	"runtime"
+	"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"
+	v1 "github.com/kubeclipper/kubeclipper/pkg/scheme/core/v1"
+	"github.com/kubeclipper/kubeclipper/pkg/simple/downloader"
+	"github.com/kubeclipper/kubeclipper/pkg/utils/strutil"
+)
+
+const DefaultHelmChartRepo = "kubeclipper"
+
+const (
+	chartName  = "chart"
+	AgentChart = "AgentChart"
+)
+
+func init() {
+	if err := component.RegisterAgentStep(fmt.Sprintf(component.RegisterStepKeyFormat, chartName, version, AgentChart), &Chart{}); err != nil {
+		panic(err)
+	}
+}
+
+type Chart struct {
+	PkgName string `json:"pkgName"`
+	Version string `json:"version"`
+	Offline bool   `json:"offline"`
+}
+
+func (i *Chart) Install(ctx context.Context, opts component.Options) ([]byte, error) {
+	instance, err := downloader.NewInstance(ctx, i.PkgName, i.Version, runtime.GOARCH, !i.Offline, opts.DryRun)
+	if err != nil {
+		return nil, err
+	}
+
+	if _, err = instance.DownloadCharts(); err != nil {
+		return nil, fmt.Errorf("download %s-%s chart packages failed: %v", i.PkgName, i.Version, err)
+	}
+
+	logger.Infof("%s-%s chart packages offline install successfully", i.PkgName, i.Version)
+	return nil, err
+}
+
+func (i *Chart) Uninstall(ctx context.Context, opts component.Options) ([]byte, error) {
+	instance, err := downloader.NewInstance(ctx, i.PkgName, i.Version, runtime.GOARCH, !i.Offline, opts.DryRun)
+	if err != nil {
+		return nil, err
+	}
+
+	if err = instance.RemoveCharts(); err != nil {
+		logger.Errorf("remove %s-%s chart file failed", i.PkgName, i.Version, zap.Error(err))
+	}
+
+	return nil, nil
+}
+
+func (i *Chart) NewInstance() component.ObjectMeta {
+	return &Chart{}
+}
+
+func (i *Chart) InstallStepsV2(nodes []v1.StepNode) ([]v1.Step, error) {
+	customCommand, err := json.Marshal(i)
+	if err != nil {
+		return nil, err
+	}
+	return []v1.Step{
+		{
+			ID:         strutil.GetUUID(),
+			Name:       fmt.Sprintf("%s-chartLoad", i.PkgName),
+			Timeout:    metav1.Duration{Duration: 3 * time.Minute},
+			ErrIgnore:  false,
+			RetryTimes: 1,
+			Nodes:      nodes,
+			Action:     v1.ActionInstall,
+			Commands: []v1.Command{
+				{
+					Type:          v1.CommandCustom,
+					Identity:      fmt.Sprintf(component.RegisterStepKeyFormat, chartName, version, AgentChart),
+					CustomCommand: customCommand,
+				},
+			},
+		},
+	}, nil
+}
+
+func (i *Chart) InstallSteps(nodeList component.NodeList) ([]v1.Step, error) {
+	customCommand, err := json.Marshal(i)
+	if err != nil {
+		return nil, err
+	}
+	return []v1.Step{
+		{
+			ID:         strutil.GetUUID(),
+			Name:       fmt.Sprintf("%s-chartLoad", i.PkgName),
+			Timeout:    metav1.Duration{Duration: 3 * time.Minute},
+			ErrIgnore:  false,
+			RetryTimes: 1,
+			Nodes:      utils.UnwrapNodeList(nodeList),
+			Action:     v1.ActionInstall,
+			Commands: []v1.Command{
+				{
+					Type:          v1.CommandCustom,
+					Identity:      fmt.Sprintf(component.RegisterStepKeyFormat, chartName, version, AgentChart),
+					CustomCommand: customCommand,
+				},
+			},
+		},
+	}, nil
+}
+
+func GetAddHelmRepoStep(nodes []v1.StepNode, repo string) v1.Step {
+	return v1.Step{
+		ID:         strutil.GetUUID(),
+		Name:       "addHelmRepo",
+		Timeout:    metav1.Duration{Duration: 10 * time.Second},
+		ErrIgnore:  false,
+		RetryTimes: 0,
+		Nodes:      nodes,
+		Action:     v1.ActionInstall,
+		Commands: []v1.Command{
+			{
+				Type:         v1.CommandShell,
+				ShellCommand: []string{"bin/sh", "-c", fmt.Sprintf("helm repo remove %s || true", DefaultHelmChartRepo)}, // forward action, ignore errors
+			},
+			{
+				Type:         v1.CommandShell,
+				ShellCommand: []string{"/bin/sh", "-c", fmt.Sprintf("helm repo add %s %s", DefaultHelmChartRepo, repo)},
+			},
+			{
+				Type:         v1.CommandShell,
+				ShellCommand: []string{"bin/sh", "-c", fmt.Sprintf("helm repo update %s", DefaultHelmChartRepo)},
+			},
+		},
+	}
+}
diff --git a/pkg/component/utils/utils.go b/pkg/component/utils/utils.go
index 1443c78..48217d1 100644
--- a/pkg/component/utils/utils.go
+++ b/pkg/component/utils/utils.go
@@ -20,7 +20,6 @@ package utils
 
 import (
 	"context"
-	"strings"
 	"time"
 
 	"github.com/kubeclipper/kubeclipper/pkg/component"
@@ -30,33 +29,33 @@ import (
 	"github.com/kubeclipper/kubeclipper/pkg/utils/cmdutil"
 )
 
-// decompress and load image
+// LoadImage decompress and load image
 func LoadImage(ctx context.Context, dryRun bool, file, criType string) error {
-	_, err := cmdutil.RunCmdWithContext(ctx, dryRun, "gzip", "-df", file)
-	if err != nil {
-		return err
-	}
-
-	file = strings.ReplaceAll(file, ".gz", "")
+	//_, err := cmdutil.RunCmdWithContext(ctx, dryRun, "gzip", "-df", file)
+	//if err != nil {
+	//	return err
+	//}
+	//
+	//file = strings.ReplaceAll(file, ".gz", "")
 
 	switch criType {
 	case "containerd":
 		// ctr --namespace k8s.io image import --all-platforms xxx/images.tar
 		// Why need set `--all-platforms` flag?
 		// This prevents unexpected errors due to the incorrect schema type declaration of the mirroring system that cannot be imported normally.
-		_, err = cmdutil.RunCmdWithContext(ctx, dryRun, "ctr", "--namespace", "k8s.io", "image", "import", "--all-platforms", file)
+		_, err := cmdutil.RunCmdWithContext(ctx, dryRun, "nerdctl", "-n", "k8s.io", "load", "-i", file)
 		if err != nil {
 			return err
 		}
 	case "docker":
 		// docker load -i xxx/images.tar
-		_, err = cmdutil.RunCmdWithContext(ctx, dryRun, "docker", "load", "-i", file)
+		_, err := cmdutil.RunCmdWithContext(ctx, dryRun, "docker", "load", "-i", file)
 		if err != nil {
 			return err
 		}
 	}
 
-	_, err = cmdutil.RunCmdWithContext(ctx, dryRun, "rm", "-rf", file)
+	_, err := cmdutil.RunCmdWithContext(ctx, dryRun, "rm", "-rf", file)
 
 	return err
 }
diff --git a/pkg/scheme/common/utils.go b/pkg/scheme/common/utils.go
new file mode 100644
index 0000000..4853850
--- /dev/null
+++ b/pkg/scheme/common/utils.go
@@ -0,0 +1,21 @@
+package common
+
+import (
+	"strconv"
+	"strings"
+)
+
+func IsKubeVersionGreater(kubeVersion string, baseVersion int) bool {
+	if kubeVersion == "" {
+		return false
+	}
+	kubeVersion = strings.ReplaceAll(kubeVersion, "v", "")
+	kubeVersion = strings.ReplaceAll(kubeVersion, ".", "")
+
+	kubeVersion = strings.Join(strings.Split(kubeVersion, "")[0:3], "")
+
+	if v, _ := strconv.Atoi(kubeVersion); v >= baseVersion {
+		return true
+	}
+	return false
+}
diff --git a/pkg/scheme/core/v1/cluster_types.go b/pkg/scheme/core/v1/cluster_types.go
index ac92110..500e075 100644
--- a/pkg/scheme/core/v1/cluster_types.go
+++ b/pkg/scheme/core/v1/cluster_types.go
@@ -176,7 +176,11 @@ func (c *Cluster) Complete() {
 	c.CNI.LocalRegistry = c.LocalRegistry
 	c.CNI.CriType = c.ContainerRuntime.Type
 	c.CNI.Offline = c.Offline()
-	c.CNI.Namespace = "kube-system"
+	if common.IsKubeVersionGreater(c.KubernetesVersion, 126) {
+		c.CNI.Namespace = "calico-system"
+	} else {
+		c.CNI.Namespace = "kube-system"
+	}
 }
 
 type Certification struct {
diff --git a/pkg/scheme/core/v1/cni/calico.go b/pkg/scheme/core/v1/cni/calico.go
index e9a2697..9d2ec97 100644
--- a/pkg/scheme/core/v1/cni/calico.go
+++ b/pkg/scheme/core/v1/cni/calico.go
@@ -10,7 +10,9 @@ import (
 	"time"
 
 	"github.com/kubeclipper/kubeclipper/pkg/component"
+	"github.com/kubeclipper/kubeclipper/pkg/component/common"
 	v1 "github.com/kubeclipper/kubeclipper/pkg/scheme/core/v1"
+	"github.com/kubeclipper/kubeclipper/pkg/simple/downloader"
 	"github.com/kubeclipper/kubeclipper/pkg/utils/fileutil"
 	"github.com/kubeclipper/kubeclipper/pkg/utils/strutil"
 	tmplutil "github.com/kubeclipper/kubeclipper/pkg/utils/template"
@@ -42,8 +44,15 @@ func init() {
 	}
 }
 
+type NodeAddressDetection struct {
+	Type  string `json:"type"`
+	Value string `json:"value"`
+}
+
 type CalicoRunnable struct {
 	BaseCni
+	NodeAddressDetectionV4 NodeAddressDetection
+	NodeAddressDetectionV6 NodeAddressDetection
 }
 
 func (runnable *CalicoRunnable) Type() string {
@@ -73,7 +82,9 @@ func (runnable *CalicoRunnable) InitStep(metadata *component.ExtraMetadata, cni
 	stepper.Namespace = cni.Namespace
 	stepper.DualStack = networking.IPFamily == v1.IPFamilyDualStack
 	stepper.PodIPv4CIDR = networking.Pods.CIDRBlocks[0]
-	stepper.PodIPv4CIDR = ipv6
+	stepper.PodIPv6CIDR = ipv6
+	stepper.NodeAddressDetectionV4 = ParseNodeAddressDetection(cni.Calico.IPv4AutoDetection)
+	stepper.NodeAddressDetectionV6 = ParseNodeAddressDetection(cni.Calico.IPv6AutoDetection)
 
 	return stepper
 }
@@ -92,15 +103,30 @@ func (runnable *CalicoRunnable) LoadImage(nodes []v1.StepNode) ([]v1.Step, error
 	return steps, nil
 }
 
-func (runnable *CalicoRunnable) InstallSteps(nodes []v1.StepNode) ([]v1.Step, error) {
+func (runnable *CalicoRunnable) InstallSteps(nodes []v1.StepNode, kubernetesVersion string) ([]v1.Step, error) {
 	var steps []v1.Step
 	bytes, err := json.Marshal(runnable)
 	if err != nil {
 		return nil, err
 	}
+	if IsHighKubeVersion(kubernetesVersion) {
+		chart := &common.Chart{
+			PkgName: "calico",
+			Version: runnable.Version,
+			Offline: runnable.Offline,
+		}
 
-	steps = append(steps, RenderYaml("calico", bytes, nodes))
-	steps = append(steps, ApplyYaml(filepath.Join(manifestDir, "calico.yaml"), nodes))
+		cLoadSteps, err := chart.InstallStepsV2(nodes)
+		if err != nil {
+			return nil, err
+		}
+		steps = append(steps, cLoadSteps...)
+		steps = append(steps, RenderYaml("calico", bytes, nodes))
+		steps = append(steps, InstallCalicoRelease(filepath.Join(downloader.BaseDstDir, "."+chart.PkgName, chart.Version, downloader.ChartFilename), filepath.Join(manifestDir, "calico.yaml"), nodes))
+	} else {
+		steps = append(steps, RenderYaml("calico", bytes, nodes))
+		steps = append(steps, ApplyYaml(filepath.Join(manifestDir, "calico.yaml"), nodes))
+	}
 
 	return steps, nil
 }
@@ -162,6 +188,23 @@ func (runnable *CalicoRunnable) clear(calico *v1.Calico, nodes []v1.StepNode) []
 			},
 		})
 	}
+	// clean all cali* interface
+	steps = append(steps, v1.Step{
+		ID:         strutil.GetUUID(),
+		Name:       "removeCali",
+		Timeout:    metav1.Duration{Duration: 30 * time.Second},
+		ErrIgnore:  true,
+		Nodes:      nodes,
+		Action:     v1.ActionUninstall,
+		RetryTimes: 1,
+		Commands: []v1.Command{
+			{
+				Type: v1.CommandShell,
+				// ip addr | grep cali | awk '{cmd="ip link delete "$2;system(cmd)}'
+				ShellCommand: []string{"ip", "addr", "|", "grep", "cali", "|", "awk", "'{cmd=\"ip link delete \"$2;system(cmd)}'"},
+			},
+		},
+	})
 
 	return steps
 }
@@ -208,6 +251,8 @@ func (runnable *CalicoRunnable) CalicoTemplate() (string, error) {
 		return calicoV3224, nil
 	case "v3.24.5":
 		return calicoV3245, nil
+	case "v3.26.1":
+		return calicoV3261, nil
 	}
 	return "", fmt.Errorf("calico dose not support version: %s", runnable.Version)
 }
diff --git a/pkg/scheme/core/v1/cni/calico_template.go b/pkg/scheme/core/v1/cni/calico_template.go
index a0be6bf..3532ca8 100644
--- a/pkg/scheme/core/v1/cni/calico_template.go
+++ b/pkg/scheme/core/v1/cni/calico_template.go
@@ -17903,3 +17903,77 @@ spec:
               - /usr/bin/check-status
               - -r
             periodSeconds: 10`
+
+const calicoV3261 = `installation:
+  registry: {{with .CNI.LocalRegistry}}{{.}}{{end}}
+  cni:
+    type: Calico
+    ipam:
+      type: Calico
+  {{if eq .CNI.Calico.Mode "BGP"}}
+  bgp: Enabled
+  {{else}}
+  bgp: Disabled
+  {{end}}
+  calicoNetwork:
+    # Iptables, BPF
+    linuxDataplane: Iptables
+    mtu: {{.CNI.Calico.MTU}}
+    nodeAddressAutodetectionV4:
+      {{if eq .NodeAddressDetectionV4.Type "first-found"}}
+      firstFound: true
+      {{else if eq .NodeAddressDetectionV4.Type "interface"}}
+      interface: {{.NodeAddressDetectionV4.Value}}
+      {{else if eq .NodeAddressDetectionV4.Type "skip-interface"}}
+      skipInterface: {{.NodeAddressDetectionV4.Value}}
+      {{else if eq .NodeAddressDetectionV4.Type "can-reach"}}
+      canReach: {{.NodeAddressDetectionV4.Value}}
+      {{end}}
+      #cidrs: []
+      #kubernetes: xxx
+    {{if .DualStack}}
+    nodeAddressAutodetectionV6:
+      {{if eq .NodeAddressDetectionV6.Type "first-found"}}
+      firstFound: true
+      {{else if eq .NodeAddressDetectionV6.Type "interface"}}
+      interface: {{.NodeAddressDetectionV6.Value}}
+      {{else if eq .NodeAddressDetectionV6.Type "skip-interface"}}
+      skipInterface: {{.NodeAddressDetectionV6.Value}}
+      {{else if eq .NodeAddressDetectionV6.Type "can-reach"}}
+      canReach: {{.NodeAddressDetectionV6.Value}}
+      {{end}}
+    {{end}}
+    ipPools:
+      - blockSize: 26
+        cidr: {{.PodIPv4CIDR}}
+        {{if eq .CNI.Calico.Mode "Overlay-IPIP-All"}}
+        encapsulation: IPIP
+        {{else if eq .CNI.Calico.Mode "Overlay-IPIP-Cross-Subnet"}}
+        encapsulation: IPIPCrossSubnet
+        {{else if eq .CNI.Calico.Mode "Overlay-Vxlan-All"}}
+        encapsulation: VXLAN
+        {{else if eq .CNI.Calico.Mode "Overlay-Vxlan-Cross-Subnet"}}
+        encapsulation: VXLANCrossSubnet
+        {{else}}
+        encapsulation: None
+        {{end}}
+        natOutgoing: Enabled
+        nodeSelector: all()
+      {{if .DualStack}}
+      - blockSize: 122
+        cidr: {{.PodIPv6CIDR}}
+        encapsulation: None
+        natOutgoing: Enabled
+        nodeSelector: all()
+      {{end}}
+
+apiServer:
+  enabled: true
+
+tigeraOperator:
+  image: tigera/operator
+  version: v1.30.4
+  registry: {{with .CNI.LocalRegistry}}{{.}}{{else}}quay.io{{end}}
+calicoctl:
+  image: {{with .CNI.LocalRegistry}}{{.}}{{else}}docker.io{{end}}/calico/ctl
+  tag: v3.26.1`
diff --git a/pkg/scheme/core/v1/cni/cni.go b/pkg/scheme/core/v1/cni/cni.go
index 945f2b8..206bf03 100644
--- a/pkg/scheme/core/v1/cni/cni.go
+++ b/pkg/scheme/core/v1/cni/cni.go
@@ -47,7 +47,7 @@ type BaseCni struct {
 type Stepper interface {
 	InitStep(metadata *component.ExtraMetadata, cni *v1.CNI, networking *v1.Networking) Stepper
 	LoadImage(nodes []v1.StepNode) ([]v1.Step, error)
-	InstallSteps(nodes []v1.StepNode) ([]v1.Step, error)
+	InstallSteps(nodes []v1.StepNode, kubeVersion string) ([]v1.Step, error)
 	UninstallSteps(nodes []v1.StepNode) ([]v1.Step, error)
 	CmdList(namespace string) map[string]string
 }
@@ -61,16 +61,20 @@ func (runnable *BaseCni) Install(ctx context.Context, opts component.Options) ([
 	if err != nil {
 		return nil, err
 	}
-	dstFile, err := instance.DownloadImages()
-	if err != nil {
-		return nil, err
-	}
-	// load image package
-	if err = utils.LoadImage(ctx, opts.DryRun, dstFile, runnable.CriType); err == nil {
+
+	if runnable.Offline && runnable.LocalRegistry == "" {
+		dstFile, err := instance.DownloadImages()
+		if err != nil {
+			return nil, err
+		}
+		// load image package
+		if err = utils.LoadImage(ctx, opts.DryRun, dstFile, runnable.CriType); err != nil {
+			return nil, err
+		}
 		logger.Info("calico packages offline install successfully")
 	}
 
-	return nil, err
+	return nil, nil
 }
 
 func (runnable *BaseCni) Uninstall(ctx context.Context, opts component.Options) ([]byte, error) {
diff --git a/pkg/scheme/core/v1/cni/utils.go b/pkg/scheme/core/v1/cni/utils.go
index 053896b..3c9e905 100644
--- a/pkg/scheme/core/v1/cni/utils.go
+++ b/pkg/scheme/core/v1/cni/utils.go
@@ -2,6 +2,8 @@ package cni
 
 import (
 	"fmt"
+	"strconv"
+	"strings"
 	"time"
 
 	"github.com/kubeclipper/kubeclipper/pkg/component"
@@ -85,3 +87,64 @@ func RemoveImage(name string, custom []byte, nodes []v1.StepNode) v1.Step {
 		},
 	}
 }
+
+func InstallCalicoRelease(chartPath string, yamlName string, nodes []v1.StepNode) v1.Step {
+	return v1.Step{
+		ID:         strutil.GetUUID(),
+		Name:       "installCalicoRelease",
+		Timeout:    metav1.Duration{Duration: 1 * time.Minute},
+		ErrIgnore:  false,
+		RetryTimes: 1,
+		Nodes:      nodes,
+		Commands: []v1.Command{
+			{
+				Type:         v1.CommandShell,
+				ShellCommand: []string{"helm", "upgrade", "--install", "--create-namespace", "calico", "-n", "calico-system", chartPath, "-f", yamlName},
+			},
+		},
+	}
+}
+
+func IsHighKubeVersion(kubeVersion string) bool {
+	if kubeVersion == "" {
+		return false
+	}
+	kubeVersion = strings.ReplaceAll(kubeVersion, "v", "")
+	kubeVersion = strings.ReplaceAll(kubeVersion, ".", "")
+
+	kubeVersion = strings.Join(strings.Split(kubeVersion, "")[0:3], "")
+
+	if v, _ := strconv.Atoi(kubeVersion); v >= 126 {
+		return true
+	}
+	return false
+}
+
+func ParseNodeAddressDetection(nodeAddressDetection string) NodeAddressDetection {
+	// nodeAddressDetection first-found|can-reach=DESTINATION|interface=INTERFACE-REGEX|skip-interface=INTERFACE-REGEX
+	switch nodeAddressDetection {
+	case "first-found":
+		return NodeAddressDetection{
+			Type: "first-found",
+		}
+	case "can-reach":
+		return NodeAddressDetection{
+			Type:  "can-reach",
+			Value: strings.TrimPrefix(nodeAddressDetection, "can-reach="),
+		}
+	case "interface":
+		return NodeAddressDetection{
+			Type:  "interface",
+			Value: strings.TrimPrefix(nodeAddressDetection, "interface="),
+		}
+	case "skip-interface":
+		return NodeAddressDetection{
+			Type:  "skip-interface",
+			Value: strings.TrimPrefix(nodeAddressDetection, "skip-interface="),
+		}
+	default:
+		return NodeAddressDetection{
+			Type: "first-found",
+		}
+	}
+}
diff --git a/pkg/scheme/core/v1/cri/cri.go b/pkg/scheme/core/v1/cri/cri.go
index cff9331..723dc31 100644
--- a/pkg/scheme/core/v1/cri/cri.go
+++ b/pkg/scheme/core/v1/cri/cri.go
@@ -87,6 +87,9 @@ var k8sMatchPauseVersion = map[string]string{
 	"123": "3.6",
 	"124": "3.7",
 	"125": "3.8",
+	"126": "3.9",
+	"127": "3.9",
+	"128": "3.9",
 }
 
 var _ component.StepRunnable = (*ContainerdRunnable)(nil)
diff --git a/pkg/scheme/core/v1/k8s/extension.go b/pkg/scheme/core/v1/k8s/extension.go
index cd3df9b..2734fb8 100644
--- a/pkg/scheme/core/v1/k8s/extension.go
+++ b/pkg/scheme/core/v1/k8s/extension.go
@@ -4,7 +4,6 @@ import (
 	"context"
 	"encoding/json"
 	"fmt"
-
 	"runtime"
 	"time"
 
@@ -51,6 +50,10 @@ func (stepper *Extension) Install(ctx context.Context, opts component.Options) (
 	if err != nil {
 		return nil, err
 	}
+	// install configs.tar.gz
+	if _, err = instance.DownloadAndUnpackConfigs(); err != nil {
+		return nil, err
+	}
 	// local registry not filled and is in offline mode, download images.tar.gz file from tarballs
 	if stepper.Offline && stepper.LocalRegistry == "" {
 		imageSrc, err := instance.DownloadImages()
@@ -62,11 +65,6 @@ func (stepper *Extension) Install(ctx context.Context, opts component.Options) (
 		}
 		logger.Info("image tarball decompress successfully")
 	}
-	// install configs.tar.gz
-	if _, err = instance.DownloadAndUnpackConfigs(); err != nil {
-		return nil, err
-	}
-
 	logger.Debug("k8s packages offline install successfully")
 	return nil, nil
 }
diff --git a/pkg/scheme/core/v1/k8s/kubeadm_step.go b/pkg/scheme/core/v1/k8s/kubeadm_step.go
index cefe81a..22b200f 100644
--- a/pkg/scheme/core/v1/k8s/kubeadm_step.go
+++ b/pkg/scheme/core/v1/k8s/kubeadm_step.go
@@ -148,15 +148,15 @@ func (runnable *Runnable) makeInstallSteps(metadata *component.ExtraMetadata) ([
 	}
 	installSteps = append(installSteps, steps...)
 
-	pack := Package{}
-	steps, err = pack.InitStepper(&c).InstallSteps(nodes)
+	ext := Extension{}
+	steps, err = ext.InitStepper(&c).InstallSteps(nodes)
 	if err != nil {
 		return nil, err
 	}
 	installSteps = append(installSteps, steps...)
 
-	ext := Extension{}
-	steps, err = ext.InitStepper(&c).InstallSteps(nodes)
+	pack := Package{}
+	steps, err = pack.InitStepper(&c).InstallSteps(nodes)
 	if err != nil {
 		return nil, err
 	}
@@ -224,7 +224,7 @@ func (runnable *Runnable) makeInstallSteps(metadata *component.ExtraMetadata) ([
 		}
 		installSteps = append(installSteps, steps...)
 	}
-	steps, err = cniStepper.InstallSteps([]v1.StepNode{masters[0]})
+	steps, err = cniStepper.InstallSteps([]v1.StepNode{masters[0]}, runnable.KubernetesVersion)
 	if err != nil {
 		return nil, err
 	}
diff --git a/pkg/simple/downloader/downloader.go b/pkg/simple/downloader/downloader.go
index ced52e5..6066481 100644
--- a/pkg/simple/downloader/downloader.go
+++ b/pkg/simple/downloader/downloader.go
@@ -44,6 +44,7 @@ const (
 	ConfigFilename   = "configs.tar.gz"
 	BaseDstDir       = "/tmp/kc-downloader"
 	baseManifestDir  = "/opt/kc/manifest"
+	ChartFilename    = "charts.tgz"
 )
 
 var options *Options
@@ -80,7 +81,7 @@ func NewInstance(ctx context.Context, name, version, arch string, online, dryRun
 		baseURI = options.Address
 	}
 	if !dryRun {
-		dstDir = filepath.Join(BaseDstDir, "."+name, version, arch)
+		dstDir = filepath.Join(BaseDstDir, "."+name, version)
 		manifestDir = filepath.Join(baseManifestDir, name, version, arch)
 		cManifestDir = filepath.Join(baseManifestDir, name, version, arch, "config")
 		// create required directories
@@ -134,6 +135,20 @@ func (dl *Downloader) RemoveImages() error {
 	return os.RemoveAll(filepath.Join(dl.dstDir, ImageFilename))
 }
 
+// DownloadCharts download chart file
+func (dl *Downloader) DownloadCharts() (string, error) {
+	return filepath.Join(dl.dstDir, ChartFilename), dl.Download(ChartFilename)
+}
+
+// RemoveCharts remove chart file
+func (dl *Downloader) RemoveCharts() error {
+	return os.RemoveAll(filepath.Join(dl.dstDir, ChartFilename))
+}
+
+func (dl *Downloader) GetChartDownloadPath() string {
+	return filepath.Join(dl.dstDir, ChartFilename)
+}
+
 // DownloadCustomImages download custom image file
 func (dl *Downloader) DownloadCustomImages(imageList ...string) (files []string, err error) {
 	for _, image := range imageList {


Test patch:
diff --git a/pkg/scheme/core/v1/cni/calico_test.go b/pkg/scheme/core/v1/cni/calico_test.go
index a861e4a..7c38971 100644
--- a/pkg/scheme/core/v1/cni/calico_test.go
+++ b/pkg/scheme/core/v1/cni/calico_test.go
@@ -18,14 +18,14 @@ func TestCNI_renderCalicoTo(t *testing.T) {
 		{
 			name: "base",
 			stepper: CalicoRunnable{
-				BaseCni{
-					DualStack:   false,
+				BaseCni: BaseCni{
+					DualStack:   true,
 					PodIPv4CIDR: constatns.ClusterPodSubnet,
 					PodIPv6CIDR: "aaa:bbb",
 					CNI: v1.CNI{
 						LocalRegistry: "172.0.0.1:5000",
 						Type:          "calico",
-						Version:       "v3.21.2",
+						Version:       "v3.26.1",
 						Calico: &v1.Calico{
 							IPv4AutoDetection: "first-found",
 							IPv6AutoDetection: "first-found",
@@ -39,6 +39,8 @@ func TestCNI_renderCalicoTo(t *testing.T) {
 		},
 	}
 	for _, tt := range tests {
+		tt.stepper.NodeAddressDetectionV4 = ParseNodeAddressDetection(tt.stepper.Calico.IPv4AutoDetection)
+		tt.stepper.NodeAddressDetectionV6 = ParseNodeAddressDetection(tt.stepper.Calico.IPv6AutoDetection)
 		t.Run(tt.name, func(t *testing.T) {
 			w := &bytes.Buffer{}
 			err := tt.stepper.renderCalicoTo(w)