From a19fecee875834ee99cbd1bdce509a148eed20af Mon Sep 17 00:00:00 2001 From: JChien Date: Wed, 11 Feb 2015 21:25:55 +0800 Subject: [PATCH 1/2] update import url Signed-off-by: JChien --- discovery/consul/consul.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/discovery/consul/consul.go b/discovery/consul/consul.go index 129865fb6c..f7cf60570e 100644 --- a/discovery/consul/consul.go +++ b/discovery/consul/consul.go @@ -7,8 +7,8 @@ import ( "time" log "github.com/Sirupsen/logrus" - consul "github.com/armon/consul-api" "github.com/docker/swarm/discovery" + consul "github.com/hashicorp/consul/api" ) type ConsulDiscoveryService struct { From 050c0572d2c069d4caa190bf70e22bc2171e9f65 Mon Sep 17 00:00:00 2001 From: JChien Date: Wed, 11 Feb 2015 21:26:44 +0800 Subject: [PATCH 2/2] update godeps Signed-off-by: JChien --- Godeps/Godeps.json | 9 +- .../github.com/armon/consul-api/.gitignore | 23 - .../src/github.com/armon/consul-api/LICENSE | 362 ------------- .../src/github.com/armon/consul-api/README.md | 42 -- .../github.com/armon/consul-api/agent_test.go | 162 ------ .../github.com/armon/consul-api/api_test.go | 126 ----- .../armon/consul-api/catalog_test.go | 219 -------- .../armon/consul-api/health_test.go | 98 ---- .../github.com/hashicorp/consul/api/README.md | 39 ++ .../consul/api}/acl.go | 2 +- .../consul/api}/acl_test.go | 18 +- .../consul/api}/agent.go | 79 ++- .../hashicorp/consul/api/agent_test.go | 404 +++++++++++++++ .../consul/api}/api.go | 50 +- .../hashicorp/consul/api/api_test.go | 339 ++++++++++++ .../consul/api}/catalog.go | 2 +- .../hashicorp/consul/api/catalog_test.go | 273 ++++++++++ .../consul/api}/event.go | 2 +- .../consul/api}/event_test.go | 6 +- .../consul/api}/health.go | 2 +- .../hashicorp/consul/api/health_test.go | 121 +++++ .../consul-api => hashicorp/consul/api}/kv.go | 35 +- .../consul/api}/kv_test.go | 77 ++- .../github.com/hashicorp/consul/api/lock.go | 321 ++++++++++++ .../hashicorp/consul/api/lock_test.go | 289 +++++++++++ .../hashicorp/consul/api/semaphore.go | 482 ++++++++++++++++++ .../hashicorp/consul/api/semaphore_test.go | 306 +++++++++++ .../consul/api}/session.go | 43 +- .../consul/api}/session_test.go | 22 +- .../consul/api}/status.go | 2 +- .../consul/api}/status_test.go | 10 +- 31 files changed, 2868 insertions(+), 1097 deletions(-) delete mode 100644 Godeps/_workspace/src/github.com/armon/consul-api/.gitignore delete mode 100644 Godeps/_workspace/src/github.com/armon/consul-api/LICENSE delete mode 100644 Godeps/_workspace/src/github.com/armon/consul-api/README.md delete mode 100644 Godeps/_workspace/src/github.com/armon/consul-api/agent_test.go delete mode 100644 Godeps/_workspace/src/github.com/armon/consul-api/api_test.go delete mode 100644 Godeps/_workspace/src/github.com/armon/consul-api/catalog_test.go delete mode 100644 Godeps/_workspace/src/github.com/armon/consul-api/health_test.go create mode 100644 Godeps/_workspace/src/github.com/hashicorp/consul/api/README.md rename Godeps/_workspace/src/github.com/{armon/consul-api => hashicorp/consul/api}/acl.go (99%) rename Godeps/_workspace/src/github.com/{armon/consul-api => hashicorp/consul/api}/acl_test.go (92%) rename Godeps/_workspace/src/github.com/{armon/consul-api => hashicorp/consul/api}/agent.go (75%) create mode 100644 Godeps/_workspace/src/github.com/hashicorp/consul/api/agent_test.go rename Godeps/_workspace/src/github.com/{armon/consul-api => hashicorp/consul/api}/api.go (89%) create mode 100644 Godeps/_workspace/src/github.com/hashicorp/consul/api/api_test.go rename Godeps/_workspace/src/github.com/{armon/consul-api => hashicorp/consul/api}/catalog.go (99%) create mode 100644 Godeps/_workspace/src/github.com/hashicorp/consul/api/catalog_test.go rename Godeps/_workspace/src/github.com/{armon/consul-api => hashicorp/consul/api}/event.go (99%) rename Godeps/_workspace/src/github.com/{armon/consul-api => hashicorp/consul/api}/event_test.go (91%) rename Godeps/_workspace/src/github.com/{armon/consul-api => hashicorp/consul/api}/health.go (99%) create mode 100644 Godeps/_workspace/src/github.com/hashicorp/consul/api/health_test.go rename Godeps/_workspace/src/github.com/{armon/consul-api => hashicorp/consul/api}/kv.go (84%) rename Godeps/_workspace/src/github.com/{armon/consul-api => hashicorp/consul/api}/kv_test.go (85%) create mode 100644 Godeps/_workspace/src/github.com/hashicorp/consul/api/lock.go create mode 100644 Godeps/_workspace/src/github.com/hashicorp/consul/api/lock_test.go create mode 100644 Godeps/_workspace/src/github.com/hashicorp/consul/api/semaphore.go create mode 100644 Godeps/_workspace/src/github.com/hashicorp/consul/api/semaphore_test.go rename Godeps/_workspace/src/github.com/{armon/consul-api => hashicorp/consul/api}/session.go (80%) rename Godeps/_workspace/src/github.com/{armon/consul-api => hashicorp/consul/api}/session_test.go (93%) rename Godeps/_workspace/src/github.com/{armon/consul-api => hashicorp/consul/api}/status.go (98%) rename Godeps/_workspace/src/github.com/{armon/consul-api => hashicorp/consul/api}/status_test.go (81%) diff --git a/Godeps/Godeps.json b/Godeps/Godeps.json index cfc83ee302..7773787187 100644 --- a/Godeps/Godeps.json +++ b/Godeps/Godeps.json @@ -10,10 +10,6 @@ "Comment": "v0.6.4-6-g539d4dc", "Rev": "539d4dc034c079a7188b5d4ca9650632d73c66e8" }, - { - "ImportPath": "github.com/armon/consul-api", - "Rev": "dcfedd50ed5334f96adee43fc88518a4f095e15c" - }, { "ImportPath": "github.com/codegangsta/cli", "Comment": "1.2.0-62-gbf4a526", @@ -52,6 +48,11 @@ "ImportPath": "github.com/gorilla/mux", "Rev": "e444e69cbd2e2e3e0749a2f3c717cec491552bbf" }, + { + "ImportPath": "github.com/hashicorp/consul/api", + "Comment": "v0.5.0rc1-108-g79d28b9", + "Rev": "79d28b9baf2be44ada3058263572d3a72ba71d4e" + }, { "ImportPath": "github.com/samalba/dockerclient", "Rev": "7e4366cfab2f2b44fcb493bee93a156a763d58b6" diff --git a/Godeps/_workspace/src/github.com/armon/consul-api/.gitignore b/Godeps/_workspace/src/github.com/armon/consul-api/.gitignore deleted file mode 100644 index 836562412f..0000000000 --- a/Godeps/_workspace/src/github.com/armon/consul-api/.gitignore +++ /dev/null @@ -1,23 +0,0 @@ -# Compiled Object files, Static and Dynamic libs (Shared Objects) -*.o -*.a -*.so - -# Folders -_obj -_test - -# Architecture specific extensions/prefixes -*.[568vq] -[568vq].out - -*.cgo1.go -*.cgo2.c -_cgo_defun.c -_cgo_gotypes.go -_cgo_export.* - -_testmain.go - -*.exe -*.test diff --git a/Godeps/_workspace/src/github.com/armon/consul-api/LICENSE b/Godeps/_workspace/src/github.com/armon/consul-api/LICENSE deleted file mode 100644 index f0e5c79e18..0000000000 --- a/Godeps/_workspace/src/github.com/armon/consul-api/LICENSE +++ /dev/null @@ -1,362 +0,0 @@ -Mozilla Public License, version 2.0 - -1. Definitions - -1.1. "Contributor" - - means each individual or legal entity that creates, contributes to the - creation of, or owns Covered Software. - -1.2. "Contributor Version" - - means the combination of the Contributions of others (if any) used by a - Contributor and that particular Contributor's Contribution. - -1.3. "Contribution" - - means Covered Software of a particular Contributor. - -1.4. "Covered Software" - - means Source Code Form to which the initial Contributor has attached the - notice in Exhibit A, the Executable Form of such Source Code Form, and - Modifications of such Source Code Form, in each case including portions - thereof. - -1.5. "Incompatible With Secondary Licenses" - means - - a. that the initial Contributor has attached the notice described in - Exhibit B to the Covered Software; or - - b. that the Covered Software was made available under the terms of - version 1.1 or earlier of the License, but not also under the terms of - a Secondary License. - -1.6. "Executable Form" - - means any form of the work other than Source Code Form. - -1.7. "Larger Work" - - means a work that combines Covered Software with other material, in a - separate file or files, that is not Covered Software. - -1.8. "License" - - means this document. - -1.9. "Licensable" - - means having the right to grant, to the maximum extent possible, whether - at the time of the initial grant or subsequently, any and all of the - rights conveyed by this License. - -1.10. "Modifications" - - means any of the following: - - a. any file in Source Code Form that results from an addition to, - deletion from, or modification of the contents of Covered Software; or - - b. any new file in Source Code Form that contains any Covered Software. - -1.11. "Patent Claims" of a Contributor - - means any patent claim(s), including without limitation, method, - process, and apparatus claims, in any patent Licensable by such - Contributor that would be infringed, but for the grant of the License, - by the making, using, selling, offering for sale, having made, import, - or transfer of either its Contributions or its Contributor Version. - -1.12. "Secondary License" - - means either the GNU General Public License, Version 2.0, the GNU Lesser - General Public License, Version 2.1, the GNU Affero General Public - License, Version 3.0, or any later versions of those licenses. - -1.13. "Source Code Form" - - means the form of the work preferred for making modifications. - -1.14. "You" (or "Your") - - means an individual or a legal entity exercising rights under this - License. For legal entities, "You" includes any entity that controls, is - controlled by, or is under common control with You. For purposes of this - definition, "control" means (a) the power, direct or indirect, to cause - the direction or management of such entity, whether by contract or - otherwise, or (b) ownership of more than fifty percent (50%) of the - outstanding shares or beneficial ownership of such entity. - - -2. License Grants and Conditions - -2.1. Grants - - Each Contributor hereby grants You a world-wide, royalty-free, - non-exclusive license: - - a. under intellectual property rights (other than patent or trademark) - Licensable by such Contributor to use, reproduce, make available, - modify, display, perform, distribute, and otherwise exploit its - Contributions, either on an unmodified basis, with Modifications, or - as part of a Larger Work; and - - b. under Patent Claims of such Contributor to make, use, sell, offer for - sale, have made, import, and otherwise transfer either its - Contributions or its Contributor Version. - -2.2. Effective Date - - The licenses granted in Section 2.1 with respect to any Contribution - become effective for each Contribution on the date the Contributor first - distributes such Contribution. - -2.3. Limitations on Grant Scope - - The licenses granted in this Section 2 are the only rights granted under - this License. No additional rights or licenses will be implied from the - distribution or licensing of Covered Software under this License. - Notwithstanding Section 2.1(b) above, no patent license is granted by a - Contributor: - - a. for any code that a Contributor has removed from Covered Software; or - - b. for infringements caused by: (i) Your and any other third party's - modifications of Covered Software, or (ii) the combination of its - Contributions with other software (except as part of its Contributor - Version); or - - c. under Patent Claims infringed by Covered Software in the absence of - its Contributions. - - This License does not grant any rights in the trademarks, service marks, - or logos of any Contributor (except as may be necessary to comply with - the notice requirements in Section 3.4). - -2.4. Subsequent Licenses - - No Contributor makes additional grants as a result of Your choice to - distribute the Covered Software under a subsequent version of this - License (see Section 10.2) or under the terms of a Secondary License (if - permitted under the terms of Section 3.3). - -2.5. Representation - - Each Contributor represents that the Contributor believes its - Contributions are its original creation(s) or it has sufficient rights to - grant the rights to its Contributions conveyed by this License. - -2.6. Fair Use - - This License is not intended to limit any rights You have under - applicable copyright doctrines of fair use, fair dealing, or other - equivalents. - -2.7. Conditions - - Sections 3.1, 3.2, 3.3, and 3.4 are conditions of the licenses granted in - Section 2.1. - - -3. Responsibilities - -3.1. Distribution of Source Form - - All distribution of Covered Software in Source Code Form, including any - Modifications that You create or to which You contribute, must be under - the terms of this License. You must inform recipients that the Source - Code Form of the Covered Software is governed by the terms of this - License, and how they can obtain a copy of this License. You may not - attempt to alter or restrict the recipients' rights in the Source Code - Form. - -3.2. Distribution of Executable Form - - If You distribute Covered Software in Executable Form then: - - a. such Covered Software must also be made available in Source Code Form, - as described in Section 3.1, and You must inform recipients of the - Executable Form how they can obtain a copy of such Source Code Form by - reasonable means in a timely manner, at a charge no more than the cost - of distribution to the recipient; and - - b. You may distribute such Executable Form under the terms of this - License, or sublicense it under different terms, provided that the - license for the Executable Form does not attempt to limit or alter the - recipients' rights in the Source Code Form under this License. - -3.3. Distribution of a Larger Work - - You may create and distribute a Larger Work under terms of Your choice, - provided that You also comply with the requirements of this License for - the Covered Software. If the Larger Work is a combination of Covered - Software with a work governed by one or more Secondary Licenses, and the - Covered Software is not Incompatible With Secondary Licenses, this - License permits You to additionally distribute such Covered Software - under the terms of such Secondary License(s), so that the recipient of - the Larger Work may, at their option, further distribute the Covered - Software under the terms of either this License or such Secondary - License(s). - -3.4. Notices - - You may not remove or alter the substance of any license notices - (including copyright notices, patent notices, disclaimers of warranty, or - limitations of liability) contained within the Source Code Form of the - Covered Software, except that You may alter any license notices to the - extent required to remedy known factual inaccuracies. - -3.5. Application of Additional Terms - - You may choose to offer, and to charge a fee for, warranty, support, - indemnity or liability obligations to one or more recipients of Covered - Software. However, You may do so only on Your own behalf, and not on - behalf of any Contributor. You must make it absolutely clear that any - such warranty, support, indemnity, or liability obligation is offered by - You alone, and You hereby agree to indemnify every Contributor for any - liability incurred by such Contributor as a result of warranty, support, - indemnity or liability terms You offer. You may include additional - disclaimers of warranty and limitations of liability specific to any - jurisdiction. - -4. Inability to Comply Due to Statute or Regulation - - If it is impossible for You to comply with any of the terms of this License - with respect to some or all of the Covered Software due to statute, - judicial order, or regulation then You must: (a) comply with the terms of - this License to the maximum extent possible; and (b) describe the - limitations and the code they affect. Such description must be placed in a - text file included with all distributions of the Covered Software under - this License. Except to the extent prohibited by statute or regulation, - such description must be sufficiently detailed for a recipient of ordinary - skill to be able to understand it. - -5. Termination - -5.1. The rights granted under this License will terminate automatically if You - fail to comply with any of its terms. However, if You become compliant, - then the rights granted under this License from a particular Contributor - are reinstated (a) provisionally, unless and until such Contributor - explicitly and finally terminates Your grants, and (b) on an ongoing - basis, if such Contributor fails to notify You of the non-compliance by - some reasonable means prior to 60 days after You have come back into - compliance. Moreover, Your grants from a particular Contributor are - reinstated on an ongoing basis if such Contributor notifies You of the - non-compliance by some reasonable means, this is the first time You have - received notice of non-compliance with this License from such - Contributor, and You become compliant prior to 30 days after Your receipt - of the notice. - -5.2. If You initiate litigation against any entity by asserting a patent - infringement claim (excluding declaratory judgment actions, - counter-claims, and cross-claims) alleging that a Contributor Version - directly or indirectly infringes any patent, then the rights granted to - You by any and all Contributors for the Covered Software under Section - 2.1 of this License shall terminate. - -5.3. In the event of termination under Sections 5.1 or 5.2 above, all end user - license agreements (excluding distributors and resellers) which have been - validly granted by You or Your distributors under this License prior to - termination shall survive termination. - -6. Disclaimer of Warranty - - Covered Software is provided under this License on an "as is" basis, - without warranty of any kind, either expressed, implied, or statutory, - including, without limitation, warranties that the Covered Software is free - of defects, merchantable, fit for a particular purpose or non-infringing. - The entire risk as to the quality and performance of the Covered Software - is with You. Should any Covered Software prove defective in any respect, - You (not any Contributor) assume the cost of any necessary servicing, - repair, or correction. This disclaimer of warranty constitutes an essential - part of this License. No use of any Covered Software is authorized under - this License except under this disclaimer. - -7. Limitation of Liability - - Under no circumstances and under no legal theory, whether tort (including - negligence), contract, or otherwise, shall any Contributor, or anyone who - distributes Covered Software as permitted above, be liable to You for any - direct, indirect, special, incidental, or consequential damages of any - character including, without limitation, damages for lost profits, loss of - goodwill, work stoppage, computer failure or malfunction, or any and all - other commercial damages or losses, even if such party shall have been - informed of the possibility of such damages. This limitation of liability - shall not apply to liability for death or personal injury resulting from - such party's negligence to the extent applicable law prohibits such - limitation. Some jurisdictions do not allow the exclusion or limitation of - incidental or consequential damages, so this exclusion and limitation may - not apply to You. - -8. Litigation - - Any litigation relating to this License may be brought only in the courts - of a jurisdiction where the defendant maintains its principal place of - business and such litigation shall be governed by laws of that - jurisdiction, without reference to its conflict-of-law provisions. Nothing - in this Section shall prevent a party's ability to bring cross-claims or - counter-claims. - -9. Miscellaneous - - This License represents the complete agreement concerning the subject - matter hereof. If any provision of this License is held to be - unenforceable, such provision shall be reformed only to the extent - necessary to make it enforceable. Any law or regulation which provides that - the language of a contract shall be construed against the drafter shall not - be used to construe this License against a Contributor. - - -10. Versions of the License - -10.1. New Versions - - Mozilla Foundation is the license steward. Except as provided in Section - 10.3, no one other than the license steward has the right to modify or - publish new versions of this License. Each version will be given a - distinguishing version number. - -10.2. Effect of New Versions - - You may distribute the Covered Software under the terms of the version - of the License under which You originally received the Covered Software, - or under the terms of any subsequent version published by the license - steward. - -10.3. Modified Versions - - If you create software not governed by this License, and you want to - create a new license for such software, you may create and use a - modified version of this License if you rename the license and remove - any references to the name of the license steward (except to note that - such modified license differs from this License). - -10.4. Distributing Source Code Form that is Incompatible With Secondary - Licenses If You choose to distribute Source Code Form that is - Incompatible With Secondary Licenses under the terms of this version of - the License, the notice described in Exhibit B of this License must be - attached. - -Exhibit A - Source Code Form License Notice - - This Source Code Form is subject to the - terms of the Mozilla Public License, v. - 2.0. If a copy of the MPL was not - distributed with this file, You can - obtain one at - http://mozilla.org/MPL/2.0/. - -If it is not possible or desirable to put the notice in a particular file, -then You may include the notice in a location (such as a LICENSE file in a -relevant directory) where a recipient would be likely to look for such a -notice. - -You may add additional accurate notices of copyright ownership. - -Exhibit B - "Incompatible With Secondary Licenses" Notice - - This Source Code Form is "Incompatible - With Secondary Licenses", as defined by - the Mozilla Public License, v. 2.0. \ No newline at end of file diff --git a/Godeps/_workspace/src/github.com/armon/consul-api/README.md b/Godeps/_workspace/src/github.com/armon/consul-api/README.md deleted file mode 100644 index c95d9dee33..0000000000 --- a/Godeps/_workspace/src/github.com/armon/consul-api/README.md +++ /dev/null @@ -1,42 +0,0 @@ -consul-api -========== - -*DEPRECATED* Please use [consul api package](https://github.com/hashicorp/consul/tree/master/api) instead. -Godocs for that package [are here](http://godoc.org/github.com/hashicorp/consul/api). - -This package provides the `consulapi` package which attempts to -provide programmatic access to the full Consul API. - -Currently, all of the Consul APIs included in version 0.4 are supported. - -Documentation -============= - -The full documentation is available on [Godoc](http://godoc.org/github.com/armon/consul-api) - -Usage -===== - -Below is an example of using the Consul client: - -```go -// Get a new client, with KV endpoints -client, _ := consulapi.NewClient(consulapi.DefaultConfig()) -kv := client.KV() - -// PUT a new KV pair -p := &consulapi.KVPair{Key: "foo", Value: []byte("test")} -_, err := kv.Put(p, nil) -if err != nil { - panic(err) -} - -// Lookup the pair -pair, _, err := kv.Get("foo", nil) -if err != nil { - panic(err) -} -fmt.Printf("KV: %v", pair) - -``` - diff --git a/Godeps/_workspace/src/github.com/armon/consul-api/agent_test.go b/Godeps/_workspace/src/github.com/armon/consul-api/agent_test.go deleted file mode 100644 index 8d97af4af5..0000000000 --- a/Godeps/_workspace/src/github.com/armon/consul-api/agent_test.go +++ /dev/null @@ -1,162 +0,0 @@ -package consulapi - -import ( - "testing" -) - -func TestAgent_Self(t *testing.T) { - c := makeClient(t) - agent := c.Agent() - - info, err := agent.Self() - if err != nil { - t.Fatalf("err: %v", err) - } - - name := info["Config"]["NodeName"] - if name == "" { - t.Fatalf("bad: %v", info) - } -} - -func TestAgent_Members(t *testing.T) { - c := makeClient(t) - agent := c.Agent() - - members, err := agent.Members(false) - if err != nil { - t.Fatalf("err: %v", err) - } - - if len(members) != 1 { - t.Fatalf("bad: %v", members) - } -} - -func TestAgent_Services(t *testing.T) { - c := makeClient(t) - agent := c.Agent() - - reg := &AgentServiceRegistration{ - Name: "foo", - Tags: []string{"bar", "baz"}, - Port: 8000, - Check: &AgentServiceCheck{ - TTL: "15s", - }, - } - if err := agent.ServiceRegister(reg); err != nil { - t.Fatalf("err: %v", err) - } - - services, err := agent.Services() - if err != nil { - t.Fatalf("err: %v", err) - } - if _, ok := services["foo"]; !ok { - t.Fatalf("missing service: %v", services) - } - - checks, err := agent.Checks() - if err != nil { - t.Fatalf("err: %v", err) - } - if _, ok := checks["service:foo"]; !ok { - t.Fatalf("missing check: %v", checks) - } - - if err := agent.ServiceDeregister("foo"); err != nil { - t.Fatalf("err: %v", err) - } -} - -func TestAgent_SetTTLStatus(t *testing.T) { - c := makeClient(t) - agent := c.Agent() - - reg := &AgentServiceRegistration{ - Name: "foo", - Check: &AgentServiceCheck{ - TTL: "15s", - }, - } - if err := agent.ServiceRegister(reg); err != nil { - t.Fatalf("err: %v", err) - } - - if err := agent.WarnTTL("service:foo", "test"); err != nil { - t.Fatalf("err: %v", err) - } - - checks, err := agent.Checks() - if err != nil { - t.Fatalf("err: %v", err) - } - chk, ok := checks["service:foo"] - if !ok { - t.Fatalf("missing check: %v", checks) - } - if chk.Status != "warning" { - t.Fatalf("Bad: %#v", chk) - } - if chk.Output != "test" { - t.Fatalf("Bad: %#v", chk) - } - - if err := agent.ServiceDeregister("foo"); err != nil { - t.Fatalf("err: %v", err) - } -} - -func TestAgent_Checks(t *testing.T) { - c := makeClient(t) - agent := c.Agent() - - reg := &AgentCheckRegistration{ - Name: "foo", - } - reg.TTL = "15s" - if err := agent.CheckRegister(reg); err != nil { - t.Fatalf("err: %v", err) - } - - checks, err := agent.Checks() - if err != nil { - t.Fatalf("err: %v", err) - } - if _, ok := checks["foo"]; !ok { - t.Fatalf("missing check: %v", checks) - } - - if err := agent.CheckDeregister("foo"); err != nil { - t.Fatalf("err: %v", err) - } -} - -func TestAgent_Join(t *testing.T) { - c := makeClient(t) - agent := c.Agent() - - info, err := agent.Self() - if err != nil { - t.Fatalf("err: %v", err) - } - - // Join ourself - addr := info["Config"]["AdvertiseAddr"].(string) - err = agent.Join(addr, false) - if err != nil { - t.Fatalf("err: %v", err) - } -} - -func TestAgent_ForceLeave(t *testing.T) { - c := makeClient(t) - agent := c.Agent() - - // Eject somebody - err := agent.ForceLeave("foo") - if err != nil { - t.Fatalf("err: %v", err) - } -} diff --git a/Godeps/_workspace/src/github.com/armon/consul-api/api_test.go b/Godeps/_workspace/src/github.com/armon/consul-api/api_test.go deleted file mode 100644 index 3a608c539b..0000000000 --- a/Godeps/_workspace/src/github.com/armon/consul-api/api_test.go +++ /dev/null @@ -1,126 +0,0 @@ -package consulapi - -import ( - crand "crypto/rand" - "fmt" - "net/http" - "testing" - "time" -) - -func makeClient(t *testing.T) *Client { - conf := DefaultConfig() - client, err := NewClient(conf) - if err != nil { - t.Fatalf("err: %v", err) - } - return client -} - -func testKey() string { - buf := make([]byte, 16) - if _, err := crand.Read(buf); err != nil { - panic(fmt.Errorf("Failed to read random bytes: %v", err)) - } - - return fmt.Sprintf("%08x-%04x-%04x-%04x-%12x", - buf[0:4], - buf[4:6], - buf[6:8], - buf[8:10], - buf[10:16]) -} - -func TestSetQueryOptions(t *testing.T) { - c := makeClient(t) - r := c.newRequest("GET", "/v1/kv/foo") - q := &QueryOptions{ - Datacenter: "foo", - AllowStale: true, - RequireConsistent: true, - WaitIndex: 1000, - WaitTime: 100 * time.Second, - Token: "12345", - } - r.setQueryOptions(q) - - if r.params.Get("dc") != "foo" { - t.Fatalf("bad: %v", r.params) - } - if _, ok := r.params["stale"]; !ok { - t.Fatalf("bad: %v", r.params) - } - if _, ok := r.params["consistent"]; !ok { - t.Fatalf("bad: %v", r.params) - } - if r.params.Get("index") != "1000" { - t.Fatalf("bad: %v", r.params) - } - if r.params.Get("wait") != "100000ms" { - t.Fatalf("bad: %v", r.params) - } - if r.params.Get("token") != "12345" { - t.Fatalf("bad: %v", r.params) - } -} - -func TestSetWriteOptions(t *testing.T) { - c := makeClient(t) - r := c.newRequest("GET", "/v1/kv/foo") - q := &WriteOptions{ - Datacenter: "foo", - Token: "23456", - } - r.setWriteOptions(q) - - if r.params.Get("dc") != "foo" { - t.Fatalf("bad: %v", r.params) - } - if r.params.Get("token") != "23456" { - t.Fatalf("bad: %v", r.params) - } -} - -func TestRequestToHTTP(t *testing.T) { - c := makeClient(t) - r := c.newRequest("DELETE", "/v1/kv/foo") - q := &QueryOptions{ - Datacenter: "foo", - } - r.setQueryOptions(q) - req, err := r.toHTTP() - if err != nil { - t.Fatalf("err: %v", err) - } - - if req.Method != "DELETE" { - t.Fatalf("bad: %v", req) - } - if req.URL.String() != "http://127.0.0.1:8500/v1/kv/foo?dc=foo" { - t.Fatalf("bad: %v", req) - } -} - -func TestParseQueryMeta(t *testing.T) { - resp := &http.Response{ - Header: make(map[string][]string), - } - resp.Header.Set("X-Consul-Index", "12345") - resp.Header.Set("X-Consul-LastContact", "80") - resp.Header.Set("X-Consul-KnownLeader", "true") - - qm := &QueryMeta{} - if err := parseQueryMeta(resp, qm); err != nil { - t.Fatalf("err: %v", err) - } - - if qm.LastIndex != 12345 { - t.Fatalf("Bad: %v", qm) - } - if qm.LastContact != 80*time.Millisecond { - t.Fatalf("Bad: %v", qm) - } - if !qm.KnownLeader { - t.Fatalf("Bad: %v", qm) - } -} diff --git a/Godeps/_workspace/src/github.com/armon/consul-api/catalog_test.go b/Godeps/_workspace/src/github.com/armon/consul-api/catalog_test.go deleted file mode 100644 index 7ed6cfc2ce..0000000000 --- a/Godeps/_workspace/src/github.com/armon/consul-api/catalog_test.go +++ /dev/null @@ -1,219 +0,0 @@ -package consulapi - -import ( - "testing" -) - -func TestCatalog_Datacenters(t *testing.T) { - c := makeClient(t) - catalog := c.Catalog() - - datacenters, err := catalog.Datacenters() - if err != nil { - t.Fatalf("err: %v", err) - } - - if len(datacenters) == 0 { - t.Fatalf("Bad: %v", datacenters) - } -} - -func TestCatalog_Nodes(t *testing.T) { - c := makeClient(t) - catalog := c.Catalog() - - nodes, meta, err := catalog.Nodes(nil) - if err != nil { - t.Fatalf("err: %v", err) - } - - if meta.LastIndex == 0 { - t.Fatalf("Bad: %v", meta) - } - - if len(nodes) == 0 { - t.Fatalf("Bad: %v", nodes) - } -} - -func TestCatalog_Services(t *testing.T) { - c := makeClient(t) - catalog := c.Catalog() - - services, meta, err := catalog.Services(nil) - if err != nil { - t.Fatalf("err: %v", err) - } - - if meta.LastIndex == 0 { - t.Fatalf("Bad: %v", meta) - } - - if len(services) == 0 { - t.Fatalf("Bad: %v", services) - } -} - -func TestCatalog_Service(t *testing.T) { - c := makeClient(t) - catalog := c.Catalog() - - services, meta, err := catalog.Service("consul", "", nil) - if err != nil { - t.Fatalf("err: %v", err) - } - - if meta.LastIndex == 0 { - t.Fatalf("Bad: %v", meta) - } - - if len(services) == 0 { - t.Fatalf("Bad: %v", services) - } -} - -func TestCatalog_Node(t *testing.T) { - c := makeClient(t) - catalog := c.Catalog() - - name, _ := c.Agent().NodeName() - info, meta, err := catalog.Node(name, nil) - if err != nil { - t.Fatalf("err: %v", err) - } - - if meta.LastIndex == 0 { - t.Fatalf("Bad: %v", meta) - } - if len(info.Services) == 0 { - t.Fatalf("Bad: %v", info) - } -} - -func TestCatalog_Registration(t *testing.T) { - c := makeClient(t) - catalog := c.Catalog() - - service := &AgentService{ - ID: "redis1", - Service: "redis", - Tags: []string{"master", "v1"}, - Port: 8000, - } - - check := &AgentCheck{ - Node: "foobar", - CheckID: "service:redis1", - Name: "Redis health check", - Notes: "Script based health check", - Status: "passing", - ServiceID: "redis1", - } - - reg := &CatalogRegistration{ - Datacenter: "dc1", - Node: "foobar", - Address: "192.168.10.10", - Service: service, - Check: check, - } - - _, err := catalog.Register(reg, nil) - - if err != nil { - t.Fatalf("err: %v", err) - } - - node, _, err := catalog.Node("foobar", nil) - - if err != nil { - t.Fatalf("err: %v", err) - } - - if _, ok := node.Services["redis1"]; !ok { - t.Fatalf("missing service: redis1") - } - - health, _, err := c.Health().Node("foobar", nil) - - if err != nil { - t.Fatalf("err: %v", err) - } - - if health[0].CheckID != "service:redis1" { - t.Fatalf("missing checkid service:redis1") - } -} - -func TestCatalog_Deregistration(t *testing.T) { - c := makeClient(t) - catalog := c.Catalog() - - dereg := &CatalogDeregistration{ - Datacenter: "dc1", - Node: "foobar", - Address: "192.168.10.10", - ServiceID: "redis1", - } - - _, err := catalog.Deregister(dereg, nil) - - if err != nil { - t.Fatalf("err: %v", err) - } - - node, _, err := catalog.Node("foobar", nil) - - if err != nil { - t.Fatalf("err: %v", err) - } - - if _, ok := node.Services["redis1"]; ok { - t.Fatalf("ServiceID:redis1 is not deregistered") - } - - dereg = &CatalogDeregistration{ - Datacenter: "dc1", - Node: "foobar", - Address: "192.168.10.10", - CheckID: "service:redis1", - } - - _, err = catalog.Deregister(dereg, nil) - - if err != nil { - t.Fatalf("err: %v", err) - } - - health, _, err := c.Health().Node("foobar", nil) - - if err != nil { - t.Fatalf("err: %v", err) - } - - if len(health) != 0 { - t.Fatalf("CheckID:service:redis1 is not deregistered") - } - - dereg = &CatalogDeregistration{ - Datacenter: "dc1", - Node: "foobar", - Address: "192.168.10.10", - } - - _, err = catalog.Deregister(dereg, nil) - - if err != nil { - t.Fatalf("err: %v", err) - } - - node, _, err = catalog.Node("foobar", nil) - - if err != nil { - t.Fatalf("err: %v", err) - } - - if node != nil { - t.Fatalf("node is not deregistered: %v", node) - } -} diff --git a/Godeps/_workspace/src/github.com/armon/consul-api/health_test.go b/Godeps/_workspace/src/github.com/armon/consul-api/health_test.go deleted file mode 100644 index d2b3da2e99..0000000000 --- a/Godeps/_workspace/src/github.com/armon/consul-api/health_test.go +++ /dev/null @@ -1,98 +0,0 @@ -package consulapi - -import ( - "testing" - "time" -) - -func TestHealth_Node(t *testing.T) { - c := makeClient(t) - agent := c.Agent() - health := c.Health() - - info, err := agent.Self() - if err != nil { - t.Fatalf("err: %v", err) - } - name := info["Config"]["NodeName"].(string) - - checks, meta, err := health.Node(name, nil) - if err != nil { - t.Fatalf("err: %v", err) - } - - if meta.LastIndex == 0 { - t.Fatalf("bad: %v", meta) - } - if len(checks) == 0 { - t.Fatalf("Bad: %v", checks) - } -} - -func TestHealth_Checks(t *testing.T) { - c := makeClient(t) - agent := c.Agent() - health := c.Health() - - // Make a service with a check - reg := &AgentServiceRegistration{ - Name: "foo", - Check: &AgentServiceCheck{ - TTL: "15s", - }, - } - if err := agent.ServiceRegister(reg); err != nil { - t.Fatalf("err: %v", err) - } - defer agent.ServiceDeregister("foo") - - // Wait for the register... - time.Sleep(20 * time.Millisecond) - - checks, meta, err := health.Checks("foo", nil) - if err != nil { - t.Fatalf("err: %v", err) - } - - if meta.LastIndex == 0 { - t.Fatalf("bad: %v", meta) - } - if len(checks) == 0 { - t.Fatalf("Bad: %v", checks) - } -} - -func TestHealth_Service(t *testing.T) { - c := makeClient(t) - health := c.Health() - - // consul service should always exist... - checks, meta, err := health.Service("consul", "", true, nil) - if err != nil { - t.Fatalf("err: %v", err) - } - - if meta.LastIndex == 0 { - t.Fatalf("bad: %v", meta) - } - if len(checks) == 0 { - t.Fatalf("Bad: %v", checks) - } -} - -func TestHealth_State(t *testing.T) { - c := makeClient(t) - health := c.Health() - - checks, meta, err := health.State("any", nil) - if err != nil { - t.Fatalf("err: %v", err) - } - - if meta.LastIndex == 0 { - t.Fatalf("bad: %v", meta) - } - if len(checks) == 0 { - t.Fatalf("Bad: %v", checks) - } -} diff --git a/Godeps/_workspace/src/github.com/hashicorp/consul/api/README.md b/Godeps/_workspace/src/github.com/hashicorp/consul/api/README.md new file mode 100644 index 0000000000..bce2ebb516 --- /dev/null +++ b/Godeps/_workspace/src/github.com/hashicorp/consul/api/README.md @@ -0,0 +1,39 @@ +Consul API client +================= + +This package provides the `api` package which attempts to +provide programmatic access to the full Consul API. + +Currently, all of the Consul APIs included in version 0.3 are supported. + +Documentation +============= + +The full documentation is available on [Godoc](http://godoc.org/github.com/hashicorp/consul/api) + +Usage +===== + +Below is an example of using the Consul client: + +```go +// Get a new client, with KV endpoints +client, _ := api.NewClient(api.DefaultConfig()) +kv := client.KV() + +// PUT a new KV pair +p := &api.KVPair{Key: "foo", Value: []byte("test")} +_, err := kv.Put(p, nil) +if err != nil { + panic(err) +} + +// Lookup the pair +pair, _, err := kv.Get("foo", nil) +if err != nil { + panic(err) +} +fmt.Printf("KV: %v", pair) + +``` + diff --git a/Godeps/_workspace/src/github.com/armon/consul-api/acl.go b/Godeps/_workspace/src/github.com/hashicorp/consul/api/acl.go similarity index 99% rename from Godeps/_workspace/src/github.com/armon/consul-api/acl.go rename to Godeps/_workspace/src/github.com/hashicorp/consul/api/acl.go index e0179f54df..c3fb0d53aa 100644 --- a/Godeps/_workspace/src/github.com/armon/consul-api/acl.go +++ b/Godeps/_workspace/src/github.com/hashicorp/consul/api/acl.go @@ -1,4 +1,4 @@ -package consulapi +package api const ( // ACLCLientType is the client type token diff --git a/Godeps/_workspace/src/github.com/armon/consul-api/acl_test.go b/Godeps/_workspace/src/github.com/hashicorp/consul/api/acl_test.go similarity index 92% rename from Godeps/_workspace/src/github.com/armon/consul-api/acl_test.go rename to Godeps/_workspace/src/github.com/hashicorp/consul/api/acl_test.go index 7932c5905a..166b892dbe 100644 --- a/Godeps/_workspace/src/github.com/armon/consul-api/acl_test.go +++ b/Godeps/_workspace/src/github.com/hashicorp/consul/api/acl_test.go @@ -1,4 +1,4 @@ -package consulapi +package api import ( "os" @@ -16,7 +16,9 @@ func TestACL_CreateDestroy(t *testing.T) { if CONSUL_ROOT == "" { t.SkipNow() } - c := makeClient(t) + c, s := makeClient(t) + defer s.stop() + c.config.Token = CONSUL_ROOT acl := c.ACL() @@ -62,7 +64,9 @@ func TestACL_CloneDestroy(t *testing.T) { if CONSUL_ROOT == "" { t.SkipNow() } - c := makeClient(t) + c, s := makeClient(t) + defer s.stop() + c.config.Token = CONSUL_ROOT acl := c.ACL() @@ -93,7 +97,9 @@ func TestACL_Info(t *testing.T) { if CONSUL_ROOT == "" { t.SkipNow() } - c := makeClient(t) + c, s := makeClient(t) + defer s.stop() + c.config.Token = CONSUL_ROOT acl := c.ACL() @@ -118,7 +124,9 @@ func TestACL_List(t *testing.T) { if CONSUL_ROOT == "" { t.SkipNow() } - c := makeClient(t) + c, s := makeClient(t) + defer s.stop() + c.config.Token = CONSUL_ROOT acl := c.ACL() diff --git a/Godeps/_workspace/src/github.com/armon/consul-api/agent.go b/Godeps/_workspace/src/github.com/hashicorp/consul/api/agent.go similarity index 75% rename from Godeps/_workspace/src/github.com/armon/consul-api/agent.go rename to Godeps/_workspace/src/github.com/hashicorp/consul/api/agent.go index eec93cb970..4b144fe181 100644 --- a/Godeps/_workspace/src/github.com/armon/consul-api/agent.go +++ b/Godeps/_workspace/src/github.com/hashicorp/consul/api/agent.go @@ -1,4 +1,4 @@ -package consulapi +package api import ( "fmt" @@ -22,6 +22,7 @@ type AgentService struct { Service string Tags []string Port int + Address string } // AgentMember represents a cluster member known to the agent @@ -41,18 +42,21 @@ type AgentMember struct { // AgentServiceRegistration is used to register a new service type AgentServiceRegistration struct { - ID string `json:",omitempty"` - Name string `json:",omitempty"` - Tags []string `json:",omitempty"` - Port int `json:",omitempty"` - Check *AgentServiceCheck + ID string `json:",omitempty"` + Name string `json:",omitempty"` + Tags []string `json:",omitempty"` + Port int `json:",omitempty"` + Address string `json:",omitempty"` + Check *AgentServiceCheck + Checks AgentServiceChecks } // AgentCheckRegistration is used to register a new check type AgentCheckRegistration struct { - ID string `json:",omitempty"` - Name string `json:",omitempty"` - Notes string `json:",omitempty"` + ID string `json:",omitempty"` + Name string `json:",omitempty"` + Notes string `json:",omitempty"` + ServiceID string `json:",omitempty"` AgentServiceCheck } @@ -61,8 +65,11 @@ type AgentCheckRegistration struct { type AgentServiceCheck struct { Script string `json:",omitempty"` Interval string `json:",omitempty"` + Timeout string `json:",omitempty"` TTL string `json:",omitempty"` + HTTP string `json:",omitempty"` } +type AgentServiceChecks []*AgentServiceCheck // Agent can be used to query the Agent endpoints type Agent struct { @@ -270,3 +277,57 @@ func (a *Agent) ForceLeave(node string) error { resp.Body.Close() return nil } + +// EnableServiceMaintenance toggles service maintenance mode on +// for the given service ID. +func (a *Agent) EnableServiceMaintenance(serviceID, reason string) error { + r := a.c.newRequest("PUT", "/v1/agent/service/maintenance/"+serviceID) + r.params.Set("enable", "true") + r.params.Set("reason", reason) + _, resp, err := requireOK(a.c.doRequest(r)) + if err != nil { + return err + } + resp.Body.Close() + return nil +} + +// DisableServiceMaintenance toggles service maintenance mode off +// for the given service ID. +func (a *Agent) DisableServiceMaintenance(serviceID string) error { + r := a.c.newRequest("PUT", "/v1/agent/service/maintenance/"+serviceID) + r.params.Set("enable", "false") + _, resp, err := requireOK(a.c.doRequest(r)) + if err != nil { + return err + } + resp.Body.Close() + return nil +} + +// EnableNodeMaintenance toggles node maintenance mode on for the +// agent we are connected to. +func (a *Agent) EnableNodeMaintenance(reason string) error { + r := a.c.newRequest("PUT", "/v1/agent/maintenance") + r.params.Set("enable", "true") + r.params.Set("reason", reason) + _, resp, err := requireOK(a.c.doRequest(r)) + if err != nil { + return err + } + resp.Body.Close() + return nil +} + +// DisableNodeMaintenance toggles node maintenance mode off for the +// agent we are connected to. +func (a *Agent) DisableNodeMaintenance() error { + r := a.c.newRequest("PUT", "/v1/agent/maintenance") + r.params.Set("enable", "false") + _, resp, err := requireOK(a.c.doRequest(r)) + if err != nil { + return err + } + resp.Body.Close() + return nil +} diff --git a/Godeps/_workspace/src/github.com/hashicorp/consul/api/agent_test.go b/Godeps/_workspace/src/github.com/hashicorp/consul/api/agent_test.go new file mode 100644 index 0000000000..60cc4ae1e6 --- /dev/null +++ b/Godeps/_workspace/src/github.com/hashicorp/consul/api/agent_test.go @@ -0,0 +1,404 @@ +package api + +import ( + "strings" + "testing" +) + +func TestAgent_Self(t *testing.T) { + c, s := makeClient(t) + defer s.stop() + + agent := c.Agent() + + info, err := agent.Self() + if err != nil { + t.Fatalf("err: %v", err) + } + + name := info["Config"]["NodeName"] + if name == "" { + t.Fatalf("bad: %v", info) + } +} + +func TestAgent_Members(t *testing.T) { + c, s := makeClient(t) + defer s.stop() + + agent := c.Agent() + + members, err := agent.Members(false) + if err != nil { + t.Fatalf("err: %v", err) + } + + if len(members) != 1 { + t.Fatalf("bad: %v", members) + } +} + +func TestAgent_Services(t *testing.T) { + c, s := makeClient(t) + defer s.stop() + + agent := c.Agent() + + reg := &AgentServiceRegistration{ + Name: "foo", + Tags: []string{"bar", "baz"}, + Port: 8000, + Check: &AgentServiceCheck{ + TTL: "15s", + }, + } + if err := agent.ServiceRegister(reg); err != nil { + t.Fatalf("err: %v", err) + } + + services, err := agent.Services() + if err != nil { + t.Fatalf("err: %v", err) + } + if _, ok := services["foo"]; !ok { + t.Fatalf("missing service: %v", services) + } + + checks, err := agent.Checks() + if err != nil { + t.Fatalf("err: %v", err) + } + if _, ok := checks["service:foo"]; !ok { + t.Fatalf("missing check: %v", checks) + } + + if err := agent.ServiceDeregister("foo"); err != nil { + t.Fatalf("err: %v", err) + } +} + +func TestAgent_ServiceAddress(t *testing.T) { + c, s := makeClient(t) + defer s.stop() + + agent := c.Agent() + + reg1 := &AgentServiceRegistration{ + Name: "foo1", + Port: 8000, + Address: "192.168.0.42", + } + reg2 := &AgentServiceRegistration{ + Name: "foo2", + Port: 8000, + } + if err := agent.ServiceRegister(reg1); err != nil { + t.Fatalf("err: %v", err) + } + if err := agent.ServiceRegister(reg2); err != nil { + t.Fatalf("err: %v", err) + } + + services, err := agent.Services() + if err != nil { + t.Fatalf("err: %v", err) + } + + if _, ok := services["foo1"]; !ok { + t.Fatalf("missing service: %v", services) + } + if _, ok := services["foo2"]; !ok { + t.Fatalf("missing service: %v", services) + } + + if services["foo1"].Address != "192.168.0.42" { + t.Fatalf("missing Address field in service foo1: %v", services) + } + if services["foo2"].Address != "" { + t.Fatalf("missing Address field in service foo2: %v", services) + } + + if err := agent.ServiceDeregister("foo"); err != nil { + t.Fatalf("err: %v", err) + } +} + +func TestAgent_Services_MultipleChecks(t *testing.T) { + c, s := makeClient(t) + defer s.stop() + + agent := c.Agent() + + reg := &AgentServiceRegistration{ + Name: "foo", + Tags: []string{"bar", "baz"}, + Port: 8000, + Checks: AgentServiceChecks{ + &AgentServiceCheck{ + TTL: "15s", + }, + &AgentServiceCheck{ + TTL: "30s", + }, + }, + } + if err := agent.ServiceRegister(reg); err != nil { + t.Fatalf("err: %v", err) + } + + services, err := agent.Services() + if err != nil { + t.Fatalf("err: %v", err) + } + if _, ok := services["foo"]; !ok { + t.Fatalf("missing service: %v", services) + } + + checks, err := agent.Checks() + if err != nil { + t.Fatalf("err: %v", err) + } + if _, ok := checks["service:foo:1"]; !ok { + t.Fatalf("missing check: %v", checks) + } + if _, ok := checks["service:foo:2"]; !ok { + t.Fatalf("missing check: %v", checks) + } +} + +func TestAgent_SetTTLStatus(t *testing.T) { + c, s := makeClient(t) + defer s.stop() + + agent := c.Agent() + + reg := &AgentServiceRegistration{ + Name: "foo", + Check: &AgentServiceCheck{ + TTL: "15s", + }, + } + if err := agent.ServiceRegister(reg); err != nil { + t.Fatalf("err: %v", err) + } + + if err := agent.WarnTTL("service:foo", "test"); err != nil { + t.Fatalf("err: %v", err) + } + + checks, err := agent.Checks() + if err != nil { + t.Fatalf("err: %v", err) + } + chk, ok := checks["service:foo"] + if !ok { + t.Fatalf("missing check: %v", checks) + } + if chk.Status != "warning" { + t.Fatalf("Bad: %#v", chk) + } + if chk.Output != "test" { + t.Fatalf("Bad: %#v", chk) + } + + if err := agent.ServiceDeregister("foo"); err != nil { + t.Fatalf("err: %v", err) + } +} + +func TestAgent_Checks(t *testing.T) { + c, s := makeClient(t) + defer s.stop() + + agent := c.Agent() + + reg := &AgentCheckRegistration{ + Name: "foo", + } + reg.TTL = "15s" + if err := agent.CheckRegister(reg); err != nil { + t.Fatalf("err: %v", err) + } + + checks, err := agent.Checks() + if err != nil { + t.Fatalf("err: %v", err) + } + if _, ok := checks["foo"]; !ok { + t.Fatalf("missing check: %v", checks) + } + + if err := agent.CheckDeregister("foo"); err != nil { + t.Fatalf("err: %v", err) + } +} + +func TestAgent_Checks_serviceBound(t *testing.T) { + c, s := makeClient(t) + defer s.stop() + + agent := c.Agent() + + // First register a service + serviceReg := &AgentServiceRegistration{ + Name: "redis", + } + if err := agent.ServiceRegister(serviceReg); err != nil { + t.Fatalf("err: %v", err) + } + + // Register a check bound to the service + reg := &AgentCheckRegistration{ + Name: "redischeck", + ServiceID: "redis", + } + reg.TTL = "15s" + if err := agent.CheckRegister(reg); err != nil { + t.Fatalf("err: %v", err) + } + + checks, err := agent.Checks() + if err != nil { + t.Fatalf("err: %v", err) + } + + check, ok := checks["redischeck"] + if !ok { + t.Fatalf("missing check: %v", checks) + } + if check.ServiceID != "redis" { + t.Fatalf("missing service association for check: %v", check) + } +} + +func TestAgent_Join(t *testing.T) { + c, s := makeClient(t) + defer s.stop() + + agent := c.Agent() + + info, err := agent.Self() + if err != nil { + t.Fatalf("err: %v", err) + } + + // Join ourself + addr := info["Config"]["AdvertiseAddr"].(string) + err = agent.Join(addr, false) + if err != nil { + t.Fatalf("err: %v", err) + } +} + +func TestAgent_ForceLeave(t *testing.T) { + c, s := makeClient(t) + defer s.stop() + + agent := c.Agent() + + // Eject somebody + err := agent.ForceLeave("foo") + if err != nil { + t.Fatalf("err: %v", err) + } +} + +func TestServiceMaintenance(t *testing.T) { + c, s := makeClient(t) + defer s.stop() + + agent := c.Agent() + + // First register a service + serviceReg := &AgentServiceRegistration{ + Name: "redis", + } + if err := agent.ServiceRegister(serviceReg); err != nil { + t.Fatalf("err: %v", err) + } + + // Enable maintenance mode + if err := agent.EnableServiceMaintenance("redis", "broken"); err != nil { + t.Fatalf("err: %s", err) + } + + // Ensure a critical check was added + checks, err := agent.Checks() + if err != nil { + t.Fatalf("err: %v", err) + } + found := false + for _, check := range checks { + if strings.Contains(check.CheckID, "maintenance") { + found = true + if check.Status != "critical" || check.Notes != "broken" { + t.Fatalf("bad: %#v", checks) + } + } + } + if !found { + t.Fatalf("bad: %#v", checks) + } + + // Disable maintenance mode + if err := agent.DisableServiceMaintenance("redis"); err != nil { + t.Fatalf("err: %s", err) + } + + // Ensure the critical health check was removed + checks, err = agent.Checks() + if err != nil { + t.Fatalf("err: %s", err) + } + for _, check := range checks { + if strings.Contains(check.CheckID, "maintenance") { + t.Fatalf("should have removed health check") + } + } +} + +func TestNodeMaintenance(t *testing.T) { + c, s := makeClient(t) + defer s.stop() + + agent := c.Agent() + + // Enable maintenance mode + if err := agent.EnableNodeMaintenance("broken"); err != nil { + t.Fatalf("err: %s", err) + } + + // Check that a critical check was added + checks, err := agent.Checks() + if err != nil { + t.Fatalf("err: %s", err) + } + found := false + for _, check := range checks { + if strings.Contains(check.CheckID, "maintenance") { + found = true + if check.Status != "critical" || check.Notes != "broken" { + t.Fatalf("bad: %#v", checks) + } + } + } + if !found { + t.Fatalf("bad: %#v", checks) + } + + // Disable maintenance mode + if err := agent.DisableNodeMaintenance(); err != nil { + t.Fatalf("err: %s", err) + } + + // Ensure the check was removed + checks, err = agent.Checks() + if err != nil { + t.Fatalf("err: %s", err) + } + for _, check := range checks { + if strings.Contains(check.CheckID, "maintenance") { + t.Fatalf("should have removed health check") + } + } +} diff --git a/Godeps/_workspace/src/github.com/armon/consul-api/api.go b/Godeps/_workspace/src/github.com/hashicorp/consul/api/api.go similarity index 89% rename from Godeps/_workspace/src/github.com/armon/consul-api/api.go rename to Godeps/_workspace/src/github.com/hashicorp/consul/api/api.go index e1335769b7..aa0060816d 100644 --- a/Godeps/_workspace/src/github.com/armon/consul-api/api.go +++ b/Godeps/_workspace/src/github.com/hashicorp/consul/api/api.go @@ -1,13 +1,16 @@ -package consulapi +package api import ( "bytes" "encoding/json" "fmt" "io" + "net" "net/http" "net/url" + "os" "strconv" + "strings" "time" ) @@ -111,11 +114,17 @@ type Config struct { // DefaultConfig returns a default configuration for the client func DefaultConfig() *Config { - return &Config{ + config := &Config{ Address: "127.0.0.1:8500", Scheme: "http", HttpClient: http.DefaultClient, } + + if addr := os.Getenv("CONSUL_HTTP_ADDR"); addr != "" { + config.Address = addr + } + + return config } // Client provides a client to the Consul API @@ -140,6 +149,17 @@ func NewClient(config *Config) (*Client, error) { config.HttpClient = defConfig.HttpClient } + if parts := strings.SplitN(config.Address, "unix://", 2); len(parts) == 2 { + config.HttpClient = &http.Client{ + Transport: &http.Transport{ + Dial: func(_, _ string) (net.Conn, error) { + return net.Dial("unix", parts[1]) + }, + }, + } + config.Address = parts[1] + } + client := &Client{ config: *config, } @@ -206,9 +226,6 @@ func (r *request) toHTTP() (*http.Request, error) { // Encode the query parameters r.url.RawQuery = r.params.Encode() - // Get the url sring - urlRaw := r.url.String() - // Check if we should encode the body if r.body == nil && r.obj != nil { if b, err := encodeBody(r.obj); err != nil { @@ -219,14 +236,21 @@ func (r *request) toHTTP() (*http.Request, error) { } // Create the HTTP request - req, err := http.NewRequest(r.method, urlRaw, r.body) + req, err := http.NewRequest(r.method, r.url.RequestURI(), r.body) + if err != nil { + return nil, err + } + + req.URL.Host = r.url.Host + req.URL.Scheme = r.url.Scheme + req.Host = r.url.Host // Setup auth - if err == nil && r.config.HttpAuth != nil { + if r.config.HttpAuth != nil { req.SetBasicAuth(r.config.HttpAuth.Username, r.config.HttpAuth.Password) } - return req, err + return req, nil } // newRequest is used to create a new request @@ -312,12 +336,16 @@ func encodeBody(obj interface{}) (io.Reader, error) { // requireOK is used to wrap doRequest and check for a 200 func requireOK(d time.Duration, resp *http.Response, e error) (time.Duration, *http.Response, error) { if e != nil { - return d, resp, e + if resp != nil { + resp.Body.Close() + } + return d, nil, e } if resp.StatusCode != 200 { var buf bytes.Buffer io.Copy(&buf, resp.Body) - return d, resp, fmt.Errorf("Unexpected response code: %d (%s)", resp.StatusCode, buf.Bytes()) + resp.Body.Close() + return d, nil, fmt.Errorf("Unexpected response code: %d (%s)", resp.StatusCode, buf.Bytes()) } - return d, resp, e + return d, resp, nil } diff --git a/Godeps/_workspace/src/github.com/hashicorp/consul/api/api_test.go b/Godeps/_workspace/src/github.com/hashicorp/consul/api/api_test.go new file mode 100644 index 0000000000..cbf6ccefeb --- /dev/null +++ b/Godeps/_workspace/src/github.com/hashicorp/consul/api/api_test.go @@ -0,0 +1,339 @@ +package api + +import ( + crand "crypto/rand" + "encoding/json" + "fmt" + "io/ioutil" + "net/http" + "os" + "os/exec" + "path/filepath" + "runtime" + "testing" + "time" + + "github.com/hashicorp/consul/testutil" +) + +var consulConfig = `{ + "ports": { + "dns": 19000, + "http": 18800, + "rpc": 18600, + "serf_lan": 18200, + "serf_wan": 18400, + "server": 18000 + }, + "bind_addr": "127.0.0.1", + "data_dir": "%s", + "bootstrap": true, + "log_level": "debug", + "server": true +}` + +type testServer struct { + pid int + dataDir string + configFile string +} + +type testPortConfig struct { + DNS int `json:"dns,omitempty"` + HTTP int `json:"http,omitempty"` + RPC int `json:"rpc,omitempty"` + SerfLan int `json:"serf_lan,omitempty"` + SerfWan int `json:"serf_wan,omitempty"` + Server int `json:"server,omitempty"` +} + +type testAddressConfig struct { + HTTP string `json:"http,omitempty"` +} + +type testServerConfig struct { + Bootstrap bool `json:"bootstrap,omitempty"` + Server bool `json:"server,omitempty"` + DataDir string `json:"data_dir,omitempty"` + LogLevel string `json:"log_level,omitempty"` + Addresses *testAddressConfig `json:"addresses,omitempty"` + Ports testPortConfig `json:"ports,omitempty"` +} + +// Callback functions for modifying config +type configCallback func(c *Config) +type serverConfigCallback func(c *testServerConfig) + +func defaultConfig() *testServerConfig { + return &testServerConfig{ + Bootstrap: true, + Server: true, + LogLevel: "debug", + Ports: testPortConfig{ + DNS: 19000, + HTTP: 18800, + RPC: 18600, + SerfLan: 18200, + SerfWan: 18400, + Server: 18000, + }, + } +} + +func (s *testServer) stop() { + defer os.RemoveAll(s.dataDir) + defer os.RemoveAll(s.configFile) + + cmd := exec.Command("kill", "-9", fmt.Sprintf("%d", s.pid)) + if err := cmd.Run(); err != nil { + panic(err) + } +} + +func newTestServer(t *testing.T) *testServer { + return newTestServerWithConfig(t, func(c *testServerConfig) {}) +} + +func newTestServerWithConfig(t *testing.T, cb serverConfigCallback) *testServer { + if path, err := exec.LookPath("consul"); err != nil || path == "" { + t.Log("consul not found on $PATH, skipping") + t.SkipNow() + } + + pidFile, err := ioutil.TempFile("", "consul") + if err != nil { + t.Fatalf("err: %s", err) + } + pidFile.Close() + os.Remove(pidFile.Name()) + + dataDir, err := ioutil.TempDir("", "consul") + if err != nil { + t.Fatalf("err: %s", err) + } + + configFile, err := ioutil.TempFile("", "consul") + if err != nil { + t.Fatalf("err: %s", err) + } + + consulConfig := defaultConfig() + consulConfig.DataDir = dataDir + + cb(consulConfig) + + configContent, err := json.Marshal(consulConfig) + if err != nil { + t.Fatalf("err: %s", err) + } + + if _, err := configFile.Write(configContent); err != nil { + t.Fatalf("err: %s", err) + } + configFile.Close() + + // Start the server + cmd := exec.Command("consul", "agent", "-config-file", configFile.Name()) + cmd.Stdout = os.Stdout + cmd.Stderr = os.Stderr + if err := cmd.Start(); err != nil { + t.Fatalf("err: %s", err) + } + + return &testServer{ + pid: cmd.Process.Pid, + dataDir: dataDir, + configFile: configFile.Name(), + } +} + +func makeClient(t *testing.T) (*Client, *testServer) { + return makeClientWithConfig(t, func(c *Config) { + c.Address = "127.0.0.1:18800" + }, func(c *testServerConfig) {}) +} + +func makeClientWithConfig(t *testing.T, cb1 configCallback, cb2 serverConfigCallback) (*Client, *testServer) { + // Make client config + conf := DefaultConfig() + cb1(conf) + + // Create client + client, err := NewClient(conf) + if err != nil { + t.Fatalf("err: %v", err) + } + + // Create server + server := newTestServerWithConfig(t, cb2) + + // Allow the server some time to start, and verify we have a leader. + testutil.WaitForResult(func() (bool, error) { + req := client.newRequest("GET", "/v1/catalog/nodes") + _, resp, err := client.doRequest(req) + if err != nil { + return false, err + } + resp.Body.Close() + + // Ensure we have a leader and a node registeration + if leader := resp.Header.Get("X-Consul-KnownLeader"); leader != "true" { + return false, fmt.Errorf("Consul leader status: %#v", leader) + } + if resp.Header.Get("X-Consul-Index") == "0" { + return false, fmt.Errorf("Consul index is 0") + } + + return true, nil + }, func(err error) { + t.Fatalf("err: %s", err) + }) + + return client, server +} + +func testKey() string { + buf := make([]byte, 16) + if _, err := crand.Read(buf); err != nil { + panic(fmt.Errorf("Failed to read random bytes: %v", err)) + } + + return fmt.Sprintf("%08x-%04x-%04x-%04x-%12x", + buf[0:4], + buf[4:6], + buf[6:8], + buf[8:10], + buf[10:16]) +} + +func TestSetQueryOptions(t *testing.T) { + c, s := makeClient(t) + defer s.stop() + + r := c.newRequest("GET", "/v1/kv/foo") + q := &QueryOptions{ + Datacenter: "foo", + AllowStale: true, + RequireConsistent: true, + WaitIndex: 1000, + WaitTime: 100 * time.Second, + Token: "12345", + } + r.setQueryOptions(q) + + if r.params.Get("dc") != "foo" { + t.Fatalf("bad: %v", r.params) + } + if _, ok := r.params["stale"]; !ok { + t.Fatalf("bad: %v", r.params) + } + if _, ok := r.params["consistent"]; !ok { + t.Fatalf("bad: %v", r.params) + } + if r.params.Get("index") != "1000" { + t.Fatalf("bad: %v", r.params) + } + if r.params.Get("wait") != "100000ms" { + t.Fatalf("bad: %v", r.params) + } + if r.params.Get("token") != "12345" { + t.Fatalf("bad: %v", r.params) + } +} + +func TestSetWriteOptions(t *testing.T) { + c, s := makeClient(t) + defer s.stop() + + r := c.newRequest("GET", "/v1/kv/foo") + q := &WriteOptions{ + Datacenter: "foo", + Token: "23456", + } + r.setWriteOptions(q) + + if r.params.Get("dc") != "foo" { + t.Fatalf("bad: %v", r.params) + } + if r.params.Get("token") != "23456" { + t.Fatalf("bad: %v", r.params) + } +} + +func TestRequestToHTTP(t *testing.T) { + c, s := makeClient(t) + defer s.stop() + + r := c.newRequest("DELETE", "/v1/kv/foo") + q := &QueryOptions{ + Datacenter: "foo", + } + r.setQueryOptions(q) + req, err := r.toHTTP() + if err != nil { + t.Fatalf("err: %v", err) + } + + if req.Method != "DELETE" { + t.Fatalf("bad: %v", req) + } + if req.URL.RequestURI() != "/v1/kv/foo?dc=foo" { + t.Fatalf("bad: %v", req) + } +} + +func TestParseQueryMeta(t *testing.T) { + resp := &http.Response{ + Header: make(map[string][]string), + } + resp.Header.Set("X-Consul-Index", "12345") + resp.Header.Set("X-Consul-LastContact", "80") + resp.Header.Set("X-Consul-KnownLeader", "true") + + qm := &QueryMeta{} + if err := parseQueryMeta(resp, qm); err != nil { + t.Fatalf("err: %v", err) + } + + if qm.LastIndex != 12345 { + t.Fatalf("Bad: %v", qm) + } + if qm.LastContact != 80*time.Millisecond { + t.Fatalf("Bad: %v", qm) + } + if !qm.KnownLeader { + t.Fatalf("Bad: %v", qm) + } +} + +func TestAPI_UnixSocket(t *testing.T) { + if runtime.GOOS == "windows" { + t.SkipNow() + } + + tempDir, err := ioutil.TempDir("", "consul") + if err != nil { + t.Fatalf("err: %s", err) + } + defer os.RemoveAll(tempDir) + socket := filepath.Join(tempDir, "test.sock") + + c, s := makeClientWithConfig(t, func(c *Config) { + c.Address = "unix://" + socket + }, func(c *testServerConfig) { + c.Addresses = &testAddressConfig{ + HTTP: "unix://" + socket, + } + }) + defer s.stop() + + agent := c.Agent() + + info, err := agent.Self() + if err != nil { + t.Fatalf("err: %s", err) + } + if info["Config"]["NodeName"] == "" { + t.Fatalf("bad: %v", info) + } +} diff --git a/Godeps/_workspace/src/github.com/armon/consul-api/catalog.go b/Godeps/_workspace/src/github.com/hashicorp/consul/api/catalog.go similarity index 99% rename from Godeps/_workspace/src/github.com/armon/consul-api/catalog.go rename to Godeps/_workspace/src/github.com/hashicorp/consul/api/catalog.go index 8080e2a910..fee1695677 100644 --- a/Godeps/_workspace/src/github.com/armon/consul-api/catalog.go +++ b/Godeps/_workspace/src/github.com/hashicorp/consul/api/catalog.go @@ -1,4 +1,4 @@ -package consulapi +package api type Node struct { Node string diff --git a/Godeps/_workspace/src/github.com/hashicorp/consul/api/catalog_test.go b/Godeps/_workspace/src/github.com/hashicorp/consul/api/catalog_test.go new file mode 100644 index 0000000000..61980fcc24 --- /dev/null +++ b/Godeps/_workspace/src/github.com/hashicorp/consul/api/catalog_test.go @@ -0,0 +1,273 @@ +package api + +import ( + "fmt" + "testing" + + "github.com/hashicorp/consul/testutil" +) + +func TestCatalog_Datacenters(t *testing.T) { + c, s := makeClient(t) + defer s.stop() + + catalog := c.Catalog() + + testutil.WaitForResult(func() (bool, error) { + datacenters, err := catalog.Datacenters() + if err != nil { + return false, err + } + + if len(datacenters) == 0 { + return false, fmt.Errorf("Bad: %v", datacenters) + } + + return true, nil + }, func(err error) { + t.Fatalf("err: %s", err) + }) +} + +func TestCatalog_Nodes(t *testing.T) { + c, s := makeClient(t) + defer s.stop() + + catalog := c.Catalog() + + testutil.WaitForResult(func() (bool, error) { + nodes, meta, err := catalog.Nodes(nil) + if err != nil { + return false, err + } + + if meta.LastIndex == 0 { + return false, fmt.Errorf("Bad: %v", meta) + } + + if len(nodes) == 0 { + return false, fmt.Errorf("Bad: %v", nodes) + } + + return true, nil + }, func(err error) { + t.Fatalf("err: %s", err) + }) +} + +func TestCatalog_Services(t *testing.T) { + c, s := makeClient(t) + defer s.stop() + + catalog := c.Catalog() + + testutil.WaitForResult(func() (bool, error) { + services, meta, err := catalog.Services(nil) + if err != nil { + return false, err + } + + if meta.LastIndex == 0 { + return false, fmt.Errorf("Bad: %v", meta) + } + + if len(services) == 0 { + return false, fmt.Errorf("Bad: %v", services) + } + + return true, nil + }, func(err error) { + t.Fatalf("err: %s", err) + }) +} + +func TestCatalog_Service(t *testing.T) { + c, s := makeClient(t) + defer s.stop() + + catalog := c.Catalog() + + testutil.WaitForResult(func() (bool, error) { + services, meta, err := catalog.Service("consul", "", nil) + if err != nil { + return false, err + } + + if meta.LastIndex == 0 { + return false, fmt.Errorf("Bad: %v", meta) + } + + if len(services) == 0 { + return false, fmt.Errorf("Bad: %v", services) + } + + return true, nil + }, func(err error) { + t.Fatalf("err: %s", err) + }) +} + +func TestCatalog_Node(t *testing.T) { + c, s := makeClient(t) + defer s.stop() + + catalog := c.Catalog() + name, _ := c.Agent().NodeName() + + testutil.WaitForResult(func() (bool, error) { + info, meta, err := catalog.Node(name, nil) + if err != nil { + return false, err + } + + if meta.LastIndex == 0 { + return false, fmt.Errorf("Bad: %v", meta) + } + if len(info.Services) == 0 { + return false, fmt.Errorf("Bad: %v", info) + } + + return true, nil + }, func(err error) { + t.Fatalf("err: %s", err) + }) +} + +func TestCatalog_Registration(t *testing.T) { + c, s := makeClient(t) + defer s.stop() + + catalog := c.Catalog() + + service := &AgentService{ + ID: "redis1", + Service: "redis", + Tags: []string{"master", "v1"}, + Port: 8000, + } + + check := &AgentCheck{ + Node: "foobar", + CheckID: "service:redis1", + Name: "Redis health check", + Notes: "Script based health check", + Status: "passing", + ServiceID: "redis1", + } + + reg := &CatalogRegistration{ + Datacenter: "dc1", + Node: "foobar", + Address: "192.168.10.10", + Service: service, + Check: check, + } + + testutil.WaitForResult(func() (bool, error) { + if _, err := catalog.Register(reg, nil); err != nil { + return false, err + } + + node, _, err := catalog.Node("foobar", nil) + if err != nil { + return false, err + } + + if _, ok := node.Services["redis1"]; !ok { + return false, fmt.Errorf("missing service: redis1") + } + + health, _, err := c.Health().Node("foobar", nil) + if err != nil { + return false, err + } + + if health[0].CheckID != "service:redis1" { + return false, fmt.Errorf("missing checkid service:redis1") + } + + return true, nil + }, func(err error) { + t.Fatalf("err: %s", err) + }) + + // Test catalog deregistration of the previously registered service + dereg := &CatalogDeregistration{ + Datacenter: "dc1", + Node: "foobar", + Address: "192.168.10.10", + ServiceID: "redis1", + } + + if _, err := catalog.Deregister(dereg, nil); err != nil { + t.Fatalf("err: %v", err) + } + + testutil.WaitForResult(func() (bool, error) { + node, _, err := catalog.Node("foobar", nil) + if err != nil { + return false, err + } + + if _, ok := node.Services["redis1"]; ok { + return false, fmt.Errorf("ServiceID:redis1 is not deregistered") + } + + return true, nil + }, func(err error) { + t.Fatalf("err: %s", err) + }) + + // Test deregistration of the previously registered check + dereg = &CatalogDeregistration{ + Datacenter: "dc1", + Node: "foobar", + Address: "192.168.10.10", + CheckID: "service:redis1", + } + + if _, err := catalog.Deregister(dereg, nil); err != nil { + t.Fatalf("err: %v", err) + } + + testutil.WaitForResult(func() (bool, error) { + health, _, err := c.Health().Node("foobar", nil) + if err != nil { + return false, err + } + + if len(health) != 0 { + return false, fmt.Errorf("CheckID:service:redis1 is not deregistered") + } + + return true, nil + }, func(err error) { + t.Fatalf("err: %s", err) + }) + + // Test node deregistration of the previously registered node + dereg = &CatalogDeregistration{ + Datacenter: "dc1", + Node: "foobar", + Address: "192.168.10.10", + } + + if _, err := catalog.Deregister(dereg, nil); err != nil { + t.Fatalf("err: %v", err) + } + + testutil.WaitForResult(func() (bool, error) { + node, _, err := catalog.Node("foobar", nil) + if err != nil { + return false, err + } + + if node != nil { + return false, fmt.Errorf("node is not deregistered: %v", node) + } + + return true, nil + }, func(err error) { + t.Fatalf("err: %s", err) + }) +} diff --git a/Godeps/_workspace/src/github.com/armon/consul-api/event.go b/Godeps/_workspace/src/github.com/hashicorp/consul/api/event.go similarity index 99% rename from Godeps/_workspace/src/github.com/armon/consul-api/event.go rename to Godeps/_workspace/src/github.com/hashicorp/consul/api/event.go index 59813d40fa..85b5b069b0 100644 --- a/Godeps/_workspace/src/github.com/armon/consul-api/event.go +++ b/Godeps/_workspace/src/github.com/hashicorp/consul/api/event.go @@ -1,4 +1,4 @@ -package consulapi +package api import ( "bytes" diff --git a/Godeps/_workspace/src/github.com/armon/consul-api/event_test.go b/Godeps/_workspace/src/github.com/hashicorp/consul/api/event_test.go similarity index 91% rename from Godeps/_workspace/src/github.com/armon/consul-api/event_test.go rename to Godeps/_workspace/src/github.com/hashicorp/consul/api/event_test.go index f2be010ad9..974c40e42d 100644 --- a/Godeps/_workspace/src/github.com/armon/consul-api/event_test.go +++ b/Godeps/_workspace/src/github.com/hashicorp/consul/api/event_test.go @@ -1,11 +1,13 @@ -package consulapi +package api import ( "testing" ) func TestEvent_FireList(t *testing.T) { - c := makeClient(t) + c, s := makeClient(t) + defer s.stop() + event := c.Event() params := &UserEvent{Name: "foo"} diff --git a/Godeps/_workspace/src/github.com/armon/consul-api/health.go b/Godeps/_workspace/src/github.com/hashicorp/consul/api/health.go similarity index 99% rename from Godeps/_workspace/src/github.com/armon/consul-api/health.go rename to Godeps/_workspace/src/github.com/hashicorp/consul/api/health.go index 574801e29b..02b161e28e 100644 --- a/Godeps/_workspace/src/github.com/armon/consul-api/health.go +++ b/Godeps/_workspace/src/github.com/hashicorp/consul/api/health.go @@ -1,4 +1,4 @@ -package consulapi +package api import ( "fmt" diff --git a/Godeps/_workspace/src/github.com/hashicorp/consul/api/health_test.go b/Godeps/_workspace/src/github.com/hashicorp/consul/api/health_test.go new file mode 100644 index 0000000000..e445910240 --- /dev/null +++ b/Godeps/_workspace/src/github.com/hashicorp/consul/api/health_test.go @@ -0,0 +1,121 @@ +package api + +import ( + "fmt" + "testing" + + "github.com/hashicorp/consul/testutil" +) + +func TestHealth_Node(t *testing.T) { + c, s := makeClient(t) + defer s.stop() + + agent := c.Agent() + health := c.Health() + + info, err := agent.Self() + if err != nil { + t.Fatalf("err: %v", err) + } + name := info["Config"]["NodeName"].(string) + + testutil.WaitForResult(func() (bool, error) { + checks, meta, err := health.Node(name, nil) + if err != nil { + return false, err + } + if meta.LastIndex == 0 { + return false, fmt.Errorf("bad: %v", meta) + } + if len(checks) == 0 { + return false, fmt.Errorf("bad: %v", checks) + } + return true, nil + }, func(err error) { + t.Fatalf("err: %s", err) + }) +} + +func TestHealth_Checks(t *testing.T) { + c, s := makeClient(t) + defer s.stop() + + agent := c.Agent() + health := c.Health() + + // Make a service with a check + reg := &AgentServiceRegistration{ + Name: "foo", + Check: &AgentServiceCheck{ + TTL: "15s", + }, + } + if err := agent.ServiceRegister(reg); err != nil { + t.Fatalf("err: %v", err) + } + defer agent.ServiceDeregister("foo") + + testutil.WaitForResult(func() (bool, error) { + checks, meta, err := health.Checks("foo", nil) + if err != nil { + return false, err + } + if meta.LastIndex == 0 { + return false, fmt.Errorf("bad: %v", meta) + } + if len(checks) == 0 { + return false, fmt.Errorf("Bad: %v", checks) + } + return true, nil + }, func(err error) { + t.Fatalf("err: %s", err) + }) +} + +func TestHealth_Service(t *testing.T) { + c, s := makeClient(t) + defer s.stop() + + health := c.Health() + + testutil.WaitForResult(func() (bool, error) { + // consul service should always exist... + checks, meta, err := health.Service("consul", "", true, nil) + if err != nil { + return false, err + } + if meta.LastIndex == 0 { + return false, fmt.Errorf("bad: %v", meta) + } + if len(checks) == 0 { + return false, fmt.Errorf("Bad: %v", checks) + } + return true, nil + }, func(err error) { + t.Fatalf("err: %s", err) + }) +} + +func TestHealth_State(t *testing.T) { + c, s := makeClient(t) + defer s.stop() + + health := c.Health() + + testutil.WaitForResult(func() (bool, error) { + checks, meta, err := health.State("any", nil) + if err != nil { + return false, err + } + if meta.LastIndex == 0 { + return false, fmt.Errorf("bad: %v", meta) + } + if len(checks) == 0 { + return false, fmt.Errorf("Bad: %v", checks) + } + return true, nil + }, func(err error) { + t.Fatalf("err: %s", err) + }) +} diff --git a/Godeps/_workspace/src/github.com/armon/consul-api/kv.go b/Godeps/_workspace/src/github.com/hashicorp/consul/api/kv.go similarity index 84% rename from Godeps/_workspace/src/github.com/armon/consul-api/kv.go rename to Godeps/_workspace/src/github.com/hashicorp/consul/api/kv.go index 98c3b1a035..ba74057fcc 100644 --- a/Godeps/_workspace/src/github.com/armon/consul-api/kv.go +++ b/Godeps/_workspace/src/github.com/hashicorp/consul/api/kv.go @@ -1,4 +1,4 @@ -package consulapi +package api import ( "bytes" @@ -193,27 +193,44 @@ func (k *KV) put(key string, params map[string]string, body []byte, q *WriteOpti // Delete is used to delete a single key func (k *KV) Delete(key string, w *WriteOptions) (*WriteMeta, error) { - return k.deleteInternal(key, nil, w) + _, qm, err := k.deleteInternal(key, nil, w) + return qm, err +} + +// DeleteCAS is used for a Delete Check-And-Set operation. The Key +// and ModifyIndex are respected. Returns true on success or false on failures. +func (k *KV) DeleteCAS(p *KVPair, q *WriteOptions) (bool, *WriteMeta, error) { + params := map[string]string{ + "cas": strconv.FormatUint(p.ModifyIndex, 10), + } + return k.deleteInternal(p.Key, params, q) } // DeleteTree is used to delete all keys under a prefix func (k *KV) DeleteTree(prefix string, w *WriteOptions) (*WriteMeta, error) { - return k.deleteInternal(prefix, []string{"recurse"}, w) + _, qm, err := k.deleteInternal(prefix, map[string]string{"recurse": ""}, w) + return qm, err } -func (k *KV) deleteInternal(key string, params []string, q *WriteOptions) (*WriteMeta, error) { +func (k *KV) deleteInternal(key string, params map[string]string, q *WriteOptions) (bool, *WriteMeta, error) { r := k.c.newRequest("DELETE", "/v1/kv/"+key) r.setWriteOptions(q) - for _, param := range params { - r.params.Set(param, "") + for param, val := range params { + r.params.Set(param, val) } rtt, resp, err := requireOK(k.c.doRequest(r)) if err != nil { - return nil, err + return false, nil, err } - resp.Body.Close() + defer resp.Body.Close() qm := &WriteMeta{} qm.RequestTime = rtt - return qm, nil + + var buf bytes.Buffer + if _, err := io.Copy(&buf, resp.Body); err != nil { + return false, nil, fmt.Errorf("Failed to read response: %v", err) + } + res := strings.Contains(string(buf.Bytes()), "true") + return res, qm, nil } diff --git a/Godeps/_workspace/src/github.com/armon/consul-api/kv_test.go b/Godeps/_workspace/src/github.com/hashicorp/consul/api/kv_test.go similarity index 85% rename from Godeps/_workspace/src/github.com/armon/consul-api/kv_test.go rename to Godeps/_workspace/src/github.com/hashicorp/consul/api/kv_test.go index 2d92d69f62..8f2b54945d 100644 --- a/Godeps/_workspace/src/github.com/armon/consul-api/kv_test.go +++ b/Godeps/_workspace/src/github.com/hashicorp/consul/api/kv_test.go @@ -1,4 +1,4 @@ -package consulapi +package api import ( "bytes" @@ -8,7 +8,9 @@ import ( ) func TestClientPutGetDelete(t *testing.T) { - c := makeClient(t) + c, s := makeClient(t) + defer s.stop() + kv := c.KV() // Get a get without a key @@ -62,7 +64,9 @@ func TestClientPutGetDelete(t *testing.T) { } func TestClient_List_DeleteRecurse(t *testing.T) { - c := makeClient(t) + c, s := makeClient(t) + defer s.stop() + kv := c.KV() // Generate some test keys @@ -113,8 +117,55 @@ func TestClient_List_DeleteRecurse(t *testing.T) { } } +func TestClient_DeleteCAS(t *testing.T) { + c, s := makeClient(t) + defer s.stop() + + kv := c.KV() + + // Put the key + key := testKey() + value := []byte("test") + p := &KVPair{Key: key, Value: value} + if work, _, err := kv.CAS(p, nil); err != nil { + t.Fatalf("err: %v", err) + } else if !work { + t.Fatalf("CAS failure") + } + + // Get should work + pair, meta, err := kv.Get(key, nil) + if err != nil { + t.Fatalf("err: %v", err) + } + if pair == nil { + t.Fatalf("expected value: %#v", pair) + } + if meta.LastIndex == 0 { + t.Fatalf("unexpected value: %#v", meta) + } + + // CAS update with bad index + p.ModifyIndex = 1 + if work, _, err := kv.DeleteCAS(p, nil); err != nil { + t.Fatalf("err: %v", err) + } else if work { + t.Fatalf("unexpected CAS") + } + + // CAS update with valid index + p.ModifyIndex = meta.LastIndex + if work, _, err := kv.DeleteCAS(p, nil); err != nil { + t.Fatalf("err: %v", err) + } else if !work { + t.Fatalf("unexpected CAS failure") + } +} + func TestClient_CAS(t *testing.T) { - c := makeClient(t) + c, s := makeClient(t) + defer s.stop() + kv := c.KV() // Put the key @@ -159,7 +210,9 @@ func TestClient_CAS(t *testing.T) { } func TestClient_WatchGet(t *testing.T) { - c := makeClient(t) + c, s := makeClient(t) + defer s.stop() + kv := c.KV() // Get a get without a key @@ -178,7 +231,6 @@ func TestClient_WatchGet(t *testing.T) { // Put the key value := []byte("test") go func() { - c := makeClient(t) kv := c.KV() time.Sleep(100 * time.Millisecond) @@ -209,7 +261,9 @@ func TestClient_WatchGet(t *testing.T) { } func TestClient_WatchList(t *testing.T) { - c := makeClient(t) + c, s := makeClient(t) + defer s.stop() + kv := c.KV() // Get a get without a key @@ -229,7 +283,6 @@ func TestClient_WatchList(t *testing.T) { // Put the key value := []byte("test") go func() { - c := makeClient(t) kv := c.KV() time.Sleep(100 * time.Millisecond) @@ -261,7 +314,9 @@ func TestClient_WatchList(t *testing.T) { } func TestClient_Keys_DeleteRecurse(t *testing.T) { - c := makeClient(t) + c, s := makeClient(t) + defer s.stop() + kv := c.KV() // Generate some test keys @@ -308,7 +363,9 @@ func TestClient_Keys_DeleteRecurse(t *testing.T) { } func TestClient_AcquireRelease(t *testing.T) { - c := makeClient(t) + c, s := makeClient(t) + defer s.stop() + session := c.Session() kv := c.KV() diff --git a/Godeps/_workspace/src/github.com/hashicorp/consul/api/lock.go b/Godeps/_workspace/src/github.com/hashicorp/consul/api/lock.go new file mode 100644 index 0000000000..f6fdbbb166 --- /dev/null +++ b/Godeps/_workspace/src/github.com/hashicorp/consul/api/lock.go @@ -0,0 +1,321 @@ +package api + +import ( + "fmt" + "sync" + "time" +) + +const ( + // DefaultLockSessionName is the Session Name we assign if none is provided + DefaultLockSessionName = "Consul API Lock" + + // DefaultLockSessionTTL is the default session TTL if no Session is provided + // when creating a new Lock. This is used because we do not have another + // other check to depend upon. + DefaultLockSessionTTL = "15s" + + // DefaultLockWaitTime is how long we block for at a time to check if lock + // acquisition is possible. This affects the minimum time it takes to cancel + // a Lock acquisition. + DefaultLockWaitTime = 15 * time.Second + + // DefaultLockRetryTime is how long we wait after a failed lock acquisition + // before attempting to do the lock again. This is so that once a lock-delay + // is in affect, we do not hot loop retrying the acquisition. + DefaultLockRetryTime = 5 * time.Second + + // LockFlagValue is a magic flag we set to indicate a key + // is being used for a lock. It is used to detect a potential + // conflict with a semaphore. + LockFlagValue = 0x2ddccbc058a50c18 +) + +var ( + // ErrLockHeld is returned if we attempt to double lock + ErrLockHeld = fmt.Errorf("Lock already held") + + // ErrLockNotHeld is returned if we attempt to unlock a lock + // that we do not hold. + ErrLockNotHeld = fmt.Errorf("Lock not held") + + // ErrLockInUse is returned if we attempt to destroy a lock + // that is in use. + ErrLockInUse = fmt.Errorf("Lock in use") + + // ErrLockConflict is returned if the flags on a key + // used for a lock do not match expectation + ErrLockConflict = fmt.Errorf("Existing key does not match lock use") +) + +// Lock is used to implement client-side leader election. It is follows the +// algorithm as described here: https://consul.io/docs/guides/leader-election.html. +type Lock struct { + c *Client + opts *LockOptions + + isHeld bool + sessionRenew chan struct{} + lockSession string + l sync.Mutex +} + +// LockOptions is used to parameterize the Lock behavior. +type LockOptions struct { + Key string // Must be set and have write permissions + Value []byte // Optional, value to associate with the lock + Session string // Optional, created if not specified + SessionName string // Optional, defaults to DefaultLockSessionName + SessionTTL string // Optional, defaults to DefaultLockSessionTTL +} + +// LockKey returns a handle to a lock struct which can be used +// to acquire and release the mutex. The key used must have +// write permissions. +func (c *Client) LockKey(key string) (*Lock, error) { + opts := &LockOptions{ + Key: key, + } + return c.LockOpts(opts) +} + +// LockOpts returns a handle to a lock struct which can be used +// to acquire and release the mutex. The key used must have +// write permissions. +func (c *Client) LockOpts(opts *LockOptions) (*Lock, error) { + if opts.Key == "" { + return nil, fmt.Errorf("missing key") + } + if opts.SessionName == "" { + opts.SessionName = DefaultLockSessionName + } + if opts.SessionTTL == "" { + opts.SessionTTL = DefaultLockSessionTTL + } else { + if _, err := time.ParseDuration(opts.SessionTTL); err != nil { + return nil, fmt.Errorf("invalid SessionTTL: %v", err) + } + } + l := &Lock{ + c: c, + opts: opts, + } + return l, nil +} + +// Lock attempts to acquire the lock and blocks while doing so. +// Providing a non-nil stopCh can be used to abort the lock attempt. +// Returns a channel that is closed if our lock is lost or an error. +// This channel could be closed at any time due to session invalidation, +// communication errors, operator intervention, etc. It is NOT safe to +// assume that the lock is held until Unlock() unless the Session is specifically +// created without any associated health checks. By default Consul sessions +// prefer liveness over safety and an application must be able to handle +// the lock being lost. +func (l *Lock) Lock(stopCh <-chan struct{}) (<-chan struct{}, error) { + // Hold the lock as we try to acquire + l.l.Lock() + defer l.l.Unlock() + + // Check if we already hold the lock + if l.isHeld { + return nil, ErrLockHeld + } + + // Check if we need to create a session first + l.lockSession = l.opts.Session + if l.lockSession == "" { + if s, err := l.createSession(); err != nil { + return nil, fmt.Errorf("failed to create session: %v", err) + } else { + l.sessionRenew = make(chan struct{}) + l.lockSession = s + session := l.c.Session() + go session.RenewPeriodic(l.opts.SessionTTL, s, nil, l.sessionRenew) + + // If we fail to acquire the lock, cleanup the session + defer func() { + if !l.isHeld { + close(l.sessionRenew) + l.sessionRenew = nil + } + }() + } + } + + // Setup the query options + kv := l.c.KV() + qOpts := &QueryOptions{ + WaitTime: DefaultLockWaitTime, + } + +WAIT: + // Check if we should quit + select { + case <-stopCh: + return nil, nil + default: + } + + // Look for an existing lock, blocking until not taken + pair, meta, err := kv.Get(l.opts.Key, qOpts) + if err != nil { + return nil, fmt.Errorf("failed to read lock: %v", err) + } + if pair != nil && pair.Flags != LockFlagValue { + return nil, ErrLockConflict + } + if pair != nil && pair.Session != "" { + qOpts.WaitIndex = meta.LastIndex + goto WAIT + } + + // Try to acquire the lock + lockEnt := l.lockEntry(l.lockSession) + locked, _, err := kv.Acquire(lockEnt, nil) + if err != nil { + return nil, fmt.Errorf("failed to acquire lock: %v", err) + } + + // Handle the case of not getting the lock + if !locked { + select { + case <-time.After(DefaultLockRetryTime): + goto WAIT + case <-stopCh: + return nil, nil + } + } + + // Watch to ensure we maintain leadership + leaderCh := make(chan struct{}) + go l.monitorLock(l.lockSession, leaderCh) + + // Set that we own the lock + l.isHeld = true + + // Locked! All done + return leaderCh, nil +} + +// Unlock released the lock. It is an error to call this +// if the lock is not currently held. +func (l *Lock) Unlock() error { + // Hold the lock as we try to release + l.l.Lock() + defer l.l.Unlock() + + // Ensure the lock is actually held + if !l.isHeld { + return ErrLockNotHeld + } + + // Set that we no longer own the lock + l.isHeld = false + + // Stop the session renew + if l.sessionRenew != nil { + defer func() { + close(l.sessionRenew) + l.sessionRenew = nil + }() + } + + // Get the lock entry, and clear the lock session + lockEnt := l.lockEntry(l.lockSession) + l.lockSession = "" + + // Release the lock explicitly + kv := l.c.KV() + _, _, err := kv.Release(lockEnt, nil) + if err != nil { + return fmt.Errorf("failed to release lock: %v", err) + } + return nil +} + +// Destroy is used to cleanup the lock entry. It is not necessary +// to invoke. It will fail if the lock is in use. +func (l *Lock) Destroy() error { + // Hold the lock as we try to release + l.l.Lock() + defer l.l.Unlock() + + // Check if we already hold the lock + if l.isHeld { + return ErrLockHeld + } + + // Look for an existing lock + kv := l.c.KV() + pair, _, err := kv.Get(l.opts.Key, nil) + if err != nil { + return fmt.Errorf("failed to read lock: %v", err) + } + + // Nothing to do if the lock does not exist + if pair == nil { + return nil + } + + // Check for possible flag conflict + if pair.Flags != LockFlagValue { + return ErrLockConflict + } + + // Check if it is in use + if pair.Session != "" { + return ErrLockInUse + } + + // Attempt the delete + didRemove, _, err := kv.DeleteCAS(pair, nil) + if err != nil { + return fmt.Errorf("failed to remove lock: %v", err) + } + if !didRemove { + return ErrLockInUse + } + return nil +} + +// createSession is used to create a new managed session +func (l *Lock) createSession() (string, error) { + session := l.c.Session() + se := &SessionEntry{ + Name: l.opts.SessionName, + TTL: l.opts.SessionTTL, + } + id, _, err := session.Create(se, nil) + if err != nil { + return "", err + } + return id, nil +} + +// lockEntry returns a formatted KVPair for the lock +func (l *Lock) lockEntry(session string) *KVPair { + return &KVPair{ + Key: l.opts.Key, + Value: l.opts.Value, + Session: session, + Flags: LockFlagValue, + } +} + +// monitorLock is a long running routine to monitor a lock ownership +// It closes the stopCh if we lose our leadership. +func (l *Lock) monitorLock(session string, stopCh chan struct{}) { + defer close(stopCh) + kv := l.c.KV() + opts := &QueryOptions{RequireConsistent: true} +WAIT: + pair, meta, err := kv.Get(l.opts.Key, opts) + if err != nil { + return + } + if pair != nil && pair.Session == session { + opts.WaitIndex = meta.LastIndex + goto WAIT + } +} diff --git a/Godeps/_workspace/src/github.com/hashicorp/consul/api/lock_test.go b/Godeps/_workspace/src/github.com/hashicorp/consul/api/lock_test.go new file mode 100644 index 0000000000..a4aea7349c --- /dev/null +++ b/Godeps/_workspace/src/github.com/hashicorp/consul/api/lock_test.go @@ -0,0 +1,289 @@ +package api + +import ( + "log" + "sync" + "testing" + "time" +) + +func TestLock_LockUnlock(t *testing.T) { + c, s := makeClient(t) + defer s.stop() + + lock, err := c.LockKey("test/lock") + if err != nil { + t.Fatalf("err: %v", err) + } + + // Initial unlock should fail + err = lock.Unlock() + if err != ErrLockNotHeld { + t.Fatalf("err: %v", err) + } + + // Should work + leaderCh, err := lock.Lock(nil) + if err != nil { + t.Fatalf("err: %v", err) + } + if leaderCh == nil { + t.Fatalf("not leader") + } + + // Double lock should fail + _, err = lock.Lock(nil) + if err != ErrLockHeld { + t.Fatalf("err: %v", err) + } + + // Should be leader + select { + case <-leaderCh: + t.Fatalf("should be leader") + default: + } + + // Initial unlock should work + err = lock.Unlock() + if err != nil { + t.Fatalf("err: %v", err) + } + + // Double unlock should fail + err = lock.Unlock() + if err != ErrLockNotHeld { + t.Fatalf("err: %v", err) + } + + // Should loose leadership + select { + case <-leaderCh: + case <-time.After(time.Second): + t.Fatalf("should not be leader") + } +} + +func TestLock_ForceInvalidate(t *testing.T) { + c, s := makeClient(t) + defer s.stop() + + lock, err := c.LockKey("test/lock") + if err != nil { + t.Fatalf("err: %v", err) + } + + // Should work + leaderCh, err := lock.Lock(nil) + if err != nil { + t.Fatalf("err: %v", err) + } + if leaderCh == nil { + t.Fatalf("not leader") + } + defer lock.Unlock() + + go func() { + // Nuke the session, simulator an operator invalidation + // or a health check failure + session := c.Session() + session.Destroy(lock.lockSession, nil) + }() + + // Should loose leadership + select { + case <-leaderCh: + case <-time.After(time.Second): + t.Fatalf("should not be leader") + } +} + +func TestLock_DeleteKey(t *testing.T) { + c, s := makeClient(t) + defer s.stop() + + lock, err := c.LockKey("test/lock") + if err != nil { + t.Fatalf("err: %v", err) + } + + // Should work + leaderCh, err := lock.Lock(nil) + if err != nil { + t.Fatalf("err: %v", err) + } + if leaderCh == nil { + t.Fatalf("not leader") + } + defer lock.Unlock() + + go func() { + // Nuke the key, simulate an operator intervention + kv := c.KV() + kv.Delete("test/lock", nil) + }() + + // Should loose leadership + select { + case <-leaderCh: + case <-time.After(time.Second): + t.Fatalf("should not be leader") + } +} + +func TestLock_Contend(t *testing.T) { + c, s := makeClient(t) + defer s.stop() + + wg := &sync.WaitGroup{} + acquired := make([]bool, 3) + for idx := range acquired { + wg.Add(1) + go func(idx int) { + defer wg.Done() + lock, err := c.LockKey("test/lock") + if err != nil { + t.Fatalf("err: %v", err) + } + + // Should work eventually, will contend + leaderCh, err := lock.Lock(nil) + if err != nil { + t.Fatalf("err: %v", err) + } + if leaderCh == nil { + t.Fatalf("not leader") + } + defer lock.Unlock() + log.Printf("Contender %d acquired", idx) + + // Set acquired and then leave + acquired[idx] = true + }(idx) + } + + // Wait for termination + doneCh := make(chan struct{}) + go func() { + wg.Wait() + close(doneCh) + }() + + // Wait for everybody to get a turn + select { + case <-doneCh: + case <-time.After(3 * DefaultLockRetryTime): + t.Fatalf("timeout") + } + + for idx, did := range acquired { + if !did { + t.Fatalf("contender %d never acquired", idx) + } + } +} + +func TestLock_Destroy(t *testing.T) { + c, s := makeClient(t) + defer s.stop() + + lock, err := c.LockKey("test/lock") + if err != nil { + t.Fatalf("err: %v", err) + } + + // Should work + leaderCh, err := lock.Lock(nil) + if err != nil { + t.Fatalf("err: %v", err) + } + if leaderCh == nil { + t.Fatalf("not leader") + } + + // Destroy should fail + if err := lock.Destroy(); err != ErrLockHeld { + t.Fatalf("err: %v", err) + } + + // Should be able to release + err = lock.Unlock() + if err != nil { + t.Fatalf("err: %v", err) + } + + // Acquire with a different lock + l2, err := c.LockKey("test/lock") + if err != nil { + t.Fatalf("err: %v", err) + } + + // Should work + leaderCh, err = l2.Lock(nil) + if err != nil { + t.Fatalf("err: %v", err) + } + if leaderCh == nil { + t.Fatalf("not leader") + } + + // Destroy should still fail + if err := lock.Destroy(); err != ErrLockInUse { + t.Fatalf("err: %v", err) + } + + // Should relese + err = l2.Unlock() + if err != nil { + t.Fatalf("err: %v", err) + } + + // Destroy should work + err = lock.Destroy() + if err != nil { + t.Fatalf("err: %v", err) + } + + // Double destroy should work + err = l2.Destroy() + if err != nil { + t.Fatalf("err: %v", err) + } +} + +func TestLock_Conflict(t *testing.T) { + c, s := makeClient(t) + defer s.stop() + + sema, err := c.SemaphorePrefix("test/lock/", 2) + if err != nil { + t.Fatalf("err: %v", err) + } + + // Should work + lockCh, err := sema.Acquire(nil) + if err != nil { + t.Fatalf("err: %v", err) + } + if lockCh == nil { + t.Fatalf("not hold") + } + defer sema.Release() + + lock, err := c.LockKey("test/lock/.lock") + if err != nil { + t.Fatalf("err: %v", err) + } + + // Should conflict with semaphore + _, err = lock.Lock(nil) + if err != ErrLockConflict { + t.Fatalf("err: %v", err) + } + + // Should conflict with semaphore + err = lock.Destroy() + if err != ErrLockConflict { + t.Fatalf("err: %v", err) + } +} diff --git a/Godeps/_workspace/src/github.com/hashicorp/consul/api/semaphore.go b/Godeps/_workspace/src/github.com/hashicorp/consul/api/semaphore.go new file mode 100644 index 0000000000..957f884a4d --- /dev/null +++ b/Godeps/_workspace/src/github.com/hashicorp/consul/api/semaphore.go @@ -0,0 +1,482 @@ +package api + +import ( + "encoding/json" + "fmt" + "path" + "sync" + "time" +) + +const ( + // DefaultSemaphoreSessionName is the Session Name we assign if none is provided + DefaultSemaphoreSessionName = "Consul API Semaphore" + + // DefaultSemaphoreSessionTTL is the default session TTL if no Session is provided + // when creating a new Semaphore. This is used because we do not have another + // other check to depend upon. + DefaultSemaphoreSessionTTL = "15s" + + // DefaultSemaphoreWaitTime is how long we block for at a time to check if semaphore + // acquisition is possible. This affects the minimum time it takes to cancel + // a Semaphore acquisition. + DefaultSemaphoreWaitTime = 15 * time.Second + + // DefaultSemaphoreRetryTime is how long we wait after a failed lock acquisition + // before attempting to do the lock again. This is so that once a lock-delay + // is in affect, we do not hot loop retrying the acquisition. + DefaultSemaphoreRetryTime = 5 * time.Second + + // DefaultSemaphoreKey is the key used within the prefix to + // use for coordination between all the contenders. + DefaultSemaphoreKey = ".lock" + + // SemaphoreFlagValue is a magic flag we set to indicate a key + // is being used for a semaphore. It is used to detect a potential + // conflict with a lock. + SemaphoreFlagValue = 0xe0f69a2baa414de0 +) + +var ( + // ErrSemaphoreHeld is returned if we attempt to double lock + ErrSemaphoreHeld = fmt.Errorf("Semaphore already held") + + // ErrSemaphoreNotHeld is returned if we attempt to unlock a semaphore + // that we do not hold. + ErrSemaphoreNotHeld = fmt.Errorf("Semaphore not held") + + // ErrSemaphoreInUse is returned if we attempt to destroy a semaphore + // that is in use. + ErrSemaphoreInUse = fmt.Errorf("Semaphore in use") + + // ErrSemaphoreConflict is returned if the flags on a key + // used for a semaphore do not match expectation + ErrSemaphoreConflict = fmt.Errorf("Existing key does not match semaphore use") +) + +// Semaphore is used to implement a distributed semaphore +// using the Consul KV primitives. +type Semaphore struct { + c *Client + opts *SemaphoreOptions + + isHeld bool + sessionRenew chan struct{} + lockSession string + l sync.Mutex +} + +// SemaphoreOptions is used to parameterize the Semaphore +type SemaphoreOptions struct { + Prefix string // Must be set and have write permissions + Limit int // Must be set, and be positive + Value []byte // Optional, value to associate with the contender entry + Session string // OPtional, created if not specified + SessionName string // Optional, defaults to DefaultLockSessionName + SessionTTL string // Optional, defaults to DefaultLockSessionTTL +} + +// semaphoreLock is written under the DefaultSemaphoreKey and +// is used to coordinate between all the contenders. +type semaphoreLock struct { + // Limit is the integer limit of holders. This is used to + // verify that all the holders agree on the value. + Limit int + + // Holders is a list of all the semaphore holders. + // It maps the session ID to true. It is used as a set effectively. + Holders map[string]bool +} + +// SemaphorePrefix is used to created a Semaphore which will operate +// at the given KV prefix and uses the given limit for the semaphore. +// The prefix must have write privileges, and the limit must be agreed +// upon by all contenders. +func (c *Client) SemaphorePrefix(prefix string, limit int) (*Semaphore, error) { + opts := &SemaphoreOptions{ + Prefix: prefix, + Limit: limit, + } + return c.SemaphoreOpts(opts) +} + +// SemaphoreOpts is used to create a Semaphore with the given options. +// The prefix must have write privileges, and the limit must be agreed +// upon by all contenders. If a Session is not provided, one will be created. +func (c *Client) SemaphoreOpts(opts *SemaphoreOptions) (*Semaphore, error) { + if opts.Prefix == "" { + return nil, fmt.Errorf("missing prefix") + } + if opts.Limit <= 0 { + return nil, fmt.Errorf("semaphore limit must be positive") + } + if opts.SessionName == "" { + opts.SessionName = DefaultSemaphoreSessionName + } + if opts.SessionTTL == "" { + opts.SessionTTL = DefaultSemaphoreSessionTTL + } else { + if _, err := time.ParseDuration(opts.SessionTTL); err != nil { + return nil, fmt.Errorf("invalid SessionTTL: %v", err) + } + } + s := &Semaphore{ + c: c, + opts: opts, + } + return s, nil +} + +// Acquire attempts to reserve a slot in the semaphore, blocking until +// success, interrupted via the stopCh or an error is encounted. +// Providing a non-nil stopCh can be used to abort the attempt. +// On success, a channel is returned that represents our slot. +// This channel could be closed at any time due to session invalidation, +// communication errors, operator intervention, etc. It is NOT safe to +// assume that the slot is held until Release() unless the Session is specifically +// created without any associated health checks. By default Consul sessions +// prefer liveness over safety and an application must be able to handle +// the session being lost. +func (s *Semaphore) Acquire(stopCh <-chan struct{}) (<-chan struct{}, error) { + // Hold the lock as we try to acquire + s.l.Lock() + defer s.l.Unlock() + + // Check if we already hold the semaphore + if s.isHeld { + return nil, ErrSemaphoreHeld + } + + // Check if we need to create a session first + s.lockSession = s.opts.Session + if s.lockSession == "" { + if sess, err := s.createSession(); err != nil { + return nil, fmt.Errorf("failed to create session: %v", err) + } else { + s.sessionRenew = make(chan struct{}) + s.lockSession = sess + session := s.c.Session() + go session.RenewPeriodic(s.opts.SessionTTL, sess, nil, s.sessionRenew) + + // If we fail to acquire the lock, cleanup the session + defer func() { + if !s.isHeld { + close(s.sessionRenew) + s.sessionRenew = nil + } + }() + } + } + + // Create the contender entry + kv := s.c.KV() + made, _, err := kv.Acquire(s.contenderEntry(s.lockSession), nil) + if err != nil || !made { + return nil, fmt.Errorf("failed to make contender entry: %v", err) + } + + // Setup the query options + qOpts := &QueryOptions{ + WaitTime: DefaultSemaphoreWaitTime, + } + +WAIT: + // Check if we should quit + select { + case <-stopCh: + return nil, nil + default: + } + + // Read the prefix + pairs, meta, err := kv.List(s.opts.Prefix, qOpts) + if err != nil { + return nil, fmt.Errorf("failed to read prefix: %v", err) + } + + // Decode the lock + lockPair := s.findLock(pairs) + if lockPair.Flags != SemaphoreFlagValue { + return nil, ErrSemaphoreConflict + } + lock, err := s.decodeLock(lockPair) + if err != nil { + return nil, err + } + + // Verify we agree with the limit + if lock.Limit != s.opts.Limit { + return nil, fmt.Errorf("semaphore limit conflict (lock: %d, local: %d)", + lock.Limit, s.opts.Limit) + } + + // Prune the dead holders + s.pruneDeadHolders(lock, pairs) + + // Check if the lock is held + if len(lock.Holders) >= lock.Limit { + qOpts.WaitIndex = meta.LastIndex + goto WAIT + } + + // Create a new lock with us as a holder + lock.Holders[s.lockSession] = true + newLock, err := s.encodeLock(lock, lockPair.ModifyIndex) + if err != nil { + return nil, err + } + + // Attempt the acquisition + didSet, _, err := kv.CAS(newLock, nil) + if err != nil { + return nil, fmt.Errorf("failed to update lock: %v", err) + } + if !didSet { + // Update failed, could have been a race with another contender, + // retry the operation + goto WAIT + } + + // Watch to ensure we maintain ownership of the slot + lockCh := make(chan struct{}) + go s.monitorLock(s.lockSession, lockCh) + + // Set that we own the lock + s.isHeld = true + + // Acquired! All done + return lockCh, nil +} + +// Release is used to voluntarily give up our semaphore slot. It is +// an error to call this if the semaphore has not been acquired. +func (s *Semaphore) Release() error { + // Hold the lock as we try to release + s.l.Lock() + defer s.l.Unlock() + + // Ensure the lock is actually held + if !s.isHeld { + return ErrSemaphoreNotHeld + } + + // Set that we no longer own the lock + s.isHeld = false + + // Stop the session renew + if s.sessionRenew != nil { + defer func() { + close(s.sessionRenew) + s.sessionRenew = nil + }() + } + + // Get and clear the lock session + lockSession := s.lockSession + s.lockSession = "" + + // Remove ourselves as a lock holder + kv := s.c.KV() + key := path.Join(s.opts.Prefix, DefaultSemaphoreKey) +READ: + pair, _, err := kv.Get(key, nil) + if err != nil { + return err + } + if pair == nil { + pair = &KVPair{} + } + lock, err := s.decodeLock(pair) + if err != nil { + return err + } + + // Create a new lock without us as a holder + if _, ok := lock.Holders[lockSession]; ok { + delete(lock.Holders, lockSession) + newLock, err := s.encodeLock(lock, pair.ModifyIndex) + if err != nil { + return err + } + + // Swap the locks + didSet, _, err := kv.CAS(newLock, nil) + if err != nil { + return fmt.Errorf("failed to update lock: %v", err) + } + if !didSet { + goto READ + } + } + + // Destroy the contender entry + contenderKey := path.Join(s.opts.Prefix, lockSession) + if _, err := kv.Delete(contenderKey, nil); err != nil { + return err + } + return nil +} + +// Destroy is used to cleanup the semaphore entry. It is not necessary +// to invoke. It will fail if the semaphore is in use. +func (s *Semaphore) Destroy() error { + // Hold the lock as we try to acquire + s.l.Lock() + defer s.l.Unlock() + + // Check if we already hold the semaphore + if s.isHeld { + return ErrSemaphoreHeld + } + + // List for the semaphore + kv := s.c.KV() + pairs, _, err := kv.List(s.opts.Prefix, nil) + if err != nil { + return fmt.Errorf("failed to read prefix: %v", err) + } + + // Find the lock pair, bail if it doesn't exist + lockPair := s.findLock(pairs) + if lockPair.ModifyIndex == 0 { + return nil + } + if lockPair.Flags != SemaphoreFlagValue { + return ErrSemaphoreConflict + } + + // Decode the lock + lock, err := s.decodeLock(lockPair) + if err != nil { + return err + } + + // Prune the dead holders + s.pruneDeadHolders(lock, pairs) + + // Check if there are any holders + if len(lock.Holders) > 0 { + return ErrSemaphoreInUse + } + + // Attempt the delete + didRemove, _, err := kv.DeleteCAS(lockPair, nil) + if err != nil { + return fmt.Errorf("failed to remove semaphore: %v", err) + } + if !didRemove { + return ErrSemaphoreInUse + } + return nil +} + +// createSession is used to create a new managed session +func (s *Semaphore) createSession() (string, error) { + session := s.c.Session() + se := &SessionEntry{ + Name: s.opts.SessionName, + TTL: s.opts.SessionTTL, + Behavior: SessionBehaviorDelete, + } + id, _, err := session.Create(se, nil) + if err != nil { + return "", err + } + return id, nil +} + +// contenderEntry returns a formatted KVPair for the contender +func (s *Semaphore) contenderEntry(session string) *KVPair { + return &KVPair{ + Key: path.Join(s.opts.Prefix, session), + Value: s.opts.Value, + Session: session, + Flags: SemaphoreFlagValue, + } +} + +// findLock is used to find the KV Pair which is used for coordination +func (s *Semaphore) findLock(pairs KVPairs) *KVPair { + key := path.Join(s.opts.Prefix, DefaultSemaphoreKey) + for _, pair := range pairs { + if pair.Key == key { + return pair + } + } + return &KVPair{Flags: SemaphoreFlagValue} +} + +// decodeLock is used to decode a semaphoreLock from an +// entry in Consul +func (s *Semaphore) decodeLock(pair *KVPair) (*semaphoreLock, error) { + // Handle if there is no lock + if pair == nil || pair.Value == nil { + return &semaphoreLock{ + Limit: s.opts.Limit, + Holders: make(map[string]bool), + }, nil + } + + l := &semaphoreLock{} + if err := json.Unmarshal(pair.Value, l); err != nil { + return nil, fmt.Errorf("lock decoding failed: %v", err) + } + return l, nil +} + +// encodeLock is used to encode a semaphoreLock into a KVPair +// that can be PUT +func (s *Semaphore) encodeLock(l *semaphoreLock, oldIndex uint64) (*KVPair, error) { + enc, err := json.Marshal(l) + if err != nil { + return nil, fmt.Errorf("lock encoding failed: %v", err) + } + pair := &KVPair{ + Key: path.Join(s.opts.Prefix, DefaultSemaphoreKey), + Value: enc, + Flags: SemaphoreFlagValue, + ModifyIndex: oldIndex, + } + return pair, nil +} + +// pruneDeadHolders is used to remove all the dead lock holders +func (s *Semaphore) pruneDeadHolders(lock *semaphoreLock, pairs KVPairs) { + // Gather all the live holders + alive := make(map[string]struct{}, len(pairs)) + for _, pair := range pairs { + if pair.Session != "" { + alive[pair.Session] = struct{}{} + } + } + + // Remove any holders that are dead + for holder := range lock.Holders { + if _, ok := alive[holder]; !ok { + delete(lock.Holders, holder) + } + } +} + +// monitorLock is a long running routine to monitor a semaphore ownership +// It closes the stopCh if we lose our slot. +func (s *Semaphore) monitorLock(session string, stopCh chan struct{}) { + defer close(stopCh) + kv := s.c.KV() + opts := &QueryOptions{RequireConsistent: true} +WAIT: + pairs, meta, err := kv.List(s.opts.Prefix, opts) + if err != nil { + return + } + lockPair := s.findLock(pairs) + lock, err := s.decodeLock(lockPair) + if err != nil { + return + } + s.pruneDeadHolders(lock, pairs) + if _, ok := lock.Holders[session]; ok { + opts.WaitIndex = meta.LastIndex + goto WAIT + } +} diff --git a/Godeps/_workspace/src/github.com/hashicorp/consul/api/semaphore_test.go b/Godeps/_workspace/src/github.com/hashicorp/consul/api/semaphore_test.go new file mode 100644 index 0000000000..b931d25938 --- /dev/null +++ b/Godeps/_workspace/src/github.com/hashicorp/consul/api/semaphore_test.go @@ -0,0 +1,306 @@ +package api + +import ( + "log" + "sync" + "testing" + "time" +) + +func TestSemaphore_AcquireRelease(t *testing.T) { + c, s := makeClient(t) + defer s.stop() + + sema, err := c.SemaphorePrefix("test/semaphore", 2) + if err != nil { + t.Fatalf("err: %v", err) + } + + // Initial release should fail + err = sema.Release() + if err != ErrSemaphoreNotHeld { + t.Fatalf("err: %v", err) + } + + // Should work + lockCh, err := sema.Acquire(nil) + if err != nil { + t.Fatalf("err: %v", err) + } + if lockCh == nil { + t.Fatalf("not hold") + } + + // Double lock should fail + _, err = sema.Acquire(nil) + if err != ErrSemaphoreHeld { + t.Fatalf("err: %v", err) + } + + // Should be held + select { + case <-lockCh: + t.Fatalf("should be held") + default: + } + + // Initial release should work + err = sema.Release() + if err != nil { + t.Fatalf("err: %v", err) + } + + // Double unlock should fail + err = sema.Release() + if err != ErrSemaphoreNotHeld { + t.Fatalf("err: %v", err) + } + + // Should lose resource + select { + case <-lockCh: + case <-time.After(time.Second): + t.Fatalf("should not be held") + } +} + +func TestSemaphore_ForceInvalidate(t *testing.T) { + c, s := makeClient(t) + defer s.stop() + + sema, err := c.SemaphorePrefix("test/semaphore", 2) + if err != nil { + t.Fatalf("err: %v", err) + } + + // Should work + lockCh, err := sema.Acquire(nil) + if err != nil { + t.Fatalf("err: %v", err) + } + if lockCh == nil { + t.Fatalf("not acquired") + } + defer sema.Release() + + go func() { + // Nuke the session, simulator an operator invalidation + // or a health check failure + session := c.Session() + session.Destroy(sema.lockSession, nil) + }() + + // Should loose slot + select { + case <-lockCh: + case <-time.After(time.Second): + t.Fatalf("should not be locked") + } +} + +func TestSemaphore_DeleteKey(t *testing.T) { + c, s := makeClient(t) + defer s.stop() + + sema, err := c.SemaphorePrefix("test/semaphore", 2) + if err != nil { + t.Fatalf("err: %v", err) + } + + // Should work + lockCh, err := sema.Acquire(nil) + if err != nil { + t.Fatalf("err: %v", err) + } + if lockCh == nil { + t.Fatalf("not locked") + } + defer sema.Release() + + go func() { + // Nuke the key, simulate an operator intervention + kv := c.KV() + kv.DeleteTree("test/semaphore", nil) + }() + + // Should loose leadership + select { + case <-lockCh: + case <-time.After(time.Second): + t.Fatalf("should not be locked") + } +} + +func TestSemaphore_Contend(t *testing.T) { + c, s := makeClient(t) + defer s.stop() + + wg := &sync.WaitGroup{} + acquired := make([]bool, 4) + for idx := range acquired { + wg.Add(1) + go func(idx int) { + defer wg.Done() + sema, err := c.SemaphorePrefix("test/semaphore", 2) + if err != nil { + t.Fatalf("err: %v", err) + } + + // Should work eventually, will contend + lockCh, err := sema.Acquire(nil) + if err != nil { + t.Fatalf("err: %v", err) + } + if lockCh == nil { + t.Fatalf("not locked") + } + defer sema.Release() + log.Printf("Contender %d acquired", idx) + + // Set acquired and then leave + acquired[idx] = true + }(idx) + } + + // Wait for termination + doneCh := make(chan struct{}) + go func() { + wg.Wait() + close(doneCh) + }() + + // Wait for everybody to get a turn + select { + case <-doneCh: + case <-time.After(3 * DefaultLockRetryTime): + t.Fatalf("timeout") + } + + for idx, did := range acquired { + if !did { + t.Fatalf("contender %d never acquired", idx) + } + } +} + +func TestSemaphore_BadLimit(t *testing.T) { + c, s := makeClient(t) + defer s.stop() + + sema, err := c.SemaphorePrefix("test/semaphore", 0) + if err == nil { + t.Fatalf("should error") + } + + sema, err = c.SemaphorePrefix("test/semaphore", 1) + if err != nil { + t.Fatalf("err: %v", err) + } + + _, err = sema.Acquire(nil) + if err != nil { + t.Fatalf("err: %v", err) + } + + sema2, err := c.SemaphorePrefix("test/semaphore", 2) + if err != nil { + t.Fatalf("err: %v", err) + } + + _, err = sema2.Acquire(nil) + if err.Error() != "semaphore limit conflict (lock: 1, local: 2)" { + t.Fatalf("err: %v", err) + } +} + +func TestSemaphore_Destroy(t *testing.T) { + c, s := makeClient(t) + defer s.stop() + + sema, err := c.SemaphorePrefix("test/semaphore", 2) + if err != nil { + t.Fatalf("err: %v", err) + } + + sema2, err := c.SemaphorePrefix("test/semaphore", 2) + if err != nil { + t.Fatalf("err: %v", err) + } + + _, err = sema.Acquire(nil) + if err != nil { + t.Fatalf("err: %v", err) + } + + _, err = sema2.Acquire(nil) + if err != nil { + t.Fatalf("err: %v", err) + } + + // Destroy should fail, still held + if err := sema.Destroy(); err != ErrSemaphoreHeld { + t.Fatalf("err: %v", err) + } + + err = sema.Release() + if err != nil { + t.Fatalf("err: %v", err) + } + + // Destroy should fail, still in use + if err := sema.Destroy(); err != ErrSemaphoreInUse { + t.Fatalf("err: %v", err) + } + + err = sema2.Release() + if err != nil { + t.Fatalf("err: %v", err) + } + + // Destroy should work + if err := sema.Destroy(); err != nil { + t.Fatalf("err: %v", err) + } + + // Destroy should work + if err := sema2.Destroy(); err != nil { + t.Fatalf("err: %v", err) + } +} + +func TestSemaphore_Conflict(t *testing.T) { + c, s := makeClient(t) + defer s.stop() + + lock, err := c.LockKey("test/sema/.lock") + if err != nil { + t.Fatalf("err: %v", err) + } + + // Should work + leaderCh, err := lock.Lock(nil) + if err != nil { + t.Fatalf("err: %v", err) + } + if leaderCh == nil { + t.Fatalf("not leader") + } + defer lock.Unlock() + + sema, err := c.SemaphorePrefix("test/sema/", 2) + if err != nil { + t.Fatalf("err: %v", err) + } + + // Should conflict with lock + _, err = sema.Acquire(nil) + if err != ErrSemaphoreConflict { + t.Fatalf("err: %v", err) + } + + // Should conflict with lock + err = sema.Destroy() + if err != ErrSemaphoreConflict { + t.Fatalf("err: %v", err) + } +} diff --git a/Godeps/_workspace/src/github.com/armon/consul-api/session.go b/Godeps/_workspace/src/github.com/hashicorp/consul/api/session.go similarity index 80% rename from Godeps/_workspace/src/github.com/armon/consul-api/session.go rename to Godeps/_workspace/src/github.com/hashicorp/consul/api/session.go index 4fbfc5ee9a..bb84644fd9 100644 --- a/Godeps/_workspace/src/github.com/armon/consul-api/session.go +++ b/Godeps/_workspace/src/github.com/hashicorp/consul/api/session.go @@ -1,9 +1,20 @@ -package consulapi +package api import ( "time" ) +const ( + // SessionBehaviorRelease is the default behavior and causes + // all associated locks to be released on session invalidation. + SessionBehaviorRelease = "release" + + // SessionBehaviorDelete is new in Consul 0.5 and changes the + // behavior to delete all associated locks on session invalidation. + // It can be used in a way similar to Ephemeral Nodes in ZooKeeper. + SessionBehaviorDelete = "delete" +) + // SessionEntry represents a session in consul type SessionEntry struct { CreateIndex uint64 @@ -136,6 +147,36 @@ func (s *Session) Renew(id string, q *WriteOptions) (*SessionEntry, *WriteMeta, return nil, wm, nil } +// RenewPeriodic is used to periodically invoke Session.Renew on a +// session until a doneCh is closed. This is meant to be used in a long running +// goroutine to ensure a session stays valid. +func (s *Session) RenewPeriodic(initialTTL string, id string, q *WriteOptions, doneCh chan struct{}) error { + ttl, err := time.ParseDuration(initialTTL) + if err != nil { + return err + } + for { + select { + case <-time.After(ttl / 2): + entry, _, err := s.Renew(id, q) + if err != nil { + return err + } + if entry == nil { + return nil + } + + // Handle the server updating the TTL + ttl, _ = time.ParseDuration(entry.TTL) + + case <-doneCh: + // Attempt a session destroy + s.Destroy(id, q) + return nil + } + } +} + // Info looks up a single session func (s *Session) Info(id string, q *QueryOptions) (*SessionEntry, *QueryMeta, error) { r := s.c.newRequest("GET", "/v1/session/info/"+id) diff --git a/Godeps/_workspace/src/github.com/armon/consul-api/session_test.go b/Godeps/_workspace/src/github.com/hashicorp/consul/api/session_test.go similarity index 93% rename from Godeps/_workspace/src/github.com/armon/consul-api/session_test.go rename to Godeps/_workspace/src/github.com/hashicorp/consul/api/session_test.go index 9351c999ef..3579e48b1b 100644 --- a/Godeps/_workspace/src/github.com/armon/consul-api/session_test.go +++ b/Godeps/_workspace/src/github.com/hashicorp/consul/api/session_test.go @@ -1,11 +1,13 @@ -package consulapi +package api import ( "testing" ) func TestSession_CreateDestroy(t *testing.T) { - c := makeClient(t) + c, s := makeClient(t) + defer s.stop() + session := c.Session() id, meta, err := session.Create(nil, nil) @@ -32,7 +34,9 @@ func TestSession_CreateDestroy(t *testing.T) { } func TestSession_CreateRenewDestroy(t *testing.T) { - c := makeClient(t) + c, s := makeClient(t) + defer s.stop() + session := c.Session() se := &SessionEntry{ @@ -80,7 +84,9 @@ func TestSession_CreateRenewDestroy(t *testing.T) { } func TestSession_Info(t *testing.T) { - c := makeClient(t) + c, s := makeClient(t) + defer s.stop() + session := c.Session() id, _, err := session.Create(nil, nil) @@ -131,7 +137,9 @@ func TestSession_Info(t *testing.T) { } func TestSession_Node(t *testing.T) { - c := makeClient(t) + c, s := makeClient(t) + defer s.stop() + session := c.Session() id, _, err := session.Create(nil, nil) @@ -163,7 +171,9 @@ func TestSession_Node(t *testing.T) { } func TestSession_List(t *testing.T) { - c := makeClient(t) + c, s := makeClient(t) + defer s.stop() + session := c.Session() id, _, err := session.Create(nil, nil) diff --git a/Godeps/_workspace/src/github.com/armon/consul-api/status.go b/Godeps/_workspace/src/github.com/hashicorp/consul/api/status.go similarity index 98% rename from Godeps/_workspace/src/github.com/armon/consul-api/status.go rename to Godeps/_workspace/src/github.com/hashicorp/consul/api/status.go index 21c31982f4..74ef61a678 100644 --- a/Godeps/_workspace/src/github.com/armon/consul-api/status.go +++ b/Godeps/_workspace/src/github.com/hashicorp/consul/api/status.go @@ -1,4 +1,4 @@ -package consulapi +package api // Status can be used to query the Status endpoints type Status struct { diff --git a/Godeps/_workspace/src/github.com/armon/consul-api/status_test.go b/Godeps/_workspace/src/github.com/hashicorp/consul/api/status_test.go similarity index 81% rename from Godeps/_workspace/src/github.com/armon/consul-api/status_test.go rename to Godeps/_workspace/src/github.com/hashicorp/consul/api/status_test.go index ab9b42f503..096b13da09 100644 --- a/Godeps/_workspace/src/github.com/armon/consul-api/status_test.go +++ b/Godeps/_workspace/src/github.com/hashicorp/consul/api/status_test.go @@ -1,11 +1,13 @@ -package consulapi +package api import ( "testing" ) func TestStatusLeader(t *testing.T) { - c := makeClient(t) + c, s := makeClient(t) + defer s.stop() + status := c.Status() leader, err := status.Leader() @@ -18,7 +20,9 @@ func TestStatusLeader(t *testing.T) { } func TestStatusPeers(t *testing.T) { - c := makeClient(t) + c, s := makeClient(t) + defer s.stop() + status := c.Status() peers, err := status.Peers()