mirror of https://github.com/hashicorp/consul
You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
255 lines
7.0 KiB
255 lines
7.0 KiB
package testutil |
|
|
|
import ( |
|
"bytes" |
|
"encoding/base64" |
|
"encoding/json" |
|
"fmt" |
|
"io" |
|
"log" |
|
"net/http" |
|
"testing" |
|
|
|
"github.com/pkg/errors" |
|
) |
|
|
|
// copied from testutil to break circular dependency |
|
const ( |
|
HealthAny = "any" |
|
HealthPassing = "passing" |
|
HealthWarning = "warning" |
|
HealthCritical = "critical" |
|
HealthMaint = "maintenance" |
|
) |
|
|
|
// JoinLAN is used to join local datacenters together. |
|
func (s *TestServer) JoinLAN(t testing.TB, addr string) { |
|
resp := s.put(t, "/v1/agent/join/"+addr, nil) |
|
defer resp.Body.Close() |
|
} |
|
|
|
// JoinWAN is used to join remote datacenters together. |
|
func (s *TestServer) JoinWAN(t testing.TB, addr string) { |
|
resp := s.put(t, "/v1/agent/join/"+addr+"?wan=1", nil) |
|
resp.Body.Close() |
|
} |
|
|
|
// SetKV sets an individual key in the K/V store. |
|
func (s *TestServer) SetKV(t testing.TB, key string, val []byte) { |
|
resp := s.put(t, "/v1/kv/"+key, bytes.NewBuffer(val)) |
|
resp.Body.Close() |
|
} |
|
|
|
// SetKVString sets an individual key in the K/V store, but accepts a string |
|
// instead of []byte. |
|
func (s *TestServer) SetKVString(t testing.TB, key string, val string) { |
|
resp := s.put(t, "/v1/kv/"+key, bytes.NewBufferString(val)) |
|
resp.Body.Close() |
|
} |
|
|
|
// GetKV retrieves a single key and returns its value |
|
func (s *TestServer) GetKV(t testing.TB, key string) []byte { |
|
resp := s.get(t, "/v1/kv/"+key) |
|
defer resp.Body.Close() |
|
|
|
raw, err := io.ReadAll(resp.Body) |
|
if err != nil { |
|
t.Fatalf("failed to read body: %s", err) |
|
} |
|
|
|
var result []*TestKVResponse |
|
if err := json.Unmarshal(raw, &result); err != nil { |
|
t.Fatalf("failed to unmarshal: %s", err) |
|
} |
|
if len(result) < 1 { |
|
t.Fatalf("key does not exist: %s", key) |
|
} |
|
|
|
v, err := base64.StdEncoding.DecodeString(result[0].Value) |
|
if err != nil { |
|
t.Fatalf("failed to base64 decode: %s", err) |
|
} |
|
|
|
return v |
|
} |
|
|
|
// GetKVString retrieves a value from the store, but returns as a string instead |
|
// of []byte. |
|
func (s *TestServer) GetKVString(t testing.TB, key string) string { |
|
return string(s.GetKV(t, key)) |
|
} |
|
|
|
// PopulateKV fills the Consul KV with data from a generic map. |
|
func (s *TestServer) PopulateKV(t testing.TB, data map[string][]byte) { |
|
for k, v := range data { |
|
s.SetKV(t, k, v) |
|
} |
|
} |
|
|
|
// ListKV returns a list of keys present in the KV store. This will list all |
|
// keys under the given prefix recursively and return them as a slice. |
|
func (s *TestServer) ListKV(t testing.TB, prefix string) []string { |
|
resp := s.get(t, "/v1/kv/"+prefix+"?keys") |
|
defer resp.Body.Close() |
|
|
|
raw, err := io.ReadAll(resp.Body) |
|
if err != nil { |
|
t.Fatalf("failed to read body: %s", err) |
|
} |
|
|
|
var result []string |
|
if err := json.Unmarshal(raw, &result); err != nil { |
|
t.Fatalf("failed to unmarshal: %s", err) |
|
} |
|
return result |
|
} |
|
|
|
// AddService adds a new service to the Consul instance. It also |
|
// automatically adds a health check with the given status, which |
|
// can be one of "passing", "warning", or "critical". |
|
func (s *TestServer) AddService(t testing.TB, name, status string, tags []string) { |
|
s.AddAddressableService(t, name, status, "", 0, tags) // set empty address and 0 as port for non-accessible service |
|
} |
|
|
|
// AddAddressableService adds a new service to the Consul instance by |
|
// passing "address" and "port". It is helpful when you need to prepare a fakeService |
|
// that maybe accessed with in target source code. |
|
// It also automatically adds a health check with the given status, which |
|
// can be one of "passing", "warning", or "critical", just like `AddService` does. |
|
func (s *TestServer) AddAddressableService(t testing.TB, name, status, address string, port int, tags []string) { |
|
svc := &TestService{ |
|
Name: name, |
|
Tags: tags, |
|
Address: address, |
|
Port: port, |
|
} |
|
payload, err := s.encodePayload(svc) |
|
if err != nil { |
|
t.Fatal(err) |
|
} |
|
s.put(t, "/v1/agent/service/register", payload) |
|
|
|
chkName := "service:" + name |
|
chk := &TestCheck{ |
|
Name: chkName, |
|
ServiceID: name, |
|
TTL: "10m", |
|
} |
|
payload, err = s.encodePayload(chk) |
|
if err != nil { |
|
t.Fatal(err) |
|
} |
|
s.put(t, "/v1/agent/check/register", payload) |
|
|
|
switch status { |
|
case HealthPassing: |
|
s.put(t, "/v1/agent/check/pass/"+chkName, nil) |
|
case HealthWarning: |
|
s.put(t, "/v1/agent/check/warn/"+chkName, nil) |
|
case HealthCritical: |
|
s.put(t, "/v1/agent/check/fail/"+chkName, nil) |
|
default: |
|
t.Fatalf("Unrecognized status: %s", status) |
|
} |
|
} |
|
|
|
// AddCheck adds a check to the Consul instance. If the serviceID is |
|
// left empty (""), then the check will be associated with the node. |
|
// The check status may be "passing", "warning", or "critical". |
|
func (s *TestServer) AddCheck(t testing.TB, name, serviceID, status string) { |
|
chk := &TestCheck{ |
|
ID: name, |
|
Name: name, |
|
TTL: "10m", |
|
} |
|
if serviceID != "" { |
|
chk.ServiceID = serviceID |
|
} |
|
|
|
payload, err := s.encodePayload(chk) |
|
if err != nil { |
|
t.Fatal(err) |
|
} |
|
s.put(t, "/v1/agent/check/register", payload) |
|
|
|
switch status { |
|
case HealthPassing: |
|
s.put(t, "/v1/agent/check/pass/"+name, nil) |
|
case HealthWarning: |
|
s.put(t, "/v1/agent/check/warn/"+name, nil) |
|
case HealthCritical: |
|
s.put(t, "/v1/agent/check/fail/"+name, nil) |
|
default: |
|
t.Fatalf("Unrecognized status: %s", status) |
|
} |
|
} |
|
|
|
// put performs a new HTTP PUT request. |
|
func (s *TestServer) put(t testing.TB, path string, body io.Reader) *http.Response { |
|
req, err := http.NewRequest("PUT", s.url(path), body) |
|
if err != nil { |
|
t.Fatalf("failed to create PUT request: %s", err) |
|
} |
|
resp, err := s.HTTPClient.Do(req) |
|
if err != nil { |
|
t.Fatalf("failed to make PUT request: %s", err) |
|
} |
|
if err := s.requireOK(resp); err != nil { |
|
defer resp.Body.Close() |
|
t.Fatalf("not OK PUT: %s", err) |
|
} |
|
return resp |
|
} |
|
|
|
// get performs a new HTTP GET request. |
|
func (s *TestServer) get(t testing.TB, path string) *http.Response { |
|
resp, err := s.HTTPClient.Get(s.url(path)) |
|
if err != nil { |
|
t.Fatalf("failed to create GET request: %s", err) |
|
} |
|
if err := s.requireOK(resp); err != nil { |
|
defer resp.Body.Close() |
|
t.Fatalf("not OK GET: %s", err) |
|
} |
|
return resp |
|
} |
|
|
|
// encodePayload returns a new io.Reader wrapping the encoded contents |
|
// of the payload, suitable for passing directly to a new request. |
|
func (s *TestServer) encodePayload(payload interface{}) (io.Reader, error) { |
|
var encoded bytes.Buffer |
|
enc := json.NewEncoder(&encoded) |
|
if err := enc.Encode(payload); err != nil { |
|
return nil, errors.Wrap(err, "failed to encode payload") |
|
} |
|
return &encoded, nil |
|
} |
|
|
|
// url is a helper function which takes a relative URL and |
|
// makes it into a proper URL against the local Consul server. |
|
func (s *TestServer) url(path string) string { |
|
if s == nil { |
|
log.Fatal("s is nil") |
|
} |
|
if s.Config == nil { |
|
log.Fatal("s.Config is nil") |
|
} |
|
if s.Config.Ports == nil { |
|
log.Fatal("s.Config.Ports is nil") |
|
} |
|
if s.Config.Ports.HTTP == 0 { |
|
log.Fatal("s.Config.Ports.HTTP is 0") |
|
} |
|
if path == "" { |
|
log.Fatal("path is empty") |
|
} |
|
return fmt.Sprintf("http://127.0.0.1:%d%s", s.Config.Ports.HTTP, path) |
|
} |
|
|
|
// requireOK checks the HTTP response code and ensures it is acceptable. |
|
func (s *TestServer) requireOK(resp *http.Response) error { |
|
if resp.StatusCode != 200 { |
|
return fmt.Errorf("Bad status code: %d", resp.StatusCode) |
|
} |
|
return nil |
|
}
|
|
|