package testutil
import (
"bytes"
"encoding/base64"
"encoding/json"
"fmt"
"io"
"io/ioutil"
"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 := ioutil . 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 := ioutil . 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
}