فهرست منبع

vendor: replace go-etcd with etcd/client

Eugene Yakubovich 9 سال پیش
والد
کامیت
9ff11c2a95
55فایلهای تغییر یافته به همراه5898 افزوده شده و 2654 حذف شده
  1. 10 5
      Godeps/Godeps.json
  2. 235 0
      Godeps/_workspace/src/github.com/coreos/etcd/client/auth_role.go
  3. 297 0
      Godeps/_workspace/src/github.com/coreos/etcd/client/auth_user.go
  4. 430 0
      Godeps/_workspace/src/github.com/coreos/etcd/client/client.go
  5. 797 0
      Godeps/_workspace/src/github.com/coreos/etcd/client/client_test.go
  6. 33 0
      Godeps/_workspace/src/github.com/coreos/etcd/client/cluster_error.go
  7. 70 0
      Godeps/_workspace/src/github.com/coreos/etcd/client/curl.go
  8. 21 0
      Godeps/_workspace/src/github.com/coreos/etcd/client/discover.go
  9. 71 0
      Godeps/_workspace/src/github.com/coreos/etcd/client/doc.go
  10. 631 0
      Godeps/_workspace/src/github.com/coreos/etcd/client/keys.go
  11. 1393 0
      Godeps/_workspace/src/github.com/coreos/etcd/client/keys_test.go
  12. 271 0
      Godeps/_workspace/src/github.com/coreos/etcd/client/members.go
  13. 521 0
      Godeps/_workspace/src/github.com/coreos/etcd/client/members_test.go
  14. 65 0
      Godeps/_workspace/src/github.com/coreos/etcd/client/srv.go
  15. 102 0
      Godeps/_workspace/src/github.com/coreos/etcd/client/srv_test.go
  16. 5 1
      Godeps/_workspace/src/github.com/coreos/etcd/pkg/transport/keepalive_listener.go
  17. 7 0
      Godeps/_workspace/src/github.com/coreos/etcd/pkg/transport/keepalive_listener_test.go
  18. 41 0
      Godeps/_workspace/src/github.com/coreos/etcd/pkg/types/id.go
  19. 95 0
      Godeps/_workspace/src/github.com/coreos/etcd/pkg/types/id_test.go
  20. 178 0
      Godeps/_workspace/src/github.com/coreos/etcd/pkg/types/set.go
  21. 186 0
      Godeps/_workspace/src/github.com/coreos/etcd/pkg/types/set_test.go
  22. 22 0
      Godeps/_workspace/src/github.com/coreos/etcd/pkg/types/slice.go
  23. 30 0
      Godeps/_workspace/src/github.com/coreos/etcd/pkg/types/slice_test.go
  24. 74 0
      Godeps/_workspace/src/github.com/coreos/etcd/pkg/types/urls.go
  25. 169 0
      Godeps/_workspace/src/github.com/coreos/etcd/pkg/types/urls_test.go
  26. 75 0
      Godeps/_workspace/src/github.com/coreos/etcd/pkg/types/urlsmap.go
  27. 69 0
      Godeps/_workspace/src/github.com/coreos/etcd/pkg/types/urlsmap_test.go
  28. 0 23
      Godeps/_workspace/src/github.com/coreos/go-etcd/etcd/add_child.go
  29. 0 73
      Godeps/_workspace/src/github.com/coreos/go-etcd/etcd/add_child_test.go
  30. 0 481
      Godeps/_workspace/src/github.com/coreos/go-etcd/etcd/client.go
  31. 0 108
      Godeps/_workspace/src/github.com/coreos/go-etcd/etcd/client_test.go
  32. 0 37
      Godeps/_workspace/src/github.com/coreos/go-etcd/etcd/cluster.go
  33. 0 34
      Godeps/_workspace/src/github.com/coreos/go-etcd/etcd/compare_and_delete.go
  34. 0 46
      Godeps/_workspace/src/github.com/coreos/go-etcd/etcd/compare_and_delete_test.go
  35. 0 36
      Godeps/_workspace/src/github.com/coreos/go-etcd/etcd/compare_and_swap.go
  36. 0 57
      Godeps/_workspace/src/github.com/coreos/go-etcd/etcd/compare_and_swap_test.go
  37. 0 55
      Godeps/_workspace/src/github.com/coreos/go-etcd/etcd/debug.go
  38. 0 28
      Godeps/_workspace/src/github.com/coreos/go-etcd/etcd/debug_test.go
  39. 0 40
      Godeps/_workspace/src/github.com/coreos/go-etcd/etcd/delete.go
  40. 0 81
      Godeps/_workspace/src/github.com/coreos/go-etcd/etcd/delete_test.go
  41. 0 49
      Godeps/_workspace/src/github.com/coreos/go-etcd/etcd/error.go
  42. 0 32
      Godeps/_workspace/src/github.com/coreos/go-etcd/etcd/get.go
  43. 0 131
      Godeps/_workspace/src/github.com/coreos/go-etcd/etcd/get_test.go
  44. 0 30
      Godeps/_workspace/src/github.com/coreos/go-etcd/etcd/member.go
  45. 0 71
      Godeps/_workspace/src/github.com/coreos/go-etcd/etcd/member_test.go
  46. 0 72
      Godeps/_workspace/src/github.com/coreos/go-etcd/etcd/options.go
  47. 0 405
      Godeps/_workspace/src/github.com/coreos/go-etcd/etcd/requests.go
  48. 0 22
      Godeps/_workspace/src/github.com/coreos/go-etcd/etcd/requests_test.go
  49. 0 89
      Godeps/_workspace/src/github.com/coreos/go-etcd/etcd/response.go
  50. 0 42
      Godeps/_workspace/src/github.com/coreos/go-etcd/etcd/set_curl_chan_test.go
  51. 0 137
      Godeps/_workspace/src/github.com/coreos/go-etcd/etcd/set_update_create.go
  52. 0 241
      Godeps/_workspace/src/github.com/coreos/go-etcd/etcd/set_update_create_test.go
  53. 0 6
      Godeps/_workspace/src/github.com/coreos/go-etcd/etcd/version.go
  54. 0 103
      Godeps/_workspace/src/github.com/coreos/go-etcd/etcd/watch.go
  55. 0 119
      Godeps/_workspace/src/github.com/coreos/go-etcd/etcd/watch_test.go

+ 10 - 5
Godeps/Godeps.json

@@ -25,15 +25,20 @@
 			"Comment": "release-107",
 			"Comment": "release-107",
 			"Rev": "6ddfebb10ece847f1ae09c701834f1b15abbd8b2"
 			"Rev": "6ddfebb10ece847f1ae09c701834f1b15abbd8b2"
 		},
 		},
+		{
+			"ImportPath": "github.com/coreos/etcd/client",
+			"Comment": "v2.1.1-85-gff0b872",
+			"Rev": "ff0b8723c747e76d2651bcddaff26563128ab216"
+		},
 		{
 		{
 			"ImportPath": "github.com/coreos/etcd/pkg/transport",
 			"ImportPath": "github.com/coreos/etcd/pkg/transport",
-			"Comment": "v2.1.0-rc.0-24-g8ab388f",
-			"Rev": "8ab388fa56e6ad4d5566a42ef9f5ebeadc433a6e"
+			"Comment": "v2.1.1-85-gff0b872",
+			"Rev": "ff0b8723c747e76d2651bcddaff26563128ab216"
 		},
 		},
 		{
 		{
-			"ImportPath": "github.com/coreos/go-etcd/etcd",
-			"Comment": "v2.0.0-3-g0424b5f",
-			"Rev": "0424b5f86ef0ca57a5309c599f74bbb3e97ecd9d"
+			"ImportPath": "github.com/coreos/etcd/pkg/types",
+			"Comment": "v2.1.1-85-gff0b872",
+			"Rev": "ff0b8723c747e76d2651bcddaff26563128ab216"
 		},
 		},
 		{
 		{
 			"ImportPath": "github.com/coreos/go-iptables/iptables",
 			"ImportPath": "github.com/coreos/go-iptables/iptables",

+ 235 - 0
Godeps/_workspace/src/github.com/coreos/etcd/client/auth_role.go

@@ -0,0 +1,235 @@
+// Copyright 2015 CoreOS, Inc.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package client
+
+import (
+	"bytes"
+	"encoding/json"
+	"net/http"
+	"net/url"
+
+	"github.com/coreos/flannel/Godeps/_workspace/src/golang.org/x/net/context"
+)
+
+type Role struct {
+	Role        string       `json:"role"`
+	Permissions Permissions  `json:"permissions"`
+	Grant       *Permissions `json:"grant,omitempty"`
+	Revoke      *Permissions `json:"revoke,omitempty"`
+}
+
+type Permissions struct {
+	KV rwPermission `json:"kv"`
+}
+
+type rwPermission struct {
+	Read  []string `json:"read"`
+	Write []string `json:"write"`
+}
+
+type PermissionType int
+
+const (
+	ReadPermission PermissionType = iota
+	WritePermission
+	ReadWritePermission
+)
+
+// NewAuthRoleAPI constructs a new AuthRoleAPI that uses HTTP to
+// interact with etcd's role creation and modification features.
+func NewAuthRoleAPI(c Client) AuthRoleAPI {
+	return &httpAuthRoleAPI{
+		client: c,
+	}
+}
+
+type AuthRoleAPI interface {
+	// Add a role.
+	AddRole(ctx context.Context, role string) error
+
+	// Remove a role.
+	RemoveRole(ctx context.Context, role string) error
+
+	// Get role details.
+	GetRole(ctx context.Context, role string) (*Role, error)
+
+	// Grant a role some permission prefixes for the KV store.
+	GrantRoleKV(ctx context.Context, role string, prefixes []string, permType PermissionType) (*Role, error)
+
+	// Revoke some some permission prefixes for a role on the KV store.
+	RevokeRoleKV(ctx context.Context, role string, prefixes []string, permType PermissionType) (*Role, error)
+
+	// List roles.
+	ListRoles(ctx context.Context) ([]string, error)
+}
+
+type httpAuthRoleAPI struct {
+	client httpClient
+}
+
+type authRoleAPIAction struct {
+	verb string
+	name string
+	role *Role
+}
+
+type authRoleAPIList struct{}
+
+func (list *authRoleAPIList) HTTPRequest(ep url.URL) *http.Request {
+	u := v2AuthURL(ep, "roles", "")
+	req, _ := http.NewRequest("GET", u.String(), nil)
+	req.Header.Set("Content-Type", "application/json")
+	return req
+}
+
+func (l *authRoleAPIAction) HTTPRequest(ep url.URL) *http.Request {
+	u := v2AuthURL(ep, "roles", l.name)
+	if l.role == nil {
+		req, _ := http.NewRequest(l.verb, u.String(), nil)
+		return req
+	}
+	b, err := json.Marshal(l.role)
+	if err != nil {
+		panic(err)
+	}
+	body := bytes.NewReader(b)
+	req, _ := http.NewRequest(l.verb, u.String(), body)
+	req.Header.Set("Content-Type", "application/json")
+	return req
+}
+
+func (r *httpAuthRoleAPI) ListRoles(ctx context.Context) ([]string, error) {
+	resp, body, err := r.client.Do(ctx, &authRoleAPIList{})
+	if err != nil {
+		return nil, err
+	}
+	if err := assertStatusCode(resp.StatusCode, http.StatusOK); err != nil {
+		return nil, err
+	}
+	var userList struct {
+		Roles []string `json:"roles"`
+	}
+	err = json.Unmarshal(body, &userList)
+	if err != nil {
+		return nil, err
+	}
+	return userList.Roles, nil
+}
+
+func (r *httpAuthRoleAPI) AddRole(ctx context.Context, rolename string) error {
+	role := &Role{
+		Role: rolename,
+	}
+	return r.addRemoveRole(ctx, &authRoleAPIAction{
+		verb: "PUT",
+		name: rolename,
+		role: role,
+	})
+}
+
+func (r *httpAuthRoleAPI) RemoveRole(ctx context.Context, rolename string) error {
+	return r.addRemoveRole(ctx, &authRoleAPIAction{
+		verb: "DELETE",
+		name: rolename,
+	})
+}
+
+func (r *httpAuthRoleAPI) addRemoveRole(ctx context.Context, req *authRoleAPIAction) error {
+	resp, body, err := r.client.Do(ctx, req)
+	if err != nil {
+		return err
+	}
+	if err := assertStatusCode(resp.StatusCode, http.StatusOK, http.StatusCreated); err != nil {
+		var sec authError
+		err := json.Unmarshal(body, &sec)
+		if err != nil {
+			return err
+		}
+		return sec
+	}
+	return nil
+}
+
+func (r *httpAuthRoleAPI) GetRole(ctx context.Context, rolename string) (*Role, error) {
+	return r.modRole(ctx, &authRoleAPIAction{
+		verb: "GET",
+		name: rolename,
+	})
+}
+
+func buildRWPermission(prefixes []string, permType PermissionType) rwPermission {
+	var out rwPermission
+	switch permType {
+	case ReadPermission:
+		out.Read = prefixes
+	case WritePermission:
+		out.Write = prefixes
+	case ReadWritePermission:
+		out.Read = prefixes
+		out.Write = prefixes
+	}
+	return out
+}
+
+func (r *httpAuthRoleAPI) GrantRoleKV(ctx context.Context, rolename string, prefixes []string, permType PermissionType) (*Role, error) {
+	rwp := buildRWPermission(prefixes, permType)
+	role := &Role{
+		Role: rolename,
+		Grant: &Permissions{
+			KV: rwp,
+		},
+	}
+	return r.modRole(ctx, &authRoleAPIAction{
+		verb: "PUT",
+		name: rolename,
+		role: role,
+	})
+}
+
+func (r *httpAuthRoleAPI) RevokeRoleKV(ctx context.Context, rolename string, prefixes []string, permType PermissionType) (*Role, error) {
+	rwp := buildRWPermission(prefixes, permType)
+	role := &Role{
+		Role: rolename,
+		Revoke: &Permissions{
+			KV: rwp,
+		},
+	}
+	return r.modRole(ctx, &authRoleAPIAction{
+		verb: "PUT",
+		name: rolename,
+		role: role,
+	})
+}
+
+func (r *httpAuthRoleAPI) modRole(ctx context.Context, req *authRoleAPIAction) (*Role, error) {
+	resp, body, err := r.client.Do(ctx, req)
+	if err != nil {
+		return nil, err
+	}
+	if err := assertStatusCode(resp.StatusCode, http.StatusOK); err != nil {
+		var sec authError
+		err := json.Unmarshal(body, &sec)
+		if err != nil {
+			return nil, err
+		}
+		return nil, sec
+	}
+	var role Role
+	err = json.Unmarshal(body, &role)
+	if err != nil {
+		return nil, err
+	}
+	return &role, nil
+}

+ 297 - 0
Godeps/_workspace/src/github.com/coreos/etcd/client/auth_user.go

@@ -0,0 +1,297 @@
+// Copyright 2015 CoreOS, Inc.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package client
+
+import (
+	"bytes"
+	"encoding/json"
+	"net/http"
+	"net/url"
+	"path"
+
+	"github.com/coreos/flannel/Godeps/_workspace/src/golang.org/x/net/context"
+)
+
+var (
+	defaultV2AuthPrefix = "/v2/auth"
+)
+
+type User struct {
+	User     string   `json:"user"`
+	Password string   `json:"password,omitempty"`
+	Roles    []string `json:"roles"`
+	Grant    []string `json:"grant,omitempty"`
+	Revoke   []string `json:"revoke,omitempty"`
+}
+
+func v2AuthURL(ep url.URL, action string, name string) *url.URL {
+	if name != "" {
+		ep.Path = path.Join(ep.Path, defaultV2AuthPrefix, action, name)
+		return &ep
+	}
+	ep.Path = path.Join(ep.Path, defaultV2AuthPrefix, action)
+	return &ep
+}
+
+// NewAuthAPI constructs a new AuthAPI that uses HTTP to
+// interact with etcd's general auth features.
+func NewAuthAPI(c Client) AuthAPI {
+	return &httpAuthAPI{
+		client: c,
+	}
+}
+
+type AuthAPI interface {
+	// Enable auth.
+	Enable(ctx context.Context) error
+
+	// Disable auth.
+	Disable(ctx context.Context) error
+}
+
+type httpAuthAPI struct {
+	client httpClient
+}
+
+func (s *httpAuthAPI) Enable(ctx context.Context) error {
+	return s.enableDisable(ctx, &authAPIAction{"PUT"})
+}
+
+func (s *httpAuthAPI) Disable(ctx context.Context) error {
+	return s.enableDisable(ctx, &authAPIAction{"DELETE"})
+}
+
+func (s *httpAuthAPI) enableDisable(ctx context.Context, req httpAction) error {
+	resp, body, err := s.client.Do(ctx, req)
+	if err != nil {
+		return err
+	}
+	if err := assertStatusCode(resp.StatusCode, http.StatusOK, http.StatusCreated); err != nil {
+		var sec authError
+		err := json.Unmarshal(body, &sec)
+		if err != nil {
+			return err
+		}
+		return sec
+	}
+	return nil
+}
+
+type authAPIAction struct {
+	verb string
+}
+
+func (l *authAPIAction) HTTPRequest(ep url.URL) *http.Request {
+	u := v2AuthURL(ep, "enable", "")
+	req, _ := http.NewRequest(l.verb, u.String(), nil)
+	return req
+}
+
+type authError struct {
+	Message string `json:"message"`
+	Code    int    `json:"-"`
+}
+
+func (e authError) Error() string {
+	return e.Message
+}
+
+// NewAuthUserAPI constructs a new AuthUserAPI that uses HTTP to
+// interact with etcd's user creation and modification features.
+func NewAuthUserAPI(c Client) AuthUserAPI {
+	return &httpAuthUserAPI{
+		client: c,
+	}
+}
+
+type AuthUserAPI interface {
+	// Add a user.
+	AddUser(ctx context.Context, username string, password string) error
+
+	// Remove a user.
+	RemoveUser(ctx context.Context, username string) error
+
+	// Get user details.
+	GetUser(ctx context.Context, username string) (*User, error)
+
+	// Grant a user some permission roles.
+	GrantUser(ctx context.Context, username string, roles []string) (*User, error)
+
+	// Revoke some permission roles from a user.
+	RevokeUser(ctx context.Context, username string, roles []string) (*User, error)
+
+	// Change the user's password.
+	ChangePassword(ctx context.Context, username string, password string) (*User, error)
+
+	// List users.
+	ListUsers(ctx context.Context) ([]string, error)
+}
+
+type httpAuthUserAPI struct {
+	client httpClient
+}
+
+type authUserAPIAction struct {
+	verb     string
+	username string
+	user     *User
+}
+
+type authUserAPIList struct{}
+
+func (list *authUserAPIList) HTTPRequest(ep url.URL) *http.Request {
+	u := v2AuthURL(ep, "users", "")
+	req, _ := http.NewRequest("GET", u.String(), nil)
+	req.Header.Set("Content-Type", "application/json")
+	return req
+}
+
+func (l *authUserAPIAction) HTTPRequest(ep url.URL) *http.Request {
+	u := v2AuthURL(ep, "users", l.username)
+	if l.user == nil {
+		req, _ := http.NewRequest(l.verb, u.String(), nil)
+		return req
+	}
+	b, err := json.Marshal(l.user)
+	if err != nil {
+		panic(err)
+	}
+	body := bytes.NewReader(b)
+	req, _ := http.NewRequest(l.verb, u.String(), body)
+	req.Header.Set("Content-Type", "application/json")
+	return req
+}
+
+func (u *httpAuthUserAPI) ListUsers(ctx context.Context) ([]string, error) {
+	resp, body, err := u.client.Do(ctx, &authUserAPIList{})
+	if err != nil {
+		return nil, err
+	}
+	if err := assertStatusCode(resp.StatusCode, http.StatusOK); err != nil {
+		var sec authError
+		err := json.Unmarshal(body, &sec)
+		if err != nil {
+			return nil, err
+		}
+		return nil, sec
+	}
+	var userList struct {
+		Users []string `json:"users"`
+	}
+	err = json.Unmarshal(body, &userList)
+	if err != nil {
+		return nil, err
+	}
+	return userList.Users, nil
+}
+
+func (u *httpAuthUserAPI) AddUser(ctx context.Context, username string, password string) error {
+	user := &User{
+		User:     username,
+		Password: password,
+	}
+	return u.addRemoveUser(ctx, &authUserAPIAction{
+		verb:     "PUT",
+		username: username,
+		user:     user,
+	})
+}
+
+func (u *httpAuthUserAPI) RemoveUser(ctx context.Context, username string) error {
+	return u.addRemoveUser(ctx, &authUserAPIAction{
+		verb:     "DELETE",
+		username: username,
+	})
+}
+
+func (u *httpAuthUserAPI) addRemoveUser(ctx context.Context, req *authUserAPIAction) error {
+	resp, body, err := u.client.Do(ctx, req)
+	if err != nil {
+		return err
+	}
+	if err := assertStatusCode(resp.StatusCode, http.StatusOK, http.StatusCreated); err != nil {
+		var sec authError
+		err := json.Unmarshal(body, &sec)
+		if err != nil {
+			return err
+		}
+		return sec
+	}
+	return nil
+}
+
+func (u *httpAuthUserAPI) GetUser(ctx context.Context, username string) (*User, error) {
+	return u.modUser(ctx, &authUserAPIAction{
+		verb:     "GET",
+		username: username,
+	})
+}
+
+func (u *httpAuthUserAPI) GrantUser(ctx context.Context, username string, roles []string) (*User, error) {
+	user := &User{
+		User:  username,
+		Grant: roles,
+	}
+	return u.modUser(ctx, &authUserAPIAction{
+		verb:     "PUT",
+		username: username,
+		user:     user,
+	})
+}
+
+func (u *httpAuthUserAPI) RevokeUser(ctx context.Context, username string, roles []string) (*User, error) {
+	user := &User{
+		User:   username,
+		Revoke: roles,
+	}
+	return u.modUser(ctx, &authUserAPIAction{
+		verb:     "PUT",
+		username: username,
+		user:     user,
+	})
+}
+
+func (u *httpAuthUserAPI) ChangePassword(ctx context.Context, username string, password string) (*User, error) {
+	user := &User{
+		User:     username,
+		Password: password,
+	}
+	return u.modUser(ctx, &authUserAPIAction{
+		verb:     "PUT",
+		username: username,
+		user:     user,
+	})
+}
+
+func (u *httpAuthUserAPI) modUser(ctx context.Context, req *authUserAPIAction) (*User, error) {
+	resp, body, err := u.client.Do(ctx, req)
+	if err != nil {
+		return nil, err
+	}
+	if err := assertStatusCode(resp.StatusCode, http.StatusOK); err != nil {
+		var sec authError
+		err := json.Unmarshal(body, &sec)
+		if err != nil {
+			return nil, err
+		}
+		return nil, sec
+	}
+	var user User
+	err = json.Unmarshal(body, &user)
+	if err != nil {
+		return nil, err
+	}
+	return &user, nil
+}

+ 430 - 0
Godeps/_workspace/src/github.com/coreos/etcd/client/client.go

@@ -0,0 +1,430 @@
+// Copyright 2015 CoreOS, Inc.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package client
+
+import (
+	"errors"
+	"fmt"
+	"io/ioutil"
+	"math/rand"
+	"net"
+	"net/http"
+	"net/url"
+	"sync"
+	"time"
+
+	"github.com/coreos/flannel/Godeps/_workspace/src/golang.org/x/net/context"
+)
+
+var (
+	ErrNoEndpoints           = errors.New("client: no endpoints available")
+	ErrTooManyRedirects      = errors.New("client: too many redirects")
+	ErrClusterUnavailable    = errors.New("client: etcd cluster is unavailable or misconfigured")
+	errTooManyRedirectChecks = errors.New("client: too many redirect checks")
+)
+
+var DefaultRequestTimeout = 5 * time.Second
+
+var DefaultTransport CancelableTransport = &http.Transport{
+	Proxy: http.ProxyFromEnvironment,
+	Dial: (&net.Dialer{
+		Timeout:   30 * time.Second,
+		KeepAlive: 30 * time.Second,
+	}).Dial,
+	TLSHandshakeTimeout: 10 * time.Second,
+}
+
+type Config struct {
+	// Endpoints defines a set of URLs (schemes, hosts and ports only)
+	// that can be used to communicate with a logical etcd cluster. For
+	// example, a three-node cluster could be provided like so:
+	//
+	// 	Endpoints: []string{
+	//		"http://node1.example.com:2379",
+	//		"http://node2.example.com:2379",
+	//		"http://node3.example.com:2379",
+	//	}
+	//
+	// If multiple endpoints are provided, the Client will attempt to
+	// use them all in the event that one or more of them are unusable.
+	//
+	// If Client.Sync is ever called, the Client may cache an alternate
+	// set of endpoints to continue operation.
+	Endpoints []string
+
+	// Transport is used by the Client to drive HTTP requests. If not
+	// provided, DefaultTransport will be used.
+	Transport CancelableTransport
+
+	// CheckRedirect specifies the policy for handling HTTP redirects.
+	// If CheckRedirect is not nil, the Client calls it before
+	// following an HTTP redirect. The sole argument is the number of
+	// requests that have alrady been made. If CheckRedirect returns
+	// an error, Client.Do will not make any further requests and return
+	// the error back it to the caller.
+	//
+	// If CheckRedirect is nil, the Client uses its default policy,
+	// which is to stop after 10 consecutive requests.
+	CheckRedirect CheckRedirectFunc
+
+	// Username specifies the user credential to add as an authorization header
+	Username string
+
+	// Password is the password for the specified user to add as an authorization header
+	// to the request.
+	Password string
+}
+
+func (cfg *Config) transport() CancelableTransport {
+	if cfg.Transport == nil {
+		return DefaultTransport
+	}
+	return cfg.Transport
+}
+
+func (cfg *Config) checkRedirect() CheckRedirectFunc {
+	if cfg.CheckRedirect == nil {
+		return DefaultCheckRedirect
+	}
+	return cfg.CheckRedirect
+}
+
+// CancelableTransport mimics net/http.Transport, but requires that
+// the object also support request cancellation.
+type CancelableTransport interface {
+	http.RoundTripper
+	CancelRequest(req *http.Request)
+}
+
+type CheckRedirectFunc func(via int) error
+
+// DefaultCheckRedirect follows up to 10 redirects, but no more.
+var DefaultCheckRedirect CheckRedirectFunc = func(via int) error {
+	if via > 10 {
+		return ErrTooManyRedirects
+	}
+	return nil
+}
+
+type Client interface {
+	// Sync updates the internal cache of the etcd cluster's membership.
+	Sync(context.Context) error
+
+	// Endpoints returns a copy of the current set of API endpoints used
+	// by Client to resolve HTTP requests. If Sync has ever been called,
+	// this may differ from the initial Endpoints provided in the Config.
+	Endpoints() []string
+
+	httpClient
+}
+
+func New(cfg Config) (Client, error) {
+	c := &httpClusterClient{
+		clientFactory: newHTTPClientFactory(cfg.transport(), cfg.checkRedirect()),
+		rand:          rand.New(rand.NewSource(int64(time.Now().Nanosecond()))),
+	}
+	if cfg.Username != "" {
+		c.credentials = &credentials{
+			username: cfg.Username,
+			password: cfg.Password,
+		}
+	}
+	if err := c.reset(cfg.Endpoints); err != nil {
+		return nil, err
+	}
+	return c, nil
+}
+
+type httpClient interface {
+	Do(context.Context, httpAction) (*http.Response, []byte, error)
+}
+
+func newHTTPClientFactory(tr CancelableTransport, cr CheckRedirectFunc) httpClientFactory {
+	return func(ep url.URL) httpClient {
+		return &redirectFollowingHTTPClient{
+			checkRedirect: cr,
+			client: &simpleHTTPClient{
+				transport: tr,
+				endpoint:  ep,
+			},
+		}
+	}
+}
+
+type credentials struct {
+	username string
+	password string
+}
+
+type httpClientFactory func(url.URL) httpClient
+
+type httpAction interface {
+	HTTPRequest(url.URL) *http.Request
+}
+
+type httpClusterClient struct {
+	clientFactory httpClientFactory
+	endpoints     []url.URL
+	pinned        int
+	credentials   *credentials
+	sync.RWMutex
+	rand *rand.Rand
+}
+
+func (c *httpClusterClient) reset(eps []string) error {
+	if len(eps) == 0 {
+		return ErrNoEndpoints
+	}
+
+	neps := make([]url.URL, len(eps))
+	for i, ep := range eps {
+		u, err := url.Parse(ep)
+		if err != nil {
+			return err
+		}
+		neps[i] = *u
+	}
+
+	c.endpoints = shuffleEndpoints(c.rand, neps)
+	// TODO: pin old endpoint if possible, and rebalance when new endpoint appears
+	c.pinned = 0
+
+	return nil
+}
+
+func (c *httpClusterClient) Do(ctx context.Context, act httpAction) (*http.Response, []byte, error) {
+	action := act
+	c.RLock()
+	leps := len(c.endpoints)
+	eps := make([]url.URL, leps)
+	n := copy(eps, c.endpoints)
+	pinned := c.pinned
+
+	if c.credentials != nil {
+		action = &authedAction{
+			act:         act,
+			credentials: *c.credentials,
+		}
+	}
+	c.RUnlock()
+
+	if leps == 0 {
+		return nil, nil, ErrNoEndpoints
+	}
+
+	if leps != n {
+		return nil, nil, errors.New("unable to pick endpoint: copy failed")
+	}
+
+	var resp *http.Response
+	var body []byte
+	var err error
+	cerr := &ClusterError{}
+
+	for i := pinned; i < leps+pinned; i++ {
+		k := i % leps
+		hc := c.clientFactory(eps[k])
+		resp, body, err = hc.Do(ctx, action)
+		if err != nil {
+			cerr.Errors = append(cerr.Errors, err)
+			if err == context.DeadlineExceeded || err == context.Canceled {
+				return nil, nil, cerr
+			}
+			continue
+		}
+		if resp.StatusCode/100 == 5 {
+			// TODO: make sure this is a no leader response
+			cerr.Errors = append(cerr.Errors, fmt.Errorf("client: etcd member %s has no leader", eps[k].String()))
+			continue
+		}
+		if k != pinned {
+			c.Lock()
+			c.pinned = k
+			c.Unlock()
+		}
+		return resp, body, nil
+	}
+
+	return nil, nil, cerr
+}
+
+func (c *httpClusterClient) Endpoints() []string {
+	c.RLock()
+	defer c.RUnlock()
+
+	eps := make([]string, len(c.endpoints))
+	for i, ep := range c.endpoints {
+		eps[i] = ep.String()
+	}
+
+	return eps
+}
+
+func (c *httpClusterClient) Sync(ctx context.Context) error {
+	mAPI := NewMembersAPI(c)
+	ms, err := mAPI.List(ctx)
+	if err != nil {
+		return err
+	}
+
+	c.Lock()
+	defer c.Unlock()
+
+	eps := make([]string, 0)
+	for _, m := range ms {
+		eps = append(eps, m.ClientURLs...)
+	}
+
+	return c.reset(eps)
+}
+
+type roundTripResponse struct {
+	resp *http.Response
+	err  error
+}
+
+type simpleHTTPClient struct {
+	transport CancelableTransport
+	endpoint  url.URL
+}
+
+func (c *simpleHTTPClient) Do(ctx context.Context, act httpAction) (*http.Response, []byte, error) {
+	req := act.HTTPRequest(c.endpoint)
+
+	if err := printcURL(req); err != nil {
+		return nil, nil, err
+	}
+
+	rtchan := make(chan roundTripResponse, 1)
+	go func() {
+		resp, err := c.transport.RoundTrip(req)
+		rtchan <- roundTripResponse{resp: resp, err: err}
+		close(rtchan)
+	}()
+
+	var resp *http.Response
+	var err error
+
+	select {
+	case rtresp := <-rtchan:
+		resp, err = rtresp.resp, rtresp.err
+	case <-ctx.Done():
+		// cancel and wait for request to actually exit before continuing
+		c.transport.CancelRequest(req)
+		rtresp := <-rtchan
+		resp = rtresp.resp
+		err = ctx.Err()
+	}
+
+	// always check for resp nil-ness to deal with possible
+	// race conditions between channels above
+	defer func() {
+		if resp != nil {
+			resp.Body.Close()
+		}
+	}()
+
+	if err != nil {
+		return nil, nil, err
+	}
+
+	var body []byte
+	done := make(chan struct{})
+	go func() {
+		body, err = ioutil.ReadAll(resp.Body)
+		done <- struct{}{}
+	}()
+
+	select {
+	case <-ctx.Done():
+		err = resp.Body.Close()
+		<-done
+		if err == nil {
+			err = ctx.Err()
+		}
+	case <-done:
+	}
+
+	return resp, body, err
+}
+
+type authedAction struct {
+	act         httpAction
+	credentials credentials
+}
+
+func (a *authedAction) HTTPRequest(url url.URL) *http.Request {
+	r := a.act.HTTPRequest(url)
+	r.SetBasicAuth(a.credentials.username, a.credentials.password)
+	return r
+}
+
+type redirectFollowingHTTPClient struct {
+	client        httpClient
+	checkRedirect CheckRedirectFunc
+}
+
+func (r *redirectFollowingHTTPClient) Do(ctx context.Context, act httpAction) (*http.Response, []byte, error) {
+	next := act
+	for i := 0; i < 100; i++ {
+		if i > 0 {
+			if err := r.checkRedirect(i); err != nil {
+				return nil, nil, err
+			}
+		}
+		resp, body, err := r.client.Do(ctx, next)
+		if err != nil {
+			return nil, nil, err
+		}
+		if resp.StatusCode/100 == 3 {
+			hdr := resp.Header.Get("Location")
+			if hdr == "" {
+				return nil, nil, fmt.Errorf("Location header not set")
+			}
+			loc, err := url.Parse(hdr)
+			if err != nil {
+				return nil, nil, fmt.Errorf("Location header not valid URL: %s", hdr)
+			}
+			next = &redirectedHTTPAction{
+				action:   act,
+				location: *loc,
+			}
+			continue
+		}
+		return resp, body, nil
+	}
+
+	return nil, nil, errTooManyRedirectChecks
+}
+
+type redirectedHTTPAction struct {
+	action   httpAction
+	location url.URL
+}
+
+func (r *redirectedHTTPAction) HTTPRequest(ep url.URL) *http.Request {
+	orig := r.action.HTTPRequest(ep)
+	orig.URL = &r.location
+	return orig
+}
+
+func shuffleEndpoints(r *rand.Rand, eps []url.URL) []url.URL {
+	p := r.Perm(len(eps))
+	neps := make([]url.URL, len(eps))
+	for i, k := range p {
+		neps[i] = eps[k]
+	}
+	return neps
+}

+ 797 - 0
Godeps/_workspace/src/github.com/coreos/etcd/client/client_test.go

@@ -0,0 +1,797 @@
+// Copyright 2015 CoreOS, Inc.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package client
+
+import (
+	"errors"
+	"io"
+	"io/ioutil"
+	"math/rand"
+	"net/http"
+	"net/url"
+	"reflect"
+	"sort"
+	"strings"
+	"testing"
+	"time"
+
+	"github.com/coreos/etcd/pkg/testutil"
+	"github.com/coreos/flannel/Godeps/_workspace/src/golang.org/x/net/context"
+)
+
+type actionAssertingHTTPClient struct {
+	t   *testing.T
+	num int
+	act httpAction
+
+	resp http.Response
+	body []byte
+	err  error
+}
+
+func (a *actionAssertingHTTPClient) Do(_ context.Context, act httpAction) (*http.Response, []byte, error) {
+	if !reflect.DeepEqual(a.act, act) {
+		a.t.Errorf("#%d: unexpected httpAction: want=%#v got=%#v", a.num, a.act, act)
+	}
+
+	return &a.resp, a.body, a.err
+}
+
+type staticHTTPClient struct {
+	resp http.Response
+	body []byte
+	err  error
+}
+
+func (s *staticHTTPClient) Do(context.Context, httpAction) (*http.Response, []byte, error) {
+	return &s.resp, s.body, s.err
+}
+
+type staticHTTPAction struct {
+	request http.Request
+}
+
+func (s *staticHTTPAction) HTTPRequest(url.URL) *http.Request {
+	return &s.request
+}
+
+type staticHTTPResponse struct {
+	resp http.Response
+	body []byte
+	err  error
+}
+
+type multiStaticHTTPClient struct {
+	responses []staticHTTPResponse
+	cur       int
+}
+
+func (s *multiStaticHTTPClient) Do(context.Context, httpAction) (*http.Response, []byte, error) {
+	r := s.responses[s.cur]
+	s.cur++
+	return &r.resp, r.body, r.err
+}
+
+func newStaticHTTPClientFactory(responses []staticHTTPResponse) httpClientFactory {
+	var cur int
+	return func(url.URL) httpClient {
+		r := responses[cur]
+		cur++
+		return &staticHTTPClient{resp: r.resp, body: r.body, err: r.err}
+	}
+}
+
+type fakeTransport struct {
+	respchan     chan *http.Response
+	errchan      chan error
+	startCancel  chan struct{}
+	finishCancel chan struct{}
+}
+
+func newFakeTransport() *fakeTransport {
+	return &fakeTransport{
+		respchan:     make(chan *http.Response, 1),
+		errchan:      make(chan error, 1),
+		startCancel:  make(chan struct{}, 1),
+		finishCancel: make(chan struct{}, 1),
+	}
+}
+
+func (t *fakeTransport) RoundTrip(*http.Request) (*http.Response, error) {
+	select {
+	case resp := <-t.respchan:
+		return resp, nil
+	case err := <-t.errchan:
+		return nil, err
+	case <-t.startCancel:
+		select {
+		// this simulates that the request is finished before cancel effects
+		case resp := <-t.respchan:
+			return resp, nil
+		// wait on finishCancel to simulate taking some amount of
+		// time while calling CancelRequest
+		case <-t.finishCancel:
+			return nil, errors.New("cancelled")
+		}
+	}
+}
+
+func (t *fakeTransport) CancelRequest(*http.Request) {
+	t.startCancel <- struct{}{}
+}
+
+type fakeAction struct{}
+
+func (a *fakeAction) HTTPRequest(url.URL) *http.Request {
+	return &http.Request{}
+}
+
+func TestSimpleHTTPClientDoSuccess(t *testing.T) {
+	tr := newFakeTransport()
+	c := &simpleHTTPClient{transport: tr}
+
+	tr.respchan <- &http.Response{
+		StatusCode: http.StatusTeapot,
+		Body:       ioutil.NopCloser(strings.NewReader("foo")),
+	}
+
+	resp, body, err := c.Do(context.Background(), &fakeAction{})
+	if err != nil {
+		t.Fatalf("incorrect error value: want=nil got=%v", err)
+	}
+
+	wantCode := http.StatusTeapot
+	if wantCode != resp.StatusCode {
+		t.Fatalf("invalid response code: want=%d got=%d", wantCode, resp.StatusCode)
+	}
+
+	wantBody := []byte("foo")
+	if !reflect.DeepEqual(wantBody, body) {
+		t.Fatalf("invalid response body: want=%q got=%q", wantBody, body)
+	}
+}
+
+func TestSimpleHTTPClientDoError(t *testing.T) {
+	tr := newFakeTransport()
+	c := &simpleHTTPClient{transport: tr}
+
+	tr.errchan <- errors.New("fixture")
+
+	_, _, err := c.Do(context.Background(), &fakeAction{})
+	if err == nil {
+		t.Fatalf("expected non-nil error, got nil")
+	}
+}
+
+func TestSimpleHTTPClientDoCancelContext(t *testing.T) {
+	tr := newFakeTransport()
+	c := &simpleHTTPClient{transport: tr}
+
+	tr.startCancel <- struct{}{}
+	tr.finishCancel <- struct{}{}
+
+	_, _, err := c.Do(context.Background(), &fakeAction{})
+	if err == nil {
+		t.Fatalf("expected non-nil error, got nil")
+	}
+}
+
+type checkableReadCloser struct {
+	io.ReadCloser
+	closed bool
+}
+
+func (c *checkableReadCloser) Close() error {
+	if !c.closed {
+		c.closed = true
+		return c.ReadCloser.Close()
+	}
+	return nil
+}
+
+func TestSimpleHTTPClientDoCancelContextResponseBodyClosed(t *testing.T) {
+	tr := newFakeTransport()
+	c := &simpleHTTPClient{transport: tr}
+
+	// create an already-cancelled context
+	ctx, cancel := context.WithCancel(context.Background())
+	cancel()
+
+	body := &checkableReadCloser{ReadCloser: ioutil.NopCloser(strings.NewReader("foo"))}
+	go func() {
+		// wait that simpleHTTPClient knows the context is already timed out,
+		// and calls CancelRequest
+		testutil.WaitSchedule()
+
+		// response is returned before cancel effects
+		tr.respchan <- &http.Response{Body: body}
+	}()
+
+	_, _, err := c.Do(ctx, &fakeAction{})
+	if err == nil {
+		t.Fatalf("expected non-nil error, got nil")
+	}
+
+	if !body.closed {
+		t.Fatalf("expected closed body")
+	}
+}
+
+type blockingBody struct {
+	c chan struct{}
+}
+
+func (bb *blockingBody) Read(p []byte) (n int, err error) {
+	<-bb.c
+	return 0, errors.New("closed")
+}
+
+func (bb *blockingBody) Close() error {
+	close(bb.c)
+	return nil
+}
+
+func TestSimpleHTTPClientDoCancelContextResponseBodyClosedWithBlockingBody(t *testing.T) {
+	tr := newFakeTransport()
+	c := &simpleHTTPClient{transport: tr}
+
+	ctx, cancel := context.WithCancel(context.Background())
+	body := &checkableReadCloser{ReadCloser: &blockingBody{c: make(chan struct{})}}
+	go func() {
+		tr.respchan <- &http.Response{Body: body}
+		time.Sleep(2 * time.Millisecond)
+		// cancel after the body is received
+		cancel()
+	}()
+
+	_, _, err := c.Do(ctx, &fakeAction{})
+	if err == nil {
+		t.Fatalf("expected non-nil error, got nil")
+	}
+
+	if !body.closed {
+		t.Fatalf("expected closed body")
+	}
+}
+
+func TestSimpleHTTPClientDoCancelContextWaitForRoundTrip(t *testing.T) {
+	tr := newFakeTransport()
+	c := &simpleHTTPClient{transport: tr}
+
+	donechan := make(chan struct{})
+	ctx, cancel := context.WithCancel(context.Background())
+	go func() {
+		c.Do(ctx, &fakeAction{})
+		close(donechan)
+	}()
+
+	// This should call CancelRequest and begin the cancellation process
+	cancel()
+
+	select {
+	case <-donechan:
+		t.Fatalf("simpleHTTPClient.Do should not have exited yet")
+	default:
+	}
+
+	tr.finishCancel <- struct{}{}
+
+	select {
+	case <-donechan:
+		//expected behavior
+		return
+	case <-time.After(time.Second):
+		t.Fatalf("simpleHTTPClient.Do did not exit within 1s")
+	}
+}
+
+func TestHTTPClusterClientDo(t *testing.T) {
+	fakeErr := errors.New("fake!")
+	fakeURL := url.URL{}
+	tests := []struct {
+		client     *httpClusterClient
+		wantCode   int
+		wantErr    error
+		wantPinned int
+	}{
+		// first good response short-circuits Do
+		{
+			client: &httpClusterClient{
+				endpoints: []url.URL{fakeURL, fakeURL},
+				clientFactory: newStaticHTTPClientFactory(
+					[]staticHTTPResponse{
+						staticHTTPResponse{resp: http.Response{StatusCode: http.StatusTeapot}},
+						staticHTTPResponse{err: fakeErr},
+					},
+				),
+				rand: rand.New(rand.NewSource(0)),
+			},
+			wantCode: http.StatusTeapot,
+		},
+
+		// fall through to good endpoint if err is arbitrary
+		{
+			client: &httpClusterClient{
+				endpoints: []url.URL{fakeURL, fakeURL},
+				clientFactory: newStaticHTTPClientFactory(
+					[]staticHTTPResponse{
+						staticHTTPResponse{err: fakeErr},
+						staticHTTPResponse{resp: http.Response{StatusCode: http.StatusTeapot}},
+					},
+				),
+				rand: rand.New(rand.NewSource(0)),
+			},
+			wantCode:   http.StatusTeapot,
+			wantPinned: 1,
+		},
+
+		// context.DeadlineExceeded short-circuits Do
+		{
+			client: &httpClusterClient{
+				endpoints: []url.URL{fakeURL, fakeURL},
+				clientFactory: newStaticHTTPClientFactory(
+					[]staticHTTPResponse{
+						staticHTTPResponse{err: context.DeadlineExceeded},
+						staticHTTPResponse{resp: http.Response{StatusCode: http.StatusTeapot}},
+					},
+				),
+				rand: rand.New(rand.NewSource(0)),
+			},
+			wantErr: &ClusterError{Errors: []error{context.DeadlineExceeded}},
+		},
+
+		// context.Canceled short-circuits Do
+		{
+			client: &httpClusterClient{
+				endpoints: []url.URL{fakeURL, fakeURL},
+				clientFactory: newStaticHTTPClientFactory(
+					[]staticHTTPResponse{
+						staticHTTPResponse{err: context.Canceled},
+						staticHTTPResponse{resp: http.Response{StatusCode: http.StatusTeapot}},
+					},
+				),
+				rand: rand.New(rand.NewSource(0)),
+			},
+			wantErr: &ClusterError{Errors: []error{context.Canceled}},
+		},
+
+		// return err if there are no endpoints
+		{
+			client: &httpClusterClient{
+				endpoints:     []url.URL{},
+				clientFactory: newHTTPClientFactory(nil, nil),
+				rand:          rand.New(rand.NewSource(0)),
+			},
+			wantErr: ErrNoEndpoints,
+		},
+
+		// return err if all endpoints return arbitrary errors
+		{
+			client: &httpClusterClient{
+				endpoints: []url.URL{fakeURL, fakeURL},
+				clientFactory: newStaticHTTPClientFactory(
+					[]staticHTTPResponse{
+						staticHTTPResponse{err: fakeErr},
+						staticHTTPResponse{err: fakeErr},
+					},
+				),
+				rand: rand.New(rand.NewSource(0)),
+			},
+			wantErr: &ClusterError{Errors: []error{fakeErr, fakeErr}},
+		},
+
+		// 500-level errors cause Do to fallthrough to next endpoint
+		{
+			client: &httpClusterClient{
+				endpoints: []url.URL{fakeURL, fakeURL},
+				clientFactory: newStaticHTTPClientFactory(
+					[]staticHTTPResponse{
+						staticHTTPResponse{resp: http.Response{StatusCode: http.StatusBadGateway}},
+						staticHTTPResponse{resp: http.Response{StatusCode: http.StatusTeapot}},
+					},
+				),
+				rand: rand.New(rand.NewSource(0)),
+			},
+			wantCode:   http.StatusTeapot,
+			wantPinned: 1,
+		},
+	}
+
+	for i, tt := range tests {
+		resp, _, err := tt.client.Do(context.Background(), nil)
+		if !reflect.DeepEqual(tt.wantErr, err) {
+			t.Errorf("#%d: got err=%v, want=%v", i, err, tt.wantErr)
+			continue
+		}
+
+		if resp == nil {
+			if tt.wantCode != 0 {
+				t.Errorf("#%d: resp is nil, want=%d", i, tt.wantCode)
+			}
+			continue
+		}
+
+		if resp.StatusCode != tt.wantCode {
+			t.Errorf("#%d: resp code=%d, want=%d", i, resp.StatusCode, tt.wantCode)
+			continue
+		}
+
+		if tt.client.pinned != tt.wantPinned {
+			t.Errorf("#%d: pinned=%d, want=%d", i, tt.client.pinned, tt.wantPinned)
+		}
+	}
+}
+
+func TestRedirectedHTTPAction(t *testing.T) {
+	act := &redirectedHTTPAction{
+		action: &staticHTTPAction{
+			request: http.Request{
+				Method: "DELETE",
+				URL: &url.URL{
+					Scheme: "https",
+					Host:   "foo.example.com",
+					Path:   "/ping",
+				},
+			},
+		},
+		location: url.URL{
+			Scheme: "https",
+			Host:   "bar.example.com",
+			Path:   "/pong",
+		},
+	}
+
+	want := &http.Request{
+		Method: "DELETE",
+		URL: &url.URL{
+			Scheme: "https",
+			Host:   "bar.example.com",
+			Path:   "/pong",
+		},
+	}
+	got := act.HTTPRequest(url.URL{Scheme: "http", Host: "baz.example.com", Path: "/pang"})
+
+	if !reflect.DeepEqual(want, got) {
+		t.Fatalf("HTTPRequest is %#v, want %#v", want, got)
+	}
+}
+
+func TestRedirectFollowingHTTPClient(t *testing.T) {
+	tests := []struct {
+		checkRedirect CheckRedirectFunc
+		client        httpClient
+		wantCode      int
+		wantErr       error
+	}{
+		// errors bubbled up
+		{
+			checkRedirect: func(int) error { return ErrTooManyRedirects },
+			client: &multiStaticHTTPClient{
+				responses: []staticHTTPResponse{
+					staticHTTPResponse{
+						err: errors.New("fail!"),
+					},
+				},
+			},
+			wantErr: errors.New("fail!"),
+		},
+
+		// no need to follow redirect if none given
+		{
+			checkRedirect: func(int) error { return ErrTooManyRedirects },
+			client: &multiStaticHTTPClient{
+				responses: []staticHTTPResponse{
+					staticHTTPResponse{
+						resp: http.Response{
+							StatusCode: http.StatusTeapot,
+						},
+					},
+				},
+			},
+			wantCode: http.StatusTeapot,
+		},
+
+		// redirects if less than max
+		{
+			checkRedirect: func(via int) error {
+				if via >= 2 {
+					return ErrTooManyRedirects
+				}
+				return nil
+			},
+			client: &multiStaticHTTPClient{
+				responses: []staticHTTPResponse{
+					staticHTTPResponse{
+						resp: http.Response{
+							StatusCode: http.StatusTemporaryRedirect,
+							Header:     http.Header{"Location": []string{"http://example.com"}},
+						},
+					},
+					staticHTTPResponse{
+						resp: http.Response{
+							StatusCode: http.StatusTeapot,
+						},
+					},
+				},
+			},
+			wantCode: http.StatusTeapot,
+		},
+
+		// succeed after reaching max redirects
+		{
+			checkRedirect: func(via int) error {
+				if via >= 3 {
+					return ErrTooManyRedirects
+				}
+				return nil
+			},
+			client: &multiStaticHTTPClient{
+				responses: []staticHTTPResponse{
+					staticHTTPResponse{
+						resp: http.Response{
+							StatusCode: http.StatusTemporaryRedirect,
+							Header:     http.Header{"Location": []string{"http://example.com"}},
+						},
+					},
+					staticHTTPResponse{
+						resp: http.Response{
+							StatusCode: http.StatusTemporaryRedirect,
+							Header:     http.Header{"Location": []string{"http://example.com"}},
+						},
+					},
+					staticHTTPResponse{
+						resp: http.Response{
+							StatusCode: http.StatusTeapot,
+						},
+					},
+				},
+			},
+			wantCode: http.StatusTeapot,
+		},
+
+		// fail if too many redirects
+		{
+			checkRedirect: func(via int) error {
+				if via >= 2 {
+					return ErrTooManyRedirects
+				}
+				return nil
+			},
+			client: &multiStaticHTTPClient{
+				responses: []staticHTTPResponse{
+					staticHTTPResponse{
+						resp: http.Response{
+							StatusCode: http.StatusTemporaryRedirect,
+							Header:     http.Header{"Location": []string{"http://example.com"}},
+						},
+					},
+					staticHTTPResponse{
+						resp: http.Response{
+							StatusCode: http.StatusTemporaryRedirect,
+							Header:     http.Header{"Location": []string{"http://example.com"}},
+						},
+					},
+					staticHTTPResponse{
+						resp: http.Response{
+							StatusCode: http.StatusTeapot,
+						},
+					},
+				},
+			},
+			wantErr: ErrTooManyRedirects,
+		},
+
+		// fail if Location header not set
+		{
+			checkRedirect: func(int) error { return ErrTooManyRedirects },
+			client: &multiStaticHTTPClient{
+				responses: []staticHTTPResponse{
+					staticHTTPResponse{
+						resp: http.Response{
+							StatusCode: http.StatusTemporaryRedirect,
+						},
+					},
+				},
+			},
+			wantErr: errors.New("Location header not set"),
+		},
+
+		// fail if Location header is invalid
+		{
+			checkRedirect: func(int) error { return ErrTooManyRedirects },
+			client: &multiStaticHTTPClient{
+				responses: []staticHTTPResponse{
+					staticHTTPResponse{
+						resp: http.Response{
+							StatusCode: http.StatusTemporaryRedirect,
+							Header:     http.Header{"Location": []string{":"}},
+						},
+					},
+				},
+			},
+			wantErr: errors.New("Location header not valid URL: :"),
+		},
+
+		// fail if redirects checked way too many times
+		{
+			checkRedirect: func(int) error { return nil },
+			client: &staticHTTPClient{
+				resp: http.Response{
+					StatusCode: http.StatusTemporaryRedirect,
+					Header:     http.Header{"Location": []string{"http://example.com"}},
+				},
+			},
+			wantErr: errTooManyRedirectChecks,
+		},
+	}
+
+	for i, tt := range tests {
+		client := &redirectFollowingHTTPClient{client: tt.client, checkRedirect: tt.checkRedirect}
+		resp, _, err := client.Do(context.Background(), nil)
+		if !reflect.DeepEqual(tt.wantErr, err) {
+			t.Errorf("#%d: got err=%v, want=%v", i, err, tt.wantErr)
+			continue
+		}
+
+		if resp == nil {
+			if tt.wantCode != 0 {
+				t.Errorf("#%d: resp is nil, want=%d", i, tt.wantCode)
+			}
+			continue
+		}
+
+		if resp.StatusCode != tt.wantCode {
+			t.Errorf("#%d: resp code=%d, want=%d", i, resp.StatusCode, tt.wantCode)
+			continue
+		}
+	}
+}
+
+func TestDefaultCheckRedirect(t *testing.T) {
+	tests := []struct {
+		num int
+		err error
+	}{
+		{0, nil},
+		{5, nil},
+		{10, nil},
+		{11, ErrTooManyRedirects},
+		{29, ErrTooManyRedirects},
+	}
+
+	for i, tt := range tests {
+		err := DefaultCheckRedirect(tt.num)
+		if !reflect.DeepEqual(tt.err, err) {
+			t.Errorf("#%d: want=%#v got=%#v", i, tt.err, err)
+		}
+	}
+}
+
+func TestHTTPClusterClientSync(t *testing.T) {
+	cf := newStaticHTTPClientFactory([]staticHTTPResponse{
+		staticHTTPResponse{
+			resp: http.Response{StatusCode: http.StatusOK, Header: http.Header{"Content-Type": []string{"application/json"}}},
+			body: []byte(`{"members":[{"id":"2745e2525fce8fe","peerURLs":["http://127.0.0.1:7003"],"name":"node3","clientURLs":["http://127.0.0.1:4003"]},{"id":"42134f434382925","peerURLs":["http://127.0.0.1:2380","http://127.0.0.1:7001"],"name":"node1","clientURLs":["http://127.0.0.1:2379","http://127.0.0.1:4001"]},{"id":"94088180e21eb87b","peerURLs":["http://127.0.0.1:7002"],"name":"node2","clientURLs":["http://127.0.0.1:4002"]}]}`),
+		},
+	})
+
+	hc := &httpClusterClient{
+		clientFactory: cf,
+		rand:          rand.New(rand.NewSource(0)),
+	}
+	err := hc.reset([]string{"http://127.0.0.1:2379"})
+	if err != nil {
+		t.Fatalf("unexpected error during setup: %#v", err)
+	}
+
+	want := []string{"http://127.0.0.1:2379"}
+	got := hc.Endpoints()
+	if !reflect.DeepEqual(want, got) {
+		t.Fatalf("incorrect endpoints: want=%#v got=%#v", want, got)
+	}
+
+	err = hc.Sync(context.Background())
+	if err != nil {
+		t.Fatalf("unexpected error during Sync: %#v", err)
+	}
+
+	want = []string{"http://127.0.0.1:2379", "http://127.0.0.1:4001", "http://127.0.0.1:4002", "http://127.0.0.1:4003"}
+	got = hc.Endpoints()
+	sort.Sort(sort.StringSlice(got))
+	if !reflect.DeepEqual(want, got) {
+		t.Fatalf("incorrect endpoints post-Sync: want=%#v got=%#v", want, got)
+	}
+
+	err = hc.reset([]string{"http://127.0.0.1:4009"})
+	if err != nil {
+		t.Fatalf("unexpected error during reset: %#v", err)
+	}
+
+	want = []string{"http://127.0.0.1:4009"}
+	got = hc.Endpoints()
+	if !reflect.DeepEqual(want, got) {
+		t.Fatalf("incorrect endpoints post-reset: want=%#v got=%#v", want, got)
+	}
+}
+
+func TestHTTPClusterClientSyncFail(t *testing.T) {
+	cf := newStaticHTTPClientFactory([]staticHTTPResponse{
+		staticHTTPResponse{err: errors.New("fail!")},
+	})
+
+	hc := &httpClusterClient{
+		clientFactory: cf,
+		rand:          rand.New(rand.NewSource(0)),
+	}
+	err := hc.reset([]string{"http://127.0.0.1:2379"})
+	if err != nil {
+		t.Fatalf("unexpected error during setup: %#v", err)
+	}
+
+	want := []string{"http://127.0.0.1:2379"}
+	got := hc.Endpoints()
+	if !reflect.DeepEqual(want, got) {
+		t.Fatalf("incorrect endpoints: want=%#v got=%#v", want, got)
+	}
+
+	err = hc.Sync(context.Background())
+	if err == nil {
+		t.Fatalf("got nil error during Sync")
+	}
+
+	got = hc.Endpoints()
+	if !reflect.DeepEqual(want, got) {
+		t.Fatalf("incorrect endpoints after failed Sync: want=%#v got=%#v", want, got)
+	}
+}
+
+func TestHTTPClusterClientResetFail(t *testing.T) {
+	tests := [][]string{
+		// need at least one endpoint
+		[]string{},
+
+		// urls must be valid
+		[]string{":"},
+	}
+
+	for i, tt := range tests {
+		hc := &httpClusterClient{rand: rand.New(rand.NewSource(0))}
+		err := hc.reset(tt)
+		if err == nil {
+			t.Errorf("#%d: expected non-nil error", i)
+		}
+	}
+}
+
+func TestHTTPClusterClientResetPinRandom(t *testing.T) {
+	round := 2000
+	pinNum := 0
+	for i := 0; i < round; i++ {
+		hc := &httpClusterClient{rand: rand.New(rand.NewSource(int64(i)))}
+		err := hc.reset([]string{"http://127.0.0.1:4001", "http://127.0.0.1:4002", "http://127.0.0.1:4003"})
+		if err != nil {
+			t.Fatalf("#%d: reset error (%v)", i, err)
+		}
+		if hc.endpoints[hc.pinned].String() == "http://127.0.0.1:4001" {
+			pinNum++
+		}
+	}
+
+	min := 1.0/3.0 - 0.05
+	max := 1.0/3.0 + 0.05
+	if ratio := float64(pinNum) / float64(round); ratio > max || ratio < min {
+		t.Errorf("pinned ratio = %v, want [%v, %v]", ratio, min, max)
+	}
+}

+ 33 - 0
Godeps/_workspace/src/github.com/coreos/etcd/client/cluster_error.go

@@ -0,0 +1,33 @@
+// Copyright 2015 CoreOS, Inc.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package client
+
+import "fmt"
+
+type ClusterError struct {
+	Errors []error
+}
+
+func (ce *ClusterError) Error() string {
+	return ErrClusterUnavailable.Error()
+}
+
+func (ce *ClusterError) Detail() string {
+	s := ""
+	for i, e := range ce.Errors {
+		s += fmt.Sprintf("error #%d: %s\n", i, e)
+	}
+	return s
+}

+ 70 - 0
Godeps/_workspace/src/github.com/coreos/etcd/client/curl.go

@@ -0,0 +1,70 @@
+// Copyright 2015 CoreOS, Inc.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package client
+
+import (
+	"bytes"
+	"fmt"
+	"io/ioutil"
+	"net/http"
+	"os"
+)
+
+var (
+	cURLDebug = false
+)
+
+func EnablecURLDebug() {
+	cURLDebug = true
+}
+
+func DisablecURLDebug() {
+	cURLDebug = false
+}
+
+// printcURL prints the cURL equivalent request to stderr.
+// It returns an error if the body of the request cannot
+// be read.
+// The caller MUST cancel the request if there is an error.
+func printcURL(req *http.Request) error {
+	if !cURLDebug {
+		return nil
+	}
+	var (
+		command string
+		b       []byte
+		err     error
+	)
+
+	if req.URL != nil {
+		command = fmt.Sprintf("curl -X %s %s", req.Method, req.URL.String())
+	}
+
+	if req.Body != nil {
+		b, err = ioutil.ReadAll(req.Body)
+		if err != nil {
+			return err
+		}
+		command += fmt.Sprintf(" -d %q", string(b))
+	}
+
+	fmt.Fprintf(os.Stderr, "cURL Command: %s\n", command)
+
+	// reset body
+	body := bytes.NewBuffer(b)
+	req.Body = ioutil.NopCloser(body)
+
+	return nil
+}

+ 21 - 0
Godeps/_workspace/src/github.com/coreos/etcd/client/discover.go

@@ -0,0 +1,21 @@
+// Copyright 2015 CoreOS, Inc.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package client
+
+// Discoverer is an interface that wraps the Discover method.
+type Discoverer interface {
+	// Dicover looks up the etcd servers for the domain.
+	Discover(domain string) ([]string, error)
+}

+ 71 - 0
Godeps/_workspace/src/github.com/coreos/etcd/client/doc.go

@@ -0,0 +1,71 @@
+// Copyright 2015 CoreOS, Inc.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+/*
+Package client provides bindings for the etcd APIs.
+
+Create a Config and exchange it for a Client:
+
+	import (
+		"net/http"
+
+		"github.com/coreos/etcd/client"
+		"golang.org/x/net/context"
+	)
+
+	cfg := client.Config{
+		Endpoints: []string{"http://127.0.0.1:2379"},
+		Transport: DefaultTransport,
+	}
+
+	c, err := client.New(cfg)
+	if err != nil {
+		// handle error
+	}
+
+Create a KeysAPI using the Client, then use it to interact with etcd:
+
+	kAPI := client.NewKeysAPI(c)
+
+	// create a new key /foo with the value "bar"
+	_, err = kAPI.Create(context.Background(), "/foo", "bar")
+	if err != nil {
+		// handle error
+	}
+
+	// delete the newly created key only if the value is still "bar"
+	_, err = kAPI.Delete(context.Background(), "/foo", &DeleteOptions{PrevValue: "bar"})
+	if err != nil {
+		// handle error
+	}
+
+Use a custom context to set timeouts on your operations:
+
+	import "time"
+
+	ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
+	defer cancel()
+
+	// set a new key, ignoring it's previous state
+	_, err := kAPI.Set(ctx, "/ping", "pong", nil)
+	if err != nil {
+		if err == context.DeadlineExceeded {
+			// request took longer than 5s
+		} else {
+			// handle error
+		}
+	}
+
+*/
+package client

+ 631 - 0
Godeps/_workspace/src/github.com/coreos/etcd/client/keys.go

@@ -0,0 +1,631 @@
+// Copyright 2015 CoreOS, Inc.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package client
+
+import (
+	"encoding/json"
+	"errors"
+	"fmt"
+	"net/http"
+	"net/url"
+	"path"
+	"strconv"
+	"strings"
+	"time"
+
+	"github.com/coreos/flannel/Godeps/_workspace/src/golang.org/x/net/context"
+)
+
+const (
+	ErrorCodeKeyNotFound = 100
+	ErrorCodeTestFailed  = 101
+	ErrorCodeNotFile     = 102
+	ErrorCodeNotDir      = 104
+	ErrorCodeNodeExist   = 105
+	ErrorCodeRootROnly   = 107
+	ErrorCodeDirNotEmpty = 108
+
+	ErrorCodePrevValueRequired = 201
+	ErrorCodeTTLNaN            = 202
+	ErrorCodeIndexNaN          = 203
+	ErrorCodeInvalidField      = 209
+	ErrorCodeInvalidForm       = 210
+
+	ErrorCodeRaftInternal = 300
+	ErrorCodeLeaderElect  = 301
+
+	ErrorCodeWatcherCleared    = 400
+	ErrorCodeEventIndexCleared = 401
+)
+
+type Error struct {
+	Code    int    `json:"errorCode"`
+	Message string `json:"message"`
+	Cause   string `json:"cause"`
+	Index   uint64 `json:"index"`
+}
+
+func (e Error) Error() string {
+	return fmt.Sprintf("%v: %v (%v) [%v]", e.Code, e.Message, e.Cause, e.Index)
+}
+
+var (
+	ErrInvalidJSON = errors.New("client: response is invalid json. The endpoint is probably not valid etcd cluster endpoint.")
+	ErrEmptyBody   = errors.New("client: response body is empty")
+)
+
+// PrevExistType is used to define an existence condition when setting
+// or deleting Nodes.
+type PrevExistType string
+
+const (
+	PrevIgnore  = PrevExistType("")
+	PrevExist   = PrevExistType("true")
+	PrevNoExist = PrevExistType("false")
+)
+
+var (
+	defaultV2KeysPrefix = "/v2/keys"
+)
+
+// NewKeysAPI builds a KeysAPI that interacts with etcd's key-value
+// API over HTTP.
+func NewKeysAPI(c Client) KeysAPI {
+	return NewKeysAPIWithPrefix(c, defaultV2KeysPrefix)
+}
+
+// NewKeysAPIWithPrefix acts like NewKeysAPI, but allows the caller
+// to provide a custom base URL path. This should only be used in
+// very rare cases.
+func NewKeysAPIWithPrefix(c Client, p string) KeysAPI {
+	return &httpKeysAPI{
+		client: c,
+		prefix: p,
+	}
+}
+
+type KeysAPI interface {
+	// Get retrieves a set of Nodes from etcd
+	Get(ctx context.Context, key string, opts *GetOptions) (*Response, error)
+
+	// Set assigns a new value to a Node identified by a given key. The caller
+	// may define a set of conditions in the SetOptions. If SetOptions.Dir=true
+	// than value is ignored.
+	Set(ctx context.Context, key, value string, opts *SetOptions) (*Response, error)
+
+	// Delete removes a Node identified by the given key, optionally destroying
+	// all of its children as well. The caller may define a set of required
+	// conditions in an DeleteOptions object.
+	Delete(ctx context.Context, key string, opts *DeleteOptions) (*Response, error)
+
+	// Create is an alias for Set w/ PrevExist=false
+	Create(ctx context.Context, key, value string) (*Response, error)
+
+	// CreateInOrder is used to atomically create in-order keys within the given directory.
+	CreateInOrder(ctx context.Context, dir, value string, opts *CreateInOrderOptions) (*Response, error)
+
+	// Update is an alias for Set w/ PrevExist=true
+	Update(ctx context.Context, key, value string) (*Response, error)
+
+	// Watcher builds a new Watcher targeted at a specific Node identified
+	// by the given key. The Watcher may be configured at creation time
+	// through a WatcherOptions object. The returned Watcher is designed
+	// to emit events that happen to a Node, and optionally to its children.
+	Watcher(key string, opts *WatcherOptions) Watcher
+}
+
+type WatcherOptions struct {
+	// AfterIndex defines the index after-which the Watcher should
+	// start emitting events. For example, if a value of 5 is
+	// provided, the first event will have an index >= 6.
+	//
+	// Setting AfterIndex to 0 (default) means that the Watcher
+	// should start watching for events starting at the current
+	// index, whatever that may be.
+	AfterIndex uint64
+
+	// Recursive specifices whether or not the Watcher should emit
+	// events that occur in children of the given keyspace. If set
+	// to false (default), events will be limited to those that
+	// occur for the exact key.
+	Recursive bool
+}
+
+type CreateInOrderOptions struct {
+	// TTL defines a period of time after-which the Node should
+	// expire and no longer exist. Values <= 0 are ignored. Given
+	// that the zero-value is ignored, TTL cannot be used to set
+	// a TTL of 0.
+	TTL time.Duration
+}
+
+type SetOptions struct {
+	// PrevValue specifies what the current value of the Node must
+	// be in order for the Set operation to succeed.
+	//
+	// Leaving this field empty means that the caller wishes to
+	// ignore the current value of the Node. This cannot be used
+	// to compare the Node's current value to an empty string.
+	//
+	// PrevValue is ignored if Dir=true
+	PrevValue string
+
+	// PrevIndex indicates what the current ModifiedIndex of the
+	// Node must be in order for the Set operation to succeed.
+	//
+	// If PrevIndex is set to 0 (default), no comparison is made.
+	PrevIndex uint64
+
+	// PrevExist specifies whether the Node must currently exist
+	// (PrevExist) or not (PrevNoExist). If the caller does not
+	// care about existence, set PrevExist to PrevIgnore, or simply
+	// leave it unset.
+	PrevExist PrevExistType
+
+	// TTL defines a period of time after-which the Node should
+	// expire and no longer exist. Values <= 0 are ignored. Given
+	// that the zero-value is ignored, TTL cannot be used to set
+	// a TTL of 0.
+	TTL time.Duration
+
+	// Dir specifies whether or not this Node should be created as a directory.
+	Dir bool
+}
+
+type GetOptions struct {
+	// Recursive defines whether or not all children of the Node
+	// should be returned.
+	Recursive bool
+
+	// Sort instructs the server whether or not to sort the Nodes.
+	// If true, the Nodes are sorted alphabetically by key in
+	// ascending order (A to z). If false (default), the Nodes will
+	// not be sorted and the ordering used should not be considered
+	// predictable.
+	Sort bool
+
+	// Quorum specifies whether it gets the latest committed value that
+	// has been applied in quorum of members, which ensures external
+	// consistency (or linearizability).
+	Quorum bool
+}
+
+type DeleteOptions struct {
+	// PrevValue specifies what the current value of the Node must
+	// be in order for the Delete operation to succeed.
+	//
+	// Leaving this field empty means that the caller wishes to
+	// ignore the current value of the Node. This cannot be used
+	// to compare the Node's current value to an empty string.
+	PrevValue string
+
+	// PrevIndex indicates what the current ModifiedIndex of the
+	// Node must be in order for the Delete operation to succeed.
+	//
+	// If PrevIndex is set to 0 (default), no comparison is made.
+	PrevIndex uint64
+
+	// Recursive defines whether or not all children of the Node
+	// should be deleted. If set to true, all children of the Node
+	// identified by the given key will be deleted. If left unset
+	// or explicitly set to false, only a single Node will be
+	// deleted.
+	Recursive bool
+
+	// Dir specifies whether or not this Node should be removed as a directory.
+	Dir bool
+}
+
+type Watcher interface {
+	// Next blocks until an etcd event occurs, then returns a Response
+	// represeting that event. The behavior of Next depends on the
+	// WatcherOptions used to construct the Watcher. Next is designed to
+	// be called repeatedly, each time blocking until a subsequent event
+	// is available.
+	//
+	// If the provided context is cancelled, Next will return a non-nil
+	// error. Any other failures encountered while waiting for the next
+	// event (connection issues, deserialization failures, etc) will
+	// also result in a non-nil error.
+	Next(context.Context) (*Response, error)
+}
+
+type Response struct {
+	// Action is the name of the operation that occurred. Possible values
+	// include get, set, delete, update, create, compareAndSwap,
+	// compareAndDelete and expire.
+	Action string `json:"action"`
+
+	// Node represents the state of the relevant etcd Node.
+	Node *Node `json:"node"`
+
+	// PrevNode represents the previous state of the Node. PrevNode is non-nil
+	// only if the Node existed before the action occured and the action
+	// caused a change to the Node.
+	PrevNode *Node `json:"prevNode"`
+
+	// Index holds the cluster-level index at the time the Response was generated.
+	// This index is not tied to the Node(s) contained in this Response.
+	Index uint64 `json:"-"`
+}
+
+type Node struct {
+	// Key represents the unique location of this Node (e.g. "/foo/bar").
+	Key string `json:"key"`
+
+	// Dir reports whether node describes a directory.
+	Dir bool `json:"dir,omitempty"`
+
+	// Value is the current data stored on this Node. If this Node
+	// is a directory, Value will be empty.
+	Value string `json:"value"`
+
+	// Nodes holds the children of this Node, only if this Node is a directory.
+	// This slice of will be arbitrarily deep (children, grandchildren, great-
+	// grandchildren, etc.) if a recursive Get or Watch request were made.
+	Nodes []*Node `json:"nodes"`
+
+	// CreatedIndex is the etcd index at-which this Node was created.
+	CreatedIndex uint64 `json:"createdIndex"`
+
+	// ModifiedIndex is the etcd index at-which this Node was last modified.
+	ModifiedIndex uint64 `json:"modifiedIndex"`
+
+	// Expiration is the server side expiration time of the key.
+	Expiration *time.Time `json:"expiration,omitempty"`
+
+	// TTL is the time to live of the key in second.
+	TTL int64 `json:"ttl,omitempty"`
+}
+
+func (n *Node) String() string {
+	return fmt.Sprintf("{Key: %s, CreatedIndex: %d, ModifiedIndex: %d, TTL: %d}", n.Key, n.CreatedIndex, n.ModifiedIndex, n.TTL)
+}
+
+// TTLDuration returns the Node's TTL as a time.Duration object
+func (n *Node) TTLDuration() time.Duration {
+	return time.Duration(n.TTL) * time.Second
+}
+
+type httpKeysAPI struct {
+	client httpClient
+	prefix string
+}
+
+func (k *httpKeysAPI) Set(ctx context.Context, key, val string, opts *SetOptions) (*Response, error) {
+	act := &setAction{
+		Prefix: k.prefix,
+		Key:    key,
+		Value:  val,
+	}
+
+	if opts != nil {
+		act.PrevValue = opts.PrevValue
+		act.PrevIndex = opts.PrevIndex
+		act.PrevExist = opts.PrevExist
+		act.TTL = opts.TTL
+		act.Dir = opts.Dir
+	}
+
+	resp, body, err := k.client.Do(ctx, act)
+	if err != nil {
+		return nil, err
+	}
+
+	return unmarshalHTTPResponse(resp.StatusCode, resp.Header, body)
+}
+
+func (k *httpKeysAPI) Create(ctx context.Context, key, val string) (*Response, error) {
+	return k.Set(ctx, key, val, &SetOptions{PrevExist: PrevNoExist})
+}
+
+func (k *httpKeysAPI) CreateInOrder(ctx context.Context, dir, val string, opts *CreateInOrderOptions) (*Response, error) {
+	act := &createInOrderAction{
+		Prefix: k.prefix,
+		Dir:    dir,
+		Value:  val,
+	}
+
+	if opts != nil {
+		act.TTL = opts.TTL
+	}
+
+	resp, body, err := k.client.Do(ctx, act)
+	if err != nil {
+		return nil, err
+	}
+
+	return unmarshalHTTPResponse(resp.StatusCode, resp.Header, body)
+}
+
+func (k *httpKeysAPI) Update(ctx context.Context, key, val string) (*Response, error) {
+	return k.Set(ctx, key, val, &SetOptions{PrevExist: PrevExist})
+}
+
+func (k *httpKeysAPI) Delete(ctx context.Context, key string, opts *DeleteOptions) (*Response, error) {
+	act := &deleteAction{
+		Prefix: k.prefix,
+		Key:    key,
+	}
+
+	if opts != nil {
+		act.PrevValue = opts.PrevValue
+		act.PrevIndex = opts.PrevIndex
+		act.Dir = opts.Dir
+		act.Recursive = opts.Recursive
+	}
+
+	resp, body, err := k.client.Do(ctx, act)
+	if err != nil {
+		return nil, err
+	}
+
+	return unmarshalHTTPResponse(resp.StatusCode, resp.Header, body)
+}
+
+func (k *httpKeysAPI) Get(ctx context.Context, key string, opts *GetOptions) (*Response, error) {
+	act := &getAction{
+		Prefix: k.prefix,
+		Key:    key,
+	}
+
+	if opts != nil {
+		act.Recursive = opts.Recursive
+		act.Sorted = opts.Sort
+		act.Quorum = opts.Quorum
+	}
+
+	resp, body, err := k.client.Do(ctx, act)
+	if err != nil {
+		return nil, err
+	}
+
+	return unmarshalHTTPResponse(resp.StatusCode, resp.Header, body)
+}
+
+func (k *httpKeysAPI) Watcher(key string, opts *WatcherOptions) Watcher {
+	act := waitAction{
+		Prefix: k.prefix,
+		Key:    key,
+	}
+
+	if opts != nil {
+		act.Recursive = opts.Recursive
+		if opts.AfterIndex > 0 {
+			act.WaitIndex = opts.AfterIndex + 1
+		}
+	}
+
+	return &httpWatcher{
+		client:   k.client,
+		nextWait: act,
+	}
+}
+
+type httpWatcher struct {
+	client   httpClient
+	nextWait waitAction
+}
+
+func (hw *httpWatcher) Next(ctx context.Context) (*Response, error) {
+	for {
+		httpresp, body, err := hw.client.Do(ctx, &hw.nextWait)
+		if err != nil {
+			return nil, err
+		}
+
+		resp, err := unmarshalHTTPResponse(httpresp.StatusCode, httpresp.Header, body)
+		if err != nil {
+			if err == ErrEmptyBody {
+				continue
+			}
+			return nil, err
+		}
+
+		hw.nextWait.WaitIndex = resp.Node.ModifiedIndex + 1
+		return resp, nil
+	}
+}
+
+// v2KeysURL forms a URL representing the location of a key.
+// The endpoint argument represents the base URL of an etcd
+// server. The prefix is the path needed to route from the
+// provided endpoint's path to the root of the keys API
+// (typically "/v2/keys").
+func v2KeysURL(ep url.URL, prefix, key string) *url.URL {
+	ep.Path = path.Join(ep.Path, prefix, key)
+	return &ep
+}
+
+type getAction struct {
+	Prefix    string
+	Key       string
+	Recursive bool
+	Sorted    bool
+	Quorum    bool
+}
+
+func (g *getAction) HTTPRequest(ep url.URL) *http.Request {
+	u := v2KeysURL(ep, g.Prefix, g.Key)
+
+	params := u.Query()
+	params.Set("recursive", strconv.FormatBool(g.Recursive))
+	params.Set("sorted", strconv.FormatBool(g.Sorted))
+	params.Set("quorum", strconv.FormatBool(g.Quorum))
+	u.RawQuery = params.Encode()
+
+	req, _ := http.NewRequest("GET", u.String(), nil)
+	return req
+}
+
+type waitAction struct {
+	Prefix    string
+	Key       string
+	WaitIndex uint64
+	Recursive bool
+}
+
+func (w *waitAction) HTTPRequest(ep url.URL) *http.Request {
+	u := v2KeysURL(ep, w.Prefix, w.Key)
+
+	params := u.Query()
+	params.Set("wait", "true")
+	params.Set("waitIndex", strconv.FormatUint(w.WaitIndex, 10))
+	params.Set("recursive", strconv.FormatBool(w.Recursive))
+	u.RawQuery = params.Encode()
+
+	req, _ := http.NewRequest("GET", u.String(), nil)
+	return req
+}
+
+type setAction struct {
+	Prefix    string
+	Key       string
+	Value     string
+	PrevValue string
+	PrevIndex uint64
+	PrevExist PrevExistType
+	TTL       time.Duration
+	Dir       bool
+}
+
+func (a *setAction) HTTPRequest(ep url.URL) *http.Request {
+	u := v2KeysURL(ep, a.Prefix, a.Key)
+
+	params := u.Query()
+	form := url.Values{}
+
+	// we're either creating a directory or setting a key
+	if a.Dir {
+		params.Set("dir", strconv.FormatBool(a.Dir))
+	} else {
+		// These options are only valid for setting a key
+		if a.PrevValue != "" {
+			params.Set("prevValue", a.PrevValue)
+		}
+		form.Add("value", a.Value)
+	}
+
+	// Options which apply to both setting a key and creating a dir
+	if a.PrevIndex != 0 {
+		params.Set("prevIndex", strconv.FormatUint(a.PrevIndex, 10))
+	}
+	if a.PrevExist != PrevIgnore {
+		params.Set("prevExist", string(a.PrevExist))
+	}
+	if a.TTL > 0 {
+		form.Add("ttl", strconv.FormatUint(uint64(a.TTL.Seconds()), 10))
+	}
+
+	u.RawQuery = params.Encode()
+	body := strings.NewReader(form.Encode())
+
+	req, _ := http.NewRequest("PUT", u.String(), body)
+	req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
+
+	return req
+}
+
+type deleteAction struct {
+	Prefix    string
+	Key       string
+	PrevValue string
+	PrevIndex uint64
+	Dir       bool
+	Recursive bool
+}
+
+func (a *deleteAction) HTTPRequest(ep url.URL) *http.Request {
+	u := v2KeysURL(ep, a.Prefix, a.Key)
+
+	params := u.Query()
+	if a.PrevValue != "" {
+		params.Set("prevValue", a.PrevValue)
+	}
+	if a.PrevIndex != 0 {
+		params.Set("prevIndex", strconv.FormatUint(a.PrevIndex, 10))
+	}
+	if a.Dir {
+		params.Set("dir", "true")
+	}
+	if a.Recursive {
+		params.Set("recursive", "true")
+	}
+	u.RawQuery = params.Encode()
+
+	req, _ := http.NewRequest("DELETE", u.String(), nil)
+	req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
+
+	return req
+}
+
+type createInOrderAction struct {
+	Prefix string
+	Dir    string
+	Value  string
+	TTL    time.Duration
+}
+
+func (a *createInOrderAction) HTTPRequest(ep url.URL) *http.Request {
+	u := v2KeysURL(ep, a.Prefix, a.Dir)
+
+	form := url.Values{}
+	form.Add("value", a.Value)
+	if a.TTL > 0 {
+		form.Add("ttl", strconv.FormatUint(uint64(a.TTL.Seconds()), 10))
+	}
+	body := strings.NewReader(form.Encode())
+
+	req, _ := http.NewRequest("POST", u.String(), body)
+	req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
+	return req
+}
+
+func unmarshalHTTPResponse(code int, header http.Header, body []byte) (res *Response, err error) {
+	switch code {
+	case http.StatusOK, http.StatusCreated:
+		if len(body) == 0 {
+			return nil, ErrEmptyBody
+		}
+		res, err = unmarshalSuccessfulKeysResponse(header, body)
+	default:
+		err = unmarshalFailedKeysResponse(body)
+	}
+
+	return
+}
+
+func unmarshalSuccessfulKeysResponse(header http.Header, body []byte) (*Response, error) {
+	var res Response
+	err := json.Unmarshal(body, &res)
+	if err != nil {
+		return nil, ErrInvalidJSON
+	}
+	if header.Get("X-Etcd-Index") != "" {
+		res.Index, err = strconv.ParseUint(header.Get("X-Etcd-Index"), 10, 64)
+		if err != nil {
+			return nil, err
+		}
+	}
+	return &res, nil
+}
+
+func unmarshalFailedKeysResponse(body []byte) error {
+	var etcdErr Error
+	if err := json.Unmarshal(body, &etcdErr); err != nil {
+		return ErrInvalidJSON
+	}
+	return etcdErr
+}

+ 1393 - 0
Godeps/_workspace/src/github.com/coreos/etcd/client/keys_test.go

@@ -0,0 +1,1393 @@
+// Copyright 2015 CoreOS, Inc.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package client
+
+import (
+	"errors"
+	"fmt"
+	"io/ioutil"
+	"net/http"
+	"net/url"
+	"reflect"
+	"testing"
+	"time"
+
+	"github.com/coreos/flannel/Godeps/_workspace/src/golang.org/x/net/context"
+)
+
+func TestV2KeysURLHelper(t *testing.T) {
+	tests := []struct {
+		endpoint url.URL
+		prefix   string
+		key      string
+		want     url.URL
+	}{
+		// key is empty, no problem
+		{
+			endpoint: url.URL{Scheme: "http", Host: "example.com", Path: "/v2/keys"},
+			prefix:   "",
+			key:      "",
+			want:     url.URL{Scheme: "http", Host: "example.com", Path: "/v2/keys"},
+		},
+
+		// key is joined to path
+		{
+			endpoint: url.URL{Scheme: "http", Host: "example.com", Path: "/v2/keys"},
+			prefix:   "",
+			key:      "/foo/bar",
+			want:     url.URL{Scheme: "http", Host: "example.com", Path: "/v2/keys/foo/bar"},
+		},
+
+		// key is joined to path when path is empty
+		{
+			endpoint: url.URL{Scheme: "http", Host: "example.com", Path: ""},
+			prefix:   "",
+			key:      "/foo/bar",
+			want:     url.URL{Scheme: "http", Host: "example.com", Path: "/foo/bar"},
+		},
+
+		// Host field carries through with port
+		{
+			endpoint: url.URL{Scheme: "http", Host: "example.com:8080", Path: "/v2/keys"},
+			prefix:   "",
+			key:      "",
+			want:     url.URL{Scheme: "http", Host: "example.com:8080", Path: "/v2/keys"},
+		},
+
+		// Scheme carries through
+		{
+			endpoint: url.URL{Scheme: "https", Host: "example.com", Path: "/v2/keys"},
+			prefix:   "",
+			key:      "",
+			want:     url.URL{Scheme: "https", Host: "example.com", Path: "/v2/keys"},
+		},
+		// Prefix is applied
+		{
+			endpoint: url.URL{Scheme: "https", Host: "example.com", Path: "/foo"},
+			prefix:   "/bar",
+			key:      "/baz",
+			want:     url.URL{Scheme: "https", Host: "example.com", Path: "/foo/bar/baz"},
+		},
+	}
+
+	for i, tt := range tests {
+		got := v2KeysURL(tt.endpoint, tt.prefix, tt.key)
+		if tt.want != *got {
+			t.Errorf("#%d: want=%#v, got=%#v", i, tt.want, *got)
+		}
+	}
+}
+
+func TestGetAction(t *testing.T) {
+	ep := url.URL{Scheme: "http", Host: "example.com/v2/keys"}
+	baseWantURL := &url.URL{
+		Scheme: "http",
+		Host:   "example.com",
+		Path:   "/v2/keys/foo/bar",
+	}
+	wantHeader := http.Header{}
+
+	tests := []struct {
+		recursive bool
+		sorted    bool
+		quorum    bool
+		wantQuery string
+	}{
+		{
+			recursive: false,
+			sorted:    false,
+			quorum:    false,
+			wantQuery: "quorum=false&recursive=false&sorted=false",
+		},
+		{
+			recursive: true,
+			sorted:    false,
+			quorum:    false,
+			wantQuery: "quorum=false&recursive=true&sorted=false",
+		},
+		{
+			recursive: false,
+			sorted:    true,
+			quorum:    false,
+			wantQuery: "quorum=false&recursive=false&sorted=true",
+		},
+		{
+			recursive: true,
+			sorted:    true,
+			quorum:    false,
+			wantQuery: "quorum=false&recursive=true&sorted=true",
+		},
+		{
+			recursive: false,
+			sorted:    false,
+			quorum:    true,
+			wantQuery: "quorum=true&recursive=false&sorted=false",
+		},
+	}
+
+	for i, tt := range tests {
+		f := getAction{
+			Key:       "/foo/bar",
+			Recursive: tt.recursive,
+			Sorted:    tt.sorted,
+			Quorum:    tt.quorum,
+		}
+		got := *f.HTTPRequest(ep)
+
+		wantURL := baseWantURL
+		wantURL.RawQuery = tt.wantQuery
+
+		err := assertRequest(got, "GET", wantURL, wantHeader, nil)
+		if err != nil {
+			t.Errorf("#%d: %v", i, err)
+		}
+	}
+}
+
+func TestWaitAction(t *testing.T) {
+	ep := url.URL{Scheme: "http", Host: "example.com/v2/keys"}
+	baseWantURL := &url.URL{
+		Scheme: "http",
+		Host:   "example.com",
+		Path:   "/v2/keys/foo/bar",
+	}
+	wantHeader := http.Header{}
+
+	tests := []struct {
+		waitIndex uint64
+		recursive bool
+		wantQuery string
+	}{
+		{
+			recursive: false,
+			waitIndex: uint64(0),
+			wantQuery: "recursive=false&wait=true&waitIndex=0",
+		},
+		{
+			recursive: false,
+			waitIndex: uint64(12),
+			wantQuery: "recursive=false&wait=true&waitIndex=12",
+		},
+		{
+			recursive: true,
+			waitIndex: uint64(12),
+			wantQuery: "recursive=true&wait=true&waitIndex=12",
+		},
+	}
+
+	for i, tt := range tests {
+		f := waitAction{
+			Key:       "/foo/bar",
+			WaitIndex: tt.waitIndex,
+			Recursive: tt.recursive,
+		}
+		got := *f.HTTPRequest(ep)
+
+		wantURL := baseWantURL
+		wantURL.RawQuery = tt.wantQuery
+
+		err := assertRequest(got, "GET", wantURL, wantHeader, nil)
+		if err != nil {
+			t.Errorf("#%d: unexpected error: %#v", i, err)
+		}
+	}
+}
+
+func TestSetAction(t *testing.T) {
+	wantHeader := http.Header(map[string][]string{
+		"Content-Type": []string{"application/x-www-form-urlencoded"},
+	})
+
+	tests := []struct {
+		act      setAction
+		wantURL  string
+		wantBody string
+	}{
+		// default prefix
+		{
+			act: setAction{
+				Prefix: defaultV2KeysPrefix,
+				Key:    "foo",
+			},
+			wantURL:  "http://example.com/v2/keys/foo",
+			wantBody: "value=",
+		},
+
+		// non-default prefix
+		{
+			act: setAction{
+				Prefix: "/pfx",
+				Key:    "foo",
+			},
+			wantURL:  "http://example.com/pfx/foo",
+			wantBody: "value=",
+		},
+
+		// no prefix
+		{
+			act: setAction{
+				Key: "foo",
+			},
+			wantURL:  "http://example.com/foo",
+			wantBody: "value=",
+		},
+
+		// Key with path separators
+		{
+			act: setAction{
+				Prefix: defaultV2KeysPrefix,
+				Key:    "foo/bar/baz",
+			},
+			wantURL:  "http://example.com/v2/keys/foo/bar/baz",
+			wantBody: "value=",
+		},
+
+		// Key with leading slash, Prefix with trailing slash
+		{
+			act: setAction{
+				Prefix: "/foo/",
+				Key:    "/bar",
+			},
+			wantURL:  "http://example.com/foo/bar",
+			wantBody: "value=",
+		},
+
+		// Key with trailing slash
+		{
+			act: setAction{
+				Key: "/foo/",
+			},
+			wantURL:  "http://example.com/foo",
+			wantBody: "value=",
+		},
+
+		// Value is set
+		{
+			act: setAction{
+				Key:   "foo",
+				Value: "baz",
+			},
+			wantURL:  "http://example.com/foo",
+			wantBody: "value=baz",
+		},
+
+		// PrevExist set, but still ignored
+		{
+			act: setAction{
+				Key:       "foo",
+				PrevExist: PrevIgnore,
+			},
+			wantURL:  "http://example.com/foo",
+			wantBody: "value=",
+		},
+
+		// PrevExist set to true
+		{
+			act: setAction{
+				Key:       "foo",
+				PrevExist: PrevExist,
+			},
+			wantURL:  "http://example.com/foo?prevExist=true",
+			wantBody: "value=",
+		},
+
+		// PrevExist set to false
+		{
+			act: setAction{
+				Key:       "foo",
+				PrevExist: PrevNoExist,
+			},
+			wantURL:  "http://example.com/foo?prevExist=false",
+			wantBody: "value=",
+		},
+
+		// PrevValue is urlencoded
+		{
+			act: setAction{
+				Key:       "foo",
+				PrevValue: "bar baz",
+			},
+			wantURL:  "http://example.com/foo?prevValue=bar+baz",
+			wantBody: "value=",
+		},
+
+		// PrevIndex is set
+		{
+			act: setAction{
+				Key:       "foo",
+				PrevIndex: uint64(12),
+			},
+			wantURL:  "http://example.com/foo?prevIndex=12",
+			wantBody: "value=",
+		},
+
+		// TTL is set
+		{
+			act: setAction{
+				Key: "foo",
+				TTL: 3 * time.Minute,
+			},
+			wantURL:  "http://example.com/foo",
+			wantBody: "ttl=180&value=",
+		},
+		// Dir is set
+		{
+			act: setAction{
+				Key: "foo",
+				Dir: true,
+			},
+			wantURL:  "http://example.com/foo?dir=true",
+			wantBody: "",
+		},
+		// Dir is set with a value
+		{
+			act: setAction{
+				Key:   "foo",
+				Value: "bar",
+				Dir:   true,
+			},
+			wantURL:  "http://example.com/foo?dir=true",
+			wantBody: "",
+		},
+		// Dir is set with PrevExist set to true
+		{
+			act: setAction{
+				Key:       "foo",
+				PrevExist: PrevExist,
+				Dir:       true,
+			},
+			wantURL:  "http://example.com/foo?dir=true&prevExist=true",
+			wantBody: "",
+		},
+		// Dir is set with PrevValue
+		{
+			act: setAction{
+				Key:       "foo",
+				PrevValue: "bar",
+				Dir:       true,
+			},
+			wantURL:  "http://example.com/foo?dir=true",
+			wantBody: "",
+		},
+	}
+
+	for i, tt := range tests {
+		u, err := url.Parse(tt.wantURL)
+		if err != nil {
+			t.Errorf("#%d: unable to use wantURL fixture: %v", i, err)
+		}
+
+		got := tt.act.HTTPRequest(url.URL{Scheme: "http", Host: "example.com"})
+		if err := assertRequest(*got, "PUT", u, wantHeader, []byte(tt.wantBody)); err != nil {
+			t.Errorf("#%d: %v", i, err)
+		}
+	}
+}
+
+func TestCreateInOrderAction(t *testing.T) {
+	wantHeader := http.Header(map[string][]string{
+		"Content-Type": []string{"application/x-www-form-urlencoded"},
+	})
+
+	tests := []struct {
+		act      createInOrderAction
+		wantURL  string
+		wantBody string
+	}{
+		// default prefix
+		{
+			act: createInOrderAction{
+				Prefix: defaultV2KeysPrefix,
+				Dir:    "foo",
+			},
+			wantURL:  "http://example.com/v2/keys/foo",
+			wantBody: "value=",
+		},
+
+		// non-default prefix
+		{
+			act: createInOrderAction{
+				Prefix: "/pfx",
+				Dir:    "foo",
+			},
+			wantURL:  "http://example.com/pfx/foo",
+			wantBody: "value=",
+		},
+
+		// no prefix
+		{
+			act: createInOrderAction{
+				Dir: "foo",
+			},
+			wantURL:  "http://example.com/foo",
+			wantBody: "value=",
+		},
+
+		// Key with path separators
+		{
+			act: createInOrderAction{
+				Prefix: defaultV2KeysPrefix,
+				Dir:    "foo/bar/baz",
+			},
+			wantURL:  "http://example.com/v2/keys/foo/bar/baz",
+			wantBody: "value=",
+		},
+
+		// Key with leading slash, Prefix with trailing slash
+		{
+			act: createInOrderAction{
+				Prefix: "/foo/",
+				Dir:    "/bar",
+			},
+			wantURL:  "http://example.com/foo/bar",
+			wantBody: "value=",
+		},
+
+		// Key with trailing slash
+		{
+			act: createInOrderAction{
+				Dir: "/foo/",
+			},
+			wantURL:  "http://example.com/foo",
+			wantBody: "value=",
+		},
+
+		// Value is set
+		{
+			act: createInOrderAction{
+				Dir:   "foo",
+				Value: "baz",
+			},
+			wantURL:  "http://example.com/foo",
+			wantBody: "value=baz",
+		},
+		// TTL is set
+		{
+			act: createInOrderAction{
+				Dir: "foo",
+				TTL: 3 * time.Minute,
+			},
+			wantURL:  "http://example.com/foo",
+			wantBody: "ttl=180&value=",
+		},
+	}
+
+	for i, tt := range tests {
+		u, err := url.Parse(tt.wantURL)
+		if err != nil {
+			t.Errorf("#%d: unable to use wantURL fixture: %v", i, err)
+		}
+
+		got := tt.act.HTTPRequest(url.URL{Scheme: "http", Host: "example.com"})
+		if err := assertRequest(*got, "POST", u, wantHeader, []byte(tt.wantBody)); err != nil {
+			t.Errorf("#%d: %v", i, err)
+		}
+	}
+}
+
+func TestDeleteAction(t *testing.T) {
+	wantHeader := http.Header(map[string][]string{
+		"Content-Type": []string{"application/x-www-form-urlencoded"},
+	})
+
+	tests := []struct {
+		act     deleteAction
+		wantURL string
+	}{
+		// default prefix
+		{
+			act: deleteAction{
+				Prefix: defaultV2KeysPrefix,
+				Key:    "foo",
+			},
+			wantURL: "http://example.com/v2/keys/foo",
+		},
+
+		// non-default prefix
+		{
+			act: deleteAction{
+				Prefix: "/pfx",
+				Key:    "foo",
+			},
+			wantURL: "http://example.com/pfx/foo",
+		},
+
+		// no prefix
+		{
+			act: deleteAction{
+				Key: "foo",
+			},
+			wantURL: "http://example.com/foo",
+		},
+
+		// Key with path separators
+		{
+			act: deleteAction{
+				Prefix: defaultV2KeysPrefix,
+				Key:    "foo/bar/baz",
+			},
+			wantURL: "http://example.com/v2/keys/foo/bar/baz",
+		},
+
+		// Key with leading slash, Prefix with trailing slash
+		{
+			act: deleteAction{
+				Prefix: "/foo/",
+				Key:    "/bar",
+			},
+			wantURL: "http://example.com/foo/bar",
+		},
+
+		// Key with trailing slash
+		{
+			act: deleteAction{
+				Key: "/foo/",
+			},
+			wantURL: "http://example.com/foo",
+		},
+
+		// Recursive set to true
+		{
+			act: deleteAction{
+				Key:       "foo",
+				Recursive: true,
+			},
+			wantURL: "http://example.com/foo?recursive=true",
+		},
+
+		// PrevValue is urlencoded
+		{
+			act: deleteAction{
+				Key:       "foo",
+				PrevValue: "bar baz",
+			},
+			wantURL: "http://example.com/foo?prevValue=bar+baz",
+		},
+
+		// PrevIndex is set
+		{
+			act: deleteAction{
+				Key:       "foo",
+				PrevIndex: uint64(12),
+			},
+			wantURL: "http://example.com/foo?prevIndex=12",
+		},
+	}
+
+	for i, tt := range tests {
+		u, err := url.Parse(tt.wantURL)
+		if err != nil {
+			t.Errorf("#%d: unable to use wantURL fixture: %v", i, err)
+		}
+
+		got := tt.act.HTTPRequest(url.URL{Scheme: "http", Host: "example.com"})
+		if err := assertRequest(*got, "DELETE", u, wantHeader, nil); err != nil {
+			t.Errorf("#%d: %v", i, err)
+		}
+	}
+}
+
+func assertRequest(got http.Request, wantMethod string, wantURL *url.URL, wantHeader http.Header, wantBody []byte) error {
+	if wantMethod != got.Method {
+		return fmt.Errorf("want.Method=%#v got.Method=%#v", wantMethod, got.Method)
+	}
+
+	if !reflect.DeepEqual(wantURL, got.URL) {
+		return fmt.Errorf("want.URL=%#v got.URL=%#v", wantURL, got.URL)
+	}
+
+	if !reflect.DeepEqual(wantHeader, got.Header) {
+		return fmt.Errorf("want.Header=%#v got.Header=%#v", wantHeader, got.Header)
+	}
+
+	if got.Body == nil {
+		if wantBody != nil {
+			return fmt.Errorf("want.Body=%v got.Body=%v", wantBody, got.Body)
+		}
+	} else {
+		if wantBody == nil {
+			return fmt.Errorf("want.Body=%v got.Body=%s", wantBody, got.Body)
+		} else {
+			gotBytes, err := ioutil.ReadAll(got.Body)
+			if err != nil {
+				return err
+			}
+
+			if !reflect.DeepEqual(wantBody, gotBytes) {
+				return fmt.Errorf("want.Body=%s got.Body=%s", wantBody, gotBytes)
+			}
+		}
+	}
+
+	return nil
+}
+
+func TestUnmarshalSuccessfulResponse(t *testing.T) {
+	var expiration time.Time
+	expiration.UnmarshalText([]byte("2015-04-07T04:40:23.044979686Z"))
+
+	tests := []struct {
+		hdr     string
+		body    string
+		wantRes *Response
+		wantErr bool
+	}{
+		// Neither PrevNode or Node
+		{
+			hdr:     "1",
+			body:    `{"action":"delete"}`,
+			wantRes: &Response{Action: "delete", Index: 1},
+			wantErr: false,
+		},
+
+		// PrevNode
+		{
+			hdr:  "15",
+			body: `{"action":"delete", "prevNode": {"key": "/foo", "value": "bar", "modifiedIndex": 12, "createdIndex": 10}}`,
+			wantRes: &Response{
+				Action: "delete",
+				Index:  15,
+				Node:   nil,
+				PrevNode: &Node{
+					Key:           "/foo",
+					Value:         "bar",
+					ModifiedIndex: 12,
+					CreatedIndex:  10,
+				},
+			},
+			wantErr: false,
+		},
+
+		// Node
+		{
+			hdr:  "15",
+			body: `{"action":"get", "node": {"key": "/foo", "value": "bar", "modifiedIndex": 12, "createdIndex": 10, "ttl": 10, "expiration": "2015-04-07T04:40:23.044979686Z"}}`,
+			wantRes: &Response{
+				Action: "get",
+				Index:  15,
+				Node: &Node{
+					Key:           "/foo",
+					Value:         "bar",
+					ModifiedIndex: 12,
+					CreatedIndex:  10,
+					TTL:           10,
+					Expiration:    &expiration,
+				},
+				PrevNode: nil,
+			},
+			wantErr: false,
+		},
+
+		// Node Dir
+		{
+			hdr:  "15",
+			body: `{"action":"get", "node": {"key": "/foo", "dir": true, "modifiedIndex": 12, "createdIndex": 10}}`,
+			wantRes: &Response{
+				Action: "get",
+				Index:  15,
+				Node: &Node{
+					Key:           "/foo",
+					Dir:           true,
+					ModifiedIndex: 12,
+					CreatedIndex:  10,
+				},
+				PrevNode: nil,
+			},
+			wantErr: false,
+		},
+
+		// PrevNode and Node
+		{
+			hdr:  "15",
+			body: `{"action":"update", "prevNode": {"key": "/foo", "value": "baz", "modifiedIndex": 10, "createdIndex": 10}, "node": {"key": "/foo", "value": "bar", "modifiedIndex": 12, "createdIndex": 10}}`,
+			wantRes: &Response{
+				Action: "update",
+				Index:  15,
+				PrevNode: &Node{
+					Key:           "/foo",
+					Value:         "baz",
+					ModifiedIndex: 10,
+					CreatedIndex:  10,
+				},
+				Node: &Node{
+					Key:           "/foo",
+					Value:         "bar",
+					ModifiedIndex: 12,
+					CreatedIndex:  10,
+				},
+			},
+			wantErr: false,
+		},
+
+		// Garbage in body
+		{
+			hdr:     "",
+			body:    `garbage`,
+			wantRes: nil,
+			wantErr: true,
+		},
+
+		// non-integer index
+		{
+			hdr:     "poo",
+			body:    `{}`,
+			wantRes: nil,
+			wantErr: true,
+		},
+	}
+
+	for i, tt := range tests {
+		h := make(http.Header)
+		h.Add("X-Etcd-Index", tt.hdr)
+		res, err := unmarshalSuccessfulKeysResponse(h, []byte(tt.body))
+		if tt.wantErr != (err != nil) {
+			t.Errorf("#%d: wantErr=%t, err=%v", i, tt.wantErr, err)
+		}
+
+		if (res == nil) != (tt.wantRes == nil) {
+			t.Errorf("#%d: received res=%#v, but expected res=%#v", i, res, tt.wantRes)
+			continue
+		} else if tt.wantRes == nil {
+			// expected and successfully got nil response
+			continue
+		}
+
+		if res.Action != tt.wantRes.Action {
+			t.Errorf("#%d: Action=%s, expected %s", i, res.Action, tt.wantRes.Action)
+		}
+		if res.Index != tt.wantRes.Index {
+			t.Errorf("#%d: Index=%d, expected %d", i, res.Index, tt.wantRes.Index)
+		}
+		if !reflect.DeepEqual(res.Node, tt.wantRes.Node) {
+			t.Errorf("#%d: Node=%v, expected %v", i, res.Node, tt.wantRes.Node)
+		}
+	}
+}
+
+func TestUnmarshalFailedKeysResponse(t *testing.T) {
+	body := []byte(`{"errorCode":100,"message":"Key not found","cause":"/foo","index":18}`)
+
+	wantErr := Error{
+		Code:    100,
+		Message: "Key not found",
+		Cause:   "/foo",
+		Index:   uint64(18),
+	}
+
+	gotErr := unmarshalFailedKeysResponse(body)
+	if !reflect.DeepEqual(wantErr, gotErr) {
+		t.Errorf("unexpected error: want=%#v got=%#v", wantErr, gotErr)
+	}
+}
+
+func TestUnmarshalFailedKeysResponseBadJSON(t *testing.T) {
+	err := unmarshalFailedKeysResponse([]byte(`{"er`))
+	if err == nil {
+		t.Errorf("got nil error")
+	} else if _, ok := err.(Error); ok {
+		t.Errorf("error is of incorrect type *Error: %#v", err)
+	}
+}
+
+func TestHTTPWatcherNextWaitAction(t *testing.T) {
+	initAction := waitAction{
+		Prefix:    "/pants",
+		Key:       "/foo/bar",
+		Recursive: true,
+		WaitIndex: 19,
+	}
+
+	client := &actionAssertingHTTPClient{
+		t:   t,
+		act: &initAction,
+		resp: http.Response{
+			StatusCode: http.StatusOK,
+			Header:     http.Header{"X-Etcd-Index": []string{"42"}},
+		},
+		body: []byte(`{"action":"update","node":{"key":"/pants/foo/bar/baz","value":"snarf","modifiedIndex":21,"createdIndex":19},"prevNode":{"key":"/pants/foo/bar/baz","value":"snazz","modifiedIndex":20,"createdIndex":19}}`),
+	}
+
+	wantResponse := &Response{
+		Action:   "update",
+		Node:     &Node{Key: "/pants/foo/bar/baz", Value: "snarf", CreatedIndex: uint64(19), ModifiedIndex: uint64(21)},
+		PrevNode: &Node{Key: "/pants/foo/bar/baz", Value: "snazz", CreatedIndex: uint64(19), ModifiedIndex: uint64(20)},
+		Index:    uint64(42),
+	}
+
+	wantNextWait := waitAction{
+		Prefix:    "/pants",
+		Key:       "/foo/bar",
+		Recursive: true,
+		WaitIndex: 22,
+	}
+
+	watcher := &httpWatcher{
+		client:   client,
+		nextWait: initAction,
+	}
+
+	resp, err := watcher.Next(context.Background())
+	if err != nil {
+		t.Errorf("non-nil error: %#v", err)
+	}
+
+	if !reflect.DeepEqual(wantResponse, resp) {
+		t.Errorf("received incorrect Response: want=%#v got=%#v", wantResponse, resp)
+	}
+
+	if !reflect.DeepEqual(wantNextWait, watcher.nextWait) {
+		t.Errorf("nextWait incorrect: want=%#v got=%#v", wantNextWait, watcher.nextWait)
+	}
+}
+
+func TestHTTPWatcherNextFail(t *testing.T) {
+	tests := []httpClient{
+		// generic HTTP client failure
+		&staticHTTPClient{
+			err: errors.New("fail!"),
+		},
+
+		// unusable status code
+		&staticHTTPClient{
+			resp: http.Response{
+				StatusCode: http.StatusTeapot,
+			},
+		},
+
+		// etcd Error response
+		&staticHTTPClient{
+			resp: http.Response{
+				StatusCode: http.StatusNotFound,
+			},
+			body: []byte(`{"errorCode":100,"message":"Key not found","cause":"/foo","index":18}`),
+		},
+	}
+
+	for i, tt := range tests {
+		act := waitAction{
+			Prefix:    "/pants",
+			Key:       "/foo/bar",
+			Recursive: true,
+			WaitIndex: 19,
+		}
+
+		watcher := &httpWatcher{
+			client:   tt,
+			nextWait: act,
+		}
+
+		resp, err := watcher.Next(context.Background())
+		if err == nil {
+			t.Errorf("#%d: expected non-nil error", i)
+		}
+		if resp != nil {
+			t.Errorf("#%d: expected nil Response, got %#v", i, resp)
+		}
+		if !reflect.DeepEqual(act, watcher.nextWait) {
+			t.Errorf("#%d: nextWait changed: want=%#v got=%#v", i, act, watcher.nextWait)
+		}
+	}
+}
+
+func TestHTTPKeysAPIWatcherAction(t *testing.T) {
+	tests := []struct {
+		key  string
+		opts *WatcherOptions
+		want waitAction
+	}{
+		{
+			key:  "/foo",
+			opts: nil,
+			want: waitAction{
+				Key:       "/foo",
+				Recursive: false,
+				WaitIndex: 0,
+			},
+		},
+
+		{
+			key: "/foo",
+			opts: &WatcherOptions{
+				Recursive:  false,
+				AfterIndex: 0,
+			},
+			want: waitAction{
+				Key:       "/foo",
+				Recursive: false,
+				WaitIndex: 0,
+			},
+		},
+
+		{
+			key: "/foo",
+			opts: &WatcherOptions{
+				Recursive:  true,
+				AfterIndex: 0,
+			},
+			want: waitAction{
+				Key:       "/foo",
+				Recursive: true,
+				WaitIndex: 0,
+			},
+		},
+
+		{
+			key: "/foo",
+			opts: &WatcherOptions{
+				Recursive:  false,
+				AfterIndex: 19,
+			},
+			want: waitAction{
+				Key:       "/foo",
+				Recursive: false,
+				WaitIndex: 20,
+			},
+		},
+	}
+
+	for i, tt := range tests {
+		kAPI := &httpKeysAPI{
+			client: &staticHTTPClient{err: errors.New("fail!")},
+		}
+
+		want := &httpWatcher{
+			client:   &staticHTTPClient{err: errors.New("fail!")},
+			nextWait: tt.want,
+		}
+
+		got := kAPI.Watcher(tt.key, tt.opts)
+		if !reflect.DeepEqual(want, got) {
+			t.Errorf("#%d: incorrect watcher: want=%#v got=%#v", i, want, got)
+		}
+	}
+}
+
+func TestHTTPKeysAPISetAction(t *testing.T) {
+	tests := []struct {
+		key        string
+		value      string
+		opts       *SetOptions
+		wantAction httpAction
+	}{
+		// nil SetOptions
+		{
+			key:   "/foo",
+			value: "bar",
+			opts:  nil,
+			wantAction: &setAction{
+				Key:       "/foo",
+				Value:     "bar",
+				PrevValue: "",
+				PrevIndex: 0,
+				PrevExist: PrevIgnore,
+				TTL:       0,
+			},
+		},
+		// empty SetOptions
+		{
+			key:   "/foo",
+			value: "bar",
+			opts:  &SetOptions{},
+			wantAction: &setAction{
+				Key:       "/foo",
+				Value:     "bar",
+				PrevValue: "",
+				PrevIndex: 0,
+				PrevExist: PrevIgnore,
+				TTL:       0,
+			},
+		},
+		// populated SetOptions
+		{
+			key:   "/foo",
+			value: "bar",
+			opts: &SetOptions{
+				PrevValue: "baz",
+				PrevIndex: 13,
+				PrevExist: PrevExist,
+				TTL:       time.Minute,
+				Dir:       true,
+			},
+			wantAction: &setAction{
+				Key:       "/foo",
+				Value:     "bar",
+				PrevValue: "baz",
+				PrevIndex: 13,
+				PrevExist: PrevExist,
+				TTL:       time.Minute,
+				Dir:       true,
+			},
+		},
+	}
+
+	for i, tt := range tests {
+		client := &actionAssertingHTTPClient{t: t, num: i, act: tt.wantAction}
+		kAPI := httpKeysAPI{client: client}
+		kAPI.Set(context.Background(), tt.key, tt.value, tt.opts)
+	}
+}
+
+func TestHTTPKeysAPISetError(t *testing.T) {
+	tests := []httpClient{
+		// generic HTTP client failure
+		&staticHTTPClient{
+			err: errors.New("fail!"),
+		},
+
+		// unusable status code
+		&staticHTTPClient{
+			resp: http.Response{
+				StatusCode: http.StatusTeapot,
+			},
+		},
+
+		// etcd Error response
+		&staticHTTPClient{
+			resp: http.Response{
+				StatusCode: http.StatusInternalServerError,
+			},
+			body: []byte(`{"errorCode":300,"message":"Raft internal error","cause":"/foo","index":18}`),
+		},
+	}
+
+	for i, tt := range tests {
+		kAPI := httpKeysAPI{client: tt}
+		resp, err := kAPI.Set(context.Background(), "/foo", "bar", nil)
+		if err == nil {
+			t.Errorf("#%d: received nil error", i)
+		}
+		if resp != nil {
+			t.Errorf("#%d: received non-nil Response: %#v", i, resp)
+		}
+	}
+}
+
+func TestHTTPKeysAPISetResponse(t *testing.T) {
+	client := &staticHTTPClient{
+		resp: http.Response{
+			StatusCode: http.StatusOK,
+			Header:     http.Header{"X-Etcd-Index": []string{"21"}},
+		},
+		body: []byte(`{"action":"set","node":{"key":"/pants/foo/bar/baz","value":"snarf","modifiedIndex":21,"createdIndex":21},"prevNode":{"key":"/pants/foo/bar/baz","value":"snazz","modifiedIndex":20,"createdIndex":19}}`),
+	}
+
+	wantResponse := &Response{
+		Action:   "set",
+		Node:     &Node{Key: "/pants/foo/bar/baz", Value: "snarf", CreatedIndex: uint64(21), ModifiedIndex: uint64(21)},
+		PrevNode: &Node{Key: "/pants/foo/bar/baz", Value: "snazz", CreatedIndex: uint64(19), ModifiedIndex: uint64(20)},
+		Index:    uint64(21),
+	}
+
+	kAPI := &httpKeysAPI{client: client, prefix: "/pants"}
+	resp, err := kAPI.Set(context.Background(), "/foo/bar/baz", "snarf", nil)
+	if err != nil {
+		t.Errorf("non-nil error: %#v", err)
+	}
+	if !reflect.DeepEqual(wantResponse, resp) {
+		t.Errorf("incorrect Response: want=%#v got=%#v", wantResponse, resp)
+	}
+}
+
+func TestHTTPKeysAPIGetAction(t *testing.T) {
+	tests := []struct {
+		key        string
+		opts       *GetOptions
+		wantAction httpAction
+	}{
+		// nil GetOptions
+		{
+			key:  "/foo",
+			opts: nil,
+			wantAction: &getAction{
+				Key:       "/foo",
+				Sorted:    false,
+				Recursive: false,
+			},
+		},
+		// empty GetOptions
+		{
+			key:  "/foo",
+			opts: &GetOptions{},
+			wantAction: &getAction{
+				Key:       "/foo",
+				Sorted:    false,
+				Recursive: false,
+			},
+		},
+		// populated GetOptions
+		{
+			key: "/foo",
+			opts: &GetOptions{
+				Sort:      true,
+				Recursive: true,
+				Quorum:    true,
+			},
+			wantAction: &getAction{
+				Key:       "/foo",
+				Sorted:    true,
+				Recursive: true,
+				Quorum:    true,
+			},
+		},
+	}
+
+	for i, tt := range tests {
+		client := &actionAssertingHTTPClient{t: t, num: i, act: tt.wantAction}
+		kAPI := httpKeysAPI{client: client}
+		kAPI.Get(context.Background(), tt.key, tt.opts)
+	}
+}
+
+func TestHTTPKeysAPIGetError(t *testing.T) {
+	tests := []httpClient{
+		// generic HTTP client failure
+		&staticHTTPClient{
+			err: errors.New("fail!"),
+		},
+
+		// unusable status code
+		&staticHTTPClient{
+			resp: http.Response{
+				StatusCode: http.StatusTeapot,
+			},
+		},
+
+		// etcd Error response
+		&staticHTTPClient{
+			resp: http.Response{
+				StatusCode: http.StatusInternalServerError,
+			},
+			body: []byte(`{"errorCode":300,"message":"Raft internal error","cause":"/foo","index":18}`),
+		},
+	}
+
+	for i, tt := range tests {
+		kAPI := httpKeysAPI{client: tt}
+		resp, err := kAPI.Get(context.Background(), "/foo", nil)
+		if err == nil {
+			t.Errorf("#%d: received nil error", i)
+		}
+		if resp != nil {
+			t.Errorf("#%d: received non-nil Response: %#v", i, resp)
+		}
+	}
+}
+
+func TestHTTPKeysAPIGetResponse(t *testing.T) {
+	client := &staticHTTPClient{
+		resp: http.Response{
+			StatusCode: http.StatusOK,
+			Header:     http.Header{"X-Etcd-Index": []string{"42"}},
+		},
+		body: []byte(`{"action":"get","node":{"key":"/pants/foo/bar","modifiedIndex":25,"createdIndex":19,"nodes":[{"key":"/pants/foo/bar/baz","value":"snarf","createdIndex":21,"modifiedIndex":25}]}}`),
+	}
+
+	wantResponse := &Response{
+		Action: "get",
+		Node: &Node{
+			Key: "/pants/foo/bar",
+			Nodes: []*Node{
+				&Node{Key: "/pants/foo/bar/baz", Value: "snarf", CreatedIndex: 21, ModifiedIndex: 25},
+			},
+			CreatedIndex:  uint64(19),
+			ModifiedIndex: uint64(25),
+		},
+		Index: uint64(42),
+	}
+
+	kAPI := &httpKeysAPI{client: client, prefix: "/pants"}
+	resp, err := kAPI.Get(context.Background(), "/foo/bar", &GetOptions{Recursive: true})
+	if err != nil {
+		t.Errorf("non-nil error: %#v", err)
+	}
+	if !reflect.DeepEqual(wantResponse, resp) {
+		t.Errorf("incorrect Response: want=%#v got=%#v", wantResponse, resp)
+	}
+}
+
+func TestHTTPKeysAPIDeleteAction(t *testing.T) {
+	tests := []struct {
+		key        string
+		value      string
+		opts       *DeleteOptions
+		wantAction httpAction
+	}{
+		// nil DeleteOptions
+		{
+			key:  "/foo",
+			opts: nil,
+			wantAction: &deleteAction{
+				Key:       "/foo",
+				PrevValue: "",
+				PrevIndex: 0,
+				Recursive: false,
+			},
+		},
+		// empty DeleteOptions
+		{
+			key:  "/foo",
+			opts: &DeleteOptions{},
+			wantAction: &deleteAction{
+				Key:       "/foo",
+				PrevValue: "",
+				PrevIndex: 0,
+				Recursive: false,
+			},
+		},
+		// populated DeleteOptions
+		{
+			key: "/foo",
+			opts: &DeleteOptions{
+				PrevValue: "baz",
+				PrevIndex: 13,
+				Recursive: true,
+			},
+			wantAction: &deleteAction{
+				Key:       "/foo",
+				PrevValue: "baz",
+				PrevIndex: 13,
+				Recursive: true,
+			},
+		},
+	}
+
+	for i, tt := range tests {
+		client := &actionAssertingHTTPClient{t: t, num: i, act: tt.wantAction}
+		kAPI := httpKeysAPI{client: client}
+		kAPI.Delete(context.Background(), tt.key, tt.opts)
+	}
+}
+
+func TestHTTPKeysAPIDeleteError(t *testing.T) {
+	tests := []httpClient{
+		// generic HTTP client failure
+		&staticHTTPClient{
+			err: errors.New("fail!"),
+		},
+
+		// unusable status code
+		&staticHTTPClient{
+			resp: http.Response{
+				StatusCode: http.StatusTeapot,
+			},
+		},
+
+		// etcd Error response
+		&staticHTTPClient{
+			resp: http.Response{
+				StatusCode: http.StatusInternalServerError,
+			},
+			body: []byte(`{"errorCode":300,"message":"Raft internal error","cause":"/foo","index":18}`),
+		},
+	}
+
+	for i, tt := range tests {
+		kAPI := httpKeysAPI{client: tt}
+		resp, err := kAPI.Delete(context.Background(), "/foo", nil)
+		if err == nil {
+			t.Errorf("#%d: received nil error", i)
+		}
+		if resp != nil {
+			t.Errorf("#%d: received non-nil Response: %#v", i, resp)
+		}
+	}
+}
+
+func TestHTTPKeysAPIDeleteResponse(t *testing.T) {
+	client := &staticHTTPClient{
+		resp: http.Response{
+			StatusCode: http.StatusOK,
+			Header:     http.Header{"X-Etcd-Index": []string{"22"}},
+		},
+		body: []byte(`{"action":"delete","node":{"key":"/pants/foo/bar/baz","value":"snarf","modifiedIndex":22,"createdIndex":19},"prevNode":{"key":"/pants/foo/bar/baz","value":"snazz","modifiedIndex":20,"createdIndex":19}}`),
+	}
+
+	wantResponse := &Response{
+		Action:   "delete",
+		Node:     &Node{Key: "/pants/foo/bar/baz", Value: "snarf", CreatedIndex: uint64(19), ModifiedIndex: uint64(22)},
+		PrevNode: &Node{Key: "/pants/foo/bar/baz", Value: "snazz", CreatedIndex: uint64(19), ModifiedIndex: uint64(20)},
+		Index:    uint64(22),
+	}
+
+	kAPI := &httpKeysAPI{client: client, prefix: "/pants"}
+	resp, err := kAPI.Delete(context.Background(), "/foo/bar/baz", nil)
+	if err != nil {
+		t.Errorf("non-nil error: %#v", err)
+	}
+	if !reflect.DeepEqual(wantResponse, resp) {
+		t.Errorf("incorrect Response: want=%#v got=%#v", wantResponse, resp)
+	}
+}
+
+func TestHTTPKeysAPICreateAction(t *testing.T) {
+	act := &setAction{
+		Key:       "/foo",
+		Value:     "bar",
+		PrevExist: PrevNoExist,
+		PrevIndex: 0,
+		PrevValue: "",
+		TTL:       0,
+	}
+
+	kAPI := httpKeysAPI{client: &actionAssertingHTTPClient{t: t, act: act}}
+	kAPI.Create(context.Background(), "/foo", "bar")
+}
+
+func TestHTTPKeysAPICreateInOrderAction(t *testing.T) {
+	act := &createInOrderAction{
+		Dir:   "/foo",
+		Value: "bar",
+		TTL:   0,
+	}
+	kAPI := httpKeysAPI{client: &actionAssertingHTTPClient{t: t, act: act}}
+	kAPI.CreateInOrder(context.Background(), "/foo", "bar", nil)
+}
+
+func TestHTTPKeysAPIUpdateAction(t *testing.T) {
+	act := &setAction{
+		Key:       "/foo",
+		Value:     "bar",
+		PrevExist: PrevExist,
+		PrevIndex: 0,
+		PrevValue: "",
+		TTL:       0,
+	}
+
+	kAPI := httpKeysAPI{client: &actionAssertingHTTPClient{t: t, act: act}}
+	kAPI.Update(context.Background(), "/foo", "bar")
+}
+
+func TestNodeTTLDuration(t *testing.T) {
+	tests := []struct {
+		node *Node
+		want time.Duration
+	}{
+		{
+			node: &Node{TTL: 0},
+			want: 0,
+		},
+		{
+			node: &Node{TTL: 97},
+			want: 97 * time.Second,
+		},
+	}
+
+	for i, tt := range tests {
+		got := tt.node.TTLDuration()
+		if tt.want != got {
+			t.Errorf("#%d: incorrect duration: want=%v got=%v", i, tt.want, got)
+		}
+	}
+}

+ 271 - 0
Godeps/_workspace/src/github.com/coreos/etcd/client/members.go

@@ -0,0 +1,271 @@
+// Copyright 2015 CoreOS, Inc.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package client
+
+import (
+	"bytes"
+	"encoding/json"
+	"fmt"
+	"net/http"
+	"net/url"
+	"path"
+
+	"github.com/coreos/flannel/Godeps/_workspace/src/github.com/coreos/etcd/pkg/types"
+	"github.com/coreos/flannel/Godeps/_workspace/src/golang.org/x/net/context"
+)
+
+var (
+	defaultV2MembersPrefix = "/v2/members"
+)
+
+type Member struct {
+	// ID is the unique identifier of this Member.
+	ID string `json:"id"`
+
+	// Name is a human-readable, non-unique identifier of this Member.
+	Name string `json:"name"`
+
+	// PeerURLs represents the HTTP(S) endpoints this Member uses to
+	// participate in etcd's consensus protocol.
+	PeerURLs []string `json:"peerURLs"`
+
+	// ClientURLs represents the HTTP(S) endpoints on which this Member
+	// serves it's client-facing APIs.
+	ClientURLs []string `json:"clientURLs"`
+}
+
+type memberCollection []Member
+
+func (c *memberCollection) UnmarshalJSON(data []byte) error {
+	d := struct {
+		Members []Member
+	}{}
+
+	if err := json.Unmarshal(data, &d); err != nil {
+		return err
+	}
+
+	if d.Members == nil {
+		*c = make([]Member, 0)
+		return nil
+	}
+
+	*c = d.Members
+	return nil
+}
+
+type memberCreateOrUpdateRequest struct {
+	PeerURLs types.URLs
+}
+
+func (m *memberCreateOrUpdateRequest) MarshalJSON() ([]byte, error) {
+	s := struct {
+		PeerURLs []string `json:"peerURLs"`
+	}{
+		PeerURLs: make([]string, len(m.PeerURLs)),
+	}
+
+	for i, u := range m.PeerURLs {
+		s.PeerURLs[i] = u.String()
+	}
+
+	return json.Marshal(&s)
+}
+
+// NewMembersAPI constructs a new MembersAPI that uses HTTP to
+// interact with etcd's membership API.
+func NewMembersAPI(c Client) MembersAPI {
+	return &httpMembersAPI{
+		client: c,
+	}
+}
+
+type MembersAPI interface {
+	// List enumerates the current cluster membership.
+	List(ctx context.Context) ([]Member, error)
+
+	// Add instructs etcd to accept a new Member into the cluster.
+	Add(ctx context.Context, peerURL string) (*Member, error)
+
+	// Remove demotes an existing Member out of the cluster.
+	Remove(ctx context.Context, mID string) error
+
+	// Update instructs etcd to update an existing Member in the cluster.
+	Update(ctx context.Context, mID string, peerURLs []string) error
+}
+
+type httpMembersAPI struct {
+	client httpClient
+}
+
+func (m *httpMembersAPI) List(ctx context.Context) ([]Member, error) {
+	req := &membersAPIActionList{}
+	resp, body, err := m.client.Do(ctx, req)
+	if err != nil {
+		return nil, err
+	}
+
+	if err := assertStatusCode(resp.StatusCode, http.StatusOK); err != nil {
+		return nil, err
+	}
+
+	var mCollection memberCollection
+	if err := json.Unmarshal(body, &mCollection); err != nil {
+		return nil, err
+	}
+
+	return []Member(mCollection), nil
+}
+
+func (m *httpMembersAPI) Add(ctx context.Context, peerURL string) (*Member, error) {
+	urls, err := types.NewURLs([]string{peerURL})
+	if err != nil {
+		return nil, err
+	}
+
+	req := &membersAPIActionAdd{peerURLs: urls}
+	resp, body, err := m.client.Do(ctx, req)
+	if err != nil {
+		return nil, err
+	}
+
+	if err := assertStatusCode(resp.StatusCode, http.StatusCreated, http.StatusConflict); err != nil {
+		return nil, err
+	}
+
+	if resp.StatusCode != http.StatusCreated {
+		var merr membersError
+		if err := json.Unmarshal(body, &merr); err != nil {
+			return nil, err
+		}
+		return nil, merr
+	}
+
+	var memb Member
+	if err := json.Unmarshal(body, &memb); err != nil {
+		return nil, err
+	}
+
+	return &memb, nil
+}
+
+func (m *httpMembersAPI) Update(ctx context.Context, memberID string, peerURLs []string) error {
+	urls, err := types.NewURLs(peerURLs)
+	if err != nil {
+		return err
+	}
+
+	req := &membersAPIActionUpdate{peerURLs: urls, memberID: memberID}
+	resp, body, err := m.client.Do(ctx, req)
+	if err != nil {
+		return err
+	}
+
+	if err := assertStatusCode(resp.StatusCode, http.StatusNoContent, http.StatusNotFound, http.StatusConflict); err != nil {
+		return err
+	}
+
+	if resp.StatusCode != http.StatusNoContent {
+		var merr membersError
+		if err := json.Unmarshal(body, &merr); err != nil {
+			return err
+		}
+		return merr
+	}
+
+	return nil
+}
+
+func (m *httpMembersAPI) Remove(ctx context.Context, memberID string) error {
+	req := &membersAPIActionRemove{memberID: memberID}
+	resp, _, err := m.client.Do(ctx, req)
+	if err != nil {
+		return err
+	}
+
+	return assertStatusCode(resp.StatusCode, http.StatusNoContent, http.StatusGone)
+}
+
+type membersAPIActionList struct{}
+
+func (l *membersAPIActionList) HTTPRequest(ep url.URL) *http.Request {
+	u := v2MembersURL(ep)
+	req, _ := http.NewRequest("GET", u.String(), nil)
+	return req
+}
+
+type membersAPIActionRemove struct {
+	memberID string
+}
+
+func (d *membersAPIActionRemove) HTTPRequest(ep url.URL) *http.Request {
+	u := v2MembersURL(ep)
+	u.Path = path.Join(u.Path, d.memberID)
+	req, _ := http.NewRequest("DELETE", u.String(), nil)
+	return req
+}
+
+type membersAPIActionAdd struct {
+	peerURLs types.URLs
+}
+
+func (a *membersAPIActionAdd) HTTPRequest(ep url.URL) *http.Request {
+	u := v2MembersURL(ep)
+	m := memberCreateOrUpdateRequest{PeerURLs: a.peerURLs}
+	b, _ := json.Marshal(&m)
+	req, _ := http.NewRequest("POST", u.String(), bytes.NewReader(b))
+	req.Header.Set("Content-Type", "application/json")
+	return req
+}
+
+type membersAPIActionUpdate struct {
+	memberID string
+	peerURLs types.URLs
+}
+
+func (a *membersAPIActionUpdate) HTTPRequest(ep url.URL) *http.Request {
+	u := v2MembersURL(ep)
+	m := memberCreateOrUpdateRequest{PeerURLs: a.peerURLs}
+	u.Path = path.Join(u.Path, a.memberID)
+	b, _ := json.Marshal(&m)
+	req, _ := http.NewRequest("PUT", u.String(), bytes.NewReader(b))
+	req.Header.Set("Content-Type", "application/json")
+	return req
+}
+
+func assertStatusCode(got int, want ...int) (err error) {
+	for _, w := range want {
+		if w == got {
+			return nil
+		}
+	}
+	return fmt.Errorf("unexpected status code %d", got)
+}
+
+// v2MembersURL add the necessary path to the provided endpoint
+// to route requests to the default v2 members API.
+func v2MembersURL(ep url.URL) *url.URL {
+	ep.Path = path.Join(ep.Path, defaultV2MembersPrefix)
+	return &ep
+}
+
+type membersError struct {
+	Message string `json:"message"`
+	Code    int    `json:"-"`
+}
+
+func (e membersError) Error() string {
+	return e.Message
+}

+ 521 - 0
Godeps/_workspace/src/github.com/coreos/etcd/client/members_test.go

@@ -0,0 +1,521 @@
+// Copyright 2015 CoreOS, Inc.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package client
+
+import (
+	"encoding/json"
+	"errors"
+	"net/http"
+	"net/url"
+	"reflect"
+	"testing"
+
+	"github.com/coreos/flannel/Godeps/_workspace/src/github.com/coreos/etcd/pkg/types"
+	"github.com/coreos/flannel/Godeps/_workspace/src/golang.org/x/net/context"
+)
+
+func TestMembersAPIActionList(t *testing.T) {
+	ep := url.URL{Scheme: "http", Host: "example.com"}
+	act := &membersAPIActionList{}
+
+	wantURL := &url.URL{
+		Scheme: "http",
+		Host:   "example.com",
+		Path:   "/v2/members",
+	}
+
+	got := *act.HTTPRequest(ep)
+	err := assertRequest(got, "GET", wantURL, http.Header{}, nil)
+	if err != nil {
+		t.Error(err.Error())
+	}
+}
+
+func TestMembersAPIActionAdd(t *testing.T) {
+	ep := url.URL{Scheme: "http", Host: "example.com"}
+	act := &membersAPIActionAdd{
+		peerURLs: types.URLs([]url.URL{
+			url.URL{Scheme: "https", Host: "127.0.0.1:8081"},
+			url.URL{Scheme: "http", Host: "127.0.0.1:8080"},
+		}),
+	}
+
+	wantURL := &url.URL{
+		Scheme: "http",
+		Host:   "example.com",
+		Path:   "/v2/members",
+	}
+	wantHeader := http.Header{
+		"Content-Type": []string{"application/json"},
+	}
+	wantBody := []byte(`{"peerURLs":["https://127.0.0.1:8081","http://127.0.0.1:8080"]}`)
+
+	got := *act.HTTPRequest(ep)
+	err := assertRequest(got, "POST", wantURL, wantHeader, wantBody)
+	if err != nil {
+		t.Error(err.Error())
+	}
+}
+
+func TestMembersAPIActionUpdate(t *testing.T) {
+	ep := url.URL{Scheme: "http", Host: "example.com"}
+	act := &membersAPIActionUpdate{
+		memberID: "0xabcd",
+		peerURLs: types.URLs([]url.URL{
+			url.URL{Scheme: "https", Host: "127.0.0.1:8081"},
+			url.URL{Scheme: "http", Host: "127.0.0.1:8080"},
+		}),
+	}
+
+	wantURL := &url.URL{
+		Scheme: "http",
+		Host:   "example.com",
+		Path:   "/v2/members/0xabcd",
+	}
+	wantHeader := http.Header{
+		"Content-Type": []string{"application/json"},
+	}
+	wantBody := []byte(`{"peerURLs":["https://127.0.0.1:8081","http://127.0.0.1:8080"]}`)
+
+	got := *act.HTTPRequest(ep)
+	err := assertRequest(got, "PUT", wantURL, wantHeader, wantBody)
+	if err != nil {
+		t.Error(err.Error())
+	}
+}
+
+func TestMembersAPIActionRemove(t *testing.T) {
+	ep := url.URL{Scheme: "http", Host: "example.com"}
+	act := &membersAPIActionRemove{memberID: "XXX"}
+
+	wantURL := &url.URL{
+		Scheme: "http",
+		Host:   "example.com",
+		Path:   "/v2/members/XXX",
+	}
+
+	got := *act.HTTPRequest(ep)
+	err := assertRequest(got, "DELETE", wantURL, http.Header{}, nil)
+	if err != nil {
+		t.Error(err.Error())
+	}
+}
+
+func TestAssertStatusCode(t *testing.T) {
+	if err := assertStatusCode(404, 400); err == nil {
+		t.Errorf("assertStatusCode failed to detect conflict in 400 vs 404")
+	}
+
+	if err := assertStatusCode(404, 400, 404); err != nil {
+		t.Errorf("assertStatusCode found conflict in (404,400) vs 400: %v", err)
+	}
+}
+
+func TestV2MembersURL(t *testing.T) {
+	got := v2MembersURL(url.URL{
+		Scheme: "http",
+		Host:   "foo.example.com:4002",
+		Path:   "/pants",
+	})
+	want := &url.URL{
+		Scheme: "http",
+		Host:   "foo.example.com:4002",
+		Path:   "/pants/v2/members",
+	}
+
+	if !reflect.DeepEqual(want, got) {
+		t.Fatalf("v2MembersURL got %#v, want %#v", got, want)
+	}
+}
+
+func TestMemberUnmarshal(t *testing.T) {
+	tests := []struct {
+		body       []byte
+		wantMember Member
+		wantError  bool
+	}{
+		// no URLs, just check ID & Name
+		{
+			body:       []byte(`{"id": "c", "name": "dungarees"}`),
+			wantMember: Member{ID: "c", Name: "dungarees", PeerURLs: nil, ClientURLs: nil},
+		},
+
+		// both client and peer URLs
+		{
+			body: []byte(`{"peerURLs": ["http://127.0.0.1:2379"], "clientURLs": ["http://127.0.0.1:2379"]}`),
+			wantMember: Member{
+				PeerURLs: []string{
+					"http://127.0.0.1:2379",
+				},
+				ClientURLs: []string{
+					"http://127.0.0.1:2379",
+				},
+			},
+		},
+
+		// multiple peer URLs
+		{
+			body: []byte(`{"peerURLs": ["http://127.0.0.1:2379", "https://example.com"]}`),
+			wantMember: Member{
+				PeerURLs: []string{
+					"http://127.0.0.1:2379",
+					"https://example.com",
+				},
+				ClientURLs: nil,
+			},
+		},
+
+		// multiple client URLs
+		{
+			body: []byte(`{"clientURLs": ["http://127.0.0.1:2379", "https://example.com"]}`),
+			wantMember: Member{
+				PeerURLs: nil,
+				ClientURLs: []string{
+					"http://127.0.0.1:2379",
+					"https://example.com",
+				},
+			},
+		},
+
+		// invalid JSON
+		{
+			body:      []byte(`{"peerU`),
+			wantError: true,
+		},
+	}
+
+	for i, tt := range tests {
+		got := Member{}
+		err := json.Unmarshal(tt.body, &got)
+		if tt.wantError != (err != nil) {
+			t.Errorf("#%d: want error %t, got %v", i, tt.wantError, err)
+			continue
+		}
+
+		if !reflect.DeepEqual(tt.wantMember, got) {
+			t.Errorf("#%d: incorrect output: want=%#v, got=%#v", i, tt.wantMember, got)
+		}
+	}
+}
+
+func TestMemberCollectionUnmarshalFail(t *testing.T) {
+	mc := &memberCollection{}
+	if err := mc.UnmarshalJSON([]byte(`{`)); err == nil {
+		t.Errorf("got nil error")
+	}
+}
+
+func TestMemberCollectionUnmarshal(t *testing.T) {
+	tests := []struct {
+		body []byte
+		want memberCollection
+	}{
+		{
+			body: []byte(`{}`),
+			want: memberCollection([]Member{}),
+		},
+		{
+			body: []byte(`{"members":[]}`),
+			want: memberCollection([]Member{}),
+		},
+		{
+			body: []byte(`{"members":[{"id":"2745e2525fce8fe","peerURLs":["http://127.0.0.1:7003"],"name":"node3","clientURLs":["http://127.0.0.1:4003"]},{"id":"42134f434382925","peerURLs":["http://127.0.0.1:2380","http://127.0.0.1:7001"],"name":"node1","clientURLs":["http://127.0.0.1:2379","http://127.0.0.1:4001"]},{"id":"94088180e21eb87b","peerURLs":["http://127.0.0.1:7002"],"name":"node2","clientURLs":["http://127.0.0.1:4002"]}]}`),
+			want: memberCollection(
+				[]Member{
+					{
+						ID:   "2745e2525fce8fe",
+						Name: "node3",
+						PeerURLs: []string{
+							"http://127.0.0.1:7003",
+						},
+						ClientURLs: []string{
+							"http://127.0.0.1:4003",
+						},
+					},
+					{
+						ID:   "42134f434382925",
+						Name: "node1",
+						PeerURLs: []string{
+							"http://127.0.0.1:2380",
+							"http://127.0.0.1:7001",
+						},
+						ClientURLs: []string{
+							"http://127.0.0.1:2379",
+							"http://127.0.0.1:4001",
+						},
+					},
+					{
+						ID:   "94088180e21eb87b",
+						Name: "node2",
+						PeerURLs: []string{
+							"http://127.0.0.1:7002",
+						},
+						ClientURLs: []string{
+							"http://127.0.0.1:4002",
+						},
+					},
+				},
+			),
+		},
+	}
+
+	for i, tt := range tests {
+		var got memberCollection
+		err := json.Unmarshal(tt.body, &got)
+		if err != nil {
+			t.Errorf("#%d: unexpected error: %v", i, err)
+			continue
+		}
+
+		if !reflect.DeepEqual(tt.want, got) {
+			t.Errorf("#%d: incorrect output: want=%#v, got=%#v", i, tt.want, got)
+		}
+	}
+}
+
+func TestMemberCreateRequestMarshal(t *testing.T) {
+	req := memberCreateOrUpdateRequest{
+		PeerURLs: types.URLs([]url.URL{
+			url.URL{Scheme: "http", Host: "127.0.0.1:8081"},
+			url.URL{Scheme: "https", Host: "127.0.0.1:8080"},
+		}),
+	}
+	want := []byte(`{"peerURLs":["http://127.0.0.1:8081","https://127.0.0.1:8080"]}`)
+
+	got, err := json.Marshal(&req)
+	if err != nil {
+		t.Fatalf("Marshal returned unexpected err=%v", err)
+	}
+
+	if !reflect.DeepEqual(want, got) {
+		t.Fatalf("Failed to marshal memberCreateRequest: want=%s, got=%s", want, got)
+	}
+}
+
+func TestHTTPMembersAPIAddSuccess(t *testing.T) {
+	wantAction := &membersAPIActionAdd{
+		peerURLs: types.URLs([]url.URL{
+			url.URL{Scheme: "http", Host: "127.0.0.1:7002"},
+		}),
+	}
+
+	mAPI := &httpMembersAPI{
+		client: &actionAssertingHTTPClient{
+			t:   t,
+			act: wantAction,
+			resp: http.Response{
+				StatusCode: http.StatusCreated,
+			},
+			body: []byte(`{"id":"94088180e21eb87b","peerURLs":["http://127.0.0.1:7002"]}`),
+		},
+	}
+
+	wantResponseMember := &Member{
+		ID:       "94088180e21eb87b",
+		PeerURLs: []string{"http://127.0.0.1:7002"},
+	}
+
+	m, err := mAPI.Add(context.Background(), "http://127.0.0.1:7002")
+	if err != nil {
+		t.Errorf("got non-nil err: %#v", err)
+	}
+	if !reflect.DeepEqual(wantResponseMember, m) {
+		t.Errorf("incorrect Member: want=%#v got=%#v", wantResponseMember, m)
+	}
+}
+
+func TestHTTPMembersAPIAddError(t *testing.T) {
+	okPeer := "http://example.com:2379"
+	tests := []struct {
+		peerURL string
+		client  httpClient
+
+		// if wantErr == nil, assert that the returned error is non-nil
+		// if wantErr != nil, assert that the returned error matches
+		wantErr error
+	}{
+		// malformed peer URL
+		{
+			peerURL: ":",
+		},
+
+		// generic httpClient failure
+		{
+			peerURL: okPeer,
+			client:  &staticHTTPClient{err: errors.New("fail!")},
+		},
+
+		// unrecognized HTTP status code
+		{
+			peerURL: okPeer,
+			client: &staticHTTPClient{
+				resp: http.Response{StatusCode: http.StatusTeapot},
+			},
+		},
+
+		// unmarshal body into membersError on StatusConflict
+		{
+			peerURL: okPeer,
+			client: &staticHTTPClient{
+				resp: http.Response{
+					StatusCode: http.StatusConflict,
+				},
+				body: []byte(`{"message":"fail!"}`),
+			},
+			wantErr: membersError{Message: "fail!"},
+		},
+
+		// fail to unmarshal body on StatusConflict
+		{
+			peerURL: okPeer,
+			client: &staticHTTPClient{
+				resp: http.Response{
+					StatusCode: http.StatusConflict,
+				},
+				body: []byte(`{"`),
+			},
+		},
+
+		// fail to unmarshal body on StatusCreated
+		{
+			peerURL: okPeer,
+			client: &staticHTTPClient{
+				resp: http.Response{
+					StatusCode: http.StatusCreated,
+				},
+				body: []byte(`{"id":"XX`),
+			},
+		},
+	}
+
+	for i, tt := range tests {
+		mAPI := &httpMembersAPI{client: tt.client}
+		m, err := mAPI.Add(context.Background(), tt.peerURL)
+		if err == nil {
+			t.Errorf("#%d: got nil err", i)
+		}
+		if tt.wantErr != nil && !reflect.DeepEqual(tt.wantErr, err) {
+			t.Errorf("#%d: incorrect error: want=%#v got=%#v", i, tt.wantErr, err)
+		}
+		if m != nil {
+			t.Errorf("#%d: got non-nil Member", i)
+		}
+	}
+}
+
+func TestHTTPMembersAPIRemoveSuccess(t *testing.T) {
+	wantAction := &membersAPIActionRemove{
+		memberID: "94088180e21eb87b",
+	}
+
+	mAPI := &httpMembersAPI{
+		client: &actionAssertingHTTPClient{
+			t:   t,
+			act: wantAction,
+			resp: http.Response{
+				StatusCode: http.StatusNoContent,
+			},
+		},
+	}
+
+	if err := mAPI.Remove(context.Background(), "94088180e21eb87b"); err != nil {
+		t.Errorf("got non-nil err: %#v", err)
+	}
+}
+
+func TestHTTPMembersAPIRemoveFail(t *testing.T) {
+	tests := []httpClient{
+		// generic error
+		&staticHTTPClient{
+			err: errors.New("fail!"),
+		},
+
+		// unexpected HTTP status code
+		&staticHTTPClient{
+			resp: http.Response{
+				StatusCode: http.StatusInternalServerError,
+			},
+		},
+	}
+
+	for i, tt := range tests {
+		mAPI := &httpMembersAPI{client: tt}
+		if err := mAPI.Remove(context.Background(), "94088180e21eb87b"); err == nil {
+			t.Errorf("#%d: got nil err", i)
+		}
+	}
+}
+
+func TestHTTPMembersAPIListSuccess(t *testing.T) {
+	wantAction := &membersAPIActionList{}
+	mAPI := &httpMembersAPI{
+		client: &actionAssertingHTTPClient{
+			t:   t,
+			act: wantAction,
+			resp: http.Response{
+				StatusCode: http.StatusOK,
+			},
+			body: []byte(`{"members":[{"id":"94088180e21eb87b","name":"node2","peerURLs":["http://127.0.0.1:7002"],"clientURLs":["http://127.0.0.1:4002"]}]}`),
+		},
+	}
+
+	wantResponseMembers := []Member{
+		Member{
+			ID:         "94088180e21eb87b",
+			Name:       "node2",
+			PeerURLs:   []string{"http://127.0.0.1:7002"},
+			ClientURLs: []string{"http://127.0.0.1:4002"},
+		},
+	}
+
+	m, err := mAPI.List(context.Background())
+	if err != nil {
+		t.Errorf("got non-nil err: %#v", err)
+	}
+	if !reflect.DeepEqual(wantResponseMembers, m) {
+		t.Errorf("incorrect Members: want=%#v got=%#v", wantResponseMembers, m)
+	}
+}
+
+func TestHTTPMembersAPIListError(t *testing.T) {
+	tests := []httpClient{
+		// generic httpClient failure
+		&staticHTTPClient{err: errors.New("fail!")},
+
+		// unrecognized HTTP status code
+		&staticHTTPClient{
+			resp: http.Response{StatusCode: http.StatusTeapot},
+		},
+
+		// fail to unmarshal body on StatusOK
+		&staticHTTPClient{
+			resp: http.Response{
+				StatusCode: http.StatusOK,
+			},
+			body: []byte(`[{"id":"XX`),
+		},
+	}
+
+	for i, tt := range tests {
+		mAPI := &httpMembersAPI{client: tt}
+		ms, err := mAPI.List(context.Background())
+		if err == nil {
+			t.Errorf("#%d: got nil err", i)
+		}
+		if ms != nil {
+			t.Errorf("#%d: got non-nil Member slice", i)
+		}
+	}
+}

+ 65 - 0
Godeps/_workspace/src/github.com/coreos/etcd/client/srv.go

@@ -0,0 +1,65 @@
+// Copyright 2015 CoreOS, Inc.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package client
+
+import (
+	"fmt"
+	"net"
+	"net/url"
+)
+
+var (
+	// indirection for testing
+	lookupSRV = net.LookupSRV
+)
+
+type srvDiscover struct{}
+
+// NewSRVDiscover constructs a new Dicoverer that uses the stdlib to lookup SRV records.
+func NewSRVDiscover() Discoverer {
+	return &srvDiscover{}
+}
+
+// Discover looks up the etcd servers for the domain.
+func (d *srvDiscover) Discover(domain string) ([]string, error) {
+	var urls []*url.URL
+
+	updateURLs := func(service, scheme string) error {
+		_, addrs, err := lookupSRV(service, "tcp", domain)
+		if err != nil {
+			return err
+		}
+		for _, srv := range addrs {
+			urls = append(urls, &url.URL{
+				Scheme: scheme,
+				Host:   net.JoinHostPort(srv.Target, fmt.Sprintf("%d", srv.Port)),
+			})
+		}
+		return nil
+	}
+
+	errHTTPS := updateURLs("etcd-server-ssl", "https")
+	errHTTP := updateURLs("etcd-server", "http")
+
+	if errHTTPS != nil && errHTTP != nil {
+		return nil, fmt.Errorf("dns lookup errors: %s and %s", errHTTPS, errHTTP)
+	}
+
+	endpoints := make([]string, len(urls))
+	for i := range urls {
+		endpoints[i] = urls[i].String()
+	}
+	return endpoints, nil
+}

+ 102 - 0
Godeps/_workspace/src/github.com/coreos/etcd/client/srv_test.go

@@ -0,0 +1,102 @@
+// Copyright 2015 CoreOS, Inc.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package client
+
+import (
+	"errors"
+	"net"
+	"reflect"
+	"testing"
+)
+
+func TestSRVDiscover(t *testing.T) {
+	defer func() { lookupSRV = net.LookupSRV }()
+
+	tests := []struct {
+		withSSL    []*net.SRV
+		withoutSSL []*net.SRV
+		expected   []string
+	}{
+		{
+			[]*net.SRV{},
+			[]*net.SRV{},
+			[]string{},
+		},
+		{
+			[]*net.SRV{
+				&net.SRV{Target: "10.0.0.1", Port: 2480},
+				&net.SRV{Target: "10.0.0.2", Port: 2480},
+				&net.SRV{Target: "10.0.0.3", Port: 2480},
+			},
+			[]*net.SRV{},
+			[]string{"https://10.0.0.1:2480", "https://10.0.0.2:2480", "https://10.0.0.3:2480"},
+		},
+		{
+			[]*net.SRV{
+				&net.SRV{Target: "10.0.0.1", Port: 2480},
+				&net.SRV{Target: "10.0.0.2", Port: 2480},
+				&net.SRV{Target: "10.0.0.3", Port: 2480},
+			},
+			[]*net.SRV{
+				&net.SRV{Target: "10.0.0.1", Port: 7001},
+			},
+			[]string{"https://10.0.0.1:2480", "https://10.0.0.2:2480", "https://10.0.0.3:2480", "http://10.0.0.1:7001"},
+		},
+		{
+			[]*net.SRV{
+				&net.SRV{Target: "10.0.0.1", Port: 2480},
+				&net.SRV{Target: "10.0.0.2", Port: 2480},
+				&net.SRV{Target: "10.0.0.3", Port: 2480},
+			},
+			[]*net.SRV{
+				&net.SRV{Target: "10.0.0.1", Port: 7001},
+			},
+			[]string{"https://10.0.0.1:2480", "https://10.0.0.2:2480", "https://10.0.0.3:2480", "http://10.0.0.1:7001"},
+		},
+		{
+			[]*net.SRV{
+				&net.SRV{Target: "a.example.com", Port: 2480},
+				&net.SRV{Target: "b.example.com", Port: 2480},
+				&net.SRV{Target: "c.example.com", Port: 2480},
+			},
+			[]*net.SRV{},
+			[]string{"https://a.example.com:2480", "https://b.example.com:2480", "https://c.example.com:2480"},
+		},
+	}
+
+	for i, tt := range tests {
+		lookupSRV = func(service string, proto string, domain string) (string, []*net.SRV, error) {
+			if service == "etcd-server-ssl" {
+				return "", tt.withSSL, nil
+			}
+			if service == "etcd-server" {
+				return "", tt.withoutSSL, nil
+			}
+			return "", nil, errors.New("Unkown service in mock")
+		}
+
+		d := NewSRVDiscover()
+
+		endpoints, err := d.Discover("example.com")
+		if err != nil {
+			t.Fatalf("%d: err: %#v", i, err)
+		}
+
+		if !reflect.DeepEqual(endpoints, tt.expected) {
+			t.Errorf("#%d: endpoints = %v, want %v", i, endpoints, tt.expected)
+		}
+
+	}
+}

+ 5 - 1
Godeps/_workspace/src/github.com/coreos/etcd/pkg/transport/keepalive_listener.go

@@ -16,6 +16,7 @@ package transport
 
 
 import (
 import (
 	"crypto/tls"
 	"crypto/tls"
+	"fmt"
 	"net"
 	"net"
 	"time"
 	"time"
 )
 )
@@ -28,7 +29,10 @@ func NewKeepAliveListener(addr string, scheme string, info TLSInfo) (net.Listene
 		return nil, err
 		return nil, err
 	}
 	}
 
 
-	if !info.Empty() && scheme == "https" {
+	if scheme == "https" {
+		if info.Empty() {
+			return nil, fmt.Errorf("cannot listen on TLS for %s: KeyFile and CertFile are not presented", scheme+"://"+addr)
+		}
 		cfg, err := info.ServerConfig()
 		cfg, err := info.ServerConfig()
 		if err != nil {
 		if err != nil {
 			return nil, err
 			return nil, err

+ 7 - 0
Godeps/_workspace/src/github.com/coreos/etcd/pkg/transport/keepalive_listener_test.go

@@ -62,3 +62,10 @@ func TestNewKeepAliveListener(t *testing.T) {
 	conn.Close()
 	conn.Close()
 	tlsln.Close()
 	tlsln.Close()
 }
 }
+
+func TestNewKeepAliveListenerTLSEmptyInfo(t *testing.T) {
+	_, err := NewListener("127.0.0.1:0", "https", TLSInfo{})
+	if err == nil {
+		t.Errorf("err = nil, want not presented error")
+	}
+}

+ 41 - 0
Godeps/_workspace/src/github.com/coreos/etcd/pkg/types/id.go

@@ -0,0 +1,41 @@
+// Copyright 2015 CoreOS, Inc.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package types
+
+import (
+	"strconv"
+)
+
+// ID represents a generic identifier which is canonically
+// stored as a uint64 but is typically represented as a
+// base-16 string for input/output
+type ID uint64
+
+func (i ID) String() string {
+	return strconv.FormatUint(uint64(i), 16)
+}
+
+// IDFromString attempts to create an ID from a base-16 string.
+func IDFromString(s string) (ID, error) {
+	i, err := strconv.ParseUint(s, 16, 64)
+	return ID(i), err
+}
+
+// IDSlice implements the sort interface
+type IDSlice []ID
+
+func (p IDSlice) Len() int           { return len(p) }
+func (p IDSlice) Less(i, j int) bool { return uint64(p[i]) < uint64(p[j]) }
+func (p IDSlice) Swap(i, j int)      { p[i], p[j] = p[j], p[i] }

+ 95 - 0
Godeps/_workspace/src/github.com/coreos/etcd/pkg/types/id_test.go

@@ -0,0 +1,95 @@
+// Copyright 2015 CoreOS, Inc.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package types
+
+import (
+	"reflect"
+	"sort"
+	"testing"
+)
+
+func TestIDString(t *testing.T) {
+	tests := []struct {
+		input ID
+		want  string
+	}{
+		{
+			input: 12,
+			want:  "c",
+		},
+		{
+			input: 4918257920282737594,
+			want:  "444129853c343bba",
+		},
+	}
+
+	for i, tt := range tests {
+		got := tt.input.String()
+		if tt.want != got {
+			t.Errorf("#%d: ID.String failure: want=%v, got=%v", i, tt.want, got)
+		}
+	}
+}
+
+func TestIDFromString(t *testing.T) {
+	tests := []struct {
+		input string
+		want  ID
+	}{
+		{
+			input: "17",
+			want:  23,
+		},
+		{
+			input: "612840dae127353",
+			want:  437557308098245459,
+		},
+	}
+
+	for i, tt := range tests {
+		got, err := IDFromString(tt.input)
+		if err != nil {
+			t.Errorf("#%d: IDFromString failure: err=%v", i, err)
+			continue
+		}
+		if tt.want != got {
+			t.Errorf("#%d: IDFromString failure: want=%v, got=%v", i, tt.want, got)
+		}
+	}
+}
+
+func TestIDFromStringFail(t *testing.T) {
+	tests := []string{
+		"",
+		"XXX",
+		"612840dae127353612840dae127353",
+	}
+
+	for i, tt := range tests {
+		_, err := IDFromString(tt)
+		if err == nil {
+			t.Fatalf("#%d: IDFromString expected error, but err=nil", i)
+		}
+	}
+}
+
+func TestIDSlice(t *testing.T) {
+	g := []ID{10, 500, 5, 1, 100, 25}
+	w := []ID{1, 5, 10, 25, 100, 500}
+	sort.Sort(IDSlice(g))
+	if !reflect.DeepEqual(g, w) {
+		t.Errorf("slice after sort = %#v, want %#v", g, w)
+	}
+}

+ 178 - 0
Godeps/_workspace/src/github.com/coreos/etcd/pkg/types/set.go

@@ -0,0 +1,178 @@
+// Copyright 2015 CoreOS, Inc.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package types
+
+import (
+	"reflect"
+	"sort"
+	"sync"
+)
+
+type Set interface {
+	Add(string)
+	Remove(string)
+	Contains(string) bool
+	Equals(Set) bool
+	Length() int
+	Values() []string
+	Copy() Set
+	Sub(Set) Set
+}
+
+func NewUnsafeSet(values ...string) *unsafeSet {
+	set := &unsafeSet{make(map[string]struct{})}
+	for _, v := range values {
+		set.Add(v)
+	}
+	return set
+}
+
+func NewThreadsafeSet(values ...string) *tsafeSet {
+	us := NewUnsafeSet(values...)
+	return &tsafeSet{us, sync.RWMutex{}}
+}
+
+type unsafeSet struct {
+	d map[string]struct{}
+}
+
+// Add adds a new value to the set (no-op if the value is already present)
+func (us *unsafeSet) Add(value string) {
+	us.d[value] = struct{}{}
+}
+
+// Remove removes the given value from the set
+func (us *unsafeSet) Remove(value string) {
+	delete(us.d, value)
+}
+
+// Contains returns whether the set contains the given value
+func (us *unsafeSet) Contains(value string) (exists bool) {
+	_, exists = us.d[value]
+	return
+}
+
+// ContainsAll returns whether the set contains all given values
+func (us *unsafeSet) ContainsAll(values []string) bool {
+	for _, s := range values {
+		if !us.Contains(s) {
+			return false
+		}
+	}
+	return true
+}
+
+// Equals returns whether the contents of two sets are identical
+func (us *unsafeSet) Equals(other Set) bool {
+	v1 := sort.StringSlice(us.Values())
+	v2 := sort.StringSlice(other.Values())
+	v1.Sort()
+	v2.Sort()
+	return reflect.DeepEqual(v1, v2)
+}
+
+// Length returns the number of elements in the set
+func (us *unsafeSet) Length() int {
+	return len(us.d)
+}
+
+// Values returns the values of the Set in an unspecified order.
+func (us *unsafeSet) Values() (values []string) {
+	values = make([]string, 0)
+	for val, _ := range us.d {
+		values = append(values, val)
+	}
+	return
+}
+
+// Copy creates a new Set containing the values of the first
+func (us *unsafeSet) Copy() Set {
+	cp := NewUnsafeSet()
+	for val, _ := range us.d {
+		cp.Add(val)
+	}
+
+	return cp
+}
+
+// Sub removes all elements in other from the set
+func (us *unsafeSet) Sub(other Set) Set {
+	oValues := other.Values()
+	result := us.Copy().(*unsafeSet)
+
+	for _, val := range oValues {
+		if _, ok := result.d[val]; !ok {
+			continue
+		}
+		delete(result.d, val)
+	}
+
+	return result
+}
+
+type tsafeSet struct {
+	us *unsafeSet
+	m  sync.RWMutex
+}
+
+func (ts *tsafeSet) Add(value string) {
+	ts.m.Lock()
+	defer ts.m.Unlock()
+	ts.us.Add(value)
+}
+
+func (ts *tsafeSet) Remove(value string) {
+	ts.m.Lock()
+	defer ts.m.Unlock()
+	ts.us.Remove(value)
+}
+
+func (ts *tsafeSet) Contains(value string) (exists bool) {
+	ts.m.RLock()
+	defer ts.m.RUnlock()
+	return ts.us.Contains(value)
+}
+
+func (ts *tsafeSet) Equals(other Set) bool {
+	ts.m.RLock()
+	defer ts.m.RUnlock()
+	return ts.us.Equals(other)
+}
+
+func (ts *tsafeSet) Length() int {
+	ts.m.RLock()
+	defer ts.m.RUnlock()
+	return ts.us.Length()
+}
+
+func (ts *tsafeSet) Values() (values []string) {
+	ts.m.RLock()
+	defer ts.m.RUnlock()
+	return ts.us.Values()
+}
+
+func (ts *tsafeSet) Copy() Set {
+	ts.m.RLock()
+	defer ts.m.RUnlock()
+	usResult := ts.us.Copy().(*unsafeSet)
+	return &tsafeSet{usResult, sync.RWMutex{}}
+}
+
+func (ts *tsafeSet) Sub(other Set) Set {
+	ts.m.RLock()
+	defer ts.m.RUnlock()
+	usResult := ts.us.Sub(other).(*unsafeSet)
+	return &tsafeSet{usResult, sync.RWMutex{}}
+}

+ 186 - 0
Godeps/_workspace/src/github.com/coreos/etcd/pkg/types/set_test.go

@@ -0,0 +1,186 @@
+// Copyright 2015 CoreOS, Inc.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package types
+
+import (
+	"reflect"
+	"sort"
+	"testing"
+)
+
+func TestUnsafeSet(t *testing.T) {
+	driveSetTests(t, NewUnsafeSet())
+}
+
+func TestThreadsafeSet(t *testing.T) {
+	driveSetTests(t, NewThreadsafeSet())
+}
+
+// Check that two slices contents are equal; order is irrelevant
+func equal(a, b []string) bool {
+	as := sort.StringSlice(a)
+	bs := sort.StringSlice(b)
+	as.Sort()
+	bs.Sort()
+	return reflect.DeepEqual(as, bs)
+}
+
+func driveSetTests(t *testing.T, s Set) {
+	// Verify operations on an empty set
+	eValues := []string{}
+	values := s.Values()
+	if !reflect.DeepEqual(values, eValues) {
+		t.Fatalf("Expect values=%v got %v", eValues, values)
+	}
+	if l := s.Length(); l != 0 {
+		t.Fatalf("Expected length=0, got %d", l)
+	}
+	for _, v := range []string{"foo", "bar", "baz"} {
+		if s.Contains(v) {
+			t.Fatalf("Expect s.Contains(%q) to be fale, got true", v)
+		}
+	}
+
+	// Add three items, ensure they show up
+	s.Add("foo")
+	s.Add("bar")
+	s.Add("baz")
+
+	eValues = []string{"foo", "bar", "baz"}
+	values = s.Values()
+	if !equal(values, eValues) {
+		t.Fatalf("Expect values=%v got %v", eValues, values)
+	}
+
+	for _, v := range eValues {
+		if !s.Contains(v) {
+			t.Fatalf("Expect s.Contains(%q) to be true, got false", v)
+		}
+	}
+
+	if l := s.Length(); l != 3 {
+		t.Fatalf("Expected length=3, got %d", l)
+	}
+
+	// Add the same item a second time, ensuring it is not duplicated
+	s.Add("foo")
+
+	values = s.Values()
+	if !equal(values, eValues) {
+		t.Fatalf("Expect values=%v got %v", eValues, values)
+	}
+	if l := s.Length(); l != 3 {
+		t.Fatalf("Expected length=3, got %d", l)
+	}
+
+	// Remove all items, ensure they are gone
+	s.Remove("foo")
+	s.Remove("bar")
+	s.Remove("baz")
+
+	eValues = []string{}
+	values = s.Values()
+	if !equal(values, eValues) {
+		t.Fatalf("Expect values=%v got %v", eValues, values)
+	}
+
+	if l := s.Length(); l != 0 {
+		t.Fatalf("Expected length=0, got %d", l)
+	}
+
+	// Create new copies of the set, and ensure they are unlinked to the
+	// original Set by making modifications
+	s.Add("foo")
+	s.Add("bar")
+	cp1 := s.Copy()
+	cp2 := s.Copy()
+	s.Remove("foo")
+	cp3 := s.Copy()
+	cp1.Add("baz")
+
+	for i, tt := range []struct {
+		want []string
+		got  []string
+	}{
+		{[]string{"bar"}, s.Values()},
+		{[]string{"foo", "bar", "baz"}, cp1.Values()},
+		{[]string{"foo", "bar"}, cp2.Values()},
+		{[]string{"bar"}, cp3.Values()},
+	} {
+		if !equal(tt.want, tt.got) {
+			t.Fatalf("case %d: expect values=%v got %v", i, tt.want, tt.got)
+		}
+	}
+
+	for i, tt := range []struct {
+		want bool
+		got  bool
+	}{
+		{true, s.Equals(cp3)},
+		{true, cp3.Equals(s)},
+		{false, s.Equals(cp2)},
+		{false, s.Equals(cp1)},
+		{false, cp1.Equals(s)},
+		{false, cp2.Equals(s)},
+		{false, cp2.Equals(cp1)},
+	} {
+		if tt.got != tt.want {
+			t.Fatalf("case %d: want %t, got %t", i, tt.want, tt.got)
+
+		}
+	}
+
+	// Subtract values from a Set, ensuring a new Set is created and
+	// the original Sets are unmodified
+	sub1 := cp1.Sub(s)
+	sub2 := cp2.Sub(cp1)
+
+	for i, tt := range []struct {
+		want []string
+		got  []string
+	}{
+		{[]string{"foo", "bar", "baz"}, cp1.Values()},
+		{[]string{"foo", "bar"}, cp2.Values()},
+		{[]string{"bar"}, s.Values()},
+		{[]string{"foo", "baz"}, sub1.Values()},
+		{[]string{}, sub2.Values()},
+	} {
+		if !equal(tt.want, tt.got) {
+			t.Fatalf("case %d: expect values=%v got %v", i, tt.want, tt.got)
+		}
+	}
+}
+
+func TestUnsafeSetContainsAll(t *testing.T) {
+	vals := []string{"foo", "bar", "baz"}
+	s := NewUnsafeSet(vals...)
+
+	tests := []struct {
+		strs     []string
+		wcontain bool
+	}{
+		{[]string{}, true},
+		{vals[:1], true},
+		{vals[:2], true},
+		{vals, true},
+		{[]string{"cuz"}, false},
+		{[]string{vals[0], "cuz"}, false},
+	}
+	for i, tt := range tests {
+		if g := s.ContainsAll(tt.strs); g != tt.wcontain {
+			t.Errorf("#%d: ok = %v, want %v", i, g, tt.wcontain)
+		}
+	}
+}

+ 22 - 0
Godeps/_workspace/src/github.com/coreos/etcd/pkg/types/slice.go

@@ -0,0 +1,22 @@
+// Copyright 2015 CoreOS, Inc.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package types
+
+// Uint64Slice implements sort interface
+type Uint64Slice []uint64
+
+func (p Uint64Slice) Len() int           { return len(p) }
+func (p Uint64Slice) Less(i, j int) bool { return p[i] < p[j] }
+func (p Uint64Slice) Swap(i, j int)      { p[i], p[j] = p[j], p[i] }

+ 30 - 0
Godeps/_workspace/src/github.com/coreos/etcd/pkg/types/slice_test.go

@@ -0,0 +1,30 @@
+// Copyright 2015 CoreOS, Inc.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package types
+
+import (
+	"reflect"
+	"sort"
+	"testing"
+)
+
+func TestUint64Slice(t *testing.T) {
+	g := Uint64Slice{10, 500, 5, 1, 100, 25}
+	w := Uint64Slice{1, 5, 10, 25, 100, 500}
+	sort.Sort(g)
+	if !reflect.DeepEqual(g, w) {
+		t.Errorf("slice after sort = %#v, want %#v", g, w)
+	}
+}

+ 74 - 0
Godeps/_workspace/src/github.com/coreos/etcd/pkg/types/urls.go

@@ -0,0 +1,74 @@
+// Copyright 2015 CoreOS, Inc.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package types
+
+import (
+	"errors"
+	"fmt"
+	"net"
+	"net/url"
+	"sort"
+	"strings"
+)
+
+type URLs []url.URL
+
+func NewURLs(strs []string) (URLs, error) {
+	all := make([]url.URL, len(strs))
+	if len(all) == 0 {
+		return nil, errors.New("no valid URLs given")
+	}
+	for i, in := range strs {
+		in = strings.TrimSpace(in)
+		u, err := url.Parse(in)
+		if err != nil {
+			return nil, err
+		}
+		if u.Scheme != "http" && u.Scheme != "https" {
+			return nil, fmt.Errorf("URL scheme must be http or https: %s", in)
+		}
+		if _, _, err := net.SplitHostPort(u.Host); err != nil {
+			return nil, fmt.Errorf(`URL address does not have the form "host:port": %s`, in)
+		}
+		if u.Path != "" {
+			return nil, fmt.Errorf("URL must not contain a path: %s", in)
+		}
+		all[i] = *u
+	}
+	us := URLs(all)
+	us.Sort()
+
+	return us, nil
+}
+
+func (us URLs) String() string {
+	return strings.Join(us.StringSlice(), ",")
+}
+
+func (us *URLs) Sort() {
+	sort.Sort(us)
+}
+func (us URLs) Len() int           { return len(us) }
+func (us URLs) Less(i, j int) bool { return us[i].String() < us[j].String() }
+func (us URLs) Swap(i, j int)      { us[i], us[j] = us[j], us[i] }
+
+func (us URLs) StringSlice() []string {
+	out := make([]string, len(us))
+	for i := range us {
+		out[i] = us[i].String()
+	}
+
+	return out
+}

+ 169 - 0
Godeps/_workspace/src/github.com/coreos/etcd/pkg/types/urls_test.go

@@ -0,0 +1,169 @@
+// Copyright 2015 CoreOS, Inc.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package types
+
+import (
+	"reflect"
+	"testing"
+
+	"github.com/coreos/etcd/pkg/testutil"
+)
+
+func TestNewURLs(t *testing.T) {
+	tests := []struct {
+		strs  []string
+		wurls URLs
+	}{
+		{
+			[]string{"http://127.0.0.1:2379"},
+			testutil.MustNewURLs(t, []string{"http://127.0.0.1:2379"}),
+		},
+		// it can trim space
+		{
+			[]string{"   http://127.0.0.1:2379    "},
+			testutil.MustNewURLs(t, []string{"http://127.0.0.1:2379"}),
+		},
+		// it does sort
+		{
+			[]string{
+				"http://127.0.0.2:2379",
+				"http://127.0.0.1:2379",
+			},
+			testutil.MustNewURLs(t, []string{
+				"http://127.0.0.1:2379",
+				"http://127.0.0.2:2379",
+			}),
+		},
+	}
+	for i, tt := range tests {
+		urls, _ := NewURLs(tt.strs)
+		if !reflect.DeepEqual(urls, tt.wurls) {
+			t.Errorf("#%d: urls = %+v, want %+v", i, urls, tt.wurls)
+		}
+	}
+}
+
+func TestURLsString(t *testing.T) {
+	tests := []struct {
+		us   URLs
+		wstr string
+	}{
+		{
+			URLs{},
+			"",
+		},
+		{
+			testutil.MustNewURLs(t, []string{"http://127.0.0.1:2379"}),
+			"http://127.0.0.1:2379",
+		},
+		{
+			testutil.MustNewURLs(t, []string{
+				"http://127.0.0.1:2379",
+				"http://127.0.0.2:2379",
+			}),
+			"http://127.0.0.1:2379,http://127.0.0.2:2379",
+		},
+		{
+			testutil.MustNewURLs(t, []string{
+				"http://127.0.0.2:2379",
+				"http://127.0.0.1:2379",
+			}),
+			"http://127.0.0.2:2379,http://127.0.0.1:2379",
+		},
+	}
+	for i, tt := range tests {
+		g := tt.us.String()
+		if g != tt.wstr {
+			t.Errorf("#%d: string = %s, want %s", i, g, tt.wstr)
+		}
+	}
+}
+
+func TestURLsSort(t *testing.T) {
+	g := testutil.MustNewURLs(t, []string{
+		"http://127.0.0.4:2379",
+		"http://127.0.0.2:2379",
+		"http://127.0.0.1:2379",
+		"http://127.0.0.3:2379",
+	})
+	w := testutil.MustNewURLs(t, []string{
+		"http://127.0.0.1:2379",
+		"http://127.0.0.2:2379",
+		"http://127.0.0.3:2379",
+		"http://127.0.0.4:2379",
+	})
+	gurls := URLs(g)
+	gurls.Sort()
+	if !reflect.DeepEqual(g, w) {
+		t.Errorf("URLs after sort = %#v, want %#v", g, w)
+	}
+}
+
+func TestURLsStringSlice(t *testing.T) {
+	tests := []struct {
+		us   URLs
+		wstr []string
+	}{
+		{
+			URLs{},
+			[]string{},
+		},
+		{
+			testutil.MustNewURLs(t, []string{"http://127.0.0.1:2379"}),
+			[]string{"http://127.0.0.1:2379"},
+		},
+		{
+			testutil.MustNewURLs(t, []string{
+				"http://127.0.0.1:2379",
+				"http://127.0.0.2:2379",
+			}),
+			[]string{"http://127.0.0.1:2379", "http://127.0.0.2:2379"},
+		},
+		{
+			testutil.MustNewURLs(t, []string{
+				"http://127.0.0.2:2379",
+				"http://127.0.0.1:2379",
+			}),
+			[]string{"http://127.0.0.2:2379", "http://127.0.0.1:2379"},
+		},
+	}
+	for i, tt := range tests {
+		g := tt.us.StringSlice()
+		if !reflect.DeepEqual(g, tt.wstr) {
+			t.Errorf("#%d: string slice = %+v, want %+v", i, g, tt.wstr)
+		}
+	}
+}
+
+func TestNewURLsFail(t *testing.T) {
+	tests := [][]string{
+		// no urls given
+		{},
+		// missing protocol scheme
+		{"://127.0.0.1:2379"},
+		// unsupported scheme
+		{"mailto://127.0.0.1:2379"},
+		// not conform to host:port
+		{"http://127.0.0.1"},
+		// contain a path
+		{"http://127.0.0.1:2379/path"},
+	}
+	for i, tt := range tests {
+		_, err := NewURLs(tt)
+		if err == nil {
+			t.Errorf("#%d: err = nil, but error", i)
+		}
+	}
+}

+ 75 - 0
Godeps/_workspace/src/github.com/coreos/etcd/pkg/types/urlsmap.go

@@ -0,0 +1,75 @@
+// Copyright 2015 CoreOS, Inc.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package types
+
+import (
+	"fmt"
+	"net/url"
+	"sort"
+	"strings"
+)
+
+type URLsMap map[string]URLs
+
+// NewURLsMap returns a URLsMap instantiated from the given string,
+// which consists of discovery-formatted names-to-URLs, like:
+// mach0=http://1.1.1.1:2380,mach0=http://2.2.2.2::2380,mach1=http://3.3.3.3:2380,mach2=http://4.4.4.4:2380
+func NewURLsMap(s string) (URLsMap, error) {
+	cl := URLsMap{}
+	v, err := url.ParseQuery(strings.Replace(s, ",", "&", -1))
+	if err != nil {
+		return nil, err
+	}
+	for name, urls := range v {
+		if len(urls) == 0 || urls[0] == "" {
+			return nil, fmt.Errorf("empty URL given for %q", name)
+		}
+		us, err := NewURLs(urls)
+		if err != nil {
+			return nil, err
+		}
+		cl[name] = us
+	}
+	return cl, nil
+}
+
+// String returns NameURLPairs into discovery-formatted name-to-URLs sorted by name.
+func (c URLsMap) String() string {
+	pairs := make([]string, 0)
+	for name, urls := range c {
+		for _, url := range urls {
+			pairs = append(pairs, fmt.Sprintf("%s=%s", name, url.String()))
+		}
+	}
+	sort.Strings(pairs)
+	return strings.Join(pairs, ",")
+}
+
+// URLs returns a list of all URLs.
+// The returned list is sorted in ascending lexicographical order.
+func (c URLsMap) URLs() []string {
+	urls := make([]string, 0)
+	for _, us := range c {
+		for _, u := range us {
+			urls = append(urls, u.String())
+		}
+	}
+	sort.Strings(urls)
+	return urls
+}
+
+func (c URLsMap) Len() int {
+	return len(c)
+}

+ 69 - 0
Godeps/_workspace/src/github.com/coreos/etcd/pkg/types/urlsmap_test.go

@@ -0,0 +1,69 @@
+// Copyright 2015 CoreOS, Inc.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package types
+
+import (
+	"reflect"
+	"testing"
+
+	"github.com/coreos/etcd/pkg/testutil"
+)
+
+func TestParseInitialCluster(t *testing.T) {
+	c, err := NewURLsMap("mem1=http://10.0.0.1:2379,mem1=http://128.193.4.20:2379,mem2=http://10.0.0.2:2379,default=http://127.0.0.1:2379")
+	if err != nil {
+		t.Fatalf("unexpected parse error: %v", err)
+	}
+	wc := URLsMap(map[string]URLs{
+		"mem1":    testutil.MustNewURLs(t, []string{"http://10.0.0.1:2379", "http://128.193.4.20:2379"}),
+		"mem2":    testutil.MustNewURLs(t, []string{"http://10.0.0.2:2379"}),
+		"default": testutil.MustNewURLs(t, []string{"http://127.0.0.1:2379"}),
+	})
+	if !reflect.DeepEqual(c, wc) {
+		t.Errorf("cluster = %+v, want %+v", c, wc)
+	}
+}
+
+func TestParseInitialClusterBad(t *testing.T) {
+	tests := []string{
+		// invalid URL
+		"%^",
+		// no URL defined for member
+		"mem1=,mem2=http://128.193.4.20:2379,mem3=http://10.0.0.2:2379",
+		"mem1,mem2=http://128.193.4.20:2379,mem3=http://10.0.0.2:2379",
+		// bad URL for member
+		"default=http://localhost/",
+	}
+	for i, tt := range tests {
+		if _, err := NewURLsMap(tt); err == nil {
+			t.Errorf("#%d: unexpected successful parse, want err", i)
+		}
+	}
+}
+
+func TestNameURLPairsString(t *testing.T) {
+	cls := URLsMap(map[string]URLs{
+		"abc": testutil.MustNewURLs(t, []string{"http://1.1.1.1:1111", "http://0.0.0.0:0000"}),
+		"def": testutil.MustNewURLs(t, []string{"http://2.2.2.2:2222"}),
+		"ghi": testutil.MustNewURLs(t, []string{"http://3.3.3.3:1234", "http://127.0.0.1:2380"}),
+		// no PeerURLs = not included
+		"four": testutil.MustNewURLs(t, []string{}),
+		"five": testutil.MustNewURLs(t, nil),
+	})
+	w := "abc=http://0.0.0.0:0000,abc=http://1.1.1.1:1111,def=http://2.2.2.2:2222,ghi=http://127.0.0.1:2380,ghi=http://3.3.3.3:1234"
+	if g := cls.String(); g != w {
+		t.Fatalf("NameURLPairs.String():\ngot  %#v\nwant %#v", g, w)
+	}
+}

+ 0 - 23
Godeps/_workspace/src/github.com/coreos/go-etcd/etcd/add_child.go

@@ -1,23 +0,0 @@
-package etcd
-
-// Add a new directory with a random etcd-generated key under the given path.
-func (c *Client) AddChildDir(key string, ttl uint64) (*Response, error) {
-	raw, err := c.post(key, "", ttl)
-
-	if err != nil {
-		return nil, err
-	}
-
-	return raw.Unmarshal()
-}
-
-// Add a new file with a random etcd-generated key under the given path.
-func (c *Client) AddChild(key string, value string, ttl uint64) (*Response, error) {
-	raw, err := c.post(key, value, ttl)
-
-	if err != nil {
-		return nil, err
-	}
-
-	return raw.Unmarshal()
-}

+ 0 - 73
Godeps/_workspace/src/github.com/coreos/go-etcd/etcd/add_child_test.go

@@ -1,73 +0,0 @@
-package etcd
-
-import "testing"
-
-func TestAddChild(t *testing.T) {
-	c := NewClient(nil)
-	defer func() {
-		c.Delete("fooDir", true)
-		c.Delete("nonexistentDir", true)
-	}()
-
-	c.CreateDir("fooDir", 5)
-
-	_, err := c.AddChild("fooDir", "v0", 5)
-	if err != nil {
-		t.Fatal(err)
-	}
-
-	_, err = c.AddChild("fooDir", "v1", 5)
-	if err != nil {
-		t.Fatal(err)
-	}
-
-	resp, err := c.Get("fooDir", true, false)
-	// The child with v0 should proceed the child with v1 because it's added
-	// earlier, so it should have a lower key.
-	if !(len(resp.Node.Nodes) == 2 && (resp.Node.Nodes[0].Value == "v0" && resp.Node.Nodes[1].Value == "v1")) {
-		t.Fatalf("AddChild 1 failed.  There should be two chlidren whose values are v0 and v1, respectively."+
-			"  The response was: %#v", resp)
-	}
-
-	// Creating a child under a nonexistent directory should succeed.
-	// The directory should be created.
-	resp, err = c.AddChild("nonexistentDir", "foo", 5)
-	if err != nil {
-		t.Fatal(err)
-	}
-}
-
-func TestAddChildDir(t *testing.T) {
-	c := NewClient(nil)
-	defer func() {
-		c.Delete("fooDir", true)
-		c.Delete("nonexistentDir", true)
-	}()
-
-	c.CreateDir("fooDir", 5)
-
-	_, err := c.AddChildDir("fooDir", 5)
-	if err != nil {
-		t.Fatal(err)
-	}
-
-	_, err = c.AddChildDir("fooDir", 5)
-	if err != nil {
-		t.Fatal(err)
-	}
-
-	resp, err := c.Get("fooDir", true, false)
-	// The child with v0 should proceed the child with v1 because it's added
-	// earlier, so it should have a lower key.
-	if !(len(resp.Node.Nodes) == 2 && (len(resp.Node.Nodes[0].Nodes) == 0 && len(resp.Node.Nodes[1].Nodes) == 0)) {
-		t.Fatalf("AddChildDir 1 failed.  There should be two chlidren whose values are v0 and v1, respectively."+
-			"  The response was: %#v", resp)
-	}
-
-	// Creating a child under a nonexistent directory should succeed.
-	// The directory should be created.
-	resp, err = c.AddChildDir("nonexistentDir", 5)
-	if err != nil {
-		t.Fatal(err)
-	}
-}

+ 0 - 481
Godeps/_workspace/src/github.com/coreos/go-etcd/etcd/client.go

@@ -1,481 +0,0 @@
-package etcd
-
-import (
-	"crypto/tls"
-	"crypto/x509"
-	"encoding/json"
-	"errors"
-	"io"
-	"io/ioutil"
-	"math/rand"
-	"net"
-	"net/http"
-	"net/url"
-	"os"
-	"path"
-	"strings"
-	"time"
-)
-
-// See SetConsistency for how to use these constants.
-const (
-	// Using strings rather than iota because the consistency level
-	// could be persisted to disk, so it'd be better to use
-	// human-readable values.
-	STRONG_CONSISTENCY = "STRONG"
-	WEAK_CONSISTENCY   = "WEAK"
-)
-
-const (
-	defaultBufferSize = 10
-)
-
-func init() {
-	rand.Seed(int64(time.Now().Nanosecond()))
-}
-
-type Config struct {
-	CertFile    string        `json:"certFile"`
-	KeyFile     string        `json:"keyFile"`
-	CaCertFile  []string      `json:"caCertFiles"`
-	DialTimeout time.Duration `json:"timeout"`
-	Consistency string        `json:"consistency"`
-}
-
-type credentials struct {
-	username string
-	password string
-}
-
-type Client struct {
-	config      Config   `json:"config"`
-	cluster     *Cluster `json:"cluster"`
-	httpClient  *http.Client
-	credentials *credentials
-	transport   *http.Transport
-	persistence io.Writer
-	cURLch      chan string
-	// CheckRetry can be used to control the policy for failed requests
-	// and modify the cluster if needed.
-	// The client calls it before sending requests again, and
-	// stops retrying if CheckRetry returns some error. The cases that
-	// this function needs to handle include no response and unexpected
-	// http status code of response.
-	// If CheckRetry is nil, client will call the default one
-	// `DefaultCheckRetry`.
-	// Argument cluster is the etcd.Cluster object that these requests have been made on.
-	// Argument numReqs is the number of http.Requests that have been made so far.
-	// Argument lastResp is the http.Responses from the last request.
-	// Argument err is the reason of the failure.
-	CheckRetry func(cluster *Cluster, numReqs int,
-		lastResp http.Response, err error) error
-}
-
-// NewClient create a basic client that is configured to be used
-// with the given machine list.
-func NewClient(machines []string) *Client {
-	config := Config{
-		// default timeout is one second
-		DialTimeout: time.Second,
-		Consistency: WEAK_CONSISTENCY,
-	}
-
-	client := &Client{
-		cluster: NewCluster(machines),
-		config:  config,
-	}
-
-	client.initHTTPClient()
-	client.saveConfig()
-
-	return client
-}
-
-// NewTLSClient create a basic client with TLS configuration
-func NewTLSClient(machines []string, cert, key, caCert string) (*Client, error) {
-	// overwrite the default machine to use https
-	if len(machines) == 0 {
-		machines = []string{"https://127.0.0.1:4001"}
-	}
-
-	config := Config{
-		// default timeout is one second
-		DialTimeout: time.Second,
-		Consistency: WEAK_CONSISTENCY,
-		CertFile:    cert,
-		KeyFile:     key,
-		CaCertFile:  make([]string, 0),
-	}
-
-	client := &Client{
-		cluster: NewCluster(machines),
-		config:  config,
-	}
-
-	err := client.initHTTPSClient(cert, key)
-	if err != nil {
-		return nil, err
-	}
-
-	err = client.AddRootCA(caCert)
-
-	client.saveConfig()
-
-	return client, nil
-}
-
-// NewClientFromFile creates a client from a given file path.
-// The given file is expected to use the JSON format.
-func NewClientFromFile(fpath string) (*Client, error) {
-	fi, err := os.Open(fpath)
-	if err != nil {
-		return nil, err
-	}
-
-	defer func() {
-		if err := fi.Close(); err != nil {
-			panic(err)
-		}
-	}()
-
-	return NewClientFromReader(fi)
-}
-
-// NewClientFromReader creates a Client configured from a given reader.
-// The configuration is expected to use the JSON format.
-func NewClientFromReader(reader io.Reader) (*Client, error) {
-	c := new(Client)
-
-	b, err := ioutil.ReadAll(reader)
-	if err != nil {
-		return nil, err
-	}
-
-	err = json.Unmarshal(b, c)
-	if err != nil {
-		return nil, err
-	}
-	if c.config.CertFile == "" {
-		c.initHTTPClient()
-	} else {
-		err = c.initHTTPSClient(c.config.CertFile, c.config.KeyFile)
-	}
-
-	if err != nil {
-		return nil, err
-	}
-
-	for _, caCert := range c.config.CaCertFile {
-		if err := c.AddRootCA(caCert); err != nil {
-			return nil, err
-		}
-	}
-
-	return c, nil
-}
-
-// Override the Client's HTTP Transport object
-func (c *Client) SetTransport(tr *http.Transport) {
-	c.httpClient.Transport = tr
-	c.transport = tr
-}
-
-func (c *Client) SetCredentials(username, password string) {
-	c.credentials = &credentials{username, password}
-}
-
-func (c *Client) Close() {
-	c.transport.DisableKeepAlives = true
-	c.transport.CloseIdleConnections()
-}
-
-// initHTTPClient initializes a HTTP client for etcd client
-func (c *Client) initHTTPClient() {
-	c.transport = &http.Transport{
-		Dial: c.dial,
-		TLSClientConfig: &tls.Config{
-			InsecureSkipVerify: true,
-		},
-	}
-	c.httpClient = &http.Client{Transport: c.transport}
-}
-
-// initHTTPClient initializes a HTTPS client for etcd client
-func (c *Client) initHTTPSClient(cert, key string) error {
-	if cert == "" || key == "" {
-		return errors.New("Require both cert and key path")
-	}
-
-	tlsCert, err := tls.LoadX509KeyPair(cert, key)
-	if err != nil {
-		return err
-	}
-
-	tlsConfig := &tls.Config{
-		Certificates:       []tls.Certificate{tlsCert},
-		InsecureSkipVerify: true,
-	}
-
-	tr := &http.Transport{
-		TLSClientConfig: tlsConfig,
-		Dial:            c.dial,
-	}
-
-	c.httpClient = &http.Client{Transport: tr}
-	return nil
-}
-
-// SetPersistence sets a writer to which the config will be
-// written every time it's changed.
-func (c *Client) SetPersistence(writer io.Writer) {
-	c.persistence = writer
-}
-
-// SetConsistency changes the consistency level of the client.
-//
-// When consistency is set to STRONG_CONSISTENCY, all requests,
-// including GET, are sent to the leader.  This means that, assuming
-// the absence of leader failures, GET requests are guaranteed to see
-// the changes made by previous requests.
-//
-// When consistency is set to WEAK_CONSISTENCY, other requests
-// are still sent to the leader, but GET requests are sent to a
-// random server from the server pool.  This reduces the read
-// load on the leader, but it's not guaranteed that the GET requests
-// will see changes made by previous requests (they might have not
-// yet been committed on non-leader servers).
-func (c *Client) SetConsistency(consistency string) error {
-	if !(consistency == STRONG_CONSISTENCY || consistency == WEAK_CONSISTENCY) {
-		return errors.New("The argument must be either STRONG_CONSISTENCY or WEAK_CONSISTENCY.")
-	}
-	c.config.Consistency = consistency
-	return nil
-}
-
-// Sets the DialTimeout value
-func (c *Client) SetDialTimeout(d time.Duration) {
-	c.config.DialTimeout = d
-}
-
-// AddRootCA adds a root CA cert for the etcd client
-func (c *Client) AddRootCA(caCert string) error {
-	if c.httpClient == nil {
-		return errors.New("Client has not been initialized yet!")
-	}
-
-	certBytes, err := ioutil.ReadFile(caCert)
-	if err != nil {
-		return err
-	}
-
-	tr, ok := c.httpClient.Transport.(*http.Transport)
-
-	if !ok {
-		panic("AddRootCA(): Transport type assert should not fail")
-	}
-
-	if tr.TLSClientConfig.RootCAs == nil {
-		caCertPool := x509.NewCertPool()
-		ok = caCertPool.AppendCertsFromPEM(certBytes)
-		if ok {
-			tr.TLSClientConfig.RootCAs = caCertPool
-		}
-		tr.TLSClientConfig.InsecureSkipVerify = false
-	} else {
-		ok = tr.TLSClientConfig.RootCAs.AppendCertsFromPEM(certBytes)
-	}
-
-	if !ok {
-		err = errors.New("Unable to load caCert")
-	}
-
-	c.config.CaCertFile = append(c.config.CaCertFile, caCert)
-	c.saveConfig()
-
-	return err
-}
-
-// SetCluster updates cluster information using the given machine list.
-func (c *Client) SetCluster(machines []string) bool {
-	success := c.internalSyncCluster(machines)
-	return success
-}
-
-func (c *Client) GetCluster() []string {
-	return c.cluster.Machines
-}
-
-// SyncCluster updates the cluster information using the internal machine list.
-func (c *Client) SyncCluster() bool {
-	return c.internalSyncCluster(c.cluster.Machines)
-}
-
-// internalSyncCluster syncs cluster information using the given machine list.
-func (c *Client) internalSyncCluster(machines []string) bool {
-	for _, machine := range machines {
-		httpPath := c.createHttpPath(machine, path.Join(version, "members"))
-		resp, err := c.httpClient.Get(httpPath)
-		if err != nil {
-			// try another machine in the cluster
-			continue
-		}
-
-		if resp.StatusCode != http.StatusOK { // fall-back to old endpoint
-			httpPath := c.createHttpPath(machine, path.Join(version, "machines"))
-			resp, err := c.httpClient.Get(httpPath)
-			if err != nil {
-				// try another machine in the cluster
-				continue
-			}
-			b, err := ioutil.ReadAll(resp.Body)
-			resp.Body.Close()
-			if err != nil {
-				// try another machine in the cluster
-				continue
-			}
-			// update Machines List
-			c.cluster.updateFromStr(string(b))
-		} else {
-			b, err := ioutil.ReadAll(resp.Body)
-			resp.Body.Close()
-			if err != nil {
-				// try another machine in the cluster
-				continue
-			}
-
-			var mCollection memberCollection
-			if err := json.Unmarshal(b, &mCollection); err != nil {
-				// try another machine
-				continue
-			}
-
-			urls := make([]string, 0)
-			for _, m := range mCollection {
-				urls = append(urls, m.ClientURLs...)
-			}
-
-			// update Machines List
-			c.cluster.updateFromStr(strings.Join(urls, ","))
-		}
-
-		logger.Debug("sync.machines ", c.cluster.Machines)
-		c.saveConfig()
-		return true
-	}
-
-	return false
-}
-
-// createHttpPath creates a complete HTTP URL.
-// serverName should contain both the host name and a port number, if any.
-func (c *Client) createHttpPath(serverName string, _path string) string {
-	u, err := url.Parse(serverName)
-	if err != nil {
-		panic(err)
-	}
-
-	u.Path = path.Join(u.Path, _path)
-
-	if u.Scheme == "" {
-		u.Scheme = "http"
-	}
-	return u.String()
-}
-
-// dial attempts to open a TCP connection to the provided address, explicitly
-// enabling keep-alives with a one-second interval.
-func (c *Client) dial(network, addr string) (net.Conn, error) {
-	conn, err := net.DialTimeout(network, addr, c.config.DialTimeout)
-	if err != nil {
-		return nil, err
-	}
-
-	tcpConn, ok := conn.(*net.TCPConn)
-	if !ok {
-		return nil, errors.New("Failed type-assertion of net.Conn as *net.TCPConn")
-	}
-
-	// Keep TCP alive to check whether or not the remote machine is down
-	if err = tcpConn.SetKeepAlive(true); err != nil {
-		return nil, err
-	}
-
-	if err = tcpConn.SetKeepAlivePeriod(time.Second); err != nil {
-		return nil, err
-	}
-
-	return tcpConn, nil
-}
-
-func (c *Client) OpenCURL() {
-	c.cURLch = make(chan string, defaultBufferSize)
-}
-
-func (c *Client) CloseCURL() {
-	c.cURLch = nil
-}
-
-func (c *Client) sendCURL(command string) {
-	go func() {
-		select {
-		case c.cURLch <- command:
-		default:
-		}
-	}()
-}
-
-func (c *Client) RecvCURL() string {
-	return <-c.cURLch
-}
-
-// saveConfig saves the current config using c.persistence.
-func (c *Client) saveConfig() error {
-	if c.persistence != nil {
-		b, err := json.Marshal(c)
-		if err != nil {
-			return err
-		}
-
-		_, err = c.persistence.Write(b)
-		if err != nil {
-			return err
-		}
-	}
-
-	return nil
-}
-
-// MarshalJSON implements the Marshaller interface
-// as defined by the standard JSON package.
-func (c *Client) MarshalJSON() ([]byte, error) {
-	b, err := json.Marshal(struct {
-		Config  Config   `json:"config"`
-		Cluster *Cluster `json:"cluster"`
-	}{
-		Config:  c.config,
-		Cluster: c.cluster,
-	})
-
-	if err != nil {
-		return nil, err
-	}
-
-	return b, nil
-}
-
-// UnmarshalJSON implements the Unmarshaller interface
-// as defined by the standard JSON package.
-func (c *Client) UnmarshalJSON(b []byte) error {
-	temp := struct {
-		Config  Config   `json:"config"`
-		Cluster *Cluster `json:"cluster"`
-	}{}
-	err := json.Unmarshal(b, &temp)
-	if err != nil {
-		return err
-	}
-
-	c.cluster = temp.Cluster
-	c.config = temp.Config
-	return nil
-}

+ 0 - 108
Godeps/_workspace/src/github.com/coreos/go-etcd/etcd/client_test.go

@@ -1,108 +0,0 @@
-package etcd
-
-import (
-	"encoding/json"
-	"fmt"
-	"net"
-	"net/url"
-	"os"
-	"testing"
-)
-
-// To pass this test, we need to create a cluster of 3 machines
-// The server should be listening on localhost:4001, 4002, 4003
-func TestSync(t *testing.T) {
-	fmt.Println("Make sure there are three nodes at 0.0.0.0:4001-4003")
-
-	// Explicit trailing slash to ensure this doesn't reproduce:
-	// https://github.com/coreos/go-etcd/issues/82
-	c := NewClient([]string{"http://127.0.0.1:4001/"})
-
-	success := c.SyncCluster()
-	if !success {
-		t.Fatal("cannot sync machines")
-	}
-
-	for _, m := range c.GetCluster() {
-		u, err := url.Parse(m)
-		if err != nil {
-			t.Fatal(err)
-		}
-		if u.Scheme != "http" {
-			t.Fatal("scheme must be http")
-		}
-
-		host, _, err := net.SplitHostPort(u.Host)
-		if err != nil {
-			t.Fatal(err)
-		}
-		if host != "localhost" {
-			t.Fatal("Host must be localhost")
-		}
-	}
-
-	badMachines := []string{"abc", "edef"}
-
-	success = c.SetCluster(badMachines)
-
-	if success {
-		t.Fatal("should not sync on bad machines")
-	}
-
-	goodMachines := []string{"127.0.0.1:4002"}
-
-	success = c.SetCluster(goodMachines)
-
-	if !success {
-		t.Fatal("cannot sync machines")
-	} else {
-		fmt.Println(c.cluster.Machines)
-	}
-
-}
-
-func TestPersistence(t *testing.T) {
-	c := NewClient(nil)
-	c.SyncCluster()
-
-	fo, err := os.Create("config.json")
-	if err != nil {
-		t.Fatal(err)
-	}
-	defer func() {
-		if err := fo.Close(); err != nil {
-			panic(err)
-		}
-	}()
-
-	c.SetPersistence(fo)
-	err = c.saveConfig()
-	if err != nil {
-		t.Fatal(err)
-	}
-
-	c2, err := NewClientFromFile("config.json")
-	if err != nil {
-		t.Fatal(err)
-	}
-
-	// Verify that the two clients have the same config
-	b1, _ := json.Marshal(c)
-	b2, _ := json.Marshal(c2)
-
-	if string(b1) != string(b2) {
-		t.Fatalf("The two configs should be equal!")
-	}
-}
-
-func TestClientRetry(t *testing.T) {
-	c := NewClient([]string{"http://strange", "http://127.0.0.1:4001"})
-	// use first endpoint as the picked url
-	c.cluster.picked = 0
-	if _, err := c.Set("foo", "bar", 5); err != nil {
-		t.Fatal(err)
-	}
-	if _, err := c.Delete("foo", true); err != nil {
-		t.Fatal(err)
-	}
-}

+ 0 - 37
Godeps/_workspace/src/github.com/coreos/go-etcd/etcd/cluster.go

@@ -1,37 +0,0 @@
-package etcd
-
-import (
-	"math/rand"
-	"strings"
-)
-
-type Cluster struct {
-	Leader   string   `json:"leader"`
-	Machines []string `json:"machines"`
-	picked   int
-}
-
-func NewCluster(machines []string) *Cluster {
-	// if an empty slice was sent in then just assume HTTP 4001 on localhost
-	if len(machines) == 0 {
-		machines = []string{"http://127.0.0.1:4001"}
-	}
-
-	// default leader and machines
-	return &Cluster{
-		Leader:   "",
-		Machines: machines,
-		picked:   rand.Intn(len(machines)),
-	}
-}
-
-func (cl *Cluster) failure()     { cl.picked = rand.Intn(len(cl.Machines)) }
-func (cl *Cluster) pick() string { return cl.Machines[cl.picked] }
-
-func (cl *Cluster) updateFromStr(machines string) {
-	cl.Machines = strings.Split(machines, ",")
-	for i := range cl.Machines {
-		cl.Machines[i] = strings.TrimSpace(cl.Machines[i])
-	}
-	cl.picked = rand.Intn(len(cl.Machines))
-}

+ 0 - 34
Godeps/_workspace/src/github.com/coreos/go-etcd/etcd/compare_and_delete.go

@@ -1,34 +0,0 @@
-package etcd
-
-import "fmt"
-
-func (c *Client) CompareAndDelete(key string, prevValue string, prevIndex uint64) (*Response, error) {
-	raw, err := c.RawCompareAndDelete(key, prevValue, prevIndex)
-	if err != nil {
-		return nil, err
-	}
-
-	return raw.Unmarshal()
-}
-
-func (c *Client) RawCompareAndDelete(key string, prevValue string, prevIndex uint64) (*RawResponse, error) {
-	if prevValue == "" && prevIndex == 0 {
-		return nil, fmt.Errorf("You must give either prevValue or prevIndex.")
-	}
-
-	options := Options{}
-	if prevValue != "" {
-		options["prevValue"] = prevValue
-	}
-	if prevIndex != 0 {
-		options["prevIndex"] = prevIndex
-	}
-
-	raw, err := c.delete(key, options)
-
-	if err != nil {
-		return nil, err
-	}
-
-	return raw, err
-}

+ 0 - 46
Godeps/_workspace/src/github.com/coreos/go-etcd/etcd/compare_and_delete_test.go

@@ -1,46 +0,0 @@
-package etcd
-
-import (
-	"testing"
-)
-
-func TestCompareAndDelete(t *testing.T) {
-	c := NewClient(nil)
-	defer func() {
-		c.Delete("foo", true)
-	}()
-
-	c.Set("foo", "bar", 5)
-
-	// This should succeed an correct prevValue
-	resp, err := c.CompareAndDelete("foo", "bar", 0)
-	if err != nil {
-		t.Fatal(err)
-	}
-	if !(resp.PrevNode.Value == "bar" && resp.PrevNode.Key == "/foo" && resp.PrevNode.TTL == 5) {
-		t.Fatalf("CompareAndDelete 1 prevNode failed: %#v", resp)
-	}
-
-	resp, _ = c.Set("foo", "bar", 5)
-	// This should fail because it gives an incorrect prevValue
-	_, err = c.CompareAndDelete("foo", "xxx", 0)
-	if err == nil {
-		t.Fatalf("CompareAndDelete 2 should have failed.  The response is: %#v", resp)
-	}
-
-	// This should succeed because it gives an correct prevIndex
-	resp, err = c.CompareAndDelete("foo", "", resp.Node.ModifiedIndex)
-	if err != nil {
-		t.Fatal(err)
-	}
-	if !(resp.PrevNode.Value == "bar" && resp.PrevNode.Key == "/foo" && resp.PrevNode.TTL == 5) {
-		t.Fatalf("CompareAndSwap 3 prevNode failed: %#v", resp)
-	}
-
-	c.Set("foo", "bar", 5)
-	// This should fail because it gives an incorrect prevIndex
-	resp, err = c.CompareAndDelete("foo", "", 29817514)
-	if err == nil {
-		t.Fatalf("CompareAndDelete 4 should have failed.  The response is: %#v", resp)
-	}
-}

+ 0 - 36
Godeps/_workspace/src/github.com/coreos/go-etcd/etcd/compare_and_swap.go

@@ -1,36 +0,0 @@
-package etcd
-
-import "fmt"
-
-func (c *Client) CompareAndSwap(key string, value string, ttl uint64,
-	prevValue string, prevIndex uint64) (*Response, error) {
-	raw, err := c.RawCompareAndSwap(key, value, ttl, prevValue, prevIndex)
-	if err != nil {
-		return nil, err
-	}
-
-	return raw.Unmarshal()
-}
-
-func (c *Client) RawCompareAndSwap(key string, value string, ttl uint64,
-	prevValue string, prevIndex uint64) (*RawResponse, error) {
-	if prevValue == "" && prevIndex == 0 {
-		return nil, fmt.Errorf("You must give either prevValue or prevIndex.")
-	}
-
-	options := Options{}
-	if prevValue != "" {
-		options["prevValue"] = prevValue
-	}
-	if prevIndex != 0 {
-		options["prevIndex"] = prevIndex
-	}
-
-	raw, err := c.put(key, value, ttl, options)
-
-	if err != nil {
-		return nil, err
-	}
-
-	return raw, err
-}

+ 0 - 57
Godeps/_workspace/src/github.com/coreos/go-etcd/etcd/compare_and_swap_test.go

@@ -1,57 +0,0 @@
-package etcd
-
-import (
-	"testing"
-)
-
-func TestCompareAndSwap(t *testing.T) {
-	c := NewClient(nil)
-	defer func() {
-		c.Delete("foo", true)
-	}()
-
-	c.Set("foo", "bar", 5)
-
-	// This should succeed
-	resp, err := c.CompareAndSwap("foo", "bar2", 5, "bar", 0)
-	if err != nil {
-		t.Fatal(err)
-	}
-	if !(resp.Node.Value == "bar2" && resp.Node.Key == "/foo" && resp.Node.TTL == 5) {
-		t.Fatalf("CompareAndSwap 1 failed: %#v", resp)
-	}
-
-	if !(resp.PrevNode.Value == "bar" && resp.PrevNode.Key == "/foo" && resp.PrevNode.TTL == 5) {
-		t.Fatalf("CompareAndSwap 1 prevNode failed: %#v", resp)
-	}
-
-	// This should fail because it gives an incorrect prevValue
-	resp, err = c.CompareAndSwap("foo", "bar3", 5, "xxx", 0)
-	if err == nil {
-		t.Fatalf("CompareAndSwap 2 should have failed.  The response is: %#v", resp)
-	}
-
-	resp, err = c.Set("foo", "bar", 5)
-	if err != nil {
-		t.Fatal(err)
-	}
-
-	// This should succeed
-	resp, err = c.CompareAndSwap("foo", "bar2", 5, "", resp.Node.ModifiedIndex)
-	if err != nil {
-		t.Fatal(err)
-	}
-	if !(resp.Node.Value == "bar2" && resp.Node.Key == "/foo" && resp.Node.TTL == 5) {
-		t.Fatalf("CompareAndSwap 3 failed: %#v", resp)
-	}
-
-	if !(resp.PrevNode.Value == "bar" && resp.PrevNode.Key == "/foo" && resp.PrevNode.TTL == 5) {
-		t.Fatalf("CompareAndSwap 3 prevNode failed: %#v", resp)
-	}
-
-	// This should fail because it gives an incorrect prevIndex
-	resp, err = c.CompareAndSwap("foo", "bar3", 5, "", 29817514)
-	if err == nil {
-		t.Fatalf("CompareAndSwap 4 should have failed.  The response is: %#v", resp)
-	}
-}

+ 0 - 55
Godeps/_workspace/src/github.com/coreos/go-etcd/etcd/debug.go

@@ -1,55 +0,0 @@
-package etcd
-
-import (
-	"fmt"
-	"io/ioutil"
-	"log"
-	"strings"
-)
-
-var logger *etcdLogger
-
-func SetLogger(l *log.Logger) {
-	logger = &etcdLogger{l}
-}
-
-func GetLogger() *log.Logger {
-	return logger.log
-}
-
-type etcdLogger struct {
-	log *log.Logger
-}
-
-func (p *etcdLogger) Debug(args ...interface{}) {
-	msg := "DEBUG: " + fmt.Sprint(args...)
-	p.log.Println(msg)
-}
-
-func (p *etcdLogger) Debugf(f string, args ...interface{}) {
-	msg := "DEBUG: " + fmt.Sprintf(f, args...)
-	// Append newline if necessary
-	if !strings.HasSuffix(msg, "\n") {
-		msg = msg + "\n"
-	}
-	p.log.Print(msg)
-}
-
-func (p *etcdLogger) Warning(args ...interface{}) {
-	msg := "WARNING: " + fmt.Sprint(args...)
-	p.log.Println(msg)
-}
-
-func (p *etcdLogger) Warningf(f string, args ...interface{}) {
-	msg := "WARNING: " + fmt.Sprintf(f, args...)
-	// Append newline if necessary
-	if !strings.HasSuffix(msg, "\n") {
-		msg = msg + "\n"
-	}
-	p.log.Print(msg)
-}
-
-func init() {
-	// Default logger uses the go default log.
-	SetLogger(log.New(ioutil.Discard, "go-etcd", log.LstdFlags))
-}

+ 0 - 28
Godeps/_workspace/src/github.com/coreos/go-etcd/etcd/debug_test.go

@@ -1,28 +0,0 @@
-package etcd
-
-import (
-	"testing"
-)
-
-type Foo struct{}
-type Bar struct {
-	one string
-	two int
-}
-
-// Tests that logs don't panic with arbitrary interfaces
-func TestDebug(t *testing.T) {
-	f := &Foo{}
-	b := &Bar{"asfd", 3}
-	for _, test := range []interface{}{
-		1234,
-		"asdf",
-		f,
-		b,
-	} {
-		logger.Debug(test)
-		logger.Debugf("something, %s", test)
-		logger.Warning(test)
-		logger.Warningf("something, %s", test)
-	}
-}

+ 0 - 40
Godeps/_workspace/src/github.com/coreos/go-etcd/etcd/delete.go

@@ -1,40 +0,0 @@
-package etcd
-
-// Delete deletes the given key.
-//
-// When recursive set to false, if the key points to a
-// directory the method will fail.
-//
-// When recursive set to true, if the key points to a file,
-// the file will be deleted; if the key points to a directory,
-// then everything under the directory (including all child directories)
-// will be deleted.
-func (c *Client) Delete(key string, recursive bool) (*Response, error) {
-	raw, err := c.RawDelete(key, recursive, false)
-
-	if err != nil {
-		return nil, err
-	}
-
-	return raw.Unmarshal()
-}
-
-// DeleteDir deletes an empty directory or a key value pair
-func (c *Client) DeleteDir(key string) (*Response, error) {
-	raw, err := c.RawDelete(key, false, true)
-
-	if err != nil {
-		return nil, err
-	}
-
-	return raw.Unmarshal()
-}
-
-func (c *Client) RawDelete(key string, recursive bool, dir bool) (*RawResponse, error) {
-	ops := Options{
-		"recursive": recursive,
-		"dir":       dir,
-	}
-
-	return c.delete(key, ops)
-}

+ 0 - 81
Godeps/_workspace/src/github.com/coreos/go-etcd/etcd/delete_test.go

@@ -1,81 +0,0 @@
-package etcd
-
-import (
-	"testing"
-)
-
-func TestDelete(t *testing.T) {
-	c := NewClient(nil)
-	defer func() {
-		c.Delete("foo", true)
-	}()
-
-	c.Set("foo", "bar", 5)
-	resp, err := c.Delete("foo", false)
-	if err != nil {
-		t.Fatal(err)
-	}
-
-	if !(resp.Node.Value == "") {
-		t.Fatalf("Delete failed with %s", resp.Node.Value)
-	}
-
-	if !(resp.PrevNode.Value == "bar") {
-		t.Fatalf("Delete PrevNode failed with %s", resp.Node.Value)
-	}
-
-	resp, err = c.Delete("foo", false)
-	if err == nil {
-		t.Fatalf("Delete should have failed because the key foo did not exist.  "+
-			"The response was: %v", resp)
-	}
-}
-
-func TestDeleteAll(t *testing.T) {
-	c := NewClient(nil)
-	defer func() {
-		c.Delete("foo", true)
-		c.Delete("fooDir", true)
-	}()
-
-	c.SetDir("foo", 5)
-	// test delete an empty dir
-	resp, err := c.DeleteDir("foo")
-	if err != nil {
-		t.Fatal(err)
-	}
-
-	if !(resp.Node.Value == "") {
-		t.Fatalf("DeleteAll 1 failed: %#v", resp)
-	}
-
-	if !(resp.PrevNode.Dir == true && resp.PrevNode.Value == "") {
-		t.Fatalf("DeleteAll 1 PrevNode failed: %#v", resp)
-	}
-
-	c.CreateDir("fooDir", 5)
-	c.Set("fooDir/foo", "bar", 5)
-	_, err = c.DeleteDir("fooDir")
-	if err == nil {
-		t.Fatal("should not able to delete a non-empty dir with deletedir")
-	}
-
-	resp, err = c.Delete("fooDir", true)
-	if err != nil {
-		t.Fatal(err)
-	}
-
-	if !(resp.Node.Value == "") {
-		t.Fatalf("DeleteAll 2 failed: %#v", resp)
-	}
-
-	if !(resp.PrevNode.Dir == true && resp.PrevNode.Value == "") {
-		t.Fatalf("DeleteAll 2 PrevNode failed: %#v", resp)
-	}
-
-	resp, err = c.Delete("foo", true)
-	if err == nil {
-		t.Fatalf("DeleteAll should have failed because the key foo did not exist.  "+
-			"The response was: %v", resp)
-	}
-}

+ 0 - 49
Godeps/_workspace/src/github.com/coreos/go-etcd/etcd/error.go

@@ -1,49 +0,0 @@
-package etcd
-
-import (
-	"encoding/json"
-	"fmt"
-)
-
-const (
-	ErrCodeEtcdNotReachable    = 501
-	ErrCodeUnhandledHTTPStatus = 502
-)
-
-var (
-	errorMap = map[int]string{
-		ErrCodeEtcdNotReachable: "All the given peers are not reachable",
-	}
-)
-
-type EtcdError struct {
-	ErrorCode int    `json:"errorCode"`
-	Message   string `json:"message"`
-	Cause     string `json:"cause,omitempty"`
-	Index     uint64 `json:"index"`
-}
-
-func (e EtcdError) Error() string {
-	return fmt.Sprintf("%v: %v (%v) [%v]", e.ErrorCode, e.Message, e.Cause, e.Index)
-}
-
-func newError(errorCode int, cause string, index uint64) *EtcdError {
-	return &EtcdError{
-		ErrorCode: errorCode,
-		Message:   errorMap[errorCode],
-		Cause:     cause,
-		Index:     index,
-	}
-}
-
-func handleError(b []byte) error {
-	etcdErr := new(EtcdError)
-
-	err := json.Unmarshal(b, etcdErr)
-	if err != nil {
-		logger.Warningf("cannot unmarshal etcd error: %v", err)
-		return err
-	}
-
-	return etcdErr
-}

+ 0 - 32
Godeps/_workspace/src/github.com/coreos/go-etcd/etcd/get.go

@@ -1,32 +0,0 @@
-package etcd
-
-// Get gets the file or directory associated with the given key.
-// If the key points to a directory, files and directories under
-// it will be returned in sorted or unsorted order, depending on
-// the sort flag.
-// If recursive is set to false, contents under child directories
-// will not be returned.
-// If recursive is set to true, all the contents will be returned.
-func (c *Client) Get(key string, sort, recursive bool) (*Response, error) {
-	raw, err := c.RawGet(key, sort, recursive)
-
-	if err != nil {
-		return nil, err
-	}
-
-	return raw.Unmarshal()
-}
-
-func (c *Client) RawGet(key string, sort, recursive bool) (*RawResponse, error) {
-	var q bool
-	if c.config.Consistency == STRONG_CONSISTENCY {
-		q = true
-	}
-	ops := Options{
-		"recursive": recursive,
-		"sorted":    sort,
-		"quorum":    q,
-	}
-
-	return c.get(key, ops)
-}

+ 0 - 131
Godeps/_workspace/src/github.com/coreos/go-etcd/etcd/get_test.go

@@ -1,131 +0,0 @@
-package etcd
-
-import (
-	"reflect"
-	"testing"
-)
-
-// cleanNode scrubs Expiration, ModifiedIndex and CreatedIndex of a node.
-func cleanNode(n *Node) {
-	n.Expiration = nil
-	n.ModifiedIndex = 0
-	n.CreatedIndex = 0
-}
-
-// cleanResult scrubs a result object two levels deep of Expiration,
-// ModifiedIndex and CreatedIndex.
-func cleanResult(result *Response) {
-	//  TODO(philips): make this recursive.
-	cleanNode(result.Node)
-	for i, _ := range result.Node.Nodes {
-		cleanNode(result.Node.Nodes[i])
-		for j, _ := range result.Node.Nodes[i].Nodes {
-			cleanNode(result.Node.Nodes[i].Nodes[j])
-		}
-	}
-}
-
-func TestGet(t *testing.T) {
-	c := NewClient(nil)
-	defer func() {
-		c.Delete("foo", true)
-	}()
-
-	c.Set("foo", "bar", 5)
-
-	result, err := c.Get("foo", false, false)
-
-	if err != nil {
-		t.Fatal(err)
-	}
-
-	if result.Node.Key != "/foo" || result.Node.Value != "bar" {
-		t.Fatalf("Get failed with %s %s %v", result.Node.Key, result.Node.Value, result.Node.TTL)
-	}
-
-	result, err = c.Get("goo", false, false)
-	if err == nil {
-		t.Fatalf("should not be able to get non-exist key")
-	}
-}
-
-func TestGetAll(t *testing.T) {
-	c := NewClient(nil)
-	defer func() {
-		c.Delete("fooDir", true)
-	}()
-
-	c.CreateDir("fooDir", 5)
-	c.Set("fooDir/k0", "v0", 5)
-	c.Set("fooDir/k1", "v1", 5)
-
-	// Return kv-pairs in sorted order
-	result, err := c.Get("fooDir", true, false)
-
-	if err != nil {
-		t.Fatal(err)
-	}
-
-	expected := Nodes{
-		&Node{
-			Key:   "/fooDir/k0",
-			Value: "v0",
-			TTL:   5,
-		},
-		&Node{
-			Key:   "/fooDir/k1",
-			Value: "v1",
-			TTL:   5,
-		},
-	}
-
-	cleanResult(result)
-
-	if !reflect.DeepEqual(result.Node.Nodes, expected) {
-		t.Fatalf("(actual) %v != (expected) %v", result.Node.Nodes, expected)
-	}
-
-	// Test the `recursive` option
-	c.CreateDir("fooDir/childDir", 5)
-	c.Set("fooDir/childDir/k2", "v2", 5)
-
-	// Return kv-pairs in sorted order
-	result, err = c.Get("fooDir", true, true)
-
-	cleanResult(result)
-
-	if err != nil {
-		t.Fatal(err)
-	}
-
-	expected = Nodes{
-		&Node{
-			Key: "/fooDir/childDir",
-			Dir: true,
-			Nodes: Nodes{
-				&Node{
-					Key:   "/fooDir/childDir/k2",
-					Value: "v2",
-					TTL:   5,
-				},
-			},
-			TTL: 5,
-		},
-		&Node{
-			Key:   "/fooDir/k0",
-			Value: "v0",
-			TTL:   5,
-		},
-		&Node{
-			Key:   "/fooDir/k1",
-			Value: "v1",
-			TTL:   5,
-		},
-	}
-
-	cleanResult(result)
-
-	if !reflect.DeepEqual(result.Node.Nodes, expected) {
-		t.Fatalf("(actual) %v != (expected) %v", result.Node.Nodes, expected)
-	}
-}

+ 0 - 30
Godeps/_workspace/src/github.com/coreos/go-etcd/etcd/member.go

@@ -1,30 +0,0 @@
-package etcd
-
-import "encoding/json"
-
-type Member struct {
-	ID         string   `json:"id"`
-	Name       string   `json:"name"`
-	PeerURLs   []string `json:"peerURLs"`
-	ClientURLs []string `json:"clientURLs"`
-}
-
-type memberCollection []Member
-
-func (c *memberCollection) UnmarshalJSON(data []byte) error {
-	d := struct {
-		Members []Member
-	}{}
-
-	if err := json.Unmarshal(data, &d); err != nil {
-		return err
-	}
-
-	if d.Members == nil {
-		*c = make([]Member, 0)
-		return nil
-	}
-
-	*c = d.Members
-	return nil
-}

+ 0 - 71
Godeps/_workspace/src/github.com/coreos/go-etcd/etcd/member_test.go

@@ -1,71 +0,0 @@
-package etcd
-
-import (
-	"encoding/json"
-	"reflect"
-	"testing"
-)
-
-func TestMemberCollectionUnmarshal(t *testing.T) {
-	tests := []struct {
-		body []byte
-		want memberCollection
-	}{
-		{
-			body: []byte(`{"members":[]}`),
-			want: memberCollection([]Member{}),
-		},
-		{
-			body: []byte(`{"members":[{"id":"2745e2525fce8fe","peerURLs":["http://127.0.0.1:7003"],"name":"node3","clientURLs":["http://127.0.0.1:4003"]},{"id":"42134f434382925","peerURLs":["http://127.0.0.1:2380","http://127.0.0.1:7001"],"name":"node1","clientURLs":["http://127.0.0.1:2379","http://127.0.0.1:4001"]},{"id":"94088180e21eb87b","peerURLs":["http://127.0.0.1:7002"],"name":"node2","clientURLs":["http://127.0.0.1:4002"]}]}`),
-			want: memberCollection(
-				[]Member{
-					{
-						ID:   "2745e2525fce8fe",
-						Name: "node3",
-						PeerURLs: []string{
-							"http://127.0.0.1:7003",
-						},
-						ClientURLs: []string{
-							"http://127.0.0.1:4003",
-						},
-					},
-					{
-						ID:   "42134f434382925",
-						Name: "node1",
-						PeerURLs: []string{
-							"http://127.0.0.1:2380",
-							"http://127.0.0.1:7001",
-						},
-						ClientURLs: []string{
-							"http://127.0.0.1:2379",
-							"http://127.0.0.1:4001",
-						},
-					},
-					{
-						ID:   "94088180e21eb87b",
-						Name: "node2",
-						PeerURLs: []string{
-							"http://127.0.0.1:7002",
-						},
-						ClientURLs: []string{
-							"http://127.0.0.1:4002",
-						},
-					},
-				},
-			),
-		},
-	}
-
-	for i, tt := range tests {
-		var got memberCollection
-		err := json.Unmarshal(tt.body, &got)
-		if err != nil {
-			t.Errorf("#%d: unexpected error: %v", i, err)
-			continue
-		}
-
-		if !reflect.DeepEqual(tt.want, got) {
-			t.Errorf("#%d: incorrect output: want=%#v, got=%#v", i, tt.want, got)
-		}
-	}
-}

+ 0 - 72
Godeps/_workspace/src/github.com/coreos/go-etcd/etcd/options.go

@@ -1,72 +0,0 @@
-package etcd
-
-import (
-	"fmt"
-	"net/url"
-	"reflect"
-)
-
-type Options map[string]interface{}
-
-// An internally-used data structure that represents a mapping
-// between valid options and their kinds
-type validOptions map[string]reflect.Kind
-
-// Valid options for GET, PUT, POST, DELETE
-// Using CAPITALIZED_UNDERSCORE to emphasize that these
-// values are meant to be used as constants.
-var (
-	VALID_GET_OPTIONS = validOptions{
-		"recursive": reflect.Bool,
-		"quorum":    reflect.Bool,
-		"sorted":    reflect.Bool,
-		"wait":      reflect.Bool,
-		"waitIndex": reflect.Uint64,
-	}
-
-	VALID_PUT_OPTIONS = validOptions{
-		"prevValue": reflect.String,
-		"prevIndex": reflect.Uint64,
-		"prevExist": reflect.Bool,
-		"dir":       reflect.Bool,
-	}
-
-	VALID_POST_OPTIONS = validOptions{}
-
-	VALID_DELETE_OPTIONS = validOptions{
-		"recursive": reflect.Bool,
-		"dir":       reflect.Bool,
-		"prevValue": reflect.String,
-		"prevIndex": reflect.Uint64,
-	}
-)
-
-// Convert options to a string of HTML parameters
-func (ops Options) toParameters(validOps validOptions) (string, error) {
-	p := "?"
-	values := url.Values{}
-
-	if ops == nil {
-		return "", nil
-	}
-
-	for k, v := range ops {
-		// Check if the given option is valid (that it exists)
-		kind := validOps[k]
-		if kind == reflect.Invalid {
-			return "", fmt.Errorf("Invalid option: %v", k)
-		}
-
-		// Check if the given option is of the valid type
-		t := reflect.TypeOf(v)
-		if kind != t.Kind() {
-			return "", fmt.Errorf("Option %s should be of %v kind, not of %v kind.",
-				k, kind, t.Kind())
-		}
-
-		values.Set(k, fmt.Sprintf("%v", v))
-	}
-
-	p += values.Encode()
-	return p, nil
-}

+ 0 - 405
Godeps/_workspace/src/github.com/coreos/go-etcd/etcd/requests.go

@@ -1,405 +0,0 @@
-package etcd
-
-import (
-	"errors"
-	"fmt"
-	"io"
-	"io/ioutil"
-	"net/http"
-	"net/url"
-	"path"
-	"strings"
-	"sync"
-	"time"
-)
-
-// Errors introduced by handling requests
-var (
-	ErrRequestCancelled = errors.New("sending request is cancelled")
-)
-
-type RawRequest struct {
-	Method       string
-	RelativePath string
-	Values       url.Values
-	Cancel       <-chan bool
-}
-
-// NewRawRequest returns a new RawRequest
-func NewRawRequest(method, relativePath string, values url.Values, cancel <-chan bool) *RawRequest {
-	return &RawRequest{
-		Method:       method,
-		RelativePath: relativePath,
-		Values:       values,
-		Cancel:       cancel,
-	}
-}
-
-// getCancelable issues a cancelable GET request
-func (c *Client) getCancelable(key string, options Options,
-	cancel <-chan bool) (*RawResponse, error) {
-	logger.Debugf("get %s [%s]", key, c.cluster.pick())
-	p := keyToPath(key)
-
-	str, err := options.toParameters(VALID_GET_OPTIONS)
-	if err != nil {
-		return nil, err
-	}
-	p += str
-
-	req := NewRawRequest("GET", p, nil, cancel)
-	resp, err := c.SendRequest(req)
-
-	if err != nil {
-		return nil, err
-	}
-
-	return resp, nil
-}
-
-// get issues a GET request
-func (c *Client) get(key string, options Options) (*RawResponse, error) {
-	return c.getCancelable(key, options, nil)
-}
-
-// put issues a PUT request
-func (c *Client) put(key string, value string, ttl uint64,
-	options Options) (*RawResponse, error) {
-
-	logger.Debugf("put %s, %s, ttl: %d, [%s]", key, value, ttl, c.cluster.pick())
-	p := keyToPath(key)
-
-	str, err := options.toParameters(VALID_PUT_OPTIONS)
-	if err != nil {
-		return nil, err
-	}
-	p += str
-
-	req := NewRawRequest("PUT", p, buildValues(value, ttl), nil)
-	resp, err := c.SendRequest(req)
-
-	if err != nil {
-		return nil, err
-	}
-
-	return resp, nil
-}
-
-// post issues a POST request
-func (c *Client) post(key string, value string, ttl uint64) (*RawResponse, error) {
-	logger.Debugf("post %s, %s, ttl: %d, [%s]", key, value, ttl, c.cluster.pick())
-	p := keyToPath(key)
-
-	req := NewRawRequest("POST", p, buildValues(value, ttl), nil)
-	resp, err := c.SendRequest(req)
-
-	if err != nil {
-		return nil, err
-	}
-
-	return resp, nil
-}
-
-// delete issues a DELETE request
-func (c *Client) delete(key string, options Options) (*RawResponse, error) {
-	logger.Debugf("delete %s [%s]", key, c.cluster.pick())
-	p := keyToPath(key)
-
-	str, err := options.toParameters(VALID_DELETE_OPTIONS)
-	if err != nil {
-		return nil, err
-	}
-	p += str
-
-	req := NewRawRequest("DELETE", p, nil, nil)
-	resp, err := c.SendRequest(req)
-
-	if err != nil {
-		return nil, err
-	}
-
-	return resp, nil
-}
-
-// SendRequest sends a HTTP request and returns a Response as defined by etcd
-func (c *Client) SendRequest(rr *RawRequest) (*RawResponse, error) {
-	var req *http.Request
-	var resp *http.Response
-	var httpPath string
-	var err error
-	var respBody []byte
-
-	var numReqs = 1
-
-	checkRetry := c.CheckRetry
-	if checkRetry == nil {
-		checkRetry = DefaultCheckRetry
-	}
-
-	cancelled := make(chan bool, 1)
-	reqLock := new(sync.Mutex)
-
-	if rr.Cancel != nil {
-		cancelRoutine := make(chan bool)
-		defer close(cancelRoutine)
-
-		go func() {
-			select {
-			case <-rr.Cancel:
-				cancelled <- true
-				logger.Debug("send.request is cancelled")
-			case <-cancelRoutine:
-				return
-			}
-
-			// Repeat canceling request until this thread is stopped
-			// because we have no idea about whether it succeeds.
-			for {
-				reqLock.Lock()
-				c.httpClient.Transport.(*http.Transport).CancelRequest(req)
-				reqLock.Unlock()
-
-				select {
-				case <-time.After(100 * time.Millisecond):
-				case <-cancelRoutine:
-					return
-				}
-			}
-		}()
-	}
-
-	// If we connect to a follower and consistency is required, retry until
-	// we connect to a leader
-	sleep := 25 * time.Millisecond
-	maxSleep := time.Second
-
-	for attempt := 0; ; attempt++ {
-		if attempt > 0 {
-			select {
-			case <-cancelled:
-				return nil, ErrRequestCancelled
-			case <-time.After(sleep):
-				sleep = sleep * 2
-				if sleep > maxSleep {
-					sleep = maxSleep
-				}
-			}
-		}
-
-		logger.Debug("Connecting to etcd: attempt ", attempt+1, " for ", rr.RelativePath)
-
-		// get httpPath if not set
-		if httpPath == "" {
-			httpPath = c.getHttpPath(rr.RelativePath)
-		}
-
-		// Return a cURL command if curlChan is set
-		if c.cURLch != nil {
-			command := fmt.Sprintf("curl -X %s %s", rr.Method, httpPath)
-			for key, value := range rr.Values {
-				command += fmt.Sprintf(" -d %s=%s", key, value[0])
-			}
-			if c.credentials != nil {
-				command += fmt.Sprintf(" -u %s", c.credentials.username)
-			}
-			c.sendCURL(command)
-		}
-
-		logger.Debug("send.request.to ", httpPath, " | method ", rr.Method)
-
-		req, err := func() (*http.Request, error) {
-			reqLock.Lock()
-			defer reqLock.Unlock()
-
-			if rr.Values == nil {
-				if req, err = http.NewRequest(rr.Method, httpPath, nil); err != nil {
-					return nil, err
-				}
-			} else {
-				body := strings.NewReader(rr.Values.Encode())
-				if req, err = http.NewRequest(rr.Method, httpPath, body); err != nil {
-					return nil, err
-				}
-
-				req.Header.Set("Content-Type",
-					"application/x-www-form-urlencoded; param=value")
-			}
-			return req, nil
-		}()
-
-		if err != nil {
-			return nil, err
-		}
-
-		if c.credentials != nil {
-			req.SetBasicAuth(c.credentials.username, c.credentials.password)
-		}
-
-		resp, err = c.httpClient.Do(req)
-		// clear previous httpPath
-		httpPath = ""
-		defer func() {
-			if resp != nil {
-				resp.Body.Close()
-			}
-		}()
-
-		// If the request was cancelled, return ErrRequestCancelled directly
-		select {
-		case <-cancelled:
-			return nil, ErrRequestCancelled
-		default:
-		}
-
-		numReqs++
-
-		// network error, change a machine!
-		if err != nil {
-			logger.Debug("network error: ", err.Error())
-			lastResp := http.Response{}
-			if checkErr := checkRetry(c.cluster, numReqs, lastResp, err); checkErr != nil {
-				return nil, checkErr
-			}
-
-			c.cluster.failure()
-			continue
-		}
-
-		// if there is no error, it should receive response
-		logger.Debug("recv.response.from ", httpPath)
-
-		if validHttpStatusCode[resp.StatusCode] {
-			// try to read byte code and break the loop
-			respBody, err = ioutil.ReadAll(resp.Body)
-			if err == nil {
-				logger.Debug("recv.success ", httpPath)
-				break
-			}
-			// ReadAll error may be caused due to cancel request
-			select {
-			case <-cancelled:
-				return nil, ErrRequestCancelled
-			default:
-			}
-
-			if err == io.ErrUnexpectedEOF {
-				// underlying connection was closed prematurely, probably by timeout
-				// TODO: empty body or unexpectedEOF can cause http.Transport to get hosed;
-				// this allows the client to detect that and take evasive action. Need
-				// to revisit once code.google.com/p/go/issues/detail?id=8648 gets fixed.
-				respBody = []byte{}
-				break
-			}
-		}
-
-		if resp.StatusCode == http.StatusTemporaryRedirect {
-			u, err := resp.Location()
-
-			if err != nil {
-				logger.Warning(err)
-			} else {
-				// set httpPath for following redirection
-				httpPath = u.String()
-			}
-			resp.Body.Close()
-			continue
-		}
-
-		if checkErr := checkRetry(c.cluster, numReqs, *resp,
-			errors.New("Unexpected HTTP status code")); checkErr != nil {
-			return nil, checkErr
-		}
-		resp.Body.Close()
-	}
-
-	r := &RawResponse{
-		StatusCode: resp.StatusCode,
-		Body:       respBody,
-		Header:     resp.Header,
-	}
-
-	return r, nil
-}
-
-// DefaultCheckRetry defines the retrying behaviour for bad HTTP requests
-// If we have retried 2 * machine number, stop retrying.
-// If status code is InternalServerError, sleep for 200ms.
-func DefaultCheckRetry(cluster *Cluster, numReqs int, lastResp http.Response,
-	err error) error {
-
-	if isEmptyResponse(lastResp) {
-		// always retry if it failed to get response from one machine
-		return nil
-	} else if !shouldRetry(lastResp) {
-		body := []byte("nil")
-		if lastResp.Body != nil {
-			if b, err := ioutil.ReadAll(lastResp.Body); err == nil {
-				body = b
-			}
-		}
-		errStr := fmt.Sprintf("unhandled http status [%s] with body [%s]", http.StatusText(lastResp.StatusCode), body)
-		return newError(ErrCodeUnhandledHTTPStatus, errStr, 0)
-	}
-
-	if numReqs > 2*len(cluster.Machines) {
-		errStr := fmt.Sprintf("failed to propose on members %v twice [last error: %v]", cluster.Machines, err)
-		return newError(ErrCodeEtcdNotReachable, errStr, 0)
-	}
-	if shouldRetry(lastResp) {
-		// sleep some time and expect leader election finish
-		time.Sleep(time.Millisecond * 200)
-	}
-
-	logger.Warning("bad response status code", lastResp.StatusCode)
-	return nil
-}
-
-func isEmptyResponse(r http.Response) bool { return r.StatusCode == 0 }
-
-// shouldRetry returns whether the reponse deserves retry.
-func shouldRetry(r http.Response) bool {
-	// TODO: only retry when the cluster is in leader election
-	// We cannot do it exactly because etcd doesn't support it well.
-	return r.StatusCode == http.StatusInternalServerError
-}
-
-func (c *Client) getHttpPath(s ...string) string {
-	fullPath := c.cluster.pick() + "/" + version
-	for _, seg := range s {
-		fullPath = fullPath + "/" + seg
-	}
-	return fullPath
-}
-
-// buildValues builds a url.Values map according to the given value and ttl
-func buildValues(value string, ttl uint64) url.Values {
-	v := url.Values{}
-
-	if value != "" {
-		v.Set("value", value)
-	}
-
-	if ttl > 0 {
-		v.Set("ttl", fmt.Sprintf("%v", ttl))
-	}
-
-	return v
-}
-
-// convert key string to http path exclude version, including URL escaping
-// for example: key[foo] -> path[keys/foo]
-// key[/%z] -> path[keys/%25z]
-// key[/] -> path[keys/]
-func keyToPath(key string) string {
-	// URL-escape our key, except for slashes
-	p := strings.Replace(url.QueryEscape(path.Join("keys", key)), "%2F", "/", -1)
-
-	// corner case: if key is "/" or "//" ect
-	// path join will clear the tailing "/"
-	// we need to add it back
-	if p == "keys" {
-		p = "keys/"
-	}
-
-	return p
-}

+ 0 - 22
Godeps/_workspace/src/github.com/coreos/go-etcd/etcd/requests_test.go

@@ -1,22 +0,0 @@
-package etcd
-
-import "testing"
-
-func TestKeyToPath(t *testing.T) {
-	tests := []struct {
-		key   string
-		wpath string
-	}{
-		{"", "keys/"},
-		{"foo", "keys/foo"},
-		{"foo/bar", "keys/foo/bar"},
-		{"%z", "keys/%25z"},
-		{"/", "keys/"},
-	}
-	for i, tt := range tests {
-		path := keyToPath(tt.key)
-		if path != tt.wpath {
-			t.Errorf("#%d: path = %s, want %s", i, path, tt.wpath)
-		}
-	}
-}

+ 0 - 89
Godeps/_workspace/src/github.com/coreos/go-etcd/etcd/response.go

@@ -1,89 +0,0 @@
-package etcd
-
-import (
-	"encoding/json"
-	"net/http"
-	"strconv"
-	"time"
-)
-
-const (
-	rawResponse = iota
-	normalResponse
-)
-
-type responseType int
-
-type RawResponse struct {
-	StatusCode int
-	Body       []byte
-	Header     http.Header
-}
-
-var (
-	validHttpStatusCode = map[int]bool{
-		http.StatusCreated:            true,
-		http.StatusOK:                 true,
-		http.StatusBadRequest:         true,
-		http.StatusNotFound:           true,
-		http.StatusPreconditionFailed: true,
-		http.StatusForbidden:          true,
-	}
-)
-
-// Unmarshal parses RawResponse and stores the result in Response
-func (rr *RawResponse) Unmarshal() (*Response, error) {
-	if rr.StatusCode != http.StatusOK && rr.StatusCode != http.StatusCreated {
-		return nil, handleError(rr.Body)
-	}
-
-	resp := new(Response)
-
-	err := json.Unmarshal(rr.Body, resp)
-
-	if err != nil {
-		return nil, err
-	}
-
-	// attach index and term to response
-	resp.EtcdIndex, _ = strconv.ParseUint(rr.Header.Get("X-Etcd-Index"), 10, 64)
-	resp.RaftIndex, _ = strconv.ParseUint(rr.Header.Get("X-Raft-Index"), 10, 64)
-	resp.RaftTerm, _ = strconv.ParseUint(rr.Header.Get("X-Raft-Term"), 10, 64)
-
-	return resp, nil
-}
-
-type Response struct {
-	Action    string `json:"action"`
-	Node      *Node  `json:"node"`
-	PrevNode  *Node  `json:"prevNode,omitempty"`
-	EtcdIndex uint64 `json:"etcdIndex"`
-	RaftIndex uint64 `json:"raftIndex"`
-	RaftTerm  uint64 `json:"raftTerm"`
-}
-
-type Node struct {
-	Key           string     `json:"key, omitempty"`
-	Value         string     `json:"value,omitempty"`
-	Dir           bool       `json:"dir,omitempty"`
-	Expiration    *time.Time `json:"expiration,omitempty"`
-	TTL           int64      `json:"ttl,omitempty"`
-	Nodes         Nodes      `json:"nodes,omitempty"`
-	ModifiedIndex uint64     `json:"modifiedIndex,omitempty"`
-	CreatedIndex  uint64     `json:"createdIndex,omitempty"`
-}
-
-type Nodes []*Node
-
-// interfaces for sorting
-func (ns Nodes) Len() int {
-	return len(ns)
-}
-
-func (ns Nodes) Less(i, j int) bool {
-	return ns[i].Key < ns[j].Key
-}
-
-func (ns Nodes) Swap(i, j int) {
-	ns[i], ns[j] = ns[j], ns[i]
-}

+ 0 - 42
Godeps/_workspace/src/github.com/coreos/go-etcd/etcd/set_curl_chan_test.go

@@ -1,42 +0,0 @@
-package etcd
-
-import (
-	"fmt"
-	"testing"
-)
-
-func TestSetCurlChan(t *testing.T) {
-	c := NewClient(nil)
-	c.OpenCURL()
-
-	defer func() {
-		c.Delete("foo", true)
-	}()
-
-	_, err := c.Set("foo", "bar", 5)
-	if err != nil {
-		t.Fatal(err)
-	}
-
-	expected := fmt.Sprintf("curl -X PUT %s/v2/keys/foo -d value=bar -d ttl=5",
-		c.cluster.pick())
-	actual := c.RecvCURL()
-	if expected != actual {
-		t.Fatalf(`Command "%s" is not equal to expected value "%s"`,
-			actual, expected)
-	}
-
-	c.SetConsistency(STRONG_CONSISTENCY)
-	_, err = c.Get("foo", false, false)
-	if err != nil {
-		t.Fatal(err)
-	}
-
-	expected = fmt.Sprintf("curl -X GET %s/v2/keys/foo?quorum=true&recursive=false&sorted=false",
-		c.cluster.pick())
-	actual = c.RecvCURL()
-	if expected != actual {
-		t.Fatalf(`Command "%s" is not equal to expected value "%s"`,
-			actual, expected)
-	}
-}

+ 0 - 137
Godeps/_workspace/src/github.com/coreos/go-etcd/etcd/set_update_create.go

@@ -1,137 +0,0 @@
-package etcd
-
-// Set sets the given key to the given value.
-// It will create a new key value pair or replace the old one.
-// It will not replace a existing directory.
-func (c *Client) Set(key string, value string, ttl uint64) (*Response, error) {
-	raw, err := c.RawSet(key, value, ttl)
-
-	if err != nil {
-		return nil, err
-	}
-
-	return raw.Unmarshal()
-}
-
-// SetDir sets the given key to a directory.
-// It will create a new directory or replace the old key value pair by a directory.
-// It will not replace a existing directory.
-func (c *Client) SetDir(key string, ttl uint64) (*Response, error) {
-	raw, err := c.RawSetDir(key, ttl)
-
-	if err != nil {
-		return nil, err
-	}
-
-	return raw.Unmarshal()
-}
-
-// CreateDir creates a directory. It succeeds only if
-// the given key does not yet exist.
-func (c *Client) CreateDir(key string, ttl uint64) (*Response, error) {
-	raw, err := c.RawCreateDir(key, ttl)
-
-	if err != nil {
-		return nil, err
-	}
-
-	return raw.Unmarshal()
-}
-
-// UpdateDir updates the given directory. It succeeds only if the
-// given key already exists.
-func (c *Client) UpdateDir(key string, ttl uint64) (*Response, error) {
-	raw, err := c.RawUpdateDir(key, ttl)
-
-	if err != nil {
-		return nil, err
-	}
-
-	return raw.Unmarshal()
-}
-
-// Create creates a file with the given value under the given key.  It succeeds
-// only if the given key does not yet exist.
-func (c *Client) Create(key string, value string, ttl uint64) (*Response, error) {
-	raw, err := c.RawCreate(key, value, ttl)
-
-	if err != nil {
-		return nil, err
-	}
-
-	return raw.Unmarshal()
-}
-
-// CreateInOrder creates a file with a key that's guaranteed to be higher than other
-// keys in the given directory. It is useful for creating queues.
-func (c *Client) CreateInOrder(dir string, value string, ttl uint64) (*Response, error) {
-	raw, err := c.RawCreateInOrder(dir, value, ttl)
-
-	if err != nil {
-		return nil, err
-	}
-
-	return raw.Unmarshal()
-}
-
-// Update updates the given key to the given value.  It succeeds only if the
-// given key already exists.
-func (c *Client) Update(key string, value string, ttl uint64) (*Response, error) {
-	raw, err := c.RawUpdate(key, value, ttl)
-
-	if err != nil {
-		return nil, err
-	}
-
-	return raw.Unmarshal()
-}
-
-func (c *Client) RawUpdateDir(key string, ttl uint64) (*RawResponse, error) {
-	ops := Options{
-		"prevExist": true,
-		"dir":       true,
-	}
-
-	return c.put(key, "", ttl, ops)
-}
-
-func (c *Client) RawCreateDir(key string, ttl uint64) (*RawResponse, error) {
-	ops := Options{
-		"prevExist": false,
-		"dir":       true,
-	}
-
-	return c.put(key, "", ttl, ops)
-}
-
-func (c *Client) RawSet(key string, value string, ttl uint64) (*RawResponse, error) {
-	return c.put(key, value, ttl, nil)
-}
-
-func (c *Client) RawSetDir(key string, ttl uint64) (*RawResponse, error) {
-	ops := Options{
-		"dir": true,
-	}
-
-	return c.put(key, "", ttl, ops)
-}
-
-func (c *Client) RawUpdate(key string, value string, ttl uint64) (*RawResponse, error) {
-	ops := Options{
-		"prevExist": true,
-	}
-
-	return c.put(key, value, ttl, ops)
-}
-
-func (c *Client) RawCreate(key string, value string, ttl uint64) (*RawResponse, error) {
-	ops := Options{
-		"prevExist": false,
-	}
-
-	return c.put(key, value, ttl, ops)
-}
-
-func (c *Client) RawCreateInOrder(dir string, value string, ttl uint64) (*RawResponse, error) {
-	return c.post(dir, value, ttl)
-}

+ 0 - 241
Godeps/_workspace/src/github.com/coreos/go-etcd/etcd/set_update_create_test.go

@@ -1,241 +0,0 @@
-package etcd
-
-import (
-	"testing"
-)
-
-func TestSet(t *testing.T) {
-	c := NewClient(nil)
-	defer func() {
-		c.Delete("foo", true)
-	}()
-
-	resp, err := c.Set("foo", "bar", 5)
-	if err != nil {
-		t.Fatal(err)
-	}
-	if resp.Node.Key != "/foo" || resp.Node.Value != "bar" || resp.Node.TTL != 5 {
-		t.Fatalf("Set 1 failed: %#v", resp)
-	}
-	if resp.PrevNode != nil {
-		t.Fatalf("Set 1 PrevNode failed: %#v", resp)
-	}
-
-	resp, err = c.Set("foo", "bar2", 5)
-	if err != nil {
-		t.Fatal(err)
-	}
-	if !(resp.Node.Key == "/foo" && resp.Node.Value == "bar2" && resp.Node.TTL == 5) {
-		t.Fatalf("Set 2 failed: %#v", resp)
-	}
-	if resp.PrevNode.Key != "/foo" || resp.PrevNode.Value != "bar" || resp.Node.TTL != 5 {
-		t.Fatalf("Set 2 PrevNode failed: %#v", resp)
-	}
-}
-
-func TestUpdate(t *testing.T) {
-	c := NewClient(nil)
-	defer func() {
-		c.Delete("foo", true)
-		c.Delete("nonexistent", true)
-	}()
-
-	resp, err := c.Set("foo", "bar", 5)
-
-	if err != nil {
-		t.Fatal(err)
-	}
-
-	// This should succeed.
-	resp, err = c.Update("foo", "wakawaka", 5)
-	if err != nil {
-		t.Fatal(err)
-	}
-
-	if !(resp.Action == "update" && resp.Node.Key == "/foo" && resp.Node.TTL == 5) {
-		t.Fatalf("Update 1 failed: %#v", resp)
-	}
-	if !(resp.PrevNode.Key == "/foo" && resp.PrevNode.Value == "bar" && resp.Node.TTL == 5) {
-		t.Fatalf("Update 1 prevValue failed: %#v", resp)
-	}
-
-	// This should fail because the key does not exist.
-	resp, err = c.Update("nonexistent", "whatever", 5)
-	if err == nil {
-		t.Fatalf("The key %v did not exist, so the update should have failed."+
-			"The response was: %#v", resp.Node.Key, resp)
-	}
-}
-
-func TestCreate(t *testing.T) {
-	c := NewClient(nil)
-	defer func() {
-		c.Delete("newKey", true)
-	}()
-
-	newKey := "/newKey"
-	newValue := "/newValue"
-
-	// This should succeed
-	resp, err := c.Create(newKey, newValue, 5)
-	if err != nil {
-		t.Fatal(err)
-	}
-
-	if !(resp.Action == "create" && resp.Node.Key == newKey &&
-		resp.Node.Value == newValue && resp.Node.TTL == 5) {
-		t.Fatalf("Create 1 failed: %#v", resp)
-	}
-	if resp.PrevNode != nil {
-		t.Fatalf("Create 1 PrevNode failed: %#v", resp)
-	}
-
-	// This should fail, because the key is already there
-	resp, err = c.Create(newKey, newValue, 5)
-	if err == nil {
-		t.Fatalf("The key %v did exist, so the creation should have failed."+
-			"The response was: %#v", resp.Node.Key, resp)
-	}
-}
-
-func TestCreateInOrder(t *testing.T) {
-	c := NewClient(nil)
-	dir := "/queue"
-	defer func() {
-		c.DeleteDir(dir)
-	}()
-
-	var firstKey, secondKey string
-
-	resp, err := c.CreateInOrder(dir, "1", 5)
-	if err != nil {
-		t.Fatal(err)
-	}
-
-	if !(resp.Action == "create" && resp.Node.Value == "1" && resp.Node.TTL == 5) {
-		t.Fatalf("Create 1 failed: %#v", resp)
-	}
-
-	firstKey = resp.Node.Key
-
-	resp, err = c.CreateInOrder(dir, "2", 5)
-	if err != nil {
-		t.Fatal(err)
-	}
-
-	if !(resp.Action == "create" && resp.Node.Value == "2" && resp.Node.TTL == 5) {
-		t.Fatalf("Create 2 failed: %#v", resp)
-	}
-
-	secondKey = resp.Node.Key
-
-	if firstKey >= secondKey {
-		t.Fatalf("Expected first key to be greater than second key, but %s is not greater than %s",
-			firstKey, secondKey)
-	}
-}
-
-func TestSetDir(t *testing.T) {
-	c := NewClient(nil)
-	defer func() {
-		c.Delete("foo", true)
-		c.Delete("fooDir", true)
-	}()
-
-	resp, err := c.CreateDir("fooDir", 5)
-	if err != nil {
-		t.Fatal(err)
-	}
-	if !(resp.Node.Key == "/fooDir" && resp.Node.Value == "" && resp.Node.TTL == 5) {
-		t.Fatalf("SetDir 1 failed: %#v", resp)
-	}
-	if resp.PrevNode != nil {
-		t.Fatalf("SetDir 1 PrevNode failed: %#v", resp)
-	}
-
-	// This should fail because /fooDir already points to a directory
-	resp, err = c.CreateDir("/fooDir", 5)
-	if err == nil {
-		t.Fatalf("fooDir already points to a directory, so SetDir should have failed."+
-			"The response was: %#v", resp)
-	}
-
-	_, err = c.Set("foo", "bar", 5)
-	if err != nil {
-		t.Fatal(err)
-	}
-
-	// This should succeed
-	// It should replace the key
-	resp, err = c.SetDir("foo", 5)
-	if err != nil {
-		t.Fatal(err)
-	}
-	if !(resp.Node.Key == "/foo" && resp.Node.Value == "" && resp.Node.TTL == 5) {
-		t.Fatalf("SetDir 2 failed: %#v", resp)
-	}
-	if !(resp.PrevNode.Key == "/foo" && resp.PrevNode.Value == "bar" && resp.PrevNode.TTL == 5) {
-		t.Fatalf("SetDir 2 failed: %#v", resp)
-	}
-}
-
-func TestUpdateDir(t *testing.T) {
-	c := NewClient(nil)
-	defer func() {
-		c.Delete("fooDir", true)
-	}()
-
-	resp, err := c.CreateDir("fooDir", 5)
-	if err != nil {
-		t.Fatal(err)
-	}
-
-	// This should succeed.
-	resp, err = c.UpdateDir("fooDir", 5)
-	if err != nil {
-		t.Fatal(err)
-	}
-
-	if !(resp.Action == "update" && resp.Node.Key == "/fooDir" &&
-		resp.Node.Value == "" && resp.Node.TTL == 5) {
-		t.Fatalf("UpdateDir 1 failed: %#v", resp)
-	}
-	if !(resp.PrevNode.Key == "/fooDir" && resp.PrevNode.Dir == true && resp.PrevNode.TTL == 5) {
-		t.Fatalf("UpdateDir 1 PrevNode failed: %#v", resp)
-	}
-
-	// This should fail because the key does not exist.
-	resp, err = c.UpdateDir("nonexistentDir", 5)
-	if err == nil {
-		t.Fatalf("The key %v did not exist, so the update should have failed."+
-			"The response was: %#v", resp.Node.Key, resp)
-	}
-}
-
-func TestCreateDir(t *testing.T) {
-	c := NewClient(nil)
-	defer func() {
-		c.Delete("fooDir", true)
-	}()
-
-	// This should succeed
-	resp, err := c.CreateDir("fooDir", 5)
-	if err != nil {
-		t.Fatal(err)
-	}
-
-	if !(resp.Action == "create" && resp.Node.Key == "/fooDir" &&
-		resp.Node.Value == "" && resp.Node.TTL == 5) {
-		t.Fatalf("CreateDir 1 failed: %#v", resp)
-	}
-	if resp.PrevNode != nil {
-		t.Fatalf("CreateDir 1 PrevNode failed: %#v", resp)
-	}
-
-	// This should fail, because the key is already there
-	resp, err = c.CreateDir("fooDir", 5)
-	if err == nil {
-		t.Fatalf("The key %v did exist, so the creation should have failed."+
-			"The response was: %#v", resp.Node.Key, resp)
-	}
-}

+ 0 - 6
Godeps/_workspace/src/github.com/coreos/go-etcd/etcd/version.go

@@ -1,6 +0,0 @@
-package etcd
-
-const (
-	version        = "v2"
-	packageVersion = "v2.0.0+git"
-)

+ 0 - 103
Godeps/_workspace/src/github.com/coreos/go-etcd/etcd/watch.go

@@ -1,103 +0,0 @@
-package etcd
-
-import (
-	"errors"
-)
-
-// Errors introduced by the Watch command.
-var (
-	ErrWatchStoppedByUser = errors.New("Watch stopped by the user via stop channel")
-)
-
-// If recursive is set to true the watch returns the first change under the given
-// prefix since the given index.
-//
-// If recursive is set to false the watch returns the first change to the given key
-// since the given index.
-//
-// To watch for the latest change, set waitIndex = 0.
-//
-// If a receiver channel is given, it will be a long-term watch. Watch will block at the
-//channel. After someone receives the channel, it will go on to watch that
-// prefix.  If a stop channel is given, the client can close long-term watch using
-// the stop channel.
-func (c *Client) Watch(prefix string, waitIndex uint64, recursive bool,
-	receiver chan *Response, stop chan bool) (*Response, error) {
-	logger.Debugf("watch %s [%s]", prefix, c.cluster.Leader)
-	if receiver == nil {
-		raw, err := c.watchOnce(prefix, waitIndex, recursive, stop)
-
-		if err != nil {
-			return nil, err
-		}
-
-		return raw.Unmarshal()
-	}
-	defer close(receiver)
-
-	for {
-		raw, err := c.watchOnce(prefix, waitIndex, recursive, stop)
-
-		if err != nil {
-			return nil, err
-		}
-
-		resp, err := raw.Unmarshal()
-
-		if err != nil {
-			return nil, err
-		}
-
-		waitIndex = resp.Node.ModifiedIndex + 1
-		receiver <- resp
-	}
-}
-
-func (c *Client) RawWatch(prefix string, waitIndex uint64, recursive bool,
-	receiver chan *RawResponse, stop chan bool) (*RawResponse, error) {
-
-	logger.Debugf("rawWatch %s [%s]", prefix, c.cluster.Leader)
-	if receiver == nil {
-		return c.watchOnce(prefix, waitIndex, recursive, stop)
-	}
-
-	for {
-		raw, err := c.watchOnce(prefix, waitIndex, recursive, stop)
-
-		if err != nil {
-			return nil, err
-		}
-
-		resp, err := raw.Unmarshal()
-
-		if err != nil {
-			return nil, err
-		}
-
-		waitIndex = resp.Node.ModifiedIndex + 1
-		receiver <- raw
-	}
-}
-
-// helper func
-// return when there is change under the given prefix
-func (c *Client) watchOnce(key string, waitIndex uint64, recursive bool, stop chan bool) (*RawResponse, error) {
-
-	options := Options{
-		"wait": true,
-	}
-	if waitIndex > 0 {
-		options["waitIndex"] = waitIndex
-	}
-	if recursive {
-		options["recursive"] = true
-	}
-
-	resp, err := c.getCancelable(key, options, stop)
-
-	if err == ErrRequestCancelled {
-		return nil, ErrWatchStoppedByUser
-	}
-
-	return resp, err
-}

+ 0 - 119
Godeps/_workspace/src/github.com/coreos/go-etcd/etcd/watch_test.go

@@ -1,119 +0,0 @@
-package etcd
-
-import (
-	"fmt"
-	"runtime"
-	"testing"
-	"time"
-)
-
-func TestWatch(t *testing.T) {
-	c := NewClient(nil)
-	defer func() {
-		c.Delete("watch_foo", true)
-	}()
-
-	go setHelper("watch_foo", "bar", c)
-
-	resp, err := c.Watch("watch_foo", 0, false, nil, nil)
-	if err != nil {
-		t.Fatal(err)
-	}
-	if !(resp.Node.Key == "/watch_foo" && resp.Node.Value == "bar") {
-		t.Fatalf("Watch 1 failed: %#v", resp)
-	}
-
-	go setHelper("watch_foo", "bar", c)
-
-	resp, err = c.Watch("watch_foo", resp.Node.ModifiedIndex+1, false, nil, nil)
-	if err != nil {
-		t.Fatal(err)
-	}
-	if !(resp.Node.Key == "/watch_foo" && resp.Node.Value == "bar") {
-		t.Fatalf("Watch 2 failed: %#v", resp)
-	}
-
-	routineNum := runtime.NumGoroutine()
-
-	ch := make(chan *Response, 10)
-	stop := make(chan bool, 1)
-
-	go setLoop("watch_foo", "bar", c)
-
-	go receiver(ch, stop)
-
-	_, err = c.Watch("watch_foo", 0, false, ch, stop)
-	if err != ErrWatchStoppedByUser {
-		t.Fatalf("Watch returned a non-user stop error")
-	}
-
-	if newRoutineNum := runtime.NumGoroutine(); newRoutineNum != routineNum {
-		t.Fatalf("Routine numbers differ after watch stop: %v, %v", routineNum, newRoutineNum)
-	}
-}
-
-func TestWatchAll(t *testing.T) {
-	c := NewClient(nil)
-	defer func() {
-		c.Delete("watch_foo", true)
-	}()
-
-	go setHelper("watch_foo/foo", "bar", c)
-
-	resp, err := c.Watch("watch_foo", 0, true, nil, nil)
-	if err != nil {
-		t.Fatal(err)
-	}
-	if !(resp.Node.Key == "/watch_foo/foo" && resp.Node.Value == "bar") {
-		t.Fatalf("WatchAll 1 failed: %#v", resp)
-	}
-
-	go setHelper("watch_foo/foo", "bar", c)
-
-	resp, err = c.Watch("watch_foo", resp.Node.ModifiedIndex+1, true, nil, nil)
-	if err != nil {
-		t.Fatal(err)
-	}
-	if !(resp.Node.Key == "/watch_foo/foo" && resp.Node.Value == "bar") {
-		t.Fatalf("WatchAll 2 failed: %#v", resp)
-	}
-
-	ch := make(chan *Response, 10)
-	stop := make(chan bool, 1)
-
-	routineNum := runtime.NumGoroutine()
-
-	go setLoop("watch_foo/foo", "bar", c)
-
-	go receiver(ch, stop)
-
-	_, err = c.Watch("watch_foo", 0, true, ch, stop)
-	if err != ErrWatchStoppedByUser {
-		t.Fatalf("Watch returned a non-user stop error")
-	}
-
-	if newRoutineNum := runtime.NumGoroutine(); newRoutineNum != routineNum {
-		t.Fatalf("Routine numbers differ after watch stop: %v, %v", routineNum, newRoutineNum)
-	}
-}
-
-func setHelper(key, value string, c *Client) {
-	time.Sleep(time.Second)
-	c.Set(key, value, 100)
-}
-
-func setLoop(key, value string, c *Client) {
-	time.Sleep(time.Second)
-	for i := 0; i < 10; i++ {
-		newValue := fmt.Sprintf("%s_%v", value, i)
-		c.Set(key, newValue, 100)
-		time.Sleep(time.Second / 10)
-	}
-}
-
-func receiver(c chan *Response, stop chan bool) {
-	for i := 0; i < 10; i++ {
-		<-c
-	}
-	stop <- true
-}