1// Copyright 2015 The etcd Authors
2//
3// Licensed under the Apache License, Version 2.0 (the "License");
4// you may not use this file except in compliance with the License.
5// You may obtain a copy of the License at
6//
7//     http://www.apache.org/licenses/LICENSE-2.0
8//
9// Unless required by applicable law or agreed to in writing, software
10// distributed under the License is distributed on an "AS IS" BASIS,
11// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12// See the License for the specific language governing permissions and
13// limitations under the License.
14
15package client
16
17import (
18	"bytes"
19	"context"
20	"encoding/json"
21	"fmt"
22	"net/http"
23	"net/url"
24	"path"
25
26	"go.etcd.io/etcd/pkg/types"
27)
28
29var (
30	defaultV2MembersPrefix = "/v2/members"
31	defaultLeaderSuffix    = "/leader"
32)
33
34type Member struct {
35	// ID is the unique identifier of this Member.
36	ID string `json:"id"`
37
38	// Name is a human-readable, non-unique identifier of this Member.
39	Name string `json:"name"`
40
41	// PeerURLs represents the HTTP(S) endpoints this Member uses to
42	// participate in etcd's consensus protocol.
43	PeerURLs []string `json:"peerURLs"`
44
45	// ClientURLs represents the HTTP(S) endpoints on which this Member
46	// serves its client-facing APIs.
47	ClientURLs []string `json:"clientURLs"`
48}
49
50type memberCollection []Member
51
52func (c *memberCollection) UnmarshalJSON(data []byte) error {
53	d := struct {
54		Members []Member
55	}{}
56
57	if err := json.Unmarshal(data, &d); err != nil {
58		return err
59	}
60
61	if d.Members == nil {
62		*c = make([]Member, 0)
63		return nil
64	}
65
66	*c = d.Members
67	return nil
68}
69
70type memberCreateOrUpdateRequest struct {
71	PeerURLs types.URLs
72}
73
74func (m *memberCreateOrUpdateRequest) MarshalJSON() ([]byte, error) {
75	s := struct {
76		PeerURLs []string `json:"peerURLs"`
77	}{
78		PeerURLs: make([]string, len(m.PeerURLs)),
79	}
80
81	for i, u := range m.PeerURLs {
82		s.PeerURLs[i] = u.String()
83	}
84
85	return json.Marshal(&s)
86}
87
88// NewMembersAPI constructs a new MembersAPI that uses HTTP to
89// interact with etcd's membership API.
90func NewMembersAPI(c Client) MembersAPI {
91	return &httpMembersAPI{
92		client: c,
93	}
94}
95
96type MembersAPI interface {
97	// List enumerates the current cluster membership.
98	List(ctx context.Context) ([]Member, error)
99
100	// Add instructs etcd to accept a new Member into the cluster.
101	Add(ctx context.Context, peerURL string) (*Member, error)
102
103	// Remove demotes an existing Member out of the cluster.
104	Remove(ctx context.Context, mID string) error
105
106	// Update instructs etcd to update an existing Member in the cluster.
107	Update(ctx context.Context, mID string, peerURLs []string) error
108
109	// Leader gets current leader of the cluster
110	Leader(ctx context.Context) (*Member, error)
111}
112
113type httpMembersAPI struct {
114	client httpClient
115}
116
117func (m *httpMembersAPI) List(ctx context.Context) ([]Member, error) {
118	req := &membersAPIActionList{}
119	resp, body, err := m.client.Do(ctx, req)
120	if err != nil {
121		return nil, err
122	}
123
124	if err := assertStatusCode(resp.StatusCode, http.StatusOK); err != nil {
125		return nil, err
126	}
127
128	var mCollection memberCollection
129	if err := json.Unmarshal(body, &mCollection); err != nil {
130		return nil, err
131	}
132
133	return []Member(mCollection), nil
134}
135
136func (m *httpMembersAPI) Add(ctx context.Context, peerURL string) (*Member, error) {
137	urls, err := types.NewURLs([]string{peerURL})
138	if err != nil {
139		return nil, err
140	}
141
142	req := &membersAPIActionAdd{peerURLs: urls}
143	resp, body, err := m.client.Do(ctx, req)
144	if err != nil {
145		return nil, err
146	}
147
148	if err := assertStatusCode(resp.StatusCode, http.StatusCreated, http.StatusConflict); err != nil {
149		return nil, err
150	}
151
152	if resp.StatusCode != http.StatusCreated {
153		var merr membersError
154		if err := json.Unmarshal(body, &merr); err != nil {
155			return nil, err
156		}
157		return nil, merr
158	}
159
160	var memb Member
161	if err := json.Unmarshal(body, &memb); err != nil {
162		return nil, err
163	}
164
165	return &memb, nil
166}
167
168func (m *httpMembersAPI) Update(ctx context.Context, memberID string, peerURLs []string) error {
169	urls, err := types.NewURLs(peerURLs)
170	if err != nil {
171		return err
172	}
173
174	req := &membersAPIActionUpdate{peerURLs: urls, memberID: memberID}
175	resp, body, err := m.client.Do(ctx, req)
176	if err != nil {
177		return err
178	}
179
180	if err := assertStatusCode(resp.StatusCode, http.StatusNoContent, http.StatusNotFound, http.StatusConflict); err != nil {
181		return err
182	}
183
184	if resp.StatusCode != http.StatusNoContent {
185		var merr membersError
186		if err := json.Unmarshal(body, &merr); err != nil {
187			return err
188		}
189		return merr
190	}
191
192	return nil
193}
194
195func (m *httpMembersAPI) Remove(ctx context.Context, memberID string) error {
196	req := &membersAPIActionRemove{memberID: memberID}
197	resp, _, err := m.client.Do(ctx, req)
198	if err != nil {
199		return err
200	}
201
202	return assertStatusCode(resp.StatusCode, http.StatusNoContent, http.StatusGone)
203}
204
205func (m *httpMembersAPI) Leader(ctx context.Context) (*Member, error) {
206	req := &membersAPIActionLeader{}
207	resp, body, err := m.client.Do(ctx, req)
208	if err != nil {
209		return nil, err
210	}
211
212	if err := assertStatusCode(resp.StatusCode, http.StatusOK); err != nil {
213		return nil, err
214	}
215
216	var leader Member
217	if err := json.Unmarshal(body, &leader); err != nil {
218		return nil, err
219	}
220
221	return &leader, nil
222}
223
224type membersAPIActionList struct{}
225
226func (l *membersAPIActionList) HTTPRequest(ep url.URL) *http.Request {
227	u := v2MembersURL(ep)
228	req, _ := http.NewRequest("GET", u.String(), nil)
229	return req
230}
231
232type membersAPIActionRemove struct {
233	memberID string
234}
235
236func (d *membersAPIActionRemove) HTTPRequest(ep url.URL) *http.Request {
237	u := v2MembersURL(ep)
238	u.Path = path.Join(u.Path, d.memberID)
239	req, _ := http.NewRequest("DELETE", u.String(), nil)
240	return req
241}
242
243type membersAPIActionAdd struct {
244	peerURLs types.URLs
245}
246
247func (a *membersAPIActionAdd) HTTPRequest(ep url.URL) *http.Request {
248	u := v2MembersURL(ep)
249	m := memberCreateOrUpdateRequest{PeerURLs: a.peerURLs}
250	b, _ := json.Marshal(&m)
251	req, _ := http.NewRequest("POST", u.String(), bytes.NewReader(b))
252	req.Header.Set("Content-Type", "application/json")
253	return req
254}
255
256type membersAPIActionUpdate struct {
257	memberID string
258	peerURLs types.URLs
259}
260
261func (a *membersAPIActionUpdate) HTTPRequest(ep url.URL) *http.Request {
262	u := v2MembersURL(ep)
263	m := memberCreateOrUpdateRequest{PeerURLs: a.peerURLs}
264	u.Path = path.Join(u.Path, a.memberID)
265	b, _ := json.Marshal(&m)
266	req, _ := http.NewRequest("PUT", u.String(), bytes.NewReader(b))
267	req.Header.Set("Content-Type", "application/json")
268	return req
269}
270
271func assertStatusCode(got int, want ...int) (err error) {
272	for _, w := range want {
273		if w == got {
274			return nil
275		}
276	}
277	return fmt.Errorf("unexpected status code %d", got)
278}
279
280type membersAPIActionLeader struct{}
281
282func (l *membersAPIActionLeader) HTTPRequest(ep url.URL) *http.Request {
283	u := v2MembersURL(ep)
284	u.Path = path.Join(u.Path, defaultLeaderSuffix)
285	req, _ := http.NewRequest("GET", u.String(), nil)
286	return req
287}
288
289// v2MembersURL add the necessary path to the provided endpoint
290// to route requests to the default v2 members API.
291func v2MembersURL(ep url.URL) *url.URL {
292	ep.Path = path.Join(ep.Path, defaultV2MembersPrefix)
293	return &ep
294}
295
296type membersError struct {
297	Message string `json:"message"`
298	Code    int    `json:"-"`
299}
300
301func (e membersError) Error() string {
302	return e.Message
303}
304