1package api
2
3import (
4	"bytes"
5	"fmt"
6	"io"
7	"net/http"
8)
9
10// Txn is used to manipulate the Txn API
11type Txn struct {
12	c *Client
13}
14
15// Txn is used to return a handle to the K/V apis
16func (c *Client) Txn() *Txn {
17	return &Txn{c}
18}
19
20// TxnOp is the internal format we send to Consul. Currently only K/V and
21// check operations are supported.
22type TxnOp struct {
23	KV      *KVTxnOp
24	Node    *NodeTxnOp
25	Service *ServiceTxnOp
26	Check   *CheckTxnOp
27}
28
29// TxnOps is a list of transaction operations.
30type TxnOps []*TxnOp
31
32// TxnResult is the internal format we receive from Consul.
33type TxnResult struct {
34	KV      *KVPair
35	Node    *Node
36	Service *CatalogService
37	Check   *HealthCheck
38}
39
40// TxnResults is a list of TxnResult objects.
41type TxnResults []*TxnResult
42
43// TxnError is used to return information about an operation in a transaction.
44type TxnError struct {
45	OpIndex int
46	What    string
47}
48
49// TxnErrors is a list of TxnError objects.
50type TxnErrors []*TxnError
51
52// TxnResponse is the internal format we receive from Consul.
53type TxnResponse struct {
54	Results TxnResults
55	Errors  TxnErrors
56}
57
58// KVOp constants give possible operations available in a transaction.
59type KVOp string
60
61const (
62	KVSet            KVOp = "set"
63	KVDelete         KVOp = "delete"
64	KVDeleteCAS      KVOp = "delete-cas"
65	KVDeleteTree     KVOp = "delete-tree"
66	KVCAS            KVOp = "cas"
67	KVLock           KVOp = "lock"
68	KVUnlock         KVOp = "unlock"
69	KVGet            KVOp = "get"
70	KVGetTree        KVOp = "get-tree"
71	KVCheckSession   KVOp = "check-session"
72	KVCheckIndex     KVOp = "check-index"
73	KVCheckNotExists KVOp = "check-not-exists"
74)
75
76// KVTxnOp defines a single operation inside a transaction.
77type KVTxnOp struct {
78	Verb    KVOp
79	Key     string
80	Value   []byte
81	Flags   uint64
82	Index   uint64
83	Session string
84}
85
86// KVTxnOps defines a set of operations to be performed inside a single
87// transaction.
88type KVTxnOps []*KVTxnOp
89
90// KVTxnResponse has the outcome of a transaction.
91type KVTxnResponse struct {
92	Results []*KVPair
93	Errors  TxnErrors
94}
95
96// NodeOp constants give possible operations available in a transaction.
97type NodeOp string
98
99const (
100	NodeGet       NodeOp = "get"
101	NodeSet       NodeOp = "set"
102	NodeCAS       NodeOp = "cas"
103	NodeDelete    NodeOp = "delete"
104	NodeDeleteCAS NodeOp = "delete-cas"
105)
106
107// NodeTxnOp defines a single operation inside a transaction.
108type NodeTxnOp struct {
109	Verb NodeOp
110	Node Node
111}
112
113// ServiceOp constants give possible operations available in a transaction.
114type ServiceOp string
115
116const (
117	ServiceGet       ServiceOp = "get"
118	ServiceSet       ServiceOp = "set"
119	ServiceCAS       ServiceOp = "cas"
120	ServiceDelete    ServiceOp = "delete"
121	ServiceDeleteCAS ServiceOp = "delete-cas"
122)
123
124// ServiceTxnOp defines a single operation inside a transaction.
125type ServiceTxnOp struct {
126	Verb    ServiceOp
127	Node    string
128	Service AgentService
129}
130
131// CheckOp constants give possible operations available in a transaction.
132type CheckOp string
133
134const (
135	CheckGet       CheckOp = "get"
136	CheckSet       CheckOp = "set"
137	CheckCAS       CheckOp = "cas"
138	CheckDelete    CheckOp = "delete"
139	CheckDeleteCAS CheckOp = "delete-cas"
140)
141
142// CheckTxnOp defines a single operation inside a transaction.
143type CheckTxnOp struct {
144	Verb  CheckOp
145	Check HealthCheck
146}
147
148// Txn is used to apply multiple Consul operations in a single, atomic transaction.
149//
150// Note that Go will perform the required base64 encoding on the values
151// automatically because the type is a byte slice. Transactions are defined as a
152// list of operations to perform, using the different fields in the TxnOp structure
153// to define operations. If any operation fails, none of the changes are applied
154// to the state store.
155//
156// Even though this is generally a write operation, we take a QueryOptions input
157// and return a QueryMeta output. If the transaction contains only read ops, then
158// Consul will fast-path it to a different endpoint internally which supports
159// consistency controls, but not blocking. If there are write operations then
160// the request will always be routed through raft and any consistency settings
161// will be ignored.
162//
163// Here's an example:
164//
165//	   ops := KVTxnOps{
166//		   &KVTxnOp{
167//			   Verb:    KVLock,
168//			   Key:     "test/lock",
169//			   Session: "adf4238a-882b-9ddc-4a9d-5b6758e4159e",
170//			   Value:   []byte("hello"),
171//		   },
172//		   &KVTxnOp{
173//			   Verb:    KVGet,
174//			   Key:     "another/key",
175//		   },
176//		   &CheckTxnOp{
177//			   Verb:        CheckSet,
178//			   HealthCheck: HealthCheck{
179//				   Node:    "foo",
180//				   CheckID: "redis:a",
181//				   Name:    "Redis Health Check",
182//				   Status:  "passing",
183//			   },
184//		   }
185//	   }
186//	   ok, response, _, err := kv.Txn(&ops, nil)
187//
188// If there is a problem making the transaction request then an error will be
189// returned. Otherwise, the ok value will be true if the transaction succeeded
190// or false if it was rolled back. The response is a structured return value which
191// will have the outcome of the transaction. Its Results member will have entries
192// for each operation. For KV operations, Deleted keys will have a nil entry in the
193// results, and to save space, the Value of each key in the Results will be nil
194// unless the operation is a KVGet. If the transaction was rolled back, the Errors
195// member will have entries referencing the index of the operation that failed
196// along with an error message.
197func (t *Txn) Txn(txn TxnOps, q *QueryOptions) (bool, *TxnResponse, *QueryMeta, error) {
198	return t.c.txn(txn, q)
199}
200
201func (c *Client) txn(txn TxnOps, q *QueryOptions) (bool, *TxnResponse, *QueryMeta, error) {
202	r := c.newRequest("PUT", "/v1/txn")
203	r.setQueryOptions(q)
204
205	r.obj = txn
206	rtt, resp, err := c.doRequest(r)
207	if err != nil {
208		return false, nil, nil, err
209	}
210	defer resp.Body.Close()
211
212	qm := &QueryMeta{}
213	parseQueryMeta(resp, qm)
214	qm.RequestTime = rtt
215
216	if resp.StatusCode == http.StatusOK || resp.StatusCode == http.StatusConflict {
217		var txnResp TxnResponse
218		if err := decodeBody(resp, &txnResp); err != nil {
219			return false, nil, nil, err
220		}
221
222		return resp.StatusCode == http.StatusOK, &txnResp, qm, nil
223	}
224
225	var buf bytes.Buffer
226	if _, err := io.Copy(&buf, resp.Body); err != nil {
227		return false, nil, nil, fmt.Errorf("Failed to read response: %v", err)
228	}
229	return false, nil, nil, fmt.Errorf("Failed request: %s", buf.String())
230}
231