From cba19e348f817053757e3d9de8ee97fa95a85fb8 Mon Sep 17 00:00:00 2001 From: Brian Waldon Date: Sat, 25 Oct 2014 09:49:35 -0700 Subject: [PATCH] client: MembersAPI.List --- client/members.go | 155 +++++++++++++++++++++++++++++++++++++ client/members_test.go | 168 +++++++++++++++++++++++++++++++++++++++++ 2 files changed, 323 insertions(+) create mode 100644 client/members.go create mode 100644 client/members_test.go diff --git a/client/members.go b/client/members.go new file mode 100644 index 000000000..6b0e840e1 --- /dev/null +++ b/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 +} diff --git a/client/members_test.go b/client/members_test.go new file mode 100644 index 000000000..acd7d9e44 --- /dev/null +++ b/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) + } +}