client.go 18 KB

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