├── .gitignore ├── LICENSE ├── README.md ├── acl.go ├── acl_test.go ├── agent.go ├── agent_test.go ├── api.go ├── api_test.go ├── catalog.go ├── catalog_test.go ├── event.go ├── event_test.go ├── health.go ├── health_test.go ├── kv.go ├── kv_test.go ├── session.go ├── session_test.go ├── status.go └── status_test.go /.gitignore: -------------------------------------------------------------------------------- 1 | # Compiled Object files, Static and Dynamic libs (Shared Objects) 2 | *.o 3 | *.a 4 | *.so 5 | 6 | # Folders 7 | _obj 8 | _test 9 | 10 | # Architecture specific extensions/prefixes 11 | *.[568vq] 12 | [568vq].out 13 | 14 | *.cgo1.go 15 | *.cgo2.c 16 | _cgo_defun.c 17 | _cgo_gotypes.go 18 | _cgo_export.* 19 | 20 | _testmain.go 21 | 22 | *.exe 23 | *.test 24 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Mozilla Public License, version 2.0 2 | 3 | 1. Definitions 4 | 5 | 1.1. "Contributor" 6 | 7 | means each individual or legal entity that creates, contributes to the 8 | creation of, or owns Covered Software. 9 | 10 | 1.2. "Contributor Version" 11 | 12 | means the combination of the Contributions of others (if any) used by a 13 | Contributor and that particular Contributor's Contribution. 14 | 15 | 1.3. "Contribution" 16 | 17 | means Covered Software of a particular Contributor. 18 | 19 | 1.4. "Covered Software" 20 | 21 | means Source Code Form to which the initial Contributor has attached the 22 | notice in Exhibit A, the Executable Form of such Source Code Form, and 23 | Modifications of such Source Code Form, in each case including portions 24 | thereof. 25 | 26 | 1.5. "Incompatible With Secondary Licenses" 27 | means 28 | 29 | a. that the initial Contributor has attached the notice described in 30 | Exhibit B to the Covered Software; or 31 | 32 | b. that the Covered Software was made available under the terms of 33 | version 1.1 or earlier of the License, but not also under the terms of 34 | a Secondary License. 35 | 36 | 1.6. "Executable Form" 37 | 38 | means any form of the work other than Source Code Form. 39 | 40 | 1.7. "Larger Work" 41 | 42 | means a work that combines Covered Software with other material, in a 43 | separate file or files, that is not Covered Software. 44 | 45 | 1.8. "License" 46 | 47 | means this document. 48 | 49 | 1.9. "Licensable" 50 | 51 | means having the right to grant, to the maximum extent possible, whether 52 | at the time of the initial grant or subsequently, any and all of the 53 | rights conveyed by this License. 54 | 55 | 1.10. "Modifications" 56 | 57 | means any of the following: 58 | 59 | a. any file in Source Code Form that results from an addition to, 60 | deletion from, or modification of the contents of Covered Software; or 61 | 62 | b. any new file in Source Code Form that contains any Covered Software. 63 | 64 | 1.11. "Patent Claims" of a Contributor 65 | 66 | means any patent claim(s), including without limitation, method, 67 | process, and apparatus claims, in any patent Licensable by such 68 | Contributor that would be infringed, but for the grant of the License, 69 | by the making, using, selling, offering for sale, having made, import, 70 | or transfer of either its Contributions or its Contributor Version. 71 | 72 | 1.12. "Secondary License" 73 | 74 | means either the GNU General Public License, Version 2.0, the GNU Lesser 75 | General Public License, Version 2.1, the GNU Affero General Public 76 | License, Version 3.0, or any later versions of those licenses. 77 | 78 | 1.13. "Source Code Form" 79 | 80 | means the form of the work preferred for making modifications. 81 | 82 | 1.14. "You" (or "Your") 83 | 84 | means an individual or a legal entity exercising rights under this 85 | License. For legal entities, "You" includes any entity that controls, is 86 | controlled by, or is under common control with You. For purposes of this 87 | definition, "control" means (a) the power, direct or indirect, to cause 88 | the direction or management of such entity, whether by contract or 89 | otherwise, or (b) ownership of more than fifty percent (50%) of the 90 | outstanding shares or beneficial ownership of such entity. 91 | 92 | 93 | 2. License Grants and Conditions 94 | 95 | 2.1. Grants 96 | 97 | Each Contributor hereby grants You a world-wide, royalty-free, 98 | non-exclusive license: 99 | 100 | a. under intellectual property rights (other than patent or trademark) 101 | Licensable by such Contributor to use, reproduce, make available, 102 | modify, display, perform, distribute, and otherwise exploit its 103 | Contributions, either on an unmodified basis, with Modifications, or 104 | as part of a Larger Work; and 105 | 106 | b. under Patent Claims of such Contributor to make, use, sell, offer for 107 | sale, have made, import, and otherwise transfer either its 108 | Contributions or its Contributor Version. 109 | 110 | 2.2. Effective Date 111 | 112 | The licenses granted in Section 2.1 with respect to any Contribution 113 | become effective for each Contribution on the date the Contributor first 114 | distributes such Contribution. 115 | 116 | 2.3. Limitations on Grant Scope 117 | 118 | The licenses granted in this Section 2 are the only rights granted under 119 | this License. No additional rights or licenses will be implied from the 120 | distribution or licensing of Covered Software under this License. 121 | Notwithstanding Section 2.1(b) above, no patent license is granted by a 122 | Contributor: 123 | 124 | a. for any code that a Contributor has removed from Covered Software; or 125 | 126 | b. for infringements caused by: (i) Your and any other third party's 127 | modifications of Covered Software, or (ii) the combination of its 128 | Contributions with other software (except as part of its Contributor 129 | Version); or 130 | 131 | c. under Patent Claims infringed by Covered Software in the absence of 132 | its Contributions. 133 | 134 | This License does not grant any rights in the trademarks, service marks, 135 | or logos of any Contributor (except as may be necessary to comply with 136 | the notice requirements in Section 3.4). 137 | 138 | 2.4. Subsequent Licenses 139 | 140 | No Contributor makes additional grants as a result of Your choice to 141 | distribute the Covered Software under a subsequent version of this 142 | License (see Section 10.2) or under the terms of a Secondary License (if 143 | permitted under the terms of Section 3.3). 144 | 145 | 2.5. Representation 146 | 147 | Each Contributor represents that the Contributor believes its 148 | Contributions are its original creation(s) or it has sufficient rights to 149 | grant the rights to its Contributions conveyed by this License. 150 | 151 | 2.6. Fair Use 152 | 153 | This License is not intended to limit any rights You have under 154 | applicable copyright doctrines of fair use, fair dealing, or other 155 | equivalents. 156 | 157 | 2.7. Conditions 158 | 159 | Sections 3.1, 3.2, 3.3, and 3.4 are conditions of the licenses granted in 160 | Section 2.1. 161 | 162 | 163 | 3. Responsibilities 164 | 165 | 3.1. Distribution of Source Form 166 | 167 | All distribution of Covered Software in Source Code Form, including any 168 | Modifications that You create or to which You contribute, must be under 169 | the terms of this License. You must inform recipients that the Source 170 | Code Form of the Covered Software is governed by the terms of this 171 | License, and how they can obtain a copy of this License. You may not 172 | attempt to alter or restrict the recipients' rights in the Source Code 173 | Form. 174 | 175 | 3.2. Distribution of Executable Form 176 | 177 | If You distribute Covered Software in Executable Form then: 178 | 179 | a. such Covered Software must also be made available in Source Code Form, 180 | as described in Section 3.1, and You must inform recipients of the 181 | Executable Form how they can obtain a copy of such Source Code Form by 182 | reasonable means in a timely manner, at a charge no more than the cost 183 | of distribution to the recipient; and 184 | 185 | b. You may distribute such Executable Form under the terms of this 186 | License, or sublicense it under different terms, provided that the 187 | license for the Executable Form does not attempt to limit or alter the 188 | recipients' rights in the Source Code Form under this License. 189 | 190 | 3.3. Distribution of a Larger Work 191 | 192 | You may create and distribute a Larger Work under terms of Your choice, 193 | provided that You also comply with the requirements of this License for 194 | the Covered Software. If the Larger Work is a combination of Covered 195 | Software with a work governed by one or more Secondary Licenses, and the 196 | Covered Software is not Incompatible With Secondary Licenses, this 197 | License permits You to additionally distribute such Covered Software 198 | under the terms of such Secondary License(s), so that the recipient of 199 | the Larger Work may, at their option, further distribute the Covered 200 | Software under the terms of either this License or such Secondary 201 | License(s). 202 | 203 | 3.4. Notices 204 | 205 | You may not remove or alter the substance of any license notices 206 | (including copyright notices, patent notices, disclaimers of warranty, or 207 | limitations of liability) contained within the Source Code Form of the 208 | Covered Software, except that You may alter any license notices to the 209 | extent required to remedy known factual inaccuracies. 210 | 211 | 3.5. Application of Additional Terms 212 | 213 | You may choose to offer, and to charge a fee for, warranty, support, 214 | indemnity or liability obligations to one or more recipients of Covered 215 | Software. However, You may do so only on Your own behalf, and not on 216 | behalf of any Contributor. You must make it absolutely clear that any 217 | such warranty, support, indemnity, or liability obligation is offered by 218 | You alone, and You hereby agree to indemnify every Contributor for any 219 | liability incurred by such Contributor as a result of warranty, support, 220 | indemnity or liability terms You offer. You may include additional 221 | disclaimers of warranty and limitations of liability specific to any 222 | jurisdiction. 223 | 224 | 4. Inability to Comply Due to Statute or Regulation 225 | 226 | If it is impossible for You to comply with any of the terms of this License 227 | with respect to some or all of the Covered Software due to statute, 228 | judicial order, or regulation then You must: (a) comply with the terms of 229 | this License to the maximum extent possible; and (b) describe the 230 | limitations and the code they affect. Such description must be placed in a 231 | text file included with all distributions of the Covered Software under 232 | this License. Except to the extent prohibited by statute or regulation, 233 | such description must be sufficiently detailed for a recipient of ordinary 234 | skill to be able to understand it. 235 | 236 | 5. Termination 237 | 238 | 5.1. The rights granted under this License will terminate automatically if You 239 | fail to comply with any of its terms. However, if You become compliant, 240 | then the rights granted under this License from a particular Contributor 241 | are reinstated (a) provisionally, unless and until such Contributor 242 | explicitly and finally terminates Your grants, and (b) on an ongoing 243 | basis, if such Contributor fails to notify You of the non-compliance by 244 | some reasonable means prior to 60 days after You have come back into 245 | compliance. Moreover, Your grants from a particular Contributor are 246 | reinstated on an ongoing basis if such Contributor notifies You of the 247 | non-compliance by some reasonable means, this is the first time You have 248 | received notice of non-compliance with this License from such 249 | Contributor, and You become compliant prior to 30 days after Your receipt 250 | of the notice. 251 | 252 | 5.2. If You initiate litigation against any entity by asserting a patent 253 | infringement claim (excluding declaratory judgment actions, 254 | counter-claims, and cross-claims) alleging that a Contributor Version 255 | directly or indirectly infringes any patent, then the rights granted to 256 | You by any and all Contributors for the Covered Software under Section 257 | 2.1 of this License shall terminate. 258 | 259 | 5.3. In the event of termination under Sections 5.1 or 5.2 above, all end user 260 | license agreements (excluding distributors and resellers) which have been 261 | validly granted by You or Your distributors under this License prior to 262 | termination shall survive termination. 263 | 264 | 6. Disclaimer of Warranty 265 | 266 | Covered Software is provided under this License on an "as is" basis, 267 | without warranty of any kind, either expressed, implied, or statutory, 268 | including, without limitation, warranties that the Covered Software is free 269 | of defects, merchantable, fit for a particular purpose or non-infringing. 270 | The entire risk as to the quality and performance of the Covered Software 271 | is with You. Should any Covered Software prove defective in any respect, 272 | You (not any Contributor) assume the cost of any necessary servicing, 273 | repair, or correction. This disclaimer of warranty constitutes an essential 274 | part of this License. No use of any Covered Software is authorized under 275 | this License except under this disclaimer. 276 | 277 | 7. Limitation of Liability 278 | 279 | Under no circumstances and under no legal theory, whether tort (including 280 | negligence), contract, or otherwise, shall any Contributor, or anyone who 281 | distributes Covered Software as permitted above, be liable to You for any 282 | direct, indirect, special, incidental, or consequential damages of any 283 | character including, without limitation, damages for lost profits, loss of 284 | goodwill, work stoppage, computer failure or malfunction, or any and all 285 | other commercial damages or losses, even if such party shall have been 286 | informed of the possibility of such damages. This limitation of liability 287 | shall not apply to liability for death or personal injury resulting from 288 | such party's negligence to the extent applicable law prohibits such 289 | limitation. Some jurisdictions do not allow the exclusion or limitation of 290 | incidental or consequential damages, so this exclusion and limitation may 291 | not apply to You. 292 | 293 | 8. Litigation 294 | 295 | Any litigation relating to this License may be brought only in the courts 296 | of a jurisdiction where the defendant maintains its principal place of 297 | business and such litigation shall be governed by laws of that 298 | jurisdiction, without reference to its conflict-of-law provisions. Nothing 299 | in this Section shall prevent a party's ability to bring cross-claims or 300 | counter-claims. 301 | 302 | 9. Miscellaneous 303 | 304 | This License represents the complete agreement concerning the subject 305 | matter hereof. If any provision of this License is held to be 306 | unenforceable, such provision shall be reformed only to the extent 307 | necessary to make it enforceable. Any law or regulation which provides that 308 | the language of a contract shall be construed against the drafter shall not 309 | be used to construe this License against a Contributor. 310 | 311 | 312 | 10. Versions of the License 313 | 314 | 10.1. New Versions 315 | 316 | Mozilla Foundation is the license steward. Except as provided in Section 317 | 10.3, no one other than the license steward has the right to modify or 318 | publish new versions of this License. Each version will be given a 319 | distinguishing version number. 320 | 321 | 10.2. Effect of New Versions 322 | 323 | You may distribute the Covered Software under the terms of the version 324 | of the License under which You originally received the Covered Software, 325 | or under the terms of any subsequent version published by the license 326 | steward. 327 | 328 | 10.3. Modified Versions 329 | 330 | If you create software not governed by this License, and you want to 331 | create a new license for such software, you may create and use a 332 | modified version of this License if you rename the license and remove 333 | any references to the name of the license steward (except to note that 334 | such modified license differs from this License). 335 | 336 | 10.4. Distributing Source Code Form that is Incompatible With Secondary 337 | Licenses If You choose to distribute Source Code Form that is 338 | Incompatible With Secondary Licenses under the terms of this version of 339 | the License, the notice described in Exhibit B of this License must be 340 | attached. 341 | 342 | Exhibit A - Source Code Form License Notice 343 | 344 | This Source Code Form is subject to the 345 | terms of the Mozilla Public License, v. 346 | 2.0. If a copy of the MPL was not 347 | distributed with this file, You can 348 | obtain one at 349 | http://mozilla.org/MPL/2.0/. 350 | 351 | If it is not possible or desirable to put the notice in a particular file, 352 | then You may include the notice in a location (such as a LICENSE file in a 353 | relevant directory) where a recipient would be likely to look for such a 354 | notice. 355 | 356 | You may add additional accurate notices of copyright ownership. 357 | 358 | Exhibit B - "Incompatible With Secondary Licenses" Notice 359 | 360 | This Source Code Form is "Incompatible 361 | With Secondary Licenses", as defined by 362 | the Mozilla Public License, v. 2.0. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | consul-api 2 | ========== 3 | 4 | *DEPRECATED* Please use [consul api package](https://github.com/hashicorp/consul/tree/master/api) instead. 5 | Godocs for that package [are here](http://godoc.org/github.com/hashicorp/consul/api). 6 | 7 | This package provides the `consulapi` package which attempts to 8 | provide programmatic access to the full Consul API. 9 | 10 | Currently, all of the Consul APIs included in version 0.4 are supported. 11 | 12 | Documentation 13 | ============= 14 | 15 | The full documentation is available on [Godoc](http://godoc.org/github.com/armon/consul-api) 16 | 17 | Usage 18 | ===== 19 | 20 | Below is an example of using the Consul client: 21 | 22 | ```go 23 | // Get a new client, with KV endpoints 24 | client, _ := consulapi.NewClient(consulapi.DefaultConfig()) 25 | kv := client.KV() 26 | 27 | // PUT a new KV pair 28 | p := &consulapi.KVPair{Key: "foo", Value: []byte("test")} 29 | _, err := kv.Put(p, nil) 30 | if err != nil { 31 | panic(err) 32 | } 33 | 34 | // Lookup the pair 35 | pair, _, err := kv.Get("foo", nil) 36 | if err != nil { 37 | panic(err) 38 | } 39 | fmt.Printf("KV: %v", pair) 40 | 41 | ``` 42 | 43 | -------------------------------------------------------------------------------- /acl.go: -------------------------------------------------------------------------------- 1 | package consulapi 2 | 3 | const ( 4 | // ACLCLientType is the client type token 5 | ACLClientType = "client" 6 | 7 | // ACLManagementType is the management type token 8 | ACLManagementType = "management" 9 | ) 10 | 11 | // ACLEntry is used to represent an ACL entry 12 | type ACLEntry struct { 13 | CreateIndex uint64 14 | ModifyIndex uint64 15 | ID string 16 | Name string 17 | Type string 18 | Rules string 19 | } 20 | 21 | // ACL can be used to query the ACL endpoints 22 | type ACL struct { 23 | c *Client 24 | } 25 | 26 | // ACL returns a handle to the ACL endpoints 27 | func (c *Client) ACL() *ACL { 28 | return &ACL{c} 29 | } 30 | 31 | // Create is used to generate a new token with the given parameters 32 | func (a *ACL) Create(acl *ACLEntry, q *WriteOptions) (string, *WriteMeta, error) { 33 | r := a.c.newRequest("PUT", "/v1/acl/create") 34 | r.setWriteOptions(q) 35 | r.obj = acl 36 | rtt, resp, err := requireOK(a.c.doRequest(r)) 37 | if err != nil { 38 | return "", nil, err 39 | } 40 | defer resp.Body.Close() 41 | 42 | wm := &WriteMeta{RequestTime: rtt} 43 | var out struct{ ID string } 44 | if err := decodeBody(resp, &out); err != nil { 45 | return "", nil, err 46 | } 47 | return out.ID, wm, nil 48 | } 49 | 50 | // Update is used to update the rules of an existing token 51 | func (a *ACL) Update(acl *ACLEntry, q *WriteOptions) (*WriteMeta, error) { 52 | r := a.c.newRequest("PUT", "/v1/acl/update") 53 | r.setWriteOptions(q) 54 | r.obj = acl 55 | rtt, resp, err := requireOK(a.c.doRequest(r)) 56 | if err != nil { 57 | return nil, err 58 | } 59 | defer resp.Body.Close() 60 | 61 | wm := &WriteMeta{RequestTime: rtt} 62 | return wm, nil 63 | } 64 | 65 | // Destroy is used to destroy a given ACL token ID 66 | func (a *ACL) Destroy(id string, q *WriteOptions) (*WriteMeta, error) { 67 | r := a.c.newRequest("PUT", "/v1/acl/destroy/"+id) 68 | r.setWriteOptions(q) 69 | rtt, resp, err := requireOK(a.c.doRequest(r)) 70 | if err != nil { 71 | return nil, err 72 | } 73 | resp.Body.Close() 74 | 75 | wm := &WriteMeta{RequestTime: rtt} 76 | return wm, nil 77 | } 78 | 79 | // Clone is used to return a new token cloned from an existing one 80 | func (a *ACL) Clone(id string, q *WriteOptions) (string, *WriteMeta, error) { 81 | r := a.c.newRequest("PUT", "/v1/acl/clone/"+id) 82 | r.setWriteOptions(q) 83 | rtt, resp, err := requireOK(a.c.doRequest(r)) 84 | if err != nil { 85 | return "", nil, err 86 | } 87 | defer resp.Body.Close() 88 | 89 | wm := &WriteMeta{RequestTime: rtt} 90 | var out struct{ ID string } 91 | if err := decodeBody(resp, &out); err != nil { 92 | return "", nil, err 93 | } 94 | return out.ID, wm, nil 95 | } 96 | 97 | // Info is used to query for information about an ACL token 98 | func (a *ACL) Info(id string, q *QueryOptions) (*ACLEntry, *QueryMeta, error) { 99 | r := a.c.newRequest("GET", "/v1/acl/info/"+id) 100 | r.setQueryOptions(q) 101 | rtt, resp, err := requireOK(a.c.doRequest(r)) 102 | if err != nil { 103 | return nil, nil, err 104 | } 105 | defer resp.Body.Close() 106 | 107 | qm := &QueryMeta{} 108 | parseQueryMeta(resp, qm) 109 | qm.RequestTime = rtt 110 | 111 | var entries []*ACLEntry 112 | if err := decodeBody(resp, &entries); err != nil { 113 | return nil, nil, err 114 | } 115 | if len(entries) > 0 { 116 | return entries[0], qm, nil 117 | } 118 | return nil, qm, nil 119 | } 120 | 121 | // List is used to get all the ACL tokens 122 | func (a *ACL) List(q *QueryOptions) ([]*ACLEntry, *QueryMeta, error) { 123 | r := a.c.newRequest("GET", "/v1/acl/list") 124 | r.setQueryOptions(q) 125 | rtt, resp, err := requireOK(a.c.doRequest(r)) 126 | if err != nil { 127 | return nil, nil, err 128 | } 129 | defer resp.Body.Close() 130 | 131 | qm := &QueryMeta{} 132 | parseQueryMeta(resp, qm) 133 | qm.RequestTime = rtt 134 | 135 | var entries []*ACLEntry 136 | if err := decodeBody(resp, &entries); err != nil { 137 | return nil, nil, err 138 | } 139 | return entries, qm, nil 140 | } 141 | -------------------------------------------------------------------------------- /acl_test.go: -------------------------------------------------------------------------------- 1 | package consulapi 2 | 3 | import ( 4 | "os" 5 | "testing" 6 | ) 7 | 8 | // ROOT is a management token for the tests 9 | var CONSUL_ROOT string 10 | 11 | func init() { 12 | CONSUL_ROOT = os.Getenv("CONSUL_ROOT") 13 | } 14 | 15 | func TestACL_CreateDestroy(t *testing.T) { 16 | if CONSUL_ROOT == "" { 17 | t.SkipNow() 18 | } 19 | c := makeClient(t) 20 | c.config.Token = CONSUL_ROOT 21 | acl := c.ACL() 22 | 23 | ae := ACLEntry{ 24 | Name: "API test", 25 | Type: ACLClientType, 26 | Rules: `key "" { policy = "deny" }`, 27 | } 28 | 29 | id, wm, err := acl.Create(&ae, nil) 30 | if err != nil { 31 | t.Fatalf("err: %v", err) 32 | } 33 | 34 | if wm.RequestTime == 0 { 35 | t.Fatalf("bad: %v", wm) 36 | } 37 | 38 | if id == "" { 39 | t.Fatalf("invalid: %v", id) 40 | } 41 | 42 | ae2, _, err := acl.Info(id, nil) 43 | if err != nil { 44 | t.Fatalf("err: %v", err) 45 | } 46 | 47 | if ae2.Name != ae.Name || ae2.Type != ae.Type || ae2.Rules != ae.Rules { 48 | t.Fatalf("Bad: %#v", ae2) 49 | } 50 | 51 | wm, err = acl.Destroy(id, nil) 52 | if err != nil { 53 | t.Fatalf("err: %v", err) 54 | } 55 | 56 | if wm.RequestTime == 0 { 57 | t.Fatalf("bad: %v", wm) 58 | } 59 | } 60 | 61 | func TestACL_CloneDestroy(t *testing.T) { 62 | if CONSUL_ROOT == "" { 63 | t.SkipNow() 64 | } 65 | c := makeClient(t) 66 | c.config.Token = CONSUL_ROOT 67 | acl := c.ACL() 68 | 69 | id, wm, err := acl.Clone(CONSUL_ROOT, nil) 70 | if err != nil { 71 | t.Fatalf("err: %v", err) 72 | } 73 | 74 | if wm.RequestTime == 0 { 75 | t.Fatalf("bad: %v", wm) 76 | } 77 | 78 | if id == "" { 79 | t.Fatalf("invalid: %v", id) 80 | } 81 | 82 | wm, err = acl.Destroy(id, nil) 83 | if err != nil { 84 | t.Fatalf("err: %v", err) 85 | } 86 | 87 | if wm.RequestTime == 0 { 88 | t.Fatalf("bad: %v", wm) 89 | } 90 | } 91 | 92 | func TestACL_Info(t *testing.T) { 93 | if CONSUL_ROOT == "" { 94 | t.SkipNow() 95 | } 96 | c := makeClient(t) 97 | c.config.Token = CONSUL_ROOT 98 | acl := c.ACL() 99 | 100 | ae, qm, err := acl.Info(CONSUL_ROOT, nil) 101 | if err != nil { 102 | t.Fatalf("err: %v", err) 103 | } 104 | 105 | if qm.LastIndex == 0 { 106 | t.Fatalf("bad: %v", qm) 107 | } 108 | if !qm.KnownLeader { 109 | t.Fatalf("bad: %v", qm) 110 | } 111 | 112 | if ae == nil || ae.ID != CONSUL_ROOT || ae.Type != ACLManagementType { 113 | t.Fatalf("bad: %#v", ae) 114 | } 115 | } 116 | 117 | func TestACL_List(t *testing.T) { 118 | if CONSUL_ROOT == "" { 119 | t.SkipNow() 120 | } 121 | c := makeClient(t) 122 | c.config.Token = CONSUL_ROOT 123 | acl := c.ACL() 124 | 125 | acls, qm, err := acl.List(nil) 126 | if err != nil { 127 | t.Fatalf("err: %v", err) 128 | } 129 | 130 | if len(acls) < 2 { 131 | t.Fatalf("bad: %v", acls) 132 | } 133 | 134 | if qm.LastIndex == 0 { 135 | t.Fatalf("bad: %v", qm) 136 | } 137 | if !qm.KnownLeader { 138 | t.Fatalf("bad: %v", qm) 139 | } 140 | } 141 | -------------------------------------------------------------------------------- /agent.go: -------------------------------------------------------------------------------- 1 | package consulapi 2 | 3 | import ( 4 | "fmt" 5 | ) 6 | 7 | // AgentCheck represents a check known to the agent 8 | type AgentCheck struct { 9 | Node string 10 | CheckID string 11 | Name string 12 | Status string 13 | Notes string 14 | Output string 15 | ServiceID string 16 | ServiceName string 17 | } 18 | 19 | // AgentService represents a service known to the agent 20 | type AgentService struct { 21 | ID string 22 | Service string 23 | Tags []string 24 | Port int 25 | } 26 | 27 | // AgentMember represents a cluster member known to the agent 28 | type AgentMember struct { 29 | Name string 30 | Addr string 31 | Port uint16 32 | Tags map[string]string 33 | Status int 34 | ProtocolMin uint8 35 | ProtocolMax uint8 36 | ProtocolCur uint8 37 | DelegateMin uint8 38 | DelegateMax uint8 39 | DelegateCur uint8 40 | } 41 | 42 | // AgentServiceRegistration is used to register a new service 43 | type AgentServiceRegistration struct { 44 | ID string `json:",omitempty"` 45 | Name string `json:",omitempty"` 46 | Tags []string `json:",omitempty"` 47 | Port int `json:",omitempty"` 48 | Check *AgentServiceCheck 49 | } 50 | 51 | // AgentCheckRegistration is used to register a new check 52 | type AgentCheckRegistration struct { 53 | ID string `json:",omitempty"` 54 | Name string `json:",omitempty"` 55 | Notes string `json:",omitempty"` 56 | AgentServiceCheck 57 | } 58 | 59 | // AgentServiceCheck is used to create an associated 60 | // check for a service 61 | type AgentServiceCheck struct { 62 | Script string `json:",omitempty"` 63 | Interval string `json:",omitempty"` 64 | TTL string `json:",omitempty"` 65 | } 66 | 67 | // Agent can be used to query the Agent endpoints 68 | type Agent struct { 69 | c *Client 70 | 71 | // cache the node name 72 | nodeName string 73 | } 74 | 75 | // Agent returns a handle to the agent endpoints 76 | func (c *Client) Agent() *Agent { 77 | return &Agent{c: c} 78 | } 79 | 80 | // Self is used to query the agent we are speaking to for 81 | // information about itself 82 | func (a *Agent) Self() (map[string]map[string]interface{}, error) { 83 | r := a.c.newRequest("GET", "/v1/agent/self") 84 | _, resp, err := requireOK(a.c.doRequest(r)) 85 | if err != nil { 86 | return nil, err 87 | } 88 | defer resp.Body.Close() 89 | 90 | var out map[string]map[string]interface{} 91 | if err := decodeBody(resp, &out); err != nil { 92 | return nil, err 93 | } 94 | return out, nil 95 | } 96 | 97 | // NodeName is used to get the node name of the agent 98 | func (a *Agent) NodeName() (string, error) { 99 | if a.nodeName != "" { 100 | return a.nodeName, nil 101 | } 102 | info, err := a.Self() 103 | if err != nil { 104 | return "", err 105 | } 106 | name := info["Config"]["NodeName"].(string) 107 | a.nodeName = name 108 | return name, nil 109 | } 110 | 111 | // Checks returns the locally registered checks 112 | func (a *Agent) Checks() (map[string]*AgentCheck, error) { 113 | r := a.c.newRequest("GET", "/v1/agent/checks") 114 | _, resp, err := requireOK(a.c.doRequest(r)) 115 | if err != nil { 116 | return nil, err 117 | } 118 | defer resp.Body.Close() 119 | 120 | var out map[string]*AgentCheck 121 | if err := decodeBody(resp, &out); err != nil { 122 | return nil, err 123 | } 124 | return out, nil 125 | } 126 | 127 | // Services returns the locally registered services 128 | func (a *Agent) Services() (map[string]*AgentService, error) { 129 | r := a.c.newRequest("GET", "/v1/agent/services") 130 | _, resp, err := requireOK(a.c.doRequest(r)) 131 | if err != nil { 132 | return nil, err 133 | } 134 | defer resp.Body.Close() 135 | 136 | var out map[string]*AgentService 137 | if err := decodeBody(resp, &out); err != nil { 138 | return nil, err 139 | } 140 | return out, nil 141 | } 142 | 143 | // Members returns the known gossip members. The WAN 144 | // flag can be used to query a server for WAN members. 145 | func (a *Agent) Members(wan bool) ([]*AgentMember, error) { 146 | r := a.c.newRequest("GET", "/v1/agent/members") 147 | if wan { 148 | r.params.Set("wan", "1") 149 | } 150 | _, resp, err := requireOK(a.c.doRequest(r)) 151 | if err != nil { 152 | return nil, err 153 | } 154 | defer resp.Body.Close() 155 | 156 | var out []*AgentMember 157 | if err := decodeBody(resp, &out); err != nil { 158 | return nil, err 159 | } 160 | return out, nil 161 | } 162 | 163 | // ServiceRegister is used to register a new service with 164 | // the local agent 165 | func (a *Agent) ServiceRegister(service *AgentServiceRegistration) error { 166 | r := a.c.newRequest("PUT", "/v1/agent/service/register") 167 | r.obj = service 168 | _, resp, err := requireOK(a.c.doRequest(r)) 169 | if err != nil { 170 | return err 171 | } 172 | resp.Body.Close() 173 | return nil 174 | } 175 | 176 | // ServiceDeregister is used to deregister a service with 177 | // the local agent 178 | func (a *Agent) ServiceDeregister(serviceID string) error { 179 | r := a.c.newRequest("PUT", "/v1/agent/service/deregister/"+serviceID) 180 | _, resp, err := requireOK(a.c.doRequest(r)) 181 | if err != nil { 182 | return err 183 | } 184 | resp.Body.Close() 185 | return nil 186 | } 187 | 188 | // PassTTL is used to set a TTL check to the passing state 189 | func (a *Agent) PassTTL(checkID, note string) error { 190 | return a.UpdateTTL(checkID, note, "pass") 191 | } 192 | 193 | // WarnTTL is used to set a TTL check to the warning state 194 | func (a *Agent) WarnTTL(checkID, note string) error { 195 | return a.UpdateTTL(checkID, note, "warn") 196 | } 197 | 198 | // FailTTL is used to set a TTL check to the failing state 199 | func (a *Agent) FailTTL(checkID, note string) error { 200 | return a.UpdateTTL(checkID, note, "fail") 201 | } 202 | 203 | // UpdateTTL is used to update the TTL of a check 204 | func (a *Agent) UpdateTTL(checkID, note, status string) error { 205 | switch status { 206 | case "pass": 207 | case "warn": 208 | case "fail": 209 | default: 210 | return fmt.Errorf("Invalid status: %s", status) 211 | } 212 | endpoint := fmt.Sprintf("/v1/agent/check/%s/%s", status, checkID) 213 | r := a.c.newRequest("PUT", endpoint) 214 | r.params.Set("note", note) 215 | _, resp, err := requireOK(a.c.doRequest(r)) 216 | if err != nil { 217 | return err 218 | } 219 | resp.Body.Close() 220 | return nil 221 | } 222 | 223 | // CheckRegister is used to register a new check with 224 | // the local agent 225 | func (a *Agent) CheckRegister(check *AgentCheckRegistration) error { 226 | r := a.c.newRequest("PUT", "/v1/agent/check/register") 227 | r.obj = check 228 | _, resp, err := requireOK(a.c.doRequest(r)) 229 | if err != nil { 230 | return err 231 | } 232 | resp.Body.Close() 233 | return nil 234 | } 235 | 236 | // CheckDeregister is used to deregister a check with 237 | // the local agent 238 | func (a *Agent) CheckDeregister(checkID string) error { 239 | r := a.c.newRequest("PUT", "/v1/agent/check/deregister/"+checkID) 240 | _, resp, err := requireOK(a.c.doRequest(r)) 241 | if err != nil { 242 | return err 243 | } 244 | resp.Body.Close() 245 | return nil 246 | } 247 | 248 | // Join is used to instruct the agent to attempt a join to 249 | // another cluster member 250 | func (a *Agent) Join(addr string, wan bool) error { 251 | r := a.c.newRequest("PUT", "/v1/agent/join/"+addr) 252 | if wan { 253 | r.params.Set("wan", "1") 254 | } 255 | _, resp, err := requireOK(a.c.doRequest(r)) 256 | if err != nil { 257 | return err 258 | } 259 | resp.Body.Close() 260 | return nil 261 | } 262 | 263 | // ForceLeave is used to have the agent eject a failed node 264 | func (a *Agent) ForceLeave(node string) error { 265 | r := a.c.newRequest("PUT", "/v1/agent/force-leave/"+node) 266 | _, resp, err := requireOK(a.c.doRequest(r)) 267 | if err != nil { 268 | return err 269 | } 270 | resp.Body.Close() 271 | return nil 272 | } 273 | -------------------------------------------------------------------------------- /agent_test.go: -------------------------------------------------------------------------------- 1 | package consulapi 2 | 3 | import ( 4 | "testing" 5 | ) 6 | 7 | func TestAgent_Self(t *testing.T) { 8 | c := makeClient(t) 9 | agent := c.Agent() 10 | 11 | info, err := agent.Self() 12 | if err != nil { 13 | t.Fatalf("err: %v", err) 14 | } 15 | 16 | name := info["Config"]["NodeName"] 17 | if name == "" { 18 | t.Fatalf("bad: %v", info) 19 | } 20 | } 21 | 22 | func TestAgent_Members(t *testing.T) { 23 | c := makeClient(t) 24 | agent := c.Agent() 25 | 26 | members, err := agent.Members(false) 27 | if err != nil { 28 | t.Fatalf("err: %v", err) 29 | } 30 | 31 | if len(members) != 1 { 32 | t.Fatalf("bad: %v", members) 33 | } 34 | } 35 | 36 | func TestAgent_Services(t *testing.T) { 37 | c := makeClient(t) 38 | agent := c.Agent() 39 | 40 | reg := &AgentServiceRegistration{ 41 | Name: "foo", 42 | Tags: []string{"bar", "baz"}, 43 | Port: 8000, 44 | Check: &AgentServiceCheck{ 45 | TTL: "15s", 46 | }, 47 | } 48 | if err := agent.ServiceRegister(reg); err != nil { 49 | t.Fatalf("err: %v", err) 50 | } 51 | 52 | services, err := agent.Services() 53 | if err != nil { 54 | t.Fatalf("err: %v", err) 55 | } 56 | if _, ok := services["foo"]; !ok { 57 | t.Fatalf("missing service: %v", services) 58 | } 59 | 60 | checks, err := agent.Checks() 61 | if err != nil { 62 | t.Fatalf("err: %v", err) 63 | } 64 | if _, ok := checks["service:foo"]; !ok { 65 | t.Fatalf("missing check: %v", checks) 66 | } 67 | 68 | if err := agent.ServiceDeregister("foo"); err != nil { 69 | t.Fatalf("err: %v", err) 70 | } 71 | } 72 | 73 | func TestAgent_SetTTLStatus(t *testing.T) { 74 | c := makeClient(t) 75 | agent := c.Agent() 76 | 77 | reg := &AgentServiceRegistration{ 78 | Name: "foo", 79 | Check: &AgentServiceCheck{ 80 | TTL: "15s", 81 | }, 82 | } 83 | if err := agent.ServiceRegister(reg); err != nil { 84 | t.Fatalf("err: %v", err) 85 | } 86 | 87 | if err := agent.WarnTTL("service:foo", "test"); err != nil { 88 | t.Fatalf("err: %v", err) 89 | } 90 | 91 | checks, err := agent.Checks() 92 | if err != nil { 93 | t.Fatalf("err: %v", err) 94 | } 95 | chk, ok := checks["service:foo"] 96 | if !ok { 97 | t.Fatalf("missing check: %v", checks) 98 | } 99 | if chk.Status != "warning" { 100 | t.Fatalf("Bad: %#v", chk) 101 | } 102 | if chk.Output != "test" { 103 | t.Fatalf("Bad: %#v", chk) 104 | } 105 | 106 | if err := agent.ServiceDeregister("foo"); err != nil { 107 | t.Fatalf("err: %v", err) 108 | } 109 | } 110 | 111 | func TestAgent_Checks(t *testing.T) { 112 | c := makeClient(t) 113 | agent := c.Agent() 114 | 115 | reg := &AgentCheckRegistration{ 116 | Name: "foo", 117 | } 118 | reg.TTL = "15s" 119 | if err := agent.CheckRegister(reg); err != nil { 120 | t.Fatalf("err: %v", err) 121 | } 122 | 123 | checks, err := agent.Checks() 124 | if err != nil { 125 | t.Fatalf("err: %v", err) 126 | } 127 | if _, ok := checks["foo"]; !ok { 128 | t.Fatalf("missing check: %v", checks) 129 | } 130 | 131 | if err := agent.CheckDeregister("foo"); err != nil { 132 | t.Fatalf("err: %v", err) 133 | } 134 | } 135 | 136 | func TestAgent_Join(t *testing.T) { 137 | c := makeClient(t) 138 | agent := c.Agent() 139 | 140 | info, err := agent.Self() 141 | if err != nil { 142 | t.Fatalf("err: %v", err) 143 | } 144 | 145 | // Join ourself 146 | addr := info["Config"]["AdvertiseAddr"].(string) 147 | err = agent.Join(addr, false) 148 | if err != nil { 149 | t.Fatalf("err: %v", err) 150 | } 151 | } 152 | 153 | func TestAgent_ForceLeave(t *testing.T) { 154 | c := makeClient(t) 155 | agent := c.Agent() 156 | 157 | // Eject somebody 158 | err := agent.ForceLeave("foo") 159 | if err != nil { 160 | t.Fatalf("err: %v", err) 161 | } 162 | } 163 | -------------------------------------------------------------------------------- /api.go: -------------------------------------------------------------------------------- 1 | package consulapi 2 | 3 | import ( 4 | "bytes" 5 | "encoding/json" 6 | "fmt" 7 | "io" 8 | "net/http" 9 | "net/url" 10 | "strconv" 11 | "time" 12 | ) 13 | 14 | // QueryOptions are used to parameterize a query 15 | type QueryOptions struct { 16 | // Providing a datacenter overwrites the DC provided 17 | // by the Config 18 | Datacenter string 19 | 20 | // AllowStale allows any Consul server (non-leader) to service 21 | // a read. This allows for lower latency and higher throughput 22 | AllowStale bool 23 | 24 | // RequireConsistent forces the read to be fully consistent. 25 | // This is more expensive but prevents ever performing a stale 26 | // read. 27 | RequireConsistent bool 28 | 29 | // WaitIndex is used to enable a blocking query. Waits 30 | // until the timeout or the next index is reached 31 | WaitIndex uint64 32 | 33 | // WaitTime is used to bound the duration of a wait. 34 | // Defaults to that of the Config, but can be overriden. 35 | WaitTime time.Duration 36 | 37 | // Token is used to provide a per-request ACL token 38 | // which overrides the agent's default token. 39 | Token string 40 | } 41 | 42 | // WriteOptions are used to parameterize a write 43 | type WriteOptions struct { 44 | // Providing a datacenter overwrites the DC provided 45 | // by the Config 46 | Datacenter string 47 | 48 | // Token is used to provide a per-request ACL token 49 | // which overrides the agent's default token. 50 | Token string 51 | } 52 | 53 | // QueryMeta is used to return meta data about a query 54 | type QueryMeta struct { 55 | // LastIndex. This can be used as a WaitIndex to perform 56 | // a blocking query 57 | LastIndex uint64 58 | 59 | // Time of last contact from the leader for the 60 | // server servicing the request 61 | LastContact time.Duration 62 | 63 | // Is there a known leader 64 | KnownLeader bool 65 | 66 | // How long did the request take 67 | RequestTime time.Duration 68 | } 69 | 70 | // WriteMeta is used to return meta data about a write 71 | type WriteMeta struct { 72 | // How long did the request take 73 | RequestTime time.Duration 74 | } 75 | 76 | // HttpBasicAuth is used to authenticate http client with HTTP Basic Authentication 77 | type HttpBasicAuth struct { 78 | // Username to use for HTTP Basic Authentication 79 | Username string 80 | 81 | // Password to use for HTTP Basic Authentication 82 | Password string 83 | } 84 | 85 | // Config is used to configure the creation of a client 86 | type Config struct { 87 | // Address is the address of the Consul server 88 | Address string 89 | 90 | // Scheme is the URI scheme for the Consul server 91 | Scheme string 92 | 93 | // Datacenter to use. If not provided, the default agent datacenter is used. 94 | Datacenter string 95 | 96 | // HttpClient is the client to use. Default will be 97 | // used if not provided. 98 | HttpClient *http.Client 99 | 100 | // HttpAuth is the auth info to use for http access. 101 | HttpAuth *HttpBasicAuth 102 | 103 | // WaitTime limits how long a Watch will block. If not provided, 104 | // the agent default values will be used. 105 | WaitTime time.Duration 106 | 107 | // Token is used to provide a per-request ACL token 108 | // which overrides the agent's default token. 109 | Token string 110 | } 111 | 112 | // DefaultConfig returns a default configuration for the client 113 | func DefaultConfig() *Config { 114 | return &Config{ 115 | Address: "127.0.0.1:8500", 116 | Scheme: "http", 117 | HttpClient: http.DefaultClient, 118 | } 119 | } 120 | 121 | // Client provides a client to the Consul API 122 | type Client struct { 123 | config Config 124 | } 125 | 126 | // NewClient returns a new client 127 | func NewClient(config *Config) (*Client, error) { 128 | // bootstrap the config 129 | defConfig := DefaultConfig() 130 | 131 | if len(config.Address) == 0 { 132 | config.Address = defConfig.Address 133 | } 134 | 135 | if len(config.Scheme) == 0 { 136 | config.Scheme = defConfig.Scheme 137 | } 138 | 139 | if config.HttpClient == nil { 140 | config.HttpClient = defConfig.HttpClient 141 | } 142 | 143 | client := &Client{ 144 | config: *config, 145 | } 146 | return client, nil 147 | } 148 | 149 | // request is used to help build up a request 150 | type request struct { 151 | config *Config 152 | method string 153 | url *url.URL 154 | params url.Values 155 | body io.Reader 156 | obj interface{} 157 | } 158 | 159 | // setQueryOptions is used to annotate the request with 160 | // additional query options 161 | func (r *request) setQueryOptions(q *QueryOptions) { 162 | if q == nil { 163 | return 164 | } 165 | if q.Datacenter != "" { 166 | r.params.Set("dc", q.Datacenter) 167 | } 168 | if q.AllowStale { 169 | r.params.Set("stale", "") 170 | } 171 | if q.RequireConsistent { 172 | r.params.Set("consistent", "") 173 | } 174 | if q.WaitIndex != 0 { 175 | r.params.Set("index", strconv.FormatUint(q.WaitIndex, 10)) 176 | } 177 | if q.WaitTime != 0 { 178 | r.params.Set("wait", durToMsec(q.WaitTime)) 179 | } 180 | if q.Token != "" { 181 | r.params.Set("token", q.Token) 182 | } 183 | } 184 | 185 | // durToMsec converts a duration to a millisecond specified string 186 | func durToMsec(dur time.Duration) string { 187 | return fmt.Sprintf("%dms", dur/time.Millisecond) 188 | } 189 | 190 | // setWriteOptions is used to annotate the request with 191 | // additional write options 192 | func (r *request) setWriteOptions(q *WriteOptions) { 193 | if q == nil { 194 | return 195 | } 196 | if q.Datacenter != "" { 197 | r.params.Set("dc", q.Datacenter) 198 | } 199 | if q.Token != "" { 200 | r.params.Set("token", q.Token) 201 | } 202 | } 203 | 204 | // toHTTP converts the request to an HTTP request 205 | func (r *request) toHTTP() (*http.Request, error) { 206 | // Encode the query parameters 207 | r.url.RawQuery = r.params.Encode() 208 | 209 | // Get the url sring 210 | urlRaw := r.url.String() 211 | 212 | // Check if we should encode the body 213 | if r.body == nil && r.obj != nil { 214 | if b, err := encodeBody(r.obj); err != nil { 215 | return nil, err 216 | } else { 217 | r.body = b 218 | } 219 | } 220 | 221 | // Create the HTTP request 222 | req, err := http.NewRequest(r.method, urlRaw, r.body) 223 | 224 | // Setup auth 225 | if err == nil && r.config.HttpAuth != nil { 226 | req.SetBasicAuth(r.config.HttpAuth.Username, r.config.HttpAuth.Password) 227 | } 228 | 229 | return req, err 230 | } 231 | 232 | // newRequest is used to create a new request 233 | func (c *Client) newRequest(method, path string) *request { 234 | r := &request{ 235 | config: &c.config, 236 | method: method, 237 | url: &url.URL{ 238 | Scheme: c.config.Scheme, 239 | Host: c.config.Address, 240 | Path: path, 241 | }, 242 | params: make(map[string][]string), 243 | } 244 | if c.config.Datacenter != "" { 245 | r.params.Set("dc", c.config.Datacenter) 246 | } 247 | if c.config.WaitTime != 0 { 248 | r.params.Set("wait", durToMsec(r.config.WaitTime)) 249 | } 250 | if c.config.Token != "" { 251 | r.params.Set("token", r.config.Token) 252 | } 253 | return r 254 | } 255 | 256 | // doRequest runs a request with our client 257 | func (c *Client) doRequest(r *request) (time.Duration, *http.Response, error) { 258 | req, err := r.toHTTP() 259 | if err != nil { 260 | return 0, nil, err 261 | } 262 | start := time.Now() 263 | resp, err := c.config.HttpClient.Do(req) 264 | diff := time.Now().Sub(start) 265 | return diff, resp, err 266 | } 267 | 268 | // parseQueryMeta is used to help parse query meta-data 269 | func parseQueryMeta(resp *http.Response, q *QueryMeta) error { 270 | header := resp.Header 271 | 272 | // Parse the X-Consul-Index 273 | index, err := strconv.ParseUint(header.Get("X-Consul-Index"), 10, 64) 274 | if err != nil { 275 | return fmt.Errorf("Failed to parse X-Consul-Index: %v", err) 276 | } 277 | q.LastIndex = index 278 | 279 | // Parse the X-Consul-LastContact 280 | last, err := strconv.ParseUint(header.Get("X-Consul-LastContact"), 10, 64) 281 | if err != nil { 282 | return fmt.Errorf("Failed to parse X-Consul-LastContact: %v", err) 283 | } 284 | q.LastContact = time.Duration(last) * time.Millisecond 285 | 286 | // Parse the X-Consul-KnownLeader 287 | switch header.Get("X-Consul-KnownLeader") { 288 | case "true": 289 | q.KnownLeader = true 290 | default: 291 | q.KnownLeader = false 292 | } 293 | return nil 294 | } 295 | 296 | // decodeBody is used to JSON decode a body 297 | func decodeBody(resp *http.Response, out interface{}) error { 298 | dec := json.NewDecoder(resp.Body) 299 | return dec.Decode(out) 300 | } 301 | 302 | // encodeBody is used to encode a request body 303 | func encodeBody(obj interface{}) (io.Reader, error) { 304 | buf := bytes.NewBuffer(nil) 305 | enc := json.NewEncoder(buf) 306 | if err := enc.Encode(obj); err != nil { 307 | return nil, err 308 | } 309 | return buf, nil 310 | } 311 | 312 | // requireOK is used to wrap doRequest and check for a 200 313 | func requireOK(d time.Duration, resp *http.Response, e error) (time.Duration, *http.Response, error) { 314 | if e != nil { 315 | return d, resp, e 316 | } 317 | if resp.StatusCode != 200 { 318 | var buf bytes.Buffer 319 | io.Copy(&buf, resp.Body) 320 | return d, resp, fmt.Errorf("Unexpected response code: %d (%s)", resp.StatusCode, buf.Bytes()) 321 | } 322 | return d, resp, e 323 | } 324 | -------------------------------------------------------------------------------- /api_test.go: -------------------------------------------------------------------------------- 1 | package consulapi 2 | 3 | import ( 4 | crand "crypto/rand" 5 | "fmt" 6 | "net/http" 7 | "testing" 8 | "time" 9 | ) 10 | 11 | func makeClient(t *testing.T) *Client { 12 | conf := DefaultConfig() 13 | client, err := NewClient(conf) 14 | if err != nil { 15 | t.Fatalf("err: %v", err) 16 | } 17 | return client 18 | } 19 | 20 | func testKey() string { 21 | buf := make([]byte, 16) 22 | if _, err := crand.Read(buf); err != nil { 23 | panic(fmt.Errorf("Failed to read random bytes: %v", err)) 24 | } 25 | 26 | return fmt.Sprintf("%08x-%04x-%04x-%04x-%12x", 27 | buf[0:4], 28 | buf[4:6], 29 | buf[6:8], 30 | buf[8:10], 31 | buf[10:16]) 32 | } 33 | 34 | func TestSetQueryOptions(t *testing.T) { 35 | c := makeClient(t) 36 | r := c.newRequest("GET", "/v1/kv/foo") 37 | q := &QueryOptions{ 38 | Datacenter: "foo", 39 | AllowStale: true, 40 | RequireConsistent: true, 41 | WaitIndex: 1000, 42 | WaitTime: 100 * time.Second, 43 | Token: "12345", 44 | } 45 | r.setQueryOptions(q) 46 | 47 | if r.params.Get("dc") != "foo" { 48 | t.Fatalf("bad: %v", r.params) 49 | } 50 | if _, ok := r.params["stale"]; !ok { 51 | t.Fatalf("bad: %v", r.params) 52 | } 53 | if _, ok := r.params["consistent"]; !ok { 54 | t.Fatalf("bad: %v", r.params) 55 | } 56 | if r.params.Get("index") != "1000" { 57 | t.Fatalf("bad: %v", r.params) 58 | } 59 | if r.params.Get("wait") != "100000ms" { 60 | t.Fatalf("bad: %v", r.params) 61 | } 62 | if r.params.Get("token") != "12345" { 63 | t.Fatalf("bad: %v", r.params) 64 | } 65 | } 66 | 67 | func TestSetWriteOptions(t *testing.T) { 68 | c := makeClient(t) 69 | r := c.newRequest("GET", "/v1/kv/foo") 70 | q := &WriteOptions{ 71 | Datacenter: "foo", 72 | Token: "23456", 73 | } 74 | r.setWriteOptions(q) 75 | 76 | if r.params.Get("dc") != "foo" { 77 | t.Fatalf("bad: %v", r.params) 78 | } 79 | if r.params.Get("token") != "23456" { 80 | t.Fatalf("bad: %v", r.params) 81 | } 82 | } 83 | 84 | func TestRequestToHTTP(t *testing.T) { 85 | c := makeClient(t) 86 | r := c.newRequest("DELETE", "/v1/kv/foo") 87 | q := &QueryOptions{ 88 | Datacenter: "foo", 89 | } 90 | r.setQueryOptions(q) 91 | req, err := r.toHTTP() 92 | if err != nil { 93 | t.Fatalf("err: %v", err) 94 | } 95 | 96 | if req.Method != "DELETE" { 97 | t.Fatalf("bad: %v", req) 98 | } 99 | if req.URL.String() != "http://127.0.0.1:8500/v1/kv/foo?dc=foo" { 100 | t.Fatalf("bad: %v", req) 101 | } 102 | } 103 | 104 | func TestParseQueryMeta(t *testing.T) { 105 | resp := &http.Response{ 106 | Header: make(map[string][]string), 107 | } 108 | resp.Header.Set("X-Consul-Index", "12345") 109 | resp.Header.Set("X-Consul-LastContact", "80") 110 | resp.Header.Set("X-Consul-KnownLeader", "true") 111 | 112 | qm := &QueryMeta{} 113 | if err := parseQueryMeta(resp, qm); err != nil { 114 | t.Fatalf("err: %v", err) 115 | } 116 | 117 | if qm.LastIndex != 12345 { 118 | t.Fatalf("Bad: %v", qm) 119 | } 120 | if qm.LastContact != 80*time.Millisecond { 121 | t.Fatalf("Bad: %v", qm) 122 | } 123 | if !qm.KnownLeader { 124 | t.Fatalf("Bad: %v", qm) 125 | } 126 | } 127 | -------------------------------------------------------------------------------- /catalog.go: -------------------------------------------------------------------------------- 1 | package consulapi 2 | 3 | type Node struct { 4 | Node string 5 | Address string 6 | } 7 | 8 | type CatalogService struct { 9 | Node string 10 | Address string 11 | ServiceID string 12 | ServiceName string 13 | ServiceTags []string 14 | ServicePort int 15 | } 16 | 17 | type CatalogNode struct { 18 | Node *Node 19 | Services map[string]*AgentService 20 | } 21 | 22 | type CatalogRegistration struct { 23 | Node string 24 | Address string 25 | Datacenter string 26 | Service *AgentService 27 | Check *AgentCheck 28 | } 29 | 30 | type CatalogDeregistration struct { 31 | Node string 32 | Address string 33 | Datacenter string 34 | ServiceID string 35 | CheckID string 36 | } 37 | 38 | // Catalog can be used to query the Catalog endpoints 39 | type Catalog struct { 40 | c *Client 41 | } 42 | 43 | // Catalog returns a handle to the catalog endpoints 44 | func (c *Client) Catalog() *Catalog { 45 | return &Catalog{c} 46 | } 47 | 48 | func (c *Catalog) Register(reg *CatalogRegistration, q *WriteOptions) (*WriteMeta, error) { 49 | r := c.c.newRequest("PUT", "/v1/catalog/register") 50 | r.setWriteOptions(q) 51 | r.obj = reg 52 | rtt, resp, err := requireOK(c.c.doRequest(r)) 53 | if err != nil { 54 | return nil, err 55 | } 56 | resp.Body.Close() 57 | 58 | wm := &WriteMeta{} 59 | wm.RequestTime = rtt 60 | 61 | return wm, nil 62 | } 63 | 64 | func (c *Catalog) Deregister(dereg *CatalogDeregistration, q *WriteOptions) (*WriteMeta, error) { 65 | r := c.c.newRequest("PUT", "/v1/catalog/deregister") 66 | r.setWriteOptions(q) 67 | r.obj = dereg 68 | rtt, resp, err := requireOK(c.c.doRequest(r)) 69 | if err != nil { 70 | return nil, err 71 | } 72 | resp.Body.Close() 73 | 74 | wm := &WriteMeta{} 75 | wm.RequestTime = rtt 76 | 77 | return wm, nil 78 | } 79 | 80 | // Datacenters is used to query for all the known datacenters 81 | func (c *Catalog) Datacenters() ([]string, error) { 82 | r := c.c.newRequest("GET", "/v1/catalog/datacenters") 83 | _, resp, err := requireOK(c.c.doRequest(r)) 84 | if err != nil { 85 | return nil, err 86 | } 87 | defer resp.Body.Close() 88 | 89 | var out []string 90 | if err := decodeBody(resp, &out); err != nil { 91 | return nil, err 92 | } 93 | return out, nil 94 | } 95 | 96 | // Nodes is used to query all the known nodes 97 | func (c *Catalog) Nodes(q *QueryOptions) ([]*Node, *QueryMeta, error) { 98 | r := c.c.newRequest("GET", "/v1/catalog/nodes") 99 | r.setQueryOptions(q) 100 | rtt, resp, err := requireOK(c.c.doRequest(r)) 101 | if err != nil { 102 | return nil, nil, err 103 | } 104 | defer resp.Body.Close() 105 | 106 | qm := &QueryMeta{} 107 | parseQueryMeta(resp, qm) 108 | qm.RequestTime = rtt 109 | 110 | var out []*Node 111 | if err := decodeBody(resp, &out); err != nil { 112 | return nil, nil, err 113 | } 114 | return out, qm, nil 115 | } 116 | 117 | // Services is used to query for all known services 118 | func (c *Catalog) Services(q *QueryOptions) (map[string][]string, *QueryMeta, error) { 119 | r := c.c.newRequest("GET", "/v1/catalog/services") 120 | r.setQueryOptions(q) 121 | rtt, resp, err := requireOK(c.c.doRequest(r)) 122 | if err != nil { 123 | return nil, nil, err 124 | } 125 | defer resp.Body.Close() 126 | 127 | qm := &QueryMeta{} 128 | parseQueryMeta(resp, qm) 129 | qm.RequestTime = rtt 130 | 131 | var out map[string][]string 132 | if err := decodeBody(resp, &out); err != nil { 133 | return nil, nil, err 134 | } 135 | return out, qm, nil 136 | } 137 | 138 | // Service is used to query catalog entries for a given service 139 | func (c *Catalog) Service(service, tag string, q *QueryOptions) ([]*CatalogService, *QueryMeta, error) { 140 | r := c.c.newRequest("GET", "/v1/catalog/service/"+service) 141 | r.setQueryOptions(q) 142 | if tag != "" { 143 | r.params.Set("tag", tag) 144 | } 145 | rtt, resp, err := requireOK(c.c.doRequest(r)) 146 | if err != nil { 147 | return nil, nil, err 148 | } 149 | defer resp.Body.Close() 150 | 151 | qm := &QueryMeta{} 152 | parseQueryMeta(resp, qm) 153 | qm.RequestTime = rtt 154 | 155 | var out []*CatalogService 156 | if err := decodeBody(resp, &out); err != nil { 157 | return nil, nil, err 158 | } 159 | return out, qm, nil 160 | } 161 | 162 | // Node is used to query for service information about a single node 163 | func (c *Catalog) Node(node string, q *QueryOptions) (*CatalogNode, *QueryMeta, error) { 164 | r := c.c.newRequest("GET", "/v1/catalog/node/"+node) 165 | r.setQueryOptions(q) 166 | rtt, resp, err := requireOK(c.c.doRequest(r)) 167 | if err != nil { 168 | return nil, nil, err 169 | } 170 | defer resp.Body.Close() 171 | 172 | qm := &QueryMeta{} 173 | parseQueryMeta(resp, qm) 174 | qm.RequestTime = rtt 175 | 176 | var out *CatalogNode 177 | if err := decodeBody(resp, &out); err != nil { 178 | return nil, nil, err 179 | } 180 | return out, qm, nil 181 | } 182 | -------------------------------------------------------------------------------- /catalog_test.go: -------------------------------------------------------------------------------- 1 | package consulapi 2 | 3 | import ( 4 | "testing" 5 | ) 6 | 7 | func TestCatalog_Datacenters(t *testing.T) { 8 | c := makeClient(t) 9 | catalog := c.Catalog() 10 | 11 | datacenters, err := catalog.Datacenters() 12 | if err != nil { 13 | t.Fatalf("err: %v", err) 14 | } 15 | 16 | if len(datacenters) == 0 { 17 | t.Fatalf("Bad: %v", datacenters) 18 | } 19 | } 20 | 21 | func TestCatalog_Nodes(t *testing.T) { 22 | c := makeClient(t) 23 | catalog := c.Catalog() 24 | 25 | nodes, meta, err := catalog.Nodes(nil) 26 | if err != nil { 27 | t.Fatalf("err: %v", err) 28 | } 29 | 30 | if meta.LastIndex == 0 { 31 | t.Fatalf("Bad: %v", meta) 32 | } 33 | 34 | if len(nodes) == 0 { 35 | t.Fatalf("Bad: %v", nodes) 36 | } 37 | } 38 | 39 | func TestCatalog_Services(t *testing.T) { 40 | c := makeClient(t) 41 | catalog := c.Catalog() 42 | 43 | services, meta, err := catalog.Services(nil) 44 | if err != nil { 45 | t.Fatalf("err: %v", err) 46 | } 47 | 48 | if meta.LastIndex == 0 { 49 | t.Fatalf("Bad: %v", meta) 50 | } 51 | 52 | if len(services) == 0 { 53 | t.Fatalf("Bad: %v", services) 54 | } 55 | } 56 | 57 | func TestCatalog_Service(t *testing.T) { 58 | c := makeClient(t) 59 | catalog := c.Catalog() 60 | 61 | services, meta, err := catalog.Service("consul", "", nil) 62 | if err != nil { 63 | t.Fatalf("err: %v", err) 64 | } 65 | 66 | if meta.LastIndex == 0 { 67 | t.Fatalf("Bad: %v", meta) 68 | } 69 | 70 | if len(services) == 0 { 71 | t.Fatalf("Bad: %v", services) 72 | } 73 | } 74 | 75 | func TestCatalog_Node(t *testing.T) { 76 | c := makeClient(t) 77 | catalog := c.Catalog() 78 | 79 | name, _ := c.Agent().NodeName() 80 | info, meta, err := catalog.Node(name, nil) 81 | if err != nil { 82 | t.Fatalf("err: %v", err) 83 | } 84 | 85 | if meta.LastIndex == 0 { 86 | t.Fatalf("Bad: %v", meta) 87 | } 88 | if len(info.Services) == 0 { 89 | t.Fatalf("Bad: %v", info) 90 | } 91 | } 92 | 93 | func TestCatalog_Registration(t *testing.T) { 94 | c := makeClient(t) 95 | catalog := c.Catalog() 96 | 97 | service := &AgentService{ 98 | ID: "redis1", 99 | Service: "redis", 100 | Tags: []string{"master", "v1"}, 101 | Port: 8000, 102 | } 103 | 104 | check := &AgentCheck{ 105 | Node: "foobar", 106 | CheckID: "service:redis1", 107 | Name: "Redis health check", 108 | Notes: "Script based health check", 109 | Status: "passing", 110 | ServiceID: "redis1", 111 | } 112 | 113 | reg := &CatalogRegistration{ 114 | Datacenter: "dc1", 115 | Node: "foobar", 116 | Address: "192.168.10.10", 117 | Service: service, 118 | Check: check, 119 | } 120 | 121 | _, err := catalog.Register(reg, nil) 122 | 123 | if err != nil { 124 | t.Fatalf("err: %v", err) 125 | } 126 | 127 | node, _, err := catalog.Node("foobar", nil) 128 | 129 | if err != nil { 130 | t.Fatalf("err: %v", err) 131 | } 132 | 133 | if _, ok := node.Services["redis1"]; !ok { 134 | t.Fatalf("missing service: redis1") 135 | } 136 | 137 | health, _, err := c.Health().Node("foobar", nil) 138 | 139 | if err != nil { 140 | t.Fatalf("err: %v", err) 141 | } 142 | 143 | if health[0].CheckID != "service:redis1" { 144 | t.Fatalf("missing checkid service:redis1") 145 | } 146 | } 147 | 148 | func TestCatalog_Deregistration(t *testing.T) { 149 | c := makeClient(t) 150 | catalog := c.Catalog() 151 | 152 | dereg := &CatalogDeregistration{ 153 | Datacenter: "dc1", 154 | Node: "foobar", 155 | Address: "192.168.10.10", 156 | ServiceID: "redis1", 157 | } 158 | 159 | _, err := catalog.Deregister(dereg, nil) 160 | 161 | if err != nil { 162 | t.Fatalf("err: %v", err) 163 | } 164 | 165 | node, _, err := catalog.Node("foobar", nil) 166 | 167 | if err != nil { 168 | t.Fatalf("err: %v", err) 169 | } 170 | 171 | if _, ok := node.Services["redis1"]; ok { 172 | t.Fatalf("ServiceID:redis1 is not deregistered") 173 | } 174 | 175 | dereg = &CatalogDeregistration{ 176 | Datacenter: "dc1", 177 | Node: "foobar", 178 | Address: "192.168.10.10", 179 | CheckID: "service:redis1", 180 | } 181 | 182 | _, err = catalog.Deregister(dereg, nil) 183 | 184 | if err != nil { 185 | t.Fatalf("err: %v", err) 186 | } 187 | 188 | health, _, err := c.Health().Node("foobar", nil) 189 | 190 | if err != nil { 191 | t.Fatalf("err: %v", err) 192 | } 193 | 194 | if len(health) != 0 { 195 | t.Fatalf("CheckID:service:redis1 is not deregistered") 196 | } 197 | 198 | dereg = &CatalogDeregistration{ 199 | Datacenter: "dc1", 200 | Node: "foobar", 201 | Address: "192.168.10.10", 202 | } 203 | 204 | _, err = catalog.Deregister(dereg, nil) 205 | 206 | if err != nil { 207 | t.Fatalf("err: %v", err) 208 | } 209 | 210 | node, _, err = catalog.Node("foobar", nil) 211 | 212 | if err != nil { 213 | t.Fatalf("err: %v", err) 214 | } 215 | 216 | if node != nil { 217 | t.Fatalf("node is not deregistered: %v", node) 218 | } 219 | } 220 | -------------------------------------------------------------------------------- /event.go: -------------------------------------------------------------------------------- 1 | package consulapi 2 | 3 | import ( 4 | "bytes" 5 | "strconv" 6 | ) 7 | 8 | // Event can be used to query the Event endpoints 9 | type Event struct { 10 | c *Client 11 | } 12 | 13 | // UserEvent represents an event that was fired by the user 14 | type UserEvent struct { 15 | ID string 16 | Name string 17 | Payload []byte 18 | NodeFilter string 19 | ServiceFilter string 20 | TagFilter string 21 | Version int 22 | LTime uint64 23 | } 24 | 25 | // Event returns a handle to the event endpoints 26 | func (c *Client) Event() *Event { 27 | return &Event{c} 28 | } 29 | 30 | // Fire is used to fire a new user event. Only the Name, Payload and Filters 31 | // are respected. This returns the ID or an associated error. Cross DC requests 32 | // are supported. 33 | func (e *Event) Fire(params *UserEvent, q *WriteOptions) (string, *WriteMeta, error) { 34 | r := e.c.newRequest("PUT", "/v1/event/fire/"+params.Name) 35 | r.setWriteOptions(q) 36 | if params.NodeFilter != "" { 37 | r.params.Set("node", params.NodeFilter) 38 | } 39 | if params.ServiceFilter != "" { 40 | r.params.Set("service", params.ServiceFilter) 41 | } 42 | if params.TagFilter != "" { 43 | r.params.Set("tag", params.TagFilter) 44 | } 45 | if params.Payload != nil { 46 | r.body = bytes.NewReader(params.Payload) 47 | } 48 | 49 | rtt, resp, err := requireOK(e.c.doRequest(r)) 50 | if err != nil { 51 | return "", nil, err 52 | } 53 | defer resp.Body.Close() 54 | 55 | wm := &WriteMeta{RequestTime: rtt} 56 | var out UserEvent 57 | if err := decodeBody(resp, &out); err != nil { 58 | return "", nil, err 59 | } 60 | return out.ID, wm, nil 61 | } 62 | 63 | // List is used to get the most recent events an agent has received. 64 | // This list can be optionally filtered by the name. This endpoint supports 65 | // quasi-blocking queries. The index is not monotonic, nor does it provide provide 66 | // LastContact or KnownLeader. 67 | func (e *Event) List(name string, q *QueryOptions) ([]*UserEvent, *QueryMeta, error) { 68 | r := e.c.newRequest("GET", "/v1/event/list") 69 | r.setQueryOptions(q) 70 | if name != "" { 71 | r.params.Set("name", name) 72 | } 73 | rtt, resp, err := requireOK(e.c.doRequest(r)) 74 | if err != nil { 75 | return nil, nil, err 76 | } 77 | defer resp.Body.Close() 78 | 79 | qm := &QueryMeta{} 80 | parseQueryMeta(resp, qm) 81 | qm.RequestTime = rtt 82 | 83 | var entries []*UserEvent 84 | if err := decodeBody(resp, &entries); err != nil { 85 | return nil, nil, err 86 | } 87 | return entries, qm, nil 88 | } 89 | 90 | // IDToIndex is a bit of a hack. This simulates the index generation to 91 | // convert an event ID into a WaitIndex. 92 | func (e *Event) IDToIndex(uuid string) uint64 { 93 | lower := uuid[0:8] + uuid[9:13] + uuid[14:18] 94 | upper := uuid[19:23] + uuid[24:36] 95 | lowVal, err := strconv.ParseUint(lower, 16, 64) 96 | if err != nil { 97 | panic("Failed to convert " + lower) 98 | } 99 | highVal, err := strconv.ParseUint(upper, 16, 64) 100 | if err != nil { 101 | panic("Failed to convert " + upper) 102 | } 103 | return lowVal ^ highVal 104 | } 105 | -------------------------------------------------------------------------------- /event_test.go: -------------------------------------------------------------------------------- 1 | package consulapi 2 | 3 | import ( 4 | "testing" 5 | ) 6 | 7 | func TestEvent_FireList(t *testing.T) { 8 | c := makeClient(t) 9 | event := c.Event() 10 | 11 | params := &UserEvent{Name: "foo"} 12 | id, meta, err := event.Fire(params, nil) 13 | if err != nil { 14 | t.Fatalf("err: %v", err) 15 | } 16 | 17 | if meta.RequestTime == 0 { 18 | t.Fatalf("bad: %v", meta) 19 | } 20 | 21 | if id == "" { 22 | t.Fatalf("invalid: %v", id) 23 | } 24 | 25 | events, qm, err := event.List("", nil) 26 | if err != nil { 27 | t.Fatalf("err: %v", err) 28 | } 29 | 30 | if qm.LastIndex != event.IDToIndex(id) { 31 | t.Fatalf("Bad: %#v", qm) 32 | } 33 | 34 | if events[len(events)-1].ID != id { 35 | t.Fatalf("bad: %#v", events) 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /health.go: -------------------------------------------------------------------------------- 1 | package consulapi 2 | 3 | import ( 4 | "fmt" 5 | ) 6 | 7 | // HealthCheck is used to represent a single check 8 | type HealthCheck struct { 9 | Node string 10 | CheckID string 11 | Name string 12 | Status string 13 | Notes string 14 | Output string 15 | ServiceID string 16 | ServiceName string 17 | } 18 | 19 | // ServiceEntry is used for the health service endpoint 20 | type ServiceEntry struct { 21 | Node *Node 22 | Service *AgentService 23 | Checks []*HealthCheck 24 | } 25 | 26 | // Health can be used to query the Health endpoints 27 | type Health struct { 28 | c *Client 29 | } 30 | 31 | // Health returns a handle to the health endpoints 32 | func (c *Client) Health() *Health { 33 | return &Health{c} 34 | } 35 | 36 | // Node is used to query for checks belonging to a given node 37 | func (h *Health) Node(node string, q *QueryOptions) ([]*HealthCheck, *QueryMeta, error) { 38 | r := h.c.newRequest("GET", "/v1/health/node/"+node) 39 | r.setQueryOptions(q) 40 | rtt, resp, err := requireOK(h.c.doRequest(r)) 41 | if err != nil { 42 | return nil, nil, err 43 | } 44 | defer resp.Body.Close() 45 | 46 | qm := &QueryMeta{} 47 | parseQueryMeta(resp, qm) 48 | qm.RequestTime = rtt 49 | 50 | var out []*HealthCheck 51 | if err := decodeBody(resp, &out); err != nil { 52 | return nil, nil, err 53 | } 54 | return out, qm, nil 55 | } 56 | 57 | // Checks is used to return the checks associated with a service 58 | func (h *Health) Checks(service string, q *QueryOptions) ([]*HealthCheck, *QueryMeta, error) { 59 | r := h.c.newRequest("GET", "/v1/health/checks/"+service) 60 | r.setQueryOptions(q) 61 | rtt, resp, err := requireOK(h.c.doRequest(r)) 62 | if err != nil { 63 | return nil, nil, err 64 | } 65 | defer resp.Body.Close() 66 | 67 | qm := &QueryMeta{} 68 | parseQueryMeta(resp, qm) 69 | qm.RequestTime = rtt 70 | 71 | var out []*HealthCheck 72 | if err := decodeBody(resp, &out); err != nil { 73 | return nil, nil, err 74 | } 75 | return out, qm, nil 76 | } 77 | 78 | // Service is used to query health information along with service info 79 | // for a given service. It can optionally do server-side filtering on a tag 80 | // or nodes with passing health checks only. 81 | func (h *Health) Service(service, tag string, passingOnly bool, q *QueryOptions) ([]*ServiceEntry, *QueryMeta, error) { 82 | r := h.c.newRequest("GET", "/v1/health/service/"+service) 83 | r.setQueryOptions(q) 84 | if tag != "" { 85 | r.params.Set("tag", tag) 86 | } 87 | if passingOnly { 88 | r.params.Set("passing", "1") 89 | } 90 | rtt, resp, err := requireOK(h.c.doRequest(r)) 91 | if err != nil { 92 | return nil, nil, err 93 | } 94 | defer resp.Body.Close() 95 | 96 | qm := &QueryMeta{} 97 | parseQueryMeta(resp, qm) 98 | qm.RequestTime = rtt 99 | 100 | var out []*ServiceEntry 101 | if err := decodeBody(resp, &out); err != nil { 102 | return nil, nil, err 103 | } 104 | return out, qm, nil 105 | } 106 | 107 | // State is used to retrieve all the checks in a given state. 108 | // The wildcard "any" state can also be used for all checks. 109 | func (h *Health) State(state string, q *QueryOptions) ([]*HealthCheck, *QueryMeta, error) { 110 | switch state { 111 | case "any": 112 | case "warning": 113 | case "critical": 114 | case "passing": 115 | case "unknown": 116 | default: 117 | return nil, nil, fmt.Errorf("Unsupported state: %v", state) 118 | } 119 | r := h.c.newRequest("GET", "/v1/health/state/"+state) 120 | r.setQueryOptions(q) 121 | rtt, resp, err := requireOK(h.c.doRequest(r)) 122 | if err != nil { 123 | return nil, nil, err 124 | } 125 | defer resp.Body.Close() 126 | 127 | qm := &QueryMeta{} 128 | parseQueryMeta(resp, qm) 129 | qm.RequestTime = rtt 130 | 131 | var out []*HealthCheck 132 | if err := decodeBody(resp, &out); err != nil { 133 | return nil, nil, err 134 | } 135 | return out, qm, nil 136 | } 137 | -------------------------------------------------------------------------------- /health_test.go: -------------------------------------------------------------------------------- 1 | package consulapi 2 | 3 | import ( 4 | "testing" 5 | "time" 6 | ) 7 | 8 | func TestHealth_Node(t *testing.T) { 9 | c := makeClient(t) 10 | agent := c.Agent() 11 | health := c.Health() 12 | 13 | info, err := agent.Self() 14 | if err != nil { 15 | t.Fatalf("err: %v", err) 16 | } 17 | name := info["Config"]["NodeName"].(string) 18 | 19 | checks, meta, err := health.Node(name, nil) 20 | if err != nil { 21 | t.Fatalf("err: %v", err) 22 | } 23 | 24 | if meta.LastIndex == 0 { 25 | t.Fatalf("bad: %v", meta) 26 | } 27 | if len(checks) == 0 { 28 | t.Fatalf("Bad: %v", checks) 29 | } 30 | } 31 | 32 | func TestHealth_Checks(t *testing.T) { 33 | c := makeClient(t) 34 | agent := c.Agent() 35 | health := c.Health() 36 | 37 | // Make a service with a check 38 | reg := &AgentServiceRegistration{ 39 | Name: "foo", 40 | Check: &AgentServiceCheck{ 41 | TTL: "15s", 42 | }, 43 | } 44 | if err := agent.ServiceRegister(reg); err != nil { 45 | t.Fatalf("err: %v", err) 46 | } 47 | defer agent.ServiceDeregister("foo") 48 | 49 | // Wait for the register... 50 | time.Sleep(20 * time.Millisecond) 51 | 52 | checks, meta, err := health.Checks("foo", nil) 53 | if err != nil { 54 | t.Fatalf("err: %v", err) 55 | } 56 | 57 | if meta.LastIndex == 0 { 58 | t.Fatalf("bad: %v", meta) 59 | } 60 | if len(checks) == 0 { 61 | t.Fatalf("Bad: %v", checks) 62 | } 63 | } 64 | 65 | func TestHealth_Service(t *testing.T) { 66 | c := makeClient(t) 67 | health := c.Health() 68 | 69 | // consul service should always exist... 70 | checks, meta, err := health.Service("consul", "", true, nil) 71 | if err != nil { 72 | t.Fatalf("err: %v", err) 73 | } 74 | 75 | if meta.LastIndex == 0 { 76 | t.Fatalf("bad: %v", meta) 77 | } 78 | if len(checks) == 0 { 79 | t.Fatalf("Bad: %v", checks) 80 | } 81 | } 82 | 83 | func TestHealth_State(t *testing.T) { 84 | c := makeClient(t) 85 | health := c.Health() 86 | 87 | checks, meta, err := health.State("any", nil) 88 | if err != nil { 89 | t.Fatalf("err: %v", err) 90 | } 91 | 92 | if meta.LastIndex == 0 { 93 | t.Fatalf("bad: %v", meta) 94 | } 95 | if len(checks) == 0 { 96 | t.Fatalf("Bad: %v", checks) 97 | } 98 | } 99 | -------------------------------------------------------------------------------- /kv.go: -------------------------------------------------------------------------------- 1 | package consulapi 2 | 3 | import ( 4 | "bytes" 5 | "fmt" 6 | "io" 7 | "net/http" 8 | "strconv" 9 | "strings" 10 | ) 11 | 12 | // KVPair is used to represent a single K/V entry 13 | type KVPair struct { 14 | Key string 15 | CreateIndex uint64 16 | ModifyIndex uint64 17 | LockIndex uint64 18 | Flags uint64 19 | Value []byte 20 | Session string 21 | } 22 | 23 | // KVPairs is a list of KVPair objects 24 | type KVPairs []*KVPair 25 | 26 | // KV is used to manipulate the K/V API 27 | type KV struct { 28 | c *Client 29 | } 30 | 31 | // KV is used to return a handle to the K/V apis 32 | func (c *Client) KV() *KV { 33 | return &KV{c} 34 | } 35 | 36 | // Get is used to lookup a single key 37 | func (k *KV) Get(key string, q *QueryOptions) (*KVPair, *QueryMeta, error) { 38 | resp, qm, err := k.getInternal(key, nil, q) 39 | if err != nil { 40 | return nil, nil, err 41 | } 42 | if resp == nil { 43 | return nil, qm, nil 44 | } 45 | defer resp.Body.Close() 46 | 47 | var entries []*KVPair 48 | if err := decodeBody(resp, &entries); err != nil { 49 | return nil, nil, err 50 | } 51 | if len(entries) > 0 { 52 | return entries[0], qm, nil 53 | } 54 | return nil, qm, nil 55 | } 56 | 57 | // List is used to lookup all keys under a prefix 58 | func (k *KV) List(prefix string, q *QueryOptions) (KVPairs, *QueryMeta, error) { 59 | resp, qm, err := k.getInternal(prefix, map[string]string{"recurse": ""}, q) 60 | if err != nil { 61 | return nil, nil, err 62 | } 63 | if resp == nil { 64 | return nil, qm, nil 65 | } 66 | defer resp.Body.Close() 67 | 68 | var entries []*KVPair 69 | if err := decodeBody(resp, &entries); err != nil { 70 | return nil, nil, err 71 | } 72 | return entries, qm, nil 73 | } 74 | 75 | // Keys is used to list all the keys under a prefix. Optionally, 76 | // a separator can be used to limit the responses. 77 | func (k *KV) Keys(prefix, separator string, q *QueryOptions) ([]string, *QueryMeta, error) { 78 | params := map[string]string{"keys": ""} 79 | if separator != "" { 80 | params["separator"] = separator 81 | } 82 | resp, qm, err := k.getInternal(prefix, params, q) 83 | if err != nil { 84 | return nil, nil, err 85 | } 86 | if resp == nil { 87 | return nil, qm, nil 88 | } 89 | defer resp.Body.Close() 90 | 91 | var entries []string 92 | if err := decodeBody(resp, &entries); err != nil { 93 | return nil, nil, err 94 | } 95 | return entries, qm, nil 96 | } 97 | 98 | func (k *KV) getInternal(key string, params map[string]string, q *QueryOptions) (*http.Response, *QueryMeta, error) { 99 | r := k.c.newRequest("GET", "/v1/kv/"+key) 100 | r.setQueryOptions(q) 101 | for param, val := range params { 102 | r.params.Set(param, val) 103 | } 104 | rtt, resp, err := k.c.doRequest(r) 105 | if err != nil { 106 | return nil, nil, err 107 | } 108 | 109 | qm := &QueryMeta{} 110 | parseQueryMeta(resp, qm) 111 | qm.RequestTime = rtt 112 | 113 | if resp.StatusCode == 404 { 114 | resp.Body.Close() 115 | return nil, qm, nil 116 | } else if resp.StatusCode != 200 { 117 | resp.Body.Close() 118 | return nil, nil, fmt.Errorf("Unexpected response code: %d", resp.StatusCode) 119 | } 120 | return resp, qm, nil 121 | } 122 | 123 | // Put is used to write a new value. Only the 124 | // Key, Flags and Value is respected. 125 | func (k *KV) Put(p *KVPair, q *WriteOptions) (*WriteMeta, error) { 126 | params := make(map[string]string, 1) 127 | if p.Flags != 0 { 128 | params["flags"] = strconv.FormatUint(p.Flags, 10) 129 | } 130 | _, wm, err := k.put(p.Key, params, p.Value, q) 131 | return wm, err 132 | } 133 | 134 | // CAS is used for a Check-And-Set operation. The Key, 135 | // ModifyIndex, Flags and Value are respected. Returns true 136 | // on success or false on failures. 137 | func (k *KV) CAS(p *KVPair, q *WriteOptions) (bool, *WriteMeta, error) { 138 | params := make(map[string]string, 2) 139 | if p.Flags != 0 { 140 | params["flags"] = strconv.FormatUint(p.Flags, 10) 141 | } 142 | params["cas"] = strconv.FormatUint(p.ModifyIndex, 10) 143 | return k.put(p.Key, params, p.Value, q) 144 | } 145 | 146 | // Acquire is used for a lock acquisiiton operation. The Key, 147 | // Flags, Value and Session are respected. Returns true 148 | // on success or false on failures. 149 | func (k *KV) Acquire(p *KVPair, q *WriteOptions) (bool, *WriteMeta, error) { 150 | params := make(map[string]string, 2) 151 | if p.Flags != 0 { 152 | params["flags"] = strconv.FormatUint(p.Flags, 10) 153 | } 154 | params["acquire"] = p.Session 155 | return k.put(p.Key, params, p.Value, q) 156 | } 157 | 158 | // Release is used for a lock release operation. The Key, 159 | // Flags, Value and Session are respected. Returns true 160 | // on success or false on failures. 161 | func (k *KV) Release(p *KVPair, q *WriteOptions) (bool, *WriteMeta, error) { 162 | params := make(map[string]string, 2) 163 | if p.Flags != 0 { 164 | params["flags"] = strconv.FormatUint(p.Flags, 10) 165 | } 166 | params["release"] = p.Session 167 | return k.put(p.Key, params, p.Value, q) 168 | } 169 | 170 | func (k *KV) put(key string, params map[string]string, body []byte, q *WriteOptions) (bool, *WriteMeta, error) { 171 | r := k.c.newRequest("PUT", "/v1/kv/"+key) 172 | r.setWriteOptions(q) 173 | for param, val := range params { 174 | r.params.Set(param, val) 175 | } 176 | r.body = bytes.NewReader(body) 177 | rtt, resp, err := requireOK(k.c.doRequest(r)) 178 | if err != nil { 179 | return false, nil, err 180 | } 181 | defer resp.Body.Close() 182 | 183 | qm := &WriteMeta{} 184 | qm.RequestTime = rtt 185 | 186 | var buf bytes.Buffer 187 | if _, err := io.Copy(&buf, resp.Body); err != nil { 188 | return false, nil, fmt.Errorf("Failed to read response: %v", err) 189 | } 190 | res := strings.Contains(string(buf.Bytes()), "true") 191 | return res, qm, nil 192 | } 193 | 194 | // Delete is used to delete a single key 195 | func (k *KV) Delete(key string, w *WriteOptions) (*WriteMeta, error) { 196 | return k.deleteInternal(key, nil, w) 197 | } 198 | 199 | // DeleteTree is used to delete all keys under a prefix 200 | func (k *KV) DeleteTree(prefix string, w *WriteOptions) (*WriteMeta, error) { 201 | return k.deleteInternal(prefix, []string{"recurse"}, w) 202 | } 203 | 204 | func (k *KV) deleteInternal(key string, params []string, q *WriteOptions) (*WriteMeta, error) { 205 | r := k.c.newRequest("DELETE", "/v1/kv/"+key) 206 | r.setWriteOptions(q) 207 | for _, param := range params { 208 | r.params.Set(param, "") 209 | } 210 | rtt, resp, err := requireOK(k.c.doRequest(r)) 211 | if err != nil { 212 | return nil, err 213 | } 214 | resp.Body.Close() 215 | 216 | qm := &WriteMeta{} 217 | qm.RequestTime = rtt 218 | return qm, nil 219 | } 220 | -------------------------------------------------------------------------------- /kv_test.go: -------------------------------------------------------------------------------- 1 | package consulapi 2 | 3 | import ( 4 | "bytes" 5 | "path" 6 | "testing" 7 | "time" 8 | ) 9 | 10 | func TestClientPutGetDelete(t *testing.T) { 11 | c := makeClient(t) 12 | kv := c.KV() 13 | 14 | // Get a get without a key 15 | key := testKey() 16 | pair, _, err := kv.Get(key, nil) 17 | if err != nil { 18 | t.Fatalf("err: %v", err) 19 | } 20 | if pair != nil { 21 | t.Fatalf("unexpected value: %#v", pair) 22 | } 23 | 24 | // Put the key 25 | value := []byte("test") 26 | p := &KVPair{Key: key, Flags: 42, Value: value} 27 | if _, err := kv.Put(p, nil); err != nil { 28 | t.Fatalf("err: %v", err) 29 | } 30 | 31 | // Get should work 32 | pair, meta, err := kv.Get(key, nil) 33 | if err != nil { 34 | t.Fatalf("err: %v", err) 35 | } 36 | if pair == nil { 37 | t.Fatalf("expected value: %#v", pair) 38 | } 39 | if !bytes.Equal(pair.Value, value) { 40 | t.Fatalf("unexpected value: %#v", pair) 41 | } 42 | if pair.Flags != 42 { 43 | t.Fatalf("unexpected value: %#v", pair) 44 | } 45 | if meta.LastIndex == 0 { 46 | t.Fatalf("unexpected value: %#v", meta) 47 | } 48 | 49 | // Delete 50 | if _, err := kv.Delete(key, nil); err != nil { 51 | t.Fatalf("err: %v", err) 52 | } 53 | 54 | // Get should fail 55 | pair, _, err = kv.Get(key, nil) 56 | if err != nil { 57 | t.Fatalf("err: %v", err) 58 | } 59 | if pair != nil { 60 | t.Fatalf("unexpected value: %#v", pair) 61 | } 62 | } 63 | 64 | func TestClient_List_DeleteRecurse(t *testing.T) { 65 | c := makeClient(t) 66 | kv := c.KV() 67 | 68 | // Generate some test keys 69 | prefix := testKey() 70 | var keys []string 71 | for i := 0; i < 100; i++ { 72 | keys = append(keys, path.Join(prefix, testKey())) 73 | } 74 | 75 | // Set values 76 | value := []byte("test") 77 | for _, key := range keys { 78 | p := &KVPair{Key: key, Value: value} 79 | if _, err := kv.Put(p, nil); err != nil { 80 | t.Fatalf("err: %v", err) 81 | } 82 | } 83 | 84 | // List the values 85 | pairs, meta, err := kv.List(prefix, nil) 86 | if err != nil { 87 | t.Fatalf("err: %v", err) 88 | } 89 | if len(pairs) != len(keys) { 90 | t.Fatalf("got %d keys", len(pairs)) 91 | } 92 | for _, pair := range pairs { 93 | if !bytes.Equal(pair.Value, value) { 94 | t.Fatalf("unexpected value: %#v", pair) 95 | } 96 | } 97 | if meta.LastIndex == 0 { 98 | t.Fatalf("unexpected value: %#v", meta) 99 | } 100 | 101 | // Delete all 102 | if _, err := kv.DeleteTree(prefix, nil); err != nil { 103 | t.Fatalf("err: %v", err) 104 | } 105 | 106 | // List the values 107 | pairs, _, err = kv.List(prefix, nil) 108 | if err != nil { 109 | t.Fatalf("err: %v", err) 110 | } 111 | if len(pairs) != 0 { 112 | t.Fatalf("got %d keys", len(pairs)) 113 | } 114 | } 115 | 116 | func TestClient_CAS(t *testing.T) { 117 | c := makeClient(t) 118 | kv := c.KV() 119 | 120 | // Put the key 121 | key := testKey() 122 | value := []byte("test") 123 | p := &KVPair{Key: key, Value: value} 124 | if work, _, err := kv.CAS(p, nil); err != nil { 125 | t.Fatalf("err: %v", err) 126 | } else if !work { 127 | t.Fatalf("CAS failure") 128 | } 129 | 130 | // Get should work 131 | pair, meta, err := kv.Get(key, nil) 132 | if err != nil { 133 | t.Fatalf("err: %v", err) 134 | } 135 | if pair == nil { 136 | t.Fatalf("expected value: %#v", pair) 137 | } 138 | if meta.LastIndex == 0 { 139 | t.Fatalf("unexpected value: %#v", meta) 140 | } 141 | 142 | // CAS update with bad index 143 | newVal := []byte("foo") 144 | p.Value = newVal 145 | p.ModifyIndex = 1 146 | if work, _, err := kv.CAS(p, nil); err != nil { 147 | t.Fatalf("err: %v", err) 148 | } else if work { 149 | t.Fatalf("unexpected CAS") 150 | } 151 | 152 | // CAS update with valid index 153 | p.ModifyIndex = meta.LastIndex 154 | if work, _, err := kv.CAS(p, nil); err != nil { 155 | t.Fatalf("err: %v", err) 156 | } else if !work { 157 | t.Fatalf("unexpected CAS failure") 158 | } 159 | } 160 | 161 | func TestClient_WatchGet(t *testing.T) { 162 | c := makeClient(t) 163 | kv := c.KV() 164 | 165 | // Get a get without a key 166 | key := testKey() 167 | pair, meta, err := kv.Get(key, nil) 168 | if err != nil { 169 | t.Fatalf("err: %v", err) 170 | } 171 | if pair != nil { 172 | t.Fatalf("unexpected value: %#v", pair) 173 | } 174 | if meta.LastIndex == 0 { 175 | t.Fatalf("unexpected value: %#v", meta) 176 | } 177 | 178 | // Put the key 179 | value := []byte("test") 180 | go func() { 181 | c := makeClient(t) 182 | kv := c.KV() 183 | 184 | time.Sleep(100 * time.Millisecond) 185 | p := &KVPair{Key: key, Flags: 42, Value: value} 186 | if _, err := kv.Put(p, nil); err != nil { 187 | t.Fatalf("err: %v", err) 188 | } 189 | }() 190 | 191 | // Get should work 192 | options := &QueryOptions{WaitIndex: meta.LastIndex} 193 | pair, meta2, err := kv.Get(key, options) 194 | if err != nil { 195 | t.Fatalf("err: %v", err) 196 | } 197 | if pair == nil { 198 | t.Fatalf("expected value: %#v", pair) 199 | } 200 | if !bytes.Equal(pair.Value, value) { 201 | t.Fatalf("unexpected value: %#v", pair) 202 | } 203 | if pair.Flags != 42 { 204 | t.Fatalf("unexpected value: %#v", pair) 205 | } 206 | if meta2.LastIndex <= meta.LastIndex { 207 | t.Fatalf("unexpected value: %#v", meta2) 208 | } 209 | } 210 | 211 | func TestClient_WatchList(t *testing.T) { 212 | c := makeClient(t) 213 | kv := c.KV() 214 | 215 | // Get a get without a key 216 | prefix := testKey() 217 | key := path.Join(prefix, testKey()) 218 | pairs, meta, err := kv.List(prefix, nil) 219 | if err != nil { 220 | t.Fatalf("err: %v", err) 221 | } 222 | if len(pairs) != 0 { 223 | t.Fatalf("unexpected value: %#v", pairs) 224 | } 225 | if meta.LastIndex == 0 { 226 | t.Fatalf("unexpected value: %#v", meta) 227 | } 228 | 229 | // Put the key 230 | value := []byte("test") 231 | go func() { 232 | c := makeClient(t) 233 | kv := c.KV() 234 | 235 | time.Sleep(100 * time.Millisecond) 236 | p := &KVPair{Key: key, Flags: 42, Value: value} 237 | if _, err := kv.Put(p, nil); err != nil { 238 | t.Fatalf("err: %v", err) 239 | } 240 | }() 241 | 242 | // Get should work 243 | options := &QueryOptions{WaitIndex: meta.LastIndex} 244 | pairs, meta2, err := kv.List(prefix, options) 245 | if err != nil { 246 | t.Fatalf("err: %v", err) 247 | } 248 | if len(pairs) != 1 { 249 | t.Fatalf("expected value: %#v", pairs) 250 | } 251 | if !bytes.Equal(pairs[0].Value, value) { 252 | t.Fatalf("unexpected value: %#v", pairs) 253 | } 254 | if pairs[0].Flags != 42 { 255 | t.Fatalf("unexpected value: %#v", pairs) 256 | } 257 | if meta2.LastIndex <= meta.LastIndex { 258 | t.Fatalf("unexpected value: %#v", meta2) 259 | } 260 | 261 | } 262 | 263 | func TestClient_Keys_DeleteRecurse(t *testing.T) { 264 | c := makeClient(t) 265 | kv := c.KV() 266 | 267 | // Generate some test keys 268 | prefix := testKey() 269 | var keys []string 270 | for i := 0; i < 100; i++ { 271 | keys = append(keys, path.Join(prefix, testKey())) 272 | } 273 | 274 | // Set values 275 | value := []byte("test") 276 | for _, key := range keys { 277 | p := &KVPair{Key: key, Value: value} 278 | if _, err := kv.Put(p, nil); err != nil { 279 | t.Fatalf("err: %v", err) 280 | } 281 | } 282 | 283 | // List the values 284 | out, meta, err := kv.Keys(prefix, "", nil) 285 | if err != nil { 286 | t.Fatalf("err: %v", err) 287 | } 288 | if len(out) != len(keys) { 289 | t.Fatalf("got %d keys", len(out)) 290 | } 291 | if meta.LastIndex == 0 { 292 | t.Fatalf("unexpected value: %#v", meta) 293 | } 294 | 295 | // Delete all 296 | if _, err := kv.DeleteTree(prefix, nil); err != nil { 297 | t.Fatalf("err: %v", err) 298 | } 299 | 300 | // List the values 301 | out, _, err = kv.Keys(prefix, "", nil) 302 | if err != nil { 303 | t.Fatalf("err: %v", err) 304 | } 305 | if len(out) != 0 { 306 | t.Fatalf("got %d keys", len(out)) 307 | } 308 | } 309 | 310 | func TestClient_AcquireRelease(t *testing.T) { 311 | c := makeClient(t) 312 | session := c.Session() 313 | kv := c.KV() 314 | 315 | // Make a session 316 | id, _, err := session.CreateNoChecks(nil, nil) 317 | if err != nil { 318 | t.Fatalf("err: %v", err) 319 | } 320 | defer session.Destroy(id, nil) 321 | 322 | // Acquire the key 323 | key := testKey() 324 | value := []byte("test") 325 | p := &KVPair{Key: key, Value: value, Session: id} 326 | if work, _, err := kv.Acquire(p, nil); err != nil { 327 | t.Fatalf("err: %v", err) 328 | } else if !work { 329 | t.Fatalf("Lock failure") 330 | } 331 | 332 | // Get should work 333 | pair, meta, err := kv.Get(key, nil) 334 | if err != nil { 335 | t.Fatalf("err: %v", err) 336 | } 337 | if pair == nil { 338 | t.Fatalf("expected value: %#v", pair) 339 | } 340 | if pair.LockIndex != 1 { 341 | t.Fatalf("Expected lock: %v", pair) 342 | } 343 | if pair.Session != id { 344 | t.Fatalf("Expected lock: %v", pair) 345 | } 346 | if meta.LastIndex == 0 { 347 | t.Fatalf("unexpected value: %#v", meta) 348 | } 349 | 350 | // Release 351 | if work, _, err := kv.Release(p, nil); err != nil { 352 | t.Fatalf("err: %v", err) 353 | } else if !work { 354 | t.Fatalf("Release fail") 355 | } 356 | 357 | // Get should work 358 | pair, meta, err = kv.Get(key, nil) 359 | if err != nil { 360 | t.Fatalf("err: %v", err) 361 | } 362 | if pair == nil { 363 | t.Fatalf("expected value: %#v", pair) 364 | } 365 | if pair.LockIndex != 1 { 366 | t.Fatalf("Expected lock: %v", pair) 367 | } 368 | if pair.Session != "" { 369 | t.Fatalf("Expected unlock: %v", pair) 370 | } 371 | if meta.LastIndex == 0 { 372 | t.Fatalf("unexpected value: %#v", meta) 373 | } 374 | } 375 | -------------------------------------------------------------------------------- /session.go: -------------------------------------------------------------------------------- 1 | package consulapi 2 | 3 | import ( 4 | "time" 5 | ) 6 | 7 | // SessionEntry represents a session in consul 8 | type SessionEntry struct { 9 | CreateIndex uint64 10 | ID string 11 | Name string 12 | Node string 13 | Checks []string 14 | LockDelay time.Duration 15 | Behavior string 16 | TTL string 17 | } 18 | 19 | // Session can be used to query the Session endpoints 20 | type Session struct { 21 | c *Client 22 | } 23 | 24 | // Session returns a handle to the session endpoints 25 | func (c *Client) Session() *Session { 26 | return &Session{c} 27 | } 28 | 29 | // CreateNoChecks is like Create but is used specifically to create 30 | // a session with no associated health checks. 31 | func (s *Session) CreateNoChecks(se *SessionEntry, q *WriteOptions) (string, *WriteMeta, error) { 32 | body := make(map[string]interface{}) 33 | body["Checks"] = []string{} 34 | if se != nil { 35 | if se.Name != "" { 36 | body["Name"] = se.Name 37 | } 38 | if se.Node != "" { 39 | body["Node"] = se.Node 40 | } 41 | if se.LockDelay != 0 { 42 | body["LockDelay"] = durToMsec(se.LockDelay) 43 | } 44 | if se.Behavior != "" { 45 | body["Behavior"] = se.Behavior 46 | } 47 | if se.TTL != "" { 48 | body["TTL"] = se.TTL 49 | } 50 | } 51 | return s.create(body, q) 52 | 53 | } 54 | 55 | // Create makes a new session. Providing a session entry can 56 | // customize the session. It can also be nil to use defaults. 57 | func (s *Session) Create(se *SessionEntry, q *WriteOptions) (string, *WriteMeta, error) { 58 | var obj interface{} 59 | if se != nil { 60 | body := make(map[string]interface{}) 61 | obj = body 62 | if se.Name != "" { 63 | body["Name"] = se.Name 64 | } 65 | if se.Node != "" { 66 | body["Node"] = se.Node 67 | } 68 | if se.LockDelay != 0 { 69 | body["LockDelay"] = durToMsec(se.LockDelay) 70 | } 71 | if len(se.Checks) > 0 { 72 | body["Checks"] = se.Checks 73 | } 74 | if se.Behavior != "" { 75 | body["Behavior"] = se.Behavior 76 | } 77 | if se.TTL != "" { 78 | body["TTL"] = se.TTL 79 | } 80 | } 81 | return s.create(obj, q) 82 | } 83 | 84 | func (s *Session) create(obj interface{}, q *WriteOptions) (string, *WriteMeta, error) { 85 | r := s.c.newRequest("PUT", "/v1/session/create") 86 | r.setWriteOptions(q) 87 | r.obj = obj 88 | rtt, resp, err := requireOK(s.c.doRequest(r)) 89 | if err != nil { 90 | return "", nil, err 91 | } 92 | defer resp.Body.Close() 93 | 94 | wm := &WriteMeta{RequestTime: rtt} 95 | var out struct{ ID string } 96 | if err := decodeBody(resp, &out); err != nil { 97 | return "", nil, err 98 | } 99 | return out.ID, wm, nil 100 | } 101 | 102 | // Destroy invalides a given session 103 | func (s *Session) Destroy(id string, q *WriteOptions) (*WriteMeta, error) { 104 | r := s.c.newRequest("PUT", "/v1/session/destroy/"+id) 105 | r.setWriteOptions(q) 106 | rtt, resp, err := requireOK(s.c.doRequest(r)) 107 | if err != nil { 108 | return nil, err 109 | } 110 | resp.Body.Close() 111 | 112 | wm := &WriteMeta{RequestTime: rtt} 113 | return wm, nil 114 | } 115 | 116 | // Renew renews the TTL on a given session 117 | func (s *Session) Renew(id string, q *WriteOptions) (*SessionEntry, *WriteMeta, error) { 118 | r := s.c.newRequest("PUT", "/v1/session/renew/"+id) 119 | r.setWriteOptions(q) 120 | rtt, resp, err := requireOK(s.c.doRequest(r)) 121 | if err != nil { 122 | return nil, nil, err 123 | } 124 | defer resp.Body.Close() 125 | 126 | wm := &WriteMeta{RequestTime: rtt} 127 | 128 | var entries []*SessionEntry 129 | if err := decodeBody(resp, &entries); err != nil { 130 | return nil, wm, err 131 | } 132 | 133 | if len(entries) > 0 { 134 | return entries[0], wm, nil 135 | } 136 | return nil, wm, nil 137 | } 138 | 139 | // Info looks up a single session 140 | func (s *Session) Info(id string, q *QueryOptions) (*SessionEntry, *QueryMeta, error) { 141 | r := s.c.newRequest("GET", "/v1/session/info/"+id) 142 | r.setQueryOptions(q) 143 | rtt, resp, err := requireOK(s.c.doRequest(r)) 144 | if err != nil { 145 | return nil, nil, err 146 | } 147 | defer resp.Body.Close() 148 | 149 | qm := &QueryMeta{} 150 | parseQueryMeta(resp, qm) 151 | qm.RequestTime = rtt 152 | 153 | var entries []*SessionEntry 154 | if err := decodeBody(resp, &entries); err != nil { 155 | return nil, nil, err 156 | } 157 | 158 | if len(entries) > 0 { 159 | return entries[0], qm, nil 160 | } 161 | return nil, qm, nil 162 | } 163 | 164 | // List gets sessions for a node 165 | func (s *Session) Node(node string, q *QueryOptions) ([]*SessionEntry, *QueryMeta, error) { 166 | r := s.c.newRequest("GET", "/v1/session/node/"+node) 167 | r.setQueryOptions(q) 168 | rtt, resp, err := requireOK(s.c.doRequest(r)) 169 | if err != nil { 170 | return nil, nil, err 171 | } 172 | defer resp.Body.Close() 173 | 174 | qm := &QueryMeta{} 175 | parseQueryMeta(resp, qm) 176 | qm.RequestTime = rtt 177 | 178 | var entries []*SessionEntry 179 | if err := decodeBody(resp, &entries); err != nil { 180 | return nil, nil, err 181 | } 182 | return entries, qm, nil 183 | } 184 | 185 | // List gets all active sessions 186 | func (s *Session) List(q *QueryOptions) ([]*SessionEntry, *QueryMeta, error) { 187 | r := s.c.newRequest("GET", "/v1/session/list") 188 | r.setQueryOptions(q) 189 | rtt, resp, err := requireOK(s.c.doRequest(r)) 190 | if err != nil { 191 | return nil, nil, err 192 | } 193 | defer resp.Body.Close() 194 | 195 | qm := &QueryMeta{} 196 | parseQueryMeta(resp, qm) 197 | qm.RequestTime = rtt 198 | 199 | var entries []*SessionEntry 200 | if err := decodeBody(resp, &entries); err != nil { 201 | return nil, nil, err 202 | } 203 | return entries, qm, nil 204 | } 205 | -------------------------------------------------------------------------------- /session_test.go: -------------------------------------------------------------------------------- 1 | package consulapi 2 | 3 | import ( 4 | "testing" 5 | ) 6 | 7 | func TestSession_CreateDestroy(t *testing.T) { 8 | c := makeClient(t) 9 | session := c.Session() 10 | 11 | id, meta, err := session.Create(nil, nil) 12 | if err != nil { 13 | t.Fatalf("err: %v", err) 14 | } 15 | 16 | if meta.RequestTime == 0 { 17 | t.Fatalf("bad: %v", meta) 18 | } 19 | 20 | if id == "" { 21 | t.Fatalf("invalid: %v", id) 22 | } 23 | 24 | meta, err = session.Destroy(id, nil) 25 | if err != nil { 26 | t.Fatalf("err: %v", err) 27 | } 28 | 29 | if meta.RequestTime == 0 { 30 | t.Fatalf("bad: %v", meta) 31 | } 32 | } 33 | 34 | func TestSession_CreateRenewDestroy(t *testing.T) { 35 | c := makeClient(t) 36 | session := c.Session() 37 | 38 | se := &SessionEntry{ 39 | TTL: "10s", 40 | } 41 | 42 | id, meta, err := session.Create(se, nil) 43 | if err != nil { 44 | t.Fatalf("err: %v", err) 45 | } 46 | defer session.Destroy(id, nil) 47 | 48 | if meta.RequestTime == 0 { 49 | t.Fatalf("bad: %v", meta) 50 | } 51 | 52 | if id == "" { 53 | t.Fatalf("invalid: %v", id) 54 | } 55 | 56 | if meta.RequestTime == 0 { 57 | t.Fatalf("bad: %v", meta) 58 | } 59 | 60 | renew, meta, err := session.Renew(id, nil) 61 | 62 | if err != nil { 63 | t.Fatalf("err: %v", err) 64 | } 65 | if meta.RequestTime == 0 { 66 | t.Fatalf("bad: %v", meta) 67 | } 68 | 69 | if renew == nil { 70 | t.Fatalf("should get session") 71 | } 72 | 73 | if renew.ID != id { 74 | t.Fatalf("should have matching id") 75 | } 76 | 77 | if renew.TTL != "10s" { 78 | t.Fatalf("should get session with TTL") 79 | } 80 | } 81 | 82 | func TestSession_Info(t *testing.T) { 83 | c := makeClient(t) 84 | session := c.Session() 85 | 86 | id, _, err := session.Create(nil, nil) 87 | if err != nil { 88 | t.Fatalf("err: %v", err) 89 | } 90 | defer session.Destroy(id, nil) 91 | 92 | info, qm, err := session.Info(id, nil) 93 | if err != nil { 94 | t.Fatalf("err: %v", err) 95 | } 96 | 97 | if qm.LastIndex == 0 { 98 | t.Fatalf("bad: %v", qm) 99 | } 100 | if !qm.KnownLeader { 101 | t.Fatalf("bad: %v", qm) 102 | } 103 | 104 | if info == nil { 105 | t.Fatalf("should get session") 106 | } 107 | if info.CreateIndex == 0 { 108 | t.Fatalf("bad: %v", info) 109 | } 110 | if info.ID != id { 111 | t.Fatalf("bad: %v", info) 112 | } 113 | if info.Name != "" { 114 | t.Fatalf("bad: %v", info) 115 | } 116 | if info.Node == "" { 117 | t.Fatalf("bad: %v", info) 118 | } 119 | if len(info.Checks) == 0 { 120 | t.Fatalf("bad: %v", info) 121 | } 122 | if info.LockDelay == 0 { 123 | t.Fatalf("bad: %v", info) 124 | } 125 | if info.Behavior != "release" { 126 | t.Fatalf("bad: %v", info) 127 | } 128 | if info.TTL != "" { 129 | t.Fatalf("bad: %v", info) 130 | } 131 | } 132 | 133 | func TestSession_Node(t *testing.T) { 134 | c := makeClient(t) 135 | session := c.Session() 136 | 137 | id, _, err := session.Create(nil, nil) 138 | if err != nil { 139 | t.Fatalf("err: %v", err) 140 | } 141 | defer session.Destroy(id, nil) 142 | 143 | info, qm, err := session.Info(id, nil) 144 | if err != nil { 145 | t.Fatalf("err: %v", err) 146 | } 147 | 148 | sessions, qm, err := session.Node(info.Node, nil) 149 | if err != nil { 150 | t.Fatalf("err: %v", err) 151 | } 152 | 153 | if len(sessions) != 1 { 154 | t.Fatalf("bad: %v", sessions) 155 | } 156 | 157 | if qm.LastIndex == 0 { 158 | t.Fatalf("bad: %v", qm) 159 | } 160 | if !qm.KnownLeader { 161 | t.Fatalf("bad: %v", qm) 162 | } 163 | } 164 | 165 | func TestSession_List(t *testing.T) { 166 | c := makeClient(t) 167 | session := c.Session() 168 | 169 | id, _, err := session.Create(nil, nil) 170 | if err != nil { 171 | t.Fatalf("err: %v", err) 172 | } 173 | defer session.Destroy(id, nil) 174 | 175 | sessions, qm, err := session.List(nil) 176 | if err != nil { 177 | t.Fatalf("err: %v", err) 178 | } 179 | 180 | if len(sessions) != 1 { 181 | t.Fatalf("bad: %v", sessions) 182 | } 183 | 184 | if qm.LastIndex == 0 { 185 | t.Fatalf("bad: %v", qm) 186 | } 187 | if !qm.KnownLeader { 188 | t.Fatalf("bad: %v", qm) 189 | } 190 | } 191 | -------------------------------------------------------------------------------- /status.go: -------------------------------------------------------------------------------- 1 | package consulapi 2 | 3 | // Status can be used to query the Status endpoints 4 | type Status struct { 5 | c *Client 6 | } 7 | 8 | // Status returns a handle to the status endpoints 9 | func (c *Client) Status() *Status { 10 | return &Status{c} 11 | } 12 | 13 | // Leader is used to query for a known leader 14 | func (s *Status) Leader() (string, error) { 15 | r := s.c.newRequest("GET", "/v1/status/leader") 16 | _, resp, err := requireOK(s.c.doRequest(r)) 17 | if err != nil { 18 | return "", err 19 | } 20 | defer resp.Body.Close() 21 | 22 | var leader string 23 | if err := decodeBody(resp, &leader); err != nil { 24 | return "", err 25 | } 26 | return leader, nil 27 | } 28 | 29 | // Peers is used to query for a known raft peers 30 | func (s *Status) Peers() ([]string, error) { 31 | r := s.c.newRequest("GET", "/v1/status/peers") 32 | _, resp, err := requireOK(s.c.doRequest(r)) 33 | if err != nil { 34 | return nil, err 35 | } 36 | defer resp.Body.Close() 37 | 38 | var peers []string 39 | if err := decodeBody(resp, &peers); err != nil { 40 | return nil, err 41 | } 42 | return peers, nil 43 | } 44 | -------------------------------------------------------------------------------- /status_test.go: -------------------------------------------------------------------------------- 1 | package consulapi 2 | 3 | import ( 4 | "testing" 5 | ) 6 | 7 | func TestStatusLeader(t *testing.T) { 8 | c := makeClient(t) 9 | status := c.Status() 10 | 11 | leader, err := status.Leader() 12 | if err != nil { 13 | t.Fatalf("err: %v", err) 14 | } 15 | if leader == "" { 16 | t.Fatalf("Expected leader") 17 | } 18 | } 19 | 20 | func TestStatusPeers(t *testing.T) { 21 | c := makeClient(t) 22 | status := c.Status() 23 | 24 | peers, err := status.Peers() 25 | if err != nil { 26 | t.Fatalf("err: %v", err) 27 | } 28 | if len(peers) == 0 { 29 | t.Fatalf("Expected peers ") 30 | } 31 | } 32 | --------------------------------------------------------------------------------