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