mirror of https://github.com/hashicorp/consul
Ryan Uber
10 years ago
28 changed files with 3050 additions and 8 deletions
@ -0,0 +1,23 @@
|
||||
# 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 |
@ -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) |
||||
|
||||
``` |
||||
|
@ -0,0 +1,140 @@
|
||||
package api |
||||
|
||||
const ( |
||||
// ACLCLientType is the client type token
|
||||
ACLClientType = "client" |
||||
|
||||
// ACLManagementType is the management type token
|
||||
ACLManagementType = "management" |
||||
) |
||||
|
||||
// ACLEntry is used to represent an ACL entry
|
||||
type ACLEntry struct { |
||||
CreateIndex uint64 |
||||
ModifyIndex uint64 |
||||
ID string |
||||
Name string |
||||
Type string |
||||
Rules string |
||||
} |
||||
|
||||
// ACL can be used to query the ACL endpoints
|
||||
type ACL struct { |
||||
c *Client |
||||
} |
||||
|
||||
// ACL returns a handle to the ACL endpoints
|
||||
func (c *Client) ACL() *ACL { |
||||
return &ACL{c} |
||||
} |
||||
|
||||
// Create is used to generate a new token with the given parameters
|
||||
func (a *ACL) Create(acl *ACLEntry, q *WriteOptions) (string, *WriteMeta, error) { |
||||
r := a.c.newRequest("PUT", "/v1/acl/create") |
||||
r.setWriteOptions(q) |
||||
r.obj = acl |
||||
rtt, resp, err := requireOK(a.c.doRequest(r)) |
||||
if err != nil { |
||||
return "", nil, err |
||||
} |
||||
defer resp.Body.Close() |
||||
|
||||
wm := &WriteMeta{RequestTime: rtt} |
||||
var out struct{ ID string } |
||||
if err := decodeBody(resp, &out); err != nil { |
||||
return "", nil, err |
||||
} |
||||
return out.ID, wm, nil |
||||
} |
||||
|
||||
// Update is used to update the rules of an existing token
|
||||
func (a *ACL) Update(acl *ACLEntry, q *WriteOptions) (*WriteMeta, error) { |
||||
r := a.c.newRequest("PUT", "/v1/acl/update") |
||||
r.setWriteOptions(q) |
||||
r.obj = acl |
||||
rtt, resp, err := requireOK(a.c.doRequest(r)) |
||||
if err != nil { |
||||
return nil, err |
||||
} |
||||
defer resp.Body.Close() |
||||
|
||||
wm := &WriteMeta{RequestTime: rtt} |
||||
return wm, nil |
||||
} |
||||
|
||||
// Destroy is used to destroy a given ACL token ID
|
||||
func (a *ACL) Destroy(id string, q *WriteOptions) (*WriteMeta, error) { |
||||
r := a.c.newRequest("PUT", "/v1/acl/destroy/"+id) |
||||
r.setWriteOptions(q) |
||||
rtt, resp, err := requireOK(a.c.doRequest(r)) |
||||
if err != nil { |
||||
return nil, err |
||||
} |
||||
resp.Body.Close() |
||||
|
||||
wm := &WriteMeta{RequestTime: rtt} |
||||
return wm, nil |
||||
} |
||||
|
||||
// Clone is used to return a new token cloned from an existing one
|
||||
func (a *ACL) Clone(id string, q *WriteOptions) (string, *WriteMeta, error) { |
||||
r := a.c.newRequest("PUT", "/v1/acl/clone/"+id) |
||||
r.setWriteOptions(q) |
||||
rtt, resp, err := requireOK(a.c.doRequest(r)) |
||||
if err != nil { |
||||
return "", nil, err |
||||
} |
||||
defer resp.Body.Close() |
||||
|
||||
wm := &WriteMeta{RequestTime: rtt} |
||||
var out struct{ ID string } |
||||
if err := decodeBody(resp, &out); err != nil { |
||||
return "", nil, err |
||||
} |
||||
return out.ID, wm, nil |
||||
} |
||||
|
||||
// Info is used to query for information about an ACL token
|
||||
func (a *ACL) Info(id string, q *QueryOptions) (*ACLEntry, *QueryMeta, error) { |
||||
r := a.c.newRequest("GET", "/v1/acl/info/"+id) |
||||
r.setQueryOptions(q) |
||||
rtt, resp, err := requireOK(a.c.doRequest(r)) |
||||
if err != nil { |
||||
return nil, nil, err |
||||
} |
||||
defer resp.Body.Close() |
||||
|
||||
qm := &QueryMeta{} |
||||
parseQueryMeta(resp, qm) |
||||
qm.RequestTime = rtt |
||||
|
||||
var entries []*ACLEntry |
||||
if err := decodeBody(resp, &entries); err != nil { |
||||
return nil, nil, err |
||||
} |
||||
if len(entries) > 0 { |
||||
return entries[0], qm, nil |
||||
} |
||||
return nil, qm, nil |
||||
} |
||||
|
||||
// List is used to get all the ACL tokens
|
||||
func (a *ACL) List(q *QueryOptions) ([]*ACLEntry, *QueryMeta, error) { |
||||
r := a.c.newRequest("GET", "/v1/acl/list") |
||||
r.setQueryOptions(q) |
||||
rtt, resp, err := requireOK(a.c.doRequest(r)) |
||||
if err != nil { |
||||
return nil, nil, err |
||||
} |
||||
defer resp.Body.Close() |
||||
|
||||
qm := &QueryMeta{} |
||||
parseQueryMeta(resp, qm) |
||||
qm.RequestTime = rtt |
||||
|
||||
var entries []*ACLEntry |
||||
if err := decodeBody(resp, &entries); err != nil { |
||||
return nil, nil, err |
||||
} |
||||
return entries, qm, nil |
||||
} |
@ -0,0 +1,140 @@
|
||||
package api |
||||
|
||||
import ( |
||||
"os" |
||||
"testing" |
||||
) |
||||
|
||||
// ROOT is a management token for the tests
|
||||
var CONSUL_ROOT string |
||||
|
||||
func init() { |
||||
CONSUL_ROOT = os.Getenv("CONSUL_ROOT") |
||||
} |
||||
|
||||
func TestACL_CreateDestroy(t *testing.T) { |
||||
if CONSUL_ROOT == "" { |
||||
t.SkipNow() |
||||
} |
||||
c := makeClient(t) |
||||
c.config.Token = CONSUL_ROOT |
||||
acl := c.ACL() |
||||
|
||||
ae := ACLEntry{ |
||||
Name: "API test", |
||||
Type: ACLClientType, |
||||
Rules: `key "" { policy = "deny" }`, |
||||
} |
||||
|
||||
id, wm, err := acl.Create(&ae, nil) |
||||
if err != nil { |
||||
t.Fatalf("err: %v", err) |
||||
} |
||||
|
||||
if wm.RequestTime == 0 { |
||||
t.Fatalf("bad: %v", wm) |
||||
} |
||||
|
||||
if id == "" { |
||||
t.Fatalf("invalid: %v", id) |
||||
} |
||||
|
||||
ae2, _, err := acl.Info(id, nil) |
||||
if err != nil { |
||||
t.Fatalf("err: %v", err) |
||||
} |
||||
|
||||
if ae2.Name != ae.Name || ae2.Type != ae.Type || ae2.Rules != ae.Rules { |
||||
t.Fatalf("Bad: %#v", ae2) |
||||
} |
||||
|
||||
wm, err = acl.Destroy(id, nil) |
||||
if err != nil { |
||||
t.Fatalf("err: %v", err) |
||||
} |
||||
|
||||
if wm.RequestTime == 0 { |
||||
t.Fatalf("bad: %v", wm) |
||||
} |
||||
} |
||||
|
||||
func TestACL_CloneDestroy(t *testing.T) { |
||||
if CONSUL_ROOT == "" { |
||||
t.SkipNow() |
||||
} |
||||
c := makeClient(t) |
||||
c.config.Token = CONSUL_ROOT |
||||
acl := c.ACL() |
||||
|
||||
id, wm, err := acl.Clone(CONSUL_ROOT, nil) |
||||
if err != nil { |
||||
t.Fatalf("err: %v", err) |
||||
} |
||||
|
||||
if wm.RequestTime == 0 { |
||||
t.Fatalf("bad: %v", wm) |
||||
} |
||||
|
||||
if id == "" { |
||||
t.Fatalf("invalid: %v", id) |
||||
} |
||||
|
||||
wm, err = acl.Destroy(id, nil) |
||||
if err != nil { |
||||
t.Fatalf("err: %v", err) |
||||
} |
||||
|
||||
if wm.RequestTime == 0 { |
||||
t.Fatalf("bad: %v", wm) |
||||
} |
||||
} |
||||
|
||||
func TestACL_Info(t *testing.T) { |
||||
if CONSUL_ROOT == "" { |
||||
t.SkipNow() |
||||
} |
||||
c := makeClient(t) |
||||
c.config.Token = CONSUL_ROOT |
||||
acl := c.ACL() |
||||
|
||||
ae, qm, err := acl.Info(CONSUL_ROOT, nil) |
||||
if err != nil { |
||||
t.Fatalf("err: %v", err) |
||||
} |
||||
|
||||
if qm.LastIndex == 0 { |
||||
t.Fatalf("bad: %v", qm) |
||||
} |
||||
if !qm.KnownLeader { |
||||
t.Fatalf("bad: %v", qm) |
||||
} |
||||
|
||||
if ae == nil || ae.ID != CONSUL_ROOT || ae.Type != ACLManagementType { |
||||
t.Fatalf("bad: %#v", ae) |
||||
} |
||||
} |
||||
|
||||
func TestACL_List(t *testing.T) { |
||||
if CONSUL_ROOT == "" { |
||||
t.SkipNow() |
||||
} |
||||
c := makeClient(t) |
||||
c.config.Token = CONSUL_ROOT |
||||
acl := c.ACL() |
||||
|
||||
acls, qm, err := acl.List(nil) |
||||
if err != nil { |
||||
t.Fatalf("err: %v", err) |
||||
} |
||||
|
||||
if len(acls) < 2 { |
||||
t.Fatalf("bad: %v", acls) |
||||
} |
||||
|
||||
if qm.LastIndex == 0 { |
||||
t.Fatalf("bad: %v", qm) |
||||
} |
||||
if !qm.KnownLeader { |
||||
t.Fatalf("bad: %v", qm) |
||||
} |
||||
} |
@ -0,0 +1,272 @@
|
||||
package api |
||||
|
||||
import ( |
||||
"fmt" |
||||
) |
||||
|
||||
// AgentCheck represents a check known to the agent
|
||||
type AgentCheck struct { |
||||
Node string |
||||
CheckID string |
||||
Name string |
||||
Status string |
||||
Notes string |
||||
Output string |
||||
ServiceID string |
||||
ServiceName string |
||||
} |
||||
|
||||
// AgentService represents a service known to the agent
|
||||
type AgentService struct { |
||||
ID string |
||||
Service string |
||||
Tags []string |
||||
Port int |
||||
} |
||||
|
||||
// AgentMember represents a cluster member known to the agent
|
||||
type AgentMember struct { |
||||
Name string |
||||
Addr string |
||||
Port uint16 |
||||
Tags map[string]string |
||||
Status int |
||||
ProtocolMin uint8 |
||||
ProtocolMax uint8 |
||||
ProtocolCur uint8 |
||||
DelegateMin uint8 |
||||
DelegateMax uint8 |
||||
DelegateCur uint8 |
||||
} |
||||
|
||||
// AgentServiceRegistration is used to register a new service
|
||||
type AgentServiceRegistration struct { |
||||
ID string `json:",omitempty"` |
||||
Name string `json:",omitempty"` |
||||
Tags []string `json:",omitempty"` |
||||
Port int `json:",omitempty"` |
||||
Check *AgentServiceCheck |
||||
} |
||||
|
||||
// AgentCheckRegistration is used to register a new check
|
||||
type AgentCheckRegistration struct { |
||||
ID string `json:",omitempty"` |
||||
Name string `json:",omitempty"` |
||||
Notes string `json:",omitempty"` |
||||
AgentServiceCheck |
||||
} |
||||
|
||||
// AgentServiceCheck is used to create an associated
|
||||
// check for a service
|
||||
type AgentServiceCheck struct { |
||||
Script string `json:",omitempty"` |
||||
Interval string `json:",omitempty"` |
||||
TTL string `json:",omitempty"` |
||||
} |
||||
|
||||
// Agent can be used to query the Agent endpoints
|
||||
type Agent struct { |
||||
c *Client |
||||
|
||||
// cache the node name
|
||||
nodeName string |
||||
} |
||||
|
||||
// Agent returns a handle to the agent endpoints
|
||||
func (c *Client) Agent() *Agent { |
||||
return &Agent{c: c} |
||||
} |
||||
|
||||
// Self is used to query the agent we are speaking to for
|
||||
// information about itself
|
||||
func (a *Agent) Self() (map[string]map[string]interface{}, error) { |
||||
r := a.c.newRequest("GET", "/v1/agent/self") |
||||
_, resp, err := requireOK(a.c.doRequest(r)) |
||||
if err != nil { |
||||
return nil, err |
||||
} |
||||
defer resp.Body.Close() |
||||
|
||||
var out map[string]map[string]interface{} |
||||
if err := decodeBody(resp, &out); err != nil { |
||||
return nil, err |
||||
} |
||||
return out, nil |
||||
} |
||||
|
||||
// NodeName is used to get the node name of the agent
|
||||
func (a *Agent) NodeName() (string, error) { |
||||
if a.nodeName != "" { |
||||
return a.nodeName, nil |
||||
} |
||||
info, err := a.Self() |
||||
if err != nil { |
||||
return "", err |
||||
} |
||||
name := info["Config"]["NodeName"].(string) |
||||
a.nodeName = name |
||||
return name, nil |
||||
} |
||||
|
||||
// Checks returns the locally registered checks
|
||||
func (a *Agent) Checks() (map[string]*AgentCheck, error) { |
||||
r := a.c.newRequest("GET", "/v1/agent/checks") |
||||
_, resp, err := requireOK(a.c.doRequest(r)) |
||||
if err != nil { |
||||
return nil, err |
||||
} |
||||
defer resp.Body.Close() |
||||
|
||||
var out map[string]*AgentCheck |
||||
if err := decodeBody(resp, &out); err != nil { |
||||
return nil, err |
||||
} |
||||
return out, nil |
||||
} |
||||
|
||||
// Services returns the locally registered services
|
||||
func (a *Agent) Services() (map[string]*AgentService, error) { |
||||
r := a.c.newRequest("GET", "/v1/agent/services") |
||||
_, resp, err := requireOK(a.c.doRequest(r)) |
||||
if err != nil { |
||||
return nil, err |
||||
} |
||||
defer resp.Body.Close() |
||||
|
||||
var out map[string]*AgentService |
||||
if err := decodeBody(resp, &out); err != nil { |
||||
return nil, err |
||||
} |
||||
return out, nil |
||||
} |
||||
|
||||
// Members returns the known gossip members. The WAN
|
||||
// flag can be used to query a server for WAN members.
|
||||
func (a *Agent) Members(wan bool) ([]*AgentMember, error) { |
||||
r := a.c.newRequest("GET", "/v1/agent/members") |
||||
if wan { |
||||
r.params.Set("wan", "1") |
||||
} |
||||
_, resp, err := requireOK(a.c.doRequest(r)) |
||||
if err != nil { |
||||
return nil, err |
||||
} |
||||
defer resp.Body.Close() |
||||
|
||||
var out []*AgentMember |
||||
if err := decodeBody(resp, &out); err != nil { |
||||
return nil, err |
||||
} |
||||
return out, nil |
||||
} |
||||
|
||||
// ServiceRegister is used to register a new service with
|
||||
// the local agent
|
||||
func (a *Agent) ServiceRegister(service *AgentServiceRegistration) error { |
||||
r := a.c.newRequest("PUT", "/v1/agent/service/register") |
||||
r.obj = service |
||||
_, resp, err := requireOK(a.c.doRequest(r)) |
||||
if err != nil { |
||||
return err |
||||
} |
||||
resp.Body.Close() |
||||
return nil |
||||
} |
||||
|
||||
// ServiceDeregister is used to deregister a service with
|
||||
// the local agent
|
||||
func (a *Agent) ServiceDeregister(serviceID string) error { |
||||
r := a.c.newRequest("PUT", "/v1/agent/service/deregister/"+serviceID) |
||||
_, resp, err := requireOK(a.c.doRequest(r)) |
||||
if err != nil { |
||||
return err |
||||
} |
||||
resp.Body.Close() |
||||
return nil |
||||
} |
||||
|
||||
// PassTTL is used to set a TTL check to the passing state
|
||||
func (a *Agent) PassTTL(checkID, note string) error { |
||||
return a.UpdateTTL(checkID, note, "pass") |
||||
} |
||||
|
||||
// WarnTTL is used to set a TTL check to the warning state
|
||||
func (a *Agent) WarnTTL(checkID, note string) error { |
||||
return a.UpdateTTL(checkID, note, "warn") |
||||
} |
||||
|
||||
// FailTTL is used to set a TTL check to the failing state
|
||||
func (a *Agent) FailTTL(checkID, note string) error { |
||||
return a.UpdateTTL(checkID, note, "fail") |
||||
} |
||||
|
||||
// UpdateTTL is used to update the TTL of a check
|
||||
func (a *Agent) UpdateTTL(checkID, note, status string) error { |
||||
switch status { |
||||
case "pass": |
||||
case "warn": |
||||
case "fail": |
||||
default: |
||||
return fmt.Errorf("Invalid status: %s", status) |
||||
} |
||||
endpoint := fmt.Sprintf("/v1/agent/check/%s/%s", status, checkID) |
||||
r := a.c.newRequest("PUT", endpoint) |
||||
r.params.Set("note", note) |
||||
_, resp, err := requireOK(a.c.doRequest(r)) |
||||
if err != nil { |
||||
return err |
||||
} |
||||
resp.Body.Close() |
||||
return nil |
||||
} |
||||
|
||||
// CheckRegister is used to register a new check with
|
||||
// the local agent
|
||||
func (a *Agent) CheckRegister(check *AgentCheckRegistration) error { |
||||
r := a.c.newRequest("PUT", "/v1/agent/check/register") |
||||
r.obj = check |
||||
_, resp, err := requireOK(a.c.doRequest(r)) |
||||
if err != nil { |
||||
return err |
||||
} |
||||
resp.Body.Close() |
||||
return nil |
||||
} |
||||
|
||||
// CheckDeregister is used to deregister a check with
|
||||
// the local agent
|
||||
func (a *Agent) CheckDeregister(checkID string) error { |
||||
r := a.c.newRequest("PUT", "/v1/agent/check/deregister/"+checkID) |
||||
_, resp, err := requireOK(a.c.doRequest(r)) |
||||
if err != nil { |
||||
return err |
||||
} |
||||
resp.Body.Close() |
||||
return nil |
||||
} |
||||
|
||||
// Join is used to instruct the agent to attempt a join to
|
||||
// another cluster member
|
||||
func (a *Agent) Join(addr string, wan bool) error { |
||||
r := a.c.newRequest("PUT", "/v1/agent/join/"+addr) |
||||
if wan { |
||||
r.params.Set("wan", "1") |
||||
} |
||||
_, resp, err := requireOK(a.c.doRequest(r)) |
||||
if err != nil { |
||||
return err |
||||
} |
||||
resp.Body.Close() |
||||
return nil |
||||
} |
||||
|
||||
// ForceLeave is used to have the agent eject a failed node
|
||||
func (a *Agent) ForceLeave(node string) error { |
||||
r := a.c.newRequest("PUT", "/v1/agent/force-leave/"+node) |
||||
_, resp, err := requireOK(a.c.doRequest(r)) |
||||
if err != nil { |
||||
return err |
||||
} |
||||
resp.Body.Close() |
||||
return nil |
||||
} |
@ -0,0 +1,162 @@
|
||||
package api |
||||
|
||||
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) |
||||
} |
||||
} |
@ -0,0 +1,304 @@
|
||||
package api |
||||
|
||||
import ( |
||||
"bytes" |
||||
"encoding/json" |
||||
"fmt" |
||||
"io" |
||||
"net/http" |
||||
"net/url" |
||||
"strconv" |
||||
"time" |
||||
) |
||||
|
||||
// QueryOptions are used to parameterize a query
|
||||
type QueryOptions struct { |
||||
// Providing a datacenter overwrites the DC provided
|
||||
// by the Config
|
||||
Datacenter string |
||||
|
||||
// AllowStale allows any Consul server (non-leader) to service
|
||||
// a read. This allows for lower latency and higher throughput
|
||||
AllowStale bool |
||||
|
||||
// RequireConsistent forces the read to be fully consistent.
|
||||
// This is more expensive but prevents ever performing a stale
|
||||
// read.
|
||||
RequireConsistent bool |
||||
|
||||
// WaitIndex is used to enable a blocking query. Waits
|
||||
// until the timeout or the next index is reached
|
||||
WaitIndex uint64 |
||||
|
||||
// WaitTime is used to bound the duration of a wait.
|
||||
// Defaults to that of the Config, but can be overriden.
|
||||
WaitTime time.Duration |
||||
|
||||
// Token is used to provide a per-request ACL token
|
||||
// which overrides the agent's default token.
|
||||
Token string |
||||
} |
||||
|
||||
// WriteOptions are used to parameterize a write
|
||||
type WriteOptions struct { |
||||
// Providing a datacenter overwrites the DC provided
|
||||
// by the Config
|
||||
Datacenter string |
||||
|
||||
// Token is used to provide a per-request ACL token
|
||||
// which overrides the agent's default token.
|
||||
Token string |
||||
} |
||||
|
||||
// QueryMeta is used to return meta data about a query
|
||||
type QueryMeta struct { |
||||
// LastIndex. This can be used as a WaitIndex to perform
|
||||
// a blocking query
|
||||
LastIndex uint64 |
||||
|
||||
// Time of last contact from the leader for the
|
||||
// server servicing the request
|
||||
LastContact time.Duration |
||||
|
||||
// Is there a known leader
|
||||
KnownLeader bool |
||||
|
||||
// How long did the request take
|
||||
RequestTime time.Duration |
||||
} |
||||
|
||||
// WriteMeta is used to return meta data about a write
|
||||
type WriteMeta struct { |
||||
// How long did the request take
|
||||
RequestTime time.Duration |
||||
} |
||||
|
||||
// Config is used to configure the creation of a client
|
||||
type Config struct { |
||||
// Address is the address of the Consul server
|
||||
Address string |
||||
|
||||
// Scheme is the URI scheme for the Consul server
|
||||
Scheme string |
||||
|
||||
// Datacenter to use. If not provided, the default agent datacenter is used.
|
||||
Datacenter string |
||||
|
||||
// HttpClient is the client to use. Default will be
|
||||
// used if not provided.
|
||||
HttpClient *http.Client |
||||
|
||||
// WaitTime limits how long a Watch will block. If not provided,
|
||||
// the agent default values will be used.
|
||||
WaitTime time.Duration |
||||
|
||||
// Token is used to provide a per-request ACL token
|
||||
// which overrides the agent's default token.
|
||||
Token string |
||||
} |
||||
|
||||
// DefaultConfig returns a default configuration for the client
|
||||
func DefaultConfig() *Config { |
||||
return &Config{ |
||||
Address: "127.0.0.1:8500", |
||||
Scheme: "http", |
||||
HttpClient: http.DefaultClient, |
||||
} |
||||
} |
||||
|
||||
// Client provides a client to the Consul API
|
||||
type Client struct { |
||||
config Config |
||||
} |
||||
|
||||
// NewClient returns a new client
|
||||
func NewClient(config *Config) (*Client, error) { |
||||
// bootstrap the config
|
||||
defConfig := DefaultConfig() |
||||
|
||||
if len(config.Address) == 0 { |
||||
config.Address = defConfig.Address |
||||
} |
||||
|
||||
if len(config.Scheme) == 0 { |
||||
config.Scheme = defConfig.Scheme |
||||
} |
||||
|
||||
if config.HttpClient == nil { |
||||
config.HttpClient = defConfig.HttpClient |
||||
} |
||||
|
||||
client := &Client{ |
||||
config: *config, |
||||
} |
||||
return client, nil |
||||
} |
||||
|
||||
// request is used to help build up a request
|
||||
type request struct { |
||||
config *Config |
||||
method string |
||||
url *url.URL |
||||
params url.Values |
||||
body io.Reader |
||||
obj interface{} |
||||
} |
||||
|
||||
// setQueryOptions is used to annotate the request with
|
||||
// additional query options
|
||||
func (r *request) setQueryOptions(q *QueryOptions) { |
||||
if q == nil { |
||||
return |
||||
} |
||||
if q.Datacenter != "" { |
||||
r.params.Set("dc", q.Datacenter) |
||||
} |
||||
if q.AllowStale { |
||||
r.params.Set("stale", "") |
||||
} |
||||
if q.RequireConsistent { |
||||
r.params.Set("consistent", "") |
||||
} |
||||
if q.WaitIndex != 0 { |
||||
r.params.Set("index", strconv.FormatUint(q.WaitIndex, 10)) |
||||
} |
||||
if q.WaitTime != 0 { |
||||
r.params.Set("wait", durToMsec(q.WaitTime)) |
||||
} |
||||
if q.Token != "" { |
||||
r.params.Set("token", q.Token) |
||||
} |
||||
} |
||||
|
||||
// durToMsec converts a duration to a millisecond specified string
|
||||
func durToMsec(dur time.Duration) string { |
||||
return fmt.Sprintf("%dms", dur/time.Millisecond) |
||||
} |
||||
|
||||
// setWriteOptions is used to annotate the request with
|
||||
// additional write options
|
||||
func (r *request) setWriteOptions(q *WriteOptions) { |
||||
if q == nil { |
||||
return |
||||
} |
||||
if q.Datacenter != "" { |
||||
r.params.Set("dc", q.Datacenter) |
||||
} |
||||
if q.Token != "" { |
||||
r.params.Set("token", q.Token) |
||||
} |
||||
} |
||||
|
||||
// toHTTP converts the request to an HTTP request
|
||||
func (r *request) toHTTP() (*http.Request, error) { |
||||
// Encode the query parameters
|
||||
r.url.RawQuery = r.params.Encode() |
||||
|
||||
// Get the url sring
|
||||
urlRaw := r.url.String() |
||||
|
||||
// Check if we should encode the body
|
||||
if r.body == nil && r.obj != nil { |
||||
if b, err := encodeBody(r.obj); err != nil { |
||||
return nil, err |
||||
} else { |
||||
r.body = b |
||||
} |
||||
} |
||||
|
||||
// Create the HTTP request
|
||||
return http.NewRequest(r.method, urlRaw, r.body) |
||||
} |
||||
|
||||
// newRequest is used to create a new request
|
||||
func (c *Client) newRequest(method, path string) *request { |
||||
r := &request{ |
||||
config: &c.config, |
||||
method: method, |
||||
url: &url.URL{ |
||||
Scheme: c.config.Scheme, |
||||
Host: c.config.Address, |
||||
Path: path, |
||||
}, |
||||
params: make(map[string][]string), |
||||
} |
||||
if c.config.Datacenter != "" { |
||||
r.params.Set("dc", c.config.Datacenter) |
||||
} |
||||
if c.config.WaitTime != 0 { |
||||
r.params.Set("wait", durToMsec(r.config.WaitTime)) |
||||
} |
||||
if c.config.Token != "" { |
||||
r.params.Set("token", r.config.Token) |
||||
} |
||||
return r |
||||
} |
||||
|
||||
// doRequest runs a request with our client
|
||||
func (c *Client) doRequest(r *request) (time.Duration, *http.Response, error) { |
||||
req, err := r.toHTTP() |
||||
if err != nil { |
||||
return 0, nil, err |
||||
} |
||||
start := time.Now() |
||||
resp, err := c.config.HttpClient.Do(req) |
||||
diff := time.Now().Sub(start) |
||||
return diff, resp, err |
||||
} |
||||
|
||||
// parseQueryMeta is used to help parse query meta-data
|
||||
func parseQueryMeta(resp *http.Response, q *QueryMeta) error { |
||||
header := resp.Header |
||||
|
||||
// Parse the X-Consul-Index
|
||||
index, err := strconv.ParseUint(header.Get("X-Consul-Index"), 10, 64) |
||||
if err != nil { |
||||
return fmt.Errorf("Failed to parse X-Consul-Index: %v", err) |
||||
} |
||||
q.LastIndex = index |
||||
|
||||
// Parse the X-Consul-LastContact
|
||||
last, err := strconv.ParseUint(header.Get("X-Consul-LastContact"), 10, 64) |
||||
if err != nil { |
||||
return fmt.Errorf("Failed to parse X-Consul-LastContact: %v", err) |
||||
} |
||||
q.LastContact = time.Duration(last) * time.Millisecond |
||||
|
||||
// Parse the X-Consul-KnownLeader
|
||||
switch header.Get("X-Consul-KnownLeader") { |
||||
case "true": |
||||
q.KnownLeader = true |
||||
default: |
||||
q.KnownLeader = false |
||||
} |
||||
return nil |
||||
} |
||||
|
||||
// decodeBody is used to JSON decode a body
|
||||
func decodeBody(resp *http.Response, out interface{}) error { |
||||
dec := json.NewDecoder(resp.Body) |
||||
return dec.Decode(out) |
||||
} |
||||
|
||||
// encodeBody is used to encode a request body
|
||||
func encodeBody(obj interface{}) (io.Reader, error) { |
||||
buf := bytes.NewBuffer(nil) |
||||
enc := json.NewEncoder(buf) |
||||
if err := enc.Encode(obj); err != nil { |
||||
return nil, err |
||||
} |
||||
return buf, nil |
||||
} |
||||
|
||||
// requireOK is used to wrap doRequest and check for a 200
|
||||
func requireOK(d time.Duration, resp *http.Response, e error) (time.Duration, *http.Response, error) { |
||||
if e != nil { |
||||
return d, resp, e |
||||
} |
||||
if resp.StatusCode != 200 { |
||||
var buf bytes.Buffer |
||||
io.Copy(&buf, resp.Body) |
||||
return d, resp, fmt.Errorf("Unexpected response code: %d (%s)", resp.StatusCode, buf.Bytes()) |
||||
} |
||||
return d, resp, e |
||||
} |
@ -0,0 +1,126 @@
|
||||
package api |
||||
|
||||
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) |
||||
} |
||||
} |
@ -0,0 +1,181 @@
|
||||
package api |
||||
|
||||
type Node struct { |
||||
Node string |
||||
Address string |
||||
} |
||||
|
||||
type CatalogService struct { |
||||
Node string |
||||
Address string |
||||
ServiceID string |
||||
ServiceName string |
||||
ServiceTags []string |
||||
ServicePort int |
||||
} |
||||
|
||||
type CatalogNode struct { |
||||
Node *Node |
||||
Services map[string]*AgentService |
||||
} |
||||
|
||||
type CatalogRegistration struct { |
||||
Node string |
||||
Address string |
||||
Datacenter string |
||||
Service *AgentService |
||||
Check *AgentCheck |
||||
} |
||||
|
||||
type CatalogDeregistration struct { |
||||
Node string |
||||
Address string |
||||
Datacenter string |
||||
ServiceID string |
||||
CheckID string |
||||
} |
||||
|
||||
// Catalog can be used to query the Catalog endpoints
|
||||
type Catalog struct { |
||||
c *Client |
||||
} |
||||
|
||||
// Catalog returns a handle to the catalog endpoints
|
||||
func (c *Client) Catalog() *Catalog { |
||||
return &Catalog{c} |
||||
} |
||||
|
||||
func (c *Catalog) Register(reg *CatalogRegistration, q *WriteOptions) (*WriteMeta, error) { |
||||
r := c.c.newRequest("PUT", "/v1/catalog/register") |
||||
r.setWriteOptions(q) |
||||
r.obj = reg |
||||
rtt, resp, err := requireOK(c.c.doRequest(r)) |
||||
if err != nil { |
||||
return nil, err |
||||
} |
||||
resp.Body.Close() |
||||
|
||||
wm := &WriteMeta{} |
||||
wm.RequestTime = rtt |
||||
|
||||
return wm, nil |
||||
} |
||||
|
||||
func (c *Catalog) Deregister(dereg *CatalogDeregistration, q *WriteOptions) (*WriteMeta, error) { |
||||
r := c.c.newRequest("PUT", "/v1/catalog/deregister") |
||||
r.setWriteOptions(q) |
||||
r.obj = dereg |
||||
rtt, resp, err := requireOK(c.c.doRequest(r)) |
||||
if err != nil { |
||||
return nil, err |
||||
} |
||||
resp.Body.Close() |
||||
|
||||
wm := &WriteMeta{} |
||||
wm.RequestTime = rtt |
||||
|
||||
return wm, nil |
||||
} |
||||
|
||||
// Datacenters is used to query for all the known datacenters
|
||||
func (c *Catalog) Datacenters() ([]string, error) { |
||||
r := c.c.newRequest("GET", "/v1/catalog/datacenters") |
||||
_, resp, err := requireOK(c.c.doRequest(r)) |
||||
if err != nil { |
||||
return nil, err |
||||
} |
||||
defer resp.Body.Close() |
||||
|
||||
var out []string |
||||
if err := decodeBody(resp, &out); err != nil { |
||||
return nil, err |
||||
} |
||||
return out, nil |
||||
} |
||||
|
||||
// Nodes is used to query all the known nodes
|
||||
func (c *Catalog) Nodes(q *QueryOptions) ([]*Node, *QueryMeta, error) { |
||||
r := c.c.newRequest("GET", "/v1/catalog/nodes") |
||||
r.setQueryOptions(q) |
||||
rtt, resp, err := requireOK(c.c.doRequest(r)) |
||||
if err != nil { |
||||
return nil, nil, err |
||||
} |
||||
defer resp.Body.Close() |
||||
|
||||
qm := &QueryMeta{} |
||||
parseQueryMeta(resp, qm) |
||||
qm.RequestTime = rtt |
||||
|
||||
var out []*Node |
||||
if err := decodeBody(resp, &out); err != nil { |
||||
return nil, nil, err |
||||
} |
||||
return out, qm, nil |
||||
} |
||||
|
||||
// Services is used to query for all known services
|
||||
func (c *Catalog) Services(q *QueryOptions) (map[string][]string, *QueryMeta, error) { |
||||
r := c.c.newRequest("GET", "/v1/catalog/services") |
||||
r.setQueryOptions(q) |
||||
rtt, resp, err := requireOK(c.c.doRequest(r)) |
||||
if err != nil { |
||||
return nil, nil, err |
||||
} |
||||
defer resp.Body.Close() |
||||
|
||||
qm := &QueryMeta{} |
||||
parseQueryMeta(resp, qm) |
||||
qm.RequestTime = rtt |
||||
|
||||
var out map[string][]string |
||||
if err := decodeBody(resp, &out); err != nil { |
||||
return nil, nil, err |
||||
} |
||||
return out, qm, nil |
||||
} |
||||
|
||||
// Service is used to query catalog entries for a given service
|
||||
func (c *Catalog) Service(service, tag string, q *QueryOptions) ([]*CatalogService, *QueryMeta, error) { |
||||
r := c.c.newRequest("GET", "/v1/catalog/service/"+service) |
||||
r.setQueryOptions(q) |
||||
if tag != "" { |
||||
r.params.Set("tag", tag) |
||||
} |
||||
rtt, resp, err := requireOK(c.c.doRequest(r)) |
||||
if err != nil { |
||||
return nil, nil, err |
||||
} |
||||
defer resp.Body.Close() |
||||
|
||||
qm := &QueryMeta{} |
||||
parseQueryMeta(resp, qm) |
||||
qm.RequestTime = rtt |
||||
|
||||
var out []*CatalogService |
||||
if err := decodeBody(resp, &out); err != nil { |
||||
return nil, nil, err |
||||
} |
||||
return out, qm, nil |
||||
} |
||||
|
||||
// Node is used to query for service information about a single node
|
||||
func (c *Catalog) Node(node string, q *QueryOptions) (*CatalogNode, *QueryMeta, error) { |
||||
r := c.c.newRequest("GET", "/v1/catalog/node/"+node) |
||||
r.setQueryOptions(q) |
||||
rtt, resp, err := requireOK(c.c.doRequest(r)) |
||||
if err != nil { |
||||
return nil, nil, err |
||||
} |
||||
defer resp.Body.Close() |
||||
|
||||
qm := &QueryMeta{} |
||||
parseQueryMeta(resp, qm) |
||||
qm.RequestTime = rtt |
||||
|
||||
var out *CatalogNode |
||||
if err := decodeBody(resp, &out); err != nil { |
||||
return nil, nil, err |
||||
} |
||||
return out, qm, nil |
||||
} |
@ -0,0 +1,219 @@
|
||||
package api |
||||
|
||||
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) |
||||
} |
||||
} |
@ -0,0 +1,104 @@
|
||||
package api |
||||
|
||||
import ( |
||||
"bytes" |
||||
"strconv" |
||||
) |
||||
|
||||
// Event can be used to query the Event endpoints
|
||||
type Event struct { |
||||
c *Client |
||||
} |
||||
|
||||
// UserEvent represents an event that was fired by the user
|
||||
type UserEvent struct { |
||||
ID string |
||||
Name string |
||||
Payload []byte |
||||
NodeFilter string |
||||
ServiceFilter string |
||||
TagFilter string |
||||
Version int |
||||
LTime uint64 |
||||
} |
||||
|
||||
// Event returns a handle to the event endpoints
|
||||
func (c *Client) Event() *Event { |
||||
return &Event{c} |
||||
} |
||||
|
||||
// Fire is used to fire a new user event. Only the Name, Payload and Filters
|
||||
// are respected. This returns the ID or an associated error. Cross DC requests
|
||||
// are supported.
|
||||
func (e *Event) Fire(params *UserEvent, q *WriteOptions) (string, *WriteMeta, error) { |
||||
r := e.c.newRequest("PUT", "/v1/event/fire/"+params.Name) |
||||
r.setWriteOptions(q) |
||||
if params.NodeFilter != "" { |
||||
r.params.Set("node", params.NodeFilter) |
||||
} |
||||
if params.ServiceFilter != "" { |
||||
r.params.Set("service", params.ServiceFilter) |
||||
} |
||||
if params.TagFilter != "" { |
||||
r.params.Set("tag", params.TagFilter) |
||||
} |
||||
if params.Payload != nil { |
||||
r.body = bytes.NewReader(params.Payload) |
||||
} |
||||
|
||||
rtt, resp, err := requireOK(e.c.doRequest(r)) |
||||
if err != nil { |
||||
return "", nil, err |
||||
} |
||||
defer resp.Body.Close() |
||||
|
||||
wm := &WriteMeta{RequestTime: rtt} |
||||
var out UserEvent |
||||
if err := decodeBody(resp, &out); err != nil { |
||||
return "", nil, err |
||||
} |
||||
return out.ID, wm, nil |
||||
} |
||||
|
||||
// List is used to get the most recent events an agent has received.
|
||||
// This list can be optionally filtered by the name. This endpoint supports
|
||||
// quasi-blocking queries. The index is not monotonic, nor does it provide provide
|
||||
// LastContact or KnownLeader.
|
||||
func (e *Event) List(name string, q *QueryOptions) ([]*UserEvent, *QueryMeta, error) { |
||||
r := e.c.newRequest("GET", "/v1/event/list") |
||||
r.setQueryOptions(q) |
||||
if name != "" { |
||||
r.params.Set("name", name) |
||||
} |
||||
rtt, resp, err := requireOK(e.c.doRequest(r)) |
||||
if err != nil { |
||||
return nil, nil, err |
||||
} |
||||
defer resp.Body.Close() |
||||
|
||||
qm := &QueryMeta{} |
||||
parseQueryMeta(resp, qm) |
||||
qm.RequestTime = rtt |
||||
|
||||
var entries []*UserEvent |
||||
if err := decodeBody(resp, &entries); err != nil { |
||||
return nil, nil, err |
||||
} |
||||
return entries, qm, nil |
||||
} |
||||
|
||||
// IDToIndex is a bit of a hack. This simulates the index generation to
|
||||
// convert an event ID into a WaitIndex.
|
||||
func (e *Event) IDToIndex(uuid string) uint64 { |
||||
lower := uuid[0:8] + uuid[9:13] + uuid[14:18] |
||||
upper := uuid[19:23] + uuid[24:36] |
||||
lowVal, err := strconv.ParseUint(lower, 16, 64) |
||||
if err != nil { |
||||
panic("Failed to convert " + lower) |
||||
} |
||||
highVal, err := strconv.ParseUint(upper, 16, 64) |
||||
if err != nil { |
||||
panic("Failed to convert " + upper) |
||||
} |
||||
return lowVal ^ highVal |
||||
} |
@ -0,0 +1,37 @@
|
||||
package api |
||||
|
||||
import ( |
||||
"testing" |
||||
) |
||||
|
||||
func TestEvent_FireList(t *testing.T) { |
||||
c := makeClient(t) |
||||
event := c.Event() |
||||
|
||||
params := &UserEvent{Name: "foo"} |
||||
id, meta, err := event.Fire(params, nil) |
||||
if err != nil { |
||||
t.Fatalf("err: %v", err) |
||||
} |
||||
|
||||
if meta.RequestTime == 0 { |
||||
t.Fatalf("bad: %v", meta) |
||||
} |
||||
|
||||
if id == "" { |
||||
t.Fatalf("invalid: %v", id) |
||||
} |
||||
|
||||
events, qm, err := event.List("", nil) |
||||
if err != nil { |
||||
t.Fatalf("err: %v", err) |
||||
} |
||||
|
||||
if qm.LastIndex != event.IDToIndex(id) { |
||||
t.Fatalf("Bad: %#v", qm) |
||||
} |
||||
|
||||
if events[len(events)-1].ID != id { |
||||
t.Fatalf("bad: %#v", events) |
||||
} |
||||
} |
@ -0,0 +1,136 @@
|
||||
package api |
||||
|
||||
import ( |
||||
"fmt" |
||||
) |
||||
|
||||
// HealthCheck is used to represent a single check
|
||||
type HealthCheck struct { |
||||
Node string |
||||
CheckID string |
||||
Name string |
||||
Status string |
||||
Notes string |
||||
Output string |
||||
ServiceID string |
||||
ServiceName string |
||||
} |
||||
|
||||
// ServiceEntry is used for the health service endpoint
|
||||
type ServiceEntry struct { |
||||
Node *Node |
||||
Service *AgentService |
||||
Checks []*HealthCheck |
||||
} |
||||
|
||||
// Health can be used to query the Health endpoints
|
||||
type Health struct { |
||||
c *Client |
||||
} |
||||
|
||||
// Health returns a handle to the health endpoints
|
||||
func (c *Client) Health() *Health { |
||||
return &Health{c} |
||||
} |
||||
|
||||
// Node is used to query for checks belonging to a given node
|
||||
func (h *Health) Node(node string, q *QueryOptions) ([]*HealthCheck, *QueryMeta, error) { |
||||
r := h.c.newRequest("GET", "/v1/health/node/"+node) |
||||
r.setQueryOptions(q) |
||||
rtt, resp, err := requireOK(h.c.doRequest(r)) |
||||
if err != nil { |
||||
return nil, nil, err |
||||
} |
||||
defer resp.Body.Close() |
||||
|
||||
qm := &QueryMeta{} |
||||
parseQueryMeta(resp, qm) |
||||
qm.RequestTime = rtt |
||||
|
||||
var out []*HealthCheck |
||||
if err := decodeBody(resp, &out); err != nil { |
||||
return nil, nil, err |
||||
} |
||||
return out, qm, nil |
||||
} |
||||
|
||||
// Checks is used to return the checks associated with a service
|
||||
func (h *Health) Checks(service string, q *QueryOptions) ([]*HealthCheck, *QueryMeta, error) { |
||||
r := h.c.newRequest("GET", "/v1/health/checks/"+service) |
||||
r.setQueryOptions(q) |
||||
rtt, resp, err := requireOK(h.c.doRequest(r)) |
||||
if err != nil { |
||||
return nil, nil, err |
||||
} |
||||
defer resp.Body.Close() |
||||
|
||||
qm := &QueryMeta{} |
||||
parseQueryMeta(resp, qm) |
||||
qm.RequestTime = rtt |
||||
|
||||
var out []*HealthCheck |
||||
if err := decodeBody(resp, &out); err != nil { |
||||
return nil, nil, err |
||||
} |
||||
return out, qm, nil |
||||
} |
||||
|
||||
// Service is used to query health information along with service info
|
||||
// for a given service. It can optionally do server-side filtering on a tag
|
||||
// or nodes with passing health checks only.
|
||||
func (h *Health) Service(service, tag string, passingOnly bool, q *QueryOptions) ([]*ServiceEntry, *QueryMeta, error) { |
||||
r := h.c.newRequest("GET", "/v1/health/service/"+service) |
||||
r.setQueryOptions(q) |
||||
if tag != "" { |
||||
r.params.Set("tag", tag) |
||||
} |
||||
if passingOnly { |
||||
r.params.Set("passing", "1") |
||||
} |
||||
rtt, resp, err := requireOK(h.c.doRequest(r)) |
||||
if err != nil { |
||||
return nil, nil, err |
||||
} |
||||
defer resp.Body.Close() |
||||
|
||||
qm := &QueryMeta{} |
||||
parseQueryMeta(resp, qm) |
||||
qm.RequestTime = rtt |
||||
|
||||
var out []*ServiceEntry |
||||
if err := decodeBody(resp, &out); err != nil { |
||||
return nil, nil, err |
||||
} |
||||
return out, qm, nil |
||||
} |
||||
|
||||
// State is used to retreive all the checks in a given state.
|
||||
// The wildcard "any" state can also be used for all checks.
|
||||
func (h *Health) State(state string, q *QueryOptions) ([]*HealthCheck, *QueryMeta, error) { |
||||
switch state { |
||||
case "any": |
||||
case "warning": |
||||
case "critical": |
||||
case "passing": |
||||
case "unknown": |
||||
default: |
||||
return nil, nil, fmt.Errorf("Unsupported state: %v", state) |
||||
} |
||||
r := h.c.newRequest("GET", "/v1/health/state/"+state) |
||||
r.setQueryOptions(q) |
||||
rtt, resp, err := requireOK(h.c.doRequest(r)) |
||||
if err != nil { |
||||
return nil, nil, err |
||||
} |
||||
defer resp.Body.Close() |
||||
|
||||
qm := &QueryMeta{} |
||||
parseQueryMeta(resp, qm) |
||||
qm.RequestTime = rtt |
||||
|
||||
var out []*HealthCheck |
||||
if err := decodeBody(resp, &out); err != nil { |
||||
return nil, nil, err |
||||
} |
||||
return out, qm, nil |
||||
} |
@ -0,0 +1,98 @@
|
||||
package api |
||||
|
||||
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,219 @@
|
||||
package api |
||||
|
||||
import ( |
||||
"bytes" |
||||
"fmt" |
||||
"io" |
||||
"net/http" |
||||
"strconv" |
||||
"strings" |
||||
) |
||||
|
||||
// KVPair is used to represent a single K/V entry
|
||||
type KVPair struct { |
||||
Key string |
||||
CreateIndex uint64 |
||||
ModifyIndex uint64 |
||||
LockIndex uint64 |
||||
Flags uint64 |
||||
Value []byte |
||||
Session string |
||||
} |
||||
|
||||
// KVPairs is a list of KVPair objects
|
||||
type KVPairs []*KVPair |
||||
|
||||
// KV is used to manipulate the K/V API
|
||||
type KV struct { |
||||
c *Client |
||||
} |
||||
|
||||
// KV is used to return a handle to the K/V apis
|
||||
func (c *Client) KV() *KV { |
||||
return &KV{c} |
||||
} |
||||
|
||||
// Get is used to lookup a single key
|
||||
func (k *KV) Get(key string, q *QueryOptions) (*KVPair, *QueryMeta, error) { |
||||
resp, qm, err := k.getInternal(key, nil, q) |
||||
if err != nil { |
||||
return nil, nil, err |
||||
} |
||||
if resp == nil { |
||||
return nil, qm, nil |
||||
} |
||||
defer resp.Body.Close() |
||||
|
||||
var entries []*KVPair |
||||
if err := decodeBody(resp, &entries); err != nil { |
||||
return nil, nil, err |
||||
} |
||||
if len(entries) > 0 { |
||||
return entries[0], qm, nil |
||||
} |
||||
return nil, qm, nil |
||||
} |
||||
|
||||
// List is used to lookup all keys under a prefix
|
||||
func (k *KV) List(prefix string, q *QueryOptions) (KVPairs, *QueryMeta, error) { |
||||
resp, qm, err := k.getInternal(prefix, map[string]string{"recurse": ""}, q) |
||||
if err != nil { |
||||
return nil, nil, err |
||||
} |
||||
if resp == nil { |
||||
return nil, qm, nil |
||||
} |
||||
defer resp.Body.Close() |
||||
|
||||
var entries []*KVPair |
||||
if err := decodeBody(resp, &entries); err != nil { |
||||
return nil, nil, err |
||||
} |
||||
return entries, qm, nil |
||||
} |
||||
|
||||
// Keys is used to list all the keys under a prefix. Optionally,
|
||||
// a separator can be used to limit the responses.
|
||||
func (k *KV) Keys(prefix, separator string, q *QueryOptions) ([]string, *QueryMeta, error) { |
||||
params := map[string]string{"keys": ""} |
||||
if separator != "" { |
||||
params["separator"] = separator |
||||
} |
||||
resp, qm, err := k.getInternal(prefix, params, q) |
||||
if err != nil { |
||||
return nil, nil, err |
||||
} |
||||
if resp == nil { |
||||
return nil, qm, nil |
||||
} |
||||
defer resp.Body.Close() |
||||
|
||||
var entries []string |
||||
if err := decodeBody(resp, &entries); err != nil { |
||||
return nil, nil, err |
||||
} |
||||
return entries, qm, nil |
||||
} |
||||
|
||||
func (k *KV) getInternal(key string, params map[string]string, q *QueryOptions) (*http.Response, *QueryMeta, error) { |
||||
r := k.c.newRequest("GET", "/v1/kv/"+key) |
||||
r.setQueryOptions(q) |
||||
for param, val := range params { |
||||
r.params.Set(param, val) |
||||
} |
||||
rtt, resp, err := k.c.doRequest(r) |
||||
if err != nil { |
||||
return nil, nil, err |
||||
} |
||||
|
||||
qm := &QueryMeta{} |
||||
parseQueryMeta(resp, qm) |
||||
qm.RequestTime = rtt |
||||
|
||||
if resp.StatusCode == 404 { |
||||
resp.Body.Close() |
||||
return nil, qm, nil |
||||
} else if resp.StatusCode != 200 { |
||||
resp.Body.Close() |
||||
return nil, nil, fmt.Errorf("Unexpected response code: %d", resp.StatusCode) |
||||
} |
||||
return resp, qm, nil |
||||
} |
||||
|
||||
// Put is used to write a new value. Only the
|
||||
// Key, Flags and Value is respected.
|
||||
func (k *KV) Put(p *KVPair, q *WriteOptions) (*WriteMeta, error) { |
||||
params := make(map[string]string, 1) |
||||
if p.Flags != 0 { |
||||
params["flags"] = strconv.FormatUint(p.Flags, 10) |
||||
} |
||||
_, wm, err := k.put(p.Key, params, p.Value, q) |
||||
return wm, err |
||||
} |
||||
|
||||
// CAS is used for a Check-And-Set operation. The Key,
|
||||
// ModifyIndex, Flags and Value are respected. Returns true
|
||||
// on success or false on failures.
|
||||
func (k *KV) CAS(p *KVPair, q *WriteOptions) (bool, *WriteMeta, error) { |
||||
params := make(map[string]string, 2) |
||||
if p.Flags != 0 { |
||||
params["flags"] = strconv.FormatUint(p.Flags, 10) |
||||
} |
||||
params["cas"] = strconv.FormatUint(p.ModifyIndex, 10) |
||||
return k.put(p.Key, params, p.Value, q) |
||||
} |
||||
|
||||
// Acquire is used for a lock acquisiiton operation. The Key,
|
||||
// Flags, Value and Session are respected. Returns true
|
||||
// on success or false on failures.
|
||||
func (k *KV) Acquire(p *KVPair, q *WriteOptions) (bool, *WriteMeta, error) { |
||||
params := make(map[string]string, 2) |
||||
if p.Flags != 0 { |
||||
params["flags"] = strconv.FormatUint(p.Flags, 10) |
||||
} |
||||
params["acquire"] = p.Session |
||||
return k.put(p.Key, params, p.Value, q) |
||||
} |
||||
|
||||
// Release is used for a lock release operation. The Key,
|
||||
// Flags, Value and Session are respected. Returns true
|
||||
// on success or false on failures.
|
||||
func (k *KV) Release(p *KVPair, q *WriteOptions) (bool, *WriteMeta, error) { |
||||
params := make(map[string]string, 2) |
||||
if p.Flags != 0 { |
||||
params["flags"] = strconv.FormatUint(p.Flags, 10) |
||||
} |
||||
params["release"] = p.Session |
||||
return k.put(p.Key, params, p.Value, q) |
||||
} |
||||
|
||||
func (k *KV) put(key string, params map[string]string, body []byte, q *WriteOptions) (bool, *WriteMeta, error) { |
||||
r := k.c.newRequest("PUT", "/v1/kv/"+key) |
||||
r.setWriteOptions(q) |
||||
for param, val := range params { |
||||
r.params.Set(param, val) |
||||
} |
||||
r.body = bytes.NewReader(body) |
||||
rtt, resp, err := requireOK(k.c.doRequest(r)) |
||||
if err != nil { |
||||
return false, nil, err |
||||
} |
||||
defer resp.Body.Close() |
||||
|
||||
qm := &WriteMeta{} |
||||
qm.RequestTime = rtt |
||||
|
||||
var buf bytes.Buffer |
||||
if _, err := io.Copy(&buf, resp.Body); err != nil { |
||||
return false, nil, fmt.Errorf("Failed to read response: %v", err) |
||||
} |
||||
res := strings.Contains(string(buf.Bytes()), "true") |
||||
return res, qm, nil |
||||
} |
||||
|
||||
// Delete is used to delete a single key
|
||||
func (k *KV) Delete(key string, w *WriteOptions) (*WriteMeta, error) { |
||||
return k.deleteInternal(key, nil, w) |
||||
} |
||||
|
||||
// DeleteTree is used to delete all keys under a prefix
|
||||
func (k *KV) DeleteTree(prefix string, w *WriteOptions) (*WriteMeta, error) { |
||||
return k.deleteInternal(prefix, []string{"recurse"}, w) |
||||
} |
||||
|
||||
func (k *KV) deleteInternal(key string, params []string, q *WriteOptions) (*WriteMeta, error) { |
||||
r := k.c.newRequest("DELETE", "/v1/kv/"+key) |
||||
r.setWriteOptions(q) |
||||
for _, param := range params { |
||||
r.params.Set(param, "") |
||||
} |
||||
rtt, resp, err := requireOK(k.c.doRequest(r)) |
||||
if err != nil { |
||||
return nil, err |
||||
} |
||||
resp.Body.Close() |
||||
|
||||
qm := &WriteMeta{} |
||||
qm.RequestTime = rtt |
||||
return qm, nil |
||||
} |
@ -0,0 +1,374 @@
|
||||
package api |
||||
|
||||
import ( |
||||
"bytes" |
||||
"path" |
||||
"testing" |
||||
"time" |
||||
) |
||||
|
||||
func TestClientPutGetDelete(t *testing.T) { |
||||
c := makeClient(t) |
||||
kv := c.KV() |
||||
|
||||
// Get a get without a key
|
||||
key := testKey() |
||||
pair, _, err := kv.Get(key, nil) |
||||
if err != nil { |
||||
t.Fatalf("err: %v", err) |
||||
} |
||||
if pair != nil { |
||||
t.Fatalf("unexpected value: %#v", pair) |
||||
} |
||||
|
||||
// Put the key
|
||||
value := []byte("test") |
||||
p := &KVPair{Key: key, Flags: 42, Value: value} |
||||
if _, err := kv.Put(p, nil); err != nil { |
||||
t.Fatalf("err: %v", err) |
||||
} |
||||
|
||||
// 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 !bytes.Equal(pair.Value, value) { |
||||
t.Fatalf("unexpected value: %#v", pair) |
||||
} |
||||
if pair.Flags != 42 { |
||||
t.Fatalf("unexpected value: %#v", pair) |
||||
} |
||||
if meta.LastIndex == 0 { |
||||
t.Fatalf("unexpected value: %#v", meta) |
||||
} |
||||
|
||||
// Delete
|
||||
if _, err := kv.Delete(key, nil); err != nil { |
||||
t.Fatalf("err: %v", err) |
||||
} |
||||
|
||||
// Get should fail
|
||||
pair, _, err = kv.Get(key, nil) |
||||
if err != nil { |
||||
t.Fatalf("err: %v", err) |
||||
} |
||||
if pair != nil { |
||||
t.Fatalf("unexpected value: %#v", pair) |
||||
} |
||||
} |
||||
|
||||
func TestClient_List_DeleteRecurse(t *testing.T) { |
||||
c := makeClient(t) |
||||
kv := c.KV() |
||||
|
||||
// Generate some test keys
|
||||
prefix := testKey() |
||||
var keys []string |
||||
for i := 0; i < 100; i++ { |
||||
keys = append(keys, path.Join(prefix, testKey())) |
||||
} |
||||
|
||||
// Set values
|
||||
value := []byte("test") |
||||
for _, key := range keys { |
||||
p := &KVPair{Key: key, Value: value} |
||||
if _, err := kv.Put(p, nil); err != nil { |
||||
t.Fatalf("err: %v", err) |
||||
} |
||||
} |
||||
|
||||
// List the values
|
||||
pairs, meta, err := kv.List(prefix, nil) |
||||
if err != nil { |
||||
t.Fatalf("err: %v", err) |
||||
} |
||||
if len(pairs) != len(keys) { |
||||
t.Fatalf("got %d keys", len(pairs)) |
||||
} |
||||
for _, pair := range pairs { |
||||
if !bytes.Equal(pair.Value, value) { |
||||
t.Fatalf("unexpected value: %#v", pair) |
||||
} |
||||
} |
||||
if meta.LastIndex == 0 { |
||||
t.Fatalf("unexpected value: %#v", meta) |
||||
} |
||||
|
||||
// Delete all
|
||||
if _, err := kv.DeleteTree(prefix, nil); err != nil { |
||||
t.Fatalf("err: %v", err) |
||||
} |
||||
|
||||
// List the values
|
||||
pairs, _, err = kv.List(prefix, nil) |
||||
if err != nil { |
||||
t.Fatalf("err: %v", err) |
||||
} |
||||
if len(pairs) != 0 { |
||||
t.Fatalf("got %d keys", len(pairs)) |
||||
} |
||||
} |
||||
|
||||
func TestClient_CAS(t *testing.T) { |
||||
c := makeClient(t) |
||||
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
|
||||
newVal := []byte("foo") |
||||
p.Value = newVal |
||||
p.ModifyIndex = 1 |
||||
if work, _, err := kv.CAS(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.CAS(p, nil); err != nil { |
||||
t.Fatalf("err: %v", err) |
||||
} else if !work { |
||||
t.Fatalf("unexpected CAS failure") |
||||
} |
||||
} |
||||
|
||||
func TestClient_WatchGet(t *testing.T) { |
||||
c := makeClient(t) |
||||
kv := c.KV() |
||||
|
||||
// Get a get without a key
|
||||
key := testKey() |
||||
pair, meta, err := kv.Get(key, nil) |
||||
if err != nil { |
||||
t.Fatalf("err: %v", err) |
||||
} |
||||
if pair != nil { |
||||
t.Fatalf("unexpected value: %#v", pair) |
||||
} |
||||
if meta.LastIndex == 0 { |
||||
t.Fatalf("unexpected value: %#v", meta) |
||||
} |
||||
|
||||
// Put the key
|
||||
value := []byte("test") |
||||
go func() { |
||||
c := makeClient(t) |
||||
kv := c.KV() |
||||
|
||||
time.Sleep(100 * time.Millisecond) |
||||
p := &KVPair{Key: key, Flags: 42, Value: value} |
||||
if _, err := kv.Put(p, nil); err != nil { |
||||
t.Fatalf("err: %v", err) |
||||
} |
||||
}() |
||||
|
||||
// Get should work
|
||||
options := &QueryOptions{WaitIndex: meta.LastIndex} |
||||
pair, meta2, err := kv.Get(key, options) |
||||
if err != nil { |
||||
t.Fatalf("err: %v", err) |
||||
} |
||||
if pair == nil { |
||||
t.Fatalf("expected value: %#v", pair) |
||||
} |
||||
if !bytes.Equal(pair.Value, value) { |
||||
t.Fatalf("unexpected value: %#v", pair) |
||||
} |
||||
if pair.Flags != 42 { |
||||
t.Fatalf("unexpected value: %#v", pair) |
||||
} |
||||
if meta2.LastIndex <= meta.LastIndex { |
||||
t.Fatalf("unexpected value: %#v", meta2) |
||||
} |
||||
} |
||||
|
||||
func TestClient_WatchList(t *testing.T) { |
||||
c := makeClient(t) |
||||
kv := c.KV() |
||||
|
||||
// Get a get without a key
|
||||
prefix := testKey() |
||||
key := path.Join(prefix, testKey()) |
||||
pairs, meta, err := kv.List(prefix, nil) |
||||
if err != nil { |
||||
t.Fatalf("err: %v", err) |
||||
} |
||||
if len(pairs) != 0 { |
||||
t.Fatalf("unexpected value: %#v", pairs) |
||||
} |
||||
if meta.LastIndex == 0 { |
||||
t.Fatalf("unexpected value: %#v", meta) |
||||
} |
||||
|
||||
// Put the key
|
||||
value := []byte("test") |
||||
go func() { |
||||
c := makeClient(t) |
||||
kv := c.KV() |
||||
|
||||
time.Sleep(100 * time.Millisecond) |
||||
p := &KVPair{Key: key, Flags: 42, Value: value} |
||||
if _, err := kv.Put(p, nil); err != nil { |
||||
t.Fatalf("err: %v", err) |
||||
} |
||||
}() |
||||
|
||||
// Get should work
|
||||
options := &QueryOptions{WaitIndex: meta.LastIndex} |
||||
pairs, meta2, err := kv.List(prefix, options) |
||||
if err != nil { |
||||
t.Fatalf("err: %v", err) |
||||
} |
||||
if len(pairs) != 1 { |
||||
t.Fatalf("expected value: %#v", pairs) |
||||
} |
||||
if !bytes.Equal(pairs[0].Value, value) { |
||||
t.Fatalf("unexpected value: %#v", pairs) |
||||
} |
||||
if pairs[0].Flags != 42 { |
||||
t.Fatalf("unexpected value: %#v", pairs) |
||||
} |
||||
if meta2.LastIndex <= meta.LastIndex { |
||||
t.Fatalf("unexpected value: %#v", meta2) |
||||
} |
||||
|
||||
} |
||||
|
||||
func TestClient_Keys_DeleteRecurse(t *testing.T) { |
||||
c := makeClient(t) |
||||
kv := c.KV() |
||||
|
||||
// Generate some test keys
|
||||
prefix := testKey() |
||||
var keys []string |
||||
for i := 0; i < 100; i++ { |
||||
keys = append(keys, path.Join(prefix, testKey())) |
||||
} |
||||
|
||||
// Set values
|
||||
value := []byte("test") |
||||
for _, key := range keys { |
||||
p := &KVPair{Key: key, Value: value} |
||||
if _, err := kv.Put(p, nil); err != nil { |
||||
t.Fatalf("err: %v", err) |
||||
} |
||||
} |
||||
|
||||
// List the values
|
||||
out, meta, err := kv.Keys(prefix, "", nil) |
||||
if err != nil { |
||||
t.Fatalf("err: %v", err) |
||||
} |
||||
if len(out) != len(keys) { |
||||
t.Fatalf("got %d keys", len(out)) |
||||
} |
||||
if meta.LastIndex == 0 { |
||||
t.Fatalf("unexpected value: %#v", meta) |
||||
} |
||||
|
||||
// Delete all
|
||||
if _, err := kv.DeleteTree(prefix, nil); err != nil { |
||||
t.Fatalf("err: %v", err) |
||||
} |
||||
|
||||
// List the values
|
||||
out, _, err = kv.Keys(prefix, "", nil) |
||||
if err != nil { |
||||
t.Fatalf("err: %v", err) |
||||
} |
||||
if len(out) != 0 { |
||||
t.Fatalf("got %d keys", len(out)) |
||||
} |
||||
} |
||||
|
||||
func TestClient_AcquireRelease(t *testing.T) { |
||||
c := makeClient(t) |
||||
session := c.Session() |
||||
kv := c.KV() |
||||
|
||||
// Make a session
|
||||
id, _, err := session.CreateNoChecks(nil, nil) |
||||
if err != nil { |
||||
t.Fatalf("err: %v", err) |
||||
} |
||||
defer session.Destroy(id, nil) |
||||
|
||||
// Acquire the key
|
||||
key := testKey() |
||||
value := []byte("test") |
||||
p := &KVPair{Key: key, Value: value, Session: id} |
||||
if work, _, err := kv.Acquire(p, nil); err != nil { |
||||
t.Fatalf("err: %v", err) |
||||
} else if !work { |
||||
t.Fatalf("Lock 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 pair.LockIndex != 1 { |
||||
t.Fatalf("Expected lock: %v", pair) |
||||
} |
||||
if pair.Session != id { |
||||
t.Fatalf("Expected lock: %v", pair) |
||||
} |
||||
if meta.LastIndex == 0 { |
||||
t.Fatalf("unexpected value: %#v", meta) |
||||
} |
||||
|
||||
// Release
|
||||
if work, _, err := kv.Release(p, nil); err != nil { |
||||
t.Fatalf("err: %v", err) |
||||
} else if !work { |
||||
t.Fatalf("Release fail") |
||||
} |
||||
|
||||
// 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 pair.LockIndex != 1 { |
||||
t.Fatalf("Expected lock: %v", pair) |
||||
} |
||||
if pair.Session != "" { |
||||
t.Fatalf("Expected unlock: %v", pair) |
||||
} |
||||
if meta.LastIndex == 0 { |
||||
t.Fatalf("unexpected value: %#v", meta) |
||||
} |
||||
} |
@ -0,0 +1,204 @@
|
||||
package api |
||||
|
||||
import ( |
||||
"time" |
||||
) |
||||
|
||||
// SessionEntry represents a session in consul
|
||||
type SessionEntry struct { |
||||
CreateIndex uint64 |
||||
ID string |
||||
Name string |
||||
Node string |
||||
Checks []string |
||||
LockDelay time.Duration |
||||
Behavior string |
||||
TTL string |
||||
} |
||||
|
||||
// Session can be used to query the Session endpoints
|
||||
type Session struct { |
||||
c *Client |
||||
} |
||||
|
||||
// Session returns a handle to the session endpoints
|
||||
func (c *Client) Session() *Session { |
||||
return &Session{c} |
||||
} |
||||
|
||||
// CreateNoChecks is like Create but is used specifically to create
|
||||
// a session with no associated health checks.
|
||||
func (s *Session) CreateNoChecks(se *SessionEntry, q *WriteOptions) (string, *WriteMeta, error) { |
||||
body := make(map[string]interface{}) |
||||
body["Checks"] = []string{} |
||||
if se != nil { |
||||
if se.Name != "" { |
||||
body["Name"] = se.Name |
||||
} |
||||
if se.Node != "" { |
||||
body["Node"] = se.Node |
||||
} |
||||
if se.LockDelay != 0 { |
||||
body["LockDelay"] = durToMsec(se.LockDelay) |
||||
} |
||||
if se.Behavior != "" { |
||||
body["Behavior"] = se.Behavior |
||||
} |
||||
if se.TTL != "" { |
||||
body["TTL"] = se.TTL |
||||
} |
||||
} |
||||
return s.create(body, q) |
||||
|
||||
} |
||||
|
||||
// Create makes a new session. Providing a session entry can
|
||||
// customize the session. It can also be nil to use defaults.
|
||||
func (s *Session) Create(se *SessionEntry, q *WriteOptions) (string, *WriteMeta, error) { |
||||
var obj interface{} |
||||
if se != nil { |
||||
body := make(map[string]interface{}) |
||||
obj = body |
||||
if se.Name != "" { |
||||
body["Name"] = se.Name |
||||
} |
||||
if se.Node != "" { |
||||
body["Node"] = se.Node |
||||
} |
||||
if se.LockDelay != 0 { |
||||
body["LockDelay"] = durToMsec(se.LockDelay) |
||||
} |
||||
if len(se.Checks) > 0 { |
||||
body["Checks"] = se.Checks |
||||
} |
||||
if se.Behavior != "" { |
||||
body["Behavior"] = se.Behavior |
||||
} |
||||
if se.TTL != "" { |
||||
body["TTL"] = se.TTL |
||||
} |
||||
} |
||||
return s.create(obj, q) |
||||
} |
||||
|
||||
func (s *Session) create(obj interface{}, q *WriteOptions) (string, *WriteMeta, error) { |
||||
r := s.c.newRequest("PUT", "/v1/session/create") |
||||
r.setWriteOptions(q) |
||||
r.obj = obj |
||||
rtt, resp, err := requireOK(s.c.doRequest(r)) |
||||
if err != nil { |
||||
return "", nil, err |
||||
} |
||||
defer resp.Body.Close() |
||||
|
||||
wm := &WriteMeta{RequestTime: rtt} |
||||
var out struct{ ID string } |
||||
if err := decodeBody(resp, &out); err != nil { |
||||
return "", nil, err |
||||
} |
||||
return out.ID, wm, nil |
||||
} |
||||
|
||||
// Destroy invalides a given session
|
||||
func (s *Session) Destroy(id string, q *WriteOptions) (*WriteMeta, error) { |
||||
r := s.c.newRequest("PUT", "/v1/session/destroy/"+id) |
||||
r.setWriteOptions(q) |
||||
rtt, resp, err := requireOK(s.c.doRequest(r)) |
||||
if err != nil { |
||||
return nil, err |
||||
} |
||||
resp.Body.Close() |
||||
|
||||
wm := &WriteMeta{RequestTime: rtt} |
||||
return wm, nil |
||||
} |
||||
|
||||
// Renew renews the TTL on a given session
|
||||
func (s *Session) Renew(id string, q *WriteOptions) (*SessionEntry, *WriteMeta, error) { |
||||
r := s.c.newRequest("PUT", "/v1/session/renew/"+id) |
||||
r.setWriteOptions(q) |
||||
rtt, resp, err := requireOK(s.c.doRequest(r)) |
||||
if err != nil { |
||||
return nil, nil, err |
||||
} |
||||
defer resp.Body.Close() |
||||
|
||||
wm := &WriteMeta{RequestTime: rtt} |
||||
|
||||
var entries []*SessionEntry |
||||
if err := decodeBody(resp, &entries); err != nil { |
||||
return nil, wm, err |
||||
} |
||||
|
||||
if len(entries) > 0 { |
||||
return entries[0], wm, nil |
||||
} |
||||
return nil, wm, nil |
||||
} |
||||
|
||||
// Info looks up a single session
|
||||
func (s *Session) Info(id string, q *QueryOptions) (*SessionEntry, *QueryMeta, error) { |
||||
r := s.c.newRequest("GET", "/v1/session/info/"+id) |
||||
r.setQueryOptions(q) |
||||
rtt, resp, err := requireOK(s.c.doRequest(r)) |
||||
if err != nil { |
||||
return nil, nil, err |
||||
} |
||||
defer resp.Body.Close() |
||||
|
||||
qm := &QueryMeta{} |
||||
parseQueryMeta(resp, qm) |
||||
qm.RequestTime = rtt |
||||
|
||||
var entries []*SessionEntry |
||||
if err := decodeBody(resp, &entries); err != nil { |
||||
return nil, nil, err |
||||
} |
||||
|
||||
if len(entries) > 0 { |
||||
return entries[0], qm, nil |
||||
} |
||||
return nil, qm, nil |
||||
} |
||||
|
||||
// List gets sessions for a node
|
||||
func (s *Session) Node(node string, q *QueryOptions) ([]*SessionEntry, *QueryMeta, error) { |
||||
r := s.c.newRequest("GET", "/v1/session/node/"+node) |
||||
r.setQueryOptions(q) |
||||
rtt, resp, err := requireOK(s.c.doRequest(r)) |
||||
if err != nil { |
||||
return nil, nil, err |
||||
} |
||||
defer resp.Body.Close() |
||||
|
||||
qm := &QueryMeta{} |
||||
parseQueryMeta(resp, qm) |
||||
qm.RequestTime = rtt |
||||
|
||||
var entries []*SessionEntry |
||||
if err := decodeBody(resp, &entries); err != nil { |
||||
return nil, nil, err |
||||
} |
||||
return entries, qm, nil |
||||
} |
||||
|
||||
// List gets all active sessions
|
||||
func (s *Session) List(q *QueryOptions) ([]*SessionEntry, *QueryMeta, error) { |
||||
r := s.c.newRequest("GET", "/v1/session/list") |
||||
r.setQueryOptions(q) |
||||
rtt, resp, err := requireOK(s.c.doRequest(r)) |
||||
if err != nil { |
||||
return nil, nil, err |
||||
} |
||||
defer resp.Body.Close() |
||||
|
||||
qm := &QueryMeta{} |
||||
parseQueryMeta(resp, qm) |
||||
qm.RequestTime = rtt |
||||
|
||||
var entries []*SessionEntry |
||||
if err := decodeBody(resp, &entries); err != nil { |
||||
return nil, nil, err |
||||
} |
||||
return entries, qm, nil |
||||
} |
@ -0,0 +1,190 @@
|
||||
package api |
||||
|
||||
import ( |
||||
"testing" |
||||
) |
||||
|
||||
func TestSession_CreateDestroy(t *testing.T) { |
||||
c := makeClient(t) |
||||
session := c.Session() |
||||
|
||||
id, meta, err := session.Create(nil, nil) |
||||
if err != nil { |
||||
t.Fatalf("err: %v", err) |
||||
} |
||||
|
||||
if meta.RequestTime == 0 { |
||||
t.Fatalf("bad: %v", meta) |
||||
} |
||||
|
||||
if id == "" { |
||||
t.Fatalf("invalid: %v", id) |
||||
} |
||||
|
||||
meta, err = session.Destroy(id, nil) |
||||
if err != nil { |
||||
t.Fatalf("err: %v", err) |
||||
} |
||||
|
||||
if meta.RequestTime == 0 { |
||||
t.Fatalf("bad: %v", meta) |
||||
} |
||||
} |
||||
|
||||
func TestSession_CreateRenewDestroy(t *testing.T) { |
||||
c := makeClient(t) |
||||
session := c.Session() |
||||
|
||||
se := &SessionEntry{ |
||||
TTL: "10s", |
||||
} |
||||
|
||||
id, meta, err := session.Create(se, nil) |
||||
if err != nil { |
||||
t.Fatalf("err: %v", err) |
||||
} |
||||
defer session.Destroy(id, nil) |
||||
|
||||
if meta.RequestTime == 0 { |
||||
t.Fatalf("bad: %v", meta) |
||||
} |
||||
|
||||
if id == "" { |
||||
t.Fatalf("invalid: %v", id) |
||||
} |
||||
|
||||
if meta.RequestTime == 0 { |
||||
t.Fatalf("bad: %v", meta) |
||||
} |
||||
|
||||
renew, meta, err := session.Renew(id, nil) |
||||
|
||||
if err != nil { |
||||
t.Fatalf("err: %v", err) |
||||
} |
||||
if meta.RequestTime == 0 { |
||||
t.Fatalf("bad: %v", meta) |
||||
} |
||||
|
||||
if renew == nil { |
||||
t.Fatalf("should get session") |
||||
} |
||||
|
||||
if renew.ID != id { |
||||
t.Fatalf("should have matching id") |
||||
} |
||||
|
||||
if renew.TTL != "10s" { |
||||
t.Fatalf("should get session with TTL") |
||||
} |
||||
} |
||||
|
||||
func TestSession_Info(t *testing.T) { |
||||
c := makeClient(t) |
||||
session := c.Session() |
||||
|
||||
id, _, err := session.Create(nil, nil) |
||||
if err != nil { |
||||
t.Fatalf("err: %v", err) |
||||
} |
||||
defer session.Destroy(id, nil) |
||||
|
||||
info, qm, err := session.Info(id, nil) |
||||
if err != nil { |
||||
t.Fatalf("err: %v", err) |
||||
} |
||||
|
||||
if qm.LastIndex == 0 { |
||||
t.Fatalf("bad: %v", qm) |
||||
} |
||||
if !qm.KnownLeader { |
||||
t.Fatalf("bad: %v", qm) |
||||
} |
||||
|
||||
if info == nil { |
||||
t.Fatalf("should get session") |
||||
} |
||||
if info.CreateIndex == 0 { |
||||
t.Fatalf("bad: %v", info) |
||||
} |
||||
if info.ID != id { |
||||
t.Fatalf("bad: %v", info) |
||||
} |
||||
if info.Name != "" { |
||||
t.Fatalf("bad: %v", info) |
||||
} |
||||
if info.Node == "" { |
||||
t.Fatalf("bad: %v", info) |
||||
} |
||||
if len(info.Checks) == 0 { |
||||
t.Fatalf("bad: %v", info) |
||||
} |
||||
if info.LockDelay == 0 { |
||||
t.Fatalf("bad: %v", info) |
||||
} |
||||
if info.Behavior != "release" { |
||||
t.Fatalf("bad: %v", info) |
||||
} |
||||
if info.TTL != "" { |
||||
t.Fatalf("bad: %v", info) |
||||
} |
||||
} |
||||
|
||||
func TestSession_Node(t *testing.T) { |
||||
c := makeClient(t) |
||||
session := c.Session() |
||||
|
||||
id, _, err := session.Create(nil, nil) |
||||
if err != nil { |
||||
t.Fatalf("err: %v", err) |
||||
} |
||||
defer session.Destroy(id, nil) |
||||
|
||||
info, qm, err := session.Info(id, nil) |
||||
if err != nil { |
||||
t.Fatalf("err: %v", err) |
||||
} |
||||
|
||||
sessions, qm, err := session.Node(info.Node, nil) |
||||
if err != nil { |
||||
t.Fatalf("err: %v", err) |
||||
} |
||||
|
||||
if len(sessions) != 1 { |
||||
t.Fatalf("bad: %v", sessions) |
||||
} |
||||
|
||||
if qm.LastIndex == 0 { |
||||
t.Fatalf("bad: %v", qm) |
||||
} |
||||
if !qm.KnownLeader { |
||||
t.Fatalf("bad: %v", qm) |
||||
} |
||||
} |
||||
|
||||
func TestSession_List(t *testing.T) { |
||||
c := makeClient(t) |
||||
session := c.Session() |
||||
|
||||
id, _, err := session.Create(nil, nil) |
||||
if err != nil { |
||||
t.Fatalf("err: %v", err) |
||||
} |
||||
defer session.Destroy(id, nil) |
||||
|
||||
sessions, qm, err := session.List(nil) |
||||
if err != nil { |
||||
t.Fatalf("err: %v", err) |
||||
} |
||||
|
||||
if len(sessions) != 1 { |
||||
t.Fatalf("bad: %v", sessions) |
||||
} |
||||
|
||||
if qm.LastIndex == 0 { |
||||
t.Fatalf("bad: %v", qm) |
||||
} |
||||
if !qm.KnownLeader { |
||||
t.Fatalf("bad: %v", qm) |
||||
} |
||||
} |
@ -0,0 +1,43 @@
|
||||
package api |
||||
|
||||
// Status can be used to query the Status endpoints
|
||||
type Status struct { |
||||
c *Client |
||||
} |
||||
|
||||
// Status returns a handle to the status endpoints
|
||||
func (c *Client) Status() *Status { |
||||
return &Status{c} |
||||
} |
||||
|
||||
// Leader is used to query for a known leader
|
||||
func (s *Status) Leader() (string, error) { |
||||
r := s.c.newRequest("GET", "/v1/status/leader") |
||||
_, resp, err := requireOK(s.c.doRequest(r)) |
||||
if err != nil { |
||||
return "", err |
||||
} |
||||
defer resp.Body.Close() |
||||
|
||||
var leader string |
||||
if err := decodeBody(resp, &leader); err != nil { |
||||
return "", err |
||||
} |
||||
return leader, nil |
||||
} |
||||
|
||||
// Peers is used to query for a known raft peers
|
||||
func (s *Status) Peers() ([]string, error) { |
||||
r := s.c.newRequest("GET", "/v1/status/peers") |
||||
_, resp, err := requireOK(s.c.doRequest(r)) |
||||
if err != nil { |
||||
return nil, err |
||||
} |
||||
defer resp.Body.Close() |
||||
|
||||
var peers []string |
||||
if err := decodeBody(resp, &peers); err != nil { |
||||
return nil, err |
||||
} |
||||
return peers, nil |
||||
} |
@ -0,0 +1,31 @@
|
||||
package api |
||||
|
||||
import ( |
||||
"testing" |
||||
) |
||||
|
||||
func TestStatusLeader(t *testing.T) { |
||||
c := makeClient(t) |
||||
status := c.Status() |
||||
|
||||
leader, err := status.Leader() |
||||
if err != nil { |
||||
t.Fatalf("err: %v", err) |
||||
} |
||||
if leader == "" { |
||||
t.Fatalf("Expected leader") |
||||
} |
||||
} |
||||
|
||||
func TestStatusPeers(t *testing.T) { |
||||
c := makeClient(t) |
||||
status := c.Status() |
||||
|
||||
peers, err := status.Peers() |
||||
if err != nil { |
||||
t.Fatalf("err: %v", err) |
||||
} |
||||
if len(peers) == 0 { |
||||
t.Fatalf("Expected peers ") |
||||
} |
||||
} |
Loading…
Reference in new issue