parent
8d758be3e4
commit
02ced2c2d7
|
@ -1,7 +1,6 @@
|
|||
# Client libraries support matrix for etcd
|
||||
|
||||
As etcd features support is really uneven between client libraries, a compatibility matrix can be important.
|
||||
We will consider in detail only the features of clients supporting the v2 API. Clients still supporting the v1 API *only* are listed below.
|
||||
|
||||
## v2 clients
|
||||
|
||||
|
@ -14,6 +13,7 @@ The v2 API has a lot of features, we will categorize them in a few categories:
|
|||
- **GET,PUT,POST,DEL Features**: Support for all the modifiers when calling the etcd server with said HTTP method.
|
||||
|
||||
### Supported features matrix
|
||||
|
||||
**Legend**
|
||||
**F**: Full support **G**: Good support **B**: Basic support
|
||||
**Y**: Feature supported **-**: Feature not supported
|
||||
|
@ -30,6 +30,7 @@ Sorted alphabetically on language/name
|
|||
|[go-etcd](https://github.com/coreos/go-etcd) |go |Y|Y|F|F|F|F|-|-|
|
||||
|[etcd4j](https://github.com/jurmous/etcd4j) |java |Y|Y|F|F|F|F|-|-|
|
||||
|[jetcd](https://github.com/diwakergupta/jetcd) |java |Y|-|B|B|-|B|-|-|
|
||||
|[jetcd](https://github.com/justinsb/jetcd) |java |-|-|B|B|-|B|-|-|
|
||||
|[Etcd.jl](https://github.com/forio/Etcd.jl) |Julia |-|-|F|F|F|F|Y|Y|
|
||||
|[etcetera](https://github.com/drusellers/etcetera) |.net |-|-|F|F|F|F|-|-|
|
||||
|[node-etcd](https://github.com/stianeikeland/node-etcd) |nodejs |Y|-|F|F|-|F|-|-|
|
||||
|
@ -37,15 +38,5 @@ Sorted alphabetically on language/name
|
|||
|[p5-etcd](https://metacpan.org/release/Etcd) |perl |-|-|F|F|F|F|-|-|
|
||||
|[python-etcd](https://github.com/jplana/python-etcd) |python |Y|Y|F|F|F|F|Y|-|
|
||||
|[python-etcd-client](https://github.com/dsoprea/PythonEtcdClient)|python |Y|Y|F|F|F|F|Y|Y|
|
||||
|[txetcd](https://github.com/russellhaering/txetcd) |python |-|-|G|G|F|G|-|-|
|
||||
|[etcd-ruby](https://github.com/ranjib/etcd-ruby) |ruby |-|-|F|F|F|F|-|-|
|
||||
|
||||
## v1-only clients
|
||||
|
||||
Clients supporting only the API version 1
|
||||
|
||||
- [justinsb/jetcd](https://github.com/justinsb/jetcd) Java
|
||||
- [transitorykris/etcd-py](https://github.com/transitorykris/etcd-py) Python
|
||||
- [russellhaering/txetcd](https://github.com/russellhaering/txetcd) Python
|
||||
- [iconara/etcd-rb](https://github.com/iconara/etcd-rb) Ruby
|
||||
- [jpfuentes2/etcd-ruby](https://github.com/jpfuentes2/etcd-ruby) Ruby
|
||||
- [marshall-lee/etcd.erl](https://github.com/marshall-lee/etcd.erl) Erlang
|
||||
|
|
|
@ -120,10 +120,10 @@ curl -L http://127.0.0.1:4001/version
|
|||
|
||||
#### API Versioning
|
||||
|
||||
Clients are encouraged to use the `v2` API. The `v1` API will not change.
|
||||
|
||||
The `v2` API responses should not change after the 0.2.0 release but new features will be added over time.
|
||||
|
||||
The `v1` API has been deprecated and will not be supported.
|
||||
|
||||
During the pre-v1.0.0 series of releases we may break the API as we fix bugs and get feedback.
|
||||
|
||||
#### 32bit systems
|
||||
|
|
|
@ -17,7 +17,6 @@ import (
|
|||
"github.com/coreos/etcd/metrics"
|
||||
"github.com/coreos/etcd/mod"
|
||||
uhttp "github.com/coreos/etcd/pkg/http"
|
||||
"github.com/coreos/etcd/server/v1"
|
||||
"github.com/coreos/etcd/server/v2"
|
||||
"github.com/coreos/etcd/store"
|
||||
_ "github.com/coreos/etcd/store/v2"
|
||||
|
@ -107,19 +106,6 @@ func (s *Server) SetStore(store store.Store) {
|
|||
s.store = store
|
||||
}
|
||||
|
||||
func (s *Server) installV1(r *mux.Router) {
|
||||
s.handleFuncV1(r, "/v1/keys/{key:.*}", v1.GetKeyHandler).Methods("GET", "HEAD")
|
||||
s.handleFuncV1(r, "/v1/keys/{key:.*}", v1.SetKeyHandler).Methods("POST", "PUT")
|
||||
s.handleFuncV1(r, "/v1/keys/{key:.*}", v1.DeleteKeyHandler).Methods("DELETE")
|
||||
s.handleFuncV1(r, "/v1/watch/{key:.*}", v1.WatchKeyHandler).Methods("GET", "HEAD", "POST")
|
||||
s.handleFunc(r, "/v1/leader", s.GetLeaderHandler).Methods("GET", "HEAD")
|
||||
s.handleFunc(r, "/v1/machines", s.GetPeersHandler).Methods("GET", "HEAD")
|
||||
s.handleFunc(r, "/v1/peers", s.GetPeersHandler).Methods("GET", "HEAD")
|
||||
s.handleFunc(r, "/v1/stats/self", s.GetStatsHandler).Methods("GET", "HEAD")
|
||||
s.handleFunc(r, "/v1/stats/leader", s.GetLeaderStatsHandler).Methods("GET", "HEAD")
|
||||
s.handleFunc(r, "/v1/stats/store", s.GetStoreStatsHandler).Methods("GET", "HEAD")
|
||||
}
|
||||
|
||||
func (s *Server) installV2(r *mux.Router) {
|
||||
r2 := mux.NewRouter()
|
||||
r.PathPrefix("/v2").Handler(ehttp.NewLowerQueryParamsHandler(r2))
|
||||
|
@ -150,13 +136,6 @@ func (s *Server) installDebug(r *mux.Router) {
|
|||
r.HandleFunc("/debug/pprof/{name}", pprof.Index)
|
||||
}
|
||||
|
||||
// Adds a v1 server handler to the router.
|
||||
func (s *Server) handleFuncV1(r *mux.Router, path string, f func(http.ResponseWriter, *http.Request, v1.Server) error) *mux.Route {
|
||||
return s.handleFunc(r, path, func(w http.ResponseWriter, req *http.Request) error {
|
||||
return f(w, req, s)
|
||||
})
|
||||
}
|
||||
|
||||
// Adds a v2 server handler to the router.
|
||||
func (s *Server) handleFuncV2(r *mux.Router, path string, f func(http.ResponseWriter, *http.Request, v2.Server) error) *mux.Route {
|
||||
return s.handleFunc(r, path, func(w http.ResponseWriter, req *http.Request) error {
|
||||
|
@ -202,7 +181,6 @@ func (s *Server) HTTPHandler() http.Handler {
|
|||
|
||||
// Install the routes.
|
||||
s.handleFunc(router, "/version", s.GetVersionHandler).Methods("GET")
|
||||
s.installV1(router)
|
||||
s.installV2(router)
|
||||
// Mod is deprecated temporariy due to its unstable state.
|
||||
// It would be added back later.
|
||||
|
@ -235,26 +213,20 @@ func (s *Server) Dispatch(c raft.Command, w http.ResponseWriter, req *http.Reque
|
|||
return nil
|
||||
}
|
||||
|
||||
var b []byte
|
||||
if strings.HasPrefix(req.URL.Path, "/v1") {
|
||||
b, _ = json.Marshal(result.(*store.Event).Response(0))
|
||||
w.WriteHeader(http.StatusOK)
|
||||
e, _ := result.(*store.Event)
|
||||
b, _ := json.Marshal(e)
|
||||
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
// etcd index should be the same as the event index
|
||||
// which is also the last modified index of the node
|
||||
w.Header().Add("X-Etcd-Index", fmt.Sprint(e.Index()))
|
||||
w.Header().Add("X-Raft-Index", fmt.Sprint(s.CommitIndex()))
|
||||
w.Header().Add("X-Raft-Term", fmt.Sprint(s.Term()))
|
||||
|
||||
if e.IsCreated() {
|
||||
w.WriteHeader(http.StatusCreated)
|
||||
} else {
|
||||
e, _ := result.(*store.Event)
|
||||
b, _ = json.Marshal(e)
|
||||
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
// etcd index should be the same as the event index
|
||||
// which is also the last modified index of the node
|
||||
w.Header().Add("X-Etcd-Index", fmt.Sprint(e.Index()))
|
||||
w.Header().Add("X-Raft-Index", fmt.Sprint(s.CommitIndex()))
|
||||
w.Header().Add("X-Raft-Term", fmt.Sprint(s.Term()))
|
||||
|
||||
if e.IsCreated() {
|
||||
w.WriteHeader(http.StatusCreated)
|
||||
} else {
|
||||
w.WriteHeader(http.StatusOK)
|
||||
}
|
||||
w.WriteHeader(http.StatusOK)
|
||||
}
|
||||
|
||||
w.Write(b)
|
||||
|
|
|
@ -1,15 +0,0 @@
|
|||
package v1
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"github.com/coreos/etcd/third_party/github.com/gorilla/mux"
|
||||
)
|
||||
|
||||
// Removes a key from the store.
|
||||
func DeleteKeyHandler(w http.ResponseWriter, req *http.Request, s Server) error {
|
||||
vars := mux.Vars(req)
|
||||
key := "/" + vars["key"]
|
||||
c := s.Store().CommandFactory().CreateDeleteCommand(key, false, false)
|
||||
return s.Dispatch(c, w, req)
|
||||
}
|
|
@ -1,31 +0,0 @@
|
|||
package v1
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
|
||||
"github.com/coreos/etcd/third_party/github.com/gorilla/mux"
|
||||
)
|
||||
|
||||
// Retrieves the value for a given key.
|
||||
func GetKeyHandler(w http.ResponseWriter, req *http.Request, s Server) error {
|
||||
vars := mux.Vars(req)
|
||||
key := "/" + vars["key"]
|
||||
|
||||
// Retrieve the key from the store.
|
||||
event, err := s.Store().Get(key, false, false)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
w.WriteHeader(http.StatusOK)
|
||||
|
||||
if req.Method == "HEAD" {
|
||||
return nil
|
||||
}
|
||||
|
||||
// Convert event to a response and write to client.
|
||||
b, _ := json.Marshal(event.Response(s.Store().Index()))
|
||||
w.Write(b)
|
||||
return nil
|
||||
}
|
|
@ -1,47 +0,0 @@
|
|||
package v1
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
etcdErr "github.com/coreos/etcd/error"
|
||||
"github.com/coreos/etcd/store"
|
||||
"github.com/coreos/etcd/third_party/github.com/goraft/raft"
|
||||
"github.com/coreos/etcd/third_party/github.com/gorilla/mux"
|
||||
)
|
||||
|
||||
// Sets the value for a given key.
|
||||
func SetKeyHandler(w http.ResponseWriter, req *http.Request, s Server) error {
|
||||
vars := mux.Vars(req)
|
||||
key := "/" + vars["key"]
|
||||
|
||||
req.ParseForm()
|
||||
|
||||
// Parse non-blank value.
|
||||
value := req.Form.Get("value")
|
||||
if len(value) == 0 {
|
||||
return etcdErr.NewError(200, "Set", s.Store().Index())
|
||||
}
|
||||
|
||||
// Convert time-to-live to an expiration time.
|
||||
expireTime, err := store.TTL(req.Form.Get("ttl"))
|
||||
if err != nil {
|
||||
return etcdErr.NewError(202, "Set", s.Store().Index())
|
||||
}
|
||||
|
||||
// If the "prevValue" is specified then test-and-set. Otherwise create a new key.
|
||||
var c raft.Command
|
||||
if prevValueArr, ok := req.Form["prevValue"]; ok {
|
||||
if len(prevValueArr[0]) > 0 {
|
||||
// test against previous value
|
||||
c = s.Store().CommandFactory().CreateCompareAndSwapCommand(key, value, prevValueArr[0], 0, expireTime)
|
||||
} else {
|
||||
// test against existence
|
||||
c = s.Store().CommandFactory().CreateCreateCommand(key, false, value, expireTime, false)
|
||||
}
|
||||
|
||||
} else {
|
||||
c = s.Store().CommandFactory().CreateSetCommand(key, false, value, expireTime)
|
||||
}
|
||||
|
||||
return s.Dispatch(c, w, req)
|
||||
}
|
|
@ -1,31 +0,0 @@
|
|||
package v1
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"testing"
|
||||
|
||||
"github.com/coreos/etcd/server"
|
||||
"github.com/coreos/etcd/tests"
|
||||
"github.com/coreos/etcd/third_party/github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
// Ensures that a key is deleted.
|
||||
//
|
||||
// $ curl -X PUT localhost:4001/v1/keys/foo/bar -d value=XXX
|
||||
// $ curl -X DELETE localhost:4001/v1/keys/foo/bar
|
||||
//
|
||||
func TestV1DeleteKey(t *testing.T) {
|
||||
tests.RunServer(func(s *server.Server) {
|
||||
v := url.Values{}
|
||||
v.Set("value", "XXX")
|
||||
resp, err := tests.PutForm(fmt.Sprintf("%s%s", s.URL(), "/v1/keys/foo/bar"), v)
|
||||
tests.ReadBody(resp)
|
||||
resp, err = tests.DeleteForm(fmt.Sprintf("%s%s", s.URL(), "/v1/keys/foo/bar"), url.Values{})
|
||||
assert.Equal(t, resp.StatusCode, http.StatusOK)
|
||||
body := tests.ReadBody(resp)
|
||||
assert.Nil(t, err, "")
|
||||
assert.Equal(t, string(body), `{"action":"delete","key":"/foo/bar","prevValue":"XXX","index":4}`, "")
|
||||
})
|
||||
}
|
|
@ -1,209 +0,0 @@
|
|||
package v1
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/coreos/etcd/server"
|
||||
"github.com/coreos/etcd/tests"
|
||||
"github.com/coreos/etcd/third_party/github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
// Ensures that a value can be retrieve for a given key.
|
||||
//
|
||||
// $ curl localhost:4001/v1/keys/foo/bar -> fail
|
||||
// $ curl -X PUT localhost:4001/v1/keys/foo/bar -d value=XXX
|
||||
// $ curl localhost:4001/v1/keys/foo/bar
|
||||
//
|
||||
func TestV1GetKey(t *testing.T) {
|
||||
tests.RunServer(func(s *server.Server) {
|
||||
v := url.Values{}
|
||||
v.Set("value", "XXX")
|
||||
fullURL := fmt.Sprintf("%s%s", s.URL(), "/v1/keys/foo/bar")
|
||||
resp, _ := tests.Get(fullURL)
|
||||
assert.Equal(t, resp.StatusCode, http.StatusNotFound)
|
||||
|
||||
resp, _ = tests.PutForm(fullURL, v)
|
||||
tests.ReadBody(resp)
|
||||
|
||||
resp, _ = tests.Get(fullURL)
|
||||
assert.Equal(t, resp.StatusCode, http.StatusOK)
|
||||
body := tests.ReadBodyJSON(resp)
|
||||
assert.Equal(t, body["action"], "get", "")
|
||||
assert.Equal(t, body["key"], "/foo/bar", "")
|
||||
assert.Equal(t, body["value"], "XXX", "")
|
||||
assert.Equal(t, body["index"], 3, "")
|
||||
})
|
||||
}
|
||||
|
||||
// Ensures that a directory of values can be retrieved for a given key.
|
||||
//
|
||||
// $ curl -X PUT localhost:4001/v1/keys/foo/x -d value=XXX
|
||||
// $ curl -X PUT localhost:4001/v1/keys/foo/y/z -d value=YYY
|
||||
// $ curl localhost:4001/v1/keys/foo
|
||||
//
|
||||
func TestV1GetKeyDir(t *testing.T) {
|
||||
tests.RunServer(func(s *server.Server) {
|
||||
v := url.Values{}
|
||||
v.Set("value", "XXX")
|
||||
resp, _ := tests.PutForm(fmt.Sprintf("%s%s", s.URL(), "/v1/keys/foo/x"), v)
|
||||
tests.ReadBody(resp)
|
||||
|
||||
v.Set("value", "YYY")
|
||||
resp, _ = tests.PutForm(fmt.Sprintf("%s%s", s.URL(), "/v1/keys/foo/y/z"), v)
|
||||
tests.ReadBody(resp)
|
||||
|
||||
resp, _ = tests.Get(fmt.Sprintf("%s%s", s.URL(), "/v1/keys/foo"))
|
||||
assert.Equal(t, resp.StatusCode, http.StatusOK)
|
||||
body := tests.ReadBody(resp)
|
||||
nodes := make([]interface{}, 0)
|
||||
if err := json.Unmarshal(body, &nodes); err != nil {
|
||||
panic(fmt.Sprintf("HTTP body JSON parse error: %v", err))
|
||||
}
|
||||
assert.Equal(t, len(nodes), 2, "")
|
||||
|
||||
node0 := nodes[0].(map[string]interface{})
|
||||
assert.Equal(t, node0["action"], "get", "")
|
||||
assert.Equal(t, node0["key"], "/foo/x", "")
|
||||
assert.Equal(t, node0["value"], "XXX", "")
|
||||
|
||||
node1 := nodes[1].(map[string]interface{})
|
||||
assert.Equal(t, node1["action"], "get", "")
|
||||
assert.Equal(t, node1["key"], "/foo/y", "")
|
||||
assert.Equal(t, node1["dir"], true, "")
|
||||
})
|
||||
}
|
||||
|
||||
// Ensures that a watcher can wait for a value to be set and return it to the client.
|
||||
//
|
||||
// $ curl localhost:4001/v1/watch/foo/bar
|
||||
// $ curl -X PUT localhost:4001/v1/keys/foo/bar -d value=XXX
|
||||
//
|
||||
func TestV1WatchKey(t *testing.T) {
|
||||
tests.RunServer(func(s *server.Server) {
|
||||
// There exists a little gap between etcd ready to serve and
|
||||
// it actually serves the first request, which means the response
|
||||
// delay could be a little bigger.
|
||||
// This test is time sensitive, so it does one request to ensure
|
||||
// that the server is working.
|
||||
tests.Get(fmt.Sprintf("%s%s", s.URL(), "/v1/keys/foo/bar"))
|
||||
|
||||
var watchResp *http.Response
|
||||
c := make(chan bool)
|
||||
go func() {
|
||||
watchResp, _ = tests.Get(fmt.Sprintf("%s%s", s.URL(), "/v1/watch/foo/bar"))
|
||||
c <- true
|
||||
}()
|
||||
|
||||
// Make sure response didn't fire early.
|
||||
time.Sleep(1 * time.Millisecond)
|
||||
|
||||
// Set a value.
|
||||
v := url.Values{}
|
||||
v.Set("value", "XXX")
|
||||
resp, _ := tests.PutForm(fmt.Sprintf("%s%s", s.URL(), "/v1/keys/foo/bar"), v)
|
||||
tests.ReadBody(resp)
|
||||
|
||||
// A response should follow from the GET above.
|
||||
time.Sleep(1 * time.Millisecond)
|
||||
|
||||
select {
|
||||
case <-c:
|
||||
|
||||
default:
|
||||
t.Fatal("cannot get watch result")
|
||||
}
|
||||
|
||||
body := tests.ReadBodyJSON(watchResp)
|
||||
assert.NotNil(t, body, "")
|
||||
assert.Equal(t, body["action"], "set", "")
|
||||
|
||||
assert.Equal(t, body["key"], "/foo/bar", "")
|
||||
assert.Equal(t, body["value"], "XXX", "")
|
||||
assert.Equal(t, body["index"], 3, "")
|
||||
})
|
||||
}
|
||||
|
||||
// Ensures that a watcher can wait for a value to be set after a given index.
|
||||
//
|
||||
// $ curl -X POST localhost:4001/v1/watch/foo/bar -d index=4
|
||||
// $ curl -X PUT localhost:4001/v1/keys/foo/bar -d value=XXX
|
||||
// $ curl -X PUT localhost:4001/v1/keys/foo/bar -d value=YYY
|
||||
//
|
||||
func TestV1WatchKeyWithIndex(t *testing.T) {
|
||||
tests.RunServer(func(s *server.Server) {
|
||||
var body map[string]interface{}
|
||||
c := make(chan bool)
|
||||
go func() {
|
||||
v := url.Values{}
|
||||
v.Set("index", "4")
|
||||
resp, _ := tests.PostForm(fmt.Sprintf("%s%s", s.URL(), "/v1/watch/foo/bar"), v)
|
||||
body = tests.ReadBodyJSON(resp)
|
||||
c <- true
|
||||
}()
|
||||
|
||||
// Make sure response didn't fire early.
|
||||
time.Sleep(1 * time.Millisecond)
|
||||
assert.Nil(t, body, "")
|
||||
|
||||
// Set a value (before given index).
|
||||
v := url.Values{}
|
||||
v.Set("value", "XXX")
|
||||
resp, _ := tests.PutForm(fmt.Sprintf("%s%s", s.URL(), "/v1/keys/foo/bar"), v)
|
||||
tests.ReadBody(resp)
|
||||
|
||||
// Make sure response didn't fire early.
|
||||
time.Sleep(1 * time.Millisecond)
|
||||
assert.Nil(t, body, "")
|
||||
|
||||
// Set a value (before given index).
|
||||
v.Set("value", "YYY")
|
||||
resp, _ = tests.PutForm(fmt.Sprintf("%s%s", s.URL(), "/v1/keys/foo/bar"), v)
|
||||
tests.ReadBody(resp)
|
||||
|
||||
// A response should follow from the GET above.
|
||||
time.Sleep(1 * time.Millisecond)
|
||||
|
||||
select {
|
||||
case <-c:
|
||||
|
||||
default:
|
||||
t.Fatal("cannot get watch result")
|
||||
}
|
||||
|
||||
assert.NotNil(t, body, "")
|
||||
assert.Equal(t, body["action"], "set", "")
|
||||
|
||||
assert.Equal(t, body["key"], "/foo/bar", "")
|
||||
assert.Equal(t, body["value"], "YYY", "")
|
||||
assert.Equal(t, body["index"], 4, "")
|
||||
})
|
||||
}
|
||||
|
||||
// Ensures that HEAD works.
|
||||
//
|
||||
// $ curl -I localhost:4001/v1/keys/foo/bar -> fail
|
||||
// $ curl -X PUT localhost:4001/v1/keys/foo/bar -d value=XXX
|
||||
// $ curl -I localhost:4001/v1/keys/foo/bar
|
||||
//
|
||||
func TestV1HeadKey(t *testing.T) {
|
||||
tests.RunServer(func(s *server.Server) {
|
||||
v := url.Values{}
|
||||
v.Set("value", "XXX")
|
||||
fullURL := fmt.Sprintf("%s%s", s.URL(), "/v1/keys/foo/bar")
|
||||
resp, _ := tests.Get(fullURL)
|
||||
assert.Equal(t, resp.StatusCode, http.StatusNotFound)
|
||||
assert.Equal(t, resp.ContentLength, -1)
|
||||
|
||||
resp, _ = tests.PutForm(fullURL, v)
|
||||
tests.ReadBody(resp)
|
||||
|
||||
resp, _ = tests.Get(fullURL)
|
||||
assert.Equal(t, resp.StatusCode, http.StatusOK)
|
||||
assert.Equal(t, resp.ContentLength, -1)
|
||||
})
|
||||
}
|
|
@ -1,157 +0,0 @@
|
|||
package v1
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/coreos/etcd/server"
|
||||
"github.com/coreos/etcd/tests"
|
||||
"github.com/coreos/etcd/third_party/github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
// Ensures that a key is set to a given value.
|
||||
//
|
||||
// $ curl -X PUT localhost:4001/v1/keys/foo/bar -d value=XXX
|
||||
//
|
||||
func TestV1SetKey(t *testing.T) {
|
||||
tests.RunServer(func(s *server.Server) {
|
||||
v := url.Values{}
|
||||
v.Set("value", "XXX")
|
||||
resp, err := tests.PutForm(fmt.Sprintf("%s%s", s.URL(), "/v1/keys/foo/bar"), v)
|
||||
assert.Equal(t, resp.StatusCode, http.StatusOK)
|
||||
body := tests.ReadBody(resp)
|
||||
assert.Nil(t, err, "")
|
||||
|
||||
assert.Equal(t, string(body), `{"action":"set","key":"/foo/bar","value":"XXX","newKey":true,"index":3}`, "")
|
||||
})
|
||||
}
|
||||
|
||||
// Ensures that a time-to-live is added to a key.
|
||||
//
|
||||
// $ curl -X PUT localhost:4001/v1/keys/foo/bar -d value=XXX -d ttl=20
|
||||
//
|
||||
func TestV1SetKeyWithTTL(t *testing.T) {
|
||||
tests.RunServer(func(s *server.Server) {
|
||||
t0 := time.Now()
|
||||
v := url.Values{}
|
||||
v.Set("value", "XXX")
|
||||
v.Set("ttl", "20")
|
||||
resp, _ := tests.PutForm(fmt.Sprintf("%s%s", s.URL(), "/v1/keys/foo/bar"), v)
|
||||
assert.Equal(t, resp.StatusCode, http.StatusOK)
|
||||
body := tests.ReadBodyJSON(resp)
|
||||
assert.Equal(t, body["ttl"], 20, "")
|
||||
|
||||
// Make sure the expiration date is correct.
|
||||
expiration, _ := time.Parse(time.RFC3339Nano, body["expiration"].(string))
|
||||
assert.Equal(t, expiration.Sub(t0)/time.Second, 20, "")
|
||||
})
|
||||
}
|
||||
|
||||
// Ensures that an invalid time-to-live is returned as an error.
|
||||
//
|
||||
// $ curl -X PUT localhost:4001/v1/keys/foo/bar -d value=XXX -d ttl=bad_ttl
|
||||
//
|
||||
func TestV1SetKeyWithBadTTL(t *testing.T) {
|
||||
tests.RunServer(func(s *server.Server) {
|
||||
v := url.Values{}
|
||||
v.Set("value", "XXX")
|
||||
v.Set("ttl", "bad_ttl")
|
||||
resp, _ := tests.PutForm(fmt.Sprintf("%s%s", s.URL(), "/v1/keys/foo/bar"), v)
|
||||
assert.Equal(t, resp.StatusCode, http.StatusBadRequest)
|
||||
body := tests.ReadBodyJSON(resp)
|
||||
assert.Equal(t, body["errorCode"], 202, "")
|
||||
assert.Equal(t, body["message"], "The given TTL in POST form is not a number", "")
|
||||
assert.Equal(t, body["cause"], "Set", "")
|
||||
})
|
||||
}
|
||||
|
||||
// Ensures that a key is conditionally set if it previously did not exist.
|
||||
//
|
||||
// $ curl -X PUT localhost:4001/v1/keys/foo/bar -d value=XXX -d prevValue=
|
||||
//
|
||||
func TestV1CreateKeySuccess(t *testing.T) {
|
||||
tests.RunServer(func(s *server.Server) {
|
||||
v := url.Values{}
|
||||
v.Set("value", "XXX")
|
||||
v.Set("prevValue", "")
|
||||
resp, _ := tests.PutForm(fmt.Sprintf("%s%s", s.URL(), "/v1/keys/foo/bar"), v)
|
||||
assert.Equal(t, resp.StatusCode, http.StatusOK)
|
||||
body := tests.ReadBodyJSON(resp)
|
||||
assert.Equal(t, body["value"], "XXX", "")
|
||||
})
|
||||
}
|
||||
|
||||
// Ensures that a key is not conditionally set because it previously existed.
|
||||
//
|
||||
// $ curl -X PUT localhost:4001/v1/keys/foo/bar -d value=XXX -d prevValue=
|
||||
// $ curl -X PUT localhost:4001/v1/keys/foo/bar -d value=XXX -d prevValue= -> fail
|
||||
//
|
||||
func TestV1CreateKeyFail(t *testing.T) {
|
||||
tests.RunServer(func(s *server.Server) {
|
||||
v := url.Values{}
|
||||
v.Set("value", "XXX")
|
||||
v.Set("prevValue", "")
|
||||
fullURL := fmt.Sprintf("%s%s", s.URL(), "/v1/keys/foo/bar")
|
||||
resp, _ := tests.PutForm(fullURL, v)
|
||||
assert.Equal(t, resp.StatusCode, http.StatusOK)
|
||||
tests.ReadBody(resp)
|
||||
resp, _ = tests.PutForm(fullURL, v)
|
||||
assert.Equal(t, resp.StatusCode, http.StatusPreconditionFailed)
|
||||
body := tests.ReadBodyJSON(resp)
|
||||
assert.Equal(t, body["errorCode"], 105, "")
|
||||
assert.Equal(t, body["message"], "Key already exists", "")
|
||||
assert.Equal(t, body["cause"], "/foo/bar", "")
|
||||
})
|
||||
}
|
||||
|
||||
// Ensures that a key is set only if the previous value matches.
|
||||
//
|
||||
// $ curl -X PUT localhost:4001/v1/keys/foo/bar -d value=XXX
|
||||
// $ curl -X PUT localhost:4001/v1/keys/foo/bar -d value=YYY -d prevValue=XXX
|
||||
//
|
||||
func TestV1SetKeyCASOnValueSuccess(t *testing.T) {
|
||||
tests.RunServer(func(s *server.Server) {
|
||||
v := url.Values{}
|
||||
v.Set("value", "XXX")
|
||||
fullURL := fmt.Sprintf("%s%s", s.URL(), "/v1/keys/foo/bar")
|
||||
resp, _ := tests.PutForm(fullURL, v)
|
||||
assert.Equal(t, resp.StatusCode, http.StatusOK)
|
||||
tests.ReadBody(resp)
|
||||
v.Set("value", "YYY")
|
||||
v.Set("prevValue", "XXX")
|
||||
resp, _ = tests.PutForm(fullURL, v)
|
||||
assert.Equal(t, resp.StatusCode, http.StatusOK)
|
||||
body := tests.ReadBodyJSON(resp)
|
||||
assert.Equal(t, body["action"], "testAndSet", "")
|
||||
assert.Equal(t, body["value"], "YYY", "")
|
||||
assert.Equal(t, body["index"], 4, "")
|
||||
})
|
||||
}
|
||||
|
||||
// Ensures that a key is not set if the previous value does not match.
|
||||
//
|
||||
// $ curl -X PUT localhost:4001/v1/keys/foo/bar -d value=XXX
|
||||
// $ curl -X PUT localhost:4001/v1/keys/foo/bar -d value=YYY -d prevValue=AAA
|
||||
//
|
||||
func TestV1SetKeyCASOnValueFail(t *testing.T) {
|
||||
tests.RunServer(func(s *server.Server) {
|
||||
v := url.Values{}
|
||||
v.Set("value", "XXX")
|
||||
fullURL := fmt.Sprintf("%s%s", s.URL(), "/v1/keys/foo/bar")
|
||||
resp, _ := tests.PutForm(fullURL, v)
|
||||
assert.Equal(t, resp.StatusCode, http.StatusOK)
|
||||
tests.ReadBody(resp)
|
||||
v.Set("value", "YYY")
|
||||
v.Set("prevValue", "AAA")
|
||||
resp, _ = tests.PutForm(fullURL, v)
|
||||
assert.Equal(t, resp.StatusCode, http.StatusPreconditionFailed)
|
||||
body := tests.ReadBodyJSON(resp)
|
||||
assert.Equal(t, body["errorCode"], 101, "")
|
||||
assert.Equal(t, body["message"], "Compare failed", "")
|
||||
assert.Equal(t, body["cause"], "[AAA != XXX]", "")
|
||||
assert.Equal(t, body["index"], 3, "")
|
||||
})
|
||||
}
|
|
@ -1,16 +0,0 @@
|
|||
package v1
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"github.com/coreos/etcd/store"
|
||||
"github.com/coreos/etcd/third_party/github.com/goraft/raft"
|
||||
)
|
||||
|
||||
// The Server interface provides all the methods required for the v1 API.
|
||||
type Server interface {
|
||||
CommitIndex() uint64
|
||||
Term() uint64
|
||||
Store() store.Store
|
||||
Dispatch(raft.Command, http.ResponseWriter, *http.Request) error
|
||||
}
|
|
@ -1,42 +0,0 @@
|
|||
package v1
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
"strconv"
|
||||
|
||||
etcdErr "github.com/coreos/etcd/error"
|
||||
"github.com/coreos/etcd/third_party/github.com/gorilla/mux"
|
||||
)
|
||||
|
||||
// Watches a given key prefix for changes.
|
||||
func WatchKeyHandler(w http.ResponseWriter, req *http.Request, s Server) error {
|
||||
var err error
|
||||
vars := mux.Vars(req)
|
||||
key := "/" + vars["key"]
|
||||
|
||||
// Create a command to watch from a given index (default 0).
|
||||
var sinceIndex uint64 = 0
|
||||
if req.Method == "POST" {
|
||||
sinceIndex, err = strconv.ParseUint(string(req.FormValue("index")), 10, 64)
|
||||
if err != nil {
|
||||
return etcdErr.NewError(203, "Watch From Index", s.Store().Index())
|
||||
}
|
||||
}
|
||||
|
||||
// Start the watcher on the store.
|
||||
watcher, err := s.Store().Watch(key, false, false, sinceIndex)
|
||||
if err != nil {
|
||||
return etcdErr.NewError(500, key, s.Store().Index())
|
||||
}
|
||||
event := <-watcher.EventChan
|
||||
|
||||
// Convert event to a response and write to client.
|
||||
w.WriteHeader(http.StatusOK)
|
||||
if req.Method == "HEAD" {
|
||||
return nil
|
||||
}
|
||||
b, _ := json.Marshal(event.Response(s.Store().Index()))
|
||||
w.Write(b)
|
||||
return nil
|
||||
}
|
|
@ -45,54 +45,3 @@ func (e *Event) IsCreated() bool {
|
|||
func (e *Event) Index() uint64 {
|
||||
return e.Node.ModifiedIndex
|
||||
}
|
||||
|
||||
// Converts an event object into a response object.
|
||||
func (event *Event) Response(currentIndex uint64) interface{} {
|
||||
if !event.Node.Dir {
|
||||
response := &Response{
|
||||
Action: event.Action,
|
||||
Key: event.Node.Key,
|
||||
Value: event.Node.Value,
|
||||
Index: event.Node.ModifiedIndex,
|
||||
TTL: event.Node.TTL,
|
||||
Expiration: event.Node.Expiration,
|
||||
}
|
||||
|
||||
if event.PrevNode != nil {
|
||||
response.PrevValue = event.PrevNode.Value
|
||||
}
|
||||
|
||||
if currentIndex != 0 {
|
||||
response.Index = currentIndex
|
||||
}
|
||||
|
||||
if response.Action == Set {
|
||||
if response.PrevValue == nil {
|
||||
response.NewKey = true
|
||||
}
|
||||
}
|
||||
|
||||
if response.Action == CompareAndSwap || response.Action == Create {
|
||||
response.Action = "testAndSet"
|
||||
}
|
||||
|
||||
return response
|
||||
} else {
|
||||
responses := make([]*Response, len(event.Node.Nodes))
|
||||
|
||||
for i, node := range event.Node.Nodes {
|
||||
responses[i] = &Response{
|
||||
Action: event.Action,
|
||||
Key: node.Key,
|
||||
Value: node.Value,
|
||||
Dir: node.Dir,
|
||||
Index: node.ModifiedIndex,
|
||||
}
|
||||
|
||||
if currentIndex != 0 {
|
||||
responses[i].Index = currentIndex
|
||||
}
|
||||
}
|
||||
return responses
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,26 +0,0 @@
|
|||
package store
|
||||
|
||||
import (
|
||||
"time"
|
||||
)
|
||||
|
||||
// The response from the store to the user who issue a command
|
||||
type Response struct {
|
||||
Action string `json:"action"`
|
||||
Key string `json:"key"`
|
||||
Dir bool `json:"dir,omitempty"`
|
||||
PrevValue *string `json:"prevValue,omitempty"`
|
||||
Value *string `json:"value,omitempty"`
|
||||
|
||||
// If the key did not exist before the action,
|
||||
// this field should be set to true
|
||||
NewKey bool `json:"newKey,omitempty"`
|
||||
|
||||
Expiration *time.Time `json:"expiration,omitempty"`
|
||||
|
||||
// Time to live in second
|
||||
TTL int64 `json:"ttl,omitempty"`
|
||||
|
||||
// The command index of the raft machine when the command is executed
|
||||
Index uint64 `json:"index"`
|
||||
}
|
3
test.sh
3
test.sh
|
@ -17,9 +17,6 @@ go test -v ./server -race
|
|||
go test -i ./config
|
||||
go test -v ./config -race
|
||||
|
||||
go test -i ./server/v1/tests
|
||||
go test -v ./server/v1/tests -race
|
||||
|
||||
go test -i ./server/v2/tests
|
||||
go test -v ./server/v2/tests -race
|
||||
|
||||
|
|
|
@ -1,15 +0,0 @@
|
|||
README
|
||||
|
||||
The scripts in this directory should be run from the project root:
|
||||
|
||||
$ cd $GOPATH/src/github.com/coreos/etcd
|
||||
$ tests/fixtures/v1/run.1.sh
|
||||
|
||||
Scripts with numbers should be run in separate terminal windows (in order):
|
||||
|
||||
$ tests/fixtures/v1/run.1.sh
|
||||
$ tests/fixtures/v1/run.2.sh
|
||||
$ tests/fixtures/v1/run.3.sh
|
||||
$ tests/fixtures/v1/run.4.sh
|
||||
|
||||
The resulting server state data can be found in tmp/node*.
|
|
@ -1 +0,0 @@
|
|||
{"commitIndex":15,"peers":[{"name":"node2","connectionString":""}]}
|
|
@ -1,18 +0,0 @@
|
|||
{
|
||||
"name": "node0",
|
||||
"raftURL": "http://127.0.0.1:7001",
|
||||
"etcdURL": "http://127.0.0.1:4001",
|
||||
"webURL": "",
|
||||
"raftListenHost": "127.0.0.1:7001",
|
||||
"etcdListenHost": "127.0.0.1:4001",
|
||||
"raftTLS": {
|
||||
"CertFile": "",
|
||||
"KeyFile": "",
|
||||
"CAFile": ""
|
||||
},
|
||||
"etcdTLS": {
|
||||
"CertFile": "",
|
||||
"KeyFile": "",
|
||||
"CAFile": ""
|
||||
}
|
||||
}
|
Binary file not shown.
|
@ -1 +0,0 @@
|
|||
{"commitIndex":15,"peers":[{"name":"node0","connectionString":""}]}
|
|
@ -1,18 +0,0 @@
|
|||
{
|
||||
"name": "node2",
|
||||
"raftURL": "http://127.0.0.1:7002",
|
||||
"etcdURL": "http://127.0.0.1:4002",
|
||||
"webURL": "",
|
||||
"raftListenHost": "127.0.0.1:7002",
|
||||
"etcdListenHost": "127.0.0.1:4002",
|
||||
"raftTLS": {
|
||||
"CertFile": "",
|
||||
"KeyFile": "",
|
||||
"CAFile": ""
|
||||
},
|
||||
"etcdTLS": {
|
||||
"CertFile": "",
|
||||
"KeyFile": "",
|
||||
"CAFile": ""
|
||||
}
|
||||
}
|
Binary file not shown.
|
@ -1 +0,0 @@
|
|||
{"commitIndex":15,"peers":[{"name":"node0","connectionString":""},{"name":"node2","connectionString":""}]}
|
|
@ -1,18 +0,0 @@
|
|||
{
|
||||
"name": "node3",
|
||||
"raftURL": "http://127.0.0.1:7003",
|
||||
"etcdURL": "http://127.0.0.1:4003",
|
||||
"webURL": "",
|
||||
"raftListenHost": "127.0.0.1:7003",
|
||||
"etcdListenHost": "127.0.0.1:4003",
|
||||
"raftTLS": {
|
||||
"CertFile": "",
|
||||
"KeyFile": "",
|
||||
"CAFile": ""
|
||||
},
|
||||
"etcdTLS": {
|
||||
"CertFile": "",
|
||||
"KeyFile": "",
|
||||
"CAFile": ""
|
||||
}
|
||||
}
|
Binary file not shown.
|
@ -1,4 +0,0 @@
|
|||
#!/bin/sh
|
||||
|
||||
./build
|
||||
./etcd -d tmp/node0 -n node0
|
|
@ -1,3 +0,0 @@
|
|||
#!/bin/sh
|
||||
|
||||
./etcd -s 127.0.0.1:7002 -c 127.0.0.1:4002 -C 127.0.0.1:7001 -d tmp/node2 -n node2
|
|
@ -1,3 +0,0 @@
|
|||
#!/bin/sh
|
||||
|
||||
./etcd -s 127.0.0.1:7003 -c 127.0.0.1:4003 -C 127.0.0.1:7001 -d tmp/node3 -n node3
|
|
@ -1,13 +0,0 @@
|
|||
#!/bin/sh
|
||||
|
||||
curl -L http://127.0.0.1:4001/v1/keys/message -d value="Hello world"
|
||||
curl -L http://127.0.0.1:4001/v1/keys/message -d value="Hello etcd"
|
||||
curl -L http://127.0.0.1:4001/v1/keys/message -X DELETE
|
||||
curl -L http://127.0.0.1:4001/v1/keys/message2 -d value="Hola"
|
||||
curl -L http://127.0.0.1:4001/v1/keys/expiring -d value=bar -d ttl=5
|
||||
curl -L http://127.0.0.1:4001/v1/keys/foo -d value=one
|
||||
curl -L http://127.0.0.1:4001/v1/keys/foo -d prevValue=two -d value=three
|
||||
curl -L http://127.0.0.1:4001/v1/keys/foo -d prevValue=one -d value=two
|
||||
curl -L http://127.0.0.1:4001/v1/keys/bar -d prevValue= -d value=four
|
||||
curl -L http://127.0.0.1:4001/v1/keys/bar -d prevValue= -d value=five
|
||||
curl -X DELETE http://127.0.0.1:7001/remove/node2
|
|
@ -1,13 +0,0 @@
|
|||
README
|
||||
|
||||
The scripts in this directory should be run from the project root:
|
||||
|
||||
$ cd $GOPATH/src/github.com/coreos/etcd
|
||||
$ tests/fixtures/v1.solo/run.1.sh
|
||||
|
||||
Scripts with numbers should be run in separate terminal windows (in order):
|
||||
|
||||
$ tests/fixtures/v1/run.1.sh
|
||||
$ tests/fixtures/v1/run.2.sh
|
||||
|
||||
The resulting server state data can be found in tmp/node0.
|
|
@ -1 +0,0 @@
|
|||
{"commitIndex":1,"peers":[]}
|
|
@ -1,18 +0,0 @@
|
|||
{
|
||||
"name": "node0",
|
||||
"raftURL": "http://127.0.0.1:7001",
|
||||
"etcdURL": "http://127.0.0.1:4001",
|
||||
"webURL": "",
|
||||
"raftListenHost": "127.0.0.1:7001",
|
||||
"etcdListenHost": "127.0.0.1:4001",
|
||||
"raftTLS": {
|
||||
"CertFile": "",
|
||||
"KeyFile": "",
|
||||
"CAFile": ""
|
||||
},
|
||||
"etcdTLS": {
|
||||
"CertFile": "",
|
||||
"KeyFile": "",
|
||||
"CAFile": ""
|
||||
}
|
||||
}
|
Binary file not shown.
|
@ -1,4 +0,0 @@
|
|||
#!/bin/sh
|
||||
|
||||
./build
|
||||
./etcd -d tmp/node0 -n node0
|
|
@ -1,3 +0,0 @@
|
|||
#!/bin/sh
|
||||
|
||||
curl -L http://127.0.0.1:4001/v1/keys/message -d value="Hello world"
|
|
@ -227,7 +227,7 @@ func Monitor(size int, allowDeadNum int, leaderChan chan string, all chan bool,
|
|||
|
||||
func getLeader(addr string) (string, error) {
|
||||
|
||||
resp, err := client.Get(addr + "/v1/leader")
|
||||
resp, err := client.Get(addr + "/v2/leader")
|
||||
|
||||
if err != nil {
|
||||
return "", err
|
||||
|
|
|
@ -1,106 +0,0 @@
|
|||
package test
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"net/http"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/coreos/etcd/tests"
|
||||
"github.com/coreos/etcd/third_party/github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
// Ensure that we can start a v2 node from the log of a v1 node.
|
||||
func TestV1SoloMigration(t *testing.T) {
|
||||
path, _ := ioutil.TempDir("", "etcd-")
|
||||
os.MkdirAll(path, 0777)
|
||||
defer os.RemoveAll(path)
|
||||
|
||||
nodepath := filepath.Join(path, "node0")
|
||||
fixturepath, _ := filepath.Abs("../fixtures/v1.solo/node0")
|
||||
fmt.Println("DATA_DIR =", nodepath)
|
||||
|
||||
// Copy over fixture files.
|
||||
c := exec.Command("cp", "-rf", fixturepath, nodepath)
|
||||
if out, err := c.CombinedOutput(); err != nil {
|
||||
fmt.Println(">>>>>>\n", string(out), "<<<<<<")
|
||||
panic("Fixture initialization error:" + err.Error())
|
||||
}
|
||||
|
||||
procAttr := new(os.ProcAttr)
|
||||
procAttr.Files = []*os.File{nil, os.Stdout, os.Stderr}
|
||||
|
||||
args := []string{"etcd", fmt.Sprintf("-data-dir=%s", nodepath)}
|
||||
args = append(args, "-addr", "127.0.0.1:4001")
|
||||
args = append(args, "-peer-addr", "127.0.0.1:7001")
|
||||
args = append(args, "-name", "node0")
|
||||
process, err := os.StartProcess(EtcdBinPath, args, procAttr)
|
||||
if err != nil {
|
||||
t.Fatal("start process failed:" + err.Error())
|
||||
return
|
||||
}
|
||||
defer process.Kill()
|
||||
time.Sleep(time.Second)
|
||||
|
||||
// Ensure deleted message is removed.
|
||||
resp, err := tests.Get("http://localhost:4001/v2/keys/message")
|
||||
tests.ReadBody(resp)
|
||||
assert.Nil(t, err, "")
|
||||
assert.Equal(t, resp.StatusCode, 200, "")
|
||||
}
|
||||
|
||||
// Ensure that we can start a v2 cluster from the logs of a v1 cluster.
|
||||
func TestV1ClusterMigration(t *testing.T) {
|
||||
path, _ := ioutil.TempDir("", "etcd-")
|
||||
os.RemoveAll(path)
|
||||
defer os.RemoveAll(path)
|
||||
|
||||
nodes := []string{"node0", "node2"}
|
||||
for i, node := range nodes {
|
||||
nodepath := filepath.Join(path, node)
|
||||
fixturepath, _ := filepath.Abs(filepath.Join("../fixtures/v1.cluster/", node))
|
||||
fmt.Println("FIXPATH =", fixturepath)
|
||||
fmt.Println("NODEPATH =", nodepath)
|
||||
os.MkdirAll(filepath.Dir(nodepath), 0777)
|
||||
|
||||
// Copy over fixture files.
|
||||
c := exec.Command("cp", "-rf", fixturepath, nodepath)
|
||||
if out, err := c.CombinedOutput(); err != nil {
|
||||
fmt.Println(">>>>>>\n", string(out), "<<<<<<")
|
||||
panic("Fixture initialization error:" + err.Error())
|
||||
}
|
||||
|
||||
procAttr := new(os.ProcAttr)
|
||||
procAttr.Files = []*os.File{nil, os.Stdout, os.Stderr}
|
||||
|
||||
args := []string{"etcd", fmt.Sprintf("-data-dir=%s", nodepath)}
|
||||
args = append(args, "-addr", fmt.Sprintf("127.0.0.1:%d", 4001+i))
|
||||
args = append(args, "-peer-addr", fmt.Sprintf("127.0.0.1:%d", 7001+i))
|
||||
args = append(args, "-name", node)
|
||||
process, err := os.StartProcess(EtcdBinPath, args, procAttr)
|
||||
if err != nil {
|
||||
t.Fatal("start process failed:" + err.Error())
|
||||
return
|
||||
}
|
||||
defer process.Kill()
|
||||
time.Sleep(time.Second)
|
||||
}
|
||||
|
||||
// Ensure deleted message is removed.
|
||||
resp, err := tests.Get("http://localhost:4001/v2/keys/message")
|
||||
body := tests.ReadBody(resp)
|
||||
assert.Nil(t, err, "")
|
||||
assert.Equal(t, resp.StatusCode, http.StatusNotFound)
|
||||
assert.Equal(t, string(body), `{"errorCode":100,"message":"Key not found","cause":"/message","index":11}`+"\n")
|
||||
|
||||
// Ensure TTL'd message is removed.
|
||||
resp, err = tests.Get("http://localhost:4001/v2/keys/foo")
|
||||
body = tests.ReadBody(resp)
|
||||
assert.Nil(t, err, "")
|
||||
assert.Equal(t, resp.StatusCode, 200, "")
|
||||
assert.Equal(t, string(body), `{"action":"get","node":{"key":"/foo","value":"one","modifiedIndex":9,"createdIndex":9}}`)
|
||||
}
|
Loading…
Reference in New Issue