diff --git a/cmd/kubeadm/app/util/etcd/etcd.go b/cmd/kubeadm/app/util/etcd/etcd.go index 50784fda107..d037d263a30 100644 --- a/cmd/kubeadm/app/util/etcd/etcd.go +++ b/cmd/kubeadm/app/util/etcd/etcd.go @@ -104,6 +104,8 @@ type Client struct { Endpoints []string newEtcdClient func(endpoints []string) (etcdClient, error) + + listMembersFunc func(backoff *wait.Backoff) (*clientv3.MemberListResponse, error) } // New creates a new EtcdCluster client @@ -135,6 +137,8 @@ func New(endpoints []string, ca, cert, key string) (*Client, error) { }) } + client.listMembersFunc = client.listMembers + return &client, nil } @@ -274,11 +278,14 @@ type Member struct { PeerURL string } -func (c *Client) listMembers() (*clientv3.MemberListResponse, error) { +func (c *Client) listMembers(backoff *wait.Backoff) (*clientv3.MemberListResponse, error) { // Gets the member list var lastError error var resp *clientv3.MemberListResponse - err := wait.ExponentialBackoff(etcdBackoff, func() (bool, error) { + if backoff == nil { + backoff = &etcdBackoff + } + err := wait.ExponentialBackoff(*backoff, func() (bool, error) { cli, err := c.newEtcdClient(c.Endpoints) if err != nil { lastError = err @@ -304,7 +311,7 @@ func (c *Client) listMembers() (*clientv3.MemberListResponse, error) { // GetMemberID returns the member ID of the given peer URL func (c *Client) GetMemberID(peerURL string) (uint64, error) { - resp, err := c.listMembers() + resp, err := c.listMembersFunc(nil) if err != nil { return 0, err } @@ -319,7 +326,7 @@ func (c *Client) GetMemberID(peerURL string) (uint64, error) { // ListMembers returns the member list. func (c *Client) ListMembers() ([]Member, error) { - resp, err := c.listMembers() + resp, err := c.listMembersFunc(nil) if err != nil { return nil, err } @@ -480,7 +487,7 @@ func (c *Client) addMember(name string, peerAddrs string, isLearner bool) ([]Mem // isLearner returns true if the given member ID is a learner. func (c *Client) isLearner(memberID uint64) (bool, error) { - resp, err := c.listMembers() + resp, err := c.listMembersFunc(nil) if err != nil { return false, err } diff --git a/cmd/kubeadm/app/util/etcd/etcd_test.go b/cmd/kubeadm/app/util/etcd/etcd_test.go index 4ebff47ea86..b2b561c4192 100644 --- a/cmd/kubeadm/app/util/etcd/etcd_test.go +++ b/cmd/kubeadm/app/util/etcd/etcd_test.go @@ -482,6 +482,11 @@ func TestClient_GetMemberID(t *testing.T) { Endpoints: tt.fields.Endpoints, newEtcdClient: tt.fields.newEtcdClient, } + c.listMembersFunc = func(backoff *wait.Backoff) (*clientv3.MemberListResponse, error) { + f, _ := c.newEtcdClient([]string{}) + resp, _ := f.MemberList(context.TODO()) + return resp, nil + } got, err := c.GetMemberID(tt.args.peerURL) if !errors.Is(tt.wantErr, err) { @@ -494,3 +499,292 @@ func TestClient_GetMemberID(t *testing.T) { }) } } + +func TestListMembers(t *testing.T) { + type fields struct { + Endpoints []string + newEtcdClient func(endpoints []string) (etcdClient, error) + listMembersFunc func(backoff *wait.Backoff) (*clientv3.MemberListResponse, error) + } + tests := []struct { + name string + fields fields + want []Member + wantError bool + }{ + { + name: "PeerURLs are empty", + fields: fields{ + Endpoints: []string{}, + newEtcdClient: func(endpoints []string) (etcdClient, error) { + f := &fakeEtcdClient{} + return f, nil + }, + }, + want: []Member{}, + }, + { + name: "PeerURLs are non-empty", + fields: fields{ + Endpoints: []string{}, + newEtcdClient: func(endpoints []string) (etcdClient, error) { + f := &fakeEtcdClient{ + members: []*pb.Member{ + { + ID: 1, + Name: "member1", + PeerURLs: []string{ + "https://member1:2380", + }, + }, + { + ID: 2, + Name: "member2", + PeerURLs: []string{ + "https://member2:2380", + }, + }, + }, + } + return f, nil + }, + }, + want: []Member{ + { + Name: "member1", + PeerURL: "https://member1:2380", + }, + { + Name: "member2", + PeerURL: "https://member2:2380", + }, + }, + }, + { + name: "PeerURLs has multiple urls", + fields: fields{ + Endpoints: []string{}, + newEtcdClient: func(endpoints []string) (etcdClient, error) { + f := &fakeEtcdClient{ + members: []*pb.Member{ + { + ID: 1, + Name: "member1", + PeerURLs: []string{ + "https://member1:2380", + "https://member2:2380", + }, + }, + }, + } + return f, nil + }, + }, + want: []Member{ + { + Name: "member1", + PeerURL: "https://member1:2380", + }, + }, + }, + { + name: "ListMembers return error", + fields: fields{ + Endpoints: []string{}, + newEtcdClient: func(endpoints []string) (etcdClient, error) { + f := &fakeEtcdClient{ + members: []*pb.Member{ + { + ID: 1, + Name: "member1", + PeerURLs: []string{ + "https://member1:2380", + "https://member2:2380", + }, + }, + }, + } + return f, nil + }, + listMembersFunc: func(backoff *wait.Backoff) (*clientv3.MemberListResponse, error) { + return nil, errNotImplemented + }, + }, + want: nil, + wantError: true, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + c := &Client{ + Endpoints: tt.fields.Endpoints, + newEtcdClient: tt.fields.newEtcdClient, + listMembersFunc: tt.fields.listMembersFunc, + } + if c.listMembersFunc == nil { + c.listMembersFunc = func(backoff *wait.Backoff) (*clientv3.MemberListResponse, error) { + return c.listMembers(&wait.Backoff{Steps: 1}) + } + } + got, err := c.ListMembers() + if !reflect.DeepEqual(got, tt.want) { + t.Errorf("ListMembers() = %v, want %v", got, tt.want) + } + if (err != nil) != (tt.wantError) { + t.Errorf("ListMembers() error = %v, wantError %v", err, tt.wantError) + } + }) + } +} + +func TestIsLearner(t *testing.T) { + type fields struct { + Endpoints []string + newEtcdClient func(endpoints []string) (etcdClient, error) + listMembersFunc func(backoff *wait.Backoff) (*clientv3.MemberListResponse, error) + } + tests := []struct { + name string + fields fields + memberID uint64 + want bool + wantError bool + }{ + { + name: "The specified member is not a learner", + fields: fields{ + Endpoints: []string{}, + newEtcdClient: func(endpoints []string) (etcdClient, error) { + f := &fakeEtcdClient{ + members: []*pb.Member{ + { + ID: 1, + Name: "member1", + PeerURLs: []string{ + "https://member1:2380", + }, + IsLearner: false, + }, + }, + } + return f, nil + }, + }, + memberID: 1, + want: false, + }, + { + name: "The specified member is a learner", + fields: fields{ + Endpoints: []string{}, + newEtcdClient: func(endpoints []string) (etcdClient, error) { + f := &fakeEtcdClient{ + members: []*pb.Member{ + { + ID: 1, + Name: "member1", + PeerURLs: []string{ + "https://member1:2380", + }, + IsLearner: true, + }, + { + ID: 2, + Name: "member2", + PeerURLs: []string{ + "https://member2:2380", + }, + }, + }, + } + return f, nil + }, + }, + memberID: 1, + want: true, + }, + { + name: "The specified member does not exist", + fields: fields{ + Endpoints: []string{}, + newEtcdClient: func(endpoints []string) (etcdClient, error) { + f := &fakeEtcdClient{ + members: []*pb.Member{}, + } + return f, nil + }, + }, + memberID: 3, + want: false, + }, + { + name: "Learner ID is empty", + fields: fields{ + Endpoints: []string{}, + newEtcdClient: func(endpoints []string) (etcdClient, error) { + f := &fakeEtcdClient{ + members: []*pb.Member{ + { + Name: "member2", + PeerURLs: []string{ + "https://member2:2380", + }, + IsLearner: true, + }, + }, + } + return f, nil + }, + }, + want: true, + }, + { + name: "ListMembers returns an error", + fields: fields{ + Endpoints: []string{}, + newEtcdClient: func(endpoints []string) (etcdClient, error) { + f := &fakeEtcdClient{ + members: []*pb.Member{ + { + Name: "member2", + PeerURLs: []string{ + "https://member2:2380", + }, + IsLearner: true, + }, + }, + } + return f, nil + }, + listMembersFunc: func(backoff *wait.Backoff) (*clientv3.MemberListResponse, error) { + return nil, errNotImplemented + }, + }, + want: false, + wantError: true, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + c := &Client{ + Endpoints: tt.fields.Endpoints, + newEtcdClient: tt.fields.newEtcdClient, + listMembersFunc: tt.fields.listMembersFunc, + } + if c.listMembersFunc == nil { + c.listMembersFunc = func(backoff *wait.Backoff) (*clientv3.MemberListResponse, error) { + f, _ := c.newEtcdClient([]string{}) + resp, _ := f.MemberList(context.TODO()) + return resp, nil + } + } + got, err := c.isLearner(tt.memberID) + if got != tt.want { + t.Errorf("isLearner() = %v, want %v", got, tt.want) + } + if (err != nil) != (tt.wantError) { + t.Errorf("isLearner() error = %v, wantError %v", err, tt.wantError) + } + }) + } +}