diff --git a/clientv3/cluster.go b/clientv3/cluster.go index bb2b7b23088..596d7d0e4eb 100644 --- a/clientv3/cluster.go +++ b/clientv3/cluster.go @@ -38,7 +38,7 @@ type Cluster interface { MemberList(ctx context.Context) (*MemberListResponse, error) // MemberAdd adds a new member into the cluster. - MemberAdd(ctx context.Context, peerAddrs []string) (*MemberAddResponse, error) + MemberAdd(ctx context.Context, peerAddrs []string, isLearner bool) (*MemberAddResponse, error) // MemberRemove removes an existing member from the cluster. MemberRemove(ctx context.Context, id uint64) (*MemberRemoveResponse, error) @@ -71,13 +71,16 @@ func NewClusterFromClusterClient(remote pb.ClusterClient, c *Client) Cluster { return api } -func (c *cluster) MemberAdd(ctx context.Context, peerAddrs []string) (*MemberAddResponse, error) { +func (c *cluster) MemberAdd(ctx context.Context, peerAddrs []string, isLearner bool) (*MemberAddResponse, error) { // fail-fast before panic in rafthttp if _, err := types.NewURLs(peerAddrs); err != nil { return nil, err } - r := &pb.MemberAddRequest{PeerURLs: peerAddrs} + r := &pb.MemberAddRequest{ + PeerURLs: peerAddrs, + IsLearner: isLearner, + } resp, err := c.remote.MemberAdd(ctx, r, c.callOpts...) if err != nil { return nil, toErr(ctx, err) diff --git a/clientv3/example_cluster_test.go b/clientv3/example_cluster_test.go index 279ea64ac69..f29a40f8b0d 100644 --- a/clientv3/example_cluster_test.go +++ b/clientv3/example_cluster_test.go @@ -51,7 +51,7 @@ func ExampleCluster_memberAdd() { defer cli.Close() peerURLs := endpoints[2:] - mresp, err := cli.MemberAdd(context.Background(), peerURLs) + mresp, err := cli.MemberAdd(context.Background(), peerURLs, false) if err != nil { log.Fatal(err) } diff --git a/clientv3/integration/cluster_test.go b/clientv3/integration/cluster_test.go index ce629488bb7..622087cbfe2 100644 --- a/clientv3/integration/cluster_test.go +++ b/clientv3/integration/cluster_test.go @@ -16,6 +16,7 @@ package integration import ( "context" + "fmt" "reflect" "testing" @@ -51,7 +52,7 @@ func TestMemberAdd(t *testing.T) { capi := clus.RandClient() urls := []string{"http://127.0.0.1:1234"} - resp, err := capi.MemberAdd(context.Background(), urls) + resp, err := capi.MemberAdd(context.Background(), urls, false) if err != nil { t.Fatalf("failed to add member %v", err) } @@ -149,7 +150,7 @@ func TestMemberAddUpdateWrongURLs(t *testing.T) { {"localhost:1234"}, } for i := range tt { - _, err := capi.MemberAdd(context.Background(), tt[i]) + _, err := capi.MemberAdd(context.Background(), tt[i], false) if err == nil { t.Errorf("#%d: MemberAdd err = nil, but error", i) } @@ -159,3 +160,47 @@ func TestMemberAddUpdateWrongURLs(t *testing.T) { } } } + +func TestMemberAddForLearner(t *testing.T) { + defer testutil.AfterTest(t) + + clus := integration.NewClusterV3(t, &integration.ClusterConfig{Size: 3}) + defer clus.Terminate(t) + + capi := clus.RandClient() + + urls := []string{"http://127.0.0.1:1234"} + isLearner := true + resp, err := capi.MemberAdd(context.Background(), urls, isLearner) + if err != nil { + t.Fatalf("failed to add member %v", err) + } + + if resp.Member.IsLearner != isLearner { + t.Errorf("Added a member with IsLearner = %v, got %v", isLearner, resp.Member.IsLearner) + } + + numOfLearners, err := getNumberOfLearners(clus) + if err != nil { + t.Fatalf("failed to get the number of learners in cluster: %v", err) + } + if numOfLearners != 1 { + t.Errorf("Added 1 learner node to cluster, got %d", numOfLearners) + } +} + +// getNumberOfLearners return the number of learner nodes in cluster using MemberList API +func getNumberOfLearners(clus *integration.ClusterV3) (int, error) { + cli := clus.RandClient() + resp, err := cli.MemberList(context.Background()) + if err != nil { + return 0, fmt.Errorf("failed to list member %v", err) + } + numberOfLearners := 0 + for _, m := range resp.Members { + if m.IsLearner { + numberOfLearners++ + } + } + return numberOfLearners, nil +} diff --git a/clientv3/snapshot/member_test.go b/clientv3/snapshot/member_test.go index a42066a5637..f7db982e31b 100644 --- a/clientv3/snapshot/member_test.go +++ b/clientv3/snapshot/member_test.go @@ -55,7 +55,7 @@ func TestSnapshotV3RestoreMultiMemberAdd(t *testing.T) { urls := newEmbedURLs(2) newCURLs, newPURLs := urls[:1], urls[1:] - if _, err = cli.MemberAdd(context.Background(), []string{newPURLs[0].String()}); err != nil { + if _, err = cli.MemberAdd(context.Background(), []string{newPURLs[0].String()}, false); err != nil { t.Fatal(err) } diff --git a/etcdctl/ctlv3/command/member_command.go b/etcdctl/ctlv3/command/member_command.go index 5b2119aba50..cbe2be4ff3f 100644 --- a/etcdctl/ctlv3/command/member_command.go +++ b/etcdctl/ctlv3/command/member_command.go @@ -23,7 +23,10 @@ import ( "github.com/spf13/cobra" ) -var memberPeerURLs string +var ( + memberPeerURLs string + isLearner bool +) // NewMemberCommand returns the cobra command for "member". func NewMemberCommand() *cobra.Command { @@ -50,6 +53,7 @@ func NewMemberAddCommand() *cobra.Command { } cc.Flags().StringVar(&memberPeerURLs, "peer-urls", "", "comma separated peer URLs for the new member.") + cc.Flags().BoolVar(&isLearner, "learner", false, "indicates if the new member is raft learner") return cc } @@ -118,7 +122,7 @@ func memberAddCommandFunc(cmd *cobra.Command, args []string) { urls := strings.Split(memberPeerURLs, ",") ctx, cancel := commandCtx(cmd) cli := mustClientFromCmd(cmd) - resp, err := cli.MemberAdd(ctx, urls) + resp, err := cli.MemberAdd(ctx, urls, isLearner) cancel() if err != nil { ExitWithError(ExitError, err) diff --git a/etcdserver/api/v2v3/server.go b/etcdserver/api/v2v3/server.go index fd5ad0744a8..d99f6c9b43f 100644 --- a/etcdserver/api/v2v3/server.go +++ b/etcdserver/api/v2v3/server.go @@ -63,7 +63,7 @@ func (s *v2v3Server) Leader() types.ID { } func (s *v2v3Server) AddMember(ctx context.Context, memb membership.Member) ([]*membership.Member, error) { - resp, err := s.c.MemberAdd(ctx, memb.PeerURLs) + resp, err := s.c.MemberAdd(ctx, memb.PeerURLs, memb.IsLearner) if err != nil { return nil, err } @@ -92,7 +92,8 @@ func v3MembersToMembership(v3membs []*pb.Member) []*membership.Member { membs[i] = &membership.Member{ ID: types.ID(m.ID), RaftAttributes: membership.RaftAttributes{ - PeerURLs: m.PeerURLs, + PeerURLs: m.PeerURLs, + IsLearner: m.IsLearner, }, Attributes: membership.Attributes{ Name: m.Name, diff --git a/proxy/grpcproxy/cluster.go b/proxy/grpcproxy/cluster.go index aa2a8fe4dd1..bce02c76ee0 100644 --- a/proxy/grpcproxy/cluster.go +++ b/proxy/grpcproxy/cluster.go @@ -109,7 +109,7 @@ func (cp *clusterProxy) monitor(wa gnaming.Watcher) { } func (cp *clusterProxy) MemberAdd(ctx context.Context, r *pb.MemberAddRequest) (*pb.MemberAddResponse, error) { - mresp, err := cp.clus.MemberAdd(ctx, r.PeerURLs) + mresp, err := cp.clus.MemberAdd(ctx, r.PeerURLs, r.IsLearner) if err != nil { return nil, err }