2015-01-06 18:40:00 +00:00
|
|
|
package api
|
|
|
|
|
|
|
|
import (
|
|
|
|
crand "crypto/rand"
|
2015-01-14 19:30:43 +00:00
|
|
|
"encoding/json"
|
2015-01-06 18:40:00 +00:00
|
|
|
"fmt"
|
2015-01-06 23:26:50 +00:00
|
|
|
"io/ioutil"
|
2015-01-06 18:40:00 +00:00
|
|
|
"net/http"
|
2015-01-06 23:26:50 +00:00
|
|
|
"os"
|
|
|
|
"os/exec"
|
2015-01-06 18:40:00 +00:00
|
|
|
"testing"
|
|
|
|
"time"
|
2015-01-07 00:48:54 +00:00
|
|
|
|
|
|
|
"github.com/hashicorp/consul/testutil"
|
2015-01-06 18:40:00 +00:00
|
|
|
)
|
|
|
|
|
2015-01-06 23:26:50 +00:00
|
|
|
type testServer struct {
|
|
|
|
pid int
|
|
|
|
dataDir string
|
|
|
|
configFile string
|
|
|
|
}
|
|
|
|
|
2015-01-14 21:25:12 +00:00
|
|
|
type testPortConfig struct {
|
2015-01-14 19:30:43 +00:00
|
|
|
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"`
|
|
|
|
}
|
|
|
|
|
2015-01-14 21:25:12 +00:00
|
|
|
type testAddressConfig struct {
|
2015-01-14 19:30:43 +00:00
|
|
|
HTTP string `json:"http,omitempty"`
|
|
|
|
}
|
|
|
|
|
2015-01-14 21:25:12 +00:00
|
|
|
type testServerConfig struct {
|
2015-01-14 19:30:43 +00:00
|
|
|
Bootstrap bool `json:"bootstrap,omitempty"`
|
|
|
|
Server bool `json:"server,omitempty"`
|
|
|
|
DataDir string `json:"data_dir,omitempty"`
|
|
|
|
LogLevel string `json:"log_level,omitempty"`
|
2015-01-14 21:25:12 +00:00
|
|
|
Addresses *testAddressConfig `json:"addresses,omitempty"`
|
|
|
|
Ports testPortConfig `json:"ports,omitempty"`
|
2015-01-14 19:30:43 +00:00
|
|
|
}
|
|
|
|
|
2015-01-16 01:24:15 +00:00
|
|
|
// Callback functions for modifying config
|
|
|
|
type configCallback func(c *Config)
|
|
|
|
type serverConfigCallback func(c *testServerConfig)
|
|
|
|
|
2015-01-14 21:25:12 +00:00
|
|
|
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,
|
|
|
|
},
|
|
|
|
}
|
2015-01-14 19:30:43 +00:00
|
|
|
}
|
|
|
|
|
2015-01-06 23:26:50 +00:00
|
|
|
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 {
|
2015-01-14 21:25:12 +00:00
|
|
|
return newTestServerWithConfig(t, func(c *testServerConfig) {})
|
2015-01-14 19:30:43 +00:00
|
|
|
}
|
|
|
|
|
2015-01-16 01:24:15 +00:00
|
|
|
func newTestServerWithConfig(t *testing.T, cb serverConfigCallback) *testServer {
|
2015-01-06 23:26:50 +00:00
|
|
|
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)
|
|
|
|
}
|
2015-01-14 19:30:43 +00:00
|
|
|
|
2015-01-14 21:25:12 +00:00
|
|
|
consulConfig := defaultConfig()
|
2015-01-14 19:30:43 +00:00
|
|
|
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 {
|
2015-01-06 23:26:50 +00:00
|
|
|
t.Fatalf("err: %s", err)
|
|
|
|
}
|
|
|
|
configFile.Close()
|
|
|
|
|
|
|
|
// Start the server
|
|
|
|
cmd := exec.Command("consul", "agent", "-config-file", configFile.Name())
|
2015-01-13 19:50:09 +00:00
|
|
|
cmd.Stdout = os.Stdout
|
|
|
|
cmd.Stderr = os.Stderr
|
2015-01-06 23:26:50 +00:00
|
|
|
if err := cmd.Start(); err != nil {
|
|
|
|
t.Fatalf("err: %s", err)
|
|
|
|
}
|
|
|
|
|
2015-01-14 19:30:43 +00:00
|
|
|
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"
|
2015-01-14 21:25:12 +00:00
|
|
|
}, func(c *testServerConfig) {})
|
2015-01-14 19:30:43 +00:00
|
|
|
}
|
|
|
|
|
2015-01-16 01:24:15 +00:00
|
|
|
func makeClientWithConfig(t *testing.T, cb1 configCallback, cb2 serverConfigCallback) (*Client, *testServer) {
|
|
|
|
// Make client config
|
2015-01-14 19:30:43 +00:00
|
|
|
conf := DefaultConfig()
|
2015-01-16 01:24:15 +00:00
|
|
|
cb1(conf)
|
|
|
|
|
|
|
|
// Create client
|
2015-01-14 19:30:43 +00:00
|
|
|
client, err := NewClient(conf)
|
|
|
|
if err != nil {
|
|
|
|
t.Fatalf("err: %v", err)
|
|
|
|
}
|
|
|
|
|
2015-01-16 01:24:15 +00:00
|
|
|
// Create server
|
|
|
|
server := newTestServerWithConfig(t, cb2)
|
|
|
|
|
2015-01-06 23:26:50 +00:00
|
|
|
// Allow the server some time to start, and verify we have a leader.
|
2015-01-07 00:48:54 +00:00
|
|
|
testutil.WaitForResult(func() (bool, error) {
|
2015-01-14 19:30:43 +00:00
|
|
|
req := client.newRequest("GET", "/v1/catalog/nodes")
|
|
|
|
_, resp, err := client.doRequest(req)
|
2015-01-06 23:26:50 +00:00
|
|
|
if err != nil {
|
2015-01-07 00:48:54 +00:00
|
|
|
return false, err
|
2015-01-06 23:26:50 +00:00
|
|
|
}
|
2015-01-10 00:39:35 +00:00
|
|
|
resp.Body.Close()
|
2015-01-06 23:26:50 +00:00
|
|
|
|
2015-01-10 00:39:35 +00:00
|
|
|
// 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")
|
2015-01-06 23:26:50 +00:00
|
|
|
}
|
|
|
|
|
2015-01-07 00:48:54 +00:00
|
|
|
return true, nil
|
|
|
|
}, func(err error) {
|
|
|
|
t.Fatalf("err: %s", err)
|
|
|
|
})
|
2015-01-06 23:26:50 +00:00
|
|
|
|
|
|
|
return client, server
|
2015-01-06 18:40:00 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
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) {
|
2015-01-06 23:26:50 +00:00
|
|
|
c, s := makeClient(t)
|
|
|
|
defer s.stop()
|
|
|
|
|
2015-01-06 18:40:00 +00:00
|
|
|
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) {
|
2015-01-06 23:26:50 +00:00
|
|
|
c, s := makeClient(t)
|
|
|
|
defer s.stop()
|
|
|
|
|
2015-01-06 18:40:00 +00:00
|
|
|
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) {
|
2015-01-06 23:26:50 +00:00
|
|
|
c, s := makeClient(t)
|
|
|
|
defer s.stop()
|
|
|
|
|
2015-01-06 18:40:00 +00:00
|
|
|
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)
|
|
|
|
}
|
2015-01-14 19:30:43 +00:00
|
|
|
if req.URL.RequestURI() != "/v1/kv/foo?dc=foo" {
|
2015-01-06 18:40:00 +00:00
|
|
|
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)
|
|
|
|
}
|
|
|
|
}
|