client.go 18 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668
  1. /*
  2. Copyright 2014 CoreOS, Inc.
  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. http://www.apache.org/licenses/LICENSE-2.0
  7. Unless required by applicable law or agreed to in writing, software
  8. distributed under the License is distributed on an "AS IS" BASIS,
  9. WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  10. See the License for the specific language governing permissions and
  11. limitations under the License.
  12. */
  13. package etcdhttp
  14. import (
  15. "encoding/json"
  16. "errors"
  17. "expvar"
  18. "fmt"
  19. "io/ioutil"
  20. "log"
  21. "net/http"
  22. "net/url"
  23. "path"
  24. "strconv"
  25. "strings"
  26. "time"
  27. "github.com/coreos/etcd/Godeps/_workspace/src/github.com/jonboulle/clockwork"
  28. "github.com/coreos/etcd/Godeps/_workspace/src/golang.org/x/net/context"
  29. etcdErr "github.com/coreos/etcd/error"
  30. "github.com/coreos/etcd/etcdserver"
  31. "github.com/coreos/etcd/etcdserver/etcdhttp/httptypes"
  32. "github.com/coreos/etcd/etcdserver/etcdserverpb"
  33. "github.com/coreos/etcd/etcdserver/stats"
  34. "github.com/coreos/etcd/pkg/metrics"
  35. "github.com/coreos/etcd/pkg/types"
  36. "github.com/coreos/etcd/raft"
  37. "github.com/coreos/etcd/store"
  38. "github.com/coreos/etcd/version"
  39. )
  40. const (
  41. keysPrefix = "/v2/keys"
  42. deprecatedMachinesPrefix = "/v2/machines"
  43. membersPrefix = "/v2/members"
  44. statsPrefix = "/v2/stats"
  45. statsPath = "/stats"
  46. healthPath = "/health"
  47. versionPath = "/version"
  48. )
  49. // NewClientHandler generates a muxed http.Handler with the given parameters to serve etcd client requests.
  50. func NewClientHandler(server *etcdserver.EtcdServer) http.Handler {
  51. kh := &keysHandler{
  52. server: server,
  53. clusterInfo: server.Cluster,
  54. timer: server,
  55. timeout: defaultServerTimeout,
  56. }
  57. sh := &statsHandler{
  58. stats: server,
  59. }
  60. mh := &membersHandler{
  61. server: server,
  62. clusterInfo: server.Cluster,
  63. clock: clockwork.NewRealClock(),
  64. }
  65. dmh := &deprecatedMachinesHandler{
  66. clusterInfo: server.Cluster,
  67. }
  68. mux := http.NewServeMux()
  69. mux.HandleFunc("/", http.NotFound)
  70. mux.Handle(healthPath, healthHandler(server))
  71. mux.HandleFunc(versionPath, serveVersion)
  72. mux.Handle(keysPrefix, kh)
  73. mux.Handle(keysPrefix+"/", kh)
  74. mux.HandleFunc(statsPrefix+"/store", sh.serveStore)
  75. mux.HandleFunc(statsPrefix+"/self", sh.serveSelf)
  76. mux.HandleFunc(statsPrefix+"/leader", sh.serveLeader)
  77. mux.HandleFunc(statsPath, serveStats)
  78. mux.Handle(membersPrefix, mh)
  79. mux.Handle(membersPrefix+"/", mh)
  80. mux.Handle(deprecatedMachinesPrefix, dmh)
  81. return mux
  82. }
  83. type keysHandler struct {
  84. server etcdserver.Server
  85. clusterInfo etcdserver.ClusterInfo
  86. timer etcdserver.RaftTimer
  87. timeout time.Duration
  88. }
  89. func (h *keysHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
  90. if !allowMethod(w, r.Method, "HEAD", "GET", "PUT", "POST", "DELETE") {
  91. return
  92. }
  93. w.Header().Set("X-Etcd-Cluster-ID", h.clusterInfo.ID().String())
  94. ctx, cancel := context.WithTimeout(context.Background(), h.timeout)
  95. defer cancel()
  96. rr, err := parseKeyRequest(r, clockwork.NewRealClock())
  97. if err != nil {
  98. writeError(w, err)
  99. return
  100. }
  101. resp, err := h.server.Do(ctx, rr)
  102. if err != nil {
  103. err = trimErrorPrefix(err, etcdserver.StoreKeysPrefix)
  104. writeError(w, err)
  105. return
  106. }
  107. switch {
  108. case resp.Event != nil:
  109. if err := writeKeyEvent(w, resp.Event, h.timer); err != nil {
  110. // Should never be reached
  111. log.Printf("error writing event: %v", err)
  112. }
  113. case resp.Watcher != nil:
  114. ctx, cancel := context.WithTimeout(context.Background(), defaultWatchTimeout)
  115. defer cancel()
  116. handleKeyWatch(ctx, w, resp.Watcher, rr.Stream, h.timer)
  117. default:
  118. writeError(w, errors.New("received response with no Event/Watcher!"))
  119. }
  120. }
  121. type deprecatedMachinesHandler struct {
  122. clusterInfo etcdserver.ClusterInfo
  123. }
  124. func (h *deprecatedMachinesHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
  125. if !allowMethod(w, r.Method, "GET", "HEAD") {
  126. return
  127. }
  128. endpoints := h.clusterInfo.ClientURLs()
  129. w.Write([]byte(strings.Join(endpoints, ", ")))
  130. }
  131. type membersHandler struct {
  132. server etcdserver.Server
  133. clusterInfo etcdserver.ClusterInfo
  134. clock clockwork.Clock
  135. }
  136. func (h *membersHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
  137. if !allowMethod(w, r.Method, "GET", "POST", "DELETE", "PUT") {
  138. return
  139. }
  140. w.Header().Set("X-Etcd-Cluster-ID", h.clusterInfo.ID().String())
  141. ctx, cancel := context.WithTimeout(context.Background(), defaultServerTimeout)
  142. defer cancel()
  143. switch r.Method {
  144. case "GET":
  145. switch trimPrefix(r.URL.Path, membersPrefix) {
  146. case "":
  147. mc := newMemberCollection(h.clusterInfo.Members())
  148. w.Header().Set("Content-Type", "application/json")
  149. if err := json.NewEncoder(w).Encode(mc); err != nil {
  150. log.Printf("etcdhttp: %v", err)
  151. }
  152. case "leader":
  153. id := h.server.Leader()
  154. if id == 0 {
  155. writeError(w, httptypes.NewHTTPError(http.StatusServiceUnavailable, "During election"))
  156. return
  157. }
  158. m := newMember(h.clusterInfo.Member(id))
  159. w.Header().Set("Content-Type", "application/json")
  160. if err := json.NewEncoder(w).Encode(m); err != nil {
  161. log.Printf("etcdhttp: %v", err)
  162. }
  163. default:
  164. writeError(w, httptypes.NewHTTPError(http.StatusNotFound, "Not found"))
  165. }
  166. case "POST":
  167. req := httptypes.MemberCreateRequest{}
  168. if ok := unmarshalRequest(r, &req, w); !ok {
  169. return
  170. }
  171. now := h.clock.Now()
  172. m := etcdserver.NewMember("", req.PeerURLs, "", &now)
  173. err := h.server.AddMember(ctx, *m)
  174. switch {
  175. case err == etcdserver.ErrIDExists || err == etcdserver.ErrPeerURLexists:
  176. writeError(w, httptypes.NewHTTPError(http.StatusConflict, err.Error()))
  177. return
  178. case err != nil:
  179. log.Printf("etcdhttp: error adding node %s: %v", m.ID, err)
  180. writeError(w, err)
  181. return
  182. }
  183. res := newMember(m)
  184. w.Header().Set("Content-Type", "application/json")
  185. w.WriteHeader(http.StatusCreated)
  186. if err := json.NewEncoder(w).Encode(res); err != nil {
  187. log.Printf("etcdhttp: %v", err)
  188. }
  189. case "DELETE":
  190. id, ok := getID(r.URL.Path, w)
  191. if !ok {
  192. return
  193. }
  194. err := h.server.RemoveMember(ctx, uint64(id))
  195. switch {
  196. case err == etcdserver.ErrIDRemoved:
  197. writeError(w, httptypes.NewHTTPError(http.StatusGone, fmt.Sprintf("Member permanently removed: %s", id)))
  198. case err == etcdserver.ErrIDNotFound:
  199. writeError(w, httptypes.NewHTTPError(http.StatusNotFound, fmt.Sprintf("No such member: %s", id)))
  200. case err != nil:
  201. log.Printf("etcdhttp: error removing node %s: %v", id, err)
  202. writeError(w, err)
  203. default:
  204. w.WriteHeader(http.StatusNoContent)
  205. }
  206. case "PUT":
  207. id, ok := getID(r.URL.Path, w)
  208. if !ok {
  209. return
  210. }
  211. req := httptypes.MemberUpdateRequest{}
  212. if ok := unmarshalRequest(r, &req, w); !ok {
  213. return
  214. }
  215. m := etcdserver.Member{
  216. ID: id,
  217. RaftAttributes: etcdserver.RaftAttributes{PeerURLs: req.PeerURLs.StringSlice()},
  218. }
  219. err := h.server.UpdateMember(ctx, m)
  220. switch {
  221. case err == etcdserver.ErrPeerURLexists:
  222. writeError(w, httptypes.NewHTTPError(http.StatusConflict, err.Error()))
  223. case err == etcdserver.ErrIDNotFound:
  224. writeError(w, httptypes.NewHTTPError(http.StatusNotFound, fmt.Sprintf("No such member: %s", id)))
  225. case err != nil:
  226. log.Printf("etcdhttp: error updating node %s: %v", m.ID, err)
  227. writeError(w, err)
  228. default:
  229. w.WriteHeader(http.StatusNoContent)
  230. }
  231. }
  232. }
  233. type statsHandler struct {
  234. stats stats.Stats
  235. }
  236. func (h *statsHandler) serveStore(w http.ResponseWriter, r *http.Request) {
  237. if !allowMethod(w, r.Method, "GET") {
  238. return
  239. }
  240. w.Header().Set("Content-Type", "application/json")
  241. w.Write(h.stats.StoreStats())
  242. }
  243. func (h *statsHandler) serveSelf(w http.ResponseWriter, r *http.Request) {
  244. if !allowMethod(w, r.Method, "GET") {
  245. return
  246. }
  247. w.Header().Set("Content-Type", "application/json")
  248. w.Write(h.stats.SelfStats())
  249. }
  250. func (h *statsHandler) serveLeader(w http.ResponseWriter, r *http.Request) {
  251. if !allowMethod(w, r.Method, "GET") {
  252. return
  253. }
  254. stats := h.stats.LeaderStats()
  255. if stats == nil {
  256. writeError(w, httptypes.NewHTTPError(http.StatusForbidden, "not current leader"))
  257. return
  258. }
  259. w.Header().Set("Content-Type", "application/json")
  260. w.Write(stats)
  261. }
  262. func serveStats(w http.ResponseWriter, r *http.Request) {
  263. w.Header().Set("Content-Type", "application/json; charset=utf-8")
  264. // TODO: getting one key or a prefix of keys based on path
  265. fmt.Fprintf(w, "{\n")
  266. first := true
  267. metrics.Do(func(kv expvar.KeyValue) {
  268. if !first {
  269. fmt.Fprintf(w, ",\n")
  270. }
  271. first = false
  272. fmt.Fprintf(w, "%q: %s", kv.Key, kv.Value)
  273. })
  274. fmt.Fprintf(w, "\n}\n")
  275. }
  276. // TODO: change etcdserver to raft interface when we have it.
  277. // add test for healthHeadler when we have the interface ready.
  278. func healthHandler(server *etcdserver.EtcdServer) http.HandlerFunc {
  279. return func(w http.ResponseWriter, r *http.Request) {
  280. if !allowMethod(w, r.Method, "GET") {
  281. return
  282. }
  283. if uint64(server.Leader()) == raft.None {
  284. http.Error(w, `{"health": "false"}`, http.StatusServiceUnavailable)
  285. return
  286. }
  287. // wait for raft's progress
  288. index := server.Index()
  289. for i := 0; i < 3; i++ {
  290. time.Sleep(250 * time.Millisecond)
  291. if server.Index() > index {
  292. w.WriteHeader(http.StatusOK)
  293. w.Write([]byte(`{"health": "true"}`))
  294. return
  295. }
  296. }
  297. http.Error(w, `{"health": "false"}`, http.StatusServiceUnavailable)
  298. return
  299. }
  300. }
  301. func serveVersion(w http.ResponseWriter, r *http.Request) {
  302. if !allowMethod(w, r.Method, "GET") {
  303. return
  304. }
  305. fmt.Fprintf(w, `{"releaseVersion":"%s","internalVersion":"%s"}`, version.Version, version.InternalVersion)
  306. }
  307. // parseKeyRequest converts a received http.Request on keysPrefix to
  308. // a server Request, performing validation of supplied fields as appropriate.
  309. // If any validation fails, an empty Request and non-nil error is returned.
  310. func parseKeyRequest(r *http.Request, clock clockwork.Clock) (etcdserverpb.Request, error) {
  311. emptyReq := etcdserverpb.Request{}
  312. err := r.ParseForm()
  313. if err != nil {
  314. return emptyReq, etcdErr.NewRequestError(
  315. etcdErr.EcodeInvalidForm,
  316. err.Error(),
  317. )
  318. }
  319. if !strings.HasPrefix(r.URL.Path, keysPrefix) {
  320. return emptyReq, etcdErr.NewRequestError(
  321. etcdErr.EcodeInvalidForm,
  322. "incorrect key prefix",
  323. )
  324. }
  325. p := path.Join(etcdserver.StoreKeysPrefix, r.URL.Path[len(keysPrefix):])
  326. var pIdx, wIdx uint64
  327. if pIdx, err = getUint64(r.Form, "prevIndex"); err != nil {
  328. return emptyReq, etcdErr.NewRequestError(
  329. etcdErr.EcodeIndexNaN,
  330. `invalid value for "prevIndex"`,
  331. )
  332. }
  333. if wIdx, err = getUint64(r.Form, "waitIndex"); err != nil {
  334. return emptyReq, etcdErr.NewRequestError(
  335. etcdErr.EcodeIndexNaN,
  336. `invalid value for "waitIndex"`,
  337. )
  338. }
  339. var rec, sort, wait, dir, quorum, stream bool
  340. if rec, err = getBool(r.Form, "recursive"); err != nil {
  341. return emptyReq, etcdErr.NewRequestError(
  342. etcdErr.EcodeInvalidField,
  343. `invalid value for "recursive"`,
  344. )
  345. }
  346. if sort, err = getBool(r.Form, "sorted"); err != nil {
  347. return emptyReq, etcdErr.NewRequestError(
  348. etcdErr.EcodeInvalidField,
  349. `invalid value for "sorted"`,
  350. )
  351. }
  352. if wait, err = getBool(r.Form, "wait"); err != nil {
  353. return emptyReq, etcdErr.NewRequestError(
  354. etcdErr.EcodeInvalidField,
  355. `invalid value for "wait"`,
  356. )
  357. }
  358. // TODO(jonboulle): define what parameters dir is/isn't compatible with?
  359. if dir, err = getBool(r.Form, "dir"); err != nil {
  360. return emptyReq, etcdErr.NewRequestError(
  361. etcdErr.EcodeInvalidField,
  362. `invalid value for "dir"`,
  363. )
  364. }
  365. if quorum, err = getBool(r.Form, "quorum"); err != nil {
  366. return emptyReq, etcdErr.NewRequestError(
  367. etcdErr.EcodeInvalidField,
  368. `invalid value for "quorum"`,
  369. )
  370. }
  371. if stream, err = getBool(r.Form, "stream"); err != nil {
  372. return emptyReq, etcdErr.NewRequestError(
  373. etcdErr.EcodeInvalidField,
  374. `invalid value for "stream"`,
  375. )
  376. }
  377. if wait && r.Method != "GET" {
  378. return emptyReq, etcdErr.NewRequestError(
  379. etcdErr.EcodeInvalidField,
  380. `"wait" can only be used with GET requests`,
  381. )
  382. }
  383. pV := r.FormValue("prevValue")
  384. if _, ok := r.Form["prevValue"]; ok && pV == "" {
  385. return emptyReq, etcdErr.NewRequestError(
  386. etcdErr.EcodePrevValueRequired,
  387. `"prevValue" cannot be empty`,
  388. )
  389. }
  390. // TTL is nullable, so leave it null if not specified
  391. // or an empty string
  392. var ttl *uint64
  393. if len(r.FormValue("ttl")) > 0 {
  394. i, err := getUint64(r.Form, "ttl")
  395. if err != nil {
  396. return emptyReq, etcdErr.NewRequestError(
  397. etcdErr.EcodeTTLNaN,
  398. `invalid value for "ttl"`,
  399. )
  400. }
  401. ttl = &i
  402. }
  403. // prevExist is nullable, so leave it null if not specified
  404. var pe *bool
  405. if _, ok := r.Form["prevExist"]; ok {
  406. bv, err := getBool(r.Form, "prevExist")
  407. if err != nil {
  408. return emptyReq, etcdErr.NewRequestError(
  409. etcdErr.EcodeInvalidField,
  410. "invalid value for prevExist",
  411. )
  412. }
  413. pe = &bv
  414. }
  415. rr := etcdserverpb.Request{
  416. Method: r.Method,
  417. Path: p,
  418. Val: r.FormValue("value"),
  419. Dir: dir,
  420. PrevValue: pV,
  421. PrevIndex: pIdx,
  422. PrevExist: pe,
  423. Wait: wait,
  424. Since: wIdx,
  425. Recursive: rec,
  426. Sorted: sort,
  427. Quorum: quorum,
  428. Stream: stream,
  429. }
  430. if pe != nil {
  431. rr.PrevExist = pe
  432. }
  433. // Null TTL is equivalent to unset Expiration
  434. if ttl != nil {
  435. expr := time.Duration(*ttl) * time.Second
  436. rr.Expiration = clock.Now().Add(expr).UnixNano()
  437. }
  438. return rr, nil
  439. }
  440. // writeKeyEvent trims the prefix of key path in a single Event under
  441. // StoreKeysPrefix, serializes it and writes the resulting JSON to the given
  442. // ResponseWriter, along with the appropriate headers.
  443. func writeKeyEvent(w http.ResponseWriter, ev *store.Event, rt etcdserver.RaftTimer) error {
  444. if ev == nil {
  445. return errors.New("cannot write empty Event!")
  446. }
  447. w.Header().Set("Content-Type", "application/json")
  448. w.Header().Set("X-Etcd-Index", fmt.Sprint(ev.EtcdIndex))
  449. w.Header().Set("X-Raft-Index", fmt.Sprint(rt.Index()))
  450. w.Header().Set("X-Raft-Term", fmt.Sprint(rt.Term()))
  451. if ev.IsCreated() {
  452. w.WriteHeader(http.StatusCreated)
  453. }
  454. ev = trimEventPrefix(ev, etcdserver.StoreKeysPrefix)
  455. return json.NewEncoder(w).Encode(ev)
  456. }
  457. func handleKeyWatch(ctx context.Context, w http.ResponseWriter, wa store.Watcher, stream bool, rt etcdserver.RaftTimer) {
  458. defer wa.Remove()
  459. ech := wa.EventChan()
  460. var nch <-chan bool
  461. if x, ok := w.(http.CloseNotifier); ok {
  462. nch = x.CloseNotify()
  463. }
  464. w.Header().Set("Content-Type", "application/json")
  465. w.Header().Set("X-Etcd-Index", fmt.Sprint(wa.StartIndex()))
  466. w.Header().Set("X-Raft-Index", fmt.Sprint(rt.Index()))
  467. w.Header().Set("X-Raft-Term", fmt.Sprint(rt.Term()))
  468. w.WriteHeader(http.StatusOK)
  469. // Ensure headers are flushed early, in case of long polling
  470. w.(http.Flusher).Flush()
  471. for {
  472. select {
  473. case <-nch:
  474. // Client closed connection. Nothing to do.
  475. return
  476. case <-ctx.Done():
  477. // Timed out. net/http will close the connection for us, so nothing to do.
  478. return
  479. case ev, ok := <-ech:
  480. if !ok {
  481. // If the channel is closed this may be an indication of
  482. // that notifications are much more than we are able to
  483. // send to the client in time. Then we simply end streaming.
  484. return
  485. }
  486. ev = trimEventPrefix(ev, etcdserver.StoreKeysPrefix)
  487. if err := json.NewEncoder(w).Encode(ev); err != nil {
  488. // Should never be reached
  489. log.Printf("error writing event: %v\n", err)
  490. return
  491. }
  492. if !stream {
  493. return
  494. }
  495. w.(http.Flusher).Flush()
  496. }
  497. }
  498. }
  499. func trimEventPrefix(ev *store.Event, prefix string) *store.Event {
  500. if ev == nil {
  501. return nil
  502. }
  503. // Since the *Event may reference one in the store history
  504. // history, we must copy it before modifying
  505. e := ev.Clone()
  506. e.Node = trimNodeExternPrefix(e.Node, prefix)
  507. e.PrevNode = trimNodeExternPrefix(e.PrevNode, prefix)
  508. return e
  509. }
  510. func trimNodeExternPrefix(n *store.NodeExtern, prefix string) *store.NodeExtern {
  511. if n == nil {
  512. return nil
  513. }
  514. n.Key = strings.TrimPrefix(n.Key, prefix)
  515. for _, nn := range n.Nodes {
  516. nn = trimNodeExternPrefix(nn, prefix)
  517. }
  518. return n
  519. }
  520. func trimErrorPrefix(err error, prefix string) error {
  521. if e, ok := err.(*etcdErr.Error); ok {
  522. e.Cause = strings.TrimPrefix(e.Cause, prefix)
  523. }
  524. return err
  525. }
  526. func unmarshalRequest(r *http.Request, req json.Unmarshaler, w http.ResponseWriter) bool {
  527. ctype := r.Header.Get("Content-Type")
  528. if ctype != "application/json" {
  529. writeError(w, httptypes.NewHTTPError(http.StatusUnsupportedMediaType, fmt.Sprintf("Bad Content-Type %s, accept application/json", ctype)))
  530. return false
  531. }
  532. b, err := ioutil.ReadAll(r.Body)
  533. if err != nil {
  534. writeError(w, httptypes.NewHTTPError(http.StatusBadRequest, err.Error()))
  535. return false
  536. }
  537. if err := req.UnmarshalJSON(b); err != nil {
  538. writeError(w, httptypes.NewHTTPError(http.StatusBadRequest, err.Error()))
  539. return false
  540. }
  541. return true
  542. }
  543. func getID(p string, w http.ResponseWriter) (types.ID, bool) {
  544. idStr := trimPrefix(p, membersPrefix)
  545. if idStr == "" {
  546. http.Error(w, "Method Not Allowed", http.StatusMethodNotAllowed)
  547. return 0, false
  548. }
  549. id, err := types.IDFromString(idStr)
  550. if err != nil {
  551. writeError(w, httptypes.NewHTTPError(http.StatusNotFound, fmt.Sprintf("No such member: %s", idStr)))
  552. return 0, false
  553. }
  554. return id, true
  555. }
  556. // getUint64 extracts a uint64 by the given key from a Form. If the key does
  557. // not exist in the form, 0 is returned. If the key exists but the value is
  558. // badly formed, an error is returned. If multiple values are present only the
  559. // first is considered.
  560. func getUint64(form url.Values, key string) (i uint64, err error) {
  561. if vals, ok := form[key]; ok {
  562. i, err = strconv.ParseUint(vals[0], 10, 64)
  563. }
  564. return
  565. }
  566. // getBool extracts a bool by the given key from a Form. If the key does not
  567. // exist in the form, false is returned. If the key exists but the value is
  568. // badly formed, an error is returned. If multiple values are present only the
  569. // first is considered.
  570. func getBool(form url.Values, key string) (b bool, err error) {
  571. if vals, ok := form[key]; ok {
  572. b, err = strconv.ParseBool(vals[0])
  573. }
  574. return
  575. }
  576. // trimPrefix removes a given prefix and any slash following the prefix
  577. // e.g.: trimPrefix("foo", "foo") == trimPrefix("foo/", "foo") == ""
  578. func trimPrefix(p, prefix string) (s string) {
  579. s = strings.TrimPrefix(p, prefix)
  580. s = strings.TrimPrefix(s, "/")
  581. return
  582. }
  583. func newMemberCollection(ms []*etcdserver.Member) *httptypes.MemberCollection {
  584. c := httptypes.MemberCollection(make([]httptypes.Member, len(ms)))
  585. for i, m := range ms {
  586. c[i] = newMember(m)
  587. }
  588. return &c
  589. }
  590. func newMember(m *etcdserver.Member) httptypes.Member {
  591. tm := httptypes.Member{
  592. ID: m.ID.String(),
  593. Name: m.Name,
  594. PeerURLs: make([]string, len(m.PeerURLs)),
  595. ClientURLs: make([]string, len(m.ClientURLs)),
  596. }
  597. copy(tm.PeerURLs, m.PeerURLs)
  598. copy(tm.ClientURLs, m.ClientURLs)
  599. return tm
  600. }