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 ee8980d  Add support of managing the raft cluster via the client (#258)
ee8980d is described below

commit ee8980d13dd0690be578acaed7f90411c074d2e6
Author: hulk <[email protected]>
AuthorDate: Wed Jan 15 11:57:58 2025 +0800

    Add support of managing the raft cluster via the client (#258)
    
    ```
    ❯ _build/kvctl raft list peers
    
    |---------|-----------------------|-----------|
    | NODE ID |     NODE ADDRESS      | IS LEADER |
    |---------|-----------------------|-----------|
    |       1 | http://127.0.0.1:6001 | YES       |
    |---------|-----------------------|-----------|
    
    ❯ _build/kvctl raft add peer 2 http://127.0.0.1:6002
    Add node '2' with address 'http://127.0.0.1:6002' successfully
    
    ❯ _build/kvctl -H "http://127.0.0.1:9380"; raft remove peer 2
    Remove node '2' successfully
    
    ```
---
 cmd/client/command/raft.go | 175 +++++++++++++++++++++++++++++++++++++++++++++
 cmd/client/main.go         |   1 +
 server/api/raft.go         |   2 +-
 3 files changed, 177 insertions(+), 1 deletion(-)

diff --git a/cmd/client/command/raft.go b/cmd/client/command/raft.go
new file mode 100644
index 0000000..8067fec
--- /dev/null
+++ b/cmd/client/command/raft.go
@@ -0,0 +1,175 @@
+/*
+ * 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"
+       "net/url"
+       "os"
+       "strconv"
+       "strings"
+
+       "github.com/olekukonko/tablewriter"
+
+       "github.com/spf13/cobra"
+)
+
+const (
+       raftCommandList   = "list"
+       raftCommandAdd    = "add"
+       raftCommandRemove = "remove"
+)
+
+var RaftCommand = &cobra.Command{
+       Use:   "raft",
+       Short: "Raft operations",
+       Example: `
+# Display all memberships in the cluster
+kvctl raft list peers
+
+# Add a node to the cluster
+kvctl raft add peer <node_id> <node_address>
+
+# Remove a node from the cluster
+kvctl raft remove peer <node_id>
+`,
+       ValidArgs: []string{raftCommandList, raftCommandAdd, raftCommandRemove},
+       RunE: func(cmd *cobra.Command, args []string) error {
+               host, _ := cmd.Flags().GetString("host")
+               client := newClient(host)
+               switch strings.ToLower(args[0]) {
+               case raftCommandList:
+                       if len(args) < 2 || args[1] != "peers" {
+                               return fmt.Errorf("unsupported openeration: 
'%s' in raft command", args[1])
+                       }
+                       return listRaftPeers(client)
+               case raftCommandAdd, raftCommandRemove:
+                       if len(args) < 2 {
+                               return errors.New("missing 'peer' in raft 
command")
+                       }
+                       if args[1] != "peer" {
+                               return fmt.Errorf("unsupported openeration: 
'%s' in raft command", args[1])
+                       }
+                       if len(args) < 3 {
+                               return fmt.Errorf("missing node_id and 
node_address")
+                       }
+                       id, err := strconv.ParseUint(args[2], 10, 64)
+                       if err != nil {
+                               return fmt.Errorf("invalid node_id: %s", 
args[1])
+                       }
+                       if args[0] == raftCommandAdd {
+                               if len(args) < 4 {
+                                       return fmt.Errorf("missing 
node_address")
+                               }
+                               address := args[3]
+                               if _, err := url.Parse(address); err != nil {
+                                       return fmt.Errorf("invalid 
node_address: %s", address)
+                               }
+                               return addRaftPeer(client, id, address)
+                       } else {
+                               return removeRaftPeer(client, id)
+                       }
+               default:
+                       return fmt.Errorf("unsupported openeration: '%s' in 
raft command", args[0])
+               }
+       },
+       SilenceUsage:  true,
+       SilenceErrors: true,
+}
+
+func listRaftPeers(cli *client) error {
+       rsp, err := cli.restyCli.R().Get("/raft/peers")
+       if err != nil {
+               return err
+       }
+       if rsp.IsError() {
+               return unmarshalError(rsp.Body())
+       }
+
+       var result struct {
+               Leader uint64            `json:"leader"`
+               Peers  map[uint64]string `json:"peers"`
+       }
+       if err := unmarshalData(rsp.Body(), &result); err != nil {
+               return err
+       }
+       writer := tablewriter.NewWriter(os.Stdout)
+       printLine("")
+       writer.SetHeader([]string{"NODE_ID", "NODE_ADDRESS", "IS_LEADER"})
+       writer.SetCenterSeparator("|")
+       for id, addr := range result.Peers {
+               isLeader := "NO"
+               if id == result.Leader {
+                       isLeader = "YES"
+               }
+               columns := []string{fmt.Sprintf("%d", id), addr, isLeader}
+               writer.Append(columns)
+       }
+       writer.Render()
+       return nil
+}
+
+func addRaftPeer(cli *client, id uint64, address string) error {
+       var request struct {
+               ID        uint64 `json:"id"`
+               Peer      string `json:"peer"`
+               Operation string `json:"operation"`
+       }
+       request.ID = id
+       request.Peer = address
+       request.Operation = "add"
+
+       rsp, err := cli.restyCli.R().
+               SetBody(&request).
+               Post("/raft/peers")
+       if err != nil {
+               return err
+       }
+       if rsp.IsError() {
+               return unmarshalError(rsp.Body())
+       }
+
+       printLine("Add node '%d' with address '%s' successfully", id, address)
+       return nil
+}
+
+func removeRaftPeer(cli *client, id uint64) error {
+       var request struct {
+               ID        uint64 `json:"id"`
+               Operation string `json:"operation"`
+       }
+       request.ID = id
+       request.Operation = "remove"
+
+       rsp, err := cli.restyCli.R().
+               SetBody(&request).
+               Post("/raft/peers")
+       if err != nil {
+               return err
+       }
+       if rsp.IsError() {
+               return unmarshalError(rsp.Body())
+       }
+
+       printLine("Remove node '%d' successfully", id)
+       return nil
+}
diff --git a/cmd/client/main.go b/cmd/client/main.go
index bddb539..e0f51e8 100644
--- a/cmd/client/main.go
+++ b/cmd/client/main.go
@@ -57,6 +57,7 @@ func init() {
        rootCommand.AddCommand(command.ImportCommand)
        rootCommand.AddCommand(command.MigrateCommand)
        rootCommand.AddCommand(command.FailoverCommand)
+       rootCommand.AddCommand(command.RaftCommand)
 
        rootCommand.SilenceUsage = true
        rootCommand.SilenceErrors = true
diff --git a/server/api/raft.go b/server/api/raft.go
index fde1d58..7cb7370 100644
--- a/server/api/raft.go
+++ b/server/api/raft.go
@@ -44,7 +44,7 @@ type RaftHandler struct{}
 type MemberRequest struct {
        ID        uint64 `json:"id" validate:"required,gt=0"`
        Operation string `json:"operation" validate:"required"`
-       Peer      string `json:"peer"`
+       Peer      string `json:"peer,omitempty"`
 }
 
 func (r *MemberRequest) validate() error {

Reply via email to