Browse Source

Merge pull request #1414 from bcwaldon/client-members-API

Add MembersAPI w/ List method
Brian Waldon 11 years ago
parent
commit
73215447c1
4 changed files with 339 additions and 23 deletions
  1. 0 15
      client/http.go
  2. 16 8
      client/keys.go
  3. 155 0
      client/members.go
  4. 168 0
      client/members_test.go

+ 0 - 15
client/http.go

@@ -53,21 +53,6 @@ type httpClient struct {
 	timeout   time.Duration
 }
 
-func newHTTPClient(tr *http.Transport, ep string, to time.Duration) (*httpClient, error) {
-	u, err := url.Parse(ep)
-	if err != nil {
-		return nil, err
-	}
-
-	c := &httpClient{
-		transport: tr,
-		endpoint:  *u,
-		timeout:   to,
-	}
-
-	return c, nil
-}
-
 func (c *httpClient) doWithTimeout(act httpAction) (*http.Response, []byte, error) {
 	ctx, cancel := context.WithTimeout(context.Background(), c.timeout)
 	defer cancel()

+ 16 - 8
client/keys.go

@@ -49,13 +49,21 @@ func NewDiscoveryKeysAPI(tr *http.Transport, ep string, to time.Duration) (KeysA
 	return newHTTPKeysAPIWithPrefix(tr, ep, to, "")
 }
 
-func newHTTPKeysAPIWithPrefix(tr *http.Transport, ep string, to time.Duration, prefix string) (*HTTPKeysAPI, error) {
-	c, err := newHTTPClient(tr, ep, to)
+func newHTTPKeysAPIWithPrefix(tr *http.Transport, ep string, to time.Duration, prefix string) (*httpKeysAPI, error) {
+	u, err := url.Parse(ep)
 	if err != nil {
 		return nil, err
 	}
 
-	kAPI := HTTPKeysAPI{
+	u.Path = path.Join(u.Path, prefix)
+
+	c := &httpClient{
+		transport: tr,
+		endpoint:  *u,
+		timeout:   to,
+	}
+
+	kAPI := httpKeysAPI{
 		client: c,
 	}
 
@@ -92,11 +100,11 @@ func (n *Node) String() string {
 	return fmt.Sprintf("{Key: %s, CreatedIndex: %d, ModifiedIndex: %d}", n.Key, n.CreatedIndex, n.ModifiedIndex)
 }
 
-type HTTPKeysAPI struct {
+type httpKeysAPI struct {
 	client *httpClient
 }
 
-func (k *HTTPKeysAPI) Create(key, val string, ttl time.Duration) (*Response, error) {
+func (k *httpKeysAPI) Create(key, val string, ttl time.Duration) (*Response, error) {
 	create := &createAction{
 		Key:   key,
 		Value: val,
@@ -114,7 +122,7 @@ func (k *HTTPKeysAPI) Create(key, val string, ttl time.Duration) (*Response, err
 	return unmarshalHTTPResponse(httpresp.StatusCode, body)
 }
 
-func (k *HTTPKeysAPI) Get(key string) (*Response, error) {
+func (k *httpKeysAPI) Get(key string) (*Response, error) {
 	get := &getAction{
 		Key:       key,
 		Recursive: false,
@@ -128,7 +136,7 @@ func (k *HTTPKeysAPI) Get(key string) (*Response, error) {
 	return unmarshalHTTPResponse(httpresp.StatusCode, body)
 }
 
-func (k *HTTPKeysAPI) Watch(key string, idx uint64) Watcher {
+func (k *httpKeysAPI) Watch(key string, idx uint64) Watcher {
 	return &httpWatcher{
 		client: k.client,
 		nextWait: waitAction{
@@ -139,7 +147,7 @@ func (k *HTTPKeysAPI) Watch(key string, idx uint64) Watcher {
 	}
 }
 
-func (k *HTTPKeysAPI) RecursiveWatch(key string, idx uint64) Watcher {
+func (k *httpKeysAPI) RecursiveWatch(key string, idx uint64) Watcher {
 	return &httpWatcher{
 		client: k.client,
 		nextWait: waitAction{

+ 155 - 0
client/members.go

@@ -0,0 +1,155 @@
+/*
+   Copyright 2014 CoreOS, Inc.
+
+   Licensed 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 client
+
+import (
+	"encoding/json"
+	"fmt"
+	"net/http"
+	"net/url"
+	"path"
+	"time"
+)
+
+var (
+	DefaultV2MembersPrefix = "/v2/admin/members"
+)
+
+func NewMembersAPI(tr *http.Transport, ep string, to time.Duration) (MembersAPI, error) {
+	u, err := url.Parse(ep)
+	if err != nil {
+		return nil, err
+	}
+
+	u.Path = path.Join(u.Path, DefaultV2MembersPrefix)
+
+	c := &httpClient{
+		transport: tr,
+		endpoint:  *u,
+		timeout:   to,
+	}
+
+	mAPI := httpMembersAPI{
+		client: c,
+	}
+
+	return &mAPI, nil
+}
+
+type MembersAPI interface {
+	List() ([]Member, error)
+}
+
+type Member struct {
+	ID         uint64
+	Name       string
+	PeerURLs   []url.URL
+	ClientURLs []url.URL
+}
+
+func (m *Member) UnmarshalJSON(data []byte) (err error) {
+	rm := struct {
+		ID         uint64
+		Name       string
+		PeerURLs   []string
+		ClientURLs []string
+	}{}
+
+	if err := json.Unmarshal(data, &rm); err != nil {
+		return err
+	}
+
+	parseURLs := func(strs []string) ([]url.URL, error) {
+		urls := make([]url.URL, len(strs))
+		for i, s := range strs {
+			u, err := url.Parse(s)
+			if err != nil {
+				return nil, err
+			}
+			urls[i] = *u
+		}
+
+		return urls, nil
+	}
+
+	if m.PeerURLs, err = parseURLs(rm.PeerURLs); err != nil {
+		return err
+	}
+
+	if m.ClientURLs, err = parseURLs(rm.ClientURLs); err != nil {
+		return err
+	}
+
+	m.ID = rm.ID
+	m.Name = rm.Name
+
+	return nil
+}
+
+type membersCollection struct {
+	Members []Member
+}
+
+type httpMembersAPI struct {
+	client *httpClient
+}
+
+func (m *httpMembersAPI) List() ([]Member, error) {
+	httpresp, body, err := m.client.doWithTimeout(&membersAPIActionList{})
+	if err != nil {
+		return nil, err
+	}
+
+	mResponse := httpMembersAPIResponse{
+		code: httpresp.StatusCode,
+		body: body,
+	}
+
+	if err = mResponse.err(); err != nil {
+		return nil, err
+	}
+
+	var mCollection membersCollection
+	if err = mResponse.unmarshalBody(&mCollection); err != nil {
+		return nil, err
+	}
+
+	return mCollection.Members, nil
+}
+
+type httpMembersAPIResponse struct {
+	code int
+	body []byte
+}
+
+func (r *httpMembersAPIResponse) err() (err error) {
+	if r.code != http.StatusOK {
+		err = fmt.Errorf("unrecognized status code %d", r.code)
+	}
+	return
+}
+
+func (r *httpMembersAPIResponse) unmarshalBody(dst interface{}) (err error) {
+	return json.Unmarshal(r.body, dst)
+}
+
+type membersAPIActionList struct{}
+
+func (l *membersAPIActionList) httpRequest(ep url.URL) *http.Request {
+	req, _ := http.NewRequest("GET", ep.String(), nil)
+	return req
+}

+ 168 - 0
client/members_test.go

@@ -0,0 +1,168 @@
+/*
+   Copyright 2014 CoreOS, Inc.
+
+   Licensed 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 client
+
+import (
+	"encoding/json"
+	"net/http"
+	"net/url"
+	"reflect"
+	"testing"
+)
+
+func TestMembersAPIListAction(t *testing.T) {
+	ep := url.URL{Scheme: "http", Host: "example.com/v2/admin/members"}
+	wantURL := &url.URL{
+		Scheme: "http",
+		Host:   "example.com",
+		Path:   "/v2/admin/members",
+	}
+
+	act := &membersAPIActionList{}
+	got := *act.httpRequest(ep)
+	err := assertResponse(got, wantURL, http.Header{}, nil)
+	if err != nil {
+		t.Errorf(err.Error())
+	}
+}
+
+func TestMembersAPIUnmarshalMember(t *testing.T) {
+	tests := []struct {
+		body       []byte
+		wantMember Member
+		wantError  bool
+	}{
+		// no URLs, just check ID & Name
+		{
+			body:       []byte(`{"id": 1, "name": "dungarees"}`),
+			wantMember: Member{ID: 1, Name: "dungarees", PeerURLs: []url.URL{}, ClientURLs: []url.URL{}},
+		},
+
+		// both client and peer URLs
+		{
+			body: []byte(`{"peerURLs": ["http://127.0.0.1:4001"], "clientURLs": ["http://127.0.0.1:4001"]}`),
+			wantMember: Member{
+				PeerURLs: []url.URL{
+					{Scheme: "http", Host: "127.0.0.1:4001"},
+				},
+				ClientURLs: []url.URL{
+					{Scheme: "http", Host: "127.0.0.1:4001"},
+				},
+			},
+		},
+
+		// multiple peer URLs
+		{
+			body: []byte(`{"peerURLs": ["http://127.0.0.1:4001", "https://example.com"]}`),
+			wantMember: Member{
+				PeerURLs: []url.URL{
+					{Scheme: "http", Host: "127.0.0.1:4001"},
+					{Scheme: "https", Host: "example.com"},
+				},
+				ClientURLs: []url.URL{},
+			},
+		},
+
+		// multiple client URLs
+		{
+			body: []byte(`{"clientURLs": ["http://127.0.0.1:4001", "https://example.com"]}`),
+			wantMember: Member{
+				PeerURLs: []url.URL{},
+				ClientURLs: []url.URL{
+					{Scheme: "http", Host: "127.0.0.1:4001"},
+					{Scheme: "https", Host: "example.com"},
+				},
+			},
+		},
+
+		// invalid JSON
+		{
+			body:      []byte(`{"peerU`),
+			wantError: true,
+		},
+
+		// valid JSON, invalid URL
+		{
+			body:      []byte(`{"peerURLs": [":"]}`),
+			wantError: true,
+		},
+	}
+
+	for i, tt := range tests {
+		got := Member{}
+		err := json.Unmarshal(tt.body, &got)
+		if tt.wantError != (err != nil) {
+			t.Errorf("#%d: want error %t, got %v", i, tt.wantError, err)
+			continue
+		}
+
+		if !reflect.DeepEqual(tt.wantMember, got) {
+			t.Errorf("#%d: incorrect output: want=%#v, got=%#v", i, tt.wantMember, got)
+		}
+	}
+}
+
+func TestMembersAPIUnmarshalMembers(t *testing.T) {
+	body := []byte(`{"members":[{"id":176869799018424574,"peerURLs":["http://127.0.0.1:7003"],"name":"node3","clientURLs":["http://127.0.0.1:4003"]},{"id":297577273835923749,"peerURLs":["http://127.0.0.1:2380","http://127.0.0.1:7001"],"name":"node1","clientURLs":["http://127.0.0.1:2379","http://127.0.0.1:4001"]},{"id":10666918107976480891,"peerURLs":["http://127.0.0.1:7002"],"name":"node2","clientURLs":["http://127.0.0.1:4002"]}]}`)
+
+	want := membersCollection{
+		Members: []Member{
+			{
+				ID:   176869799018424574,
+				Name: "node3",
+				PeerURLs: []url.URL{
+					{Scheme: "http", Host: "127.0.0.1:7003"},
+				},
+				ClientURLs: []url.URL{
+					{Scheme: "http", Host: "127.0.0.1:4003"},
+				},
+			},
+			{
+				ID:   297577273835923749,
+				Name: "node1",
+				PeerURLs: []url.URL{
+					{Scheme: "http", Host: "127.0.0.1:2380"},
+					{Scheme: "http", Host: "127.0.0.1:7001"},
+				},
+				ClientURLs: []url.URL{
+					{Scheme: "http", Host: "127.0.0.1:2379"},
+					{Scheme: "http", Host: "127.0.0.1:4001"},
+				},
+			},
+			{
+				ID:   10666918107976480891,
+				Name: "node2",
+				PeerURLs: []url.URL{
+					{Scheme: "http", Host: "127.0.0.1:7002"},
+				},
+				ClientURLs: []url.URL{
+					{Scheme: "http", Host: "127.0.0.1:4002"},
+				},
+			},
+		},
+	}
+
+	got := membersCollection{}
+	err := json.Unmarshal(body, &got)
+	if err != nil {
+		t.Fatalf("Unexpected error: %v", err)
+	}
+
+	if !reflect.DeepEqual(want, got) {
+		t.Errorf("Incorrect output: want=%#v, got=%#v", want, got)
+	}
+}