mirror of https://github.com/status-im/consul.git
Issue #2905: Add check-not-exists to TXN endpoint
This patch adds support for asserting that a given key does not exist in the KV store. Fixes #2905
This commit is contained in:
parent
eddb1af603
commit
6dd2804d09
23
api/kv.go
23
api/kv.go
|
@ -49,17 +49,18 @@ type KVPairs []*KVPair
|
|||
type KVOp string
|
||||
|
||||
const (
|
||||
KVSet KVOp = "set"
|
||||
KVDelete KVOp = "delete"
|
||||
KVDeleteCAS KVOp = "delete-cas"
|
||||
KVDeleteTree KVOp = "delete-tree"
|
||||
KVCAS KVOp = "cas"
|
||||
KVLock KVOp = "lock"
|
||||
KVUnlock KVOp = "unlock"
|
||||
KVGet KVOp = "get"
|
||||
KVGetTree KVOp = "get-tree"
|
||||
KVCheckSession KVOp = "check-session"
|
||||
KVCheckIndex KVOp = "check-index"
|
||||
KVSet KVOp = "set"
|
||||
KVDelete KVOp = "delete"
|
||||
KVDeleteCAS KVOp = "delete-cas"
|
||||
KVDeleteTree KVOp = "delete-tree"
|
||||
KVCAS KVOp = "cas"
|
||||
KVLock KVOp = "lock"
|
||||
KVUnlock KVOp = "unlock"
|
||||
KVGet KVOp = "get"
|
||||
KVGetTree KVOp = "get-tree"
|
||||
KVCheckSession KVOp = "check-session"
|
||||
KVCheckIndex KVOp = "check-index"
|
||||
KVCheckNotExists KVOp = "check-not-exists"
|
||||
)
|
||||
|
||||
// KVTxnOp defines a single operation inside a transaction.
|
||||
|
|
|
@ -79,6 +79,12 @@ func (s *StateStore) txnKVS(tx *memdb.Txn, idx uint64, op *structs.TxnKVOp) (str
|
|||
case api.KVCheckIndex:
|
||||
entry, err = s.kvsCheckIndexTxn(tx, op.DirEnt.Key, op.DirEnt.ModifyIndex)
|
||||
|
||||
case api.KVCheckNotExists:
|
||||
_, entry, err = s.kvsGetTxn(tx, nil, op.DirEnt.Key)
|
||||
if entry != nil && err == nil {
|
||||
err = fmt.Errorf("key %q exists", op.DirEnt.Key)
|
||||
}
|
||||
|
||||
default:
|
||||
err = fmt.Errorf("unknown KV verb %q", op.Verb)
|
||||
}
|
||||
|
|
|
@ -14,6 +14,57 @@ import (
|
|||
"github.com/hashicorp/net-rpc-msgpackrpc"
|
||||
)
|
||||
|
||||
func TestTxn_CheckNotExists(t *testing.T) {
|
||||
dir1, s1 := testServer(t)
|
||||
defer os.RemoveAll(dir1)
|
||||
defer s1.Shutdown()
|
||||
codec := rpcClient(t, s1)
|
||||
defer codec.Close()
|
||||
|
||||
testrpc.WaitForLeader(t, s1.RPC, "dc1")
|
||||
|
||||
apply := func(arg *structs.TxnRequest) (*structs.TxnResponse, error) {
|
||||
out := new(structs.TxnResponse)
|
||||
err := msgpackrpc.CallWithCodec(codec, "Txn.Apply", arg, out)
|
||||
return out, err
|
||||
}
|
||||
|
||||
checkKeyNotExists := &structs.TxnRequest{
|
||||
Datacenter: "dc1",
|
||||
Ops: structs.TxnOps{
|
||||
{
|
||||
KV: &structs.TxnKVOp{
|
||||
Verb: api.KVCheckNotExists,
|
||||
DirEnt: structs.DirEntry{Key: "test"},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
createKey := &structs.TxnRequest{
|
||||
Datacenter: "dc1",
|
||||
Ops: structs.TxnOps{
|
||||
{
|
||||
KV: &structs.TxnKVOp{
|
||||
Verb: api.KVSet,
|
||||
DirEnt: structs.DirEntry{Key: "test"},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
if _, err := apply(checkKeyNotExists); err != nil {
|
||||
t.Fatalf("testing for non-existent key failed: %s", err)
|
||||
}
|
||||
if _, err := apply(createKey); err != nil {
|
||||
t.Fatalf("creating new key failed: %s", err)
|
||||
}
|
||||
out, err := apply(checkKeyNotExists)
|
||||
if err != nil || out == nil || len(out.Errors) != 1 || out.Errors[0].Error() != `op 0: key "test" exists` {
|
||||
t.Fatalf("testing for existent key failed: %#v", out)
|
||||
}
|
||||
}
|
||||
|
||||
func TestTxn_Apply(t *testing.T) {
|
||||
dir1, s1 := testServer(t)
|
||||
defer os.RemoveAll(dir1)
|
||||
|
@ -234,6 +285,14 @@ func TestTxn_Apply_ACLDeny(t *testing.T) {
|
|||
},
|
||||
},
|
||||
},
|
||||
&structs.TxnOp{
|
||||
KV: &structs.TxnKVOp{
|
||||
Verb: api.KVCheckNotExists,
|
||||
DirEnt: structs.DirEntry{
|
||||
Key: "nope",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
WriteRequest: structs.WriteRequest{
|
||||
Token: id,
|
||||
|
|
|
@ -149,16 +149,17 @@ look like this:
|
|||
The following table summarizes the available verbs and the fields that apply to
|
||||
that operation ("X" means a field is required and "O" means it is optional):
|
||||
|
||||
| Verb | Operation | Key | Value | Flags | Index | Session |
|
||||
| --------------- | -------------------------------------------- | :--: | :---: | :---: | :---: | :-----: |
|
||||
| `set` | Sets the `Key` to the given `Value` | `x` | `x` | `o` | | |
|
||||
| `cas` | Sets, but with CAS semantics | `x` | `x` | `o` | `x` | |
|
||||
| `lock` | Lock with the given `Session` | `x` | `x` | `o` | | `x` |
|
||||
| `unlock` | Unlock with the given `Session` | `x` | `x` | `o` | | `x` |
|
||||
| `get` | Get the key, fails if it does not exist | `x` | | | | |
|
||||
| `get-tree` | Gets all keys with the prefix | `x` | | | | |
|
||||
| `check-index` | Fail if modify index != index | `x` | | | `x` | |
|
||||
| `check-session` | Fail if not locked by session | `x` | | | | `x` |
|
||||
| `delete` | Delete the key | `x` | | | | |
|
||||
| `delete-tree` | Delete all keys with a prefix | `x` | | | | |
|
||||
| `delete-cas` | Delete, but with CAS semantics | `x` | | | `x` | |
|
||||
| Verb | Operation | Key | Value | Flags | Index | Session |
|
||||
| ------------------ | -------------------------------------------- | :--: | :---: | :---: | :---: | :-----: |
|
||||
| `set` | Sets the `Key` to the given `Value` | `x` | `x` | `o` | | |
|
||||
| `cas` | Sets, but with CAS semantics | `x` | `x` | `o` | `x` | |
|
||||
| `lock` | Lock with the given `Session` | `x` | `x` | `o` | | `x` |
|
||||
| `unlock` | Unlock with the given `Session` | `x` | `x` | `o` | | `x` |
|
||||
| `get` | Get the key, fails if it does not exist | `x` | | | | |
|
||||
| `get-tree` | Gets all keys with the prefix | `x` | | | | |
|
||||
| `check-index` | Fail if modify index != index | `x` | | | `x` | |
|
||||
| `check-session` | Fail if not locked by session | `x` | | | | `x` |
|
||||
| `check-not-exists` | Fail if key exists | `x` | | | | |
|
||||
| `delete` | Delete the key | `x` | | | | |
|
||||
| `delete-tree` | Delete all keys with a prefix | `x` | | | | |
|
||||
| `delete-cas` | Delete, but with CAS semantics | `x` | | | `x` | |
|
||||
|
|
Loading…
Reference in New Issue