diff --git a/.changelog/14474.txt b/.changelog/14474.txt new file mode 100644 index 000000000..fcc326547 --- /dev/null +++ b/.changelog/14474.txt @@ -0,0 +1,3 @@ +```release-note:feature +http: Add new `get-or-empty` operation to the txn api. Refer to the [API docs](https://www.consul.io/api-docs/txn#kv-operations) for more information. +``` \ No newline at end of file diff --git a/agent/consul/kvs_endpoint.go b/agent/consul/kvs_endpoint.go index 434ebcada..3f2cbe1cc 100644 --- a/agent/consul/kvs_endpoint.go +++ b/agent/consul/kvs_endpoint.go @@ -49,7 +49,7 @@ func kvsPreApply(logger hclog.Logger, srv *Server, authz resolver.Result, op api return false, err } - case api.KVGet, api.KVGetTree: + case api.KVGet, api.KVGetTree, api.KVGetOrEmpty: // Filtering for GETs is done on the output side. case api.KVCheckSession, api.KVCheckIndex: diff --git a/agent/consul/state/txn.go b/agent/consul/state/txn.go index 087bb4fe8..af6e98995 100644 --- a/agent/consul/state/txn.go +++ b/agent/consul/state/txn.go @@ -60,6 +60,13 @@ func (s *Store) txnKVS(tx WriteTxn, idx uint64, op *structs.TxnKVOp) (structs.Tx err = fmt.Errorf("key %q doesn't exist", op.DirEnt.Key) } + case api.KVGetOrEmpty: + _, entry, err = kvsGetTxn(tx, nil, op.DirEnt.Key, op.DirEnt.EnterpriseMeta) + if entry == nil && err == nil { + entry = &op.DirEnt + entry.Value = nil + } + case api.KVGetTree: var entries structs.DirEntries _, entries, err = s.kvsListTxn(tx, nil, op.DirEnt.Key, op.DirEnt.EnterpriseMeta) @@ -95,7 +102,7 @@ func (s *Store) txnKVS(tx WriteTxn, idx uint64, op *structs.TxnKVOp) (structs.Tx // value (we have to clone so we don't modify the entry being used by // the state store). if entry != nil { - if op.Verb == api.KVGet { + if op.Verb == api.KVGet || op.Verb == api.KVGetOrEmpty { result := structs.TxnResult{KV: entry} return structs.TxnResults{&result}, nil } diff --git a/agent/consul/state/txn_test.go b/agent/consul/state/txn_test.go index f98325df3..a7694089b 100644 --- a/agent/consul/state/txn_test.go +++ b/agent/consul/state/txn_test.go @@ -577,6 +577,22 @@ func TestStateStore_Txn_KVS(t *testing.T) { }, }, }, + &structs.TxnOp{ + KV: &structs.TxnKVOp{ + Verb: api.KVGetOrEmpty, + DirEnt: structs.DirEntry{ + Key: "foo/update", + }, + }, + }, + &structs.TxnOp{ + KV: &structs.TxnKVOp{ + Verb: api.KVGetOrEmpty, + DirEnt: structs.DirEntry{ + Key: "foo/not-exists", + }, + }, + }, &structs.TxnOp{ KV: &structs.TxnKVOp{ Verb: api.KVCheckIndex, @@ -702,6 +718,22 @@ func TestStateStore_Txn_KVS(t *testing.T) { }, }, }, + &structs.TxnResult{ + KV: &structs.DirEntry{ + Key: "foo/update", + Value: []byte("stale"), + RaftIndex: structs.RaftIndex{ + CreateIndex: 5, + ModifyIndex: 5, + }, + }, + }, + &structs.TxnResult{ + KV: &structs.DirEntry{ + Key: "foo/not-exists", + Value: nil, + }, + }, &structs.TxnResult{ KV: &structs.DirEntry{ diff --git a/api/txn.go b/api/txn.go index 59fd1c0d9..4aa06d9f5 100644 --- a/api/txn.go +++ b/api/txn.go @@ -67,6 +67,7 @@ const ( KVLock KVOp = "lock" KVUnlock KVOp = "unlock" KVGet KVOp = "get" + KVGetOrEmpty KVOp = "get-or-empty" KVGetTree KVOp = "get-tree" KVCheckSession KVOp = "check-session" KVCheckIndex KVOp = "check-index" diff --git a/website/content/api-docs/txn.mdx b/website/content/api-docs/txn.mdx index 97eefeece..a42176cbb 100644 --- a/website/content/api-docs/txn.mdx +++ b/website/content/api-docs/txn.mdx @@ -266,20 +266,21 @@ look like this: The following tables summarize the available verbs and the fields that apply to those operations ("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` | -| `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` | | +| 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-or-empty` | Get the key, or null 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` | | #### Node Operations