github.com/sl1pm4t/consul@v1.4.5-0.20190325224627-74c31c540f9c/testutil/server.go (about)

     1  package testutil
     2  
     3  // TestServer is a test helper. It uses a fork/exec model to create
     4  // a test Consul server instance in the background and initialize it
     5  // with some data and/or services. The test server can then be used
     6  // to run a unit test, and offers an easy API to tear itself down
     7  // when the test has completed. The only prerequisite is to have a consul
     8  // binary available on the $PATH.
     9  //
    10  // This package does not use Consul's official API client. This is
    11  // because we use TestServer to test the API client, which would
    12  // otherwise cause an import cycle.
    13  
    14  import (
    15  	"context"
    16  	"encoding/json"
    17  	"fmt"
    18  	"io"
    19  	"io/ioutil"
    20  	"log"
    21  	"net"
    22  	"net/http"
    23  	"os"
    24  	"os/exec"
    25  	"path/filepath"
    26  	"strconv"
    27  	"strings"
    28  	"testing"
    29  	"time"
    30  
    31  	"github.com/hashicorp/consul/lib/freeport"
    32  	"github.com/hashicorp/consul/testutil/retry"
    33  	"github.com/hashicorp/go-cleanhttp"
    34  	"github.com/hashicorp/go-uuid"
    35  	"github.com/pkg/errors"
    36  )
    37  
    38  // TestPerformanceConfig configures the performance parameters.
    39  type TestPerformanceConfig struct {
    40  	RaftMultiplier uint `json:"raft_multiplier,omitempty"`
    41  }
    42  
    43  // TestPortConfig configures the various ports used for services
    44  // provided by the Consul server.
    45  type TestPortConfig struct {
    46  	DNS          int `json:"dns,omitempty"`
    47  	HTTP         int `json:"http,omitempty"`
    48  	HTTPS        int `json:"https,omitempty"`
    49  	SerfLan      int `json:"serf_lan,omitempty"`
    50  	SerfWan      int `json:"serf_wan,omitempty"`
    51  	Server       int `json:"server,omitempty"`
    52  	ProxyMinPort int `json:"proxy_min_port,omitempty"`
    53  	ProxyMaxPort int `json:"proxy_max_port,omitempty"`
    54  }
    55  
    56  // TestAddressConfig contains the bind addresses for various
    57  // components of the Consul server.
    58  type TestAddressConfig struct {
    59  	HTTP string `json:"http,omitempty"`
    60  }
    61  
    62  // TestNetworkSegment contains the configuration for a network segment.
    63  type TestNetworkSegment struct {
    64  	Name      string `json:"name"`
    65  	Bind      string `json:"bind"`
    66  	Port      int    `json:"port"`
    67  	Advertise string `json:"advertise"`
    68  }
    69  
    70  // TestServerConfig is the main server configuration struct.
    71  type TestServerConfig struct {
    72  	NodeName            string                 `json:"node_name"`
    73  	NodeID              string                 `json:"node_id"`
    74  	NodeMeta            map[string]string      `json:"node_meta,omitempty"`
    75  	Performance         *TestPerformanceConfig `json:"performance,omitempty"`
    76  	Bootstrap           bool                   `json:"bootstrap,omitempty"`
    77  	Server              bool                   `json:"server,omitempty"`
    78  	DataDir             string                 `json:"data_dir,omitempty"`
    79  	Datacenter          string                 `json:"datacenter,omitempty"`
    80  	Segments            []TestNetworkSegment   `json:"segments"`
    81  	DisableCheckpoint   bool                   `json:"disable_update_check"`
    82  	LogLevel            string                 `json:"log_level,omitempty"`
    83  	Bind                string                 `json:"bind_addr,omitempty"`
    84  	Addresses           *TestAddressConfig     `json:"addresses,omitempty"`
    85  	Ports               *TestPortConfig        `json:"ports,omitempty"`
    86  	RaftProtocol        int                    `json:"raft_protocol,omitempty"`
    87  	ACLMasterToken      string                 `json:"acl_master_token,omitempty"`
    88  	ACLDatacenter       string                 `json:"acl_datacenter,omitempty"`
    89  	PrimaryDatacenter   string                 `json:"primary_datacenter,omitempty"`
    90  	ACLDefaultPolicy    string                 `json:"acl_default_policy,omitempty"`
    91  	ACLEnforceVersion8  bool                   `json:"acl_enforce_version_8"`
    92  	ACL                 TestACLs               `json:"acl,omitempty"`
    93  	Encrypt             string                 `json:"encrypt,omitempty"`
    94  	CAFile              string                 `json:"ca_file,omitempty"`
    95  	CertFile            string                 `json:"cert_file,omitempty"`
    96  	KeyFile             string                 `json:"key_file,omitempty"`
    97  	VerifyIncoming      bool                   `json:"verify_incoming,omitempty"`
    98  	VerifyIncomingRPC   bool                   `json:"verify_incoming_rpc,omitempty"`
    99  	VerifyIncomingHTTPS bool                   `json:"verify_incoming_https,omitempty"`
   100  	VerifyOutgoing      bool                   `json:"verify_outgoing,omitempty"`
   101  	EnableScriptChecks  bool                   `json:"enable_script_checks,omitempty"`
   102  	Connect             map[string]interface{} `json:"connect,omitempty"`
   103  	EnableDebug         bool                   `json:"enable_debug,omitempty"`
   104  	ReadyTimeout        time.Duration          `json:"-"`
   105  	Stdout, Stderr      io.Writer              `json:"-"`
   106  	Args                []string               `json:"-"`
   107  }
   108  
   109  type TestACLs struct {
   110  	Enabled             bool       `json:"enabled,omitempty"`
   111  	TokenReplication    bool       `json:"enable_token_replication,omitempty"`
   112  	PolicyTTL           string     `json:"policy_ttl,omitempty"`
   113  	TokenTTL            string     `json:"token_ttl,omitempty"`
   114  	DownPolicy          string     `json:"down_policy,omitempty"`
   115  	DefaultPolicy       string     `json:"default_policy,omitempty"`
   116  	EnableKeyListPolicy bool       `json:"enable_key_list_policy,omitempty"`
   117  	Tokens              TestTokens `json:"tokens,omitempty"`
   118  	DisabledTTL         string     `json:"disabled_ttl,omitempty"`
   119  }
   120  
   121  type TestTokens struct {
   122  	Master      string `json:"master,omitempty"`
   123  	Replication string `json:"replication,omitempty"`
   124  	AgentMaster string `json:"agent_master,omitempty"`
   125  	Default     string `json:"default,omitempty"`
   126  	Agent       string `json:"agent,omitempty"`
   127  }
   128  
   129  // ServerConfigCallback is a function interface which can be
   130  // passed to NewTestServerConfig to modify the server config.
   131  type ServerConfigCallback func(c *TestServerConfig)
   132  
   133  // defaultServerConfig returns a new TestServerConfig struct
   134  // with all of the listen ports incremented by one.
   135  func defaultServerConfig() *TestServerConfig {
   136  	nodeID, err := uuid.GenerateUUID()
   137  	if err != nil {
   138  		panic(err)
   139  	}
   140  
   141  	ports := freeport.Get(6)
   142  	return &TestServerConfig{
   143  		NodeName:          "node-" + nodeID,
   144  		NodeID:            nodeID,
   145  		DisableCheckpoint: true,
   146  		Performance: &TestPerformanceConfig{
   147  			RaftMultiplier: 1,
   148  		},
   149  		Bootstrap: true,
   150  		Server:    true,
   151  		LogLevel:  "debug",
   152  		Bind:      "127.0.0.1",
   153  		Addresses: &TestAddressConfig{},
   154  		Ports: &TestPortConfig{
   155  			DNS:     ports[0],
   156  			HTTP:    ports[1],
   157  			HTTPS:   ports[2],
   158  			SerfLan: ports[3],
   159  			SerfWan: ports[4],
   160  			Server:  ports[5],
   161  		},
   162  		ReadyTimeout: 10 * time.Second,
   163  		Connect: map[string]interface{}{
   164  			"enabled": true,
   165  			"ca_config": map[string]interface{}{
   166  				// const TestClusterID causes import cycle so hard code it here.
   167  				"cluster_id": "11111111-2222-3333-4444-555555555555",
   168  			},
   169  			"proxy": map[string]interface{}{
   170  				"allow_managed_api_registration": true,
   171  			},
   172  		},
   173  	}
   174  }
   175  
   176  // TestService is used to serialize a service definition.
   177  type TestService struct {
   178  	ID      string   `json:",omitempty"`
   179  	Name    string   `json:",omitempty"`
   180  	Tags    []string `json:",omitempty"`
   181  	Address string   `json:",omitempty"`
   182  	Port    int      `json:",omitempty"`
   183  }
   184  
   185  // TestCheck is used to serialize a check definition.
   186  type TestCheck struct {
   187  	ID        string `json:",omitempty"`
   188  	Name      string `json:",omitempty"`
   189  	ServiceID string `json:",omitempty"`
   190  	TTL       string `json:",omitempty"`
   191  }
   192  
   193  // TestKVResponse is what we use to decode KV data.
   194  type TestKVResponse struct {
   195  	Value string
   196  }
   197  
   198  // TestServer is the main server wrapper struct.
   199  type TestServer struct {
   200  	cmd    *exec.Cmd
   201  	Config *TestServerConfig
   202  
   203  	HTTPAddr  string
   204  	HTTPSAddr string
   205  	LANAddr   string
   206  	WANAddr   string
   207  
   208  	HTTPClient *http.Client
   209  
   210  	tmpdir string
   211  }
   212  
   213  // NewTestServer is an easy helper method to create a new Consul
   214  // test server with the most basic configuration.
   215  func NewTestServer() (*TestServer, error) {
   216  	return NewTestServerConfigT(nil, nil)
   217  }
   218  
   219  func NewTestServerConfig(cb ServerConfigCallback) (*TestServer, error) {
   220  	return NewTestServerConfigT(nil, cb)
   221  }
   222  
   223  // NewTestServerConfig creates a new TestServer, and makes a call to an optional
   224  // callback function to modify the configuration. If there is an error
   225  // configuring or starting the server, the server will NOT be running when the
   226  // function returns (thus you do not need to stop it).
   227  func NewTestServerConfigT(t *testing.T, cb ServerConfigCallback) (*TestServer, error) {
   228  	return newTestServerConfigT(t, cb)
   229  }
   230  
   231  // newTestServerConfigT is the internal helper for NewTestServerConfigT.
   232  func newTestServerConfigT(t *testing.T, cb ServerConfigCallback) (*TestServer, error) {
   233  	path, err := exec.LookPath("consul")
   234  	if err != nil || path == "" {
   235  		return nil, fmt.Errorf("consul not found on $PATH - download and install " +
   236  			"consul or skip this test")
   237  	}
   238  
   239  	tmpdir := TempDir(t, "consul")
   240  	cfg := defaultServerConfig()
   241  	cfg.DataDir = filepath.Join(tmpdir, "data")
   242  	if cb != nil {
   243  		cb(cfg)
   244  	}
   245  
   246  	b, err := json.Marshal(cfg)
   247  	if err != nil {
   248  		return nil, errors.Wrap(err, "failed marshaling json")
   249  	}
   250  
   251  	log.Printf("CONFIG JSON: %s", string(b))
   252  	configFile := filepath.Join(tmpdir, "config.json")
   253  	if err := ioutil.WriteFile(configFile, b, 0644); err != nil {
   254  		defer os.RemoveAll(tmpdir)
   255  		return nil, errors.Wrap(err, "failed writing config content")
   256  	}
   257  
   258  	stdout := io.Writer(os.Stdout)
   259  	if cfg.Stdout != nil {
   260  		stdout = cfg.Stdout
   261  	}
   262  	stderr := io.Writer(os.Stderr)
   263  	if cfg.Stderr != nil {
   264  		stderr = cfg.Stderr
   265  	}
   266  
   267  	// Start the server
   268  	args := []string{"agent", "-config-file", configFile}
   269  	args = append(args, cfg.Args...)
   270  	cmd := exec.Command("consul", args...)
   271  	cmd.Stdout = stdout
   272  	cmd.Stderr = stderr
   273  	if err := cmd.Start(); err != nil {
   274  		return nil, errors.Wrap(err, "failed starting command")
   275  	}
   276  
   277  	httpAddr := fmt.Sprintf("127.0.0.1:%d", cfg.Ports.HTTP)
   278  	client := cleanhttp.DefaultClient()
   279  	if strings.HasPrefix(cfg.Addresses.HTTP, "unix://") {
   280  		httpAddr = cfg.Addresses.HTTP
   281  		tr := cleanhttp.DefaultTransport()
   282  		tr.DialContext = func(_ context.Context, _, _ string) (net.Conn, error) {
   283  			return net.Dial("unix", httpAddr[len("unix://"):])
   284  		}
   285  		client = &http.Client{Transport: tr}
   286  	}
   287  
   288  	server := &TestServer{
   289  		Config: cfg,
   290  		cmd:    cmd,
   291  
   292  		HTTPAddr:  httpAddr,
   293  		HTTPSAddr: fmt.Sprintf("127.0.0.1:%d", cfg.Ports.HTTPS),
   294  		LANAddr:   fmt.Sprintf("127.0.0.1:%d", cfg.Ports.SerfLan),
   295  		WANAddr:   fmt.Sprintf("127.0.0.1:%d", cfg.Ports.SerfWan),
   296  
   297  		HTTPClient: client,
   298  
   299  		tmpdir: tmpdir,
   300  	}
   301  
   302  	// Wait for the server to be ready
   303  	if cfg.Bootstrap {
   304  		err = server.waitForLeader()
   305  	} else {
   306  		err = server.waitForAPI()
   307  	}
   308  	if err != nil {
   309  		defer server.Stop()
   310  		return nil, errors.Wrap(err, "failed waiting for server to start")
   311  	}
   312  	return server, nil
   313  }
   314  
   315  // Stop stops the test Consul server, and removes the Consul data
   316  // directory once we are done.
   317  func (s *TestServer) Stop() error {
   318  	defer os.RemoveAll(s.tmpdir)
   319  
   320  	// There was no process
   321  	if s.cmd == nil {
   322  		return nil
   323  	}
   324  
   325  	if s.cmd.Process != nil {
   326  		if err := s.cmd.Process.Signal(os.Interrupt); err != nil {
   327  			return errors.Wrap(err, "failed to kill consul server")
   328  		}
   329  	}
   330  
   331  	// wait for the process to exit to be sure that the data dir can be
   332  	// deleted on all platforms.
   333  	return s.cmd.Wait()
   334  }
   335  
   336  type failer struct {
   337  	failed bool
   338  }
   339  
   340  func (f *failer) Log(args ...interface{}) { fmt.Println(args...) }
   341  func (f *failer) FailNow()                { f.failed = true }
   342  
   343  // waitForAPI waits for only the agent HTTP endpoint to start
   344  // responding. This is an indication that the agent has started,
   345  // but will likely return before a leader is elected.
   346  func (s *TestServer) waitForAPI() error {
   347  	f := &failer{}
   348  	retry.Run(f, func(r *retry.R) {
   349  		resp, err := s.HTTPClient.Get(s.url("/v1/agent/self"))
   350  		if err != nil {
   351  			r.Fatal(err)
   352  		}
   353  		defer resp.Body.Close()
   354  		if err := s.requireOK(resp); err != nil {
   355  			r.Fatal("failed OK response", err)
   356  		}
   357  	})
   358  	if f.failed {
   359  		return errors.New("failed waiting for API")
   360  	}
   361  	return nil
   362  }
   363  
   364  // waitForLeader waits for the Consul server's HTTP API to become
   365  // available, and then waits for a known leader and an index of
   366  // 1 or more to be observed to confirm leader election is done.
   367  // It then waits to ensure the anti-entropy sync has completed.
   368  func (s *TestServer) waitForLeader() error {
   369  	f := &failer{}
   370  	timer := &retry.Timer{
   371  		Timeout: s.Config.ReadyTimeout,
   372  		Wait:    250 * time.Millisecond,
   373  	}
   374  	var index int64
   375  	retry.RunWith(timer, f, func(r *retry.R) {
   376  		// Query the API and check the status code.
   377  		url := s.url(fmt.Sprintf("/v1/catalog/nodes?index=%d", index))
   378  		resp, err := s.HTTPClient.Get(url)
   379  		if err != nil {
   380  			r.Fatal("failed http get", err)
   381  		}
   382  		defer resp.Body.Close()
   383  		if err := s.requireOK(resp); err != nil {
   384  			r.Fatal("failed OK response", err)
   385  		}
   386  
   387  		// Ensure we have a leader and a node registration.
   388  		if leader := resp.Header.Get("X-Consul-KnownLeader"); leader != "true" {
   389  			r.Fatalf("Consul leader status: %#v", leader)
   390  		}
   391  		index, err = strconv.ParseInt(resp.Header.Get("X-Consul-Index"), 10, 64)
   392  		if err != nil {
   393  			r.Fatal("bad consul index", err)
   394  		}
   395  		if index == 0 {
   396  			r.Fatal("consul index is 0")
   397  		}
   398  
   399  		// Watch for the anti-entropy sync to finish.
   400  		var v []map[string]interface{}
   401  		dec := json.NewDecoder(resp.Body)
   402  		if err := dec.Decode(&v); err != nil {
   403  			r.Fatal(err)
   404  		}
   405  		if len(v) < 1 {
   406  			r.Fatal("No nodes")
   407  		}
   408  		taggedAddresses, ok := v[0]["TaggedAddresses"].(map[string]interface{})
   409  		if !ok {
   410  			r.Fatal("Missing tagged addresses")
   411  		}
   412  		if _, ok := taggedAddresses["lan"]; !ok {
   413  			r.Fatal("No lan tagged addresses")
   414  		}
   415  	})
   416  	if f.failed {
   417  		return errors.New("failed waiting for leader")
   418  	}
   419  	return nil
   420  }
   421  
   422  // WaitForSerfCheck ensures we have a node with serfHealth check registered
   423  // Behavior mirrors testrpc.WaitForTestAgent but avoids the dependency cycle in api pkg
   424  func (s *TestServer) WaitForSerfCheck(t *testing.T) {
   425  	retry.Run(t, func(r *retry.R) {
   426  		// Query the API and check the status code.
   427  		url := s.url("/v1/catalog/nodes?index=0")
   428  		resp, err := s.HTTPClient.Get(url)
   429  		if err != nil {
   430  			r.Fatal("failed http get", err)
   431  		}
   432  		defer resp.Body.Close()
   433  		if err := s.requireOK(resp); err != nil {
   434  			r.Fatal("failed OK response", err)
   435  		}
   436  
   437  		// Watch for the anti-entropy sync to finish.
   438  		var payload []map[string]interface{}
   439  		dec := json.NewDecoder(resp.Body)
   440  		if err := dec.Decode(&payload); err != nil {
   441  			r.Fatal(err)
   442  		}
   443  		if len(payload) < 1 {
   444  			r.Fatal("No nodes")
   445  		}
   446  
   447  		// Ensure the serfHealth check is registered
   448  		url = s.url(fmt.Sprintf("/v1/health/node/%s", payload[0]["Node"]))
   449  		resp, err = s.HTTPClient.Get(url)
   450  		if err != nil {
   451  			r.Fatal("failed http get", err)
   452  		}
   453  		defer resp.Body.Close()
   454  		if err := s.requireOK(resp); err != nil {
   455  			r.Fatal("failed OK response", err)
   456  		}
   457  		dec = json.NewDecoder(resp.Body)
   458  		if err = dec.Decode(&payload); err != nil {
   459  			r.Fatal(err)
   460  		}
   461  
   462  		var found bool
   463  		for _, check := range payload {
   464  			if check["CheckID"].(string) == "serfHealth" {
   465  				found = true
   466  				break
   467  			}
   468  		}
   469  		if !found {
   470  			r.Fatal("missing serfHealth registration")
   471  		}
   472  	})
   473  }