50424694fc
Based on discussions with @slackpad, some of the values in the KVPair are ReadOnly. This commit updates the docs to reflect that.
417 lines
12 KiB
Go
417 lines
12 KiB
Go
package api
|
|
|
|
import (
|
|
"bytes"
|
|
"fmt"
|
|
"io"
|
|
"net/http"
|
|
"strconv"
|
|
"strings"
|
|
)
|
|
|
|
// KVPair is used to represent a single K/V entry
|
|
type KVPair struct {
|
|
// Key is the name of the key. It is also part of the URL path when accessed
|
|
// via the API.
|
|
Key string
|
|
|
|
// CreateIndex holds the index corresponding the creation of this KVPair. This
|
|
// is a read-only field.
|
|
CreateIndex uint64
|
|
|
|
// ModifyIndex is used for the Check-And-Set operations.
|
|
ModifyIndex uint64
|
|
|
|
// LockIndex holds the index corresponding to a lock on this key, if any. This
|
|
// is a read-only field.
|
|
LockIndex uint64
|
|
|
|
// Flags are any user-defined flags on the key. It is up to the implementer
|
|
// to check these values, since Consul does not treat them specially.
|
|
Flags uint64
|
|
|
|
// Value is the value for the key. This can be any value, but it will be
|
|
// base64 encoded upon transport.
|
|
Value []byte
|
|
|
|
// Session is a string representing the name of the session. Any other
|
|
// interactions with this key over the same session must specify the same
|
|
// session name.
|
|
Session string
|
|
}
|
|
|
|
// KVPairs is a list of KVPair objects
|
|
type KVPairs []*KVPair
|
|
|
|
// KVOp constants give possible operations available in a KVTxn.
|
|
type KVOp string
|
|
|
|
const (
|
|
KVSet KVOp = "set"
|
|
KVDelete = "delete"
|
|
KVDeleteCAS = "delete-cas"
|
|
KVDeleteTree = "delete-tree"
|
|
KVCAS = "cas"
|
|
KVLock = "lock"
|
|
KVUnlock = "unlock"
|
|
KVGet = "get"
|
|
KVGetTree = "get-tree"
|
|
KVCheckSession = "check-session"
|
|
KVCheckIndex = "check-index"
|
|
)
|
|
|
|
// KVTxnOp defines a single operation inside a transaction.
|
|
type KVTxnOp struct {
|
|
Verb string
|
|
Key string
|
|
Value []byte
|
|
Flags uint64
|
|
Index uint64
|
|
Session string
|
|
}
|
|
|
|
// KVTxnOps defines a set of operations to be performed inside a single
|
|
// transaction.
|
|
type KVTxnOps []*KVTxnOp
|
|
|
|
// KVTxnResponse has the outcome of a transaction.
|
|
type KVTxnResponse struct {
|
|
Results []*KVPair
|
|
Errors TxnErrors
|
|
}
|
|
|
|
// KV is used to manipulate the K/V API
|
|
type KV struct {
|
|
c *Client
|
|
}
|
|
|
|
// KV is used to return a handle to the K/V apis
|
|
func (c *Client) KV() *KV {
|
|
return &KV{c}
|
|
}
|
|
|
|
// Get is used to lookup a single key
|
|
func (k *KV) Get(key string, q *QueryOptions) (*KVPair, *QueryMeta, error) {
|
|
resp, qm, err := k.getInternal(key, nil, q)
|
|
if err != nil {
|
|
return nil, nil, err
|
|
}
|
|
if resp == nil {
|
|
return nil, qm, nil
|
|
}
|
|
defer resp.Body.Close()
|
|
|
|
var entries []*KVPair
|
|
if err := decodeBody(resp, &entries); err != nil {
|
|
return nil, nil, err
|
|
}
|
|
if len(entries) > 0 {
|
|
return entries[0], qm, nil
|
|
}
|
|
return nil, qm, nil
|
|
}
|
|
|
|
// List is used to lookup all keys under a prefix
|
|
func (k *KV) List(prefix string, q *QueryOptions) (KVPairs, *QueryMeta, error) {
|
|
resp, qm, err := k.getInternal(prefix, map[string]string{"recurse": ""}, q)
|
|
if err != nil {
|
|
return nil, nil, err
|
|
}
|
|
if resp == nil {
|
|
return nil, qm, nil
|
|
}
|
|
defer resp.Body.Close()
|
|
|
|
var entries []*KVPair
|
|
if err := decodeBody(resp, &entries); err != nil {
|
|
return nil, nil, err
|
|
}
|
|
return entries, qm, nil
|
|
}
|
|
|
|
// Keys is used to list all the keys under a prefix. Optionally,
|
|
// a separator can be used to limit the responses.
|
|
func (k *KV) Keys(prefix, separator string, q *QueryOptions) ([]string, *QueryMeta, error) {
|
|
params := map[string]string{"keys": ""}
|
|
if separator != "" {
|
|
params["separator"] = separator
|
|
}
|
|
resp, qm, err := k.getInternal(prefix, params, q)
|
|
if err != nil {
|
|
return nil, nil, err
|
|
}
|
|
if resp == nil {
|
|
return nil, qm, nil
|
|
}
|
|
defer resp.Body.Close()
|
|
|
|
var entries []string
|
|
if err := decodeBody(resp, &entries); err != nil {
|
|
return nil, nil, err
|
|
}
|
|
return entries, qm, nil
|
|
}
|
|
|
|
func (k *KV) getInternal(key string, params map[string]string, q *QueryOptions) (*http.Response, *QueryMeta, error) {
|
|
r := k.c.newRequest("GET", "/v1/kv/"+key)
|
|
r.setQueryOptions(q)
|
|
for param, val := range params {
|
|
r.params.Set(param, val)
|
|
}
|
|
rtt, resp, err := k.c.doRequest(r)
|
|
if err != nil {
|
|
return nil, nil, err
|
|
}
|
|
|
|
qm := &QueryMeta{}
|
|
parseQueryMeta(resp, qm)
|
|
qm.RequestTime = rtt
|
|
|
|
if resp.StatusCode == 404 {
|
|
resp.Body.Close()
|
|
return nil, qm, nil
|
|
} else if resp.StatusCode != 200 {
|
|
resp.Body.Close()
|
|
return nil, nil, fmt.Errorf("Unexpected response code: %d", resp.StatusCode)
|
|
}
|
|
return resp, qm, nil
|
|
}
|
|
|
|
// Put is used to write a new value. Only the
|
|
// Key, Flags and Value is respected.
|
|
func (k *KV) Put(p *KVPair, q *WriteOptions) (*WriteMeta, error) {
|
|
params := make(map[string]string, 1)
|
|
if p.Flags != 0 {
|
|
params["flags"] = strconv.FormatUint(p.Flags, 10)
|
|
}
|
|
_, wm, err := k.put(p.Key, params, p.Value, q)
|
|
return wm, err
|
|
}
|
|
|
|
// CAS is used for a Check-And-Set operation. The Key,
|
|
// ModifyIndex, Flags and Value are respected. Returns true
|
|
// on success or false on failures.
|
|
func (k *KV) CAS(p *KVPair, q *WriteOptions) (bool, *WriteMeta, error) {
|
|
params := make(map[string]string, 2)
|
|
if p.Flags != 0 {
|
|
params["flags"] = strconv.FormatUint(p.Flags, 10)
|
|
}
|
|
params["cas"] = strconv.FormatUint(p.ModifyIndex, 10)
|
|
return k.put(p.Key, params, p.Value, q)
|
|
}
|
|
|
|
// Acquire is used for a lock acquisition operation. The Key,
|
|
// Flags, Value and Session are respected. Returns true
|
|
// on success or false on failures.
|
|
func (k *KV) Acquire(p *KVPair, q *WriteOptions) (bool, *WriteMeta, error) {
|
|
params := make(map[string]string, 2)
|
|
if p.Flags != 0 {
|
|
params["flags"] = strconv.FormatUint(p.Flags, 10)
|
|
}
|
|
params["acquire"] = p.Session
|
|
return k.put(p.Key, params, p.Value, q)
|
|
}
|
|
|
|
// Release is used for a lock release operation. The Key,
|
|
// Flags, Value and Session are respected. Returns true
|
|
// on success or false on failures.
|
|
func (k *KV) Release(p *KVPair, q *WriteOptions) (bool, *WriteMeta, error) {
|
|
params := make(map[string]string, 2)
|
|
if p.Flags != 0 {
|
|
params["flags"] = strconv.FormatUint(p.Flags, 10)
|
|
}
|
|
params["release"] = p.Session
|
|
return k.put(p.Key, params, p.Value, q)
|
|
}
|
|
|
|
func (k *KV) put(key string, params map[string]string, body []byte, q *WriteOptions) (bool, *WriteMeta, error) {
|
|
if len(key) > 0 && key[0] == '/' {
|
|
return false, nil, fmt.Errorf("Invalid key. Key must not begin with a '/': %s", key)
|
|
}
|
|
|
|
r := k.c.newRequest("PUT", "/v1/kv/"+key)
|
|
r.setWriteOptions(q)
|
|
for param, val := range params {
|
|
r.params.Set(param, val)
|
|
}
|
|
r.body = bytes.NewReader(body)
|
|
rtt, resp, err := requireOK(k.c.doRequest(r))
|
|
if err != nil {
|
|
return false, nil, err
|
|
}
|
|
defer resp.Body.Close()
|
|
|
|
qm := &WriteMeta{}
|
|
qm.RequestTime = rtt
|
|
|
|
var buf bytes.Buffer
|
|
if _, err := io.Copy(&buf, resp.Body); err != nil {
|
|
return false, nil, fmt.Errorf("Failed to read response: %v", err)
|
|
}
|
|
res := strings.Contains(string(buf.Bytes()), "true")
|
|
return res, qm, nil
|
|
}
|
|
|
|
// Delete is used to delete a single key
|
|
func (k *KV) Delete(key string, w *WriteOptions) (*WriteMeta, error) {
|
|
_, qm, err := k.deleteInternal(key, nil, w)
|
|
return qm, err
|
|
}
|
|
|
|
// DeleteCAS is used for a Delete Check-And-Set operation. The Key
|
|
// and ModifyIndex are respected. Returns true on success or false on failures.
|
|
func (k *KV) DeleteCAS(p *KVPair, q *WriteOptions) (bool, *WriteMeta, error) {
|
|
params := map[string]string{
|
|
"cas": strconv.FormatUint(p.ModifyIndex, 10),
|
|
}
|
|
return k.deleteInternal(p.Key, params, q)
|
|
}
|
|
|
|
// DeleteTree is used to delete all keys under a prefix
|
|
func (k *KV) DeleteTree(prefix string, w *WriteOptions) (*WriteMeta, error) {
|
|
_, qm, err := k.deleteInternal(prefix, map[string]string{"recurse": ""}, w)
|
|
return qm, err
|
|
}
|
|
|
|
func (k *KV) deleteInternal(key string, params map[string]string, q *WriteOptions) (bool, *WriteMeta, error) {
|
|
r := k.c.newRequest("DELETE", "/v1/kv/"+key)
|
|
r.setWriteOptions(q)
|
|
for param, val := range params {
|
|
r.params.Set(param, val)
|
|
}
|
|
rtt, resp, err := requireOK(k.c.doRequest(r))
|
|
if err != nil {
|
|
return false, nil, err
|
|
}
|
|
defer resp.Body.Close()
|
|
|
|
qm := &WriteMeta{}
|
|
qm.RequestTime = rtt
|
|
|
|
var buf bytes.Buffer
|
|
if _, err := io.Copy(&buf, resp.Body); err != nil {
|
|
return false, nil, fmt.Errorf("Failed to read response: %v", err)
|
|
}
|
|
res := strings.Contains(string(buf.Bytes()), "true")
|
|
return res, qm, nil
|
|
}
|
|
|
|
// TxnOp is the internal format we send to Consul. It's not specific to KV,
|
|
// though currently only KV operations are supported.
|
|
type TxnOp struct {
|
|
KV *KVTxnOp
|
|
}
|
|
|
|
// TxnOps is a list of transaction operations.
|
|
type TxnOps []*TxnOp
|
|
|
|
// TxnResult is the internal format we receive from Consul.
|
|
type TxnResult struct {
|
|
KV *KVPair
|
|
}
|
|
|
|
// TxnResults is a list of TxnResult objects.
|
|
type TxnResults []*TxnResult
|
|
|
|
// TxnError is used to return information about an operation in a transaction.
|
|
type TxnError struct {
|
|
OpIndex int
|
|
What string
|
|
}
|
|
|
|
// TxnErrors is a list of TxnError objects.
|
|
type TxnErrors []*TxnError
|
|
|
|
// TxnResponse is the internal format we receive from Consul.
|
|
type TxnResponse struct {
|
|
Results TxnResults
|
|
Errors TxnErrors
|
|
}
|
|
|
|
// Txn is used to apply multiple KV operations in a single, atomic transaction.
|
|
//
|
|
// Note that Go will perform the required base64 encoding on the values
|
|
// automatically because the type is a byte slice. Transactions are defined as a
|
|
// list of operations to perform, using the KVOp constants and KVTxnOp structure
|
|
// to define operations. If any operation fails, none of the changes are applied
|
|
// to the state store. Note that this hides the internal raw transaction interface
|
|
// and munges the input and output types into KV-specific ones for ease of use.
|
|
// If there are more non-KV operations in the future we may break out a new
|
|
// transaction API client, but it will be easy to keep this KV-specific variant
|
|
// supported.
|
|
//
|
|
// Even though this is generally a write operation, we take a QueryOptions input
|
|
// and return a QueryMeta output. If the transaction contains only read ops, then
|
|
// Consul will fast-path it to a different endpoint internally which supports
|
|
// consistency controls, but not blocking. If there are write operations then
|
|
// the request will always be routed through raft and any consistency settings
|
|
// will be ignored.
|
|
//
|
|
// Here's an example:
|
|
//
|
|
// ops := KVTxnOps{
|
|
// &KVTxnOp{
|
|
// Verb: KVLock,
|
|
// Key: "test/lock",
|
|
// Session: "adf4238a-882b-9ddc-4a9d-5b6758e4159e",
|
|
// Value: []byte("hello"),
|
|
// },
|
|
// &KVTxnOp{
|
|
// Verb: KVGet,
|
|
// Key: "another/key",
|
|
// },
|
|
// }
|
|
// ok, response, _, err := kv.Txn(&ops, nil)
|
|
//
|
|
// If there is a problem making the transaction request then an error will be
|
|
// returned. Otherwise, the ok value will be true if the transaction succeeded
|
|
// or false if it was rolled back. The response is a structured return value which
|
|
// will have the outcome of the transaction. Its Results member will have entries
|
|
// for each operation. Deleted keys will have a nil entry in the, and to save
|
|
// space, the Value of each key in the Results will be nil unless the operation
|
|
// is a KVGet. If the transaction was rolled back, the Errors member will have
|
|
// entries referencing the index of the operation that failed along with an error
|
|
// message.
|
|
func (k *KV) Txn(txn KVTxnOps, q *QueryOptions) (bool, *KVTxnResponse, *QueryMeta, error) {
|
|
r := k.c.newRequest("PUT", "/v1/txn")
|
|
r.setQueryOptions(q)
|
|
|
|
// Convert into the internal format since this is an all-KV txn.
|
|
ops := make(TxnOps, 0, len(txn))
|
|
for _, kvOp := range txn {
|
|
ops = append(ops, &TxnOp{KV: kvOp})
|
|
}
|
|
r.obj = ops
|
|
rtt, resp, err := k.c.doRequest(r)
|
|
if err != nil {
|
|
return false, nil, nil, err
|
|
}
|
|
defer resp.Body.Close()
|
|
|
|
qm := &QueryMeta{}
|
|
parseQueryMeta(resp, qm)
|
|
qm.RequestTime = rtt
|
|
|
|
if resp.StatusCode == http.StatusOK || resp.StatusCode == http.StatusConflict {
|
|
var txnResp TxnResponse
|
|
if err := decodeBody(resp, &txnResp); err != nil {
|
|
return false, nil, nil, err
|
|
}
|
|
|
|
// Convert from the internal format.
|
|
kvResp := KVTxnResponse{
|
|
Errors: txnResp.Errors,
|
|
}
|
|
for _, result := range txnResp.Results {
|
|
kvResp.Results = append(kvResp.Results, result.KV)
|
|
}
|
|
return resp.StatusCode == http.StatusOK, &kvResp, qm, nil
|
|
}
|
|
|
|
var buf bytes.Buffer
|
|
if _, err := io.Copy(&buf, resp.Body); err != nil {
|
|
return false, nil, nil, fmt.Errorf("Failed to read response: %v", err)
|
|
}
|
|
return false, nil, nil, fmt.Errorf("Failed request: %s", buf.String())
|
|
}
|