Repository: kubeclipper/kubeclipper
Pull request #372 (2022-11-11 09:32:07)

feat: support specify project when create provider
Signed-off-by: lixd <xueduan.li@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:
support specify project when create provider
### 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: [APPROVALNOTIFIER] This PR is **NOT APPROVED**

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

Patch:
diff --git a/go.mod b/go.mod
index 0c01093..47b7da5 100644
--- a/go.mod
+++ b/go.mod
@@ -140,6 +140,7 @@ require (
 	github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
 	github.com/modern-go/reflect2 v1.0.2 // indirect
 	github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect
+	github.com/mxk/go-flowrate v0.0.0-20140419014527-cca7078d478f // indirect
 	github.com/nats-io/jwt/v2 v2.0.3 // indirect
 	github.com/nats-io/nkeys v0.3.0 // indirect
 	github.com/nats-io/nuid v1.0.1 // indirect
diff --git a/go.sum b/go.sum
index 932e5a9..40a4591 100644
--- a/go.sum
+++ b/go.sum
@@ -672,6 +672,7 @@ github.com/munnerz/goautoneg v0.0.0-20120707110453-a547fc61f48d/go.mod h1:+n7T8m
 github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA=
 github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ=
 github.com/mwitkow/go-conntrack v0.0.0-20190716064945-2f068394615f/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U=
+github.com/mxk/go-flowrate v0.0.0-20140419014527-cca7078d478f h1:y5//uYreIhSUg3J1GEMiLbxo1LJaP8RfCpH6pymGZus=
 github.com/mxk/go-flowrate v0.0.0-20140419014527-cca7078d478f/go.mod h1:ZdcZmHo+o7JKHSa8/e818NopupXU1YMK5fe1lsApnBw=
 github.com/nats-io/jwt v1.2.2 h1:w3GMTO969dFg+UOKTmmyuu7IGdusK+7Ytlt//OYH/uU=
 github.com/nats-io/jwt v1.2.2/go.mod h1:/xX356yQA6LuXI9xWW7mZNpxgF2mBmGecH+Fj34sP5Q=
diff --git a/pkg/apis/core/v1/handler.go b/pkg/apis/core/v1/handler.go
index d922901..f4e052e 100644
--- a/pkg/apis/core/v1/handler.go
+++ b/pkg/apis/core/v1/handler.go
@@ -32,6 +32,8 @@ import (
 	"strings"
 	"time"
 
+	"github.com/kubeclipper/kubeclipper/pkg/controller/cronbackupcontroller"
+
 	"github.com/emicklei/go-restful"
 	"github.com/google/uuid"
 	"github.com/gorilla/websocket"
@@ -42,14 +44,13 @@ import (
 	"k8s.io/apimachinery/pkg/util/json"
 	r "k8s.io/apimachinery/pkg/util/rand"
 	"k8s.io/apimachinery/pkg/util/sets"
+	"k8s.io/client-go/tools/clientcmd"
 	"k8s.io/client-go/tools/remotecommand"
 
-	tenantapiv1 "github.com/kubeclipper/kubeclipper/pkg/apis/tenant/v1"
-	tenantv1 "github.com/kubeclipper/kubeclipper/pkg/scheme/tenant/v1"
-
-	"github.com/kubeclipper/kubeclipper/pkg/models/tenant"
-	reqpkg "github.com/kubeclipper/kubeclipper/pkg/server/request"
+	"github.com/kubeclipper/kubeclipper/pkg/models"
 
+	tenantapiv1 "github.com/kubeclipper/kubeclipper/pkg/apis/tenant/v1"
+	"github.com/kubeclipper/kubeclipper/pkg/authentication/auth"
 	"github.com/kubeclipper/kubeclipper/pkg/client/clientrest"
 	"github.com/kubeclipper/kubeclipper/pkg/clustermanage"
 	"github.com/kubeclipper/kubeclipper/pkg/component"
@@ -62,12 +63,16 @@ import (
 	"github.com/kubeclipper/kubeclipper/pkg/models/lease"
 	"github.com/kubeclipper/kubeclipper/pkg/models/operation"
 	"github.com/kubeclipper/kubeclipper/pkg/models/platform"
+	"github.com/kubeclipper/kubeclipper/pkg/models/tenant"
 	"github.com/kubeclipper/kubeclipper/pkg/oplog"
 	"github.com/kubeclipper/kubeclipper/pkg/query"
 	"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"
 	"github.com/kubeclipper/kubeclipper/pkg/scheme/core/validation"
+	tenantv1 "github.com/kubeclipper/kubeclipper/pkg/scheme/tenant/v1"
+	apirequest "github.com/kubeclipper/kubeclipper/pkg/server/request"
+	reqpkg "github.com/kubeclipper/kubeclipper/pkg/server/request"
 	"github.com/kubeclipper/kubeclipper/pkg/server/restplus"
 	"github.com/kubeclipper/kubeclipper/pkg/service"
 	bs "github.com/kubeclipper/kubeclipper/pkg/simple/backupstore"
@@ -90,6 +95,7 @@ type handler struct {
 	coreOperator     core.Operator
 	delivery         service.IDelivery
 	tenantOperator   tenant.Operator
+	tokenOperator    auth.TokenManagementInterface
 }
 
 const (
@@ -105,7 +111,8 @@ var (
 )
 
 func newHandler(clusterOperator cluster.Operator, op operation.Operator, leaseOperator lease.Operator,
-	platform platform.Operator, coreOperator core.Operator, delivery service.IDelivery, tenantOperator tenant.Operator) *handler {
+	platform platform.Operator, coreOperator core.Operator, delivery service.IDelivery,
+	tokenOperator auth.TokenManagementInterface, tenantOperator tenant.Operator) *handler {
 	return &handler{
 		clusterOperator:  clusterOperator,
 		delivery:         delivery,
@@ -114,6 +121,7 @@ func newHandler(clusterOperator cluster.Operator, op operation.Operator, leaseOp
 		leaseOperator:    leaseOperator,
 		coreOperator:     coreOperator,
 		tenantOperator:   tenantOperator,
+		tokenOperator:    tokenOperator,
 	}
 }
 
@@ -832,6 +840,8 @@ func (h *handler) UpdateClusterCertification(request *restful.Request, response
 
 func (h *handler) GetKubeConfig(request *restful.Request, response *restful.Response) {
 	name := request.PathParameter(query.ParameterName)
+	proxyMode := strings.ToLower(request.QueryParameter("proxy")) == "true"
+
 	ctx := request.Request.Context()
 	clu, err := h.clusterOperator.GetCluster(ctx, name)
 	if err != nil {
@@ -848,23 +858,65 @@ func (h *handler) GetKubeConfig(request *restful.Request, response *restful.Resp
 		}
 	}
 
-	extraMeta, err := h.getClusterMetadata(ctx, clu)
-	if err != nil {
-		restplus.HandleInternalError(response, request, err)
-		return
+	var (
+		kubeConfigData []byte
+	)
+	if proxyMode {
+		kubeConfigData, err = h.getProxyKubeConfig(ctx, name, request.Request.Host)
+		if err != nil {
+			restplus.HandleInternalError(response, request, err)
+			return
+		}
+	} else {
+		extraMeta, err := h.getClusterMetadata(ctx, clu)
+		if err != nil {
+			restplus.HandleInternalError(response, request, err)
+			return
+		}
+
+		if clu.Annotations != nil && clu.Annotations[common.AnnotationActualName] != "" {
+			extraMeta.ClusterName = clu.Annotations[common.AnnotationActualName]
+		}
+
+		kubeConfig, err := k8s.GetKubeConfig(context.TODO(), extraMeta.ClusterName, extraMeta.Masters[0], h.delivery)
+		if err != nil {
+			restplus.HandleInternalError(response, request, err)
+			return
+		}
+		kubeConfigData = []byte(kubeConfig)
 	}
 
-	if clu.Annotations != nil && clu.Annotations[common.AnnotationActualName] != "" {
-		extraMeta.ClusterName = clu.Annotations[common.AnnotationActualName]
+	_, _ = response.Write(kubeConfigData)
+}
+
+func (h *handler) getProxyKubeConfig(ctx context.Context, clusterName string, kcHost string) ([]byte, error) {
+	// FIXME: KC support secure port deploy
+	var serverCA []byte
+	// TODO: get CA from kc server config
+	serverURL := fmt.Sprintf("https://%s/cluster/%s", kcHost, clusterName)
+	user, ok := apirequest.UserFrom(ctx)
+	if !ok {
+		return nil, fmt.Errorf("not get user info")
 	}
 
-	kubeConfig, err := k8s.GetKubeConfig(context.TODO(), extraMeta.ClusterName, extraMeta.Masters[0], h.delivery)
+	token, err := h.tokenOperator.IssueTo(user)
 	if err != nil {
-		restplus.HandleInternalError(response, request, err)
-		return
+		return nil, fmt.Errorf("issue token:%w", err)
 	}
 
-	_, _ = response.Write([]byte(kubeConfig))
+	kubecfg := CreateWithToken(serverURL, clusterName, user.GetName(), serverCA, token.AccessToken)
+	// skip all tls verify
+	for _, c := range kubecfg.Clusters {
+		if strings.HasPrefix(strings.ToLower(c.Server), "https") {
+			c.InsecureSkipTLSVerify = true
+		}
+	}
+
+	buf, err := clientcmd.Write(*kubecfg)
+	if err != nil {
+		return nil, err
+	}
+	return buf, nil
 }
 
 func (h *handler) ListNodes(request *restful.Request, response *restful.Response) {
@@ -1359,6 +1411,44 @@ func (h *handler) ListRegions(request *restful.Request, response *restful.Respon
 	}
 }
 
+func (h *handler) ListRegionsInProject(request *restful.Request, response *restful.Response) {
+	q := query.ParseQueryParameter(request)
+	project := request.PathParameter("project")
+	ctx := request.Request.Context()
+
+	// list node and extract region
+	nodeQ := query.New()
+	nodeQ.LabelSelector = fmt.Sprintf("%s=%s", common.LabelProject, project)
+	nodes, err := h.clusterOperator.ListNodes(ctx, nodeQ)
+	if err != nil {
+		restplus.HandleInternalError(response, request, err)
+		return
+	}
+
+	regions := sets.NewString()
+	for _, node := range nodes.Items {
+		regions.Insert(node.Labels[common.LabelTopologyRegion])
+	}
+
+	list := make([]v1.Region, 0, regions.Len())
+	for _, region := range regions.List() {
+		getRegion, err := h.clusterOperator.GetRegion(ctx, region)
+		if err != nil {
+			restplus.HandleInternalError(response, request, err)
+			return
+		}
+		list = append(list, *getRegion)
+	}
+	// build a pageable response
+	regionList := &v1.RegionList{Items: list}
+	result, err := models.DefaultList(regionList.DeepCopyObject(), q, cluster.RegionFuzzyFilter, nil, nil)
+	if err != nil {
+		restplus.HandleInternalError(response, request, err)
+		return
+	}
+	_ = response.WriteHeaderAndEntity(http.StatusOK, result)
+}
+
 func (h *handler) watchRegions(req *restful.Request, resp *restful.Response, q *query.Query) {
 	timeout := time.Duration(0)
 	if q.TimeoutSeconds != nil {
@@ -3422,13 +3512,17 @@ func (h *handler) UpdateCronBackup(req *restful.Request, resp *restful.Response)
 	}
 
 	if cb.Spec.Schedule != "" {
-		ocb.Spec.Schedule = cb.Spec.Schedule
+		r := cronbackupcontroller.CronBackupReconciler{}
+		r.Now = func() time.Time {
+			return time.Now()
+		}
+		ocb.Spec.Schedule = r.ParseSchedule(cb.Spec.Schedule)
 		// update the next schedule time
-		s, _ := cron.NewParser(4 | 8 | 16 | 32 | 64).Parse(cb.Spec.Schedule)
+		s, _ := cron.NewParser(4 | 8 | 16 | 32 | 64).Parse(ocb.Spec.Schedule)
 		nextRunAt := metav1.NewTime(s.Next(time.Now()))
 		ocb.Status.NextScheduleTime = &nextRunAt
 	}
-
+	ocb.Spec.MaxBackupNum = cb.Spec.MaxBackupNum
 	_, err = h.clusterOperator.UpdateCronBackup(req.Request.Context(), ocb)
 	if err != nil {
 		restplus.HandleInternalError(resp, req, err)
@@ -3725,12 +3819,12 @@ func (h *handler) PreCheckCloudProvider(req *restful.Request, resp *restful.Resp
 
 func (h *handler) CreateCloudProvider(req *restful.Request, resp *restful.Response) {
 	cp := &v1.CloudProvider{}
-	err := req.ReadEntity(cp)
-	if err != nil {
+	ctx := req.Request.Context()
+	if err := req.ReadEntity(cp); err != nil {
 		restplus.HandleInternalError(resp, req, err)
 		return
 	}
-	if err = h.providerValidate(req.Request.Context(), cp); err != nil {
+	if err := h.providerValidate(req.Request.Context(), cp); err != nil {
 		restplus.HandleBadRequest(resp, req, err)
 		return
 	}
@@ -3740,7 +3834,20 @@ func (h *handler) CreateCloudProvider(req *restful.Request, resp *restful.Respon
 		restplus.HandleBadRequest(resp, req, err)
 		return
 	}
-	check, err := provider.PreCheck(req.Request.Context())
+	projectName := cp.Labels[common.LabelProject]
+	if projectName == "" {
+		restplus.HandleBadRequest(resp, req, errors.New("cloudprovider must belong to a  project"))
+		return
+	}
+	if _, err = h.tenantOperator.GetProject(ctx, projectName); err != nil {
+		if apimachineryErrors.IsNotFound(err) {
+			restplus.HandleBadRequest(resp, req, err)
+			return
+		}
+		restplus.HandleInternalError(resp, req, err)
+		return
+	}
+	check, err := provider.PreCheck(ctx)
 	if err != nil {
 		restplus.HandleBadRequest(resp, req, fmt.Errorf("preCheck connect failed: %v", err))
 		return
@@ -3796,6 +3903,11 @@ func (h *handler) UpdateCloudProvider(req *restful.Request, resp *restful.Respon
 		return
 	}
 
+	if cp.Labels[common.LabelProject] != oldCP.Labels[common.LabelProject] {
+		restplus.HandleBadRequest(resp, req, fmt.Errorf("provider already belong to project %s", cp.Labels[common.LabelProject]))
+		return
+	}
+
 	if !dryRun {
 		cp.Status = oldCP.Status // ignore status update from client
 		cp, err = h.clusterOperator.UpdateCloudProvider(req.Request.Context(), cp)
diff --git a/pkg/apis/core/v1/registry.go b/pkg/apis/core/v1/registry.go
index e6ce832..8fc1694 100644
--- a/pkg/apis/core/v1/registry.go
+++ b/pkg/apis/core/v1/registry.go
@@ -21,6 +21,7 @@ package v1
 import (
 	"net/http"
 
+	"github.com/kubeclipper/kubeclipper/pkg/authentication/auth"
 	"github.com/kubeclipper/kubeclipper/pkg/models/core"
 	"github.com/kubeclipper/kubeclipper/pkg/models/tenant"
 
@@ -258,6 +259,8 @@ func SetupWebService(h *handler) *restful.WebService {
 		Param(webservice.PathParameter(query.ParameterName, "cluster name").
 			Required(true).
 			DataType("string")).
+		Param(webservice.QueryParameter("proxy", "use kubeClipper proxy").
+			Required(false).DataType("boolean").DefaultValue("false")).
 		Returns(http.StatusOK, http.StatusText(http.StatusOK), clientcmdapi.Config{}).
 		Returns(http.StatusNotFound, http.StatusText(http.StatusNotFound), nil))
 	webservice.Route(webservice.GET("/projects/{project}/clusters/{name}/kubeconfig").
@@ -495,6 +498,30 @@ func SetupWebService(h *handler) *restful.WebService {
 			DefaultValue("60").
 			Required(false)).
 		Returns(http.StatusOK, http.StatusText(http.StatusOK), models.PageableResponse{}))
+	webservice.Route(webservice.GET("/projects/{project}/regions").
+		To(h.ListRegionsInProject).
+		Metadata(restfulspec.KeyOpenAPITags, []string{CoreRegionTag}).
+		Doc("List regions in project.").
+		Param(webservice.PathParameter("project", "project name")).
+		Param(webservice.QueryParameter(query.PagingParam, "paging query, e.g. limit=100,page=1").
+			Required(false).
+			DataFormat("limit=%d,page=%d").
+			DefaultValue("limit=10,page=1")).
+		Param(webservice.QueryParameter(query.ParameterLabelSelector, "resource filter by metadata label").
+			Required(false).
+			DataFormat("labelSelector=%s=%s")).
+		Param(webservice.QueryParameter(query.ParameterFieldSelector, "resource filter by field").
+			Required(false).
+			DataFormat("fieldSelector=%s=%s")).
+		Param(webservice.QueryParameter(query.ParamReverse, "resource sort reverse or not").Required(false).
+			DataType("boolean")).
+		Param(webservice.QueryParameter(query.ParameterWatch, "watch request").Required(false).
+			DataType("boolean")).
+		Param(webservice.QueryParameter(query.ParameterTimeoutSeconds, "watch timeout seconds").
+			DataType("integer").
+			DefaultValue("60").
+			Required(false)).
+		Returns(http.StatusOK, http.StatusText(http.StatusOK), models.PageableResponse{}))
 
 	webservice.Route(webservice.GET("/regions/{name}").
 		To(h.DescribeRegion).
@@ -1590,9 +1617,10 @@ func SetupWebService(h *handler) *restful.WebService {
 	return webservice
 }
 
-func AddToContainer(c *restful.Container, clusterOperator cluster.Operator, op operation.Operator, platform platform.Operator,
-	leaseOperator lease.Operator, coreOperator core.Operator, delivery service.IDelivery, tenantOperator tenant.Operator) error {
-	h := newHandler(clusterOperator, op, leaseOperator, platform, coreOperator, delivery, tenantOperator)
+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) error {
+	h := newHandler(clusterOperator, op, leaseOperator, platform, coreOperator, delivery, tokenOperator, tenantOperator)
 	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 f0eb111..dfd1059 100644
--- a/pkg/apis/core/v1/utils.go
+++ b/pkg/apis/core/v1/utils.go
@@ -27,6 +27,7 @@ import (
 	"k8s.io/apimachinery/pkg/util/sets"
 
 	"github.com/google/uuid"
+	clientcmdapi "k8s.io/client-go/tools/clientcmd/api"
 
 	"github.com/kubeclipper/kubeclipper/pkg/models/cluster"
 
@@ -402,3 +403,37 @@ func (h *handler) parseAddonStep(ctx context.Context, clu *v1.Cluster, addons []
 	clu.ContainerRuntime.InsecureRegistry = registry.List()
 	return steps, nil
 }
+
+// CreateWithToken creates a KubeConfig object with access to the API server with a token
+// Copy from  k8s.io/kubernetes/cmd/kubeadm/app/util/kubeconfig/kubeconfig.go
+func CreateWithToken(serverURL, clusterName, userName string, caCert []byte, token string) *clientcmdapi.Config {
+	config := CreateBasic(serverURL, clusterName, userName, caCert)
+	config.AuthInfos[userName] = &clientcmdapi.AuthInfo{
+		Token: token,
+	}
+	return config
+}
+
+// CreateBasic creates a basic, general KubeConfig object that then can be extended
+// Copy from  k8s.io/kubernetes/cmd/kubeadm/app/util/kubeconfig/kubeconfig.go
+func CreateBasic(serverURL, clusterName, userName string, caCert []byte) *clientcmdapi.Config {
+	// Use the cluster and the username as the context name
+	contextName := fmt.Sprintf("%s@%s", userName, clusterName)
+
+	return &clientcmdapi.Config{
+		Clusters: map[string]*clientcmdapi.Cluster{
+			clusterName: {
+				Server:                   serverURL,
+				CertificateAuthorityData: caCert,
+			},
+		},
+		Contexts: map[string]*clientcmdapi.Context{
+			contextName: {
+				Cluster:  clusterName,
+				AuthInfo: userName,
+			},
+		},
+		AuthInfos:      map[string]*clientcmdapi.AuthInfo{},
+		CurrentContext: contextName,
+	}
+}
diff --git a/pkg/apis/iam/v1/handler.go b/pkg/apis/iam/v1/handler.go
index 1300db9..1e6e2f3 100644
--- a/pkg/apis/iam/v1/handler.go
+++ b/pkg/apis/iam/v1/handler.go
@@ -1123,7 +1123,7 @@ func (h *handler) updateRoleBinding(ctx context.Context, operator authuser.Info,
 		ResourceRequest: true,
 		Path:            "",
 	}
-	decision, _, err := h.authz.Authorize(authRecord)
+	decision, _, err := h.authz.Authorize(ctx, authRecord)
 	if err != nil {
 		return err
 	}
@@ -1222,7 +1222,7 @@ func (h *handler) UpdateUserPassword(request *restful.Request, response *restful
 		Path:            "",
 	}
 
-	decision, _, err := h.authz.Authorize(authRecord)
+	decision, _, err := h.authz.Authorize(request.Request.Context(), authRecord)
 	if err != nil {
 		restplus.HandleInternalError(response, request, err)
 		return
diff --git a/pkg/apis/proxy/handler.go b/pkg/apis/proxy/handler.go
new file mode 100644
index 0000000..7002ea0
--- /dev/null
+++ b/pkg/apis/proxy/handler.go
@@ -0,0 +1,171 @@
+/*
+ *
+ *  * Copyright 2021 KubeClipper Authors.
+ *  *
+ *  * Licensed under the Apache License, Version 2.0 (the "License");
+ *  * you may not use this file except in compliance with the License.
+ *  * You may obtain a copy of the License at
+ *  *
+ *  *     http://www.apache.org/licenses/LICENSE-2.0
+ *  *
+ *  * Unless required by applicable law or agreed to in writing, software
+ *  * distributed under the License is distributed on an "AS IS" BASIS,
+ *  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ *  * See the License for the specific language governing permissions and
+ *  * limitations under the License.
+ *
+ */
+
+package proxy
+
+import (
+	"fmt"
+	"net"
+	"net/http"
+	"net/url"
+	"strings"
+	"time"
+
+	apimachineryErrors "k8s.io/apimachinery/pkg/api/errors"
+	utilnet "k8s.io/apimachinery/pkg/util/net"
+	"k8s.io/apimachinery/pkg/util/proxy"
+	"k8s.io/client-go/rest"
+	"k8s.io/client-go/tools/clientcmd"
+	"k8s.io/client-go/transport"
+
+	"github.com/kubeclipper/kubeclipper/pkg/models/cluster"
+)
+
+const (
+	ProxyHandlerPrefix = "/cluster/"
+)
+
+type handler struct {
+	clusterReader cluster.ClusterReader
+}
+
+func (h *handler) ServeHTTP(writer http.ResponseWriter, request *http.Request) {
+	clusterName, k8sResource := parseClusterFromPath(request.URL.Path)
+	if clusterName == "" {
+		http.Error(writer, "cluster name cannot be empty", http.StatusBadRequest)
+		return
+	}
+
+	c, err := h.clusterReader.GetClusterEx(request.Context(), clusterName, "0")
+	if err != nil {
+		if apimachineryErrors.IsNotFound(err) {
+			http.Error(writer, fmt.Sprintf("cluster %s not exists", clusterName), http.StatusNotFound)
+		} else {
+			http.Error(writer, err.Error(), http.StatusInternalServerError)
+		}
+		return
+	}
+	if len(c.KubeConfig) == 0 {
+		http.Error(writer, fmt.Sprintf("cluster %s not init", clusterName), http.StatusBadRequest)
+		return
+	}
+
+	restConfig, err := clientcmd.RESTConfigFromKubeConfig(c.KubeConfig)
+	if err != nil {
+		http.Error(writer, err.Error(), http.StatusInternalServerError)
+		return
+	}
+	request.URL.Path = k8sResource
+	request.Header.Del("Authorization")
+
+	proxyHandle, err := newClusterProxyHandler(restConfig, request.URL)
+	if err != nil {
+		http.Error(writer, err.Error(), http.StatusInternalServerError)
+		return
+	}
+	proxyHandle.ServeHTTP(writer, request)
+}
+
+func newHandler(clusterReader cluster.ClusterReader) *handler {
+	return &handler{
+		clusterReader: clusterReader,
+	}
+}
+
+func newClusterProxyHandler(restConfig *rest.Config, requestURL *url.URL) (http.Handler, error) {
+	clusterURL, err := toClusterURL(restConfig.Host, requestURL)
+	if err != nil {
+		return nil, err
+	}
+	restTransport, err := rest.TransportFor(restConfig)
+	if err != nil {
+		return nil, err
+	}
+	upgrade, err := makeUpgradeTransport(restConfig, 0)
+	if err != nil {
+		return nil, err
+	}
+	h := proxy.NewUpgradeAwareHandler(clusterURL, restTransport, true, false, nil)
+	h.UpgradeTransport = upgrade
+	return h, nil
+}
+
+func toClusterURL(restHost string, requestURL *url.URL) (*url.URL, error) {
+	if requestURL == nil {
+		return nil, fmt.Errorf("request url is nil")
+	}
+	if !strings.HasPrefix(strings.ToLower(restHost), "http") {
+		restHost = "http://" + restHost
+	}
+	u, err := url.Parse(restHost)
+	if err != nil {
+		return nil, fmt.Errorf("parse restHost:%w", err)
+	}
+	if requestURL != nil {
+		// without host and scheme
+		u.Opaque = requestURL.Opaque
+		u.User = requestURL.User
+		u.Path = requestURL.Path
+		u.RawPath = requestURL.RawPath
+		u.OmitHost = requestURL.OmitHost
+		u.ForceQuery = requestURL.ForceQuery
+		u.RawQuery = requestURL.RawQuery
+		u.Fragment = requestURL.Fragment
+		u.RawFragment = requestURL.RawFragment
+	}
+	return u, nil
+}
+
+// copy from k8s.io/kubectl/pkg/proxy/proxy_server.go
+// makeUpgradeTransport creates a transport that explicitly bypasses HTTP2 support
+// for proxy connections that must upgrade.
+func makeUpgradeTransport(config *rest.Config, keepalive time.Duration) (proxy.UpgradeRequestRoundTripper, error) {
+	transportConfig, err := config.TransportConfig()
+	if err != nil {
+		return nil, err
+	}
+	tlsConfig, err := transport.TLSConfigFor(transportConfig)
+	if err != nil {
+		return nil, err
+	}
+	rt := utilnet.SetOldTransportDefaults(&http.Transport{
+		TLSClientConfig: tlsConfig,
+		DialContext: (&net.Dialer{
+			Timeout:   30 * time.Second,
+			KeepAlive: keepalive,
+		}).DialContext,
+	})
+
+	upgrader, err := transport.HTTPWrappersForConfig(transportConfig, proxy.MirrorRequest)
+	if err != nil {
+		return nil, err
+	}
+	return proxy.NewUpgradeRequestRoundTripper(rt, upgrader), nil
+}
+
+func parseClusterFromPath(p string) (clusterName, res string) {
+	p = strings.TrimPrefix(p, ProxyHandlerPrefix)
+	items := strings.SplitN(p, "/", 2)
+	switch len(items) {
+	case 2:
+		return items[0], items[1]
+	case 1:
+		return items[0], ""
+	}
+	return "", ""
+}
diff --git a/pkg/apis/proxy/registry.go b/pkg/apis/proxy/registry.go
new file mode 100644
index 0000000..019e7f6
--- /dev/null
+++ b/pkg/apis/proxy/registry.go
@@ -0,0 +1,32 @@
+/*
+ *
+ *  * Copyright 2021 KubeClipper Authors.
+ *  *
+ *  * Licensed under the Apache License, Version 2.0 (the "License");
+ *  * you may not use this file except in compliance with the License.
+ *  * You may obtain a copy of the License at
+ *  *
+ *  *     http://www.apache.org/licenses/LICENSE-2.0
+ *  *
+ *  * Unless required by applicable law or agreed to in writing, software
+ *  * distributed under the License is distributed on an "AS IS" BASIS,
+ *  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ *  * See the License for the specific language governing permissions and
+ *  * limitations under the License.
+ *
+ */
+
+package proxy
+
+import (
+	"github.com/emicklei/go-restful"
+
+	"github.com/kubeclipper/kubeclipper/pkg/models/cluster"
+)
+
+func AddToContainer(c *restful.Container, clusterReader cluster.ClusterReader) error {
+	c.HandleWithFilter(ProxyHandlerPrefix, newHandler(clusterReader))
+	restful.DefaultResponseContentType(restful.MIME_JSON)
+	restful.DefaultRequestContentType(restful.MIME_JSON)
+	return nil
+}
diff --git a/pkg/apis/tenant/v1/handler.go b/pkg/apis/tenant/v1/handler.go
index 686281c..a090be7 100644
--- a/pkg/apis/tenant/v1/handler.go
+++ b/pkg/apis/tenant/v1/handler.go
@@ -252,7 +252,7 @@ func (h *handler) listProjects(ctx context.Context, reqUser user.Info, q *query.
 		ResourceScope:   request.GlobalScope,
 	}
 
-	decision, _, err := h.authorizer.Authorize(listWS)
+	decision, _, err := h.authorizer.Authorize(ctx, listWS)
 	if err != nil {
 		return nil, err
 	}
diff --git a/pkg/authorization/authorizer/interface.go b/pkg/authorization/authorizer/interface.go
index 155c8e2..6dad478 100644
--- a/pkg/authorization/authorizer/interface.go
+++ b/pkg/authorization/authorizer/interface.go
@@ -19,6 +19,7 @@
 package authorizer
 
 import (
+	"context"
 	"net/http"
 
 	"k8s.io/apiserver/pkg/authentication/user"
@@ -84,7 +85,7 @@ type Attributes interface {
 }
 
 type Authorizer interface {
-	Authorize(a Attributes) (authorized Decision, reason string, err error)
+	Authorize(ctx context.Context, a Attributes) (authorized Decision, reason string, err error)
 }
 
 type AuthorizerFunc func(a Attributes) (Decision, string, error)
diff --git a/pkg/authorization/rbac/rbac.go b/pkg/authorization/rbac/rbac.go
index e90b0b2..ee240f1 100644
--- a/pkg/authorization/rbac/rbac.go
+++ b/pkg/authorization/rbac/rbac.go
@@ -20,30 +20,22 @@ import (
 	"context"
 	"fmt"
 
-	v1 "github.com/kubeclipper/kubeclipper/pkg/scheme/common"
-	"github.com/kubeclipper/kubeclipper/pkg/server/request"
-
-	v12 "github.com/kubeclipper/kubeclipper/pkg/scheme/iam/v1"
-
 	"github.com/open-policy-agent/opa/rego"
 	"go.uber.org/zap"
-
-	"github.com/kubeclipper/kubeclipper/pkg/logger"
-
+	rbacv1 "k8s.io/api/rbac/v1"
 	"k8s.io/apimachinery/pkg/api/errors"
-
-	"k8s.io/apiserver/pkg/authentication/user"
-
-	"github.com/kubeclipper/kubeclipper/pkg/utils/sliceutil"
-
-	"github.com/kubeclipper/kubeclipper/pkg/query"
-
 	utilerrors "k8s.io/apimachinery/pkg/util/errors"
-
-	rbacv1 "k8s.io/api/rbac/v1"
+	"k8s.io/apiserver/pkg/authentication/user"
 
 	"github.com/kubeclipper/kubeclipper/pkg/authorization/authorizer"
+	"github.com/kubeclipper/kubeclipper/pkg/logger"
+	"github.com/kubeclipper/kubeclipper/pkg/models/cluster"
 	"github.com/kubeclipper/kubeclipper/pkg/models/iam"
+	"github.com/kubeclipper/kubeclipper/pkg/query"
+	"github.com/kubeclipper/kubeclipper/pkg/scheme/common"
+	iamv1 "github.com/kubeclipper/kubeclipper/pkg/scheme/iam/v1"
+	"github.com/kubeclipper/kubeclipper/pkg/server/request"
+	"github.com/kubeclipper/kubeclipper/pkg/utils/sliceutil"
 )
 
 const (
@@ -55,8 +47,8 @@ var (
 	_ authorizer.Authorizer = (*Authorizer)(nil)
 )
 
-func NewAuthorizer(am iam.Operator) authorizer.Authorizer {
-	return &Authorizer{am: am}
+func NewAuthorizer(am iam.Operator, cm cluster.Operator) authorizer.Authorizer {
+	return &Authorizer{am: am, cm: cm}
 }
 
 type authorizingVisitor struct {
@@ -86,12 +78,13 @@ func (v *authorizingVisitor) visit(source fmt.Stringer, regoPolicy string, rule
 
 type Authorizer struct {
 	am iam.Operator
+	cm cluster.Operator
 }
 
-func (a *Authorizer) Authorize(attributes authorizer.Attributes) (authorizer.Decision, string, error) {
+func (a *Authorizer) Authorize(ctx context.Context, attributes authorizer.Attributes) (authorizer.Decision, string, error) {
 	ruleCheckingVisitor := &authorizingVisitor{requestAttributes: attributes}
 
-	a.visitRulesFor(attributes, ruleCheckingVisitor.visit)
+	a.visitRulesFor(ctx, attributes, ruleCheckingVisitor.visit)
 
 	if ruleCheckingVisitor.allowed {
 		return authorizer.DecisionAllow, ruleCheckingVisitor.reason, nil
@@ -104,8 +97,8 @@ func (a *Authorizer) Authorize(attributes authorizer.Attributes) (authorizer.Dec
 	return authorizer.DecisionNoOpinion, reason, nil
 }
 
-func (a *Authorizer) visitRulesFor(requestAttributes authorizer.Attributes, visitor func(source fmt.Stringer, regoPolicy string, rule *rbacv1.PolicyRule, err error) bool) {
-	if globalRoleBindings, err := a.am.ListRoleBindings(context.TODO(), &query.Query{
+func (a *Authorizer) visitRulesFor(ctx context.Context, requestAttributes authorizer.Attributes, visitor func(source fmt.Stringer, regoPolicy string, rule *rbacv1.PolicyRule, err error) bool) {
+	if globalRoleBindings, err := a.am.ListRoleBindings(ctx, &query.Query{
 		Pagination:      query.NoPagination(),
 		ResourceVersion: "0",
 		LabelSelector:   "",
@@ -121,7 +114,7 @@ func (a *Authorizer) visitRulesFor(requestAttributes authorizer.Attributes, visi
 			if !applies {
 				continue
 			}
-			regoPolicy, rules, err := a.getRoleReferenceRules(globalRoleBinding.RoleRef)
+			regoPolicy, rules, err := a.getRoleReferenceRules(ctx, globalRoleBinding.RoleRef)
 			if err != nil {
 				visitor(nil, "", nil, err)
 				continue
@@ -140,11 +133,23 @@ func (a *Authorizer) visitRulesFor(requestAttributes authorizer.Attributes, visi
 	}
 
 	project := requestAttributes.GetProject()
+	if project == "" &&
+		requestAttributes.GetResource() == "cluster" &&
+		requestAttributes.GetSubresource() == "proxy" {
+		c, err := a.cm.GetClusterEx(ctx, requestAttributes.GetName(), "")
+		if err != nil && !visitor(nil, "", nil, err) {
+			return
+		}
+		if c != nil {
+			project = c.Labels[common.LabelProject]
+		}
+	}
+
 	if requestAttributes.GetResourceScope() == request.ProjectScope {
-		if projectRoleBindings, err := a.am.ListProjectRoleBinding(context.TODO(), &query.Query{
+		if projectRoleBindings, err := a.am.ListProjectRoleBinding(ctx, &query.Query{
 			Pagination:      query.NoPagination(),
 			ResourceVersion: "0",
-			LabelSelector:   fmt.Sprintf("%s=%s", v1.LabelProject, project),
+			LabelSelector:   fmt.Sprintf("%s=%s", common.LabelProject, project),
 			FieldSelector:   "",
 		}); err != nil {
 			if !visitor(nil, "", nil, err) {
@@ -157,7 +162,7 @@ func (a *Authorizer) visitRulesFor(requestAttributes authorizer.Attributes, visi
 				if !applies {
 					continue
 				}
-				regoPolicy, rules, err := a.getRoleReferenceRules(projectRoleBinding.RoleRef)
+				regoPolicy, rules, err := a.getRoleReferenceRules(ctx, projectRoleBinding.RoleRef)
 				if err != nil {
 					visitor(nil, "", nil, err)
 					continue
@@ -178,27 +183,27 @@ func (a *Authorizer) visitRulesFor(requestAttributes authorizer.Attributes, visi
 }
 
 // TODO: add am interface rather than iam.Operator
-func (a *Authorizer) getRoleReferenceRules(roleRef rbacv1.RoleRef) (regoPolicy string, rules []rbacv1.PolicyRule, err error) {
+func (a *Authorizer) getRoleReferenceRules(ctx context.Context, roleRef rbacv1.RoleRef) (regoPolicy string, rules []rbacv1.PolicyRule, err error) {
 	empty := make([]rbacv1.PolicyRule, 0)
 	switch roleRef.Kind {
-	case v1.ResourceKindGlobalRole:
-		globalRole, err := a.am.GetRoleEx(context.TODO(), roleRef.Name, "0")
+	case common.ResourceKindGlobalRole:
+		globalRole, err := a.am.GetRoleEx(ctx, roleRef.Name, "0")
 		if err != nil {
 			if errors.IsNotFound(err) {
 				return "", empty, nil
 			}
 			return "", nil, err
 		}
-		return globalRole.Annotations[v1.RegoOverrideAnnotation], globalRole.Rules, nil
-	case v1.ResourceKindProjectGlobalRole:
-		projectRole, err := a.am.GetProjectRoleEx(context.TODO(), roleRef.Name, "0")
+		return globalRole.Annotations[common.RegoOverrideAnnotation], globalRole.Rules, nil
+	case common.ResourceKindProjectGlobalRole:
+		projectRole, err := a.am.GetProjectRoleEx(ctx, roleRef.Name, "0")
 		if err != nil {
 			if errors.IsNotFound(err) {
 				return "", empty, nil
 			}
 			return "", nil, err
 		}
-		return projectRole.Annotations[v1.RegoOverrideAnnotation], projectRole.Rules, nil
+		return projectRole.Annotations[common.RegoOverrideAnnotation], projectRole.Rules, nil
 
 	default:
 		return "", nil, fmt.Errorf("unsupported role reference kind: %q", roleRef.Kind)
@@ -206,7 +211,7 @@ func (a *Authorizer) getRoleReferenceRules(roleRef rbacv1.RoleRef) (regoPolicy s
 }
 
 type globalRoleBindingDescriber struct {
-	binding *v12.GlobalRoleBinding
+	binding *iamv1.GlobalRoleBinding
 	subject *rbacv1.Subject
 }
 
@@ -220,7 +225,7 @@ func (d *globalRoleBindingDescriber) String() string {
 }
 
 type projectRoleBindingDescriber struct {
-	binding *v12.ProjectRoleBinding
+	binding *iamv1.ProjectRoleBinding
 	subject *rbacv1.Subject
 }
 
diff --git a/pkg/clustermanage/kubeadm/provider.go b/pkg/clustermanage/kubeadm/provider.go
index 6623728..d122353 100644
--- a/pkg/clustermanage/kubeadm/provider.go
+++ b/pkg/clustermanage/kubeadm/provider.go
@@ -126,6 +126,11 @@ func (r *Kubeadm) Sync(ctx context.Context) error {
 	if err != nil {
 		return err
 	}
+
+	if r.Provider.Labels[common.LabelProject] != "" {
+		clu.Labels[common.LabelProject] = r.Provider.Labels[common.LabelProject]
+	}
+
 	err = r.patch(clu)
 	if err != nil {
 		return err
diff --git a/pkg/component/common/image.go b/pkg/component/common/image.go
new file mode 100644
index 0000000..8805938
--- /dev/null
+++ b/pkg/component/common/image.go
@@ -0,0 +1,118 @@
+package common
+
+import (
+	"context"
+	"encoding/json"
+	"fmt"
+	"runtime"
+	"time"
+
+	"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"
+	"go.uber.org/zap"
+	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
+)
+
+const (
+	imageName  = "image"
+	AgentImage = "AgentImager"
+)
+
+func init() {
+	if err := component.RegisterAgentStep(fmt.Sprintf(component.RegisterStepKeyFormat, imageName, version, AgentImage), &Imager{}); err != nil {
+		panic(err)
+	}
+}
+
+type Imager struct {
+	PkgName string `json:"pkgName"`
+	Version string `json:"version"`
+	Offline bool   `json:"offline"`
+	CriName string `json:"criName"`
+	// Optional. If the value of the change field is not empty, the DownloadCustomImages and RemoveCustomImages operations will be performed
+	CustomImageList []string `json:"customConfig"`
+}
+
+func (i *Imager) 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
+	}
+
+	var dstFiles []string
+
+	if len(i.CustomImageList) > 0 {
+		dstFiles, err = instance.DownloadCustomImages(i.CustomImageList...)
+		if err != nil {
+			return nil, fmt.Errorf("download %s-%s custom image failed: %v", i.PkgName, i.Version, err)
+		}
+	} else {
+		dstFile, err := instance.DownloadImages()
+		if err != nil {
+			return nil, fmt.Errorf("download %s-%s image failed: %v", i.PkgName, i.Version, err)
+		}
+		dstFiles = []string{dstFile}
+	}
+
+	// load image package
+	for _, dstFile := range dstFiles {
+		if err := utils.LoadImage(ctx, opts.DryRun, dstFile, i.CriName); err != nil {
+			return nil, fmt.Errorf("load %s-%s image failed: %v", i.PkgName, i.Version, err)
+		}
+	}
+
+	logger.Infof("%s-%s image packages offline install successfully", i.PkgName, i.Version)
+	return nil, err
+}
+
+func (i *Imager) Uninstall(ctx context.Context, opts component.Options) ([]byte, error) {
+	instance, err := downloader.NewInstance(ctx, imageName, i.Version, runtime.GOARCH, !i.Offline, opts.DryRun)
+	if err != nil {
+		return nil, err
+	}
+
+	if len(i.CustomImageList) > 0 {
+		if err = instance.RemoveCustomImages(i.CustomImageList...); err != nil {
+			logger.Errorf("remove %s-%s custom-images compressed file failed", i.PkgName, i.Version, zap.Error(err))
+		}
+	} else {
+		if err = instance.RemoveImages(); err != nil {
+			logger.Errorf("remove %s-%s images compressed file failed", i.PkgName, i.Version, zap.Error(err))
+		}
+	}
+
+	return nil, nil
+}
+
+func (i *Imager) NewInstance() component.ObjectMeta {
+	return &Imager{}
+}
+
+func (i *Imager) 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-imageLoad", i.PkgName),
+			Timeout:    metav1.Duration{Duration: 30 * time.Minute},
+			ErrIgnore:  false,
+			RetryTimes: 0,
+			Nodes:      utils.UnwrapNodeList(nodeList),
+			Action:     v1.ActionInstall,
+			Commands: []v1.Command{
+				{
+					Type:          v1.CommandCustom,
+					Identity:      fmt.Sprintf(component.RegisterTemplateKeyFormat, imageName, version, AgentImage),
+					CustomCommand: customCommand,
+				},
+			},
+		},
+	}, nil
+}
diff --git a/pkg/component/nfs/nfs.go b/pkg/component/nfs/nfs.go
index 840abaf..5ba280f 100644
--- a/pkg/component/nfs/nfs.go
+++ b/pkg/component/nfs/nfs.go
@@ -26,13 +26,10 @@ import (
 	"io"
 	"os"
 	"path/filepath"
-	"runtime"
 	"time"
 
 	"github.com/kubeclipper/kubeclipper/pkg/component/common"
 
-	"go.uber.org/zap"
-
 	"github.com/nicksnyder/go-i18n/v2/i18n"
 
 	"github.com/kubeclipper/kubeclipper/pkg/utils/strutil"
@@ -42,9 +39,7 @@ import (
 	"github.com/kubeclipper/kubeclipper/pkg/component"
 	"github.com/kubeclipper/kubeclipper/pkg/component/utils"
 	"github.com/kubeclipper/kubeclipper/pkg/component/validation"
-	"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/fileutil"
 	"github.com/kubeclipper/kubeclipper/pkg/utils/netutil"
 	tmplutil "github.com/kubeclipper/kubeclipper/pkg/utils/template"
@@ -60,9 +55,6 @@ func init() {
 		panic(err)
 	}
 
-	if err := component.RegisterAgentStep(fmt.Sprintf(component.RegisterTemplateKeyFormat, name, version, AgentImageLoader), &ImageLoader{}); err != nil {
-		panic(err)
-	}
 	if err := initI18nForComponentMeta(); err != nil {
 		panic(err)
 	}
@@ -71,7 +63,6 @@ func init() {
 var (
 	_ component.Interface      = (*NFSProvisioner)(nil)
 	_ component.TemplateRender = (*NFSProvisioner)(nil)
-	_ component.StepRunnable   = (*ImageLoader)(nil)
 )
 
 const (
@@ -175,31 +166,17 @@ func (n *NFSProvisioner) InitSteps(ctx context.Context) error {
 	}
 	if metadata.Offline && n.ImageRepoMirror == "" {
 		// TODO: arch is unnecessary, version can be configured
-		imageloader := &ImageLoader{
+		imager := &common.Imager{
+			PkgName: nfs,
 			Version: "v4.0.2",
-			CriType: metadata.CRI,
+			CriName: metadata.CRI,
 			Offline: metadata.Offline,
 		}
-		iData, err := json.Marshal(imageloader)
+		steps, err := imager.InstallSteps(metadata.GetAllNodes())
 		if err != nil {
 			return err
 		}
-		n.installSteps = append(n.installSteps, v1.Step{
-			ID:         strutil.GetUUID(),
-			Name:       "imageLoader",
-			Timeout:    metav1.Duration{Duration: 5 * time.Minute},
-			ErrIgnore:  false,
-			RetryTimes: 1,
-			Nodes:      utils.UnwrapNodeList(metadata.GetAllNodes()),
-			Action:     v1.ActionInstall,
-			Commands: []v1.Command{
-				{
-					Type:          v1.CommandCustom,
-					Identity:      fmt.Sprintf(component.RegisterTemplateKeyFormat, name, version, AgentImageLoader),
-					CustomCommand: iData,
-				},
-			},
-		})
+		n.installSteps = append(n.installSteps, steps...)
 	}
 
 	bytes, err := json.Marshal(n)
@@ -479,41 +456,3 @@ func (n *NFSProvisioner) Render(ctx context.Context, opts component.Options) err
 func (n *NFSProvisioner) GetImageRepoMirror() string {
 	return n.ImageRepoMirror
 }
-
-type ImageLoader struct {
-	Version string
-	CriType string
-	Offline bool
-}
-
-func (n *ImageLoader) Install(ctx context.Context, opts component.Options) ([]byte, error) {
-	instance, err := downloader.NewInstance(ctx, nfs, n.Version, runtime.GOARCH, !n.Offline, opts.DryRun)
-	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, n.CriType); err == nil {
-		logger.Info("nfs packages offline install successfully")
-	}
-
-	return nil, err
-}
-
-func (n *ImageLoader) Uninstall(ctx context.Context, opts component.Options) ([]byte, error) {
-	instance, err := downloader.NewInstance(ctx, nfs, n.Version, runtime.GOARCH, !n.Offline, opts.DryRun)
-	if err != nil {
-		return nil, err
-	}
-	if err = instance.RemoveImages(); err != nil {
-		logger.Error("remove nfs images compressed file failed", zap.Error(err))
-	}
-	return nil, nil
-}
-
-func (n *ImageLoader) NewInstance() component.ObjectMeta {
-	return &ImageLoader{}
-}
diff --git a/pkg/component/nfscsi/nfs_csi.go b/pkg/component/nfscsi/nfs_csi.go
index e2651f0..b384294 100644
--- a/pkg/component/nfscsi/nfs_csi.go
+++ b/pkg/component/nfscsi/nfs_csi.go
@@ -8,21 +8,16 @@ import (
 	"io"
 	"os"
 	"path/filepath"
-	"runtime"
 	"time"
 
 	"github.com/nicksnyder/go-i18n/v2/i18n"
-	"go.uber.org/zap"
 	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
 
 	"github.com/kubeclipper/kubeclipper/pkg/component"
 	"github.com/kubeclipper/kubeclipper/pkg/component/common"
-	nfsprovisioner "github.com/kubeclipper/kubeclipper/pkg/component/nfs"
 	"github.com/kubeclipper/kubeclipper/pkg/component/utils"
 	"github.com/kubeclipper/kubeclipper/pkg/component/validation"
-	"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/fileutil"
 	"github.com/kubeclipper/kubeclipper/pkg/utils/netutil"
 	"github.com/kubeclipper/kubeclipper/pkg/utils/strutil"
@@ -58,9 +53,6 @@ func init() {
 		panic(err)
 	}
 
-	if err := component.RegisterAgentStep(fmt.Sprintf(component.RegisterTemplateKeyFormat, name, version, AgentImageLoader), &nfsprovisioner.ImageLoader{}); err != nil {
-		panic(err)
-	}
 	if err := initI18nForComponentMeta(); err != nil {
 		panic(err)
 	}
@@ -280,31 +272,17 @@ func (n *NFS) InitSteps(ctx context.Context) error {
 	}
 	if metadata.Offline && n.ImageRepoMirror == "" {
 		// TODO: arch is unnecessary, version can be configured
-		imageloader := &ImageLoader{
+		imager := &common.Imager{
+			PkgName: nfs,
 			Version: "v4.1.0",
-			CriType: metadata.CRI,
+			CriName: metadata.CRI,
 			Offline: metadata.Offline,
 		}
-		iData, err := json.Marshal(imageloader)
+		steps, err := imager.InstallSteps(metadata.GetAllNodes())
 		if err != nil {
 			return err
 		}
-		n.installSteps = append(n.installSteps, v1.Step{
-			ID:         strutil.GetUUID(),
-			Name:       "imageLoader",
-			Timeout:    metav1.Duration{Duration: 5 * time.Minute},
-			ErrIgnore:  false,
-			RetryTimes: 1,
-			Nodes:      utils.UnwrapNodeList(metadata.GetAllNodes()),
-			Action:     v1.ActionInstall,
-			Commands: []v1.Command{
-				{
-					Type:          v1.CommandCustom,
-					Identity:      fmt.Sprintf(component.RegisterTemplateKeyFormat, name, version, AgentImageLoader),
-					CustomCommand: iData,
-				},
-			},
-		})
+		n.installSteps = append(n.installSteps, steps...)
 	}
 	bytes, err := json.Marshal(n)
 	if err != nil {
@@ -405,44 +383,6 @@ func (n *NFS) GetImageRepoMirror() string {
 	return n.ImageRepoMirror
 }
 
-type ImageLoader struct {
-	Version string
-	CriType string
-	Offline bool
-}
-
-func (n *ImageLoader) Install(ctx context.Context, opts component.Options) ([]byte, error) {
-	instance, err := downloader.NewInstance(ctx, nfs, n.Version, runtime.GOARCH, !n.Offline, opts.DryRun)
-	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, n.CriType); err == nil {
-		logger.Info("nfs packages offline install successfully")
-	}
-
-	return nil, err
-}
-
-func (n *ImageLoader) Uninstall(ctx context.Context, opts component.Options) ([]byte, error) {
-	instance, err := downloader.NewInstance(ctx, nfs, n.Version, runtime.GOARCH, !n.Offline, opts.DryRun)
-	if err != nil {
-		return nil, err
-	}
-	if err = instance.RemoveImages(); err != nil {
-		logger.Error("remove nfs images compressed file failed", zap.Error(err))
-	}
-	return nil, nil
-}
-
-func (n *ImageLoader) NewInstance() component.ObjectMeta {
-	return &ImageLoader{}
-}
-
 func initI18nForComponentMeta() error {
 	return component.AddI18nMessages(component.I18nMessages{
 		{
diff --git a/pkg/controller/cronbackupcontroller/controller.go b/pkg/controller/cronbackupcontroller/controller.go
index 8780c6f..6b1e5f2 100644
--- a/pkg/controller/cronbackupcontroller/controller.go
+++ b/pkg/controller/cronbackupcontroller/controller.go
@@ -130,7 +130,7 @@ func (r *CronBackupReconciler) Reconcile(ctx context.Context, req ctrl.Request)
 	if cronBackup.Spec.Schedule != "" {
 		// first created cron backup, update next schedule time
 		if cronBackup.Status.NextScheduleTime == nil {
-			schedule := r.parseSchedule(cronBackup.Spec.Schedule)
+			schedule := r.ParseSchedule(cronBackup.Spec.Schedule)
 			s, _ := cron.NewParser(4 | 8 | 16 | 32 | 64).Parse(schedule)
 			// update the next schedule time
 			nextRunAt := metav1.NewTime(s.Next(time.Now()))
@@ -158,7 +158,7 @@ func (r *CronBackupReconciler) Reconcile(ctx context.Context, req ctrl.Request)
 			cronBackup.Status.LastScheduleTime = &now
 			cronBackup.Status.LastSuccessfulTime = cronBackup.Status.LastScheduleTime
 
-			schedule := r.parseSchedule(cronBackup.Spec.Schedule)
+			schedule := r.ParseSchedule(cronBackup.Spec.Schedule)
 			s, _ := cron.NewParser(4 | 8 | 16 | 32 | 64).Parse(schedule)
 			// update the next schedule time
 			nextRunAt := metav1.NewTime(s.Next(now.Time))
@@ -540,7 +540,7 @@ func (r *CronBackupReconciler) deleteBackup(log logger.Logging, clusterName stri
 	return nil
 }
 
-func (r *CronBackupReconciler) parseSchedule(schedule string) string {
+func (r *CronBackupReconciler) ParseSchedule(schedule string) string {
 	currentTime := r.Now()
 	arr := strings.Split(schedule, " ")
 	if arr[2] == "L" {
diff --git a/pkg/controller/projectcontroller/controller.go b/pkg/controller/projectcontroller/controller.go
index c088c75..dd374b9 100644
--- a/pkg/controller/projectcontroller/controller.go
+++ b/pkg/controller/projectcontroller/controller.go
@@ -31,8 +31,13 @@ import (
 	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
 	"k8s.io/apimachinery/pkg/labels"
 	"k8s.io/apimachinery/pkg/selection"
+	"k8s.io/apimachinery/pkg/types"
 	"k8s.io/apimachinery/pkg/util/sets"
 
+	"github.com/kubeclipper/kubeclipper/pkg/clustermanage/kubeadm"
+	"github.com/kubeclipper/kubeclipper/pkg/controller-runtime/client"
+	"github.com/kubeclipper/kubeclipper/pkg/controller-runtime/reconcile"
+
 	"github.com/kubeclipper/kubeclipper/pkg/models/iam"
 	"github.com/kubeclipper/kubeclipper/pkg/query"
 	iamv1 "github.com/kubeclipper/kubeclipper/pkg/scheme/iam/v1"
@@ -88,12 +93,56 @@ func (r *ProjectReconciler) SetupWithManager(mgr manager.Manager, cache informer
 	if err = c.Watch(source.NewKindWithCache(&tenantv1.Project{}, cache), &handler.EnqueueRequestForObject{}); err != nil {
 		return err
 	}
+	if err = c.Watch(source.NewKindWithCache(&v1.Cluster{}, cache), handler.EnqueueRequestsFromMapFunc(r.findObjectsFromCluster)); err != nil {
+		return err
+	}
+	if err = c.Watch(source.NewKindWithCache(&v1.Node{}, cache), handler.EnqueueRequestsFromMapFunc(r.findObjectsFromNode)); err != nil {
+		return err
+	}
 
 	mgr.AddRunnable(c)
 	return nil
 }
 
 // Reconcile core login of controller.
+func (r *ProjectReconciler) findObjectsFromCluster(clu client.Object) []reconcile.Request {
+	// just watch cluster from kubeadm provider
+	if clu.GetLabels()[common.LabelClusterProviderType] != kubeadm.ProviderKubeadm {
+		return []reconcile.Request{}
+	}
+	// if cluster deleted,ignore
+	if !clu.GetDeletionTimestamp().IsZero() {
+		return []reconcile.Request{}
+	}
+	projectName := clu.GetLabels()[common.LabelProject]
+	project, err := r.ProjectLister.Get(projectName)
+	if err != nil {
+		return []reconcile.Request{}
+	}
+	return []reconcile.Request{
+		{NamespacedName: types.NamespacedName{
+			Name: project.Name,
+		}},
+	}
+}
+
+func (r *ProjectReconciler) findObjectsFromNode(clu client.Object) []reconcile.Request {
+	// just watch delete event
+	if clu.GetDeletionTimestamp().IsZero() {
+		return []reconcile.Request{}
+	}
+	project := clu.GetLabels()[common.LabelProject]
+	// just watch node which joined project
+	if project == "" {
+		return []reconcile.Request{}
+	}
+	return []reconcile.Request{
+		{NamespacedName: types.NamespacedName{
+			Name: project,
+		}},
+	}
+}
+
 func (r *ProjectReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) {
 	log := logger.FromContext(ctx)
 
@@ -149,6 +198,16 @@ func (r *ProjectReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ct
 		return ctrl.Result{}, err
 	}
 
+	if err = r.addClusterNode(ctx, project); err != nil {
+		log.Error("failed to sync cluster node", zap.Error(err))
+		return ctrl.Result{}, err
+	}
+
+	if err = r.syncSpecNode(ctx, project); err != nil {
+		log.Error("failed to sync spec node", zap.Error(err))
+		return ctrl.Result{}, err
+	}
+
 	if err = r.syncNodeLabel(ctx, project); err != nil {
 		log.Error("failed to sync node label", zap.Error(err))
 		return ctrl.Result{}, err
@@ -162,6 +221,66 @@ func (r *ProjectReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ct
 	return ctrl.Result{}, nil
 }
 
+// addClusterNode add kubeadm provider cluster's node to project.
+func (r *ProjectReconciler) addClusterNode(ctx context.Context, p *tenantv1.Project) error {
+	log := logger.FromContext(ctx)
+	log.Info("addClusterNode")
+
+	selector := labels.NewSelector()
+	requirement, err := labels.NewRequirement(common.LabelProject, selection.Equals, []string{p.Name})
+	if err != nil {
+		return err
+	}
+	selector.Add(*requirement)
+	requirement2, err := labels.NewRequirement(common.LabelClusterProviderType, selection.Equals, []string{kubeadm.ProviderKubeadm})
+	if err != nil {
+		return err
+	}
+	selector.Add(*requirement2)
+	clusters, err := r.ClusterLister.List(selector)
+	if err != nil {
+		return err
+	}
+	set := sets.NewString(p.Spec.Nodes...)
+	for _, clu := range clusters {
+		nodes := clu.GetAllNodes()
+		set.Insert(nodes.List()...)
+	}
+
+	if !set.Equal(sets.NewString(p.Spec.Nodes...)) {
+		p.Spec.Nodes = set.List()
+		if _, err = r.ProjectWriter.UpdateProject(ctx, p); err != nil {
+			return err
+		}
+	}
+
+	return nil
+}
+
+// syncSpecNode update spec.Nodes if node deleted
+func (r *ProjectReconciler) syncSpecNode(ctx context.Context, p *tenantv1.Project) error {
+	del := sets.NewString()
+	for _, node := range p.Spec.Nodes {
+		if _, err := r.NodeLister.Get(node); err != nil {
+			if errors.IsNotFound(err) {
+				del.Insert(node)
+			} else {
+				return err
+			}
+		}
+	}
+	if del.Len() == 0 {
+		return nil
+	}
+	set := sets.NewString(p.Spec.Nodes...).Delete(del.List()...)
+	p.Spec.Nodes = set.List()
+
+	if _, err := r.ProjectWriter.UpdateProject(ctx, p); err != nil {
+		return err
+	}
+	return nil
+}
+
 func (r *ProjectReconciler) initProjectRole(ctx context.Context, p *tenantv1.Project) error {
 	log := logger.FromContext(ctx)
 	log.Info("initProjectRole")
diff --git a/pkg/models/cluster/cluster.go b/pkg/models/cluster/cluster.go
index 628aef7..50c2c24 100644
--- a/pkg/models/cluster/cluster.go
+++ b/pkg/models/cluster/cluster.go
@@ -202,7 +202,7 @@ func (c *clusterOperator) GetRegion(ctx context.Context, name string) (*v1.Regio
 }
 
 func (c *clusterOperator) ListRegionEx(ctx context.Context, query *query.Query) (*models.PageableResponse, error) {
-	return models.ListExV2(ctx, c.regionStorage, query, c.regionFuzzyFilter, nil, nil)
+	return models.ListExV2(ctx, c.regionStorage, query, RegionFuzzyFilter, nil, nil)
 }
 
 func (c *clusterOperator) GetRegionEx(ctx context.Context, name string, resourceVersion string) (*v1.Region, error) {
@@ -571,7 +571,7 @@ func nodeCustomFilter(node *v1.Node, key, value string) bool {
 	return true
 }
 
-func (c *clusterOperator) regionFuzzyFilter(obj runtime.Object, q *query.Query) []runtime.Object {
+func RegionFuzzyFilter(obj runtime.Object, q *query.Query) []runtime.Object {
 	regions, ok := obj.(*v1.RegionList)
 	if !ok {
 		return nil
diff --git a/pkg/scheme/core/v1/k8s/node.go b/pkg/scheme/core/v1/k8s/node.go
index 2b0cfb6..018fc57 100644
--- a/pkg/scheme/core/v1/k8s/node.go
+++ b/pkg/scheme/core/v1/k8s/node.go
@@ -167,7 +167,7 @@ func (stepper *GenNode) MakeInstallSteps(metadata *component.ExtraMetadata, patc
 			stepList := newComp.GetInstallSteps()
 			for _, st := range stepList {
 				// TODO: Temporarily use hardcode to adjust, and subsequently optimize the step code for image download and load
-				if st.Name == "imageLoader" {
+				if strings.Contains(st.Name, "imageLoad") {
 					st.Nodes = patchNodes
 					stepper.installSteps = append(stepper.installSteps, st)
 					break
diff --git a/pkg/server/config.go b/pkg/server/config.go
index 78eb14b..62e459f 100644
--- a/pkg/server/config.go
+++ b/pkg/server/config.go
@@ -241,6 +241,11 @@ var Roles = []iamv1.GlobalRole{
 				Resources: []string{"clusters/terminal"},
 				Verbs:     []string{"get"},
 			},
+			{
+				APIGroups: []string{"core.kubeclipper.io"},
+				Resources: []string{"clusters/proxy"},
+				Verbs:     []string{"*"},
+			},
 		},
 	},
 	{
@@ -501,6 +506,11 @@ var Roles = []iamv1.GlobalRole{
 				Resources: []string{"templates"},
 				Verbs:     []string{"*"},
 			},
+			{
+				APIGroups: []string{"core.kubeclipper.io"},
+				Resources: []string{"clusters/proxy"},
+				Verbs:     []string{"*"},
+			},
 		},
 	},
 	{
@@ -1412,6 +1422,11 @@ var ProjectRolesTemplate = []iamv1.ProjectRole{
 				Resources: []string{"clusters/terminal"},
 				Verbs:     []string{"get"},
 			},
+			{
+				APIGroups: []string{"core.kubeclipper.io"},
+				Resources: []string{"clusters/proxy"},
+				Verbs:     []string{"*"},
+			},
 		},
 	},
 	{
@@ -1519,6 +1534,11 @@ var ProjectRolesTemplate = []iamv1.ProjectRole{
 				Resources: []string{"templates"},
 				Verbs:     []string{"*"},
 			},
+			{
+				APIGroups: []string{"core.kubeclipper.io"},
+				Resources: []string{"clusters/proxy"},
+				Verbs:     []string{"*"},
+			},
 		},
 	},
 	{
diff --git a/pkg/server/filters/authorization.go b/pkg/server/filters/authorization.go
index b400130..bbd8767 100644
--- a/pkg/server/filters/authorization.go
+++ b/pkg/server/filters/authorization.go
@@ -44,7 +44,7 @@ func WithAuthorization(authorizers authorizer.Authorizer) restful.FilterFunction
 			restplus.HandleInternalError(response, req, err)
 			return
 		}
-		authorized, reason, err := authorizers.Authorize(attributes)
+		authorized, reason, err := authorizers.Authorize(ctx, attributes)
 		if authorized == authorizer.DecisionAllow {
 			chain.ProcessFilter(req, response)
 			return
diff --git a/pkg/server/request/request.go b/pkg/server/request/request.go
index 8c0330e..d79e297 100644
--- a/pkg/server/request/request.go
+++ b/pkg/server/request/request.go
@@ -81,6 +81,10 @@ eg: /api/core.kubeclipper.io/v1/nodes/node1
 /api/{api-group}/{version}/projects/{project}/{resource}/{resourceName}
 eg: /api/core.kubeclipper.io/v1/projects/demo1/nodes/node1
 
+Kubernetes proxy paths
+/cluster/{cluster-name}/{k8s-resource}
+eg: /cluster/test-cluster/api/v1/namespaces
+
 NonResource paths
 /healthz
 /metrics
@@ -106,13 +110,6 @@ func (i *InfoFactory) NewRequestInfo(req *http.Request) (*Info, error) {
 		return &requestInfo, nil
 	}
 
-	requestInfo.APIPrefix = currentParts[0]
-	requestInfo.APIGroup = currentParts[1]
-	requestInfo.APIVersion = currentParts[2]
-	requestInfo.IsResourceRequest = true
-
-	currentParts = currentParts[3:]
-
 	switch req.Method {
 	case "POST":
 		requestInfo.Verb = "create"
@@ -128,6 +125,22 @@ func (i *InfoFactory) NewRequestInfo(req *http.Request) (*Info, error) {
 		requestInfo.Verb = ""
 	}
 
+	requestInfo.APIPrefix = currentParts[0]
+	switch requestInfo.APIPrefix {
+	case "api":
+		requestInfo.APIGroup = currentParts[1]
+		requestInfo.APIVersion = currentParts[2]
+		requestInfo.IsResourceRequest = true
+		currentParts = currentParts[3:]
+	case "cluster":
+		requestInfo.Name = currentParts[1]
+		requestInfo.APIGroup = "core.kubeclipper.io"
+		requestInfo.IsResourceRequest = true
+		requestInfo.Resource = "clusters"
+		requestInfo.Subresource = "proxy"
+		requestInfo.ResourceScope = ProjectScope
+		return &requestInfo, nil
+	}
 	// parsing successful, so we now know the proper value for .Parts
 	// requestInfo.Parts = currentParts
 	// URL forms: /projects/{project}/*
diff --git a/pkg/server/request/response.go b/pkg/server/request/response.go
new file mode 100644
index 0000000..725b8fc
--- /dev/null
+++ b/pkg/server/request/response.go
@@ -0,0 +1,1 @@
+package request
diff --git a/pkg/server/server.go b/pkg/server/server.go
index 0b874c1..0aa6e5e 100644
--- a/pkg/server/server.go
+++ b/pkg/server/server.go
@@ -25,67 +25,57 @@ import (
 	"net/http"
 	"time"
 
-	"k8s.io/apimachinery/pkg/runtime/schema"
-
-	tenantv1 "github.com/kubeclipper/kubeclipper/pkg/apis/tenant/v1"
-	"github.com/kubeclipper/kubeclipper/pkg/controller/cloudprovidercontroller"
-	"github.com/kubeclipper/kubeclipper/pkg/controller/projectcontroller"
-	"github.com/kubeclipper/kubeclipper/pkg/models/core"
-	"github.com/kubeclipper/kubeclipper/pkg/models/tenant"
-
-	"github.com/kubeclipper/kubeclipper/pkg/controller/cronbackupcontroller"
-
-	corev1 "github.com/kubeclipper/kubeclipper/pkg/apis/core/v1"
-	"github.com/kubeclipper/kubeclipper/pkg/authentication/mfa"
-	"github.com/kubeclipper/kubeclipper/pkg/controller/tokencontroller"
-	"github.com/kubeclipper/kubeclipper/pkg/simple/client/cache"
-
+	"github.com/emicklei/go-restful"
 	"github.com/google/uuid"
-
-	"github.com/kubeclipper/kubeclipper/pkg/controller/regioncontroller"
-
-	"github.com/kubeclipper/kubeclipper/pkg/controller"
-	"github.com/kubeclipper/kubeclipper/pkg/controller-runtime/manager"
-	"github.com/kubeclipper/kubeclipper/pkg/controller/backupcontroller"
-	"github.com/kubeclipper/kubeclipper/pkg/controller/clustercontroller"
-	"github.com/kubeclipper/kubeclipper/pkg/controller/dnscontroller"
-	"github.com/kubeclipper/kubeclipper/pkg/controller/nodecontroller"
-	"github.com/kubeclipper/kubeclipper/pkg/controller/operationcontroller"
-
-	v1 "github.com/kubeclipper/kubeclipper/pkg/scheme/iam/v1"
-	schemetenantv1 "github.com/kubeclipper/kubeclipper/pkg/scheme/tenant/v1"
-
-	"github.com/kubeclipper/kubeclipper/pkg/authentication/request/internaltoken"
-
+	"go.uber.org/zap"
+	"k8s.io/apimachinery/pkg/runtime/schema"
 	"k8s.io/apimachinery/pkg/util/sets"
 	unionauth "k8s.io/apiserver/pkg/authentication/request/union"
 	etcdRESTOptions "k8s.io/apiserver/pkg/server/options"
 	"k8s.io/component-base/version"
 
-	"github.com/emicklei/go-restful"
-	"go.uber.org/zap"
-
 	auditingv1 "github.com/kubeclipper/kubeclipper/pkg/apis/auditing/v1"
 	configv1 "github.com/kubeclipper/kubeclipper/pkg/apis/config/v1"
+	corev1 "github.com/kubeclipper/kubeclipper/pkg/apis/core/v1"
 	iamv1 "github.com/kubeclipper/kubeclipper/pkg/apis/iam/v1"
 	"github.com/kubeclipper/kubeclipper/pkg/apis/oauth"
+	"github.com/kubeclipper/kubeclipper/pkg/apis/proxy"
+	tenantv1 "github.com/kubeclipper/kubeclipper/pkg/apis/tenant/v1"
 	"github.com/kubeclipper/kubeclipper/pkg/auditing"
 	"github.com/kubeclipper/kubeclipper/pkg/authentication/auth"
+	"github.com/kubeclipper/kubeclipper/pkg/authentication/mfa"
 	"github.com/kubeclipper/kubeclipper/pkg/authentication/request/anonymous"
 	"github.com/kubeclipper/kubeclipper/pkg/authentication/request/bearertoken"
+	"github.com/kubeclipper/kubeclipper/pkg/authentication/request/internaltoken"
 	authnpath "github.com/kubeclipper/kubeclipper/pkg/authentication/request/path"
 	"github.com/kubeclipper/kubeclipper/pkg/authentication/request/wstoken"
 	"github.com/kubeclipper/kubeclipper/pkg/authorization/authorizer"
 	"github.com/kubeclipper/kubeclipper/pkg/authorization/rbac"
 	"github.com/kubeclipper/kubeclipper/pkg/client/informers"
+	"github.com/kubeclipper/kubeclipper/pkg/controller"
+	"github.com/kubeclipper/kubeclipper/pkg/controller-runtime/manager"
+	"github.com/kubeclipper/kubeclipper/pkg/controller/backupcontroller"
+	"github.com/kubeclipper/kubeclipper/pkg/controller/cloudprovidercontroller"
+	"github.com/kubeclipper/kubeclipper/pkg/controller/clustercontroller"
+	"github.com/kubeclipper/kubeclipper/pkg/controller/cronbackupcontroller"
+	"github.com/kubeclipper/kubeclipper/pkg/controller/dnscontroller"
+	"github.com/kubeclipper/kubeclipper/pkg/controller/nodecontroller"
+	"github.com/kubeclipper/kubeclipper/pkg/controller/operationcontroller"
+	"github.com/kubeclipper/kubeclipper/pkg/controller/projectcontroller"
+	"github.com/kubeclipper/kubeclipper/pkg/controller/regioncontroller"
+	"github.com/kubeclipper/kubeclipper/pkg/controller/tokencontroller"
 	"github.com/kubeclipper/kubeclipper/pkg/healthz"
 	"github.com/kubeclipper/kubeclipper/pkg/logger"
 	"github.com/kubeclipper/kubeclipper/pkg/models/cluster"
+	"github.com/kubeclipper/kubeclipper/pkg/models/core"
 	"github.com/kubeclipper/kubeclipper/pkg/models/iam"
 	"github.com/kubeclipper/kubeclipper/pkg/models/lease"
 	"github.com/kubeclipper/kubeclipper/pkg/models/operation"
 	"github.com/kubeclipper/kubeclipper/pkg/models/platform"
+	"github.com/kubeclipper/kubeclipper/pkg/models/tenant"
 	"github.com/kubeclipper/kubeclipper/pkg/query"
+	v1 "github.com/kubeclipper/kubeclipper/pkg/scheme/iam/v1"
+	schemetenantv1 "github.com/kubeclipper/kubeclipper/pkg/scheme/tenant/v1"
 	"github.com/kubeclipper/kubeclipper/pkg/server/config"
 	"github.com/kubeclipper/kubeclipper/pkg/server/filters"
 	"github.com/kubeclipper/kubeclipper/pkg/server/registry"
@@ -93,6 +83,7 @@ import (
 	"github.com/kubeclipper/kubeclipper/pkg/service"
 	"github.com/kubeclipper/kubeclipper/pkg/service/delivery"
 	"github.com/kubeclipper/kubeclipper/pkg/service/staticresource"
+	"github.com/kubeclipper/kubeclipper/pkg/simple/client/cache"
 	"github.com/kubeclipper/kubeclipper/pkg/utils/hashutil"
 	"github.com/kubeclipper/kubeclipper/pkg/utils/metrics"
 )
@@ -194,7 +185,7 @@ func (s *APIServer) Run(stopCh <-chan struct{}) (err error) {
 
 func (s *APIServer) buildHandlerChain(stopCh <-chan struct{}) error {
 	infoFactory := &request.InfoFactory{
-		APIPrefixes: sets.NewString("api"),
+		APIPrefixes: sets.NewString("api", "cluster"),
 		GlobalResources: []schema.GroupResource{
 			schemetenantv1.Resource("projects"),
 		},
@@ -267,7 +258,7 @@ func (s *APIServer) installAPIs(stopCh <-chan struct{}) error {
 	opOperator := operation.NewOperationOperator(s.storageFactory.Operations())
 	iamOperator := iam.NewOperator(s.storageFactory.Users(), s.storageFactory.GlobalRoles(),
 		s.storageFactory.GlobalRoleBindings(), s.storageFactory.Tokens(), s.storageFactory.LoginRecords(), s.storageFactory.ProjectRole(), s.storageFactory.ProjectRoleBinding())
-	s.rbacAuthorizer = rbac.NewAuthorizer(iamOperator)
+	s.rbacAuthorizer = rbac.NewAuthorizer(iamOperator, clusterOperator)
 
 	deliverySvc := delivery.NewService(s.Config.MQOptions, clusterOperator, leaseOperator, opOperator)
 	s.Services = append(s.Services, deliverySvc)
@@ -313,7 +304,11 @@ func (s *APIServer) installAPIs(stopCh <-chan struct{}) error {
 		return err
 	}
 	s.Services = append(s.Services, ctrl)
-	if err = corev1.AddToContainer(s.container, clusterOperator, opOperator, platformOperator, leaseOperator, coreOperator, deliverySvc, tenantOperator); err != nil {
+
+	if err = corev1.AddToContainer(s.container, clusterOperator, opOperator, platformOperator, leaseOperator, coreOperator, deliverySvc, tokenOperator, tenantOperator); err != nil {
+		return err
+	}
+	if err = proxy.AddToContainer(s.container, clusterOperator); err != nil {
 		return err
 	}
 	staticResourceSvc, err := staticresource.NewService(s.Config.StaticServerOptions)
@@ -321,7 +316,6 @@ func (s *APIServer) installAPIs(stopCh <-chan struct{}) error {
 		return err
 	}
 	s.Services = append(s.Services, staticResourceSvc)
-
 	return nil
 }
 
diff --git a/pkg/simple/downloader/downloader.go b/pkg/simple/downloader/downloader.go
index 8661e3f..ced52e5 100644
--- a/pkg/simple/downloader/downloader.go
+++ b/pkg/simple/downloader/downloader.go
@@ -134,6 +134,26 @@ func (dl *Downloader) RemoveImages() error {
 	return os.RemoveAll(filepath.Join(dl.dstDir, ImageFilename))
 }
 
+// DownloadCustomImages download custom image file
+func (dl *Downloader) DownloadCustomImages(imageList ...string) (files []string, err error) {
+	for _, image := range imageList {
+		if err = dl.Download(image); err != nil {
+			return
+		}
+		files = append(files, filepath.Join(dl.dstDir, image))
+	}
+	return
+}
+
+// RemoveCustomImages remove custom image file
+func (dl *Downloader) RemoveCustomImages(imageList ...string) (err error) {
+	for _, v := range imageList {
+		// only the last error is recorded, can ignore it
+		err = os.RemoveAll(v)
+	}
+	return
+}
+
 // DownloadAll download config and image file
 func (dl *Downloader) DownloadAll() (cPath, iPath string, err error) {
 	cPath, err = dl.DownloadAndUnpackConfigs()
diff --git a/tools/doc-gen/main.go b/tools/doc-gen/main.go
index d2d0a5d..a62c265 100644
--- a/tools/doc-gen/main.go
+++ b/tools/doc-gen/main.go
@@ -27,6 +27,7 @@ import (
 	"os"
 
 	auditingv1 "github.com/kubeclipper/kubeclipper/pkg/apis/auditing/v1"
+	"github.com/kubeclipper/kubeclipper/pkg/apis/proxy"
 
 	"github.com/kubeclipper/kubeclipper/pkg/apis/oauth"
 
@@ -99,11 +100,12 @@ func validateSpec(apiSpec []byte) error {
 func generateSwaggerJSON() []byte {
 
 	container := restful.NewContainer()
-	urlruntime.Must(corev1.AddToContainer(container, nil, nil, nil, nil, nil, nil, nil))
+	urlruntime.Must(corev1.AddToContainer(container, 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))
 	urlruntime.Must(oauth.AddToContainer(container, nil, nil, nil, nil, nil, nil, nil))
+	urlruntime.Must(proxy.AddToContainer(container, nil))
 	urlruntime.Must(auditingv1.AddToContainer(container, nil))
 
 	config := restfulspec.Config{


Test patch:
diff --git a/pkg/apis/core/v1/utils_test.go b/pkg/apis/core/v1/utils_test.go
index 5b04ab2..33c7e2d 100644
--- a/pkg/apis/core/v1/utils_test.go
+++ b/pkg/apis/core/v1/utils_test.go
@@ -144,7 +144,7 @@ var (
 )
 
 func Test_parseOperationFromCluster(t *testing.T) {
-	h := newHandler(nil, nil, nil, nil, nil, nil, nil)
+	h := newHandler(nil, nil, nil, nil, nil, nil, nil, nil)
 	type args struct {
 		c      *v1.Cluster
 		meta   *component.ExtraMetadata
@@ -192,7 +192,7 @@ func Test_parseOperationFromComponent(t *testing.T) {
 		cluster    *v1.Cluster
 		components []v1.Addon
 	}
-	h := newHandler(nil, nil, nil, nil, nil, nil, nil)
+	h := newHandler(nil, nil, nil, nil, nil, nil, nil, nil)
 	nfs := nfsprovisioner.NFSProvisioner{
 		ManifestsDir:     "/tmp/.nfs",
 		Namespace:        "kube-system",
diff --git a/pkg/authorization/rbac/rbac_test.go b/pkg/authorization/rbac/rbac_test.go
index 07d1a79..3e614cf 100644
--- a/pkg/authorization/rbac/rbac_test.go
+++ b/pkg/authorization/rbac/rbac_test.go
@@ -19,6 +19,7 @@
 package rbac
 
 import (
+	"context"
 	"net/http"
 	"testing"
 
@@ -1515,7 +1516,7 @@ func TestAuthorizer_Authorize(t *testing.T) {
 				return
 			}
 
-			got, got1, err := authz.Authorize(getAuthorizerAttributes(tt.args.user, info))
+			got, got1, err := authz.Authorize(context.TODO(), getAuthorizerAttributes(tt.args.user, info))
 			if (err != nil) != tt.wantErr {
 				t.Errorf("Authorize() error = %v, wantErr %v", err, tt.wantErr)
 				return
diff --git a/pkg/controller/cronbackupcontroller/controller_test.go b/pkg/controller/cronbackupcontroller/controller_test.go
index 4b1bb68..0872b56 100644
--- a/pkg/controller/cronbackupcontroller/controller_test.go
+++ b/pkg/controller/cronbackupcontroller/controller_test.go
@@ -78,7 +78,7 @@ func TestCronBackupReconciler_parseSchedule(t *testing.T) {
 			s := &CronBackupReconciler{
 				Now: tt.fields.Now,
 			}
-			if got := s.parseSchedule(tt.args.schedule); got != tt.want {
+			if got := s.ParseSchedule(tt.args.schedule); got != tt.want {
 				t.Errorf("CronBackupReconciler.parseSchedule() = %v, want %v, %v", got, tt.want, tt.fields.Now().String())
 			}
 		})
diff --git a/test/e2e/cluster/cluster_ha.go b/test/e2e/cluster/cluster_ha.go
index 9755bb4..476f4bb 100644
--- a/test/e2e/cluster/cluster_ha.go
+++ b/test/e2e/cluster/cluster_ha.go
@@ -5,9 +5,10 @@ import (
 	"fmt"
 	"time"
 
-	"github.com/kubeclipper/kubeclipper/test/e2e/project"
 	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
 
+	"github.com/kubeclipper/kubeclipper/test/e2e/project"
+
 	"github.com/kubeclipper/kubeclipper/pkg/query"
 	"github.com/kubeclipper/kubeclipper/pkg/scheme/common"