Merge pull request #6308 from gyuho/manual2

client: do not send previous node data (optional)
This commit is contained in:
Gyu-Ho Lee 2016-08-30 13:33:22 -07:00 committed by GitHub
commit 48941cea95
7 changed files with 241 additions and 66 deletions

View File

@ -559,6 +559,25 @@ Let's create a key-value pair first: `foo=one`.
curl http://127.0.0.1:2379/v2/keys/foo -XPUT -d value=one curl http://127.0.0.1:2379/v2/keys/foo -XPUT -d value=one
``` ```
```json
{
"action":"set",
"node":{
"key":"/foo",
"value":"one",
"modifiedIndex":4,
"createdIndex":4
}
}
```
Specifying `noValueOnSuccess` option skips returning the node as value.
```sh
curl http://127.0.0.1:2379/v2/keys/foo?noValueOnSuccess=true -XPUT -d value=one
# {"action":"set"}
```
Now let's try some invalid `CompareAndSwap` commands. Now let's try some invalid `CompareAndSwap` commands.
Trying to set this existing key with `prevExist=false` fails as expected: Trying to set this existing key with `prevExist=false` fails as expected:

View File

@ -8,10 +8,11 @@ package client
import ( import (
"errors" "errors"
"fmt" "fmt"
codec1978 "github.com/ugorji/go/codec"
"reflect" "reflect"
"runtime" "runtime"
time "time" time "time"
codec1978 "github.com/ugorji/go/codec"
) )
const ( const (

View File

@ -191,6 +191,10 @@ type SetOptions struct {
// Dir specifies whether or not this Node should be created as a directory. // Dir specifies whether or not this Node should be created as a directory.
Dir bool Dir bool
// NoValueOnSuccess specifies whether the response contains the current value of the Node.
// If set, the response will only contain the current value when the request fails.
NoValueOnSuccess bool
} }
type GetOptions struct { type GetOptions struct {
@ -335,6 +339,7 @@ func (k *httpKeysAPI) Set(ctx context.Context, key, val string, opts *SetOptions
act.TTL = opts.TTL act.TTL = opts.TTL
act.Refresh = opts.Refresh act.Refresh = opts.Refresh
act.Dir = opts.Dir act.Dir = opts.Dir
act.NoValueOnSuccess = opts.NoValueOnSuccess
} }
doCtx := ctx doCtx := ctx
@ -523,15 +528,16 @@ func (w *waitAction) HTTPRequest(ep url.URL) *http.Request {
} }
type setAction struct { type setAction struct {
Prefix string Prefix string
Key string Key string
Value string Value string
PrevValue string PrevValue string
PrevIndex uint64 PrevIndex uint64
PrevExist PrevExistType PrevExist PrevExistType
TTL time.Duration TTL time.Duration
Refresh bool Refresh bool
Dir bool Dir bool
NoValueOnSuccess bool
} }
func (a *setAction) HTTPRequest(ep url.URL) *http.Request { func (a *setAction) HTTPRequest(ep url.URL) *http.Request {
@ -565,6 +571,9 @@ func (a *setAction) HTTPRequest(ep url.URL) *http.Request {
if a.Refresh { if a.Refresh {
form.Add("refresh", "true") form.Add("refresh", "true")
} }
if a.NoValueOnSuccess {
params.Set("noValueOnSuccess", strconv.FormatBool(a.NoValueOnSuccess))
}
u.RawQuery = params.Encode() u.RawQuery = params.Encode()
body := strings.NewReader(form.Encode()) body := strings.NewReader(form.Encode())

View File

@ -407,6 +407,15 @@ func TestSetAction(t *testing.T) {
wantURL: "http://example.com/foo?dir=true", wantURL: "http://example.com/foo?dir=true",
wantBody: "", wantBody: "",
}, },
// NoValueOnSuccess is set
{
act: setAction{
Key: "foo",
NoValueOnSuccess: true,
},
wantURL: "http://example.com/foo?noValueOnSuccess=true",
wantBody: "value=",
},
} }
for i, tt := range tests { for i, tt := range tests {

View File

@ -153,7 +153,7 @@ func (h *keysHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
defer cancel() defer cancel()
clock := clockwork.NewRealClock() clock := clockwork.NewRealClock()
startTime := clock.Now() startTime := clock.Now()
rr, err := parseKeyRequest(r, clock) rr, noValueOnSuccess, err := parseKeyRequest(r, clock)
if err != nil { if err != nil {
writeKeyError(w, err) writeKeyError(w, err)
return return
@ -175,7 +175,7 @@ func (h *keysHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
} }
switch { switch {
case resp.Event != nil: case resp.Event != nil:
if err := writeKeyEvent(w, resp.Event, h.timer); err != nil { if err := writeKeyEvent(w, resp.Event, noValueOnSuccess, h.timer); err != nil {
// Should never be reached // Should never be reached
plog.Errorf("error writing event (%v)", err) plog.Errorf("error writing event (%v)", err)
} }
@ -449,19 +449,20 @@ func logHandleFunc(w http.ResponseWriter, r *http.Request) {
// parseKeyRequest converts a received http.Request on keysPrefix to // parseKeyRequest converts a received http.Request on keysPrefix to
// a server Request, performing validation of supplied fields as appropriate. // a server Request, performing validation of supplied fields as appropriate.
// If any validation fails, an empty Request and non-nil error is returned. // If any validation fails, an empty Request and non-nil error is returned.
func parseKeyRequest(r *http.Request, clock clockwork.Clock) (etcdserverpb.Request, error) { func parseKeyRequest(r *http.Request, clock clockwork.Clock) (etcdserverpb.Request, bool, error) {
noValueOnSuccess := false
emptyReq := etcdserverpb.Request{} emptyReq := etcdserverpb.Request{}
err := r.ParseForm() err := r.ParseForm()
if err != nil { if err != nil {
return emptyReq, etcdErr.NewRequestError( return emptyReq, false, etcdErr.NewRequestError(
etcdErr.EcodeInvalidForm, etcdErr.EcodeInvalidForm,
err.Error(), err.Error(),
) )
} }
if !strings.HasPrefix(r.URL.Path, keysPrefix) { if !strings.HasPrefix(r.URL.Path, keysPrefix) {
return emptyReq, etcdErr.NewRequestError( return emptyReq, false, etcdErr.NewRequestError(
etcdErr.EcodeInvalidForm, etcdErr.EcodeInvalidForm,
"incorrect key prefix", "incorrect key prefix",
) )
@ -470,13 +471,13 @@ func parseKeyRequest(r *http.Request, clock clockwork.Clock) (etcdserverpb.Reque
var pIdx, wIdx uint64 var pIdx, wIdx uint64
if pIdx, err = getUint64(r.Form, "prevIndex"); err != nil { if pIdx, err = getUint64(r.Form, "prevIndex"); err != nil {
return emptyReq, etcdErr.NewRequestError( return emptyReq, false, etcdErr.NewRequestError(
etcdErr.EcodeIndexNaN, etcdErr.EcodeIndexNaN,
`invalid value for "prevIndex"`, `invalid value for "prevIndex"`,
) )
} }
if wIdx, err = getUint64(r.Form, "waitIndex"); err != nil { if wIdx, err = getUint64(r.Form, "waitIndex"); err != nil {
return emptyReq, etcdErr.NewRequestError( return emptyReq, false, etcdErr.NewRequestError(
etcdErr.EcodeIndexNaN, etcdErr.EcodeIndexNaN,
`invalid value for "waitIndex"`, `invalid value for "waitIndex"`,
) )
@ -484,45 +485,45 @@ func parseKeyRequest(r *http.Request, clock clockwork.Clock) (etcdserverpb.Reque
var rec, sort, wait, dir, quorum, stream bool var rec, sort, wait, dir, quorum, stream bool
if rec, err = getBool(r.Form, "recursive"); err != nil { if rec, err = getBool(r.Form, "recursive"); err != nil {
return emptyReq, etcdErr.NewRequestError( return emptyReq, false, etcdErr.NewRequestError(
etcdErr.EcodeInvalidField, etcdErr.EcodeInvalidField,
`invalid value for "recursive"`, `invalid value for "recursive"`,
) )
} }
if sort, err = getBool(r.Form, "sorted"); err != nil { if sort, err = getBool(r.Form, "sorted"); err != nil {
return emptyReq, etcdErr.NewRequestError( return emptyReq, false, etcdErr.NewRequestError(
etcdErr.EcodeInvalidField, etcdErr.EcodeInvalidField,
`invalid value for "sorted"`, `invalid value for "sorted"`,
) )
} }
if wait, err = getBool(r.Form, "wait"); err != nil { if wait, err = getBool(r.Form, "wait"); err != nil {
return emptyReq, etcdErr.NewRequestError( return emptyReq, false, etcdErr.NewRequestError(
etcdErr.EcodeInvalidField, etcdErr.EcodeInvalidField,
`invalid value for "wait"`, `invalid value for "wait"`,
) )
} }
// TODO(jonboulle): define what parameters dir is/isn't compatible with? // TODO(jonboulle): define what parameters dir is/isn't compatible with?
if dir, err = getBool(r.Form, "dir"); err != nil { if dir, err = getBool(r.Form, "dir"); err != nil {
return emptyReq, etcdErr.NewRequestError( return emptyReq, false, etcdErr.NewRequestError(
etcdErr.EcodeInvalidField, etcdErr.EcodeInvalidField,
`invalid value for "dir"`, `invalid value for "dir"`,
) )
} }
if quorum, err = getBool(r.Form, "quorum"); err != nil { if quorum, err = getBool(r.Form, "quorum"); err != nil {
return emptyReq, etcdErr.NewRequestError( return emptyReq, false, etcdErr.NewRequestError(
etcdErr.EcodeInvalidField, etcdErr.EcodeInvalidField,
`invalid value for "quorum"`, `invalid value for "quorum"`,
) )
} }
if stream, err = getBool(r.Form, "stream"); err != nil { if stream, err = getBool(r.Form, "stream"); err != nil {
return emptyReq, etcdErr.NewRequestError( return emptyReq, false, etcdErr.NewRequestError(
etcdErr.EcodeInvalidField, etcdErr.EcodeInvalidField,
`invalid value for "stream"`, `invalid value for "stream"`,
) )
} }
if wait && r.Method != "GET" { if wait && r.Method != "GET" {
return emptyReq, etcdErr.NewRequestError( return emptyReq, false, etcdErr.NewRequestError(
etcdErr.EcodeInvalidField, etcdErr.EcodeInvalidField,
`"wait" can only be used with GET requests`, `"wait" can only be used with GET requests`,
) )
@ -530,19 +531,26 @@ func parseKeyRequest(r *http.Request, clock clockwork.Clock) (etcdserverpb.Reque
pV := r.FormValue("prevValue") pV := r.FormValue("prevValue")
if _, ok := r.Form["prevValue"]; ok && pV == "" { if _, ok := r.Form["prevValue"]; ok && pV == "" {
return emptyReq, etcdErr.NewRequestError( return emptyReq, false, etcdErr.NewRequestError(
etcdErr.EcodePrevValueRequired, etcdErr.EcodePrevValueRequired,
`"prevValue" cannot be empty`, `"prevValue" cannot be empty`,
) )
} }
if noValueOnSuccess, err = getBool(r.Form, "noValueOnSuccess"); err != nil {
return emptyReq, false, etcdErr.NewRequestError(
etcdErr.EcodeInvalidField,
`invalid value for "noValueOnSuccess"`,
)
}
// TTL is nullable, so leave it null if not specified // TTL is nullable, so leave it null if not specified
// or an empty string // or an empty string
var ttl *uint64 var ttl *uint64
if len(r.FormValue("ttl")) > 0 { if len(r.FormValue("ttl")) > 0 {
i, err := getUint64(r.Form, "ttl") i, err := getUint64(r.Form, "ttl")
if err != nil { if err != nil {
return emptyReq, etcdErr.NewRequestError( return emptyReq, false, etcdErr.NewRequestError(
etcdErr.EcodeTTLNaN, etcdErr.EcodeTTLNaN,
`invalid value for "ttl"`, `invalid value for "ttl"`,
) )
@ -555,7 +563,7 @@ func parseKeyRequest(r *http.Request, clock clockwork.Clock) (etcdserverpb.Reque
if _, ok := r.Form["prevExist"]; ok { if _, ok := r.Form["prevExist"]; ok {
bv, err := getBool(r.Form, "prevExist") bv, err := getBool(r.Form, "prevExist")
if err != nil { if err != nil {
return emptyReq, etcdErr.NewRequestError( return emptyReq, false, etcdErr.NewRequestError(
etcdErr.EcodeInvalidField, etcdErr.EcodeInvalidField,
"invalid value for prevExist", "invalid value for prevExist",
) )
@ -568,7 +576,7 @@ func parseKeyRequest(r *http.Request, clock clockwork.Clock) (etcdserverpb.Reque
if _, ok := r.Form["refresh"]; ok { if _, ok := r.Form["refresh"]; ok {
bv, err := getBool(r.Form, "refresh") bv, err := getBool(r.Form, "refresh")
if err != nil { if err != nil {
return emptyReq, etcdErr.NewRequestError( return emptyReq, false, etcdErr.NewRequestError(
etcdErr.EcodeInvalidField, etcdErr.EcodeInvalidField,
"invalid value for refresh", "invalid value for refresh",
) )
@ -577,13 +585,13 @@ func parseKeyRequest(r *http.Request, clock clockwork.Clock) (etcdserverpb.Reque
if refresh != nil && *refresh { if refresh != nil && *refresh {
val := r.FormValue("value") val := r.FormValue("value")
if _, ok := r.Form["value"]; ok && val != "" { if _, ok := r.Form["value"]; ok && val != "" {
return emptyReq, etcdErr.NewRequestError( return emptyReq, false, etcdErr.NewRequestError(
etcdErr.EcodeRefreshValue, etcdErr.EcodeRefreshValue,
`A value was provided on a refresh`, `A value was provided on a refresh`,
) )
} }
if ttl == nil { if ttl == nil {
return emptyReq, etcdErr.NewRequestError( return emptyReq, false, etcdErr.NewRequestError(
etcdErr.EcodeRefreshTTLRequired, etcdErr.EcodeRefreshTTLRequired,
`No TTL value set`, `No TTL value set`,
) )
@ -621,13 +629,13 @@ func parseKeyRequest(r *http.Request, clock clockwork.Clock) (etcdserverpb.Reque
rr.Expiration = clock.Now().Add(expr).UnixNano() rr.Expiration = clock.Now().Add(expr).UnixNano()
} }
return rr, nil return rr, noValueOnSuccess, nil
} }
// writeKeyEvent trims the prefix of key path in a single Event under // writeKeyEvent trims the prefix of key path in a single Event under
// StoreKeysPrefix, serializes it and writes the resulting JSON to the given // StoreKeysPrefix, serializes it and writes the resulting JSON to the given
// ResponseWriter, along with the appropriate headers. // ResponseWriter, along with the appropriate headers.
func writeKeyEvent(w http.ResponseWriter, ev *store.Event, rt etcdserver.RaftTimer) error { func writeKeyEvent(w http.ResponseWriter, ev *store.Event, noValueOnSuccess bool, rt etcdserver.RaftTimer) error {
if ev == nil { if ev == nil {
return errors.New("cannot write empty Event!") return errors.New("cannot write empty Event!")
} }
@ -641,6 +649,12 @@ func writeKeyEvent(w http.ResponseWriter, ev *store.Event, rt etcdserver.RaftTim
} }
ev = trimEventPrefix(ev, etcdserver.StoreKeysPrefix) ev = trimEventPrefix(ev, etcdserver.StoreKeysPrefix)
if noValueOnSuccess &&
(ev.Action == store.Set || ev.Action == store.CompareAndSwap ||
ev.Action == store.Create || ev.Action == store.Update) {
ev.Node = nil
ev.PrevNode = nil
}
return json.NewEncoder(w).Encode(ev) return json.NewEncoder(w).Encode(ev)
} }

View File

@ -196,7 +196,7 @@ func TestBadRefreshRequest(t *testing.T) {
}, },
} }
for i, tt := range tests { for i, tt := range tests {
got, err := parseKeyRequest(tt.in, clockwork.NewFakeClock()) got, _, err := parseKeyRequest(tt.in, clockwork.NewFakeClock())
if err == nil { if err == nil {
t.Errorf("#%d: unexpected nil error!", i) t.Errorf("#%d: unexpected nil error!", i)
continue continue
@ -360,7 +360,7 @@ func TestBadParseRequest(t *testing.T) {
}, },
} }
for i, tt := range tests { for i, tt := range tests {
got, err := parseKeyRequest(tt.in, clockwork.NewFakeClock()) got, _, err := parseKeyRequest(tt.in, clockwork.NewFakeClock())
if err == nil { if err == nil {
t.Errorf("#%d: unexpected nil error!", i) t.Errorf("#%d: unexpected nil error!", i)
continue continue
@ -384,8 +384,9 @@ func TestGoodParseRequest(t *testing.T) {
fc := clockwork.NewFakeClock() fc := clockwork.NewFakeClock()
fc.Advance(1111) fc.Advance(1111)
tests := []struct { tests := []struct {
in *http.Request in *http.Request
w etcdserverpb.Request w etcdserverpb.Request
noValue bool
}{ }{
{ {
// good prefix, all other values default // good prefix, all other values default
@ -394,6 +395,7 @@ func TestGoodParseRequest(t *testing.T) {
Method: "GET", Method: "GET",
Path: path.Join(etcdserver.StoreKeysPrefix, "/foo"), Path: path.Join(etcdserver.StoreKeysPrefix, "/foo"),
}, },
false,
}, },
{ {
// value specified // value specified
@ -407,6 +409,7 @@ func TestGoodParseRequest(t *testing.T) {
Val: "some_value", Val: "some_value",
Path: path.Join(etcdserver.StoreKeysPrefix, "/foo"), Path: path.Join(etcdserver.StoreKeysPrefix, "/foo"),
}, },
false,
}, },
{ {
// prevIndex specified // prevIndex specified
@ -420,6 +423,7 @@ func TestGoodParseRequest(t *testing.T) {
PrevIndex: 98765, PrevIndex: 98765,
Path: path.Join(etcdserver.StoreKeysPrefix, "/foo"), Path: path.Join(etcdserver.StoreKeysPrefix, "/foo"),
}, },
false,
}, },
{ {
// recursive specified // recursive specified
@ -433,6 +437,7 @@ func TestGoodParseRequest(t *testing.T) {
Recursive: true, Recursive: true,
Path: path.Join(etcdserver.StoreKeysPrefix, "/foo"), Path: path.Join(etcdserver.StoreKeysPrefix, "/foo"),
}, },
false,
}, },
{ {
// sorted specified // sorted specified
@ -446,6 +451,7 @@ func TestGoodParseRequest(t *testing.T) {
Sorted: true, Sorted: true,
Path: path.Join(etcdserver.StoreKeysPrefix, "/foo"), Path: path.Join(etcdserver.StoreKeysPrefix, "/foo"),
}, },
false,
}, },
{ {
// quorum specified // quorum specified
@ -459,6 +465,7 @@ func TestGoodParseRequest(t *testing.T) {
Quorum: true, Quorum: true,
Path: path.Join(etcdserver.StoreKeysPrefix, "/foo"), Path: path.Join(etcdserver.StoreKeysPrefix, "/foo"),
}, },
false,
}, },
{ {
// wait specified // wait specified
@ -468,6 +475,7 @@ func TestGoodParseRequest(t *testing.T) {
Wait: true, Wait: true,
Path: path.Join(etcdserver.StoreKeysPrefix, "/foo"), Path: path.Join(etcdserver.StoreKeysPrefix, "/foo"),
}, },
false,
}, },
{ {
// empty TTL specified // empty TTL specified
@ -477,6 +485,7 @@ func TestGoodParseRequest(t *testing.T) {
Path: path.Join(etcdserver.StoreKeysPrefix, "/foo"), Path: path.Join(etcdserver.StoreKeysPrefix, "/foo"),
Expiration: 0, Expiration: 0,
}, },
false,
}, },
{ {
// non-empty TTL specified // non-empty TTL specified
@ -486,6 +495,7 @@ func TestGoodParseRequest(t *testing.T) {
Path: path.Join(etcdserver.StoreKeysPrefix, "/foo"), Path: path.Join(etcdserver.StoreKeysPrefix, "/foo"),
Expiration: fc.Now().Add(5678 * time.Second).UnixNano(), Expiration: fc.Now().Add(5678 * time.Second).UnixNano(),
}, },
false,
}, },
{ {
// zero TTL specified // zero TTL specified
@ -495,6 +505,7 @@ func TestGoodParseRequest(t *testing.T) {
Path: path.Join(etcdserver.StoreKeysPrefix, "/foo"), Path: path.Join(etcdserver.StoreKeysPrefix, "/foo"),
Expiration: fc.Now().UnixNano(), Expiration: fc.Now().UnixNano(),
}, },
false,
}, },
{ {
// dir specified // dir specified
@ -504,6 +515,7 @@ func TestGoodParseRequest(t *testing.T) {
Dir: true, Dir: true,
Path: path.Join(etcdserver.StoreKeysPrefix, "/foo"), Path: path.Join(etcdserver.StoreKeysPrefix, "/foo"),
}, },
false,
}, },
{ {
// dir specified negatively // dir specified negatively
@ -513,6 +525,7 @@ func TestGoodParseRequest(t *testing.T) {
Dir: false, Dir: false,
Path: path.Join(etcdserver.StoreKeysPrefix, "/foo"), Path: path.Join(etcdserver.StoreKeysPrefix, "/foo"),
}, },
false,
}, },
{ {
// prevExist should be non-null if specified // prevExist should be non-null if specified
@ -526,6 +539,7 @@ func TestGoodParseRequest(t *testing.T) {
PrevExist: boolp(true), PrevExist: boolp(true),
Path: path.Join(etcdserver.StoreKeysPrefix, "/foo"), Path: path.Join(etcdserver.StoreKeysPrefix, "/foo"),
}, },
false,
}, },
{ {
// prevExist should be non-null if specified // prevExist should be non-null if specified
@ -539,6 +553,7 @@ func TestGoodParseRequest(t *testing.T) {
PrevExist: boolp(false), PrevExist: boolp(false),
Path: path.Join(etcdserver.StoreKeysPrefix, "/foo"), Path: path.Join(etcdserver.StoreKeysPrefix, "/foo"),
}, },
false,
}, },
// mix various fields // mix various fields
{ {
@ -558,6 +573,7 @@ func TestGoodParseRequest(t *testing.T) {
Val: "some value", Val: "some value",
Path: path.Join(etcdserver.StoreKeysPrefix, "/foo"), Path: path.Join(etcdserver.StoreKeysPrefix, "/foo"),
}, },
false,
}, },
// query parameters should be used if given // query parameters should be used if given
{ {
@ -571,6 +587,7 @@ func TestGoodParseRequest(t *testing.T) {
PrevValue: "woof", PrevValue: "woof",
Path: path.Join(etcdserver.StoreKeysPrefix, "/foo"), Path: path.Join(etcdserver.StoreKeysPrefix, "/foo"),
}, },
false,
}, },
// but form values should take precedence over query parameters // but form values should take precedence over query parameters
{ {
@ -586,14 +603,33 @@ func TestGoodParseRequest(t *testing.T) {
PrevValue: "miaow", PrevValue: "miaow",
Path: path.Join(etcdserver.StoreKeysPrefix, "/foo"), Path: path.Join(etcdserver.StoreKeysPrefix, "/foo"),
}, },
false,
},
{
// noValueOnSuccess specified
mustNewForm(
t,
"foo",
url.Values{"noValueOnSuccess": []string{"true"}},
),
etcdserverpb.Request{
Method: "PUT",
Path: path.Join(etcdserver.StoreKeysPrefix, "/foo"),
},
true,
}, },
} }
for i, tt := range tests { for i, tt := range tests {
got, err := parseKeyRequest(tt.in, fc) got, noValueOnSuccess, err := parseKeyRequest(tt.in, fc)
if err != nil { if err != nil {
t.Errorf("#%d: err = %v, want %v", i, err, nil) t.Errorf("#%d: err = %v, want %v", i, err, nil)
} }
if noValueOnSuccess != tt.noValue {
t.Errorf("#%d: noValue=%t, want %t", i, noValueOnSuccess, tt.noValue)
}
if !reflect.DeepEqual(got, tt.w) { if !reflect.DeepEqual(got, tt.w) {
t.Errorf("#%d: request=%#v, want %#v", i, got, tt.w) t.Errorf("#%d: request=%#v, want %#v", i, got, tt.w)
} }
@ -1112,7 +1148,7 @@ func TestServeMembersFail(t *testing.T) {
func TestWriteEvent(t *testing.T) { func TestWriteEvent(t *testing.T) {
// nil event should not panic // nil event should not panic
rec := httptest.NewRecorder() rec := httptest.NewRecorder()
writeKeyEvent(rec, nil, dummyRaftTimer{}) writeKeyEvent(rec, nil, false, dummyRaftTimer{})
h := rec.Header() h := rec.Header()
if len(h) > 0 { if len(h) > 0 {
t.Fatalf("unexpected non-empty headers: %#v", h) t.Fatalf("unexpected non-empty headers: %#v", h)
@ -1123,8 +1159,9 @@ func TestWriteEvent(t *testing.T) {
} }
tests := []struct { tests := []struct {
ev *store.Event ev *store.Event
idx string noValue bool
idx string
// TODO(jonboulle): check body as well as just status code // TODO(jonboulle): check body as well as just status code
code int code int
err error err error
@ -1136,6 +1173,7 @@ func TestWriteEvent(t *testing.T) {
Node: &store.NodeExtern{}, Node: &store.NodeExtern{},
PrevNode: &store.NodeExtern{}, PrevNode: &store.NodeExtern{},
}, },
false,
"0", "0",
http.StatusOK, http.StatusOK,
nil, nil,
@ -1147,6 +1185,7 @@ func TestWriteEvent(t *testing.T) {
Node: &store.NodeExtern{}, Node: &store.NodeExtern{},
PrevNode: &store.NodeExtern{}, PrevNode: &store.NodeExtern{},
}, },
false,
"0", "0",
http.StatusCreated, http.StatusCreated,
nil, nil,
@ -1155,7 +1194,7 @@ func TestWriteEvent(t *testing.T) {
for i, tt := range tests { for i, tt := range tests {
rw := httptest.NewRecorder() rw := httptest.NewRecorder()
writeKeyEvent(rw, tt.ev, dummyRaftTimer{}) writeKeyEvent(rw, tt.ev, tt.noValue, dummyRaftTimer{})
if gct := rw.Header().Get("Content-Type"); gct != "application/json" { if gct := rw.Header().Get("Content-Type"); gct != "application/json" {
t.Errorf("case %d: bad Content-Type: got %q, want application/json", i, gct) t.Errorf("case %d: bad Content-Type: got %q, want application/json", i, gct)
} }
@ -1550,45 +1589,76 @@ func TestServeKeysGood(t *testing.T) {
} }
func TestServeKeysEvent(t *testing.T) { func TestServeKeysEvent(t *testing.T) {
req := mustNewRequest(t, "foo") tests := []struct {
server := &resServer{ req *http.Request
etcdserver.Response{ rsp etcdserver.Response
Event: &store.Event{ wcode int
event *store.Event
}{
{
mustNewRequest(t, "foo"),
etcdserver.Response{
Event: &store.Event{
Action: store.Get,
Node: &store.NodeExtern{},
},
},
http.StatusOK,
&store.Event{
Action: store.Get, Action: store.Get,
Node: &store.NodeExtern{}, Node: &store.NodeExtern{},
}, },
}, },
{
mustNewForm(
t,
"foo",
url.Values{"noValueOnSuccess": []string{"true"}},
),
etcdserver.Response{
Event: &store.Event{
Action: store.CompareAndSwap,
Node: &store.NodeExtern{},
},
},
http.StatusOK,
&store.Event{
Action: store.CompareAndSwap,
Node: nil,
},
},
} }
server := &resServer{}
h := &keysHandler{ h := &keysHandler{
timeout: time.Hour, timeout: time.Hour,
server: server, server: server,
cluster: &fakeCluster{id: 1}, cluster: &fakeCluster{id: 1},
timer: &dummyRaftTimer{}, timer: &dummyRaftTimer{},
} }
rw := httptest.NewRecorder()
h.ServeHTTP(rw, req) for _, tt := range tests {
server.res = tt.rsp
rw := httptest.NewRecorder()
h.ServeHTTP(rw, tt.req)
wcode := http.StatusOK wbody := mustMarshalEvent(
wbody := mustMarshalEvent( t,
t, tt.event,
&store.Event{ )
Action: store.Get,
Node: &store.NodeExtern{},
},
)
if rw.Code != wcode { if rw.Code != tt.wcode {
t.Errorf("got code=%d, want %d", rw.Code, wcode) t.Errorf("got code=%d, want %d", rw.Code, tt.wcode)
} }
gcid := rw.Header().Get("X-Etcd-Cluster-ID") gcid := rw.Header().Get("X-Etcd-Cluster-ID")
wcid := h.cluster.ID().String() wcid := h.cluster.ID().String()
if gcid != wcid { if gcid != wcid {
t.Errorf("cid = %s, want %s", gcid, wcid) t.Errorf("cid = %s, want %s", gcid, wcid)
} }
g := rw.Body.String() g := rw.Body.String()
if g != wbody { if g != wbody {
t.Errorf("got body=%#v, want %#v", g, wbody) t.Errorf("got body=%#v, want %#v", g, wbody)
}
} }
} }

View File

@ -45,6 +45,9 @@ func TestV2Set(t *testing.T) {
tc := NewTestClient() tc := NewTestClient()
v := url.Values{} v := url.Values{}
v.Set("value", "bar") v.Set("value", "bar")
vAndNoValue := url.Values{}
vAndNoValue.Set("value", "bar")
vAndNoValue.Set("noValueOnSuccess", "true")
tests := []struct { tests := []struct {
relativeURL string relativeURL string
@ -70,6 +73,12 @@ func TestV2Set(t *testing.T) {
http.StatusCreated, http.StatusCreated,
`{"action":"set","node":{"key":"/fooempty","value":"","modifiedIndex":6,"createdIndex":6}}`, `{"action":"set","node":{"key":"/fooempty","value":"","modifiedIndex":6,"createdIndex":6}}`,
}, },
{
"/v2/keys/foo/novalue",
vAndNoValue,
http.StatusCreated,
`{"action":"set"}`,
},
} }
for i, tt := range tests { for i, tt := range tests {
@ -183,6 +192,31 @@ func TestV2CreateUpdate(t *testing.T) {
"cause": "/nonexist", "cause": "/nonexist",
}, },
}, },
// create with no value on success
{
"/v2/keys/create/novalue",
url.Values(map[string][]string{"value": {"XXX"}, "prevExist": {"false"}, "noValueOnSuccess": {"true"}}),
http.StatusCreated,
map[string]interface{}{},
},
// update with no value on success
{
"/v2/keys/create/novalue",
url.Values(map[string][]string{"value": {"XXX"}, "prevExist": {"true"}, "noValueOnSuccess": {"true"}}),
http.StatusOK,
map[string]interface{}{},
},
// created key failed with no value on success
{
"/v2/keys/create/foo",
url.Values(map[string][]string{"value": {"XXX"}, "prevExist": {"false"}, "noValueOnSuccess": {"true"}}),
http.StatusPreconditionFailed,
map[string]interface{}{
"errorCode": float64(105),
"message": "Key already exists",
"cause": "/create/foo",
},
},
} }
for i, tt := range tests { for i, tt := range tests {
@ -312,6 +346,25 @@ func TestV2CAS(t *testing.T) {
"cause": "[bad_value != ZZZ]", "cause": "[bad_value != ZZZ]",
}, },
}, },
{
"/v2/keys/cas/foo",
url.Values(map[string][]string{"value": {"YYY"}, "prevIndex": {"6"}, "noValueOnSuccess": {"true"}}),
http.StatusOK,
map[string]interface{}{
"action": "compareAndSwap",
},
},
{
"/v2/keys/cas/foo",
url.Values(map[string][]string{"value": {"YYY"}, "prevIndex": {"10"}, "noValueOnSuccess": {"true"}}),
http.StatusPreconditionFailed,
map[string]interface{}{
"errorCode": float64(101),
"message": "Compare failed",
"cause": "[10 != 7]",
"index": float64(7),
},
},
} }
for i, tt := range tests { for i, tt := range tests {