diff --git a/etcdserver/etcdhttp/http.go b/etcdserver/etcdhttp/http.go index c63e5b550..6a91b393a 100644 --- a/etcdserver/etcdhttp/http.go +++ b/etcdserver/etcdhttp/http.go @@ -25,6 +25,7 @@ import ( "net/http" "net/url" "path" + "sort" "strconv" "strings" "time" @@ -149,13 +150,43 @@ func (h serverHandler) serveMachines(w http.ResponseWriter, r *http.Request) { } func (h serverHandler) serveAdminMembers(w http.ResponseWriter, r *http.Request) { - if !allowMethod(w, r.Method, "POST", "DELETE") { + if !allowMethod(w, r.Method, "GET", "POST", "DELETE") { return } ctx, cancel := context.WithTimeout(context.Background(), defaultServerTimeout) defer cancel() switch r.Method { + case "GET": + idStr := strings.TrimPrefix(r.URL.Path, adminMembersPrefix) + if idStr == "" { + msmap := h.clusterStore.Get().Members() + ms := make(SortableMemberSlice, 0, len(msmap)) + for _, m := range msmap { + ms = append(ms, m) + } + sort.Sort(ms) + w.Header().Set("Content-Type", "application/json") + if err := json.NewEncoder(w).Encode(ms); err != nil { + log.Printf("etcdhttp: %v", err) + } + return + } + id, err := strconv.ParseUint(idStr, 16, 64) + if err != nil { + http.Error(w, err.Error(), http.StatusBadRequest) + return + } + m := h.clusterStore.Get().FindID(id) + if m == nil { + http.Error(w, "member not found", http.StatusNotFound) + return + } + w.Header().Set("Content-Type", "application/json") + if err := json.NewEncoder(w).Encode(m); err != nil { + log.Printf("etcdhttp: %v", err) + } + return case "POST": ctype := r.Header.Get("Content-Type") if ctype != "application/json" { @@ -551,3 +582,9 @@ func trimNodeExternPrefix(n *store.NodeExtern, prefix string) *store.NodeExtern } return n } + +type SortableMemberSlice []*etcdserver.Member + +func (s SortableMemberSlice) Len() int { return len(s) } +func (s SortableMemberSlice) Less(i, j int) bool { return s[i].ID < s[j].ID } +func (s SortableMemberSlice) Swap(i, j int) { s[i], s[j] = s[j], s[i] } diff --git a/etcdserver/etcdhttp/http_test.go b/etcdserver/etcdhttp/http_test.go index 339e928a8..00c7f3a83 100644 --- a/etcdserver/etcdhttp/http_test.go +++ b/etcdserver/etcdhttp/http_test.go @@ -1485,6 +1485,16 @@ func TestServeAdminMembersFail(t *testing.T) { http.StatusInternalServerError, }, + { + // etcdserver.GetMember bad id + &http.Request{ + URL: mustNewURL(t, path.Join(adminMembersPrefix, "badid")), + Method: "GET", + }, + &errServer{}, + + http.StatusBadRequest, + }, } for i, tt := range tests { h := &serverHandler{ @@ -1527,6 +1537,61 @@ func (s *serverRecorder) RemoveMember(_ context.Context, id uint64) error { return nil } +func TestServeAdminMembersGet(t *testing.T) { + cluster := &fakeCluster{ + members: []etcdserver.Member{ + {ID: 1, Attributes: etcdserver.Attributes{ClientURLs: []string{"http://localhost:8080"}}}, + {ID: 2, Attributes: etcdserver.Attributes{ClientURLs: []string{"http://localhost:8081"}}}, + }, + } + h := &serverHandler{ + server: &serverRecorder{}, + clock: clockwork.NewFakeClock(), + clusterStore: cluster, + } + + msb, err := json.Marshal(cluster.members) + if err != nil { + t.Fatal(err) + } + wms := string(msb) + "\n" + mb, err := json.Marshal(cluster.members[0]) + if err != nil { + t.Fatal(err) + } + wm := string(mb) + "\n" + + tests := []struct { + path string + wcode int + wct string + wbody string + }{ + {adminMembersPrefix, http.StatusOK, "application/json", wms}, + {path.Join(adminMembersPrefix, "1"), http.StatusOK, "application/json", wm}, + {path.Join(adminMembersPrefix, "100"), http.StatusNotFound, "text/plain; charset=utf-8", "member not found\n"}, + } + + for i, tt := range tests { + req, err := http.NewRequest("GET", mustNewURL(t, tt.path).String(), nil) + if err != nil { + t.Fatal(err) + } + rw := httptest.NewRecorder() + h.serveAdminMembers(rw, req) + + if rw.Code != tt.wcode { + t.Errorf("#%d: code=%d, want %d", i, rw.Code, tt.wcode) + } + if gct := rw.Header().Get("Content-Type"); gct != tt.wct { + t.Errorf("#%d: content-type = %s, want %s", i, gct, tt.wct) + } + if rw.Body.String() != tt.wbody { + t.Errorf("#%d: body = %s, want %s", i, rw.Body.String(), tt.wbody) + } + } +} + func TestServeAdminMembersPut(t *testing.T) { u := mustNewURL(t, adminMembersPrefix) raftAttr := etcdserver.RaftAttributes{PeerURLs: []string{"http://127.0.0.1:1"}}