diff --git a/etcdserver/etcdhttp/client.go b/etcdserver/etcdhttp/client.go index 833221b03..ce66e044a 100644 --- a/etcdserver/etcdhttp/client.go +++ b/etcdserver/etcdhttp/client.go @@ -159,14 +159,26 @@ func (h *membersHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) { switch r.Method { case "GET": - if trimPrefix(r.URL.Path, membersPrefix) != "" { + switch trimPrefix(r.URL.Path, membersPrefix) { + case "": + mc := newMemberCollection(h.clusterInfo.Members()) + w.Header().Set("Content-Type", "application/json") + if err := json.NewEncoder(w).Encode(mc); err != nil { + log.Printf("etcdhttp: %v", err) + } + case "leader": + id := h.server.Leader() + if id == 0 { + writeError(w, httptypes.NewHTTPError(http.StatusServiceUnavailable, "During election")) + return + } + m := newMember(h.clusterInfo.Member(id)) + w.Header().Set("Content-Type", "application/json") + if err := json.NewEncoder(w).Encode(m); err != nil { + log.Printf("etcdhttp: %v", err) + } + default: writeError(w, httptypes.NewHTTPError(http.StatusNotFound, "Not found")) - return - } - mc := newMemberCollection(h.clusterInfo.Members()) - w.Header().Set("Content-Type", "application/json") - if err := json.NewEncoder(w).Encode(mc); err != nil { - log.Printf("etcdhttp: %v", err) } case "POST": req := httptypes.MemberCreateRequest{} diff --git a/etcdserver/etcdhttp/client_test.go b/etcdserver/etcdhttp/client_test.go index 5178dc059..f516e4c51 100644 --- a/etcdserver/etcdhttp/client_test.go +++ b/etcdserver/etcdhttp/client_test.go @@ -92,9 +92,10 @@ type serverRecorder struct { actions []action } -func (s *serverRecorder) Start() {} -func (s *serverRecorder) Stop() {} -func (s *serverRecorder) ID() types.ID { return types.ID(1) } +func (s *serverRecorder) Start() {} +func (s *serverRecorder) Stop() {} +func (s *serverRecorder) Leader() types.ID { return types.ID(1) } +func (s *serverRecorder) ID() types.ID { return types.ID(1) } func (s *serverRecorder) Do(_ context.Context, r etcdserverpb.Request) (etcdserver.Response, error) { s.actions = append(s.actions, action{name: "Do", params: []interface{}{r}}) return etcdserver.Response{}, nil @@ -139,9 +140,10 @@ type resServer struct { res etcdserver.Response } -func (rs *resServer) Start() {} -func (rs *resServer) Stop() {} -func (rs *resServer) ID() types.ID { return types.ID(1) } +func (rs *resServer) Start() {} +func (rs *resServer) Stop() {} +func (rs *resServer) ID() types.ID { return types.ID(1) } +func (rs *resServer) Leader() types.ID { return types.ID(1) } func (rs *resServer) Do(_ context.Context, _ etcdserverpb.Request) (etcdserver.Response, error) { return rs.res, nil } @@ -604,6 +606,57 @@ func TestServeMembers(t *testing.T) { } } +// TODO: consolidate **ALL** fake server implementations and add no leader test case. +func TestServeLeader(t *testing.T) { + memb1 := etcdserver.Member{ID: 1, Attributes: etcdserver.Attributes{ClientURLs: []string{"http://localhost:8080"}}} + memb2 := etcdserver.Member{ID: 2, Attributes: etcdserver.Attributes{ClientURLs: []string{"http://localhost:8081"}}} + cluster := &fakeCluster{ + id: 1, + members: map[uint64]*etcdserver.Member{1: &memb1, 2: &memb2}, + } + h := &membersHandler{ + server: &serverRecorder{}, + clock: clockwork.NewFakeClock(), + clusterInfo: cluster, + } + + wmc := string(`{"id":"1","name":"","peerURLs":[],"clientURLs":["http://localhost:8080"]}`) + + tests := []struct { + path string + wcode int + wct string + wbody string + }{ + {membersPrefix + "leader", http.StatusOK, "application/json", wmc + "\n"}, + // TODO: add no leader case + } + + 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.ServeHTTP(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) + } + gcid := rw.Header().Get("X-Etcd-Cluster-ID") + wcid := cluster.ID().String() + if gcid != wcid { + t.Errorf("#%d: cid = %s, want %s", i, gcid, wcid) + } + if rw.Body.String() != tt.wbody { + t.Errorf("#%d: body = %q, want %q", i, rw.Body.String(), tt.wbody) + } + } +} + func TestServeMembersCreate(t *testing.T) { u := mustNewURL(t, membersPrefix) b := []byte(`{"peerURLs":["http://127.0.0.1:1"]}`) diff --git a/etcdserver/etcdhttp/http_test.go b/etcdserver/etcdhttp/http_test.go index 6d73b51f6..842dd82ce 100644 --- a/etcdserver/etcdhttp/http_test.go +++ b/etcdserver/etcdhttp/http_test.go @@ -65,9 +65,10 @@ type errServer struct { err error } -func (fs *errServer) Start() {} -func (fs *errServer) Stop() {} -func (fs *errServer) ID() types.ID { return types.ID(1) } +func (fs *errServer) Start() {} +func (fs *errServer) Stop() {} +func (fs *errServer) ID() types.ID { return types.ID(1) } +func (fs *errServer) Leader() types.ID { return types.ID(1) } func (fs *errServer) Do(ctx context.Context, r etcdserverpb.Request) (etcdserver.Response, error) { return etcdserver.Response{}, fs.err } diff --git a/etcdserver/server.go b/etcdserver/server.go index 57abc0326..9f1876c78 100644 --- a/etcdserver/server.go +++ b/etcdserver/server.go @@ -93,6 +93,8 @@ type Server interface { Stop() // ID returns the ID of the Server. ID() types.ID + // Leader returns the ID of the leader Server. + Leader() types.ID // Do takes a request and attempts to fulfill it, returning a Response. Do(ctx context.Context, r pb.Request) (Response, error) // Process takes a raft message and applies it to the server's raft state @@ -579,6 +581,8 @@ func (s *EtcdServer) Term() uint64 { return atomic.LoadUint64(&s.raftTerm) } // Index, Term, Lead, Committed, Applied, LastIndex, etc. func (s *EtcdServer) Lead() uint64 { return atomic.LoadUint64(&s.raftLead) } +func (s *EtcdServer) Leader() types.ID { return types.ID(s.Lead()) } + // configure sends a configuration change through consensus and // then waits for it to be applied to the server. It // will block until the change is performed or there is an error.