Browse Source

client: a new API for obtaining a leader node information

Hitoshi Mitake 10 years ago
parent
commit
53be8405f3
2 changed files with 109 additions and 0 deletions
  1. 32 0
      client/members.go
  2. 77 0
      client/members_test.go

+ 32 - 0
client/members.go

@@ -29,6 +29,7 @@ import (
 
 
 var (
 var (
 	defaultV2MembersPrefix = "/v2/members"
 	defaultV2MembersPrefix = "/v2/members"
+	defaultLeaderSuffix    = "/leader"
 )
 )
 
 
 type Member struct {
 type Member struct {
@@ -105,6 +106,9 @@ type MembersAPI interface {
 
 
 	// Update instructs etcd to update an existing Member in the cluster.
 	// Update instructs etcd to update an existing Member in the cluster.
 	Update(ctx context.Context, mID string, peerURLs []string) error
 	Update(ctx context.Context, mID string, peerURLs []string) error
+
+	// Leader gets current leader of the cluster
+	Leader(ctx context.Context) (*Member, error)
 }
 }
 
 
 type httpMembersAPI struct {
 type httpMembersAPI struct {
@@ -199,6 +203,25 @@ func (m *httpMembersAPI) Remove(ctx context.Context, memberID string) error {
 	return assertStatusCode(resp.StatusCode, http.StatusNoContent, http.StatusGone)
 	return assertStatusCode(resp.StatusCode, http.StatusNoContent, http.StatusGone)
 }
 }
 
 
+func (m *httpMembersAPI) Leader(ctx context.Context) (*Member, error) {
+	req := &membersAPIActionLeader{}
+	resp, body, err := m.client.Do(ctx, req)
+	if err != nil {
+		return nil, err
+	}
+
+	if err := assertStatusCode(resp.StatusCode, http.StatusOK); err != nil {
+		return nil, err
+	}
+
+	var leader Member
+	if err := json.Unmarshal(body, &leader); err != nil {
+		return nil, err
+	}
+
+	return &leader, nil
+}
+
 type membersAPIActionList struct{}
 type membersAPIActionList struct{}
 
 
 func (l *membersAPIActionList) HTTPRequest(ep url.URL) *http.Request {
 func (l *membersAPIActionList) HTTPRequest(ep url.URL) *http.Request {
@@ -255,6 +278,15 @@ func assertStatusCode(got int, want ...int) (err error) {
 	return fmt.Errorf("unexpected status code %d", got)
 	return fmt.Errorf("unexpected status code %d", got)
 }
 }
 
 
+type membersAPIActionLeader struct{}
+
+func (l *membersAPIActionLeader) HTTPRequest(ep url.URL) *http.Request {
+	u := v2MembersURL(ep)
+	u.Path = path.Join(u.Path, defaultLeaderSuffix)
+	req, _ := http.NewRequest("GET", u.String(), nil)
+	return req
+}
+
 // v2MembersURL add the necessary path to the provided endpoint
 // v2MembersURL add the necessary path to the provided endpoint
 // to route requests to the default v2 members API.
 // to route requests to the default v2 members API.
 func v2MembersURL(ep url.URL) *url.URL {
 func v2MembersURL(ep url.URL) *url.URL {

+ 77 - 0
client/members_test.go

@@ -114,6 +114,23 @@ func TestMembersAPIActionRemove(t *testing.T) {
 	}
 	}
 }
 }
 
 
+func TestMembersAPIActionLeader(t *testing.T) {
+	ep := url.URL{Scheme: "http", Host: "example.com"}
+	act := &membersAPIActionLeader{}
+
+	wantURL := &url.URL{
+		Scheme: "http",
+		Host:   "example.com",
+		Path:   "/v2/members/leader",
+	}
+
+	got := *act.HTTPRequest(ep)
+	err := assertRequest(got, "GET", wantURL, http.Header{}, nil)
+	if err != nil {
+		t.Error(err.Error())
+	}
+}
+
 func TestAssertStatusCode(t *testing.T) {
 func TestAssertStatusCode(t *testing.T) {
 	if err := assertStatusCode(404, 400); err == nil {
 	if err := assertStatusCode(404, 400); err == nil {
 		t.Errorf("assertStatusCode failed to detect conflict in 400 vs 404")
 		t.Errorf("assertStatusCode failed to detect conflict in 400 vs 404")
@@ -520,3 +537,63 @@ func TestHTTPMembersAPIListError(t *testing.T) {
 		}
 		}
 	}
 	}
 }
 }
+
+func TestHTTPMembersAPILeaderSuccess(t *testing.T) {
+	wantAction := &membersAPIActionLeader{}
+	mAPI := &httpMembersAPI{
+		client: &actionAssertingHTTPClient{
+			t:   t,
+			act: wantAction,
+			resp: http.Response{
+				StatusCode: http.StatusOK,
+			},
+			body: []byte(`{"id":"94088180e21eb87b","name":"node2","peerURLs":["http://127.0.0.1:7002"],"clientURLs":["http://127.0.0.1:4002"]}`),
+		},
+	}
+
+	wantResponseMember := &Member{
+		ID:         "94088180e21eb87b",
+		Name:       "node2",
+		PeerURLs:   []string{"http://127.0.0.1:7002"},
+		ClientURLs: []string{"http://127.0.0.1:4002"},
+	}
+
+	m, err := mAPI.Leader(context.Background())
+	if err != nil {
+		t.Errorf("err = %v, want %v", err, nil)
+	}
+	if !reflect.DeepEqual(wantResponseMember, m) {
+		t.Errorf("incorrect member: member = %v, want %v", wantResponseMember, m)
+	}
+}
+
+func TestHTTPMembersAPILeaderError(t *testing.T) {
+	tests := []httpClient{
+		// generic httpClient failure
+		&staticHTTPClient{err: errors.New("fail!")},
+
+		// unrecognized HTTP status code
+		&staticHTTPClient{
+			resp: http.Response{StatusCode: http.StatusTeapot},
+		},
+
+		// fail to unmarshal body on StatusOK
+		&staticHTTPClient{
+			resp: http.Response{
+				StatusCode: http.StatusOK,
+			},
+			body: []byte(`[{"id":"XX`),
+		},
+	}
+
+	for i, tt := range tests {
+		mAPI := &httpMembersAPI{client: tt}
+		m, err := mAPI.Leader(context.Background())
+		if err == nil {
+			t.Errorf("#%d: err = nil, want not nil", i)
+		}
+		if m != nil {
+			t.Errorf("member slice = %v, want nil", m)
+		}
+	}
+}