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

hulk pushed a commit to branch unstable
in repository https://gitbox.apache.org/repos/asf/kvrocks-controller.git


The following commit(s) were added to refs/heads/unstable by this push:
     new 4685978  Add support of the terminal client for the server (#171)
4685978 is described below

commit 468597885763f92b7779682daeca138702d90898
Author: hulk <[email protected]>
AuthorDate: Fri May 3 08:40:26 2024 +0800

    Add support of the terminal client for the server (#171)
---
 Dockerfile                                |   4 +-
 README.md                                 |  25 ++++
 build.sh                                  |   8 +-
 cmd/client/command/client.go              |  86 +++++++++++++
 cmd/client/{main.go => command/consts.go} |  10 +-
 cmd/client/command/create.go              | 207 ++++++++++++++++++++++++++++++
 cmd/client/command/delete.go              | 185 ++++++++++++++++++++++++++
 cmd/client/command/get.go                 | 102 +++++++++++++++
 cmd/client/command/helper.go              |  62 +++++++++
 cmd/client/command/import.go              | 101 +++++++++++++++
 cmd/client/command/list.go                | 133 +++++++++++++++++++
 cmd/client/command/migrate.go             | 119 +++++++++++++++++
 cmd/client/main.go                        |  40 ++++++
 consts/context_key.go                     |   5 +-
 consts/errors.go                          |   3 +-
 controller/controller.go                  |   7 +-
 go.mod                                    |  57 ++++----
 go.sum                                    | 192 +++++++++++++++------------
 server/api/cluster.go                     |  43 ++++++-
 server/api/cluster_test.go                |   1 +
 server/api/namespace.go                   |   3 +-
 server/api/node.go                        |   6 +-
 server/api/shard.go                       |   2 +-
 server/helper/helper.go                   |   2 +
 server/middleware/middleware.go           |  12 +-
 server/route.go                           |  10 +-
 store/cluster.go                          |   8 ++
 store/cluster_node.go                     |   3 +-
 store/store.go                            |  52 ++++++--
 store/store_test.go                       |  11 ++
 30 files changed, 1352 insertions(+), 147 deletions(-)

diff --git a/Dockerfile b/Dockerfile
index a65dbde..08ba370 100644
--- a/Dockerfile
+++ b/Dockerfile
@@ -15,7 +15,7 @@
 # specific language governing permissions and limitations
 # under the License.
 #
-FROM golang:1.17 as build
+FROM golang:1.20 as build
 
 WORKDIR /kvctl
 
@@ -32,7 +32,7 @@ FROM ubuntu:focal
 WORKDIR /kvctl
 
 COPY --from=build /kvctl/_build/kvctl-server ./bin/
-COPY --from=build /kvctl/_build/kvctl-client ./bin/
+COPY --from=build /kvctl/_build/kvctl ./bin/
 
 VOLUME /var/lib/kvctl
 
diff --git a/README.md b/README.md
index 07fca9b..6837793 100644
--- a/README.md
+++ b/README.md
@@ -39,4 +39,29 @@ $ ./_build/kvctl-server -c config/config.yaml
 ```
 ![image](docs/images/server.gif)
 
+### 2. Use the terminal client to interact with the controller server
+
+```shell
+# Show help
+$ ./_build/kvctl --help
+
+# Create namespace
+$ ./_build/kvctl create namespace test-ns
+
+# List namespaces
+$ ./_build/kvctl list namespaces
+
+# Create cluster in the namespace 
+$ ./_build/kvctl create cluster test-cluster --nodes 
127.0.0.1:6666,127.0.0.1:6667 -n test-ns
+
+# List clusters in the namespace
+$ ./_build/kvctl list clusters -n test-ns
+
+# Get cluster in the namespace
+$ ./_build/kvctl get cluster test-cluster -n test-ns
+
+# Migrate slot from source to target
+$ ./_build/kvctl migrate slot 123 --target 1 -n test-ns -c test-cluster
+```
+
 For the HTTP API, you can find the [HTTP API(work in progress)](docs/API.md) 
for more details.
diff --git a/build.sh b/build.sh
index 3751e57..44a82c8 100644
--- a/build.sh
+++ b/build.sh
@@ -79,10 +79,14 @@ BUILD_DATE=`date -u +'%Y-%m-%dT%H:%M:%SZ'`
 GIT_REVISION=`git rev-parse --short HEAD`
 
 SERVER_TARGET_NAME=kvctl-server
-CLIENT_TARGET_NAME=kvctl-client
+CLIENT_TARGET_NAME=kvctl
 
 for TARGET_NAME in "$SERVER_TARGET_NAME" "$CLIENT_TARGET_NAME"; do
-    CMD_PATH="${GO_PROJECT}/cmd/${TARGET_NAME##*-}" # Remove everything to the 
left of the last - in the TARGET_NAME variable
+    if [[ "$TARGET_NAME" == "$SERVER_TARGET_NAME" ]]; then
+        CMD_PATH="${GO_PROJECT}/cmd/server"
+    else
+        CMD_PATH="${GO_PROJECT}/cmd/client"
+    fi
 
     if [[ "$BUILDER_IMAGE" == "none" ]]; then
         GOOS="$TARGET_OS" GOARCH="$TARGET_ARCH" CGO_ENABLED=0 go build -v 
-ldflags \
diff --git a/cmd/client/command/client.go b/cmd/client/command/client.go
new file mode 100644
index 0000000..12277a0
--- /dev/null
+++ b/cmd/client/command/client.go
@@ -0,0 +1,86 @@
+/*
+ * 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 command
+
+import (
+       "encoding/json"
+       "errors"
+       "reflect"
+
+       "github.com/go-resty/resty/v2"
+)
+
+const (
+       apiVersionV1 = "/api/v1"
+
+       defaultHost = "http://127.0.0.1:9379";
+)
+
+type client struct {
+       restyCli *resty.Client
+       host     string
+}
+
+type ErrorMessage struct {
+       Message string `json:"message"`
+}
+
+type response struct {
+       Error *ErrorMessage `json:"error"`
+       Data  any           `json:"data"`
+}
+
+func newClient(host string) *client {
+       if host == "" {
+               host = defaultHost
+       }
+       restyCli := resty.New().SetBaseURL(host + apiVersionV1)
+       return &client{
+               restyCli: restyCli,
+               host:     host,
+       }
+}
+
+func unmarshalData(body []byte, v any) error {
+       if len(body) == 0 {
+               return errors.New("empty response body")
+       }
+
+       rv := reflect.ValueOf(v)
+       if rv.Kind() != reflect.Ptr || rv.IsNil() {
+               return errors.New("unmarshal receiver was non-pointer")
+       }
+
+       var rsp response
+       rsp.Data = v
+       return json.Unmarshal(body, &rsp)
+}
+
+func unmarshalError(body []byte) error {
+       var rsp response
+       if err := json.Unmarshal(body, &rsp); err != nil {
+               return err
+       }
+       if rsp.Error != nil {
+               return errors.New(rsp.Error.Message)
+       }
+       return nil
+}
diff --git a/cmd/client/main.go b/cmd/client/command/consts.go
similarity index 84%
copy from cmd/client/main.go
copy to cmd/client/command/consts.go
index bac6575..59b6ef5 100644
--- a/cmd/client/main.go
+++ b/cmd/client/command/consts.go
@@ -18,7 +18,11 @@
  *
  */
 
-package main
+package command
 
-func main() {
-}
+const (
+       ResourceNamespace = "namespace"
+       ResourceCluster   = "cluster"
+       ResourceShard     = "shard"
+       ResourceNode      = "node"
+)
diff --git a/cmd/client/command/create.go b/cmd/client/command/create.go
new file mode 100644
index 0000000..fb5fcf9
--- /dev/null
+++ b/cmd/client/command/create.go
@@ -0,0 +1,207 @@
+/*
+ * 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 command
+
+import (
+       "errors"
+       "strconv"
+       "strings"
+
+       "github.com/spf13/cobra"
+)
+
+type CreateOptions struct {
+       namespace string
+       cluster   string
+       shard     int
+       replica   int
+       nodes     []string
+       password  string
+}
+
+var createOptions CreateOptions
+
+var CreateCommand = &cobra.Command{
+       Use:   "create",
+       Short: "Create a resource",
+       Example: `
+# Create a namespace
+kvctl create namespace <namespace>
+
+# Create a cluster in the namespace
+kvctl create cluster <cluster> -n <namespace> --replica 1 --nodes 
127.0.0.1:6379,127.0.0.1:6380,127.0.0.1:6381
+
+# Create a shard in the cluster
+kvctl create shard -n <namespace> -c <cluster> --nodes 
127.0.0.1:6379,127.0.0.1:6380
+
+# Create nodes in the cluster
+kvctl create node 127.0.0.1:6379 -n <namespace> -c <cluster> --shard <shard>
+`,
+       PreRunE: createPreRun,
+       RunE: func(cmd *cobra.Command, args []string) error {
+               host, _ := cmd.Flags().GetString("host")
+               client := newClient(host)
+               switch strings.ToLower(args[0]) {
+               case ResourceNamespace:
+                       if len(args) < 2 {
+                               return errors.New("missing namespace name")
+                       }
+                       return createNamespace(client, args[1])
+               case ResourceCluster:
+                       if len(args) < 2 {
+                               return errors.New("missing cluster name")
+                       }
+                       createOptions.cluster = args[1]
+                       return createCluster(client, &createOptions)
+               case ResourceShard:
+                       return createShard(client, &createOptions)
+               case ResourceNode:
+                       if len(args) < 2 {
+                               return errors.New("missing node address")
+                       }
+                       createOptions.nodes = []string{args[1]}
+                       return createNodes(client, &createOptions)
+               default:
+                       return errors.New("unsupported resource type, please 
specify one of [namespace, cluster, shard, nodes]")
+               }
+       },
+       SilenceUsage:  true,
+       SilenceErrors: true,
+}
+
+func createPreRun(_ *cobra.Command, args []string) error {
+       if len(args) == 0 {
+               return errors.New("missing resource type, please specify one of 
[namespace, cluster, shard, node]")
+       }
+       resource := strings.ToLower(args[0])
+       if resource == ResourceNamespace {
+               return nil
+       }
+       if createOptions.namespace == "" {
+               return errors.New("missing namespace, please specify the 
namespace via -n or --namespace option")
+       }
+       if resource != ResourceNode && createOptions.nodes == nil {
+               return errors.New("missing nodes, please specify the nodes via 
--nodes option")
+       }
+       if resource == ResourceCluster {
+               return nil
+       }
+       if createOptions.cluster == "" {
+               return errors.New("missing cluster, please specify the cluster 
via -c or --cluster option")
+       }
+       if resource == ResourceShard {
+               return nil
+       }
+       if createOptions.shard == -1 {
+               return errors.New("missing shard, please specify the shard via 
-s or --shard option")
+       }
+       if createOptions.shard < 0 {
+               return errors.New("shard must be a positive number")
+       }
+       return nil
+}
+
+func createNamespace(cli *client, name string) error {
+       rsp, err := cli.restyCli.R().
+               SetBody(map[string]string{"namespace": name}).
+               Post("/namespaces")
+       if err != nil {
+               return err
+       }
+
+       if rsp.IsError() {
+               return unmarshalError(rsp.Body())
+       }
+       printLine("create namespace: %s successfully.", name)
+       return nil
+}
+
+func createCluster(cli *client, options *CreateOptions) error {
+       rsp, err := cli.restyCli.R().
+               SetPathParam("namespace", options.namespace).
+               SetBody(map[string]interface{}{
+                       "name":     options.cluster,
+                       "replica":  options.replica,
+                       "nodes":    options.nodes,
+                       "password": options.password,
+               }).
+               Post("/namespaces/{namespace}/clusters")
+       if err != nil {
+               return err
+       }
+
+       if rsp.IsError() {
+               return unmarshalError(rsp.Body())
+       }
+       printLine("create cluster: %s successfully.", options.cluster)
+       return nil
+}
+
+func createShard(cli *client, options *CreateOptions) error {
+       rsp, err := cli.restyCli.R().
+               SetPathParam("namespace", options.namespace).
+               SetPathParam("cluster", options.cluster).
+               SetBody(map[string]interface{}{
+                       "name":     options.cluster,
+                       "nodes":    options.nodes,
+                       "password": options.password,
+               }).
+               Post("/namespaces/{namespace}/clusters/{cluster}/shards")
+       if err != nil {
+               return err
+       }
+
+       if rsp.IsError() {
+               return unmarshalError(rsp.Body())
+       }
+       printLine("create the new shard successfully.")
+       return nil
+}
+
+func createNodes(cli *client, options *CreateOptions) error {
+       rsp, err := cli.restyCli.R().
+               SetPathParam("namespace", options.namespace).
+               SetPathParam("cluster", options.cluster).
+               SetPathParam("shard", strconv.Itoa(options.shard)).
+               SetBody(map[string]interface{}{
+                       "addr":     options.nodes[0],
+                       "password": options.password,
+               }).
+               
Post("/namespaces/{namespace}/clusters/{cluster}/shards/{shard}/nodes")
+       if err != nil {
+               return err
+       }
+
+       if rsp.IsError() {
+               return unmarshalError(rsp.Body())
+       }
+       printLine("create node: %v successfully.", options.nodes[0])
+       return nil
+}
+
+func init() {
+       CreateCommand.Flags().StringVarP(&createOptions.namespace, "namespace", 
"n", "", "The namespace")
+       CreateCommand.Flags().StringVarP(&createOptions.cluster, "cluster", 
"c", "", "The cluster")
+       CreateCommand.Flags().IntVarP(&createOptions.shard, "shard", "s", -1, 
"The shard number")
+       CreateCommand.Flags().IntVarP(&createOptions.replica, "replica", "r", 
1, "The replica number")
+       CreateCommand.Flags().StringSliceVarP(&createOptions.nodes, "nodes", 
"", nil, "The node list")
+       CreateCommand.Flags().StringVarP(&createOptions.password, "password", 
"", "", "The password")
+}
diff --git a/cmd/client/command/delete.go b/cmd/client/command/delete.go
new file mode 100644
index 0000000..377cf6a
--- /dev/null
+++ b/cmd/client/command/delete.go
@@ -0,0 +1,185 @@
+/*
+ * 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 command
+
+import (
+       "fmt"
+       "strconv"
+       "strings"
+
+       "github.com/spf13/cobra"
+)
+
+type DeleteOptions struct {
+       namespace string
+       cluster   string
+       shard     int
+}
+
+var deleteOptions DeleteOptions
+
+var DeleteCommand = &cobra.Command{
+       Use:   "delete",
+       Short: "Delete a resource",
+       Example: `
+# Delete a namespace
+kvctl delete namespace <namespace>
+
+# Delete a cluster in the namespace
+kvctl delete cluster <cluster> -n <namespace>
+
+# Delete a shard in the cluster
+kvctl delete shard <shard> -n <namespace> -c <cluster>
+
+# Delete a node in the cluster
+kvctl delete node <node_id> -n <namespace> -c <cluster> --shard <shard>
+`,
+       PreRunE: deletePreRun,
+       RunE: func(cmd *cobra.Command, args []string) error {
+               resource := strings.ToLower(args[0])
+               if len(args) < 2 {
+                       return fmt.Errorf("missing resource name")
+               }
+               host, _ := cmd.Flags().GetString("host")
+               client := newClient(host)
+               switch resource {
+               case ResourceNamespace:
+                       namespace := args[1]
+                       return deleteNamespace(client, namespace)
+               case ResourceCluster:
+                       deleteOptions.cluster = args[1]
+                       return deleteCluster(client, &deleteOptions)
+               case ResourceShard:
+                       shard, err := strconv.Atoi(args[1])
+                       if err != nil {
+                               return err
+                       }
+                       deleteOptions.shard = shard
+                       return deleteShard(client, &deleteOptions)
+               case ResourceNode:
+                       nodeID := args[1]
+                       return deleteNode(client, &deleteOptions, nodeID)
+               default:
+                       return fmt.Errorf("unsupported resource type %s", 
resource)
+               }
+       },
+       SilenceUsage:  true,
+       SilenceErrors: true,
+}
+
+func deletePreRun(_ *cobra.Command, args []string) error {
+       if len(args) == 0 {
+               return fmt.Errorf("missing resource type")
+       }
+
+       resource := strings.ToLower(args[0])
+       if resource == ResourceNamespace {
+               return nil
+       }
+       if deleteOptions.namespace == "" {
+               return fmt.Errorf("missing namespace, please specify the 
namespace via -n or --namespace option")
+       }
+       if resource == ResourceCluster {
+               return nil
+       }
+       if deleteOptions.cluster == "" {
+               return fmt.Errorf("missing cluster, please specify the cluster 
via -c or --cluster option")
+       }
+       if resource == ResourceShard {
+               return nil
+       }
+       if deleteOptions.shard == -1 {
+               return fmt.Errorf("missing shard, please specify the shard via 
-s or --shard option")
+       }
+       if deleteOptions.shard < 0 {
+               return fmt.Errorf("invalid shard %d", deleteOptions.shard)
+       }
+       return nil
+}
+
+func deleteNamespace(client *client, namespace string) error {
+       rsp, err := client.restyCli.R().
+               SetPathParam("namespace", namespace).
+               Delete("/namespaces/{namespace}")
+       if err != nil {
+               return err
+       }
+       if rsp.IsError() {
+               return unmarshalError(rsp.Body())
+       }
+       printLine("delete namespace: %s successfully.", namespace)
+       return nil
+}
+
+func deleteCluster(client *client, options *DeleteOptions) error {
+       rsp, err := client.restyCli.R().
+               SetPathParams(map[string]string{
+                       "namespace": options.namespace,
+                       "cluster":   options.cluster,
+               }).Delete("/namespaces/{namespace}/clusters/{cluster}")
+       if err != nil {
+               return err
+       }
+       if rsp.IsError() {
+               return unmarshalError(rsp.Body())
+       }
+       printLine("delete cluster: %s successfully.", options.cluster)
+       return nil
+}
+
+func deleteShard(client *client, options *DeleteOptions) error {
+       rsp, err := client.restyCli.R().
+               SetPathParam("namespace", options.namespace).
+               SetPathParam("cluster", options.cluster).
+               SetPathParam("shard", strconv.Itoa(options.shard)).
+               
Delete("/namespaces/{namespace}/clusters/{cluster}/shards/{shard}")
+       if err != nil {
+               return err
+       }
+       if rsp.IsError() {
+               return unmarshalError(rsp.Body())
+       }
+       printLine("delete shard %d successfully.", options.shard)
+       return nil
+}
+
+func deleteNode(client *client, options *DeleteOptions, nodeID string) error {
+       rsp, err := client.restyCli.R().
+               SetPathParam("namespace", options.namespace).
+               SetPathParam("cluster", options.cluster).
+               SetPathParam("shard", strconv.Itoa(options.shard)).
+               SetPathParam("node", nodeID).
+               
Delete("/namespaces/{namespace}/clusters/{cluster}/shards/{shard}/nodes/{node}")
+       if err != nil {
+               return err
+       }
+       if rsp.IsError() {
+               return unmarshalError(rsp.Body())
+       }
+       printLine("delete node: %s successfully.", nodeID)
+       return nil
+}
+
+func init() {
+       DeleteCommand.Flags().StringVarP(&deleteOptions.namespace, "namespace", 
"n", "", "The namespace")
+       DeleteCommand.Flags().StringVarP(&deleteOptions.cluster, "cluster", 
"c", "", "The cluster")
+       DeleteCommand.Flags().IntVarP(&deleteOptions.shard, "shard", "s", -1, 
"The shard")
+}
diff --git a/cmd/client/command/get.go b/cmd/client/command/get.go
new file mode 100644
index 0000000..f2e965b
--- /dev/null
+++ b/cmd/client/command/get.go
@@ -0,0 +1,102 @@
+/*
+ * 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 command
+
+import (
+       "fmt"
+       "strings"
+
+       "github.com/spf13/cobra"
+
+       "github.com/apache/kvrocks-controller/store"
+)
+
+type GetOptions struct {
+       namespace string
+       cluster   string
+}
+
+var getOptions GetOptions
+
+var GetCommand = &cobra.Command{
+       Use:   "get",
+       Short: "Get a resource",
+       Example: `
+# Get a cluster 
+kvctl get cluster <cluster> -n <namespace>
+`,
+       PreRunE: getPreRun,
+       RunE: func(cmd *cobra.Command, args []string) error {
+               host, _ := cmd.Flags().GetString("host")
+               client := newClient(host)
+               if len(args) < 2 {
+                       return fmt.Errorf("missing resource name, must be one 
of [cluster, shard]")
+               }
+               resource := strings.ToLower(args[0])
+               switch resource {
+               case "cluster":
+                       getOptions.cluster = args[1]
+                       return getCluster(client, &getOptions)
+               default:
+                       return fmt.Errorf("unsupported resource type %s", 
resource)
+               }
+       },
+       SilenceUsage:  true,
+       SilenceErrors: true,
+}
+
+func getPreRun(_ *cobra.Command, args []string) error {
+       if len(args) == 0 {
+               return fmt.Errorf("missing resource type")
+       }
+
+       if getOptions.namespace == "" {
+               return fmt.Errorf("missing namespace, please specify the 
namespace via -n or --namespace option")
+       }
+       return nil
+}
+
+func getCluster(client *client, options *GetOptions) error {
+       rsp, err := client.restyCli.R().SetPathParams(map[string]string{
+               "namespace": options.namespace,
+               "cluster":   options.cluster,
+       }).Get("/namespaces/{namespace}/clusters/{cluster}")
+       if err != nil {
+               return err
+       }
+       if rsp.IsError() {
+               return unmarshalError(rsp.Body())
+       }
+
+       var result struct {
+               Cluster *store.Cluster `json:"cluster"`
+       }
+       if err := unmarshalData(rsp.Body(), &result); err != nil {
+               return err
+       }
+       printCluster(result.Cluster)
+       return nil
+}
+
+func init() {
+       GetCommand.Flags().StringVarP(&getOptions.namespace, "namespace", "n", 
"", "The namespace of the resource")
+       GetCommand.Flags().StringVarP(&getOptions.cluster, "cluster", "c", "", 
"The cluster of the resource")
+}
diff --git a/cmd/client/command/helper.go b/cmd/client/command/helper.go
new file mode 100644
index 0000000..1d07677
--- /dev/null
+++ b/cmd/client/command/helper.go
@@ -0,0 +1,62 @@
+/*
+ * 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 command
+
+import (
+       "fmt"
+       "os"
+       "strings"
+
+       "github.com/fatih/color"
+
+       "github.com/olekukonko/tablewriter"
+
+       "github.com/apache/kvrocks-controller/store"
+)
+
+func printLine(format string, a ...interface{}) {
+       boldColor := color.New(color.Bold)
+       _, _ = fmt.Fprintln(os.Stdout, boldColor.Sprintf(format, a...))
+}
+
+func printCluster(cluster *store.Cluster) {
+       writer := tablewriter.NewWriter(os.Stdout)
+       printLine("")
+       printLine("cluster: %s", cluster.Name)
+       printLine("version: %d\n", cluster.Version.Load())
+       writer.SetHeader([]string{"SHARD", "NODE_ID", "ADDRESS", "ROLE", 
"MIGRATING"})
+       writer.SetCenterSeparator("|")
+       for i, shard := range cluster.Shards {
+               for _, node := range shard.Nodes {
+                       role := strings.ToUpper(store.RoleSlave)
+                       if node.IsMaster() {
+                               role = strings.ToUpper(store.RoleMaster)
+                       }
+                       migratingStatus := "NO"
+                       if shard.MigratingSlot != -1 {
+                               migratingStatus = fmt.Sprintf("%d --> %d", 
shard.MigratingSlot, shard.TargetShardIndex)
+                       }
+                       columns := []string{fmt.Sprintf("%d", i), node.ID(), 
node.Addr(), role, migratingStatus}
+                       writer.Append(columns)
+               }
+       }
+       writer.Render()
+}
diff --git a/cmd/client/command/import.go b/cmd/client/command/import.go
new file mode 100644
index 0000000..51107e6
--- /dev/null
+++ b/cmd/client/command/import.go
@@ -0,0 +1,101 @@
+/*
+ * 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 command
+
+import (
+       "errors"
+       "fmt"
+       "strings"
+
+       "github.com/spf13/cobra"
+)
+
+type ImportOptions struct {
+       namespace string
+       cluster   string
+       nodes     []string
+       password  string
+}
+
+var importOptions ImportOptions
+
+var ImportCommand = &cobra.Command{
+       Use:   "import",
+       Short: "Import data from a cluster",
+       Example: `
+# Import a cluster from nodes
+kvctl import cluster <cluster> --nodes 127.0.0.1:6379,127.0.0.1:6380
+`,
+       PreRunE: importPreRun,
+       RunE: func(cmd *cobra.Command, args []string) error {
+               host, _ := cmd.Flags().GetString("host")
+               client := newClient(host)
+               resource := strings.ToLower(args[0])
+               switch resource {
+               case ResourceCluster:
+                       importOptions.cluster = args[1]
+                       return importCluster(client, &importOptions)
+               default:
+                       return fmt.Errorf("unsupported resource type: %s", 
resource)
+               }
+       },
+       SilenceUsage:  true,
+       SilenceErrors: true,
+}
+
+func importPreRun(_ *cobra.Command, args []string) error {
+       if len(args) < 2 {
+               return errors.New("missing resource name")
+       }
+       if importOptions.namespace == "" {
+               return errors.New("missing namespace, please specify with -n or 
--namespace")
+       }
+       if len(importOptions.nodes) == 0 {
+               return errors.New("missing nodes")
+       }
+       return nil
+}
+
+func importCluster(client *client, options *ImportOptions) error {
+       rsp, err := client.restyCli.R().
+               SetPathParam("namespace", options.namespace).
+               SetPathParam("cluster", options.cluster).
+               SetBody(map[string]interface{}{
+                       "nodes":    options.nodes,
+                       "password": options.password,
+               }).
+               Post("/namespaces/{namespace}/clusters/{cluster}/import")
+       if err != nil {
+               return err
+       }
+       if rsp.IsError() {
+               return errors.New(rsp.String())
+       }
+       printLine("import cluster: %s successfully.", options.cluster)
+       return nil
+}
+
+func init() {
+       ImportCommand.Flags().StringVarP(&importOptions.namespace, "namespace", 
"n", "", "The namespace of the cluster")
+       ImportCommand.Flags().StringVarP(&importOptions.cluster, "cluster", 
"c", "", "The cluster name")
+       ImportCommand.Flags().StringSliceVarP(&importOptions.nodes, "nodes", 
"", nil, "The nodes to import from")
+       ImportCommand.Flags().StringVarP(&importOptions.password, "password", 
"p", "", "The password of the cluster")
+}
diff --git a/cmd/client/command/list.go b/cmd/client/command/list.go
new file mode 100644
index 0000000..e7f628d
--- /dev/null
+++ b/cmd/client/command/list.go
@@ -0,0 +1,133 @@
+/*
+ * 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 command
+
+import (
+       "fmt"
+       "strings"
+
+       "github.com/spf13/cobra"
+)
+
+var listOptions struct {
+       namespace string
+       cluster   string
+}
+
+var ListCommand = &cobra.Command{
+       Use:   "list",
+       Short: "Display all resources",
+       Example: `
+# Display all namespaces 
+kvctl list namespaces
+
+# Display all clusters in the namespace
+kvctl list clusters -n <namespace>
+
+# Display all nodes in the cluster
+kvctl list nodes -n <namespace> -c <cluster>
+`,
+       ValidArgs: []string{"namespaces", "clusters", "shards", "nodes"},
+       RunE: func(cmd *cobra.Command, args []string) error {
+               host, _ := cmd.Flags().GetString("host")
+               client := newClient(host)
+               switch strings.ToLower(args[0]) {
+               case "namespaces":
+                       return listNamespace(client)
+               case "clusters":
+                       return listClusters(client)
+               default:
+                       return fmt.Errorf("unsupported resource type %s", 
args[0])
+               }
+       },
+       PreRunE:       listPreRun,
+       SilenceUsage:  true,
+       SilenceErrors: true,
+}
+
+func listPreRun(_ *cobra.Command, args []string) error {
+       if len(args) == 0 {
+               return fmt.Errorf("missing resource type, please specify one of 
[namespaces, clusters, nodes]")
+       }
+
+       resource := strings.ToLower(args[0])
+       if resource == "namespaces" {
+               return nil
+       }
+       if listOptions.namespace == "" {
+               return fmt.Errorf("missing namespace, please specify the 
namespace via -n or --namespace option")
+       }
+       if resource == "nodes" && listOptions.cluster == "" {
+               return fmt.Errorf("missing cluster, please specify the cluster 
via -c or --cluster option")
+       }
+       return nil
+}
+
+func listNamespace(cli *client) error {
+       rsp, err := cli.restyCli.R().Get("/namespaces")
+       if err != nil {
+               return err
+       }
+
+       var result struct {
+               Namespaces []string `json:"namespaces"`
+       }
+       if err := unmarshalData(rsp.Body(), &result); err != nil {
+               return err
+       }
+       if len(result.Namespaces) == 0 {
+               printLine("no namespace found.")
+               return nil
+       }
+       for _, ns := range result.Namespaces {
+               printLine(ns)
+       }
+       return nil
+}
+
+func listClusters(cli *client) error {
+       rsp, err := cli.restyCli.R().
+               SetPathParam("namespace", listOptions.namespace).
+               Get("/namespaces/{namespace}/clusters")
+       if err != nil {
+               return err
+       }
+
+       var result struct {
+               Clusters []string `json:"clusters"`
+       }
+       if err := unmarshalData(rsp.Body(), &result); err != nil {
+               return err
+       }
+       if len(result.Clusters) == 0 {
+               printLine("no cluster found.")
+               return nil
+       }
+       for _, cluster := range result.Clusters {
+               printLine(cluster)
+       }
+       return nil
+}
+
+func init() {
+       ListCommand.Flags().StringVarP(&listOptions.namespace, "namespace", 
"n", "", "The namespace")
+       ListCommand.Flags().StringVarP(&listOptions.cluster, "cluster", "c", 
"", "The cluster")
+}
diff --git a/cmd/client/command/migrate.go b/cmd/client/command/migrate.go
new file mode 100644
index 0000000..c8ccbac
--- /dev/null
+++ b/cmd/client/command/migrate.go
@@ -0,0 +1,119 @@
+/*
+ * 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 command
+
+import (
+       "errors"
+       "fmt"
+       "strconv"
+       "strings"
+
+       "github.com/spf13/cobra"
+)
+
+type MigrationOptions struct {
+       namespace string
+       cluster   string
+       slot      int
+       target    int
+       slotOnly  bool
+}
+
+var migrateOptions MigrationOptions
+
+var MigrateCommand = &cobra.Command{
+       Use:   "migrate",
+       Short: "Migrate slot to another node",
+       Example: `
+# Migrate slot between cluster shards 
+kvctl migrate slot <slot> --target <target_shard_index> -n <namespace> -c 
<cluster>
+`,
+       PreRunE: migrationPreRun,
+       RunE: func(cmd *cobra.Command, args []string) error {
+               host, _ := cmd.Flags().GetString("host")
+               client := newClient(host)
+               resource := strings.ToLower(args[0])
+               switch resource {
+               case "slot":
+                       return migrateSlot(client, &migrateOptions)
+               default:
+                       return fmt.Errorf("unsupported resource type: %s", 
resource)
+               }
+       },
+       SilenceUsage:  true,
+       SilenceErrors: true,
+}
+
+func migrationPreRun(_ *cobra.Command, args []string) error {
+       if len(args) < 1 {
+               return fmt.Errorf("resource type should be specified")
+       }
+       if len(args) < 2 {
+               return fmt.Errorf("the slot number should be specified")
+       }
+       slot, err := strconv.Atoi(args[1])
+       if err != nil {
+               return fmt.Errorf("invalid slot number: %s", args[1])
+       }
+       if slot < 0 || slot > 16383 {
+               return errors.New("slot number should be in range [0, 16383]")
+       }
+       migrateOptions.slot = slot
+
+       if migrateOptions.namespace == "" {
+               return fmt.Errorf("namespace is required, please specify with 
-n or --namespace")
+       }
+       if migrateOptions.cluster == "" {
+               return fmt.Errorf("cluster is required, please specify with -c 
or --cluster")
+       }
+       if migrateOptions.target < 0 {
+               return fmt.Errorf("target is required, please specify with 
--target")
+       }
+       return nil
+}
+
+func migrateSlot(client *client, options *MigrationOptions) error {
+       rsp, err := client.restyCli.R().
+               SetPathParam("namespace", options.namespace).
+               SetPathParam("cluster", options.cluster).
+               SetBody(map[string]interface{}{
+                       "slot":     options.slot,
+                       "target":   options.target,
+                       "slotOnly": strconv.FormatBool(options.slotOnly),
+               }).
+               Post("/namespaces/{namespace}/clusters/{cluster}/migrate")
+       if err != nil {
+               return err
+       }
+       if rsp.IsError() {
+               return errors.New(rsp.String())
+       }
+       printLine("migrate slot[%d] task is submitted successfully.", 
options.slot)
+       return nil
+}
+
+func init() {
+       MigrateCommand.Flags().IntVar(&migrateOptions.slot, "slot", -1, "The 
slot to migrate")
+       MigrateCommand.Flags().IntVar(&migrateOptions.target, "target", -1, 
"The target node")
+       MigrateCommand.Flags().StringVarP(&migrateOptions.namespace, 
"namespace", "n", "", "The namespace")
+       MigrateCommand.Flags().StringVarP(&migrateOptions.cluster, "cluster", 
"c", "", "The cluster")
+       MigrateCommand.Flags().BoolVar(&migrateOptions.slotOnly, "slot-only", 
false, "Only migrate slot and ignore the existing data")
+}
diff --git a/cmd/client/main.go b/cmd/client/main.go
index bac6575..93e2251 100644
--- a/cmd/client/main.go
+++ b/cmd/client/main.go
@@ -20,5 +20,45 @@
 
 package main
 
+import (
+       "fmt"
+       "os"
+
+       "github.com/fatih/color"
+
+       "github.com/spf13/cobra"
+
+       "github.com/apache/kvrocks-controller/cmd/client/command"
+)
+
+var rootCommand = &cobra.Command{
+       Use:   "kvctl",
+       Short: "kvctl is a command line tool for the Kvrocks controller 
service",
+       Run: func(cmd *cobra.Command, args []string) {
+               fmt.Println(cmd.PersistentFlags().GetString("host"))
+               _, _ = color.New(color.Bold).Println("Run 'kvctl --help' for 
usage.")
+               os.Exit(0)
+       },
+}
+
 func main() {
+       if err := rootCommand.Execute(); err != nil {
+               color.Red("error: %v", err)
+               os.Exit(1)
+       }
+}
+
+func init() {
+       rootCommand.PersistentFlags().StringP("host", "H",
+               "http://127.0.0.1:9379";, "The host of the Kvrocks controller 
service")
+
+       rootCommand.AddCommand(command.ListCommand)
+       rootCommand.AddCommand(command.CreateCommand)
+       rootCommand.AddCommand(command.GetCommand)
+       rootCommand.AddCommand(command.DeleteCommand)
+       rootCommand.AddCommand(command.ImportCommand)
+       rootCommand.AddCommand(command.MigrateCommand)
+
+       rootCommand.SilenceUsage = true
+       rootCommand.SilenceErrors = true
 }
diff --git a/consts/context_key.go b/consts/context_key.go
index f77ab01..d0d2080 100644
--- a/consts/context_key.go
+++ b/consts/context_key.go
@@ -23,9 +23,10 @@ const (
        ContextKeyStore        = "_context_key_storage"
        ContextKeyCluster      = "_context_key_cluster"
        ContextKeyClusterShard = "_context_key_cluster_shard"
+       ContextKeyHost         = "_context_key_host"
 )
 
 const (
-       HeaderIsRedirect     = "X-Is-Redirect"
-       HeaderDontDetectHost = "X-Dont-Detect-Host"
+       HeaderIsRedirect           = "X-Is-Redirect"
+       HeaderDontCheckClusterMode = "X-Dont-Check-Cluster-Mode"
 )
diff --git a/consts/errors.go b/consts/errors.go
index a44c032..8ad1034 100644
--- a/consts/errors.go
+++ b/consts/errors.go
@@ -25,6 +25,7 @@ import "errors"
 var (
        ErrInvalidArgument         = errors.New("invalid argument")
        ErrNotFound                = errors.New("not found")
+       ErrForbidden               = errors.New("forbidden")
        ErrAlreadyExists           = errors.New("already exists")
        ErrIndexOutOfRange         = errors.New("index out of range")
        ErrShardIsSame             = errors.New("source and target shard is 
same")
@@ -36,6 +37,4 @@ var (
        ErrShardIsServicing        = errors.New("shard is servicing")
        ErrShardSlotIsMigrating    = errors.New("shard slot is migrating")
        ErrShardNoMatchNewMaster   = errors.New("no match new master in shard")
-       ErrMismatchMigrateSlot     = errors.New("mismatch migrate slot")
-       ErrMigrateSlotFailed       = errors.New("migrate slot failed")
 )
diff --git a/controller/controller.go b/controller/controller.go
index 933553d..cd302d7 100644
--- a/controller/controller.go
+++ b/controller/controller.go
@@ -126,6 +126,7 @@ func (c *Controller) syncLoop(ctx context.Context) {
        prevTermLeader := ""
        if c.clusterStore.IsLeader() {
                c.becomeLeader(ctx, prevTermLeader)
+               prevTermLeader = c.clusterStore.ID()
        }
 
        c.readyCh <- struct{}{}
@@ -133,8 +134,10 @@ func (c *Controller) syncLoop(ctx context.Context) {
                select {
                case <-c.clusterStore.LeaderChange():
                        if c.clusterStore.IsLeader() {
-                               c.becomeLeader(ctx, prevTermLeader)
-                               prevTermLeader = c.clusterStore.ID()
+                               if prevTermLeader != c.clusterStore.ID() {
+                                       c.becomeLeader(ctx, prevTermLeader)
+                                       prevTermLeader = c.clusterStore.ID()
+                               }
                        } else {
                                if prevTermLeader != c.clusterStore.ID() {
                                        continue
diff --git a/go.mod b/go.mod
index 2cbf015..d03e7a5 100644
--- a/go.mod
+++ b/go.mod
@@ -3,28 +3,27 @@ module github.com/apache/kvrocks-controller
 go 1.19
 
 require (
-       github.com/c-bata/go-prompt v0.2.6
-       github.com/gin-gonic/gin v1.7.4
-       github.com/go-playground/validator/v10 v10.9.0
+       github.com/gin-gonic/gin v1.9.1
+       github.com/go-playground/validator/v10 v10.14.0
        github.com/go-redis/redis/v8 v8.11.5
-       github.com/go-resty/resty/v2 v2.7.0
+       github.com/go-resty/resty/v2 v2.12.0
        github.com/go-zookeeper/zk v1.0.3
-       github.com/google/uuid v1.3.0
-       github.com/jedib0t/go-pretty/v6 v6.4.6
+       github.com/olekukonko/tablewriter v0.0.5
        github.com/prometheus/client_golang v1.11.1
-       github.com/steinfletcher/apitest v1.5.15
-       github.com/stretchr/testify v1.7.4
+       github.com/spf13/cobra v1.8.0
+       github.com/stretchr/testify v1.8.3
        go.etcd.io/etcd v3.3.27+incompatible
        go.etcd.io/etcd/client/v3 v3.5.4
        go.uber.org/atomic v1.7.0
        go.uber.org/zap v1.21.0
-       golang.org/x/net v0.0.0-20211029224645-99673261e6eb
        gopkg.in/yaml.v1 v1.0.0-20140924161607-9f9df34309c0
 )
 
 require (
        github.com/beorn7/perks v1.0.1 // indirect
+       github.com/bytedance/sonic v1.9.1 // indirect
        github.com/cespare/xxhash/v2 v2.1.2 // indirect
+       github.com/chenzhuoyu/base64x v0.0.0-20221115062448-fe3a3abad311 // 
indirect
        github.com/coreos/etcd v3.3.27+incompatible // indirect
        github.com/coreos/go-semver v0.3.0 // indirect
        github.com/coreos/go-systemd v0.0.0-20191104093116-d3cd4ed1dbcf // 
indirect
@@ -32,36 +31,42 @@ require (
        github.com/coreos/pkg v0.0.0-20230327231512-ba87abf18a23 // indirect
        github.com/davecgh/go-spew v1.1.1 // indirect
        github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // 
indirect
+       github.com/fatih/color v1.16.0 // indirect
+       github.com/gabriel-vasile/mimetype v1.4.2 // indirect
        github.com/gin-contrib/sse v0.1.0 // indirect
-       github.com/go-playground/locales v0.14.0 // indirect
-       github.com/go-playground/universal-translator v0.18.0 // indirect
+       github.com/go-playground/locales v0.14.1 // indirect
+       github.com/go-playground/universal-translator v0.18.1 // indirect
+       github.com/goccy/go-json v0.10.2 // indirect
        github.com/gogo/protobuf v1.3.2 // indirect
        github.com/golang/protobuf v1.5.2 // indirect
-       github.com/json-iterator/go v1.1.11 // indirect
-       github.com/leodido/go-urn v1.2.1 // indirect
-       github.com/mattn/go-colorable v0.1.7 // indirect
-       github.com/mattn/go-isatty v0.0.12 // indirect
-       github.com/mattn/go-runewidth v0.0.13 // indirect
-       github.com/mattn/go-tty v0.0.3 // indirect
+       github.com/inconshreveable/mousetrap v1.1.0 // indirect
+       github.com/json-iterator/go v1.1.12 // indirect
+       github.com/klauspost/cpuid/v2 v2.2.4 // indirect
+       github.com/leodido/go-urn v1.2.4 // indirect
+       github.com/mattn/go-colorable v0.1.13 // indirect
+       github.com/mattn/go-isatty v0.0.20 // indirect
+       github.com/mattn/go-runewidth v0.0.9 // indirect
        github.com/matttproud/golang_protobuf_extensions v1.0.1 // indirect
        github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // 
indirect
-       github.com/modern-go/reflect2 v1.0.1 // indirect
-       github.com/pkg/term v1.2.0-beta.2 // indirect
+       github.com/modern-go/reflect2 v1.0.2 // indirect
+       github.com/pelletier/go-toml/v2 v2.0.8 // indirect
        github.com/pmezard/go-difflib v1.0.0 // indirect
        github.com/prometheus/client_model v0.2.0 // indirect
        github.com/prometheus/common v0.26.0 // indirect
        github.com/prometheus/procfs v0.6.0 // indirect
-       github.com/rivo/uniseg v0.2.0 // indirect
-       github.com/ugorji/go/codec v1.1.7 // indirect
+       github.com/spf13/pflag v1.0.5 // indirect
+       github.com/twitchyliquid64/golang-asm v0.15.1 // indirect
+       github.com/ugorji/go/codec v1.2.11 // indirect
        go.etcd.io/etcd/api/v3 v3.5.4 // indirect
        go.etcd.io/etcd/client/pkg/v3 v3.5.4 // indirect
        go.uber.org/multierr v1.6.0 // indirect
-       golang.org/x/crypto v0.0.0-20210711020723-a769d52b0f97 // indirect
-       golang.org/x/sys v0.1.0 // indirect
-       golang.org/x/text v0.3.7 // indirect
+       golang.org/x/arch v0.3.0 // indirect
+       golang.org/x/crypto v0.21.0 // indirect
+       golang.org/x/net v0.22.0 // indirect
+       golang.org/x/sys v0.18.0 // indirect
+       golang.org/x/text v0.14.0 // indirect
        google.golang.org/genproto v0.0.0-20210602131652-f16073e35f0c // 
indirect
        google.golang.org/grpc v1.38.0 // indirect
-       google.golang.org/protobuf v1.26.0 // indirect
-       gopkg.in/yaml.v2 v2.4.0 // indirect
+       google.golang.org/protobuf v1.30.0 // indirect
        gopkg.in/yaml.v3 v3.0.1 // indirect
 )
diff --git a/go.sum b/go.sum
index 31d49c9..89003fb 100644
--- a/go.sum
+++ b/go.sum
@@ -13,12 +13,16 @@ github.com/beorn7/perks 
v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24
 github.com/beorn7/perks v1.0.0/go.mod 
h1:KWe93zE9D1o94FZ5RNwFwVgaQK1VOXiVxmqh+CedLV8=
 github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM=
 github.com/beorn7/perks v1.0.1/go.mod 
h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw=
-github.com/c-bata/go-prompt v0.2.6 
h1:POP+nrHE+DfLYx370bedwNhsqmpCUynWPxuHi0C5vZI=
-github.com/c-bata/go-prompt v0.2.6/go.mod 
h1:/LMAke8wD2FsNu9EXNdHxNLbd9MedkPnCdfpU9wwHfY=
+github.com/bytedance/sonic v1.5.0/go.mod 
h1:ED5hyg4y6t3/9Ku1R6dU/4KyJ48DZ4jPhfY1O2AihPM=
+github.com/bytedance/sonic v1.9.1 
h1:6iJ6NqdoxCDr6mbY8h18oSO+cShGSMRGCEo7F2h0x8s=
+github.com/bytedance/sonic v1.9.1/go.mod 
h1:i736AoUSYt75HyZLoJW9ERYxcy6eaN6h4BZXU064P/U=
 github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod 
h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU=
 github.com/cespare/xxhash/v2 v2.1.1/go.mod 
h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
 github.com/cespare/xxhash/v2 v2.1.2 
h1:YRXhKfTDauu4ajMg1TPgFO5jnlC2HCbmLXMcTG5cbYE=
 github.com/cespare/xxhash/v2 v2.1.2/go.mod 
h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
+github.com/chenzhuoyu/base64x v0.0.0-20211019084208-fb5309c8db06/go.mod 
h1:DH46F32mSOjUmXrMHnKwZdA8wcEefY7UVqBKYGjpdQY=
+github.com/chenzhuoyu/base64x v0.0.0-20221115062448-fe3a3abad311 
h1:qSGYFH7+jGhDF8vLC+iwCD4WpbV1EBDSzWkJODFLams=
+github.com/chenzhuoyu/base64x v0.0.0-20221115062448-fe3a3abad311/go.mod 
h1:b583jCggY9gE99b6G5LEC39OIiVsWj+R97kbl5odCEk=
 github.com/client9/misspell v0.3.4/go.mod 
h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw=
 github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod 
h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc=
 github.com/cncf/udpa/go v0.0.0-20201120205902-5459f2c99403/go.mod 
h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk=
@@ -32,7 +36,7 @@ github.com/coreos/go-systemd/v22 v22.3.2 
h1:D9/bQk5vlXQFZ6Kwuu6zaiXJ9oTPe68++AzA
 github.com/coreos/go-systemd/v22 v22.3.2/go.mod 
h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc=
 github.com/coreos/pkg v0.0.0-20230327231512-ba87abf18a23 
h1:SrdboTJZnOqc2r4cT4wQCzQJjGYwkclLwx2sPrDsx7g=
 github.com/coreos/pkg v0.0.0-20230327231512-ba87abf18a23/go.mod 
h1:E3G3o1h8I7cfcXa63jLwjI0eiQQMgzzUDFVpN/nH/eA=
-github.com/creack/pty v1.1.9/go.mod 
h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
+github.com/cpuguy83/go-md2man/v2 v2.0.3/go.mod 
h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o=
 github.com/davecgh/go-spew v1.1.0/go.mod 
h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
 github.com/davecgh/go-spew v1.1.1 
h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
 github.com/davecgh/go-spew v1.1.1/go.mod 
h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
@@ -44,36 +48,38 @@ github.com/envoyproxy/go-control-plane 
v0.9.1-0.20191026205805-5f8ba28d4473/go.m
 github.com/envoyproxy/go-control-plane v0.9.4/go.mod 
h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98=
 github.com/envoyproxy/go-control-plane 
v0.9.9-0.20210217033140-668b12f5399d/go.mod 
h1:cXg6YxExXjJnVBQHBLXeUAgxn2UodCpnH306RInaBQk=
 github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod 
h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c=
+github.com/fatih/color v1.16.0 h1:zmkK9Ngbjj+K0yRhTVONQh1p/HknKYSlNT+vZCzyokM=
+github.com/fatih/color v1.16.0/go.mod 
h1:fL2Sau1YI5c0pdGEVCbKQbLXB6edEj1ZgiY4NijnWvE=
 github.com/fsnotify/fsnotify v1.4.9 
h1:hsms1Qyu0jgnwNXIxa+/V/PDsU6CfLf6CNO8H7IWoS4=
+github.com/gabriel-vasile/mimetype v1.4.2 
h1:w5qFW6JKBz9Y393Y4q372O9A7cUSequkh1Q7OhCmWKU=
+github.com/gabriel-vasile/mimetype v1.4.2/go.mod 
h1:zApsH/mKG4w07erKIaJPFiX0Tsq9BFQgN3qGY5GnNgA=
 github.com/ghodss/yaml v1.0.0/go.mod 
h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04=
 github.com/gin-contrib/sse v0.1.0 
h1:Y/yl/+YNO8GZSjAhjMsSuLt29uWRFHdHYUb5lYOV9qE=
 github.com/gin-contrib/sse v0.1.0/go.mod 
h1:RHrZQHXnP2xjPF+u1gW/2HnVO7nvIa9PG3Gm+fLHvGI=
-github.com/gin-gonic/gin v1.7.4 h1:QmUZXrvJ9qZ3GfWvQ+2wnW/1ePrTEJqPKMYEU3lD/DM=
-github.com/gin-gonic/gin v1.7.4/go.mod 
h1:jD2toBW3GZUr5UMcdrwQA10I7RuaFOl/SGeDjXkfUtY=
+github.com/gin-gonic/gin v1.9.1 h1:4idEAncQnU5cB7BeOkPtxjfCSye0AAm1R0RVIqJ+Jmg=
+github.com/gin-gonic/gin v1.9.1/go.mod 
h1:hPrL7YrpYKXt5YId3A/Tnip5kqbEAP+KLuI3SUcPTeU=
 github.com/go-kit/kit v0.8.0/go.mod 
h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as=
 github.com/go-kit/kit v0.9.0/go.mod 
h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as=
 github.com/go-kit/log v0.1.0/go.mod 
h1:zbhenjAZHb184qTLMA9ZjW7ThYL0H2mk7Q6pNt4vbaY=
 github.com/go-logfmt/logfmt v0.3.0/go.mod 
h1:Qt1PoO58o5twSAckw1HlFXLmHsOX5/0LbT9GBnD5lWE=
 github.com/go-logfmt/logfmt v0.4.0/go.mod 
h1:3RMwSq7FuexP4Kalkev3ejPJsZTpXXBr9+V4qmtdjCk=
 github.com/go-logfmt/logfmt v0.5.0/go.mod 
h1:wCYkCAKZfumFQihp8CzCvQ3paCTfi41vtzG1KdI/P7A=
-github.com/go-playground/assert/v2 v2.0.1 
h1:MsBgLAaY856+nPRTKrp3/OZK38U/wa0CcBYNjji3q3A=
-github.com/go-playground/assert/v2 v2.0.1/go.mod 
h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4=
-github.com/go-playground/locales v0.13.0/go.mod 
h1:taPMhCMXrRLJO55olJkUXHZBHCxTMfnGwq/HNwmWNS8=
-github.com/go-playground/locales v0.14.0 
h1:u50s323jtVGugKlcYeyzC0etD1HifMjqmJqb8WugfUU=
-github.com/go-playground/locales v0.14.0/go.mod 
h1:sawfccIbzZTqEDETgFXqTho0QybSa7l++s0DH+LDiLs=
-github.com/go-playground/universal-translator v0.17.0/go.mod 
h1:UkSxE5sNxxRwHyU+Scu5vgOQjsIJAF8j9muTVoKLVtA=
-github.com/go-playground/universal-translator v0.18.0 
h1:82dyy6p4OuJq4/CByFNOn/jYrnRPArHwAcmLoJZxyho=
-github.com/go-playground/universal-translator v0.18.0/go.mod 
h1:UvRDBj+xPUEGrFYl+lu/H90nyDXpg0fqeB/AQUGNTVA=
-github.com/go-playground/validator/v10 v10.4.1/go.mod 
h1:nlOn6nFhuKACm19sB/8EGNn9GlaMV7XkbRSipzJ0Ii4=
-github.com/go-playground/validator/v10 v10.9.0 
h1:NgTtmN58D0m8+UuxtYmGztBJB7VnPgjj221I1QHci2A=
-github.com/go-playground/validator/v10 v10.9.0/go.mod 
h1:74x4gJWsvQexRdW8Pn3dXSGrTK4nAUsbPlLADvpJkos=
+github.com/go-playground/assert/v2 v2.2.0 
h1:JvknZsQTYeFEAhQwI4qEt9cyV5ONwRHC+lYKSsYSR8s=
+github.com/go-playground/locales v0.14.1 
h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/oXslEjJA=
+github.com/go-playground/locales v0.14.1/go.mod 
h1:hxrqLVvrK65+Rwrd5Fc6F2O76J/NuW9t0sjnWqG1slY=
+github.com/go-playground/universal-translator v0.18.1 
h1:Bcnm0ZwsGyWbCzImXv+pAJnYK9S473LQFuzCbDbfSFY=
+github.com/go-playground/universal-translator v0.18.1/go.mod 
h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY=
+github.com/go-playground/validator/v10 v10.14.0 
h1:vgvQWe3XCz3gIeFDm/HnTIbj6UGmg/+t63MyGU2n5js=
+github.com/go-playground/validator/v10 v10.14.0/go.mod 
h1:9iXMNT7sEkjXb0I+enO7QXmzG6QCsPWY4zveKFVRSyU=
 github.com/go-redis/redis/v8 v8.11.5 
h1:AcZZR7igkdvfVmQTPnu9WE37LRrO/YrBH5zWyjDC0oI=
 github.com/go-redis/redis/v8 v8.11.5/go.mod 
h1:gREzHqY1hg6oD9ngVRbLStwAWKhA0FEgq8Jd4h5lpwo=
-github.com/go-resty/resty/v2 v2.7.0 
h1:me+K9p3uhSmXtrBZ4k9jcEAfJmuC8IivWHwaLZwPrFY=
-github.com/go-resty/resty/v2 v2.7.0/go.mod 
h1:9PWDzw47qPphMRFfhsyk0NnSgvluHcljSMVIq3w7q0I=
+github.com/go-resty/resty/v2 v2.12.0 
h1:rsVL8P90LFvkUYq/V5BTVe203WfRIU4gvcf+yfzJzGA=
+github.com/go-resty/resty/v2 v2.12.0/go.mod 
h1:o0yGPrkS3lOe1+eFajk6kBW8ScXzwU3hD69/gt2yB/0=
 github.com/go-stack/stack v1.8.0/go.mod 
h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY=
 github.com/go-zookeeper/zk v1.0.3 
h1:7M2kwOsc//9VeeFiPtf+uSJlVpU66x9Ba5+8XK7/TDg=
 github.com/go-zookeeper/zk v1.0.3/go.mod 
h1:nOB03cncLtlp4t+UAkGSV+9beXP/akpekBwL+UX1Qcw=
+github.com/goccy/go-json v0.10.2 
h1:CrxCmQqYDkv1z7lO7Wbh2HN93uovUHgrECaO5ZrCXAU=
+github.com/goccy/go-json v0.10.2/go.mod 
h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I=
 github.com/godbus/dbus/v5 v5.0.4/go.mod 
h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA=
 github.com/gogo/protobuf v1.1.1/go.mod 
h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ=
 github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q=
@@ -105,70 +111,64 @@ github.com/google/go-cmp v0.5.5 
h1:Khx7svrCpmxxtHBq5j2mp/xVjsi8hQMfNLvJFAlrGgU=
 github.com/google/go-cmp v0.5.5/go.mod 
h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
 github.com/google/gofuzz v1.0.0/go.mod 
h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
 github.com/google/uuid v1.1.2/go.mod 
h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
-github.com/google/uuid v1.3.0 h1:t6JiXgmwXMjEs8VusXIJk2BXHsn+wx8BZdTaoZ5fu7I=
-github.com/google/uuid v1.3.0/go.mod 
h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
 github.com/grpc-ecosystem/go-grpc-prometheus v1.2.0/go.mod 
h1:8NvIoxWQoOIhqOTXgfV/d3M/q6VIi02HzZEHgUlZvzk=
 github.com/grpc-ecosystem/grpc-gateway v1.16.0/go.mod 
h1:BDjrQk3hbvj6Nolgz8mAMFbcEtjT1g+wF4CSlocrBnw=
-github.com/jedib0t/go-pretty/v6 v6.4.6 
h1:v6aG9h6Uby3IusSSEjHaZNXpHFhzqMmjXcPq1Rjl9Jw=
-github.com/jedib0t/go-pretty/v6 v6.4.6/go.mod 
h1:Ndk3ase2CkQbXLLNf5QDHoYb6J9WtVfmHZu9n8rk2xs=
+github.com/inconshreveable/mousetrap v1.1.0 
h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8=
+github.com/inconshreveable/mousetrap v1.1.0/go.mod 
h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw=
 github.com/jpillora/backoff v1.0.0/go.mod 
h1:J/6gKK9jxlEcS3zixgDgUAsiuZ7yrSoa/FX5e0EB2j4=
 github.com/json-iterator/go v1.1.6/go.mod 
h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU=
-github.com/json-iterator/go v1.1.9/go.mod 
h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4=
 github.com/json-iterator/go v1.1.10/go.mod 
h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4=
-github.com/json-iterator/go v1.1.11 
h1:uVUAXhF2To8cbw/3xN3pxj6kk7TYKs98NIrTqPlMWAQ=
 github.com/json-iterator/go v1.1.11/go.mod 
h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4=
+github.com/json-iterator/go v1.1.12 
h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM=
+github.com/json-iterator/go v1.1.12/go.mod 
h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo=
 github.com/julienschmidt/httprouter v1.2.0/go.mod 
h1:SYymIcj16QtmaHHD7aYtjjsJG7VTCxuUUipMqKk8s4w=
 github.com/julienschmidt/httprouter v1.3.0/go.mod 
h1:JR6WtHb+2LUe8TCKY3cZOxFyyO8IZAc4RVcycCCAKdM=
 github.com/kisielk/errcheck v1.5.0/go.mod 
h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8=
 github.com/kisielk/gotool v1.0.0/go.mod 
h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck=
+github.com/klauspost/cpuid/v2 v2.0.9/go.mod 
h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg=
+github.com/klauspost/cpuid/v2 v2.2.4 
h1:acbojRNwl3o09bUq+yDCtZFc1aiwaAAxtcn8YkZXnvk=
+github.com/klauspost/cpuid/v2 v2.2.4/go.mod 
h1:RVVoqg1df56z8g3pUjL/3lE5UfnlrJX8tyFgg4nqhuY=
 github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod 
h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
 github.com/konsorten/go-windows-terminal-sequences v1.0.3/go.mod 
h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
 github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515/go.mod 
h1:+0opPa2QZZtGFBFZlji/RkVcI2GknAs/DXo4wKdlNEc=
+github.com/kr/pretty v0.1.0 h1:L/CwN0zerZDmRFUapSPitk6f+Q3+0za1rQkzVuMiMFI=
 github.com/kr/pretty v0.1.0/go.mod 
h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
-github.com/kr/pretty v0.2.1/go.mod 
h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI=
-github.com/kr/pretty v0.3.0 h1:WgNl7dwNpEZ6jJ9k1snq4pZsg7DOEN8hP9Xw0Tsjwk0=
-github.com/kr/pretty v0.3.0/go.mod 
h1:640gp4NfQd8pI5XOwp5fnNeVWj67G7CFk/SaSQn7NBk=
 github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
+github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE=
 github.com/kr/text v0.1.0/go.mod 
h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
-github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
-github.com/kr/text v0.2.0/go.mod 
h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
-github.com/leodido/go-urn v1.2.0/go.mod 
h1:+8+nEpDfqqsY+g338gtMEUOtuK+4dEMhiQEgxpxOKII=
-github.com/leodido/go-urn v1.2.1 
h1:BqpAaACuzVSgi/VLzGZIobT2z4v53pjosyNd9Yv6n/w=
-github.com/leodido/go-urn v1.2.1/go.mod 
h1:zt4jvISO2HfUBqxjfIshjdMTYS56ZS/qv49ictyFfxY=
-github.com/mattn/go-colorable v0.1.4/go.mod 
h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVcfRqFIhoBtE=
-github.com/mattn/go-colorable v0.1.7 
h1:bQGKb3vps/j0E9GfJQ03JyhRuxsvdAanXlT9BTw3mdw=
-github.com/mattn/go-colorable v0.1.7/go.mod 
h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc=
-github.com/mattn/go-isatty v0.0.8/go.mod 
h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s=
-github.com/mattn/go-isatty v0.0.10/go.mod 
h1:qgIWMr58cqv1PHHyhnkY9lrL7etaEgOFcMEpPG5Rm84=
-github.com/mattn/go-isatty v0.0.12 
h1:wuysRhFDzyxgEmMf5xjvJ2M9dZoWAXNNr5LSBS7uHXY=
-github.com/mattn/go-isatty v0.0.12/go.mod 
h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU=
-github.com/mattn/go-runewidth v0.0.6/go.mod 
h1:H031xJmbD/WCDINGzjvQ9THkh0rPKHF+m2gUSrubnMI=
+github.com/leodido/go-urn v1.2.4 
h1:XlAE/cm/ms7TE/VMVoduSpNBoyc2dOxHs5MZSwAN63Q=
+github.com/leodido/go-urn v1.2.4/go.mod 
h1:7ZrI8mTSeBSHl/UaRyKQW1qZeMgak41ANeCNaVckg+4=
+github.com/mattn/go-colorable v0.1.13 
h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA=
+github.com/mattn/go-colorable v0.1.13/go.mod 
h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg=
+github.com/mattn/go-isatty v0.0.16/go.mod 
h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM=
+github.com/mattn/go-isatty v0.0.19 
h1:JITubQf0MOLdlGRuRq+jtsDlekdYPia9ZFsB8h/APPA=
+github.com/mattn/go-isatty v0.0.19/go.mod 
h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
+github.com/mattn/go-isatty v0.0.20 
h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
+github.com/mattn/go-isatty v0.0.20/go.mod 
h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
+github.com/mattn/go-runewidth v0.0.9 
h1:Lm995f3rfxdpd6TSmuVCHVb/QhupuXlYr8sCI/QdE+0=
 github.com/mattn/go-runewidth v0.0.9/go.mod 
h1:H031xJmbD/WCDINGzjvQ9THkh0rPKHF+m2gUSrubnMI=
-github.com/mattn/go-runewidth v0.0.13 
h1:lTGmDsbAYt5DmK6OnoV7EuIF1wEIFAcxld6ypU4OSgU=
-github.com/mattn/go-runewidth v0.0.13/go.mod 
h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w=
-github.com/mattn/go-tty v0.0.3 h1:5OfyWorkyO7xP52Mq7tB36ajHDG5OHrmBGIS/DtakQI=
-github.com/mattn/go-tty v0.0.3/go.mod 
h1:ihxohKRERHTVzN+aSVRwACLCeqIoZAWpoICkkvrWyR0=
 github.com/matttproud/golang_protobuf_extensions v1.0.1 
h1:4hp9jkHxhMHkqkrB3Ix0jegS5sx/RkqARlsWZ6pIwiU=
 github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod 
h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0=
 github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod 
h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
 github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd 
h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg=
 github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod 
h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
 github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod 
h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0=
-github.com/modern-go/reflect2 v1.0.1 
h1:9f412s+6RmYXLWZSEzVVgPGK7C2PphHj5RJrvfx9AWI=
 github.com/modern-go/reflect2 v1.0.1/go.mod 
h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0=
+github.com/modern-go/reflect2 v1.0.2 
h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M=
+github.com/modern-go/reflect2 v1.0.2/go.mod 
h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk=
 github.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod 
h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U=
 github.com/mwitkow/go-conntrack v0.0.0-20190716064945-2f068394615f/go.mod 
h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U=
 github.com/nxadm/tail v1.4.8 h1:nPr65rt6Y5JFSKQO7qToXr7pePgD6Gwiw05lkbyAQTE=
+github.com/olekukonko/tablewriter v0.0.5 
h1:P2Ga83D34wi1o9J6Wh1mRuqd4mF/x/lgBS7N7AbDhec=
+github.com/olekukonko/tablewriter v0.0.5/go.mod 
h1:hPp6KlRPjbx+hW8ykQs1w3UBbZlj6HuIJcUGPhkA7kY=
 github.com/onsi/ginkgo v1.16.5 h1:8xi0RTUf59SOSfEtZMvwTvXYMzG4gV23XVHOZiXNtnE=
 github.com/onsi/gomega v1.18.1 h1:M1GfJqGRrBrrGGsbxzV5dqM2U2ApXefZCQpkukxYRLE=
-github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod 
h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA=
+github.com/pelletier/go-toml/v2 v2.0.8 
h1:0ctb6s9mE31h0/lhu+J6OPmVeDxJn+kYnJc2jZR9tGQ=
+github.com/pelletier/go-toml/v2 v2.0.8/go.mod 
h1:vuYfssBdrU2XDZ9bYydBu6t+6a6PYNcZljzZR9VXg+4=
 github.com/pkg/errors v0.8.0/go.mod 
h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
 github.com/pkg/errors v0.8.1/go.mod 
h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
 github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
 github.com/pkg/errors v0.9.1/go.mod 
h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
-github.com/pkg/profile v1.6.0/go.mod 
h1:qBsxPvzyUincmltOk6iyRVxHYg4adc0OFOv72ZdLa18=
-github.com/pkg/term v1.2.0-beta.2 
h1:L3y/h2jkuBVFdWiJvNfYfKmzcCnILw7mJWm2JQuMppw=
-github.com/pkg/term v1.2.0-beta.2/go.mod 
h1:E25nymQcrSllhX42Ok8MRm1+hyBdHY0dCeiKZ9jpNGw=
 github.com/pmezard/go-difflib v1.0.0 
h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
 github.com/pmezard/go-difflib v1.0.0/go.mod 
h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
 github.com/prometheus/client_golang v0.9.1/go.mod 
h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw=
@@ -190,35 +190,38 @@ github.com/prometheus/procfs v0.0.2/go.mod 
h1:TjEm7ze935MbeOT/UhFTIMYKhuLP4wbCsT
 github.com/prometheus/procfs v0.1.3/go.mod 
h1:lV6e/gmhEcM9IjHGsFOCxxuZ+z1YqCvr4OA4YeYWdaU=
 github.com/prometheus/procfs v0.6.0 
h1:mxy4L2jP6qMonqmq+aTtOx1ifVWUgG/TAmntgbh3xv4=
 github.com/prometheus/procfs v0.6.0/go.mod 
h1:cz+aTbrPOrUb4q7XlbU9ygM+/jj0fzG6c1xBZuNvfVA=
-github.com/rivo/uniseg v0.2.0 h1:S1pD9weZBuJdFmowNwbpi7BJ8TNftyUImj/0WQi72jY=
-github.com/rivo/uniseg v0.2.0/go.mod 
h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
 github.com/rogpeppe/fastuuid v1.2.0/go.mod 
h1:jVj6XXZzXRy/MSR5jhDC/2q6DgLz+nrA6LYCDYWNEvQ=
-github.com/rogpeppe/go-internal v1.6.1/go.mod 
h1:xXDCJY+GAPziupqXw64V24skbSoqbTEfhy4qGm1nDQc=
-github.com/rogpeppe/go-internal v1.8.0 
h1:FCbCCtXNOY3UtUuHUYaghJg4y7Fd14rXifAYUAtL9R8=
-github.com/rogpeppe/go-internal v1.8.0/go.mod 
h1:WmiCO8CzOY8rg0OYDC4/i/2WRWAB6poM+XZ2dLUbcbE=
+github.com/russross/blackfriday/v2 v2.1.0/go.mod 
h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
 github.com/sirupsen/logrus v1.2.0/go.mod 
h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo=
 github.com/sirupsen/logrus v1.4.2/go.mod 
h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE=
 github.com/sirupsen/logrus v1.6.0/go.mod 
h1:7uNnSEd1DgxDLC74fIahvMZmmYsHGZGEOFrfsX/uA88=
-github.com/steinfletcher/apitest v1.5.15 
h1:AAdTN0yMbf0VMH/PMt9uB2I7jljepO6i+5uhm1PjH3c=
-github.com/steinfletcher/apitest v1.5.15/go.mod 
h1:mF+KnYaIkuHM0C4JgGzkIIOJAEjo+EA5tTjJ+bHXnQc=
+github.com/spf13/cobra v1.8.0 h1:7aJaZx1B85qltLMc546zn58BxxfZdR/W22ej9CFoEf0=
+github.com/spf13/cobra v1.8.0/go.mod 
h1:WXLWApfZ71AjXPya3WOlMsY9yMs7YeiHhFVlvLyhcho=
+github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA=
+github.com/spf13/pflag v1.0.5/go.mod 
h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
 github.com/stretchr/objx v0.1.0/go.mod 
h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
 github.com/stretchr/objx v0.1.1/go.mod 
h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
 github.com/stretchr/objx v0.4.0/go.mod 
h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
+github.com/stretchr/objx v0.5.0/go.mod 
h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
 github.com/stretchr/testify v1.2.2/go.mod 
h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
 github.com/stretchr/testify v1.3.0/go.mod 
h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
 github.com/stretchr/testify v1.4.0/go.mod 
h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
 github.com/stretchr/testify v1.5.1/go.mod 
h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA=
-github.com/stretchr/testify v1.6.1/go.mod 
h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
 github.com/stretchr/testify v1.7.0/go.mod 
h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
 github.com/stretchr/testify v1.7.1/go.mod 
h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
-github.com/stretchr/testify v1.7.4 
h1:wZRexSlwd7ZXfKINDLsO4r7WBt3gTKONc6K/VesHvHM=
-github.com/stretchr/testify v1.7.4/go.mod 
h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
-github.com/ugorji/go v1.1.7/go.mod 
h1:kZn38zHttfInRq0xu/PH0az30d+z6vm202qpg1oXVMw=
-github.com/ugorji/go/codec v1.1.7 
h1:2SvQaVZ1ouYrrKKwoSk2pzd4A9evlKJb9oTL+OaLUSs=
-github.com/ugorji/go/codec v1.1.7/go.mod 
h1:Ax+UKWsSmolVDwsd+7N3ZtXu+yMGCf907BLYF3GoBXY=
+github.com/stretchr/testify v1.8.0/go.mod 
h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
+github.com/stretchr/testify v1.8.1/go.mod 
h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
+github.com/stretchr/testify v1.8.2/go.mod 
h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
+github.com/stretchr/testify v1.8.3 
h1:RP3t2pwF7cMEbC1dqtB6poj3niw/9gnV4Cjg5oW5gtY=
+github.com/stretchr/testify v1.8.3/go.mod 
h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
+github.com/twitchyliquid64/golang-asm v0.15.1 
h1:SU5vSMR7hnwNxj24w34ZyCi/FmDZTkS4MhqMhdFk5YI=
+github.com/twitchyliquid64/golang-asm v0.15.1/go.mod 
h1:a1lVb/DtPvCB8fslRZhAngC2+aY1QWCk3Cedj/Gdt08=
+github.com/ugorji/go/codec v1.2.11 
h1:BMaWp1Bb6fHwEtbplGBGJ498wD+LKlNSl25MjdZY4dU=
+github.com/ugorji/go/codec v1.2.11/go.mod 
h1:UNopzCgEMSXjBc6AOMqYvWC1ktqTAfzJZUZgYf6w6lg=
 github.com/yuin/goldmark v1.1.27/go.mod 
h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
 github.com/yuin/goldmark v1.2.1/go.mod 
h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
 github.com/yuin/goldmark v1.3.5/go.mod 
h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k=
+github.com/yuin/goldmark v1.4.13/go.mod 
h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
 go.etcd.io/etcd v3.3.27+incompatible 
h1:5hMrpf6REqTHV2LW2OclNpRtxI0k9ZplMemJsMSWju0=
 go.etcd.io/etcd v3.3.27+incompatible/go.mod 
h1:yaeTdrJi5lOmYerz05bd8+V7KubZs8YSFZfzsF9A6aI=
 go.etcd.io/etcd/api/v3 v3.5.4 h1:OHVyt3TopwtUQ2GKdd5wu3PmmipR4FTwCqoEjSyRdIc=
@@ -236,12 +239,17 @@ go.uber.org/multierr v1.6.0/go.mod 
h1:cdWPpRnG4AhwMwsgIHip0KRBQjJy5kYEpYjJxpXp9i
 go.uber.org/zap v1.17.0/go.mod h1:MXVU+bhUf/A7Xi2HNOnopQOrmycQ5Ih87HtOu4q5SSo=
 go.uber.org/zap v1.21.0 h1:WefMeulhovoZ2sYXz7st6K0sLj7bBhpiFaud4r4zST8=
 go.uber.org/zap v1.21.0/go.mod h1:wjWOCqI0f2ZZrJF/UufIOkiC8ii6tm1iqIsLo76RfJw=
+golang.org/x/arch v0.0.0-20210923205945-b76863e36670/go.mod 
h1:5om86z9Hs0C8fWVUuoMHwpExlXzs5Tkyp9hOrfG7pp8=
+golang.org/x/arch v0.3.0 h1:02VY4/ZcO/gBOH6PUaoiptASxtXU10jazRCP865E97k=
+golang.org/x/arch v0.3.0/go.mod h1:5om86z9Hs0C8fWVUuoMHwpExlXzs5Tkyp9hOrfG7pp8=
 golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod 
h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
 golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod 
h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
 golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod 
h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
 golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod 
h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
-golang.org/x/crypto v0.0.0-20210711020723-a769d52b0f97 
h1:/UOmuWzQfxxo9UtlXMwuQU8CMgg1eZXqTRwkSQJWKOI=
-golang.org/x/crypto v0.0.0-20210711020723-a769d52b0f97/go.mod 
h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
+golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod 
h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
+golang.org/x/crypto v0.19.0/go.mod 
h1:Iy9bg/ha4yyC70EfRS8jz+B6ybOBKMaSxLj6P6oBDfU=
+golang.org/x/crypto v0.21.0 h1:X31++rzVUdKhX5sWmSOFZxx8UW/ldWx55cbf08iNAMA=
+golang.org/x/crypto v0.21.0/go.mod 
h1:0BP7YvVV9gBbVKyeTG0Gyn+gZm94bibOW5BjDEYAOMs=
 golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod 
h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
 golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod 
h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
 golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod 
h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU=
@@ -252,6 +260,8 @@ golang.org/x/mod 
v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzB
 golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
 golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
 golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
+golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod 
h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
+golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
 golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod 
h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
 golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod 
h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
 golang.org/x/net v0.0.0-20181114220301-adae6a3d119a/go.mod 
h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
@@ -267,8 +277,12 @@ golang.org/x/net v0.0.0-20200822124328-c89045814202/go.mod 
h1:/O7V0waA8r7cgGh81R
 golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod 
h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
 golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod 
h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
 golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod 
h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM=
-golang.org/x/net v0.0.0-20211029224645-99673261e6eb 
h1:pirldcYWx7rx7kE5r+9WsOXPXK0+WH5+uZ7uPmJ44uM=
-golang.org/x/net v0.0.0-20211029224645-99673261e6eb/go.mod 
h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
+golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod 
h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
+golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
+golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg=
+golang.org/x/net v0.21.0/go.mod h1:bIjVDfnllIU7BJ2DNgfnXvpSvtn8VRwhlsaeUTyUS44=
+golang.org/x/net v0.22.0 h1:9sGLhx7iRIHEiX0oAJ3MRZMUCElJgy7Br1nO+AMN3Tc=
+golang.org/x/net v0.22.0/go.mod h1:JKghWKKOSdJwpW2GEx0Ja7fmaKnMsbu+MWVZTokSYmg=
 golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod 
h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
 golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod 
h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
 golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d/go.mod 
h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
@@ -280,43 +294,53 @@ golang.org/x/sync 
v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJ
 golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod 
h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
 golang.org/x/sync v0.0.0-20201207232520-09787c993a3a/go.mod 
h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
 golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod 
h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
+golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod 
h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
+golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
 golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod 
h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
 golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod 
h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
 golang.org/x/sys v0.0.0-20181116152217-5ac8a444bdc5/go.mod 
h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
 golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod 
h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
-golang.org/x/sys v0.0.0-20190222072716-a9d3bda3a223/go.mod 
h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
 golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod 
h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
 golang.org/x/sys v0.0.0-20190422165155-953cdadca894/go.mod 
h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
-golang.org/x/sys v0.0.0-20191008105621-543471e840be/go.mod 
h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
-golang.org/x/sys v0.0.0-20191120155948-bd437916bb0e/go.mod 
h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
 golang.org/x/sys v0.0.0-20200106162015-b016eb3dc98e/go.mod 
h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
-golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod 
h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
-golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod 
h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
 golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod 
h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
 golang.org/x/sys v0.0.0-20200615200032-f1bc736245b1/go.mod 
h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
 golang.org/x/sys v0.0.0-20200625212154-ddb9806d33ae/go.mod 
h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
-golang.org/x/sys v0.0.0-20200909081042-eff7692f9009/go.mod 
h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
-golang.org/x/sys v0.0.0-20200918174421-af09f7315aff/go.mod 
h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
 golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod 
h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
 golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod 
h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
 golang.org/x/sys v0.0.0-20210124154548-22da62e12c0c/go.mod 
h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
 golang.org/x/sys v0.0.0-20210330210617-4fbd30eecc44/go.mod 
h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
 golang.org/x/sys v0.0.0-20210403161142-5e06dd20ab57/go.mod 
h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
-golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod 
h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
 golang.org/x/sys v0.0.0-20210510120138-977fb7262007/go.mod 
h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
 golang.org/x/sys v0.0.0-20210603081109-ebe580a85c40/go.mod 
h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
 golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod 
h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
-golang.org/x/sys v0.0.0-20210806184541-e5e7981a1069/go.mod 
h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
-golang.org/x/sys v0.1.0 h1:kunALQeHf1/185U1i0GOB/fy1IPRDDpuoOOqRReG57U=
-golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod 
h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/sys v0.0.0-20220704084225-05e143d24a9e/go.mod 
h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod 
h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod 
h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
+golang.org/x/sys v0.18.0 h1:DBdB3niSjOA/O0blCZBqDefyWNYveAYMNF1Wum0DYQ4=
+golang.org/x/sys v0.18.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
 golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod 
h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
+golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod 
h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
+golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k=
+golang.org/x/term v0.8.0/go.mod h1:xPskH00ivmX89bAKVGSKKtLOWNx2+17Eiy94tnKShWo=
+golang.org/x/term v0.17.0/go.mod 
h1:lLRBjIVuehSbZlaOtGMbcMncT+aqLLLmKrsjNrUguwk=
+golang.org/x/term v0.18.0/go.mod 
h1:ILwASektA3OnRv7amZ1xhE/KTR+u50pbXfZ03+6Nx58=
 golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
 golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
 golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
 golang.org/x/text v0.3.5/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
-golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
-golang.org/x/text v0.3.7 h1:olpwvP2KacW1ZWvsR7uQhoyTYvKAupfQrRGBFM352Gk=
 golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
+golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
+golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8=
+golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ=
+golang.org/x/text v0.14.0/go.mod 
h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
+golang.org/x/time v0.5.0 h1:o7cqy6amK/52YcAKIPlM3a+Fpj35zvRj2TP+e1xFSfk=
+golang.org/x/time v0.5.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM=
 golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod 
h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
 golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod 
h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
 golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod 
h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY=
@@ -328,6 +352,8 @@ golang.org/x/tools 
v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roY
 golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod 
h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
 golang.org/x/tools v0.1.2/go.mod 
h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk=
 golang.org/x/tools v0.1.5/go.mod 
h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk=
+golang.org/x/tools v0.1.12/go.mod 
h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
+golang.org/x/tools v0.6.0/go.mod 
h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU=
 golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod 
h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
 golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod 
h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
 golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod 
h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
@@ -358,15 +384,14 @@ google.golang.org/protobuf v1.23.0/go.mod 
h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2
 google.golang.org/protobuf v1.23.1-0.20200526195155-81db48ad09cc/go.mod 
h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
 google.golang.org/protobuf v1.25.0/go.mod 
h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlbajtzgsN7c=
 google.golang.org/protobuf v1.26.0-rc.1/go.mod 
h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw=
-google.golang.org/protobuf v1.26.0 
h1:bxAC2xTBsZGibn2RTntX0oH50xLsqy1OxA9tTL3p/lk=
 google.golang.org/protobuf v1.26.0/go.mod 
h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc=
+google.golang.org/protobuf v1.30.0 
h1:kPPoIgf3TsEvrm0PFe15JQ+570QVxYzEvvHqChK+cng=
+google.golang.org/protobuf v1.30.0/go.mod 
h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I=
 gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod 
h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw=
 gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod 
h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
 gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod 
h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
+gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 
h1:YR8cESwS4TdDjEe65xsg0ogRM/Nc3DYOhEAlW+xobZo=
 gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod 
h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
-gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c 
h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
-gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod 
h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
-gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI=
 gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 
h1:uRGJdciOHaEIrze2W8Q3AKkepLTh2hOroT7a+7czfdQ=
 gopkg.in/yaml.v1 v1.0.0-20140924161607-9f9df34309c0 
h1:POO/ycCATvegFmVuPpQzZFJ+pGZeX22Ufu6fibxDVjU=
 gopkg.in/yaml.v1 v1.0.0-20140924161607-9f9df34309c0/go.mod 
h1:WDnlLJ4WF5VGsH/HVa3CI79GS0ol3YnhVnKP89i0kNg=
@@ -385,4 +410,5 @@ gopkg.in/yaml.v3 v3.0.1 
h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
 gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
 honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod 
h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
 honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod 
h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
+rsc.io/pdf v0.1.1/go.mod h1:n8OzWcQ6Sp37PL01nO98y4iUCRdTGarVfzxY20ICaU4=
 sigs.k8s.io/yaml v1.2.0/go.mod h1:yfXDCHCao9+ENCvLSE62v9VSji2MKu5jeNfTrofGhJc=
diff --git a/server/api/cluster.go b/server/api/cluster.go
index 1a07327..3d1e896 100644
--- a/server/api/cluster.go
+++ b/server/api/cluster.go
@@ -17,10 +17,12 @@
  * under the License.
  *
  */
+
 package api
 
 import (
        "errors"
+       "strings"
 
        "github.com/gin-gonic/gin"
 
@@ -36,8 +38,8 @@ type MigrateSlotRequest struct {
 }
 
 type CreateClusterRequest struct {
-       Name     string   `json:"name"`
-       Nodes    []string `json:"nodes"`
+       Name     string   `json:"name" validate:"required"`
+       Nodes    []string `json:"nodes" validate:"required"`
        Password string   `json:"password"`
        Replicas int      `json:"replicas"`
 }
@@ -49,7 +51,7 @@ type ClusterHandler struct {
 func (handler *ClusterHandler) List(c *gin.Context) {
        namespace := c.Param("namespace")
        clusters, err := handler.s.ListCluster(c, namespace)
-       if err != nil {
+       if err != nil && !errors.Is(err, consts.ErrNotFound) {
                helper.ResponseError(c, err)
                return
        }
@@ -69,13 +71,35 @@ func (handler *ClusterHandler) Create(c *gin.Context) {
                return
        }
 
+       clusterStore := handler.s
+       if err := clusterStore.CheckNewNodes(c, req.Nodes); err != nil {
+               helper.ResponseError(c, err)
+               return
+       }
+
        cluster, err := store.NewCluster(req.Name, req.Nodes, req.Replicas)
        if err != nil {
                helper.ResponseBadRequest(c, err)
                return
        }
        cluster.SetPassword(req.Password)
-       if err := handler.s.CreateCluster(c, namespace, cluster); err != nil {
+       checkClusterMode := 
strings.ToLower(c.GetHeader(consts.HeaderDontCheckClusterMode)) == "yes"
+       for _, node := range cluster.GetNodes() {
+               if !checkClusterMode {
+                       break
+               }
+               version, err := node.CheckClusterMode(c)
+               if err != nil {
+                       helper.ResponseError(c, err)
+                       return
+               }
+               if version != -1 {
+                       helper.ResponseBadRequest(c, errors.New("node is 
already in cluster mode"))
+                       return
+               }
+       }
+
+       if err := clusterStore.CreateCluster(c, namespace, cluster); err != nil 
{
                helper.ResponseError(c, err)
                return
        }
@@ -126,7 +150,7 @@ func (handler *ClusterHandler) Import(c *gin.Context) {
        namespace := c.Param("namespace")
        clusterName := c.Param("cluster")
        var req struct {
-               Nodes    []string `json:"nodes"`
+               Nodes    []string `json:"nodes" validate:"required"`
                Password string   `json:"password"`
        }
        if err := c.BindJSON(&req); err != nil {
@@ -151,6 +175,15 @@ func (handler *ClusterHandler) Import(c *gin.Context) {
        }
        cluster.SetPassword(req.Password)
 
+       newNodes := make([]string, 0)
+       for _, node := range cluster.GetNodes() {
+               newNodes = append(newNodes, node.Addr())
+       }
+       if err := handler.s.CheckNewNodes(c, newNodes); err != nil {
+               helper.ResponseError(c, err)
+               return
+       }
+
        cluster.Name = clusterName
        if err := handler.s.CreateCluster(c, namespace, cluster); err != nil {
                helper.ResponseError(c, err)
diff --git a/server/api/cluster_test.go b/server/api/cluster_test.go
index 784128b..005ba4b 100644
--- a/server/api/cluster_test.go
+++ b/server/api/cluster_test.go
@@ -52,6 +52,7 @@ func TestClusterBasics(t *testing.T) {
                body, err := json.Marshal(testCreateRequest)
                require.NoError(t, err)
 
+               ctx.Header(consts.HeaderDontCheckClusterMode, "yes")
                ctx.Request.Body = io.NopCloser(bytes.NewBuffer(body))
                ctx.Params = []gin.Param{{Key: "namespace", Value: ns}}
 
diff --git a/server/api/namespace.go b/server/api/namespace.go
index 505b979..0541885 100644
--- a/server/api/namespace.go
+++ b/server/api/namespace.go
@@ -17,6 +17,7 @@
  * under the License.
  *
  */
+
 package api
 
 import (
@@ -60,7 +61,7 @@ func (handler *NamespaceHandler) Exists(c *gin.Context) {
 
 func (handler *NamespaceHandler) Create(c *gin.Context) {
        var request struct {
-               Namespace string `json:"namespace"`
+               Namespace string `json:"namespace" validate:"required"`
        }
        if err := c.BindJSON(&request); err != nil {
                helper.ResponseBadRequest(c, err)
diff --git a/server/api/node.go b/server/api/node.go
index 47d89e3..878798b 100644
--- a/server/api/node.go
+++ b/server/api/node.go
@@ -17,6 +17,7 @@
  * under the License.
  *
  */
+
 package api
 
 import (
@@ -43,13 +44,16 @@ func (handler *NodeHandler) Create(c *gin.Context) {
        cluster, _ := c.MustGet(consts.ContextKeyCluster).(*store.Cluster)
        var req struct {
                Addr     string `json:"addr" binding:"required"`
-               Role     string `json:"role" binding:"required"`
+               Role     string `json:"role"`
                Password string `json:"password"`
        }
        if err := c.ShouldBindJSON(&req); err != nil {
                helper.ResponseBadRequest(c, err)
                return
        }
+       if req.Role == "" {
+               req.Role = store.RoleSlave
+       }
        shardIndex, _ := strconv.Atoi(c.Param("shard"))
        err := cluster.AddNode(shardIndex, req.Addr, req.Role, req.Password)
        if err != nil {
diff --git a/server/api/shard.go b/server/api/shard.go
index 9b36ced..9ccf258 100644
--- a/server/api/shard.go
+++ b/server/api/shard.go
@@ -56,7 +56,7 @@ func (handler *ShardHandler) Get(c *gin.Context) {
 func (handler *ShardHandler) Create(c *gin.Context) {
        ns := c.Param("namespace")
        var req struct {
-               Nodes    []string `json:"nodes"`
+               Nodes    []string `json:"nodes" validate:"required"`
                Password string   `json:"password"`
        }
        if err := c.BindJSON(&req); err != nil {
diff --git a/server/helper/helper.go b/server/helper/helper.go
index 22851e2..3f1eb4b 100644
--- a/server/helper/helper.go
+++ b/server/helper/helper.go
@@ -69,6 +69,8 @@ func ResponseError(c *gin.Context, err error) {
                code = http.StatusBadRequest
        } else if errors.Is(err, consts.ErrAlreadyExists) {
                code = http.StatusConflict
+       } else if errors.Is(err, consts.ErrForbidden) {
+               code = http.StatusForbidden
        } else if errors.Is(err, consts.ErrInvalidArgument) {
                code = http.StatusBadRequest
        }
diff --git a/server/middleware/middleware.go b/server/middleware/middleware.go
index 7955825..9ccfbe3 100644
--- a/server/middleware/middleware.go
+++ b/server/middleware/middleware.go
@@ -25,14 +25,13 @@ import (
        "strconv"
        "time"
 
-       "github.com/apache/kvrocks-controller/server/helper"
+       "github.com/gin-gonic/gin"
+       "github.com/prometheus/client_golang/prometheus"
 
        "github.com/apache/kvrocks-controller/consts"
        "github.com/apache/kvrocks-controller/metrics"
+       "github.com/apache/kvrocks-controller/server/helper"
        "github.com/apache/kvrocks-controller/store"
-
-       "github.com/gin-gonic/gin"
-       "github.com/prometheus/client_golang/prometheus"
 )
 
 func CollectMetrics(c *gin.Context) {
@@ -88,9 +87,10 @@ func RequiredNamespace(c *gin.Context) {
        }
        if !ok {
                helper.ResponseBadRequest(c, errors.New("namespace not found"))
-               return
+               c.Abort()
+       } else {
+               c.Next()
        }
-       c.Next()
 }
 
 func RequiredCluster(c *gin.Context) {
diff --git a/server/route.go b/server/route.go
index ba946a9..90d0455 100644
--- a/server/route.go
+++ b/server/route.go
@@ -23,6 +23,8 @@ import (
        "github.com/gin-gonic/gin"
        "github.com/prometheus/client_golang/prometheus/promhttp"
 
+       "github.com/apache/kvrocks-controller/server/helper"
+
        "github.com/apache/kvrocks-controller/consts"
        "github.com/apache/kvrocks-controller/server/api"
        "github.com/apache/kvrocks-controller/server/middleware"
@@ -38,6 +40,10 @@ func (srv *Server) initHandlers() {
 
        engine.Any("/debug/pprof/*profile", PProf)
        engine.GET("/metrics", gin.WrapH(promhttp.Handler()))
+       engine.NoRoute(func(c *gin.Context) {
+               helper.ResponseError(c, consts.ErrNotFound)
+               c.Abort()
+       })
 
        apiV1 := engine.Group("/api/v1/")
        {
@@ -53,9 +59,9 @@ func (srv *Server) initHandlers() {
                {
                        clusters.GET("", middleware.RequiredNamespace, 
handler.Cluster.List)
                        clusters.POST("", middleware.RequiredNamespace, 
handler.Cluster.Create)
+                       clusters.POST("/:cluster/import", 
middleware.RequiredNamespace, handler.Cluster.Import)
                        clusters.GET("/:cluster", middleware.RequiredCluster, 
handler.Cluster.Get)
-                       clusters.POST("/:cluster/import", 
handler.Cluster.Import)
-                       clusters.DELETE("/:cluster", handler.Cluster.Remove)
+                       clusters.DELETE("/:cluster", 
middleware.RequiredCluster, handler.Cluster.Remove)
                        clusters.POST("/:cluster/migrate", 
middleware.RequiredCluster, handler.Cluster.MigrateSlot)
                }
 
diff --git a/store/cluster.go b/store/cluster.go
index ec72586..fe8240e 100644
--- a/store/cluster.go
+++ b/store/cluster.go
@@ -143,6 +143,14 @@ func (cluster *Cluster) SyncToNodes(ctx context.Context) 
error {
        return nil
 }
 
+func (cluster *Cluster) GetNodes() []Node {
+       nodes := make([]Node, 0)
+       for i := 0; i < len(cluster.Shards); i++ {
+               nodes = append(nodes, cluster.Shards[i].Nodes...)
+       }
+       return nodes
+}
+
 func (cluster *Cluster) Reset(ctx context.Context) error {
        for i := 0; i < len(cluster.Shards); i++ {
                for _, node := range cluster.Shards[i].Nodes {
diff --git a/store/cluster_node.go b/store/cluster_node.go
index 5ee9520..997a089 100644
--- a/store/cluster_node.go
+++ b/store/cluster_node.go
@@ -64,6 +64,7 @@ type Node interface {
        GetClusterNodeInfo(ctx context.Context) (*ClusterNodeInfo, error)
        GetClusterInfo(ctx context.Context) (*ClusterInfo, error)
        SyncClusterInfo(ctx context.Context, cluster *Cluster) error
+       CheckClusterMode(ctx context.Context) (int64, error)
        MigrateSlot(ctx context.Context, slot int, NodeID string) error
 
        MarshalJSON() ([]byte, error)
@@ -167,7 +168,7 @@ func (n *ClusterNode) CheckClusterMode(ctx context.Context) 
(int64, error) {
                if strings.Contains(err.Error(), "cluster is not initialized") {
                        return -1, nil
                }
-               return -1, fmt.Errorf("error while checking node(%s) cluster 
mode: %w", n.addr, err)
+               return -1, fmt.Errorf("error while checking node cluster mode: 
%w", err)
        }
        return clusterInfo.CurrentEpoch, nil
 }
diff --git a/store/store.go b/store/store.go
index 84dea95..5afeeef 100644
--- a/store/store.go
+++ b/store/store.go
@@ -22,7 +22,7 @@ package store
 import (
        "context"
        "encoding/json"
-       "errors"
+       "fmt"
 
        "github.com/apache/kvrocks-controller/consts"
        "github.com/apache/kvrocks-controller/store/engine"
@@ -42,6 +42,8 @@ type Store interface {
        CreateCluster(ctx context.Context, ns string, cluster *Cluster) error
        UpdateCluster(ctx context.Context, ns string, cluster *Cluster) error
        SetCluster(ctx context.Context, ns string, clusterInfo *Cluster) error
+
+       CheckNewNodes(ctx context.Context, nodes []string) error
 }
 
 var _ Store = (*ClusterStore)(nil)
@@ -109,7 +111,7 @@ func (s *ClusterStore) RemoveNamespace(ctx context.Context, 
ns string) error {
                return err
        }
        if len(clusters) != 0 {
-               return errors.New("namespace wasn't empty, please remove 
clusters first")
+               return fmt.Errorf("%w: please delete clusters first", 
consts.ErrForbidden)
        }
        if err := s.e.Delete(ctx, appendPrefix(ns)); err != nil {
                return err
@@ -142,11 +144,11 @@ func (s *ClusterStore) existsCluster(ctx context.Context, 
ns, cluster string) (b
 func (s *ClusterStore) GetCluster(ctx context.Context, ns, cluster string) 
(*Cluster, error) {
        value, err := s.e.Get(ctx, buildClusterKey(ns, cluster))
        if err != nil {
-               return nil, err
+               return nil, fmt.Errorf("cluster: %w", err)
        }
        var clusterInfo Cluster
        if err = json.Unmarshal(value, &clusterInfo); err != nil {
-               return nil, err
+               return nil, fmt.Errorf("cluster: %w", err)
        }
        return &clusterInfo, nil
 }
@@ -155,7 +157,7 @@ func (s *ClusterStore) GetCluster(ctx context.Context, ns, 
cluster string) (*Clu
 func (s *ClusterStore) UpdateCluster(ctx context.Context, ns string, 
clusterInfo *Cluster) error {
        clusterInfo.Version.Inc()
        if err := s.SetCluster(ctx, ns, clusterInfo); err != nil {
-               return err
+               return fmt.Errorf("cluster: %w", err)
        }
        s.EmitEvent(EventPayload{
                Namespace: ns,
@@ -168,18 +170,18 @@ func (s *ClusterStore) UpdateCluster(ctx context.Context, 
ns string, clusterInfo
 
 func (s *ClusterStore) SetCluster(ctx context.Context, ns string, clusterInfo 
*Cluster) error {
        if len(clusterInfo.Shards) == 0 {
-               return errors.New("required at least one shard")
+               return fmt.Errorf("%w: required at least one shard", 
consts.ErrInvalidArgument)
        }
        value, err := json.Marshal(clusterInfo)
        if err != nil {
-               return err
+               return fmt.Errorf("cluster: %w", err)
        }
        return s.e.Set(ctx, buildClusterKey(ns, clusterInfo.Name), value)
 }
 
 func (s *ClusterStore) CreateCluster(ctx context.Context, ns string, 
clusterInfo *Cluster) error {
        if exists, _ := s.existsCluster(ctx, ns, clusterInfo.Name); exists {
-               return consts.ErrAlreadyExists
+               return fmt.Errorf("cluster: %w", consts.ErrAlreadyExists)
        }
        if err := s.SetCluster(ctx, ns, clusterInfo); err != nil {
                return err
@@ -209,6 +211,40 @@ func (s *ClusterStore) RemoveCluster(ctx context.Context, 
ns, cluster string) er
        return nil
 }
 
+func (s *ClusterStore) CheckNewNodes(ctx context.Context, nodes []string) 
error {
+       newNodes := make(map[string]bool, 0)
+       for _, node := range nodes {
+               newNodes[node] = true
+       }
+
+       namespaces, err := s.ListNamespace(ctx)
+       if err != nil {
+               return err
+       }
+       existingNodes := make([]string, 0)
+       for _, ns := range namespaces {
+               clusters, err := s.ListCluster(ctx, ns)
+               if err != nil {
+                       return err
+               }
+               for _, cluster := range clusters {
+                       c, err := s.GetCluster(ctx, ns, cluster)
+                       if err != nil {
+                               return err
+                       }
+                       for _, existingNode := range c.GetNodes() {
+                               if _, ok := newNodes[existingNode.Addr()]; ok {
+                                       existingNodes = append(existingNodes, 
existingNode.Addr())
+                               }
+                       }
+               }
+       }
+       if len(existingNodes) > 0 {
+               return fmt.Errorf("node: %w: %v", consts.ErrAlreadyExists, 
existingNodes)
+       }
+       return nil
+}
+
 func (s *ClusterStore) Notify() <-chan EventPayload {
        return s.eventNotifyCh
 }
diff --git a/store/store_test.go b/store/store_test.go
index 496a013..59b0fdc 100644
--- a/store/store_test.go
+++ b/store/store_test.go
@@ -94,4 +94,15 @@ func TestClusterStore(t *testing.T) {
                        require.ErrorIs(t, err, consts.ErrNotFound)
                }
        })
+
+       t.Run("check nodes", func(t *testing.T) {
+               testCluster, err := NewCluster("test-cluster-another",
+                       []string{"127.0.0.1:1111", "127.0.0.1:2222", 
"127.0.0.1:3333"}, 1)
+               require.NoError(t, err)
+
+               require.NoError(t, store.CreateCluster(ctx, "test-ns", 
testCluster))
+               require.NoError(t, store.CheckNewNodes(ctx, 
[]string{"127.0.0.1:4444", "127.0.0.1:5555"}))
+               require.NotNil(t, store.CheckNewNodes(ctx, 
[]string{"127.0.0.1:3333", "127.0.0.1:4444"}))
+               require.NotNil(t, store.CheckNewNodes(ctx, 
[]string{"127.0.0.1:2222", "127.0.0.1:3333"}))
+       })
 }


Reply via email to