This is an automated email from the ASF dual-hosted git repository.

wanggenhua pushed a commit to branch dev
in repository https://gitbox.apache.org/repos/asf/dolphinscheduler.git


The following commit(s) were added to refs/heads/dev by this push:
     new ff065d8e5b [Feature][Improvement] Support multi cluster environments - 
k8s type (#10096)
ff065d8e5b is described below

commit ff065d8e5b4ced0275878f8a8fda35ee4481637b
Author: qianli2022 <[email protected]>
AuthorDate: Wed Jun 15 13:39:20 2022 +0800

    [Feature][Improvement] Support multi cluster environments - k8s type 
(#10096)
    
    * service code
    
    * [Feature][UI] Add front-end for cluster manage
    
    * fix e2e
    
    * remove comment on cluster controller
    
    * doc
    
    * img
    
    * setting e2e.yaml
    
    * test
    
    * rerun e2e
    
    * fix bug from comment
    
    * Update index.tsx
    
    use Nspace instead of css.
    
    * Update index.tsx
    
    Remove the style.
    
    * Delete index.module.scss
    
    Remove the useless file.
    
    Co-authored-by: qianl4 <[email protected]>
    Co-authored-by: William Tong <[email protected]>
    Co-authored-by: Amy0104 <[email protected]>
---
 .github/workflows/e2e.yml                          |   2 +
 docs/docs/en/guide/security.md                     |  12 +
 docs/docs/zh/guide/security.md                     |  12 +
 docs/img/new_ui/dev/security/create-cluster.png    | Bin 0 -> 641597 bytes
 .../api/controller/ClusterController.java          | 236 +++++++++++++++
 .../dolphinscheduler/api/dto/ClusterDto.java       | 129 ++++++++
 .../apache/dolphinscheduler/api/enums/Status.java  |  18 ++
 .../dolphinscheduler/api/k8s/K8sClientService.java |  24 +-
 .../api/service/ClusterService.java                |  99 ++++++
 .../api/service/impl/ClusterServiceImpl.java       | 335 +++++++++++++++++++++
 .../api/controller/ClusterControllerTest.java      | 202 +++++++++++++
 .../api/service/ClusterServiceTest.java            | 290 ++++++++++++++++++
 .../common/utils/ClusterConfUtils.java             |  48 +++
 .../dolphinscheduler/dao/entity/Cluster.java       | 139 +++++++++
 .../dolphinscheduler/dao/mapper/ClusterMapper.java |  71 +++++
 .../dolphinscheduler/dao/mapper/ClusterMapper.xml  |  55 ++++
 .../src/main/resources/sql/dolphinscheduler_h2.sql |  23 ++
 .../main/resources/sql/dolphinscheduler_mysql.sql  |  19 ++
 .../resources/sql/dolphinscheduler_postgresql.sql  |  20 ++
 .../3.0.0_schema/mysql/dolphinscheduler_ddl.sql    |  20 +-
 .../postgresql/dolphinscheduler_ddl.sql            |  14 +
 .../dao/mapper/ClusterMapperTest.java              | 191 ++++++++++++
 .../dolphinscheduler/e2e/cases/ClusterE2ETest.java | 123 ++++++++
 .../e2e/pages/security/ClusterPage.java            | 151 ++++++++++
 .../e2e/pages/security/SecurityPage.java           |  12 +-
 .../src/layouts/content/use-dataList.ts            |   8 +-
 dolphinscheduler-ui/src/locales/en_US/menu.ts      |   1 +
 dolphinscheduler-ui/src/locales/en_US/project.ts   |  32 +-
 dolphinscheduler-ui/src/locales/en_US/security.ts  |  20 ++
 dolphinscheduler-ui/src/locales/zh_CN/menu.ts      |   1 +
 dolphinscheduler-ui/src/locales/zh_CN/project.ts   |   8 +-
 dolphinscheduler-ui/src/locales/zh_CN/security.ts  |  20 ++
 dolphinscheduler-ui/src/router/modules/security.ts |  11 +
 .../src/service/modules/cluster/index.ts           |  80 +++++
 .../src/service/modules/cluster/types.ts           |  72 +++++
 .../cluster-manage/components/cluster-modal.tsx    | 191 ++++++++++++
 .../cluster-manage/components/use-modal.ts         | 118 ++++++++
 .../src/views/security/cluster-manage/index.tsx    | 170 +++++++++++
 .../src/views/security/cluster-manage/use-table.ts | 236 +++++++++++++++
 39 files changed, 3178 insertions(+), 35 deletions(-)

diff --git a/.github/workflows/e2e.yml b/.github/workflows/e2e.yml
index db15d51594..66d1214b5a 100644
--- a/.github/workflows/e2e.yml
+++ b/.github/workflows/e2e.yml
@@ -98,6 +98,8 @@ jobs:
             class: org.apache.dolphinscheduler.e2e.cases.QueueE2ETest
           - name: Environment
             class: org.apache.dolphinscheduler.e2e.cases.EnvironmentE2ETest
+          - name: Cluster
+            class: org.apache.dolphinscheduler.e2e.cases.ClusterE2ETest
           - name: Token
             class: org.apache.dolphinscheduler.e2e.cases.TokenE2ETest
           - name: Workflow
diff --git a/docs/docs/en/guide/security.md b/docs/docs/en/guide/security.md
index 1baee568e6..24ecb45bda 100644
--- a/docs/docs/en/guide/security.md
+++ b/docs/docs/en/guide/security.md
@@ -150,6 +150,18 @@ worker.groups=default,test
 
 ![use-environment](../../../img/new_ui/dev/security/use-environment.png)
 
+## Cluster Management
+
+> Add or update cluster
+
+- Each process can be related to zero or several clusters to support multiple 
environment, now just support k8s.
+
+> Usage cluster
+
+- After creation and authorization, k8s namespaces and processes will 
associate clusters. Each cluster will have separate workflows and task 
instances running independently.
+
+![create-cluster](../../../img/new_ui/dev/security/create-cluster.png)
+
 ## Namespace Management
 
 > Add or update k8s cluster
diff --git a/docs/docs/zh/guide/security.md b/docs/docs/zh/guide/security.md
index 61666ccce3..ee8973773f 100644
--- a/docs/docs/zh/guide/security.md
+++ b/docs/docs/zh/guide/security.md
@@ -149,6 +149,18 @@ worker.groups=default,test
 
 ![use-environment](../../../img/new_ui/dev/security/use-environment.png)
 
+## 集群管理
+
+> 创建/更新 集群
+
+- 每个工作流可以绑定零到若干个集群用来支持多集群,目前先用于k8s。
+
+> 使用集群
+
+- 创建和授权后,k8s命名空间和工作流会增加关联集群的功能。每一个集群会有独立的工作流和任务实例独立运行。
+
+![create-cluster](../../../img/new_ui/dev/security/create-cluster.png)
+
 ## 命名空间管理
 
 > 创建/更新 k8s集群
diff --git a/docs/img/new_ui/dev/security/create-cluster.png 
b/docs/img/new_ui/dev/security/create-cluster.png
new file mode 100644
index 0000000000..539c0ba25d
Binary files /dev/null and b/docs/img/new_ui/dev/security/create-cluster.png 
differ
diff --git 
a/dolphinscheduler-api/src/main/java/org/apache/dolphinscheduler/api/controller/ClusterController.java
 
b/dolphinscheduler-api/src/main/java/org/apache/dolphinscheduler/api/controller/ClusterController.java
new file mode 100644
index 0000000000..311dd780e7
--- /dev/null
+++ 
b/dolphinscheduler-api/src/main/java/org/apache/dolphinscheduler/api/controller/ClusterController.java
@@ -0,0 +1,236 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You 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 org.apache.dolphinscheduler.api.controller;
+
+import static 
org.apache.dolphinscheduler.api.enums.Status.CREATE_CLUSTER_ERROR;
+import static 
org.apache.dolphinscheduler.api.enums.Status.DELETE_CLUSTER_ERROR;
+import static 
org.apache.dolphinscheduler.api.enums.Status.QUERY_CLUSTER_BY_CODE_ERROR;
+import static org.apache.dolphinscheduler.api.enums.Status.QUERY_CLUSTER_ERROR;
+import static 
org.apache.dolphinscheduler.api.enums.Status.UPDATE_CLUSTER_ERROR;
+import static 
org.apache.dolphinscheduler.api.enums.Status.VERIFY_CLUSTER_ERROR;
+
+import org.apache.dolphinscheduler.api.aspect.AccessLogAnnotation;
+import org.apache.dolphinscheduler.api.exceptions.ApiException;
+import org.apache.dolphinscheduler.api.service.ClusterService;
+import org.apache.dolphinscheduler.api.utils.Result;
+import org.apache.dolphinscheduler.common.Constants;
+import org.apache.dolphinscheduler.common.utils.ParameterUtils;
+import org.apache.dolphinscheduler.dao.entity.User;
+
+import java.util.Map;
+
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.http.HttpStatus;
+import org.springframework.web.bind.annotation.GetMapping;
+import org.springframework.web.bind.annotation.PostMapping;
+import org.springframework.web.bind.annotation.RequestAttribute;
+import org.springframework.web.bind.annotation.RequestMapping;
+import org.springframework.web.bind.annotation.RequestParam;
+import org.springframework.web.bind.annotation.ResponseStatus;
+import org.springframework.web.bind.annotation.RestController;
+
+import io.swagger.annotations.Api;
+import io.swagger.annotations.ApiImplicitParam;
+import io.swagger.annotations.ApiImplicitParams;
+import io.swagger.annotations.ApiOperation;
+import springfox.documentation.annotations.ApiIgnore;
+
+/**
+ * cluster controller
+ */
+@Api(tags = "CLUSTER_TAG")
+@RestController
+@RequestMapping("cluster")
+public class ClusterController extends BaseController {
+
+    @Autowired
+    private ClusterService clusterService;
+
+    /**
+     * create cluster
+     *
+     * @param loginUser   login user
+     * @param name        cluster name
+     * @param config      config
+     * @param description description
+     * @return returns an error if it exists
+     */
+    @ApiOperation(value = "createCluster", notes = "CREATE_CLUSTER_NOTES")
+    @ApiImplicitParams({
+        @ApiImplicitParam(name = "name", value = "CLUSTER_NAME", required = 
true, dataType = "String"),
+        @ApiImplicitParam(name = "config", value = "CONFIG", required = true, 
dataType = "String"),
+        @ApiImplicitParam(name = "description", value = "CLUSTER_DESC", 
dataType = "String")
+    })
+    @PostMapping(value = "/create")
+    @ResponseStatus(HttpStatus.CREATED)
+    @ApiException(CREATE_CLUSTER_ERROR)
+    @AccessLogAnnotation(ignoreRequestArgs = "loginUser")
+    public Result createProject(@ApiIgnore @RequestAttribute(value = 
Constants.SESSION_USER) User loginUser,
+                                @RequestParam("name") String name,
+                                @RequestParam("config") String config,
+                                @RequestParam(value = "description", required 
= false) String description) {
+
+        Map<String, Object> result = clusterService.createCluster(loginUser, 
name, config, description);
+        return returnDataList(result);
+    }
+
+    /**
+     * update cluster
+     *
+     * @param loginUser   login user
+     * @param code        cluster code
+     * @param name        cluster name
+     * @param config      cluster config
+     * @param description description
+     * @return update result code
+     */
+    @ApiOperation(value = "updateCluster", notes = "UPDATE_CLUSTER_NOTES")
+    @ApiImplicitParams({
+        @ApiImplicitParam(name = "code", value = "CLUSTER_CODE", required = 
true, dataType = "Long", example = "100"),
+        @ApiImplicitParam(name = "name", value = "CLUSTER_NAME", required = 
true, dataType = "String"),
+        @ApiImplicitParam(name = "config", value = "CLUSTER_CONFIG", required 
= true, dataType = "String"),
+        @ApiImplicitParam(name = "description", value = "CLUSTER_DESC", 
dataType = "String"),
+    })
+    @PostMapping(value = "/update")
+    @ResponseStatus(HttpStatus.OK)
+    @ApiException(UPDATE_CLUSTER_ERROR)
+    @AccessLogAnnotation(ignoreRequestArgs = "loginUser")
+    public Result updateCluster(@ApiIgnore @RequestAttribute(value = 
Constants.SESSION_USER) User loginUser,
+                                @RequestParam("code") Long code,
+                                @RequestParam("name") String name,
+                                @RequestParam("config") String config,
+                                @RequestParam(value = "description", required 
= false) String description) {
+        Map<String, Object> result = 
clusterService.updateClusterByCode(loginUser, code, name, config, description);
+        return returnDataList(result);
+    }
+
+    /**
+     * query cluster details by code
+     *
+     * @param clusterCode cluster code
+     * @return cluster detail information
+     */
+    @ApiOperation(value = "queryClusterByCode", notes = 
"QUERY_CLUSTER_BY_CODE_NOTES")
+    @ApiImplicitParams({
+        @ApiImplicitParam(name = "clusterCode", value = "CLUSTER_CODE", 
required = true, dataType = "Long", example = "100")
+    })
+    @GetMapping(value = "/query-by-code")
+    @ResponseStatus(HttpStatus.OK)
+    @ApiException(QUERY_CLUSTER_BY_CODE_ERROR)
+    @AccessLogAnnotation(ignoreRequestArgs = "loginUser")
+    public Result queryClusterByCode(@ApiIgnore @RequestAttribute(value = 
Constants.SESSION_USER) User loginUser,
+                                     @RequestParam("clusterCode") Long 
clusterCode) {
+
+        Map<String, Object> result = 
clusterService.queryClusterByCode(clusterCode);
+        return returnDataList(result);
+    }
+
+    /**
+     * query cluster list paging
+     *
+     * @param searchVal search value
+     * @param pageSize  page size
+     * @param pageNo    page number
+     * @return cluster list which the login user have permission to see
+     */
+    @ApiOperation(value = "queryClusterListPaging", notes = 
"QUERY_CLUSTER_LIST_PAGING_NOTES")
+    @ApiImplicitParams({
+        @ApiImplicitParam(name = "searchVal", value = "SEARCH_VAL", dataType = 
"String"),
+        @ApiImplicitParam(name = "pageSize", value = "PAGE_SIZE", required = 
true, dataType = "Int", example = "20"),
+        @ApiImplicitParam(name = "pageNo", value = "PAGE_NO", required = true, 
dataType = "Int", example = "1")
+    })
+    @GetMapping(value = "/list-paging")
+    @ResponseStatus(HttpStatus.OK)
+    @ApiException(QUERY_CLUSTER_ERROR)
+    @AccessLogAnnotation(ignoreRequestArgs = "loginUser")
+    public Result queryClusterListPaging(@ApiIgnore @RequestAttribute(value = 
Constants.SESSION_USER) User loginUser,
+                                         @RequestParam(value = "searchVal", 
required = false) String searchVal,
+                                         @RequestParam("pageSize") Integer 
pageSize,
+                                         @RequestParam("pageNo") Integer pageNo
+    ) {
+
+        Result result = checkPageParams(pageNo, pageSize);
+        if (!result.checkResult()) {
+            return result;
+        }
+        searchVal = ParameterUtils.handleEscapes(searchVal);
+        result = clusterService.queryClusterListPaging(pageNo, pageSize, 
searchVal);
+        return result;
+    }
+
+    /**
+     * delete cluster by code
+     *
+     * @param loginUser   login user
+     * @param clusterCode cluster code
+     * @return delete result code
+     */
+    @ApiOperation(value = "deleteClusterByCode", notes = 
"DELETE_CLUSTER_BY_CODE_NOTES")
+    @ApiImplicitParams({
+        @ApiImplicitParam(name = "clusterCode", value = "CLUSTER_CODE", 
required = true, dataType = "Long", example = "100")
+    })
+    @PostMapping(value = "/delete")
+    @ResponseStatus(HttpStatus.OK)
+    @ApiException(DELETE_CLUSTER_ERROR)
+    @AccessLogAnnotation(ignoreRequestArgs = "loginUser")
+    public Result deleteCluster(@ApiIgnore @RequestAttribute(value = 
Constants.SESSION_USER) User loginUser,
+                                @RequestParam("clusterCode") Long clusterCode
+    ) {
+
+        Map<String, Object> result = 
clusterService.deleteClusterByCode(loginUser, clusterCode);
+        return returnDataList(result);
+    }
+
+    /**
+     * query all cluster list
+     *
+     * @param loginUser login user
+     * @return all cluster list
+     */
+    @ApiOperation(value = "queryAllClusterList", notes = 
"QUERY_ALL_CLUSTER_LIST_NOTES")
+    @GetMapping(value = "/query-cluster-list")
+    @ResponseStatus(HttpStatus.OK)
+    @ApiException(QUERY_CLUSTER_ERROR)
+    @AccessLogAnnotation(ignoreRequestArgs = "loginUser")
+    public Result queryAllClusterList(@ApiIgnore @RequestAttribute(value = 
Constants.SESSION_USER) User loginUser) {
+        Map<String, Object> result = clusterService.queryAllClusterList();
+        return returnDataList(result);
+    }
+
+    /**
+     * verify cluster and cluster name
+     *
+     * @param loginUser   login user
+     * @param clusterName cluster name
+     * @return true if the cluster name not exists, otherwise return false
+     */
+    @ApiOperation(value = "verifyCluster", notes = "VERIFY_CLUSTER_NOTES")
+    @ApiImplicitParams({
+        @ApiImplicitParam(name = "clusterName", value = "CLUSTER_NAME", 
required = true, dataType = "String")
+    })
+    @PostMapping(value = "/verify-cluster")
+    @ResponseStatus(HttpStatus.OK)
+    @ApiException(VERIFY_CLUSTER_ERROR)
+    @AccessLogAnnotation(ignoreRequestArgs = "loginUser")
+    public Result verifyCluster(@ApiIgnore @RequestAttribute(value = 
Constants.SESSION_USER) User loginUser,
+                                @RequestParam(value = "clusterName") String 
clusterName
+    ) {
+        Map<String, Object> result = clusterService.verifyCluster(clusterName);
+        return returnDataList(result);
+    }
+}
diff --git 
a/dolphinscheduler-api/src/main/java/org/apache/dolphinscheduler/api/dto/ClusterDto.java
 
b/dolphinscheduler-api/src/main/java/org/apache/dolphinscheduler/api/dto/ClusterDto.java
new file mode 100644
index 0000000000..0dd0dcd23f
--- /dev/null
+++ 
b/dolphinscheduler-api/src/main/java/org/apache/dolphinscheduler/api/dto/ClusterDto.java
@@ -0,0 +1,129 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You 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 org.apache.dolphinscheduler.api.dto;
+
+import java.util.Date;
+import java.util.List;
+
+/**
+ * ClusterDto
+ */
+public class ClusterDto {
+
+    private int id;
+
+    /**
+     * clluster code
+     */
+    private Long code;
+
+    /**
+     * clluster name
+     */
+    private String name;
+
+    /**
+     * config content
+     */
+    private String config;
+
+    private String description;
+
+    private List<String> processDefinitions;
+
+    /**
+     * operator user id
+     */
+    private Integer operator;
+
+    private Date createTime;
+
+    private Date updateTime;
+
+    public int getId() {
+        return id;
+    }
+
+    public void setId(int id) {
+        this.id = id;
+    }
+
+    public String getName() {
+        return name;
+    }
+
+    public void setName(String name) {
+        this.name = name;
+    }
+
+    public Long getCode() {
+        return this.code;
+    }
+
+    public void setCode(Long code) {
+        this.code = code;
+    }
+
+    public String getConfig() {
+        return this.config;
+    }
+
+    public void setConfig(String config) {
+        this.config = config;
+    }
+
+    public String getDescription() {
+        return this.description;
+    }
+
+    public void setDescription(String description) {
+        this.description = description;
+    }
+
+    public Integer getOperator() {
+        return this.operator;
+    }
+
+    public void setOperator(Integer operator) {
+        this.operator = operator;
+    }
+
+    public Date getCreateTime() {
+        return createTime;
+    }
+
+    public void setCreateTime(Date createTime) {
+        this.createTime = createTime;
+    }
+
+    public Date getUpdateTime() {
+        return updateTime;
+    }
+
+    public void setUpdateTime(Date updateTime) {
+        this.updateTime = updateTime;
+    }
+
+    public List<String> getProcessDefinitions() {
+        return processDefinitions;
+    }
+
+    public void setProcessDefinitions(List<String> processDefinitions) {
+        this.processDefinitions = processDefinitions;
+    }
+}
diff --git 
a/dolphinscheduler-api/src/main/java/org/apache/dolphinscheduler/api/enums/Status.java
 
b/dolphinscheduler-api/src/main/java/org/apache/dolphinscheduler/api/enums/Status.java
index 8f5b30168d..80e2b2c999 100644
--- 
a/dolphinscheduler-api/src/main/java/org/apache/dolphinscheduler/api/enums/Status.java
+++ 
b/dolphinscheduler-api/src/main/java/org/apache/dolphinscheduler/api/enums/Status.java
@@ -364,6 +364,24 @@ public enum Status {
     GET_DATASOURCE_OPTIONS_ERROR(1200017, "get datasource options error", 
"获取数据源Options错误"),
     GET_DATASOURCE_TABLES_ERROR(1200018, "get datasource tables error", 
"获取数据源表列表错误"),
     GET_DATASOURCE_TABLE_COLUMNS_ERROR(1200019, "get datasource table columns 
error", "获取数据源表列名错误"),
+
+    CREATE_CLUSTER_ERROR(120020, "create cluster error", "创建集群失败"),
+    CLUSTER_NAME_EXISTS(120021, "this cluster name [{0}] already exists", 
"集群名称[{0}]已经存在"),
+    CLUSTER_NAME_IS_NULL(120022, "this cluster name shouldn't be empty.", 
"集群名称不能为空"),
+    CLUSTER_CONFIG_IS_NULL(120023, "this cluster config shouldn't be empty.", 
"集群配置信息不能为空"),
+    UPDATE_CLUSTER_ERROR(120024, "update cluster [{0}] info error", 
"更新集群[{0}]信息失败"),
+    DELETE_CLUSTER_ERROR(120025, "delete cluster error", "删除集群信息失败"),
+    DELETE_CLUSTER_RELATED_TASK_EXISTS(120026, "this cluster has been used in 
tasks,so you can't delete it.", "该集群已经被任务使用,所以不能删除该集群信息"),
+    QUERY_CLUSTER_BY_NAME_ERROR(1200027, "not found cluster [{0}] ", 
"查询集群名称[{0}]信息不存在"),
+    QUERY_CLUSTER_BY_CODE_ERROR(1200028, "not found cluster [{0}] ", 
"查询集群编码[{0}]不存在"),
+    QUERY_CLUSTER_ERROR(1200029, "login user query cluster error", 
"分页查询集群列表错误"),
+    VERIFY_CLUSTER_ERROR(1200030, "verify cluster error", "验证集群信息错误"),
+    CLUSTER_PROCESS_DEFINITIONS_IS_INVALID(1200031, "cluster worker groups is 
invalid format", "集群关联的工作组参数解析错误"),
+    UPDATE_CLUSTER_PROCESS_DEFINITION_RELATION_ERROR(1200032, "You can't 
modify the process definition, because the process definition [{0}] and this 
cluster [{1}] already be used in the task [{2}]",
+        "您不能修改集群选项,因为该工作流组 [{0}] 和 该集群 [{1}] 已经被用在任务 [{2}] 中"),
+    CLUSTER_NOT_EXISTS(120033, "this cluster can not found in db.", 
"集群配置数据库里查询不到为空"),
+    DELETE_CLUSTER_RELATED_NAMESPACE_EXISTS(120034, "this cluster has been 
used in namespace,so you can't delete it.", "该集群已经被命名空间使用,所以不能删除该集群信息"),
+
     TASK_GROUP_NAME_EXSIT(130001, "this task group name is repeated in a 
project", "该任务组名称在一个项目中已经使用"),
     TASK_GROUP_SIZE_ERROR(130002, "task group size error", "任务组大小应该为大于1的整数"),
     TASK_GROUP_STATUS_ERROR(130003, "task group status error", "任务组已经被关闭"),
diff --git 
a/dolphinscheduler-api/src/main/java/org/apache/dolphinscheduler/api/k8s/K8sClientService.java
 
b/dolphinscheduler-api/src/main/java/org/apache/dolphinscheduler/api/k8s/K8sClientService.java
index 2e73666289..b8f07c5ae5 100644
--- 
a/dolphinscheduler-api/src/main/java/org/apache/dolphinscheduler/api/k8s/K8sClientService.java
+++ 
b/dolphinscheduler-api/src/main/java/org/apache/dolphinscheduler/api/k8s/K8sClientService.java
@@ -66,35 +66,35 @@ public class K8sClientService {
 
         //创建资源
         ResourceQuota queryExist = client.resourceQuotas()
-                .inNamespace(k8sNamespace.getNamespace())
-                .withName(k8sNamespace.getNamespace())
-                .get();
+            .inNamespace(k8sNamespace.getNamespace())
+            .withName(k8sNamespace.getNamespace())
+            .get();
 
         ResourceQuota body = yaml.loadAs(yamlStr, ResourceQuota.class);
 
         if (queryExist != null) {
             if (k8sNamespace.getLimitsCpu() == null && 
k8sNamespace.getLimitsMemory() == null) {
                 
client.resourceQuotas().inNamespace(k8sNamespace.getNamespace())
-                        .withName(k8sNamespace.getNamespace())
-                        .delete();
+                    .withName(k8sNamespace.getNamespace())
+                    .delete();
                 return null;
             }
         }
 
         return client.resourceQuotas().inNamespace(k8sNamespace.getNamespace())
-                .withName(k8sNamespace.getNamespace())
-                .createOrReplace(body);
+            .withName(k8sNamespace.getNamespace())
+            .createOrReplace(body);
     }
 
     private Optional<Namespace> getNamespaceFromK8s(String name, String k8s) {
         NamespaceList listNamespace =
-                k8sManager.getK8sClient(k8s).namespaces().list();
+            k8sManager.getK8sClient(k8s).namespaces().list();
 
         Optional<Namespace> list =
-                listNamespace.getItems().stream()
-                        .filter((Namespace namespace) ->
-                                namespace.getMetadata().getName().equals(name))
-                        .findFirst();
+            listNamespace.getItems().stream()
+                .filter((Namespace namespace) ->
+                    namespace.getMetadata().getName().equals(name))
+                .findFirst();
 
         return list;
     }
diff --git 
a/dolphinscheduler-api/src/main/java/org/apache/dolphinscheduler/api/service/ClusterService.java
 
b/dolphinscheduler-api/src/main/java/org/apache/dolphinscheduler/api/service/ClusterService.java
new file mode 100644
index 0000000000..0787e5db41
--- /dev/null
+++ 
b/dolphinscheduler-api/src/main/java/org/apache/dolphinscheduler/api/service/ClusterService.java
@@ -0,0 +1,99 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You 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 org.apache.dolphinscheduler.api.service;
+
+import org.apache.dolphinscheduler.api.utils.Result;
+import org.apache.dolphinscheduler.dao.entity.User;
+
+import java.util.Map;
+
+/**
+ * cluster service
+ */
+public interface ClusterService {
+
+    /**
+     * create cluster
+     *
+     * @param loginUser login user
+     * @param name      cluster name
+     * @param config    cluster config
+     * @param desc      cluster desc
+     */
+    Map<String, Object> createCluster(User loginUser, String name, String 
config, String desc);
+
+    /**
+     * query cluster
+     *
+     * @param name cluster name
+     */
+    Map<String, Object> queryClusterByName(String name);
+
+    /**
+     * query cluster
+     *
+     * @param code cluster code
+     */
+    Map<String, Object> queryClusterByCode(Long code);
+
+    /**
+     * delete cluster
+     *
+     * @param loginUser login user
+     * @param code      cluster code
+     */
+    Map<String, Object> deleteClusterByCode(User loginUser, Long code);
+
+    /**
+     * update cluster
+     *
+     * @param loginUser login user
+     * @param code      cluster code
+     * @param name      cluster name
+     * @param config    cluster config
+     * @param desc      cluster desc
+     */
+    Map<String, Object> updateClusterByCode(User loginUser, Long code, String 
name, String config, String desc);
+
+    /**
+     * query cluster paging
+     *
+     * @param pageNo    page number
+     * @param searchVal search value
+     * @param pageSize  page size
+     * @return cluster list page
+     */
+    Result queryClusterListPaging(Integer pageNo, Integer pageSize, String 
searchVal);
+
+    /**
+     * query all cluster
+     *
+     * @return all cluster list
+     */
+    Map<String, Object> queryAllClusterList();
+
+    /**
+     * verify cluster name
+     *
+     * @param clusterName cluster name
+     * @return true if the cluster name not exists, otherwise return false
+     */
+    Map<String, Object> verifyCluster(String clusterName);
+
+}
+
diff --git 
a/dolphinscheduler-api/src/main/java/org/apache/dolphinscheduler/api/service/impl/ClusterServiceImpl.java
 
b/dolphinscheduler-api/src/main/java/org/apache/dolphinscheduler/api/service/impl/ClusterServiceImpl.java
new file mode 100644
index 0000000000..86d8eca95a
--- /dev/null
+++ 
b/dolphinscheduler-api/src/main/java/org/apache/dolphinscheduler/api/service/impl/ClusterServiceImpl.java
@@ -0,0 +1,335 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You 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 org.apache.dolphinscheduler.api.service.impl;
+
+import org.apache.dolphinscheduler.api.dto.ClusterDto;
+import org.apache.dolphinscheduler.api.enums.Status;
+import org.apache.dolphinscheduler.api.service.ClusterService;
+import org.apache.dolphinscheduler.api.utils.PageInfo;
+import org.apache.dolphinscheduler.api.utils.Result;
+import org.apache.dolphinscheduler.common.Constants;
+import org.apache.dolphinscheduler.common.utils.CodeGenerateUtils;
+import 
org.apache.dolphinscheduler.common.utils.CodeGenerateUtils.CodeGenerateException;
+import org.apache.dolphinscheduler.dao.entity.Cluster;
+import org.apache.dolphinscheduler.dao.entity.User;
+import org.apache.dolphinscheduler.dao.mapper.ClusterMapper;
+
+import org.apache.commons.collections.CollectionUtils;
+import org.apache.commons.lang.StringUtils;
+
+import java.util.ArrayList;
+import java.util.Date;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.stream.Collectors;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.springframework.beans.BeanUtils;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.stereotype.Service;
+import org.springframework.transaction.annotation.Transactional;
+
+import com.baomidou.mybatisplus.core.metadata.IPage;
+import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
+
+/**
+ * cluster definition service impl
+ */
+@Service
+public class ClusterServiceImpl extends BaseServiceImpl implements 
ClusterService {
+
+    private static final Logger logger = 
LoggerFactory.getLogger(ClusterServiceImpl.class);
+
+    @Autowired
+    private ClusterMapper clusterMapper;
+
+    /**
+     * create cluster
+     *
+     * @param loginUser login user
+     * @param name      cluster name
+     * @param config    cluster config
+     * @param desc      cluster desc
+     */
+    @Transactional(rollbackFor = RuntimeException.class)
+    @Override
+    public Map<String, Object> createCluster(User loginUser, String name, 
String config, String desc) {
+        Map<String, Object> result = new HashMap<>();
+        if (isNotAdmin(loginUser, result)) {
+            return result;
+        }
+
+        Map<String, Object> checkResult = checkParams(name, config);
+        if (checkResult.get(Constants.STATUS) != Status.SUCCESS) {
+            return checkResult;
+        }
+
+        Cluster clusterExistByName = clusterMapper.queryByClusterName(name);
+        if (clusterExistByName != null) {
+            putMsg(result, Status.CLUSTER_NAME_EXISTS, name);
+            return result;
+        }
+
+        Cluster cluster = new Cluster();
+        cluster.setName(name);
+        cluster.setConfig(config);
+        cluster.setDescription(desc);
+        cluster.setOperator(loginUser.getId());
+        cluster.setCreateTime(new Date());
+        cluster.setUpdateTime(new Date());
+        long code = 0L;
+        try {
+            code = CodeGenerateUtils.getInstance().genCode();
+            cluster.setCode(code);
+        } catch (CodeGenerateException e) {
+            logger.error("Cluster code get error, ", e);
+        }
+        if (code == 0L) {
+            putMsg(result, Status.INTERNAL_SERVER_ERROR_ARGS, "Error 
generating cluster code");
+            return result;
+        }
+
+        if (clusterMapper.insert(cluster) > 0) {
+            result.put(Constants.DATA_LIST, cluster.getCode());
+            putMsg(result, Status.SUCCESS);
+        } else {
+            putMsg(result, Status.CREATE_CLUSTER_ERROR);
+        }
+        return result;
+    }
+
+    /**
+     * query cluster paging
+     *
+     * @param pageNo    page number
+     * @param searchVal search value
+     * @param pageSize  page size
+     * @return cluster list page
+     */
+    @Override
+    public Result queryClusterListPaging(Integer pageNo, Integer pageSize, 
String searchVal) {
+        Result result = new Result();
+
+        Page<Cluster> page = new Page<>(pageNo, pageSize);
+
+        IPage<Cluster> clusterIPage = 
clusterMapper.queryClusterListPaging(page, searchVal);
+
+        PageInfo<ClusterDto> pageInfo = new PageInfo<>(pageNo, pageSize);
+        pageInfo.setTotal((int) clusterIPage.getTotal());
+
+        if (CollectionUtils.isNotEmpty(clusterIPage.getRecords())) {
+
+            List<ClusterDto> dtoList = 
clusterIPage.getRecords().stream().map(cluster -> {
+                ClusterDto dto = new ClusterDto();
+                BeanUtils.copyProperties(cluster, dto);
+                return dto;
+            }).collect(Collectors.toList());
+
+            pageInfo.setTotalList(dtoList);
+        } else {
+            pageInfo.setTotalList(new ArrayList<>());
+        }
+
+        result.setData(pageInfo);
+        putMsg(result, Status.SUCCESS);
+        return result;
+    }
+
+    /**
+     * query all cluster
+     *
+     * @return all cluster list
+     */
+    @Override
+    public Map<String, Object> queryAllClusterList() {
+        Map<String, Object> result = new HashMap<>();
+        List<Cluster> clusterList = clusterMapper.queryAllClusterList();
+
+        if (CollectionUtils.isNotEmpty(clusterList)) {
+
+            List<ClusterDto> dtoList = clusterList.stream().map(cluster -> {
+                ClusterDto dto = new ClusterDto();
+                BeanUtils.copyProperties(cluster, dto);
+                return dto;
+            }).collect(Collectors.toList());
+            result.put(Constants.DATA_LIST, dtoList);
+        } else {
+            result.put(Constants.DATA_LIST, new ArrayList<>());
+        }
+
+        putMsg(result, Status.SUCCESS);
+        return result;
+    }
+
+    /**
+     * query cluster
+     *
+     * @param code cluster code
+     */
+    @Override
+    public Map<String, Object> queryClusterByCode(Long code) {
+        Map<String, Object> result = new HashMap<>();
+
+        Cluster cluster = clusterMapper.queryByClusterCode(code);
+
+        if (cluster == null) {
+            putMsg(result, Status.QUERY_CLUSTER_BY_CODE_ERROR, code);
+        } else {
+
+            ClusterDto dto = new ClusterDto();
+            BeanUtils.copyProperties(cluster, dto);
+            result.put(Constants.DATA_LIST, dto);
+            putMsg(result, Status.SUCCESS);
+        }
+        return result;
+    }
+
+    /**
+     * query cluster
+     *
+     * @param name cluster name
+     */
+    @Override
+    public Map<String, Object> queryClusterByName(String name) {
+        Map<String, Object> result = new HashMap<>();
+
+        Cluster cluster = clusterMapper.queryByClusterName(name);
+        if (cluster == null) {
+            putMsg(result, Status.QUERY_CLUSTER_BY_NAME_ERROR, name);
+        } else {
+
+            ClusterDto dto = new ClusterDto();
+            BeanUtils.copyProperties(cluster, dto);
+            result.put(Constants.DATA_LIST, dto);
+            putMsg(result, Status.SUCCESS);
+        }
+        return result;
+    }
+
+    /**
+     * delete cluster
+     *
+     * @param loginUser login user
+     * @param code      cluster code
+     */
+    @Transactional(rollbackFor = RuntimeException.class)
+    @Override
+    public Map<String, Object> deleteClusterByCode(User loginUser, Long code) {
+        Map<String, Object> result = new HashMap<>();
+        if (isNotAdmin(loginUser, result)) {
+            return result;
+        }
+
+        int delete = clusterMapper.deleteByCode(code);
+        if (delete > 0) {
+            putMsg(result, Status.SUCCESS);
+        } else {
+            putMsg(result, Status.DELETE_CLUSTER_ERROR);
+        }
+        return result;
+    }
+
+
+    /**
+     * update cluster
+     *
+     * @param loginUser login user
+     * @param code      cluster code
+     * @param name      cluster name
+     * @param config    cluster config
+     * @param desc      cluster desc
+     */
+    @Transactional(rollbackFor = RuntimeException.class)
+    @Override
+    public Map<String, Object> updateClusterByCode(User loginUser, Long code, 
String name, String config, String desc) {
+        Map<String, Object> result = new HashMap<>();
+        if (isNotAdmin(loginUser, result)) {
+            return result;
+        }
+
+        Map<String, Object> checkResult = checkParams(name, config);
+        if (checkResult.get(Constants.STATUS) != Status.SUCCESS) {
+            return checkResult;
+        }
+
+        Cluster clusterExistByName = clusterMapper.queryByClusterName(name);
+        if (clusterExistByName != null && 
!clusterExistByName.getCode().equals(code)) {
+            putMsg(result, Status.CLUSTER_NAME_EXISTS, name);
+            return result;
+        }
+
+        Cluster clusterExist = clusterMapper.queryByClusterCode(code);
+        if (clusterExist == null) {
+            putMsg(result, Status.CLUSTER_NOT_EXISTS, name);
+            return result;
+        }
+
+        //update cluster
+        clusterExist.setConfig(config);
+        clusterExist.setName(name);
+        clusterExist.setDescription(desc);
+        clusterMapper.updateById(clusterExist);
+        //need not update relation
+
+        putMsg(result, Status.SUCCESS);
+        return result;
+    }
+
+    /**
+     * verify cluster name
+     *
+     * @param clusterName cluster name
+     * @return true if the cluster name not exists, otherwise return false
+     */
+    @Override
+    public Map<String, Object> verifyCluster(String clusterName) {
+        Map<String, Object> result = new HashMap<>();
+
+        if (StringUtils.isEmpty(clusterName)) {
+            putMsg(result, Status.CLUSTER_NAME_IS_NULL);
+            return result;
+        }
+
+        Cluster cluster = clusterMapper.queryByClusterName(clusterName);
+        if (cluster != null) {
+            putMsg(result, Status.CLUSTER_NAME_EXISTS, clusterName);
+            return result;
+        }
+
+        result.put(Constants.STATUS, Status.SUCCESS);
+        return result;
+    }
+
+    public Map<String, Object> checkParams(String name, String config) {
+        Map<String, Object> result = new HashMap<>();
+        if (StringUtils.isEmpty(name)) {
+            putMsg(result, Status.CLUSTER_NAME_IS_NULL);
+            return result;
+        }
+        if (StringUtils.isEmpty(config)) {
+            putMsg(result, Status.CLUSTER_CONFIG_IS_NULL);
+            return result;
+        }
+        result.put(Constants.STATUS, Status.SUCCESS);
+        return result;
+    }
+
+}
+
diff --git 
a/dolphinscheduler-api/src/test/java/org/apache/dolphinscheduler/api/controller/ClusterControllerTest.java
 
b/dolphinscheduler-api/src/test/java/org/apache/dolphinscheduler/api/controller/ClusterControllerTest.java
new file mode 100644
index 0000000000..fd255ce0f3
--- /dev/null
+++ 
b/dolphinscheduler-api/src/test/java/org/apache/dolphinscheduler/api/controller/ClusterControllerTest.java
@@ -0,0 +1,202 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You 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 org.apache.dolphinscheduler.api.controller;
+
+import static 
org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
+import static 
org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post;
+import static 
org.springframework.test.web.servlet.result.MockMvcResultMatchers.content;
+import static 
org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
+
+import org.apache.dolphinscheduler.api.enums.Status;
+import org.apache.dolphinscheduler.api.utils.Result;
+import org.apache.dolphinscheduler.common.utils.JSONUtils;
+
+import org.junit.After;
+import org.junit.Assert;
+import org.junit.Before;
+import org.junit.Test;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.springframework.http.MediaType;
+import org.springframework.test.web.servlet.MvcResult;
+import org.springframework.util.LinkedMultiValueMap;
+import org.springframework.util.MultiValueMap;
+
+import com.fasterxml.jackson.core.type.TypeReference;
+import com.google.common.base.Preconditions;
+
+public class ClusterControllerTest extends AbstractControllerTest {
+    public static final String clusterName = "Cluster1";
+    public static final String config = "this is config content";
+    public static final String desc = "this is cluster description";
+    private static final Logger logger = 
LoggerFactory.getLogger(ClusterControllerTest.class);
+    private String clusterCode;
+
+    @Before
+    public void before() throws Exception {
+        testCreateCluster();
+    }
+
+    @Override
+    @After
+    public void after() throws Exception {
+        testDeleteCluster();
+    }
+
+    public void testCreateCluster() throws Exception {
+
+        MultiValueMap<String, String> paramsMap = new LinkedMultiValueMap<>();
+        paramsMap.add("name", clusterName);
+        paramsMap.add("config", config);
+        paramsMap.add("description", desc);
+
+        MvcResult mvcResult = mockMvc.perform(post("/cluster/create")
+            .header(SESSION_ID, sessionId)
+            .params(paramsMap))
+            .andExpect(status().isCreated())
+            .andExpect(content().contentType(MediaType.APPLICATION_JSON))
+            .andReturn();
+
+        Result result = 
JSONUtils.parseObject(mvcResult.getResponse().getContentAsString(), new 
TypeReference<Result<String>>() {
+        });
+        logger.info(result.toString());
+        Assert.assertTrue(result != null && result.isSuccess());
+        Assert.assertNotNull(result.getData());
+        logger.info("create cluster return result:{}", 
mvcResult.getResponse().getContentAsString());
+
+        clusterCode = (String) result.getData();
+    }
+
+    @Test
+    public void testUpdateCluster() throws Exception {
+        MultiValueMap<String, String> paramsMap = new LinkedMultiValueMap<>();
+        paramsMap.add("code", clusterCode);
+        paramsMap.add("name", "cluster_test_update");
+        paramsMap.add("config", "{\"k8s\":\"apiVersion: v1\"}");
+        paramsMap.add("desc", "the test cluster update");
+
+        MvcResult mvcResult = mockMvc.perform(post("/cluster/update")
+            .header(SESSION_ID, sessionId)
+            .params(paramsMap))
+            .andExpect(status().isOk())
+            .andExpect(content().contentType(MediaType.APPLICATION_JSON))
+            .andReturn();
+
+        Result result = 
JSONUtils.parseObject(mvcResult.getResponse().getContentAsString(), 
Result.class);
+        logger.info(result.toString());
+        Assert.assertTrue(result != null && result.isSuccess());
+        logger.info("update cluster return result:{}", 
mvcResult.getResponse().getContentAsString());
+
+    }
+
+    @Test
+    public void testQueryClusterByCode() throws Exception {
+        MultiValueMap<String, String> paramsMap = new LinkedMultiValueMap<>();
+        paramsMap.add("clusterCode", clusterCode);
+
+        MvcResult mvcResult = mockMvc.perform(get("/cluster/query-by-code")
+            .header(SESSION_ID, sessionId)
+            .params(paramsMap))
+            .andExpect(status().isOk())
+            .andExpect(content().contentType(MediaType.APPLICATION_JSON))
+            .andReturn();
+
+        Result result = 
JSONUtils.parseObject(mvcResult.getResponse().getContentAsString(), 
Result.class);
+        logger.info(result.toString());
+        Assert.assertTrue(result != null && result.isSuccess());
+        logger.info(mvcResult.getResponse().getContentAsString());
+        logger.info("query cluster by id :{}, return result:{}", clusterCode, 
mvcResult.getResponse().getContentAsString());
+
+    }
+
+    @Test
+    public void testQueryClusterListPaging() throws Exception {
+        MultiValueMap<String, String> paramsMap = new LinkedMultiValueMap<>();
+        paramsMap.add("searchVal", "test");
+        paramsMap.add("pageSize", "2");
+        paramsMap.add("pageNo", "2");
+
+        MvcResult mvcResult = mockMvc.perform(get("/cluster/list-paging")
+            .header(SESSION_ID, sessionId)
+            .params(paramsMap))
+            .andExpect(status().isOk())
+            .andExpect(content().contentType(MediaType.APPLICATION_JSON))
+            .andReturn();
+
+        Result result = 
JSONUtils.parseObject(mvcResult.getResponse().getContentAsString(), 
Result.class);
+        logger.info(result.toString());
+        Assert.assertTrue(result != null && result.isSuccess());
+        logger.info("query list-paging cluster return result:{}", 
mvcResult.getResponse().getContentAsString());
+    }
+
+    @Test
+    public void testQueryAllClusterList() throws Exception {
+        MultiValueMap<String, String> paramsMap = new LinkedMultiValueMap<>();
+
+        MvcResult mvcResult = 
mockMvc.perform(get("/cluster/query-cluster-list")
+            .header(SESSION_ID, sessionId)
+            .params(paramsMap))
+            .andExpect(status().isOk())
+            .andExpect(content().contentType(MediaType.APPLICATION_JSON))
+            .andReturn();
+
+        Result result = 
JSONUtils.parseObject(mvcResult.getResponse().getContentAsString(), 
Result.class);
+        logger.info(result.toString());
+        Assert.assertTrue(result != null && result.isSuccess());
+        logger.info("query all cluster return result:{}", 
mvcResult.getResponse().getContentAsString());
+
+    }
+
+    @Test
+    public void testVerifyCluster() throws Exception {
+        MultiValueMap<String, String> paramsMap = new LinkedMultiValueMap<>();
+        paramsMap.add("clusterName", clusterName);
+
+        MvcResult mvcResult = mockMvc.perform(post("/cluster/verify-cluster")
+            .header(SESSION_ID, sessionId)
+            .params(paramsMap))
+            .andExpect(status().isOk())
+            .andExpect(content().contentType(MediaType.APPLICATION_JSON))
+            .andReturn();
+
+        Result result = 
JSONUtils.parseObject(mvcResult.getResponse().getContentAsString(), 
Result.class);
+        logger.info(result.toString());
+        Assert.assertTrue(result.isStatus(Status.CLUSTER_NAME_EXISTS));
+        logger.info("verify cluster return result:{}", 
mvcResult.getResponse().getContentAsString());
+
+    }
+
+    private void testDeleteCluster() throws Exception {
+        Preconditions.checkNotNull(clusterCode);
+
+        MultiValueMap<String, String> paramsMap = new LinkedMultiValueMap<>();
+        paramsMap.add("clusterCode", clusterCode);
+
+        MvcResult mvcResult = mockMvc.perform(post("/cluster/delete")
+            .header(SESSION_ID, sessionId)
+            .params(paramsMap))
+            .andExpect(status().isOk())
+            .andExpect(content().contentType(MediaType.APPLICATION_JSON))
+            .andReturn();
+
+        Result result = 
JSONUtils.parseObject(mvcResult.getResponse().getContentAsString(), 
Result.class);
+        logger.info(result.toString());
+        Assert.assertTrue(result != null && result.isSuccess());
+        logger.info("delete cluster return result:{}", 
mvcResult.getResponse().getContentAsString());
+    }
+}
diff --git 
a/dolphinscheduler-api/src/test/java/org/apache/dolphinscheduler/api/service/ClusterServiceTest.java
 
b/dolphinscheduler-api/src/test/java/org/apache/dolphinscheduler/api/service/ClusterServiceTest.java
new file mode 100644
index 0000000000..7c22c57fc6
--- /dev/null
+++ 
b/dolphinscheduler-api/src/test/java/org/apache/dolphinscheduler/api/service/ClusterServiceTest.java
@@ -0,0 +1,290 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You 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 org.apache.dolphinscheduler.api.service;
+
+import org.apache.dolphinscheduler.api.enums.Status;
+import org.apache.dolphinscheduler.api.k8s.K8sManager;
+import org.apache.dolphinscheduler.api.service.impl.ClusterServiceImpl;
+import org.apache.dolphinscheduler.api.utils.PageInfo;
+import org.apache.dolphinscheduler.api.utils.Result;
+import org.apache.dolphinscheduler.common.Constants;
+import org.apache.dolphinscheduler.common.enums.UserType;
+import org.apache.dolphinscheduler.dao.entity.Cluster;
+import org.apache.dolphinscheduler.dao.entity.User;
+import org.apache.dolphinscheduler.dao.mapper.ClusterMapper;
+import org.apache.dolphinscheduler.dao.mapper.K8sNamespaceMapper;
+import org.apache.dolphinscheduler.remote.exceptions.RemotingException;
+
+import org.apache.commons.collections.CollectionUtils;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Map;
+
+import org.assertj.core.util.Lists;
+import org.junit.After;
+import org.junit.Assert;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.InjectMocks;
+import org.mockito.Mock;
+import org.mockito.Mockito;
+import org.mockito.junit.MockitoJUnitRunner;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.springframework.boot.configurationprocessor.json.JSONException;
+import org.springframework.boot.configurationprocessor.json.JSONObject;
+
+import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
+import com.baomidou.mybatisplus.core.metadata.IPage;
+import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
+
+/**
+ * cluster service test
+ */
+@RunWith(MockitoJUnitRunner.class)
+public class ClusterServiceTest {
+
+    public static final Logger logger = 
LoggerFactory.getLogger(ClusterServiceTest.class);
+
+    @InjectMocks
+    private ClusterServiceImpl clusterService;
+
+    @Mock
+    private ClusterMapper clusterMapper;
+
+    @Mock
+    private K8sNamespaceMapper k8sNamespaceMapper;
+
+    @Mock
+    private K8sManager k8sManager;
+
+
+    public static final String testUserName = "clusterServerTest";
+
+    public static final String clusterName = "Env1";
+
+    @Before
+    public void setUp(){
+    }
+
+    @After
+    public void after(){
+    }
+
+    @Test
+    public void testCreateCluster() {
+        User loginUser = getGeneralUser();
+        Map<String, Object> result = 
clusterService.createCluster(loginUser,clusterName,getConfig(),getDesc());
+        logger.info(result.toString());
+        Assert.assertEquals(Status.USER_NO_OPERATION_PERM, 
result.get(Constants.STATUS));
+
+        loginUser = getAdminUser();
+        result = 
clusterService.createCluster(loginUser,clusterName,"",getDesc());
+        logger.info(result.toString());
+        Assert.assertEquals(Status.CLUSTER_CONFIG_IS_NULL, 
result.get(Constants.STATUS));
+
+        result = 
clusterService.createCluster(loginUser,"",getConfig(),getDesc());
+        logger.info(result.toString());
+        Assert.assertEquals(Status.CLUSTER_NAME_IS_NULL, 
result.get(Constants.STATUS));
+
+        
Mockito.when(clusterMapper.queryByClusterName(clusterName)).thenReturn(getCluster());
+        result = 
clusterService.createCluster(loginUser,clusterName,getConfig(),getDesc());
+        logger.info(result.toString());
+        Assert.assertEquals(Status.CLUSTER_NAME_EXISTS, 
result.get(Constants.STATUS));
+
+        
Mockito.when(clusterMapper.insert(Mockito.any(Cluster.class))).thenReturn(1);
+        result = 
clusterService.createCluster(loginUser,"testName","testConfig","testDesc");
+        logger.info(result.toString());
+        Assert.assertEquals(Status.SUCCESS, result.get(Constants.STATUS));
+    }
+
+    @Test
+    public void testCheckParams() {
+        Map<String, Object> result = 
clusterService.checkParams(clusterName,getConfig());
+        Assert.assertEquals(Status.SUCCESS, result.get(Constants.STATUS));
+        result = clusterService.checkParams("",getConfig());
+        Assert.assertEquals(Status.CLUSTER_NAME_IS_NULL, 
result.get(Constants.STATUS));
+        result = clusterService.checkParams(clusterName,"");
+        Assert.assertEquals(Status.CLUSTER_CONFIG_IS_NULL, 
result.get(Constants.STATUS));
+    }
+
+    @Test
+    public void testUpdateClusterByCode() throws RemotingException {
+        User loginUser = getGeneralUser();
+        Map<String, Object> result = 
clusterService.updateClusterByCode(loginUser,1L,clusterName,getConfig(),getDesc());
+        logger.info(result.toString());
+        Assert.assertEquals(Status.USER_NO_OPERATION_PERM, 
result.get(Constants.STATUS));
+
+        loginUser = getAdminUser();
+        result = 
clusterService.updateClusterByCode(loginUser,1L,clusterName,"",getDesc());
+        logger.info(result.toString());
+        Assert.assertEquals(Status.CLUSTER_CONFIG_IS_NULL, 
result.get(Constants.STATUS));
+
+        result = 
clusterService.updateClusterByCode(loginUser,1L,"",getConfig(),getDesc());
+        logger.info(result.toString());
+        Assert.assertEquals(Status.CLUSTER_NAME_IS_NULL, 
result.get(Constants.STATUS));
+
+        result = 
clusterService.updateClusterByCode(loginUser,2L,clusterName,getConfig(),getDesc());
+        logger.info(result.toString());
+        Assert.assertEquals(Status.CLUSTER_NOT_EXISTS, 
result.get(Constants.STATUS));
+
+        
Mockito.when(clusterMapper.queryByClusterName(clusterName)).thenReturn(getCluster());
+        result = 
clusterService.updateClusterByCode(loginUser,2L,clusterName,getConfig(),getDesc());
+        logger.info(result.toString());
+        Assert.assertEquals(Status.CLUSTER_NAME_EXISTS, 
result.get(Constants.STATUS));
+
+        
Mockito.when(clusterMapper.updateById(Mockito.any(Cluster.class))).thenReturn(1);
+        
Mockito.when(clusterMapper.queryByClusterCode(1L)).thenReturn(getCluster());
+
+        result = 
clusterService.updateClusterByCode(loginUser,1L,"testName",getConfig(),"test");
+        logger.info(result.toString());
+        Assert.assertEquals(Status.SUCCESS, result.get(Constants.STATUS));
+
+    }
+
+    @Test
+    public void testQueryAllClusterList() {
+        
Mockito.when(clusterMapper.queryAllClusterList()).thenReturn(Lists.newArrayList(getCluster()));
+        Map<String, Object> result  = clusterService.queryAllClusterList();
+        logger.info(result.toString());
+        Assert.assertEquals(Status.SUCCESS,result.get(Constants.STATUS));
+
+        List<Cluster> list = (List<Cluster>)(result.get(Constants.DATA_LIST));
+        Assert.assertEquals(1,list.size());
+    }
+
+    @Test
+    public void testQueryClusterListPaging() {
+        IPage<Cluster> page = new Page<>(1, 10);
+        page.setRecords(getList());
+        page.setTotal(1L);
+        
Mockito.when(clusterMapper.queryClusterListPaging(Mockito.any(Page.class), 
Mockito.eq(clusterName))).thenReturn(page);
+
+        Result result = clusterService.queryClusterListPaging(1, 10, 
clusterName);
+        logger.info(result.toString());
+        PageInfo<Cluster> pageInfo = (PageInfo<Cluster>) result.getData();
+        Assert.assertTrue(CollectionUtils.isNotEmpty(pageInfo.getTotalList()));
+    }
+
+    @Test
+    public void testQueryClusterByName() {
+        
Mockito.when(clusterMapper.queryByClusterName(clusterName)).thenReturn(null);
+        Map<String, Object> result = 
clusterService.queryClusterByName(clusterName);
+        logger.info(result.toString());
+        
Assert.assertEquals(Status.QUERY_CLUSTER_BY_NAME_ERROR,result.get(Constants.STATUS));
+
+        
Mockito.when(clusterMapper.queryByClusterName(clusterName)).thenReturn(getCluster());
+        result = clusterService.queryClusterByName(clusterName);
+        logger.info(result.toString());
+        Assert.assertEquals(Status.SUCCESS,result.get(Constants.STATUS));
+    }
+
+    @Test
+    public void testQueryClusterByCode() {
+        Mockito.when(clusterMapper.queryByClusterCode(1L)).thenReturn(null);
+        Map<String, Object> result = clusterService.queryClusterByCode(1L);
+        logger.info(result.toString());
+        
Assert.assertEquals(Status.QUERY_CLUSTER_BY_CODE_ERROR,result.get(Constants.STATUS));
+
+        
Mockito.when(clusterMapper.queryByClusterCode(1L)).thenReturn(getCluster());
+        result = clusterService.queryClusterByCode(1L);
+        logger.info(result.toString());
+        Assert.assertEquals(Status.SUCCESS,result.get(Constants.STATUS));
+    }
+
+    @Test
+    public void testDeleteClusterByCode() {
+        User loginUser = getGeneralUser();
+        Map<String, Object> result = 
clusterService.deleteClusterByCode(loginUser,1L);
+        logger.info(result.toString());
+        Assert.assertEquals(Status.USER_NO_OPERATION_PERM, 
result.get(Constants.STATUS));
+
+        loginUser = getAdminUser();
+        Mockito.when(clusterMapper.deleteByCode(1L)).thenReturn(1);
+        result = clusterService.deleteClusterByCode(loginUser,1L);
+        logger.info(result.toString());
+        Assert.assertEquals(Status.SUCCESS, result.get(Constants.STATUS));
+    }
+
+    @Test
+    public void testVerifyCluster() {
+        Map<String, Object> result = clusterService.verifyCluster("");
+        logger.info(result.toString());
+        Assert.assertEquals(Status.CLUSTER_NAME_IS_NULL, 
result.get(Constants.STATUS));
+
+        
Mockito.when(clusterMapper.queryByClusterName(clusterName)).thenReturn(getCluster());
+        result = clusterService.verifyCluster(clusterName);
+        logger.info(result.toString());
+        Assert.assertEquals(Status.CLUSTER_NAME_EXISTS, 
result.get(Constants.STATUS));
+    }
+
+    private Cluster getCluster() {
+        Cluster cluster = new Cluster();
+        cluster.setId(1);
+        cluster.setCode(1L);
+        cluster.setName(clusterName);
+        cluster.setConfig(getConfig());
+        cluster.setDescription(getDesc());
+        cluster.setOperator(1);
+        return cluster;
+    }
+
+    /**
+     * create an cluster description
+     */
+    private String getDesc() {
+        return "create an cluster to test ";
+    }
+
+    /**
+     * create an cluster config
+     */
+    private String getConfig() {
+        return "{\"k8s\":\"apiVersion: v1\\nclusters:\\n- cluster:\\n    
certificate-authority-data: LS0tLS1CRUdJTiBDRJUSUZJQ0FURS0tLS0tCg==\\n    
server: https:\\/\\/127.0.0.1:6443\\n  name: kubernetes\\ncontexts:\\n- 
context:\\n    cluster: kubernetes\\n    user: kubernetes-admin\\n  name: 
kubernetes-admin@kubernetes\\ncurrent-context: 
kubernetes-admin@kubernetes\\nkind: Config\\npreferences: {}\\nusers:\\n- name: 
kubernetes-admin\\n  user:\\n    client-certificate-data: LS0tLS1CRUdJTi [...]
+    }
+
+    /**
+     * create general user
+     */
+    private User getGeneralUser() {
+        User loginUser = new User();
+        loginUser.setUserType(UserType.GENERAL_USER);
+        loginUser.setUserName(testUserName);
+        loginUser.setId(1);
+        return loginUser;
+    }
+
+    /**
+     * create admin user
+     */
+    private User getAdminUser() {
+        User loginUser = new User();
+        loginUser.setUserType(UserType.ADMIN_USER);
+        loginUser.setUserName(testUserName);
+        loginUser.setId(1);
+        return loginUser;
+    }
+
+    private List<Cluster> getList() {
+        List<Cluster> list = new ArrayList<>();
+        list.add(getCluster());
+        return list;
+    }
+}
diff --git 
a/dolphinscheduler-common/src/main/java/org/apache/dolphinscheduler/common/utils/ClusterConfUtils.java
 
b/dolphinscheduler-common/src/main/java/org/apache/dolphinscheduler/common/utils/ClusterConfUtils.java
new file mode 100644
index 0000000000..922faefa10
--- /dev/null
+++ 
b/dolphinscheduler-common/src/main/java/org/apache/dolphinscheduler/common/utils/ClusterConfUtils.java
@@ -0,0 +1,48 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You 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 org.apache.dolphinscheduler.common.utils;
+
+import org.apache.dolphinscheduler.spi.utils.StringUtils;
+
+import com.fasterxml.jackson.databind.node.ObjectNode;
+
+/**
+ * cluster conf will include all env type, but only k8s config now
+ */
+public class ClusterConfUtils {
+
+    private static final String K8S_CONFIG = "k8s";
+
+    /**
+     * get k8s
+     *
+     * @param config cluster config in db
+     * @return
+     */
+    public static String getK8sConfig(String config) {
+        if (StringUtils.isEmpty(config)) {
+            return null;
+        }
+        ObjectNode conf = JSONUtils.parseObject(config);
+        if (conf == null) {
+            return null;
+        }
+        return conf.get(K8S_CONFIG).asText();
+    }
+
+}
diff --git 
a/dolphinscheduler-dao/src/main/java/org/apache/dolphinscheduler/dao/entity/Cluster.java
 
b/dolphinscheduler-dao/src/main/java/org/apache/dolphinscheduler/dao/entity/Cluster.java
new file mode 100644
index 0000000000..ee138f3c0d
--- /dev/null
+++ 
b/dolphinscheduler-dao/src/main/java/org/apache/dolphinscheduler/dao/entity/Cluster.java
@@ -0,0 +1,139 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You 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 org.apache.dolphinscheduler.dao.entity;
+
+import java.util.Date;
+
+import com.baomidou.mybatisplus.annotation.IdType;
+import com.baomidou.mybatisplus.annotation.TableId;
+import com.baomidou.mybatisplus.annotation.TableName;
+
+/**
+ * Cluster
+ */
+@TableName("t_ds_cluster")
+public class Cluster {
+
+    @TableId(value = "id", type = IdType.AUTO)
+    private int id;
+
+    /**
+     * cluster code
+     */
+    private Long code;
+
+    /**
+     * cluster name
+     */
+    private String name;
+
+    /**
+     * config content
+     */
+    private String config;
+
+    private String description;
+
+    /**
+     * operator user id
+     */
+    private Integer operator;
+
+    private Date createTime;
+
+    private Date updateTime;
+
+    public int getId() {
+        return id;
+    }
+
+    public void setId(int id) {
+        this.id = id;
+    }
+
+    public String getName() {
+        return name;
+    }
+
+    public void setName(String name) {
+        this.name = name;
+    }
+
+    public Long getCode() {
+        return this.code;
+    }
+
+    public void setCode(Long code) {
+        this.code = code;
+    }
+
+    public String getConfig() {
+        return this.config;
+    }
+
+    public void setConfig(String config) {
+        this.config = config;
+    }
+
+    public String getDescription() {
+        return this.description;
+    }
+
+    public void setDescription(String description) {
+        this.description = description;
+    }
+
+    public Integer getOperator() {
+        return this.operator;
+    }
+
+    public void setOperator(Integer operator) {
+        this.operator = operator;
+    }
+
+    public Date getCreateTime() {
+        return createTime;
+    }
+
+    public void setCreateTime(Date createTime) {
+        this.createTime = createTime;
+    }
+
+    public Date getUpdateTime() {
+        return updateTime;
+    }
+
+    public void setUpdateTime(Date updateTime) {
+        this.updateTime = updateTime;
+    }
+
+    @Override
+    public String toString() {
+        return "Cluster{"
+                + "id= " + id
+                + ", code= " + code
+                + ", name= " + name
+                + ", config= " + config
+                + ", description= " + description
+                + ", operator= " + operator
+                + ", createTime= " + createTime
+                + ", updateTime= " + updateTime
+                + "}";
+    }
+
+}
diff --git 
a/dolphinscheduler-dao/src/main/java/org/apache/dolphinscheduler/dao/mapper/ClusterMapper.java
 
b/dolphinscheduler-dao/src/main/java/org/apache/dolphinscheduler/dao/mapper/ClusterMapper.java
new file mode 100644
index 0000000000..276dfa77b9
--- /dev/null
+++ 
b/dolphinscheduler-dao/src/main/java/org/apache/dolphinscheduler/dao/mapper/ClusterMapper.java
@@ -0,0 +1,71 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You 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 org.apache.dolphinscheduler.dao.mapper;
+
+import org.apache.dolphinscheduler.dao.entity.Cluster;
+
+import org.apache.ibatis.annotations.Param;
+
+import java.util.List;
+
+import com.baomidou.mybatisplus.core.mapper.BaseMapper;
+import com.baomidou.mybatisplus.core.metadata.IPage;
+
+/**
+ * cluster mapper interface
+ */
+public interface ClusterMapper extends BaseMapper<Cluster> {
+
+    /**
+     * query cluster by name
+     *
+     * @param name name
+     * @return cluster
+     */
+    Cluster queryByClusterName(@Param("clusterName") String name);
+
+    /**
+     * query cluster by code
+     *
+     * @param clusterCode clusterCode
+     * @return cluster
+     */
+    Cluster queryByClusterCode(@Param("clusterCode") Long clusterCode);
+
+    /**
+     * query all cluster list
+     * @return cluster list
+     */
+    List<Cluster> queryAllClusterList();
+
+    /**
+     * cluster page
+     * @param page page
+     * @param searchName searchName
+     * @return cluster IPage
+     */
+    IPage<Cluster> queryClusterListPaging(IPage<Cluster> page, 
@Param("searchName") String searchName);
+
+    /**
+     * delete cluster by code
+     *
+     * @param code code
+     * @return int
+     */
+    int deleteByCode(@Param("code") Long code);
+}
diff --git 
a/dolphinscheduler-dao/src/main/resources/org/apache/dolphinscheduler/dao/mapper/ClusterMapper.xml
 
b/dolphinscheduler-dao/src/main/resources/org/apache/dolphinscheduler/dao/mapper/ClusterMapper.xml
new file mode 100644
index 0000000000..c67efba10e
--- /dev/null
+++ 
b/dolphinscheduler-dao/src/main/resources/org/apache/dolphinscheduler/dao/mapper/ClusterMapper.xml
@@ -0,0 +1,55 @@
+<?xml version="1.0" encoding="UTF-8" ?>
+<!--
+  ~ Licensed to the Apache Software Foundation (ASF) under one or more
+  ~ contributor license agreements.  See the NOTICE file distributed with
+  ~ this work for additional information regarding copyright ownership.
+  ~ The ASF licenses this file to You 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.
+  -->
+
+<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" 
"http://mybatis.org/dtd/mybatis-3-mapper.dtd"; >
+<mapper namespace="org.apache.dolphinscheduler.dao.mapper.ClusterMapper">
+    <sql id="baseSql">
+        id, code, name, config, description, operator, create_time, update_time
+    </sql>
+    <select id="queryByClusterName" 
resultType="org.apache.dolphinscheduler.dao.entity.Cluster">
+        select
+        <include refid="baseSql"/>
+        from t_ds_cluster
+        WHERE name = #{clusterName}
+    </select>
+    <select id="queryAllClusterList" 
resultType="org.apache.dolphinscheduler.dao.entity.Cluster">
+        select
+        <include refid="baseSql"/>
+        from t_ds_cluster
+        order by create_time desc
+    </select>
+    <select id="queryClusterListPaging" 
resultType="org.apache.dolphinscheduler.dao.entity.Cluster">
+        select
+        <include refid="baseSql"/>
+        from t_ds_cluster
+        where 1=1
+        <if test="searchName!=null and searchName != ''">
+            and name like concat('%', #{searchName}, '%')
+        </if>
+        order by create_time desc
+    </select>
+    <select id="queryByClusterCode" 
resultType="org.apache.dolphinscheduler.dao.entity.Cluster">
+        select
+        <include refid="baseSql"/>
+        from t_ds_cluster
+        where code = #{clusterCode}
+    </select>
+    <delete id="deleteByCode">
+        delete from t_ds_cluster where code = #{code}
+    </delete>
+</mapper>
diff --git 
a/dolphinscheduler-dao/src/main/resources/sql/dolphinscheduler_h2.sql 
b/dolphinscheduler-dao/src/main/resources/sql/dolphinscheduler_h2.sql
index 5f24d966da..b9dcc6191c 100644
--- a/dolphinscheduler-dao/src/main/resources/sql/dolphinscheduler_h2.sql
+++ b/dolphinscheduler-dao/src/main/resources/sql/dolphinscheduler_h2.sql
@@ -1963,3 +1963,26 @@ CREATE TABLE t_ds_alert_send_status
     UNIQUE KEY alert_send_status_unique (alert_id,alert_plugin_instance_id)
 );
 
+
+--
+-- Table structure for table t_ds_cluster
+--
+DROP TABLE IF EXISTS t_ds_cluster CASCADE;
+CREATE TABLE t_ds_cluster
+(
+    id          int       NOT NULL AUTO_INCREMENT,
+    code        bigint(20) NOT NULL,
+    name        varchar(100)       DEFAULT NULL,
+    config      text               DEFAULT NULL,
+    description text,
+    operator    int                DEFAULT NULL,
+    create_time timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP,
+    update_time timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE 
CURRENT_TIMESTAMP,
+    PRIMARY KEY (id),
+    UNIQUE KEY cluster_name_unique (name),
+    UNIQUE KEY cluster_code_unique (code)
+);
+
+INSERT INTO `t_ds_cluster`
+(`id`,`code`,`name`,`config`,`description`,`operator`,`create_time`,`update_time`)
+VALUES (100, 100, 'ds_null_k8s', '{"k8s":"ds_null_k8s"}', 'test', 1, 
'2021-03-03 11:31:24.0', '2021-03-03 11:31:24.0');
diff --git 
a/dolphinscheduler-dao/src/main/resources/sql/dolphinscheduler_mysql.sql 
b/dolphinscheduler-dao/src/main/resources/sql/dolphinscheduler_mysql.sql
index 2bbf3c2055..f5920cf6d1 100644
--- a/dolphinscheduler-dao/src/main/resources/sql/dolphinscheduler_mysql.sql
+++ b/dolphinscheduler-dao/src/main/resources/sql/dolphinscheduler_mysql.sql
@@ -1938,3 +1938,22 @@ CREATE TABLE t_ds_alert_send_status (
   PRIMARY KEY (`id`),
   UNIQUE KEY `alert_send_status_unique` (`alert_id`,`alert_plugin_instance_id`)
 ) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8;
+
+
+-- ----------------------------
+-- Table structure for t_ds_cluster
+-- ----------------------------
+DROP TABLE IF EXISTS `t_ds_cluster`;
+CREATE TABLE `t_ds_cluster` (
+  `id` bigint(11) NOT NULL AUTO_INCREMENT COMMENT 'id',
+  `code` bigint(20)  DEFAULT NULL COMMENT 'encoding',
+  `name` varchar(100) NOT NULL COMMENT 'cluster name',
+  `config` text NULL DEFAULT NULL COMMENT 'this config contains many cluster 
variables config',
+  `description` text NULL DEFAULT NULL COMMENT 'the details',
+  `operator` int(11) DEFAULT NULL COMMENT 'operator user id',
+  `create_time` timestamp NULL DEFAULT CURRENT_TIMESTAMP,
+  `update_time` timestamp NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE 
CURRENT_TIMESTAMP,
+  PRIMARY KEY (`id`),
+  UNIQUE KEY `cluster_name_unique` (`name`),
+  UNIQUE KEY `cluster_code_unique` (`code`)
+) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8;
diff --git 
a/dolphinscheduler-dao/src/main/resources/sql/dolphinscheduler_postgresql.sql 
b/dolphinscheduler-dao/src/main/resources/sql/dolphinscheduler_postgresql.sql
index fabfda8bb3..6bf671ecf8 100644
--- 
a/dolphinscheduler-dao/src/main/resources/sql/dolphinscheduler_postgresql.sql
+++ 
b/dolphinscheduler-dao/src/main/resources/sql/dolphinscheduler_postgresql.sql
@@ -1938,3 +1938,23 @@ CREATE TABLE t_ds_alert_send_status (
     PRIMARY KEY (id),
     CONSTRAINT alert_send_status_unique UNIQUE 
(alert_id,alert_plugin_instance_id)
 );
+
+
+--
+-- Table structure for table t_ds_cluster
+--
+
+DROP TABLE IF EXISTS t_ds_cluster;
+CREATE TABLE t_ds_cluster (
+    id serial NOT NULL,
+    code bigint NOT NULL,
+    name varchar(100) DEFAULT NULL,
+    config text DEFAULT NULL,
+    description text,
+    operator int DEFAULT NULL,
+    create_time timestamp DEFAULT NULL,
+    update_time timestamp DEFAULT NULL,
+    PRIMARY KEY (id),
+    CONSTRAINT cluster_name_unique UNIQUE (name),
+    CONSTRAINT cluster_code_unique UNIQUE (code)
+);
diff --git 
a/dolphinscheduler-dao/src/main/resources/sql/upgrade/3.0.0_schema/mysql/dolphinscheduler_ddl.sql
 
b/dolphinscheduler-dao/src/main/resources/sql/upgrade/3.0.0_schema/mysql/dolphinscheduler_ddl.sql
index 6939da6626..fc750ce6b0 100644
--- 
a/dolphinscheduler-dao/src/main/resources/sql/upgrade/3.0.0_schema/mysql/dolphinscheduler_ddl.sql
+++ 
b/dolphinscheduler-dao/src/main/resources/sql/upgrade/3.0.0_schema/mysql/dolphinscheduler_ddl.sql
@@ -280,4 +280,22 @@ CREATE TABLE `t_ds_relation_namespace_user` (
   PRIMARY KEY (`id`),
   KEY `user_id_index` (`user_id`),
   UNIQUE KEY `namespace_user_unique` (`user_id`,`namespace_id`)
-) ENGINE=InnoDB AUTO_INCREMENT= 1 DEFAULT CHARSET= utf8;
\ No newline at end of file
+) ENGINE=InnoDB AUTO_INCREMENT= 1 DEFAULT CHARSET= utf8;
+
+-- ----------------------------
+-- Table structure for t_ds_cluster
+-- ----------------------------
+DROP TABLE IF EXISTS `t_ds_cluster`;
+CREATE TABLE `t_ds_cluster` (
+  `id` bigint(11) NOT NULL AUTO_INCREMENT COMMENT 'id',
+  `code` bigint(20)  DEFAULT NULL COMMENT 'encoding',
+  `name` varchar(100) NOT NULL COMMENT 'cluster name',
+  `config` text NULL DEFAULT NULL COMMENT 'this config contains many cluster 
variables config',
+  `description` text NULL DEFAULT NULL COMMENT 'the details',
+  `operator` int(11) DEFAULT NULL COMMENT 'operator user id',
+  `create_time` timestamp NULL DEFAULT CURRENT_TIMESTAMP,
+  `update_time` timestamp NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE 
CURRENT_TIMESTAMP,
+  PRIMARY KEY (`id`),
+  UNIQUE KEY `cluster_name_unique` (`name`),
+  UNIQUE KEY `cluster_code_unique` (`code`)
+) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8;
\ No newline at end of file
diff --git 
a/dolphinscheduler-dao/src/main/resources/sql/upgrade/3.0.0_schema/postgresql/dolphinscheduler_ddl.sql
 
b/dolphinscheduler-dao/src/main/resources/sql/upgrade/3.0.0_schema/postgresql/dolphinscheduler_ddl.sql
index 86471d71b1..c1a565ccad 100644
--- 
a/dolphinscheduler-dao/src/main/resources/sql/upgrade/3.0.0_schema/postgresql/dolphinscheduler_ddl.sql
+++ 
b/dolphinscheduler-dao/src/main/resources/sql/upgrade/3.0.0_schema/postgresql/dolphinscheduler_ddl.sql
@@ -249,6 +249,20 @@ EXECUTE 'CREATE TABLE IF NOT EXISTS '|| 
quote_ident(v_schema) ||'."t_ds_relation
     CONSTRAINT namespace_user_unique UNIQUE (user_id,namespace_id)
 )';
 
+EXECUTE 'CREATE TABLE IF NOT EXISTS '|| quote_ident(v_schema) 
||'."t_ds_cluster" (
+    id serial NOT NULL,
+    code bigint NOT NULL,
+    name varchar(100) DEFAULT NULL,
+    config text DEFAULT NULL,
+    description text,
+    operator int DEFAULT NULL,
+    create_time timestamp DEFAULT NULL,
+    update_time timestamp DEFAULT NULL,
+    PRIMARY KEY (id),
+    CONSTRAINT cluster_name_unique UNIQUE (name),
+    CONSTRAINT cluster_code_unique UNIQUE (code)
+)';
+
 return 'Success!';
 exception when others then
                ---Raise EXCEPTION '(%)',SQLERRM;
diff --git 
a/dolphinscheduler-dao/src/test/java/org/apache/dolphinscheduler/dao/mapper/ClusterMapperTest.java
 
b/dolphinscheduler-dao/src/test/java/org/apache/dolphinscheduler/dao/mapper/ClusterMapperTest.java
new file mode 100644
index 0000000000..5ade44f625
--- /dev/null
+++ 
b/dolphinscheduler-dao/src/test/java/org/apache/dolphinscheduler/dao/mapper/ClusterMapperTest.java
@@ -0,0 +1,191 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You 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 org.apache.dolphinscheduler.dao.mapper;
+
+import org.apache.dolphinscheduler.dao.BaseDaoTest;
+import org.apache.dolphinscheduler.dao.entity.Cluster;
+
+import java.util.Date;
+import java.util.List;
+
+import org.junit.After;
+import org.junit.Assert;
+import org.junit.Before;
+import org.junit.Test;
+import org.springframework.beans.factory.annotation.Autowired;
+
+import com.baomidou.mybatisplus.core.metadata.IPage;
+import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
+
+public class ClusterMapperTest extends BaseDaoTest {
+
+    @Autowired
+    ClusterMapper clusterMapper;
+
+    /**
+     * insert
+     *
+     * @return Cluster
+     */
+    private Cluster insertOne() {
+        //insertOne
+        Cluster cluster = new Cluster();
+        cluster.setName("testCluster");
+        cluster.setCode(1L);
+        cluster.setOperator(1);
+        cluster.setConfig(getConfig());
+        cluster.setDescription(getDesc());
+        cluster.setCreateTime(new Date());
+        cluster.setUpdateTime(new Date());
+        clusterMapper.insert(cluster);
+        return cluster;
+    }
+
+    @Before
+    public void setUp() {
+        clearTestData();
+    }
+
+    @After
+    public void after() {
+        clearTestData();
+    }
+
+    public void clearTestData() {
+        clusterMapper.queryAllClusterList().stream().forEach(cluster -> {
+            clusterMapper.deleteByCode(cluster.getCode());
+        });
+    }
+
+    /**
+     * test update
+     */
+    @Test
+    public void testUpdate() {
+        //insertOne
+        Cluster cluster = insertOne();
+        cluster.setDescription("new description info");
+        //update
+        int update = clusterMapper.updateById(cluster);
+        Assert.assertEquals(update, 1);
+    }
+
+    /**
+     * test delete
+     */
+    @Test
+    public void testDelete() {
+        Cluster cluster = insertOne();
+        int delete = clusterMapper.deleteById(cluster.getId());
+        Assert.assertEquals(delete, 1);
+    }
+
+    /**
+     * test query
+     */
+    @Test
+    public void testQuery() {
+        insertOne();
+        //query
+        List<Cluster> clusters = clusterMapper.selectList(null);
+        Assert.assertEquals(clusters.size(), 1);
+    }
+
+    /**
+     * test query cluster by name
+     */
+    @Test
+    public void testQueryByClusterName() {
+        Cluster entity = insertOne();
+        Cluster cluster = clusterMapper.queryByClusterName(entity.getName());
+        Assert.assertEquals(entity.toString(),cluster.toString());
+    }
+
+    /**
+     * test query cluster by code
+     */
+    @Test
+    public void testQueryByClusterCode() {
+        Cluster entity = insertOne();
+        Cluster cluster = clusterMapper.queryByClusterCode(entity.getCode());
+        Assert.assertEquals(entity.toString(),cluster.toString());
+    }
+
+    /**
+     * test query all clusters
+     */
+    @Test
+    public void testQueryAllClusterList() {
+        Cluster entity = insertOne();
+        List<Cluster> clusters = clusterMapper.queryAllClusterList();
+        Assert.assertEquals(clusters.size(), 1);
+        Assert.assertEquals(entity.toString(),clusters.get(0).toString());
+    }
+
+    /**
+     * test query cluster list paging
+     */
+    @Test
+    public void testQueryClusterListPaging() {
+        Cluster entity = insertOne();
+        Page<Cluster> page = new Page<>(1, 10);
+        IPage<Cluster> clusterIPage = 
clusterMapper.queryClusterListPaging(page,"");
+        List<Cluster> clusterList = clusterIPage.getRecords();
+        Assert.assertEquals(clusterList.size(), 1);
+
+        clusterIPage = clusterMapper.queryClusterListPaging(page,"abc");
+        clusterList = clusterIPage.getRecords();
+        Assert.assertEquals(clusterList.size(), 0);
+    }
+
+    /**
+     * test query all clusters
+     */
+    @Test
+    public void testDeleteByCode() {
+        Cluster entity = insertOne();
+        int delete = clusterMapper.deleteByCode(entity.getCode());
+        Assert.assertEquals(delete, 1);
+    }
+
+    private String getDesc() {
+        return "create an cluster to test ";
+    }
+
+    /**
+     * create an cluster config
+     */
+    private String getConfig() {
+        return "export HADOOP_HOME=/opt/hadoop-2.6.5\n"
+                + "export HADOOP_CONF_DIR=/etc/hadoop/conf\n"
+                + "export SPARK_HOME1=/opt/soft/spark1\n"
+                + "export SPARK_HOME2=/opt/soft/spark2\n"
+                + "export PYTHON_HOME=/opt/soft/python\n"
+                + "export JAVA_HOME=/opt/java/jdk1.8.0_181-amd64\n"
+                + "export HIVE_HOME=/opt/soft/hive\n"
+                + "export FLINK_HOME=/opt/soft/flink\n"
+                + "export DATAX_HOME=/opt/soft/datax\n"
+                + "export YARN_CONF_DIR=\"/etc/hadoop/conf\"\n"
+                + "\n"
+                + "export 
PATH=$HADOOP_HOME/bin:$SPARK_HOME1/bin:$SPARK_HOME2/bin:$PYTHON_HOME/bin:$JAVA_HOME/bin:$HIVE_HOME/bin:$FLINK_HOME/bin:$DATAX_HOME/bin:$PATH\n"
+                + "\n"
+                + "export HADOOP_CLASSPATH=`hadoop classpath`\n"
+                + "\n"
+                + "#echo \"HADOOP_CLASSPATH=\"$HADOOP_CLASSPATH";
+    }
+}
\ No newline at end of file
diff --git 
a/dolphinscheduler-e2e/dolphinscheduler-e2e-case/src/test/java/org/apache/dolphinscheduler/e2e/cases/ClusterE2ETest.java
 
b/dolphinscheduler-e2e/dolphinscheduler-e2e-case/src/test/java/org/apache/dolphinscheduler/e2e/cases/ClusterE2ETest.java
new file mode 100644
index 0000000000..18c6a69ec0
--- /dev/null
+++ 
b/dolphinscheduler-e2e/dolphinscheduler-e2e-case/src/test/java/org/apache/dolphinscheduler/e2e/cases/ClusterE2ETest.java
@@ -0,0 +1,123 @@
+/*
+ * Licensed to Apache Software Foundation (ASF) under one or more contributor
+ * license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright
+ * ownership. Apache Software Foundation (ASF) licenses this file to you 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 org.apache.dolphinscheduler.e2e.cases;
+
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.awaitility.Awaitility.await;
+
+import org.apache.dolphinscheduler.e2e.core.DolphinScheduler;
+import org.apache.dolphinscheduler.e2e.pages.LoginPage;
+import org.apache.dolphinscheduler.e2e.pages.security.ClusterPage;
+import org.apache.dolphinscheduler.e2e.pages.security.SecurityPage;
+
+import org.junit.jupiter.api.BeforeAll;
+import org.junit.jupiter.api.Order;
+import org.junit.jupiter.api.Test;
+import org.openqa.selenium.By;
+import org.openqa.selenium.WebElement;
+import org.openqa.selenium.remote.RemoteWebDriver;
+
+@DolphinScheduler(composeFiles = "docker/basic/docker-compose.yaml")
+class ClusterE2ETest {
+
+    private static final String clusterName = "test_cluster_name";
+    private static final String clusterConfig = "test_cluster_config";
+    private static final String clusterDesc = "test_cluster_desc";
+
+    private static final String editClusterName = "edit_cluster_name";
+    private static final String editClusterConfig = "edit_cluster_config";
+    private static final String editClusterDesc = "edit_cluster_desc";
+
+    private static RemoteWebDriver browser;
+
+    @BeforeAll
+    public static void setup() {
+        new LoginPage(browser)
+                .login("admin", "dolphinscheduler123")
+                .goToNav(SecurityPage.class)
+                .goToTab(ClusterPage.class)
+        ;
+    }
+
+    @Test
+    @Order(10)
+    void testCreateCluster() {
+        final ClusterPage page = new ClusterPage(browser);
+        page.create(clusterName, clusterConfig, clusterDesc);
+
+        await().untilAsserted(() -> {
+            browser.navigate().refresh();
+            assertThat(page.clusterList())
+                    .as("Cluster list should contain newly-created cluster")
+                    .extracting(WebElement::getText)
+                    .anyMatch(it -> it.contains(clusterName));
+        });
+    }
+
+    @Test
+    @Order(20)
+    void testCreateDuplicateCluster() {
+        final ClusterPage page = new ClusterPage(browser);
+        page.create(clusterName, clusterConfig, clusterDesc);
+
+        await().untilAsserted(() ->
+                assertThat(browser.findElement(By.tagName("body")).getText())
+                        .contains("already exists")
+        );
+
+        page.createClusterForm().buttonCancel().click();
+    }
+
+    @Test
+    @Order(30)
+    void testEditCluster() {
+        final ClusterPage page = new ClusterPage(browser);
+        page.update(clusterName, editClusterName, editClusterConfig, 
editClusterDesc);
+
+        await().untilAsserted(() -> {
+            browser.navigate().refresh();
+            assertThat(page.clusterList())
+                    .as("Cluster list should contain newly-modified cluster")
+                    .extracting(WebElement::getText)
+                    .anyMatch(it -> it.contains(editClusterName));
+        });
+    }
+
+    @Test
+    @Order(40)
+    void testDeleteCluster() {
+        final ClusterPage page = new ClusterPage(browser);
+
+        page.delete(editClusterName);
+
+        await().untilAsserted(() -> {
+            browser.navigate().refresh();
+
+            assertThat(
+                    page.clusterList()
+            )
+            .as("Cluster list should not contain deleted cluster")
+            .noneMatch(
+                    it -> it.getText().contains(clusterName) || 
it.getText().contains(editClusterName)
+            );
+        });
+    }
+}
diff --git 
a/dolphinscheduler-e2e/dolphinscheduler-e2e-case/src/test/java/org/apache/dolphinscheduler/e2e/pages/security/ClusterPage.java
 
b/dolphinscheduler-e2e/dolphinscheduler-e2e-case/src/test/java/org/apache/dolphinscheduler/e2e/pages/security/ClusterPage.java
new file mode 100644
index 0000000000..f95439768a
--- /dev/null
+++ 
b/dolphinscheduler-e2e/dolphinscheduler-e2e-case/src/test/java/org/apache/dolphinscheduler/e2e/pages/security/ClusterPage.java
@@ -0,0 +1,151 @@
+/*
+ * Licensed to Apache Software Foundation (ASF) under one or more contributor
+ * license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright
+ * ownership. Apache Software Foundation (ASF) licenses this file to you 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 org.apache.dolphinscheduler.e2e.pages.security;
+
+import org.apache.dolphinscheduler.e2e.pages.common.NavBarPage;
+
+import java.util.List;
+
+import org.openqa.selenium.By;
+import org.openqa.selenium.JavascriptExecutor;
+import org.openqa.selenium.Keys;
+import org.openqa.selenium.WebElement;
+import org.openqa.selenium.remote.RemoteWebDriver;
+import org.openqa.selenium.support.FindBy;
+import org.openqa.selenium.support.FindBys;
+import org.openqa.selenium.support.PageFactory;
+import org.openqa.selenium.support.ui.ExpectedConditions;
+import org.openqa.selenium.support.ui.WebDriverWait;
+
+import lombok.Getter;
+
+@Getter
+public final class ClusterPage extends NavBarPage implements SecurityPage.Tab {
+    @FindBy(className = "btn-create-cluster")
+    private WebElement buttonCreateCluster;
+
+    @FindBy(className = "items")
+    private List<WebElement> clusterList;
+
+    @FindBys({
+        @FindBy(className = "n-popconfirm__action"),
+        @FindBy(className = "n-button--primary-type"),
+    })
+    private WebElement buttonConfirm;
+
+    private final ClusterForm createClusterForm;
+    private final ClusterForm editClusterForm;
+
+    public ClusterPage(RemoteWebDriver driver) {
+        super(driver);
+        createClusterForm = new ClusterForm();
+        editClusterForm = new ClusterForm();
+    }
+
+    public ClusterPage create(String name, String config, String desc) {
+        buttonCreateCluster().click();
+        createClusterForm().inputClusterName().sendKeys(name);
+        createClusterForm().inputClusterConfig().sendKeys(config);
+        createClusterForm().inputClusterDesc().sendKeys(desc);
+
+        createClusterForm().buttonSubmit().click();
+        return this;
+    }
+
+    public ClusterPage update(String oldName, String name, String config, 
String desc) {
+        clusterList()
+                .stream()
+                .filter(it -> 
it.findElement(By.className("cluster-name")).getAttribute("innerHTML").contains(oldName))
+                .flatMap(it -> it.findElements(By.className("edit")).stream())
+                .filter(WebElement::isDisplayed)
+                .findFirst()
+                .orElseThrow(() -> new RuntimeException("No edit button in 
cluster list"))
+                .click();
+
+
+        editClusterForm().inputClusterName().sendKeys(Keys.CONTROL + "a");
+        editClusterForm().inputClusterName().sendKeys(Keys.BACK_SPACE);
+        editClusterForm().inputClusterName().sendKeys(name);
+
+        editClusterForm().inputClusterConfig().sendKeys(Keys.CONTROL + "a");
+        editClusterForm().inputClusterConfig().sendKeys(Keys.BACK_SPACE);
+        editClusterForm().inputClusterConfig().sendKeys(config);
+
+        editClusterForm().inputClusterDesc().sendKeys(Keys.CONTROL + "a");
+        editClusterForm().inputClusterDesc().sendKeys(Keys.BACK_SPACE);
+        editClusterForm().inputClusterDesc().sendKeys(desc);
+
+        editClusterForm().buttonSubmit().click();
+
+        return this;
+    }
+
+    public ClusterPage delete(String name) {
+        clusterList()
+                .stream()
+                .filter(it -> it.getText().contains(name))
+                .flatMap(it -> 
it.findElements(By.className("delete")).stream())
+                .filter(WebElement::isDisplayed)
+                .findFirst()
+                .orElseThrow(() -> new RuntimeException("No delete button in 
cluster list"))
+                .click();
+
+        ((JavascriptExecutor) driver).executeScript("arguments[0].click();", 
buttonConfirm());
+
+        return this;
+    }
+
+    @Getter
+    public class ClusterForm {
+        ClusterForm() {
+            PageFactory.initElements(driver, this);
+        }
+
+        @FindBys({
+            @FindBy(className = "input-cluster-name"),
+            @FindBy(tagName = "input"),
+        })
+        private WebElement inputClusterName;
+
+        @FindBys({
+            @FindBy(className = "input-cluster-config"),
+            @FindBy(tagName = "textarea"),
+        })
+        private WebElement inputClusterConfig;
+
+        @FindBys({
+            @FindBy(className = "input-cluster-desc"),
+            @FindBy(tagName = "input"),
+        })
+        private WebElement inputClusterDesc;
+
+        @FindBys({
+            @FindBy(className = "n-base-selection-tags"),
+            @FindBy(className = "n-tag__content"),
+        })
+        private WebElement selectedWorkerGroup;
+
+        @FindBy(className = "btn-submit")
+        private WebElement buttonSubmit;
+
+        @FindBy(className = "btn-cancel")
+        private WebElement buttonCancel;
+    }
+}
diff --git 
a/dolphinscheduler-e2e/dolphinscheduler-e2e-case/src/test/java/org/apache/dolphinscheduler/e2e/pages/security/SecurityPage.java
 
b/dolphinscheduler-e2e/dolphinscheduler-e2e-case/src/test/java/org/apache/dolphinscheduler/e2e/pages/security/SecurityPage.java
index 1f8565090b..f6f599de71 100644
--- 
a/dolphinscheduler-e2e/dolphinscheduler-e2e-case/src/test/java/org/apache/dolphinscheduler/e2e/pages/security/SecurityPage.java
+++ 
b/dolphinscheduler-e2e/dolphinscheduler-e2e-case/src/test/java/org/apache/dolphinscheduler/e2e/pages/security/SecurityPage.java
@@ -52,9 +52,12 @@ public class SecurityPage extends NavBarPage implements 
NavBarItem {
     private WebElement menuEnvironmentManage;
 
     @FindBy(css = ".tab-vertical > .n-menu-item:nth-child(8) > 
.n-menu-item-content")
-    private WebElement menuNamespaceManage;
+    private WebElement menuClusterManage;
 
     @FindBy(css = ".tab-vertical > .n-menu-item:nth-child(9) > 
.n-menu-item-content")
+    private WebElement menuNamespaceManage;
+
+    @FindBy(css = ".tab-vertical > .n-menu-item:nth-child(10) > 
.n-menu-item-content")
     private WebElement menuTokenManage;
 
     public SecurityPage(RemoteWebDriver driver) {
@@ -97,6 +100,13 @@ public class SecurityPage extends NavBarPage implements 
NavBarItem {
             return tab.cast(new EnvironmentPage(driver));
         }
 
+        if (tab == ClusterPage.class) {
+            new WebDriverWait(driver, 
10).until(ExpectedConditions.urlContains("/security"));
+            new WebDriverWait(driver, 
60).until(ExpectedConditions.elementToBeClickable(menuClusterManage));
+            ((JavascriptExecutor) 
driver).executeScript("arguments[0].click();", menuClusterManage());
+            return tab.cast(new ClusterPage(driver));
+        }
+
         if (tab == TokenPage.class) {
             new WebDriverWait(driver, 
10).until(ExpectedConditions.urlContains("/security"));
             new WebDriverWait(driver, 
60).until(ExpectedConditions.elementToBeClickable(menuTokenManage));
diff --git a/dolphinscheduler-ui/src/layouts/content/use-dataList.ts 
b/dolphinscheduler-ui/src/layouts/content/use-dataList.ts
index b1045c5156..a93293fc7b 100644
--- a/dolphinscheduler-ui/src/layouts/content/use-dataList.ts
+++ b/dolphinscheduler-ui/src/layouts/content/use-dataList.ts
@@ -46,7 +46,8 @@ import {
   ContainerOutlined,
   ApartmentOutlined,
   BarsOutlined,
-  CloudServerOutlined
+  CloudServerOutlined,
+  ClusterOutlined
 } from '@vicons/antd'
 import { useRoute } from 'vue-router'
 import { useUserStore } from '@/store/user/user'
@@ -290,6 +291,11 @@ export function useDataList() {
                   key: '/security/environment-manage',
                   icon: renderIcon(EnvironmentOutlined)
                 },
+                {
+                  label: t('menu.cluster_manage'),
+                  key: '/security/cluster-manage',
+                  icon: renderIcon(ClusterOutlined)
+                },
                 {
                   label: t('menu.k8s_namespace_manage'),
                   key: '/security/k8s-namespace-manage',
diff --git a/dolphinscheduler-ui/src/locales/en_US/menu.ts 
b/dolphinscheduler-ui/src/locales/en_US/menu.ts
index a731f7b8ac..c605257384 100644
--- a/dolphinscheduler-ui/src/locales/en_US/menu.ts
+++ b/dolphinscheduler-ui/src/locales/en_US/menu.ts
@@ -48,6 +48,7 @@ export default {
   worker_group_manage: 'Worker Group Manage',
   yarn_queue_manage: 'Yarn Queue Manage',
   environment_manage: 'Environment Manage',
+  cluster_manage: 'Cluster Manage',
   k8s_namespace_manage: 'K8S Namespace Manage',
   token_manage: 'Token Manage',
   task_group_manage: 'Task Group Manage',
diff --git a/dolphinscheduler-ui/src/locales/en_US/project.ts 
b/dolphinscheduler-ui/src/locales/en_US/project.ts
index 962be5818c..df289e5da2 100644
--- a/dolphinscheduler-ui/src/locales/en_US/project.ts
+++ b/dolphinscheduler-ui/src/locales/en_US/project.ts
@@ -193,9 +193,9 @@ export default {
     project_name: 'Project Name',
     project_tips: 'Please select project name',
     workflow_relation_no_data_result_title:
-      'Can not find any relations of workflows.',
+        'Can not find any relations of workflows.',
     workflow_relation_no_data_result_desc:
-      'There is not any workflows. Please create a workflow, and then visit 
this page again.'
+        'There is not any workflows. Please create a workflow, and then visit 
this page again.'
   },
   task: {
     cancel_full_screen: 'Cancel full screen',
@@ -308,7 +308,7 @@ export default {
     task_priority: 'Task priority',
     worker_group: 'Worker group',
     worker_group_tips:
-      'The Worker group no longer exists, please select the correct Worker 
group!',
+        'The Worker group no longer exists, please select the correct Worker 
group!',
     environment_name: 'Environment Name',
     task_group_name: 'Task group name',
     task_group_queue_priority: 'Priority',
@@ -335,11 +335,11 @@ export default {
     success: 'Success',
     failed: 'Failed',
     backfill_tips:
-      'The newly created sub-Process has not yet been executed and cannot 
enter the sub-Process',
+        'The newly created sub-Process has not yet been executed and cannot 
enter the sub-Process',
     task_instance_tips:
-      'The task has not been executed and cannot enter the sub-Process',
+        'The task has not been executed and cannot enter the sub-Process',
     branch_tips:
-      'Cannot select the same node for successful branch flow and failed 
branch flow',
+        'Cannot select the same node for successful branch flow and failed 
branch flow',
     timeout_alarm: 'Timeout alarm',
     timeout_strategy: 'Timeout strategy',
     timeout_strategy_tips: 'Timeout strategy must be selected',
@@ -398,8 +398,8 @@ export default {
     parallelism_tips: 'Please enter Parallelism',
     parallelism_number_tips: 'Parallelism number should be positive integer',
     parallelism_complement_tips:
-      'If there are a large number of tasks requiring complement, you can use 
the custom parallelism to ' +
-      'set the complement task thread to a reasonable value to avoid too large 
impact on the server.',
+        'If there are a large number of tasks requiring complement, you can 
use the custom parallelism to ' +
+        'set the complement task thread to a reasonable value to avoid too 
large impact on the server.',
     task_manager_number: 'TaskManager Number',
     task_manager_number_tips: 'Please enter TaskManager number',
     http_url: 'Http Url',
@@ -434,7 +434,7 @@ export default {
     procedure_method: 'SQL Statement',
     procedure_method_tips: 'Please enter the procedure script',
     procedure_method_snippet:
-      '--Please enter the procedure script \n\n--call procedure:call 
<procedure-name>[(<arg1>,<arg2>, ...)]\n\n--call function:?= call 
<procedure-name>[(<arg1>,<arg2>, ...)]',
+        '--Please enter the procedure script \n\n--call procedure:call 
<procedure-name>[(<arg1>,<arg2>, ...)]\n\n--call function:?= call 
<procedure-name>[(<arg1>,<arg2>, ...)]',
     start: 'Start',
     edit: 'Edit',
     copy: 'Copy',
@@ -500,7 +500,7 @@ export default {
     sea_tunnel_master_url: 'Master URL',
     sea_tunnel_queue: 'Queue',
     sea_tunnel_master_url_tips:
-      'Please enter the master url, e.g., 127.0.0.1:7077',
+        'Please enter the master url, e.g., 127.0.0.1:7077',
     add_pre_task_check_condition: 'Add pre task check condition',
     switch_condition: 'Condition',
     switch_branch_flow: 'Branch Flow',
@@ -615,24 +615,24 @@ export default {
       'Please enter the paragraph id of your zeppelin paragraph',
     jupyter_conda_env_name: 'condaEnvName',
     jupyter_conda_env_name_tips:
-      'Please enter the conda environment name of papermill',
+        'Please enter the conda environment name of papermill',
     jupyter_input_note_path: 'inputNotePath',
     jupyter_input_note_path_tips: 'Please enter the input jupyter note path',
     jupyter_output_note_path: 'outputNotePath',
     jupyter_output_note_path_tips: 'Please enter the output jupyter note path',
     jupyter_parameters: 'parameters',
     jupyter_parameters_tips:
-      'Please enter the parameters for jupyter parameterization',
+        'Please enter the parameters for jupyter parameterization',
     jupyter_kernel: 'kernel',
     jupyter_kernel_tips: 'Please enter the jupyter kernel name',
     jupyter_engine: 'engine',
     jupyter_engine_tips: 'Please enter the engine name',
     jupyter_execution_timeout: 'executionTimeout',
     jupyter_execution_timeout_tips:
-      'Please enter the execution timeout for each jupyter note cell',
+        'Please enter the execution timeout for each jupyter note cell',
     jupyter_start_timeout: 'startTimeout',
     jupyter_start_timeout_tips:
-      'Please enter the start timeout for jupyter kernel',
+        'Please enter the start timeout for jupyter kernel',
     jupyter_others: 'others',
     jupyter_others_tips:
       'Please enter the other options you need for papermill',
@@ -645,7 +645,7 @@ export default {
     mlflow_isSearchParams: 'Search Parameters',
     mlflow_dataPath: 'Data Path',
     mlflow_dataPath_tips:
-      ' The absolute path of the file or folder. Ends with .csv for file or 
contain train.csv and test.csv for folder',
+        ' The absolute path of the file or folder. Ends with .csv for file or 
contain train.csv and test.csv for folder',
     mlflow_dataPath_error_tips: ' data data can not be empty ',
     mlflow_experimentName: 'Experiment Name',
     mlflow_experimentName_tips: 'experiment_001',
@@ -698,4 +698,4 @@ export default {
       'Please enter threshold number is needed',
     please_enter_comparison_title: 'please select comparison title'
   }
-}
\ No newline at end of file
+}
diff --git a/dolphinscheduler-ui/src/locales/en_US/security.ts 
b/dolphinscheduler-ui/src/locales/en_US/security.ts
index 19b82f6665..cde8692d9b 100644
--- a/dolphinscheduler-ui/src/locales/en_US/security.ts
+++ b/dolphinscheduler-ui/src/locales/en_US/security.ts
@@ -98,6 +98,26 @@ export default {
     environment_description_tips: 'Please enter your environment description',
     worker_group_tips: 'Please select worker group'
   },
+  cluster: {
+    create_cluster: 'Create Cluster',
+    edit_cluster: 'Edit Cluster',
+    search_tips: 'Please enter keywords',
+    edit: 'Edit',
+    delete: 'Delete',
+    cluster_name: 'Cluster Name',
+    cluster_components: 'Cluster Components',
+    cluster_config: 'Cluster Config',
+    kubernetes_config: 'Kubernetes Config',
+    yarn_config: 'Yarn Config',
+    cluster_desc: 'Cluster Desc',
+    create_time: 'Create Time',
+    update_time: 'Update Time',
+    operation: 'Operation',
+    delete_confirm: 'Delete?',
+    cluster_name_tips: 'Please enter your cluster name',
+    cluster_config_tips: 'Please enter your cluster config',
+    cluster_description_tips: 'Please enter your cluster description'
+  },
   token: {
     create_token: 'Create Token',
     edit_token: 'Edit Token',
diff --git a/dolphinscheduler-ui/src/locales/zh_CN/menu.ts 
b/dolphinscheduler-ui/src/locales/zh_CN/menu.ts
index f5395beb54..b90bf7510f 100644
--- a/dolphinscheduler-ui/src/locales/zh_CN/menu.ts
+++ b/dolphinscheduler-ui/src/locales/zh_CN/menu.ts
@@ -48,6 +48,7 @@ export default {
   worker_group_manage: 'Worker分组管理',
   yarn_queue_manage: 'Yarn队列管理',
   environment_manage: '环境管理',
+  cluster_manage: '集群管理',
   k8s_namespace_manage: 'K8S命名空间管理',
   token_manage: '令牌管理',
   task_group_manage: '任务组管理',
diff --git a/dolphinscheduler-ui/src/locales/zh_CN/project.ts 
b/dolphinscheduler-ui/src/locales/zh_CN/project.ts
index b2b49add35..51b0982d36 100644
--- a/dolphinscheduler-ui/src/locales/zh_CN/project.ts
+++ b/dolphinscheduler-ui/src/locales/zh_CN/project.ts
@@ -195,7 +195,7 @@ export default {
     project_tips: '请选择项目',
     workflow_relation_no_data_result_title: '工作流关系不存在',
     workflow_relation_no_data_result_desc:
-      '目前没有任何工作流,请先创建工作流,再访问该页面'
+        '目前没有任何工作流,请先创建工作流,再访问该页面'
   },
   task: {
     cancel_full_screen: '取消全屏',
@@ -393,7 +393,7 @@ export default {
     parallelism_tips: '请输入并行度',
     parallelism_number_tips: '并行度必须为正整数',
     parallelism_complement_tips:
-      '如果存在大量任务需要补数时,可以利用自定义并行度将补数的任务线程设置成合理的数值,避免对服务器造成过大的影响',
+        '如果存在大量任务需要补数时,可以利用自定义并行度将补数的任务线程设置成合理的数值,避免对服务器造成过大的影响',
     task_manager_number: 'TaskManager数量',
     task_manager_number_tips: '请输入TaskManager数量',
     http_url: '请求地址',
@@ -428,7 +428,7 @@ export default {
     procedure_method: 'SQL语句',
     procedure_method_tips: '请输入存储脚本',
     procedure_method_snippet:
-      '--请输入存储脚本 \n\n--调用存储过程: call <procedure-name>[(<arg1>,<arg2>, ...)] 
\n\n--调用存储函数:?= call <procedure-name>[(<arg1>,<arg2>, ...)]',
+        '--请输入存储脚本 \n\n--调用存储过程: call <procedure-name>[(<arg1>,<arg2>, ...)] 
\n\n--调用存储函数:?= call <procedure-name>[(<arg1>,<arg2>, ...)]',
     start: '运行',
     edit: '编辑',
     copy: '复制节点',
@@ -634,7 +634,7 @@ export default {
     mlflow_isSearchParams: '是否搜索参数',
     mlflow_dataPath: '数据路径',
     mlflow_dataPath_tips:
-      ' 文件/文件夹的绝对路径, 若文件需以.csv结尾, 文件夹需包含train.csv和test.csv ',
+        ' 文件/文件夹的绝对路径, 若文件需以.csv结尾, 文件夹需包含train.csv和test.csv ',
     mlflow_dataPath_error_tips: ' 数据路径不能为空 ',
     mlflow_experimentName: '实验名称',
     mlflow_experimentName_tips: 'experiment_001',
diff --git a/dolphinscheduler-ui/src/locales/zh_CN/security.ts 
b/dolphinscheduler-ui/src/locales/zh_CN/security.ts
index d17d701a61..c06129c25f 100644
--- a/dolphinscheduler-ui/src/locales/zh_CN/security.ts
+++ b/dolphinscheduler-ui/src/locales/zh_CN/security.ts
@@ -98,6 +98,26 @@ export default {
     environment_description_tips: '请输入环境描述',
     worker_group_tips: '请选择Worker分组'
   },
+  cluster: {
+    create_cluster: '创建集群',
+    edit_cluster: '编辑集群',
+    search_tips: '请输入关键词',
+    edit: '编辑',
+    delete: '删除',
+    cluster_name: '集群名称',
+    cluster_components: '集群模块',
+    cluster_config: '集群配置',
+    kubernetes_config: 'Kubernetes配置',
+    yarn_config: 'Yarn配置',
+    cluster_desc: '集群描述',
+    create_time: '创建时间',
+    update_time: '更新时间',
+    operation: '操作',
+    delete_confirm: '确定删除吗?',
+    cluster_name_tips: '请输入集群名',
+    cluster_config_tips: '请输入集群配置',
+    cluster_description_tips: '请输入集群描述'
+  },
   token: {
     create_token: '创建令牌',
     edit_token: '编辑令牌',
diff --git a/dolphinscheduler-ui/src/router/modules/security.ts 
b/dolphinscheduler-ui/src/router/modules/security.ts
index 039a5fbe09..cb256fa916 100644
--- a/dolphinscheduler-ui/src/router/modules/security.ts
+++ b/dolphinscheduler-ui/src/router/modules/security.ts
@@ -95,6 +95,17 @@ export default {
         auth: ['ADMIN_USER']
       }
     },
+    {
+      path: '/security/cluster-manage',
+      name: 'cluster-manage',
+      component: components['security-cluster-manage'],
+      meta: {
+        title: '集群管理',
+        activeMenu: 'security',
+        showSide: true,
+        auth: ['ADMIN_USER']
+      }
+    },
     {
       path: '/security/token-manage',
       name: 'token-manage',
diff --git a/dolphinscheduler-ui/src/service/modules/cluster/index.ts 
b/dolphinscheduler-ui/src/service/modules/cluster/index.ts
new file mode 100644
index 0000000000..e941443aaa
--- /dev/null
+++ b/dolphinscheduler-ui/src/service/modules/cluster/index.ts
@@ -0,0 +1,80 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You 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.
+ */
+
+import { axios } from '@/service/service'
+import {
+  ClusterReq,
+  ClusterCodeReq,
+  ClusterNameReq,
+  ListReq,
+  CodeReq
+} from './types'
+
+export function createCluster(data: ClusterReq): any {
+  return axios({
+    url: '/cluster/create',
+    method: 'post',
+    data
+  })
+}
+
+export function deleteClusterByCode(data: ClusterCodeReq): any {
+  return axios({
+    url: '/cluster/delete',
+    method: 'post',
+    data
+  })
+}
+
+export function queryClusterListPaging(params: ListReq): any {
+  return axios({
+    url: '/cluster/list-paging',
+    method: 'get',
+    params
+  })
+}
+
+export function queryClusterByCode(params: ClusterCodeReq): any {
+  return axios({
+    url: '/cluster/query-by-code',
+    method: 'get',
+    params
+  })
+}
+
+export function queryAllClusterList(): any {
+  return axios({
+    url: '/cluster/query-cluster-list',
+    method: 'get'
+  })
+}
+
+export function updateCluster(data: ClusterReq & CodeReq): any {
+  return axios({
+    url: '/cluster/update',
+    method: 'post',
+    data
+  })
+}
+
+export function verifyCluster(data: ClusterNameReq): any {
+  return axios({
+    url: '/cluster/verify-cluster',
+    method: 'post',
+    data
+  })
+}
diff --git a/dolphinscheduler-ui/src/service/modules/cluster/types.ts 
b/dolphinscheduler-ui/src/service/modules/cluster/types.ts
new file mode 100644
index 0000000000..78a1bbcbb3
--- /dev/null
+++ b/dolphinscheduler-ui/src/service/modules/cluster/types.ts
@@ -0,0 +1,72 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You 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.
+ */
+
+interface ClusterReq {
+  config: string
+  name: string
+  description?: string
+  workerGroups?: string
+}
+
+interface ClusterCodeReq {
+  clusterCode: number
+}
+
+interface ClusterNameReq {
+  clusterName: string
+}
+
+interface ListReq {
+  pageNo: number
+  pageSize: number
+  searchVal?: string
+}
+
+interface CodeReq {
+  code: number
+}
+
+interface ClusterItem {
+  id: number
+  code: any
+  name: string
+  config: string
+  description: string
+  workerGroups: string[]
+  operator: number
+  createTime: string
+  updateTime: string
+}
+
+interface ClusterRes {
+  totalList: ClusterItem[]
+  total: number
+  totalPage: number
+  pageSize: number
+  currentPage: number
+  start: number
+}
+
+export {
+  ClusterReq,
+  ClusterCodeReq,
+  ClusterNameReq,
+  ListReq,
+  CodeReq,
+  ClusterItem,
+  ClusterRes
+}
diff --git 
a/dolphinscheduler-ui/src/views/security/cluster-manage/components/cluster-modal.tsx
 
b/dolphinscheduler-ui/src/views/security/cluster-manage/components/cluster-modal.tsx
new file mode 100644
index 0000000000..e24c6dcccd
--- /dev/null
+++ 
b/dolphinscheduler-ui/src/views/security/cluster-manage/components/cluster-modal.tsx
@@ -0,0 +1,191 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You 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.
+ */
+
+import { defineComponent, PropType, toRefs, watch } from 'vue'
+import Modal from '@/components/modal'
+import { NForm, NFormItem, NInput } from 'naive-ui'
+import { useModal } from './use-modal'
+import { useI18n } from 'vue-i18n'
+
+const envK8sConfigPlaceholder = `apiVersion: v1
+clusters:
+- cluster:
+    certificate-authority-data: LS0tLS1CZJQ0FURS0tLS0tCg==
+    server: https://127.0.0.1:6443
+  name: kubernetes
+contexts:
+- context:
+    cluster: kubernetes
+    user: kubernetes-admin
+  name: kubernetes-admin@kubernetes
+current-context: kubernetes-admin@kubernetes
+kind: Config
+preferences: {}
+users:
+- name: kubernetes-admin
+  user:
+    client-certificate-data: LS0tLS1CZJQ0FURS0tLS0tCg= 
+`
+
+const envYarnConfigPlaceholder = 'In development...'
+
+const ClusterModal = defineComponent({
+  name: 'ClusterModal',
+  props: {
+    showModalRef: {
+      type: Boolean as PropType<boolean>,
+      default: false
+    },
+    statusRef: {
+      type: Number as PropType<number>,
+      default: 0
+    },
+    row: {
+      type: Object as PropType<any>,
+      default: {}
+    }
+  },
+  emits: ['cancelModal', 'confirmModal'],
+  setup(props, ctx) {
+    const { variables, handleValidate } = useModal(props, ctx)
+    const { t } = useI18n()
+
+    const cancelModal = () => {
+      if (props.statusRef === 0) {
+        variables.model.name = ''
+        variables.model.k8s_config = ''
+        variables.model.yarn_config = ''
+        variables.model.description = ''
+      }
+      ctx.emit('cancelModal', props.showModalRef)
+    }
+
+    const confirmModal = () => {
+      handleValidate(props.statusRef)
+    }
+
+    const setModal = (row: any) => {
+      variables.model.code = row.code
+      variables.model.name = row.name
+      if (row.config) {
+        const config = JSON.parse(row.config)
+        variables.model.k8s_config = config.k8s || ''
+        variables.model.yarn_config = config.yarn || ''
+      } else {
+        variables.model.k8s_config = ''
+        variables.model.yarn_config = ''
+      }
+      variables.model.description = row.description
+    }
+
+    watch(
+      () => props.statusRef,
+      () => {
+        if (props.statusRef === 0) {
+          variables.model.name = ''
+          variables.model.k8s_config = ''
+          variables.model.yarn_config = ''
+          variables.model.description = ''
+        } else {
+          setModal(props.row)
+        }
+      }
+    )
+
+    watch(
+      () => props.row,
+      () => {
+        setModal(props.row)
+      }
+    )
+
+    return { t, ...toRefs(variables), cancelModal, confirmModal }
+  },
+  render() {
+    const { t } = this
+    return (
+      <div>
+        <Modal
+          title={
+            this.statusRef === 0
+              ? t('security.cluster.create_cluster')
+              : t('security.cluster.edit_cluster')
+          }
+          show={this.showModalRef}
+          onCancel={this.cancelModal}
+          onConfirm={this.confirmModal}
+          confirmDisabled={!this.model.name || !this.model.description}
+          confirmClassName='btn-submit'
+          cancelClassName='btn-cancel'
+          confirmLoading={this.saving}
+        >
+          {{
+            default: () => (
+              <NForm model={this.model} rules={this.rules} 
ref='clusterFormRef'>
+                <NFormItem
+                  label={t('security.cluster.cluster_name')}
+                  path='name'
+                >
+                  <NInput
+                    class='input-cluster-name'
+                    placeholder={t('security.cluster.cluster_name_tips')}
+                    v-model={[this.model.name, 'value']}
+                  />
+                </NFormItem>
+                <NFormItem
+                  label={t('security.cluster.kubernetes_config')}
+                  path='k8s_config'
+                >
+                  <NInput
+                    class='input-cluster-config'
+                    placeholder={envK8sConfigPlaceholder}
+                    type='textarea'
+                    autosize={{ minRows: 16 }}
+                    v-model={[this.model.k8s_config, 'value']}
+                  />
+                </NFormItem>
+                <NFormItem
+                  label={t('security.cluster.yarn_config')}
+                  path='yarn_config'
+                >
+                  <NInput
+                    class='input-yarn-config'
+                    placeholder={envYarnConfigPlaceholder}
+                    disabled={true}
+                    v-model={[this.model.yarn_config, 'value']}
+                  />
+                </NFormItem>
+                <NFormItem
+                  label={t('security.cluster.cluster_desc')}
+                  path='description'
+                >
+                  <NInput
+                    class='input-cluster-desc'
+                    
placeholder={t('security.cluster.cluster_description_tips')}
+                    v-model={[this.model.description, 'value']}
+                  />
+                </NFormItem>
+              </NForm>
+            )
+          }}
+        </Modal>
+      </div>
+    )
+  }
+})
+
+export default ClusterModal
diff --git 
a/dolphinscheduler-ui/src/views/security/cluster-manage/components/use-modal.ts 
b/dolphinscheduler-ui/src/views/security/cluster-manage/components/use-modal.ts
new file mode 100644
index 0000000000..e76083e929
--- /dev/null
+++ 
b/dolphinscheduler-ui/src/views/security/cluster-manage/components/use-modal.ts
@@ -0,0 +1,118 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You 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.
+ */
+
+import { reactive, ref, SetupContext } from 'vue'
+import { useI18n } from 'vue-i18n'
+import {
+  verifyCluster,
+  createCluster,
+  updateCluster
+} from '@/service/modules/cluster'
+
+export function useModal(
+  props: any,
+  ctx: SetupContext<('cancelModal' | 'confirmModal')[]>
+) {
+  const { t } = useI18n()
+
+  const variables = reactive({
+    clusterFormRef: ref(),
+    model: {
+      code: ref<number>(-1),
+      name: ref(''),
+      k8s_config: ref(''),
+      yarn_config: ref(''),
+      description: ref('')
+    },
+    saving: false,
+    rules: {
+      name: {
+        required: true,
+        trigger: ['input', 'blur'],
+        validator() {
+          if (variables.model.name === '') {
+            return new Error(t('security.cluster.cluster_name_tips'))
+          }
+        }
+      },
+      description: {
+        required: true,
+        trigger: ['input', 'blur'],
+        validator() {
+          if (variables.model.description === '') {
+            return new Error(t('security.cluster.cluster_description_tips'))
+          }
+        }
+      }
+    }
+  })
+
+  const handleValidate = async (statusRef: number) => {
+    await variables.clusterFormRef.validate()
+
+    if (variables.saving) return
+    variables.saving = true
+
+    try {
+      statusRef === 0 ? await submitClusterModal() : await updateClusterModal()
+    } finally {
+      variables.saving = false
+    }
+  }
+
+  const submitClusterModal = () => {
+    verifyCluster({ clusterName: variables.model.name }).then(() => {
+      const data = {
+        name: variables.model.name,
+        config: JSON.stringify({
+          k8s: variables.model.k8s_config,
+          yarn: variables.model.yarn_config
+        }),
+        description: variables.model.description
+      }
+
+      createCluster(data).then(() => {
+        variables.model.name = ''
+        variables.model.k8s_config = ''
+        variables.model.yarn_config = ''
+        variables.model.description = ''
+        ctx.emit('confirmModal', props.showModalRef)
+      })
+    })
+  }
+
+  const updateClusterModal = () => {
+    const data = {
+      code: variables.model.code,
+      name: variables.model.name,
+      config: JSON.stringify({
+        k8s: variables.model.k8s_config,
+        yarn: variables.model.yarn_config
+      }),
+      description: variables.model.description
+    }
+
+    updateCluster(data).then(() => {
+      ctx.emit('confirmModal', props.showModalRef)
+    })
+  }
+
+  return {
+    variables,
+    handleValidate
+  }
+}
diff --git a/dolphinscheduler-ui/src/views/security/cluster-manage/index.tsx 
b/dolphinscheduler-ui/src/views/security/cluster-manage/index.tsx
new file mode 100644
index 0000000000..0e6a483f17
--- /dev/null
+++ b/dolphinscheduler-ui/src/views/security/cluster-manage/index.tsx
@@ -0,0 +1,170 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You 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.
+ */
+
+import { defineComponent, onMounted, toRefs, watch } from 'vue'
+import {
+  NButton,
+  NCard,
+  NDataTable,
+  NIcon,
+  NInput,
+  NPagination,
+  NSpace
+} from 'naive-ui'
+import { SearchOutlined } from '@vicons/antd'
+import { useI18n } from 'vue-i18n'
+import { useTable } from './use-table'
+import Card from '@/components/card'
+import ClusterModal from './components/cluster-modal'
+
+const clusterManage = defineComponent({
+  name: 'cluster-manage',
+  setup() {
+    const { t } = useI18n()
+    const { variables, getTableData, createColumns } = useTable()
+
+    const requestData = () => {
+      getTableData({
+        pageSize: variables.pageSize,
+        pageNo: variables.page,
+        searchVal: variables.searchVal
+      })
+    }
+
+    const onUpdatePageSize = () => {
+      variables.page = 1
+      requestData()
+    }
+
+    const onSearch = () => {
+      variables.page = 1
+      requestData()
+    }
+
+    const handleModalChange = () => {
+      variables.showModalRef = true
+      variables.statusRef = 0
+    }
+
+    const onCancelModal = () => {
+      variables.showModalRef = false
+    }
+
+    const onConfirmModal = () => {
+      variables.showModalRef = false
+      requestData()
+    }
+
+    onMounted(() => {
+      createColumns(variables)
+      requestData()
+    })
+
+    watch(useI18n().locale, () => {
+      createColumns(variables)
+    })
+
+    return {
+      t,
+      ...toRefs(variables),
+      requestData,
+      onCancelModal,
+      onConfirmModal,
+      onUpdatePageSize,
+      handleModalChange,
+      onSearch
+    }
+  },
+  render() {
+    const {
+      t,
+      requestData,
+      onUpdatePageSize,
+      onCancelModal,
+      onConfirmModal,
+      handleModalChange,
+      onSearch,
+      loadingRef
+    } = this
+
+    return (
+      <NSpace vertical>
+        <NCard>
+          <NSpace justify='space-between'>
+            <NButton
+              size='small'
+              type='primary'
+              onClick={handleModalChange}
+              class='btn-create-cluster'
+            >
+              {t('security.cluster.create_cluster')}
+            </NButton>
+            <NSpace>
+              <NInput
+                size='small'
+                clearable
+                v-model={[this.searchVal, 'value']}
+                placeholder={t('security.cluster.search_tips')}
+              />
+              <NButton size='small' type='primary' onClick={onSearch}>
+                {{
+                  icon: () => (
+                    <NIcon>
+                      <SearchOutlined />
+                    </NIcon>
+                  )
+                }}
+              </NButton>
+            </NSpace>
+          </NSpace>
+        </NCard>
+        <Card>
+          <NSpace vertical>
+            <NDataTable
+              loading={loadingRef}
+              row-class-name='items'
+              columns={this.columns}
+              data={this.tableData}
+              scrollX={this.tableWidth}
+            />
+            <NSpace justify='center'>
+              <NPagination
+                v-model:page={this.page}
+                v-model:page-size={this.pageSize}
+                page-count={this.totalPage}
+                show-size-picker
+                page-sizes={[10, 30, 50]}
+                show-quick-jumper
+                onUpdatePage={requestData}
+                onUpdatePageSize={onUpdatePageSize}
+              />
+            </NSpace>
+          </NSpace>
+        </Card>
+        <ClusterModal
+          showModalRef={this.showModalRef}
+          statusRef={this.statusRef}
+          row={this.row}
+          onCancelModal={onCancelModal}
+          onConfirmModal={onConfirmModal}
+        />
+      </NSpace>
+    )
+  }
+})
+
+export default clusterManage
diff --git a/dolphinscheduler-ui/src/views/security/cluster-manage/use-table.ts 
b/dolphinscheduler-ui/src/views/security/cluster-manage/use-table.ts
new file mode 100644
index 0000000000..c9fbb355fc
--- /dev/null
+++ b/dolphinscheduler-ui/src/views/security/cluster-manage/use-table.ts
@@ -0,0 +1,236 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You 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.
+ */
+
+import { useAsyncState } from '@vueuse/core'
+import { reactive, h, ref } from 'vue'
+import { format } from 'date-fns'
+import { NButton, NPopconfirm, NSpace, NTooltip, NTag, NIcon } from 'naive-ui'
+import { useI18n } from 'vue-i18n'
+import {
+  queryClusterListPaging,
+  deleteClusterByCode
+} from '@/service/modules/cluster'
+import { DeleteOutlined, EditOutlined } from '@vicons/antd'
+import type { ClusterRes, ClusterItem } from '@/service/modules/cluster/types'
+import { parseTime } from '@/common/common'
+import {
+  COLUMN_WIDTH_CONFIG,
+  calculateTableWidth,
+  DefaultTableWidth
+} from '@/common/column-width-config'
+
+export function useTable() {
+  const { t } = useI18n()
+
+  const handleEdit = (row: any) => {
+    variables.showModalRef = true
+    variables.statusRef = 1
+    variables.row = row
+  }
+
+  const createColumns = (variables: any) => {
+    variables.columns = [
+      {
+        title: '#',
+        key: 'index',
+        render: (row: any, index: number) => index + 1,
+        ...COLUMN_WIDTH_CONFIG['index']
+      },
+      {
+        title: t('security.cluster.cluster_name'),
+        key: 'name',
+        className: 'cluster-name',
+        ...COLUMN_WIDTH_CONFIG['name']
+      },
+      {
+        title: t('security.cluster.cluster_components'),
+        key: 'config',
+        ...COLUMN_WIDTH_CONFIG['tag'],
+        render: (row: ClusterItem) =>
+          h(NSpace, null, {
+            default: () => {
+              const components = []
+              if (row.config) {
+                const config = JSON.parse(row.config)
+                if (config.yarn) {
+                  components.push('yarn')
+                }
+                if (config.k8s) {
+                  components.push('kubernetes')
+                }
+              }
+              return components.map((item: any) =>
+                h(
+                  NTag,
+                  { type: 'success', size: 'small' },
+                  { default: () => item }
+                )
+              )
+            }
+          })
+      },
+      {
+        title: t('security.cluster.cluster_desc'),
+        key: 'description',
+        ...COLUMN_WIDTH_CONFIG['note']
+      },
+      {
+        title: t('security.cluster.create_time'),
+        key: 'createTime',
+        ...COLUMN_WIDTH_CONFIG['time']
+      },
+      {
+        title: t('security.cluster.update_time'),
+        key: 'updateTime',
+        ...COLUMN_WIDTH_CONFIG['time']
+      },
+      {
+        title: t('security.cluster.operation'),
+        key: 'operation',
+        ...COLUMN_WIDTH_CONFIG['operation'](2),
+        render(row: any) {
+          return h(NSpace, null, {
+            default: () => [
+              h(
+                NTooltip,
+                {},
+                {
+                  trigger: () =>
+                    h(
+                      NButton,
+                      {
+                        circle: true,
+                        type: 'info',
+                        size: 'small',
+                        class: 'edit',
+                        onClick: () => {
+                          handleEdit(row)
+                        }
+                      },
+                      {
+                        icon: () =>
+                          h(NIcon, null, { default: () => h(EditOutlined) })
+                      }
+                    ),
+                  default: () => t('security.cluster.edit')
+                }
+              ),
+              h(
+                NPopconfirm,
+                {
+                  onPositiveClick: () => {
+                    handleDelete(row)
+                  }
+                },
+                {
+                  trigger: () =>
+                    h(
+                      NTooltip,
+                      {},
+                      {
+                        trigger: () =>
+                          h(
+                            NButton,
+                            {
+                              circle: true,
+                              type: 'error',
+                              size: 'small',
+                              class: 'delete'
+                            },
+                            {
+                              icon: () =>
+                                h(NIcon, null, {
+                                  default: () => h(DeleteOutlined)
+                                })
+                            }
+                          ),
+                        default: () => t('security.cluster.delete')
+                      }
+                    ),
+                  default: () => t('security.cluster.delete_confirm')
+                }
+              )
+            ]
+          })
+        }
+      }
+    ]
+    if (variables.tableWidth) {
+      variables.tableWidth = calculateTableWidth(variables.columns)
+    }
+  }
+
+  const variables = reactive({
+    columns: [],
+    tableWidth: DefaultTableWidth,
+    tableData: [],
+    page: ref(1),
+    pageSize: ref(10),
+    searchVal: ref(null),
+    totalPage: ref(1),
+    showModalRef: ref(false),
+    statusRef: ref(0),
+    row: {},
+    loadingRef: ref(false)
+  })
+
+  const handleDelete = (row: any) => {
+    deleteClusterByCode({ clusterCode: row.code }).then(() => {
+      getTableData({
+        pageSize: variables.pageSize,
+        pageNo:
+          variables.tableData.length === 1 && variables.page > 1
+            ? variables.page - 1
+            : variables.page,
+        searchVal: variables.searchVal
+      })
+    })
+  }
+
+  const getTableData = (params: any) => {
+    if (variables.loadingRef) return
+    variables.loadingRef = true
+    const { state } = useAsyncState(
+      queryClusterListPaging({ ...params }).then((res: ClusterRes) => {
+        variables.tableData = res.totalList.map((item, unused) => {
+          item.createTime = format(
+            parseTime(item.createTime),
+            'yyyy-MM-dd HH:mm:ss'
+          )
+          item.updateTime = format(
+            parseTime(item.updateTime),
+            'yyyy-MM-dd HH:mm:ss'
+          )
+          return {
+            ...item
+          }
+        }) as any
+        variables.totalPage = res.totalPage
+        variables.loadingRef = false
+      }),
+      {}
+    )
+
+    return state
+  }
+
+  return {
+    variables,
+    getTableData,
+    createColumns
+  }
+}

Reply via email to