mirror of https://github.com/docker/docs.git
Merge pull request #391 from JeffChien/update_consul_api
Update consul api
This commit is contained in:
commit
0bc7230120
|
@ -10,10 +10,6 @@
|
||||||
"Comment": "v0.6.4-6-g539d4dc",
|
"Comment": "v0.6.4-6-g539d4dc",
|
||||||
"Rev": "539d4dc034c079a7188b5d4ca9650632d73c66e8"
|
"Rev": "539d4dc034c079a7188b5d4ca9650632d73c66e8"
|
||||||
},
|
},
|
||||||
{
|
|
||||||
"ImportPath": "github.com/armon/consul-api",
|
|
||||||
"Rev": "dcfedd50ed5334f96adee43fc88518a4f095e15c"
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
"ImportPath": "github.com/codegangsta/cli",
|
"ImportPath": "github.com/codegangsta/cli",
|
||||||
"Comment": "1.2.0-62-gbf4a526",
|
"Comment": "1.2.0-62-gbf4a526",
|
||||||
|
@ -52,6 +48,11 @@
|
||||||
"ImportPath": "github.com/gorilla/mux",
|
"ImportPath": "github.com/gorilla/mux",
|
||||||
"Rev": "e444e69cbd2e2e3e0749a2f3c717cec491552bbf"
|
"Rev": "e444e69cbd2e2e3e0749a2f3c717cec491552bbf"
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"ImportPath": "github.com/hashicorp/consul/api",
|
||||||
|
"Comment": "v0.5.0rc1-108-g79d28b9",
|
||||||
|
"Rev": "79d28b9baf2be44ada3058263572d3a72ba71d4e"
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"ImportPath": "github.com/samalba/dockerclient",
|
"ImportPath": "github.com/samalba/dockerclient",
|
||||||
"Rev": "7e4366cfab2f2b44fcb493bee93a156a763d58b6"
|
"Rev": "7e4366cfab2f2b44fcb493bee93a156a763d58b6"
|
||||||
|
|
|
@ -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
|
|
|
@ -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.
|
|
|
@ -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)
|
|
||||||
|
|
||||||
```
|
|
||||||
|
|
|
@ -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)
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -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)
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -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)
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -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)
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -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)
|
||||||
|
|
||||||
|
```
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
package consulapi
|
package api
|
||||||
|
|
||||||
const (
|
const (
|
||||||
// ACLCLientType is the client type token
|
// ACLCLientType is the client type token
|
|
@ -1,4 +1,4 @@
|
||||||
package consulapi
|
package api
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"os"
|
"os"
|
||||||
|
@ -16,7 +16,9 @@ func TestACL_CreateDestroy(t *testing.T) {
|
||||||
if CONSUL_ROOT == "" {
|
if CONSUL_ROOT == "" {
|
||||||
t.SkipNow()
|
t.SkipNow()
|
||||||
}
|
}
|
||||||
c := makeClient(t)
|
c, s := makeClient(t)
|
||||||
|
defer s.stop()
|
||||||
|
|
||||||
c.config.Token = CONSUL_ROOT
|
c.config.Token = CONSUL_ROOT
|
||||||
acl := c.ACL()
|
acl := c.ACL()
|
||||||
|
|
||||||
|
@ -62,7 +64,9 @@ func TestACL_CloneDestroy(t *testing.T) {
|
||||||
if CONSUL_ROOT == "" {
|
if CONSUL_ROOT == "" {
|
||||||
t.SkipNow()
|
t.SkipNow()
|
||||||
}
|
}
|
||||||
c := makeClient(t)
|
c, s := makeClient(t)
|
||||||
|
defer s.stop()
|
||||||
|
|
||||||
c.config.Token = CONSUL_ROOT
|
c.config.Token = CONSUL_ROOT
|
||||||
acl := c.ACL()
|
acl := c.ACL()
|
||||||
|
|
||||||
|
@ -93,7 +97,9 @@ func TestACL_Info(t *testing.T) {
|
||||||
if CONSUL_ROOT == "" {
|
if CONSUL_ROOT == "" {
|
||||||
t.SkipNow()
|
t.SkipNow()
|
||||||
}
|
}
|
||||||
c := makeClient(t)
|
c, s := makeClient(t)
|
||||||
|
defer s.stop()
|
||||||
|
|
||||||
c.config.Token = CONSUL_ROOT
|
c.config.Token = CONSUL_ROOT
|
||||||
acl := c.ACL()
|
acl := c.ACL()
|
||||||
|
|
||||||
|
@ -118,7 +124,9 @@ func TestACL_List(t *testing.T) {
|
||||||
if CONSUL_ROOT == "" {
|
if CONSUL_ROOT == "" {
|
||||||
t.SkipNow()
|
t.SkipNow()
|
||||||
}
|
}
|
||||||
c := makeClient(t)
|
c, s := makeClient(t)
|
||||||
|
defer s.stop()
|
||||||
|
|
||||||
c.config.Token = CONSUL_ROOT
|
c.config.Token = CONSUL_ROOT
|
||||||
acl := c.ACL()
|
acl := c.ACL()
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
package consulapi
|
package api
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
|
@ -22,6 +22,7 @@ type AgentService struct {
|
||||||
Service string
|
Service string
|
||||||
Tags []string
|
Tags []string
|
||||||
Port int
|
Port int
|
||||||
|
Address string
|
||||||
}
|
}
|
||||||
|
|
||||||
// AgentMember represents a cluster member known to the agent
|
// AgentMember represents a cluster member known to the agent
|
||||||
|
@ -41,18 +42,21 @@ type AgentMember struct {
|
||||||
|
|
||||||
// AgentServiceRegistration is used to register a new service
|
// AgentServiceRegistration is used to register a new service
|
||||||
type AgentServiceRegistration struct {
|
type AgentServiceRegistration struct {
|
||||||
ID string `json:",omitempty"`
|
ID string `json:",omitempty"`
|
||||||
Name string `json:",omitempty"`
|
Name string `json:",omitempty"`
|
||||||
Tags []string `json:",omitempty"`
|
Tags []string `json:",omitempty"`
|
||||||
Port int `json:",omitempty"`
|
Port int `json:",omitempty"`
|
||||||
Check *AgentServiceCheck
|
Address string `json:",omitempty"`
|
||||||
|
Check *AgentServiceCheck
|
||||||
|
Checks AgentServiceChecks
|
||||||
}
|
}
|
||||||
|
|
||||||
// AgentCheckRegistration is used to register a new check
|
// AgentCheckRegistration is used to register a new check
|
||||||
type AgentCheckRegistration struct {
|
type AgentCheckRegistration struct {
|
||||||
ID string `json:",omitempty"`
|
ID string `json:",omitempty"`
|
||||||
Name string `json:",omitempty"`
|
Name string `json:",omitempty"`
|
||||||
Notes string `json:",omitempty"`
|
Notes string `json:",omitempty"`
|
||||||
|
ServiceID string `json:",omitempty"`
|
||||||
AgentServiceCheck
|
AgentServiceCheck
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -61,8 +65,11 @@ type AgentCheckRegistration struct {
|
||||||
type AgentServiceCheck struct {
|
type AgentServiceCheck struct {
|
||||||
Script string `json:",omitempty"`
|
Script string `json:",omitempty"`
|
||||||
Interval string `json:",omitempty"`
|
Interval string `json:",omitempty"`
|
||||||
|
Timeout string `json:",omitempty"`
|
||||||
TTL string `json:",omitempty"`
|
TTL string `json:",omitempty"`
|
||||||
|
HTTP string `json:",omitempty"`
|
||||||
}
|
}
|
||||||
|
type AgentServiceChecks []*AgentServiceCheck
|
||||||
|
|
||||||
// Agent can be used to query the Agent endpoints
|
// Agent can be used to query the Agent endpoints
|
||||||
type Agent struct {
|
type Agent struct {
|
||||||
|
@ -270,3 +277,57 @@ func (a *Agent) ForceLeave(node string) error {
|
||||||
resp.Body.Close()
|
resp.Body.Close()
|
||||||
return nil
|
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
|
||||||
|
}
|
404
Godeps/_workspace/src/github.com/hashicorp/consul/api/agent_test.go
generated
vendored
Normal file
404
Godeps/_workspace/src/github.com/hashicorp/consul/api/agent_test.go
generated
vendored
Normal file
|
@ -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")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,13 +1,16 @@
|
||||||
package consulapi
|
package api
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"bytes"
|
"bytes"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"fmt"
|
"fmt"
|
||||||
"io"
|
"io"
|
||||||
|
"net"
|
||||||
"net/http"
|
"net/http"
|
||||||
"net/url"
|
"net/url"
|
||||||
|
"os"
|
||||||
"strconv"
|
"strconv"
|
||||||
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -111,11 +114,17 @@ type Config struct {
|
||||||
|
|
||||||
// DefaultConfig returns a default configuration for the client
|
// DefaultConfig returns a default configuration for the client
|
||||||
func DefaultConfig() *Config {
|
func DefaultConfig() *Config {
|
||||||
return &Config{
|
config := &Config{
|
||||||
Address: "127.0.0.1:8500",
|
Address: "127.0.0.1:8500",
|
||||||
Scheme: "http",
|
Scheme: "http",
|
||||||
HttpClient: http.DefaultClient,
|
HttpClient: http.DefaultClient,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if addr := os.Getenv("CONSUL_HTTP_ADDR"); addr != "" {
|
||||||
|
config.Address = addr
|
||||||
|
}
|
||||||
|
|
||||||
|
return config
|
||||||
}
|
}
|
||||||
|
|
||||||
// Client provides a client to the Consul API
|
// Client provides a client to the Consul API
|
||||||
|
@ -140,6 +149,17 @@ func NewClient(config *Config) (*Client, error) {
|
||||||
config.HttpClient = defConfig.HttpClient
|
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{
|
client := &Client{
|
||||||
config: *config,
|
config: *config,
|
||||||
}
|
}
|
||||||
|
@ -206,9 +226,6 @@ func (r *request) toHTTP() (*http.Request, error) {
|
||||||
// Encode the query parameters
|
// Encode the query parameters
|
||||||
r.url.RawQuery = r.params.Encode()
|
r.url.RawQuery = r.params.Encode()
|
||||||
|
|
||||||
// Get the url sring
|
|
||||||
urlRaw := r.url.String()
|
|
||||||
|
|
||||||
// Check if we should encode the body
|
// Check if we should encode the body
|
||||||
if r.body == nil && r.obj != nil {
|
if r.body == nil && r.obj != nil {
|
||||||
if b, err := encodeBody(r.obj); err != nil {
|
if b, err := encodeBody(r.obj); err != nil {
|
||||||
|
@ -219,14 +236,21 @@ func (r *request) toHTTP() (*http.Request, error) {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Create the HTTP request
|
// 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
|
// Setup auth
|
||||||
if err == nil && r.config.HttpAuth != nil {
|
if r.config.HttpAuth != nil {
|
||||||
req.SetBasicAuth(r.config.HttpAuth.Username, r.config.HttpAuth.Password)
|
req.SetBasicAuth(r.config.HttpAuth.Username, r.config.HttpAuth.Password)
|
||||||
}
|
}
|
||||||
|
|
||||||
return req, err
|
return req, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// newRequest is used to create a new request
|
// 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
|
// 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) {
|
func requireOK(d time.Duration, resp *http.Response, e error) (time.Duration, *http.Response, error) {
|
||||||
if e != nil {
|
if e != nil {
|
||||||
return d, resp, e
|
if resp != nil {
|
||||||
|
resp.Body.Close()
|
||||||
|
}
|
||||||
|
return d, nil, e
|
||||||
}
|
}
|
||||||
if resp.StatusCode != 200 {
|
if resp.StatusCode != 200 {
|
||||||
var buf bytes.Buffer
|
var buf bytes.Buffer
|
||||||
io.Copy(&buf, resp.Body)
|
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
|
||||||
}
|
}
|
339
Godeps/_workspace/src/github.com/hashicorp/consul/api/api_test.go
generated
vendored
Normal file
339
Godeps/_workspace/src/github.com/hashicorp/consul/api/api_test.go
generated
vendored
Normal file
|
@ -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)
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,4 +1,4 @@
|
||||||
package consulapi
|
package api
|
||||||
|
|
||||||
type Node struct {
|
type Node struct {
|
||||||
Node string
|
Node string
|
273
Godeps/_workspace/src/github.com/hashicorp/consul/api/catalog_test.go
generated
vendored
Normal file
273
Godeps/_workspace/src/github.com/hashicorp/consul/api/catalog_test.go
generated
vendored
Normal file
|
@ -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)
|
||||||
|
})
|
||||||
|
}
|
|
@ -1,4 +1,4 @@
|
||||||
package consulapi
|
package api
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"bytes"
|
"bytes"
|
|
@ -1,11 +1,13 @@
|
||||||
package consulapi
|
package api
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"testing"
|
"testing"
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestEvent_FireList(t *testing.T) {
|
func TestEvent_FireList(t *testing.T) {
|
||||||
c := makeClient(t)
|
c, s := makeClient(t)
|
||||||
|
defer s.stop()
|
||||||
|
|
||||||
event := c.Event()
|
event := c.Event()
|
||||||
|
|
||||||
params := &UserEvent{Name: "foo"}
|
params := &UserEvent{Name: "foo"}
|
|
@ -1,4 +1,4 @@
|
||||||
package consulapi
|
package api
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
121
Godeps/_workspace/src/github.com/hashicorp/consul/api/health_test.go
generated
vendored
Normal file
121
Godeps/_workspace/src/github.com/hashicorp/consul/api/health_test.go
generated
vendored
Normal file
|
@ -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)
|
||||||
|
})
|
||||||
|
}
|
|
@ -1,4 +1,4 @@
|
||||||
package consulapi
|
package api
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"bytes"
|
"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
|
// Delete is used to delete a single key
|
||||||
func (k *KV) Delete(key string, w *WriteOptions) (*WriteMeta, error) {
|
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
|
// DeleteTree is used to delete all keys under a prefix
|
||||||
func (k *KV) DeleteTree(prefix string, w *WriteOptions) (*WriteMeta, error) {
|
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 := k.c.newRequest("DELETE", "/v1/kv/"+key)
|
||||||
r.setWriteOptions(q)
|
r.setWriteOptions(q)
|
||||||
for _, param := range params {
|
for param, val := range params {
|
||||||
r.params.Set(param, "")
|
r.params.Set(param, val)
|
||||||
}
|
}
|
||||||
rtt, resp, err := requireOK(k.c.doRequest(r))
|
rtt, resp, err := requireOK(k.c.doRequest(r))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return false, nil, err
|
||||||
}
|
}
|
||||||
resp.Body.Close()
|
defer resp.Body.Close()
|
||||||
|
|
||||||
qm := &WriteMeta{}
|
qm := &WriteMeta{}
|
||||||
qm.RequestTime = rtt
|
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
|
||||||
}
|
}
|
|
@ -1,4 +1,4 @@
|
||||||
package consulapi
|
package api
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"bytes"
|
"bytes"
|
||||||
|
@ -8,7 +8,9 @@ import (
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestClientPutGetDelete(t *testing.T) {
|
func TestClientPutGetDelete(t *testing.T) {
|
||||||
c := makeClient(t)
|
c, s := makeClient(t)
|
||||||
|
defer s.stop()
|
||||||
|
|
||||||
kv := c.KV()
|
kv := c.KV()
|
||||||
|
|
||||||
// Get a get without a key
|
// Get a get without a key
|
||||||
|
@ -62,7 +64,9 @@ func TestClientPutGetDelete(t *testing.T) {
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestClient_List_DeleteRecurse(t *testing.T) {
|
func TestClient_List_DeleteRecurse(t *testing.T) {
|
||||||
c := makeClient(t)
|
c, s := makeClient(t)
|
||||||
|
defer s.stop()
|
||||||
|
|
||||||
kv := c.KV()
|
kv := c.KV()
|
||||||
|
|
||||||
// Generate some test keys
|
// 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) {
|
func TestClient_CAS(t *testing.T) {
|
||||||
c := makeClient(t)
|
c, s := makeClient(t)
|
||||||
|
defer s.stop()
|
||||||
|
|
||||||
kv := c.KV()
|
kv := c.KV()
|
||||||
|
|
||||||
// Put the key
|
// Put the key
|
||||||
|
@ -159,7 +210,9 @@ func TestClient_CAS(t *testing.T) {
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestClient_WatchGet(t *testing.T) {
|
func TestClient_WatchGet(t *testing.T) {
|
||||||
c := makeClient(t)
|
c, s := makeClient(t)
|
||||||
|
defer s.stop()
|
||||||
|
|
||||||
kv := c.KV()
|
kv := c.KV()
|
||||||
|
|
||||||
// Get a get without a key
|
// Get a get without a key
|
||||||
|
@ -178,7 +231,6 @@ func TestClient_WatchGet(t *testing.T) {
|
||||||
// Put the key
|
// Put the key
|
||||||
value := []byte("test")
|
value := []byte("test")
|
||||||
go func() {
|
go func() {
|
||||||
c := makeClient(t)
|
|
||||||
kv := c.KV()
|
kv := c.KV()
|
||||||
|
|
||||||
time.Sleep(100 * time.Millisecond)
|
time.Sleep(100 * time.Millisecond)
|
||||||
|
@ -209,7 +261,9 @@ func TestClient_WatchGet(t *testing.T) {
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestClient_WatchList(t *testing.T) {
|
func TestClient_WatchList(t *testing.T) {
|
||||||
c := makeClient(t)
|
c, s := makeClient(t)
|
||||||
|
defer s.stop()
|
||||||
|
|
||||||
kv := c.KV()
|
kv := c.KV()
|
||||||
|
|
||||||
// Get a get without a key
|
// Get a get without a key
|
||||||
|
@ -229,7 +283,6 @@ func TestClient_WatchList(t *testing.T) {
|
||||||
// Put the key
|
// Put the key
|
||||||
value := []byte("test")
|
value := []byte("test")
|
||||||
go func() {
|
go func() {
|
||||||
c := makeClient(t)
|
|
||||||
kv := c.KV()
|
kv := c.KV()
|
||||||
|
|
||||||
time.Sleep(100 * time.Millisecond)
|
time.Sleep(100 * time.Millisecond)
|
||||||
|
@ -261,7 +314,9 @@ func TestClient_WatchList(t *testing.T) {
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestClient_Keys_DeleteRecurse(t *testing.T) {
|
func TestClient_Keys_DeleteRecurse(t *testing.T) {
|
||||||
c := makeClient(t)
|
c, s := makeClient(t)
|
||||||
|
defer s.stop()
|
||||||
|
|
||||||
kv := c.KV()
|
kv := c.KV()
|
||||||
|
|
||||||
// Generate some test keys
|
// Generate some test keys
|
||||||
|
@ -308,7 +363,9 @@ func TestClient_Keys_DeleteRecurse(t *testing.T) {
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestClient_AcquireRelease(t *testing.T) {
|
func TestClient_AcquireRelease(t *testing.T) {
|
||||||
c := makeClient(t)
|
c, s := makeClient(t)
|
||||||
|
defer s.stop()
|
||||||
|
|
||||||
session := c.Session()
|
session := c.Session()
|
||||||
kv := c.KV()
|
kv := c.KV()
|
||||||
|
|
|
@ -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
|
||||||
|
}
|
||||||
|
}
|
289
Godeps/_workspace/src/github.com/hashicorp/consul/api/lock_test.go
generated
vendored
Normal file
289
Godeps/_workspace/src/github.com/hashicorp/consul/api/lock_test.go
generated
vendored
Normal file
|
@ -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)
|
||||||
|
}
|
||||||
|
}
|
482
Godeps/_workspace/src/github.com/hashicorp/consul/api/semaphore.go
generated
vendored
Normal file
482
Godeps/_workspace/src/github.com/hashicorp/consul/api/semaphore.go
generated
vendored
Normal file
|
@ -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
|
||||||
|
}
|
||||||
|
}
|
306
Godeps/_workspace/src/github.com/hashicorp/consul/api/semaphore_test.go
generated
vendored
Normal file
306
Godeps/_workspace/src/github.com/hashicorp/consul/api/semaphore_test.go
generated
vendored
Normal file
|
@ -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)
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,9 +1,20 @@
|
||||||
package consulapi
|
package api
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"time"
|
"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
|
// SessionEntry represents a session in consul
|
||||||
type SessionEntry struct {
|
type SessionEntry struct {
|
||||||
CreateIndex uint64
|
CreateIndex uint64
|
||||||
|
@ -136,6 +147,36 @@ func (s *Session) Renew(id string, q *WriteOptions) (*SessionEntry, *WriteMeta,
|
||||||
return nil, wm, nil
|
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
|
// Info looks up a single session
|
||||||
func (s *Session) Info(id string, q *QueryOptions) (*SessionEntry, *QueryMeta, error) {
|
func (s *Session) Info(id string, q *QueryOptions) (*SessionEntry, *QueryMeta, error) {
|
||||||
r := s.c.newRequest("GET", "/v1/session/info/"+id)
|
r := s.c.newRequest("GET", "/v1/session/info/"+id)
|
|
@ -1,11 +1,13 @@
|
||||||
package consulapi
|
package api
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"testing"
|
"testing"
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestSession_CreateDestroy(t *testing.T) {
|
func TestSession_CreateDestroy(t *testing.T) {
|
||||||
c := makeClient(t)
|
c, s := makeClient(t)
|
||||||
|
defer s.stop()
|
||||||
|
|
||||||
session := c.Session()
|
session := c.Session()
|
||||||
|
|
||||||
id, meta, err := session.Create(nil, nil)
|
id, meta, err := session.Create(nil, nil)
|
||||||
|
@ -32,7 +34,9 @@ func TestSession_CreateDestroy(t *testing.T) {
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestSession_CreateRenewDestroy(t *testing.T) {
|
func TestSession_CreateRenewDestroy(t *testing.T) {
|
||||||
c := makeClient(t)
|
c, s := makeClient(t)
|
||||||
|
defer s.stop()
|
||||||
|
|
||||||
session := c.Session()
|
session := c.Session()
|
||||||
|
|
||||||
se := &SessionEntry{
|
se := &SessionEntry{
|
||||||
|
@ -80,7 +84,9 @@ func TestSession_CreateRenewDestroy(t *testing.T) {
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestSession_Info(t *testing.T) {
|
func TestSession_Info(t *testing.T) {
|
||||||
c := makeClient(t)
|
c, s := makeClient(t)
|
||||||
|
defer s.stop()
|
||||||
|
|
||||||
session := c.Session()
|
session := c.Session()
|
||||||
|
|
||||||
id, _, err := session.Create(nil, nil)
|
id, _, err := session.Create(nil, nil)
|
||||||
|
@ -131,7 +137,9 @@ func TestSession_Info(t *testing.T) {
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestSession_Node(t *testing.T) {
|
func TestSession_Node(t *testing.T) {
|
||||||
c := makeClient(t)
|
c, s := makeClient(t)
|
||||||
|
defer s.stop()
|
||||||
|
|
||||||
session := c.Session()
|
session := c.Session()
|
||||||
|
|
||||||
id, _, err := session.Create(nil, nil)
|
id, _, err := session.Create(nil, nil)
|
||||||
|
@ -163,7 +171,9 @@ func TestSession_Node(t *testing.T) {
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestSession_List(t *testing.T) {
|
func TestSession_List(t *testing.T) {
|
||||||
c := makeClient(t)
|
c, s := makeClient(t)
|
||||||
|
defer s.stop()
|
||||||
|
|
||||||
session := c.Session()
|
session := c.Session()
|
||||||
|
|
||||||
id, _, err := session.Create(nil, nil)
|
id, _, err := session.Create(nil, nil)
|
|
@ -1,4 +1,4 @@
|
||||||
package consulapi
|
package api
|
||||||
|
|
||||||
// Status can be used to query the Status endpoints
|
// Status can be used to query the Status endpoints
|
||||||
type Status struct {
|
type Status struct {
|
|
@ -1,11 +1,13 @@
|
||||||
package consulapi
|
package api
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"testing"
|
"testing"
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestStatusLeader(t *testing.T) {
|
func TestStatusLeader(t *testing.T) {
|
||||||
c := makeClient(t)
|
c, s := makeClient(t)
|
||||||
|
defer s.stop()
|
||||||
|
|
||||||
status := c.Status()
|
status := c.Status()
|
||||||
|
|
||||||
leader, err := status.Leader()
|
leader, err := status.Leader()
|
||||||
|
@ -18,7 +20,9 @@ func TestStatusLeader(t *testing.T) {
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestStatusPeers(t *testing.T) {
|
func TestStatusPeers(t *testing.T) {
|
||||||
c := makeClient(t)
|
c, s := makeClient(t)
|
||||||
|
defer s.stop()
|
||||||
|
|
||||||
status := c.Status()
|
status := c.Status()
|
||||||
|
|
||||||
peers, err := status.Peers()
|
peers, err := status.Peers()
|
|
@ -7,8 +7,8 @@ import (
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
log "github.com/Sirupsen/logrus"
|
log "github.com/Sirupsen/logrus"
|
||||||
consul "github.com/armon/consul-api"
|
|
||||||
"github.com/docker/swarm/discovery"
|
"github.com/docker/swarm/discovery"
|
||||||
|
consul "github.com/hashicorp/consul/api"
|
||||||
)
|
)
|
||||||
|
|
||||||
type ConsulDiscoveryService struct {
|
type ConsulDiscoveryService struct {
|
||||||
|
|
Loading…
Reference in New Issue