github.com/zoomfoo/nomad@v0.8.5-0.20180907175415-f28fd3a1a056/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 Nomad 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 nomad
     8  // binary available on the $PATH.
     9  //
    10  // This package does not use Nomad'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  	"bytes"
    16  	"encoding/json"
    17  	"fmt"
    18  	"io"
    19  	"io/ioutil"
    20  	"net/http"
    21  	"os"
    22  	"os/exec"
    23  
    24  	"github.com/hashicorp/consul/lib/freeport"
    25  	cleanhttp "github.com/hashicorp/go-cleanhttp"
    26  	"github.com/hashicorp/nomad/helper/discover"
    27  	testing "github.com/mitchellh/go-testing-interface"
    28  )
    29  
    30  // TestServerConfig is the main server configuration struct.
    31  type TestServerConfig struct {
    32  	NodeName          string        `json:"name,omitempty"`
    33  	DataDir           string        `json:"data_dir,omitempty"`
    34  	Region            string        `json:"region,omitempty"`
    35  	DisableCheckpoint bool          `json:"disable_update_check"`
    36  	LogLevel          string        `json:"log_level,omitempty"`
    37  	Consul            *Consul       `json:"consul,omitempty"`
    38  	AdvertiseAddrs    *Advertise    `json:"advertise,omitempty"`
    39  	Ports             *PortsConfig  `json:"ports,omitempty"`
    40  	Server            *ServerConfig `json:"server,omitempty"`
    41  	Client            *ClientConfig `json:"client,omitempty"`
    42  	Vault             *VaultConfig  `json:"vault,omitempty"`
    43  	ACL               *ACLConfig    `json:"acl,omitempty"`
    44  	DevMode           bool          `json:"-"`
    45  	Stdout, Stderr    io.Writer     `json:"-"`
    46  }
    47  
    48  // Consul is used to configure the communication with Consul
    49  type Consul struct {
    50  	Address string `json:"address,omitempty"`
    51  	Auth    string `json:"auth,omitempty"`
    52  	Token   string `json:"token,omitempty"`
    53  }
    54  
    55  // Advertise is used to configure the addresses to advertise
    56  type Advertise struct {
    57  	HTTP string `json:"http,omitempty"`
    58  	RPC  string `json:"rpc,omitempty"`
    59  	Serf string `json:"serf,omitempty"`
    60  }
    61  
    62  // PortsConfig is used to configure the network ports we use.
    63  type PortsConfig struct {
    64  	HTTP int `json:"http,omitempty"`
    65  	RPC  int `json:"rpc,omitempty"`
    66  	Serf int `json:"serf,omitempty"`
    67  }
    68  
    69  // ServerConfig is used to configure the nomad server.
    70  type ServerConfig struct {
    71  	Enabled         bool `json:"enabled"`
    72  	BootstrapExpect int  `json:"bootstrap_expect"`
    73  	RaftProtocol    int  `json:"raft_protocol,omitempty"`
    74  }
    75  
    76  // ClientConfig is used to configure the client
    77  type ClientConfig struct {
    78  	Enabled bool `json:"enabled"`
    79  }
    80  
    81  // VaultConfig is used to configure Vault
    82  type VaultConfig struct {
    83  	Enabled bool `json:"enabled"`
    84  }
    85  
    86  // ACLConfig is used to configure ACLs
    87  type ACLConfig struct {
    88  	Enabled bool `json:"enabled"`
    89  }
    90  
    91  // ServerConfigCallback is a function interface which can be
    92  // passed to NewTestServerConfig to modify the server config.
    93  type ServerConfigCallback func(c *TestServerConfig)
    94  
    95  // defaultServerConfig returns a new TestServerConfig struct
    96  // with all of the listen ports incremented by one.
    97  func defaultServerConfig(t testing.T) *TestServerConfig {
    98  	ports := freeport.GetT(t, 3)
    99  	return &TestServerConfig{
   100  		NodeName:          fmt.Sprintf("node-%d", ports[0]),
   101  		DisableCheckpoint: true,
   102  		LogLevel:          "DEBUG",
   103  		Ports: &PortsConfig{
   104  			HTTP: ports[0],
   105  			RPC:  ports[1],
   106  			Serf: ports[2],
   107  		},
   108  		Server: &ServerConfig{
   109  			Enabled:         true,
   110  			BootstrapExpect: 1,
   111  		},
   112  		Client: &ClientConfig{
   113  			Enabled: false,
   114  		},
   115  		Vault: &VaultConfig{
   116  			Enabled: false,
   117  		},
   118  		ACL: &ACLConfig{
   119  			Enabled: false,
   120  		},
   121  	}
   122  }
   123  
   124  // TestServer is the main server wrapper struct.
   125  type TestServer struct {
   126  	cmd    *exec.Cmd
   127  	Config *TestServerConfig
   128  	t      testing.T
   129  
   130  	HTTPAddr   string
   131  	SerfAddr   string
   132  	HTTPClient *http.Client
   133  }
   134  
   135  // NewTestServer creates a new TestServer, and makes a call to
   136  // an optional callback function to modify the configuration.
   137  func NewTestServer(t testing.T, cb ServerConfigCallback) *TestServer {
   138  	path, err := discover.NomadExecutable()
   139  	if err != nil {
   140  		t.Skipf("nomad not found, skipping: %v", err)
   141  	}
   142  
   143  	// Do a sanity check that we are actually running nomad
   144  	vcmd := exec.Command(path, "-version")
   145  	vcmd.Stdout = nil
   146  	vcmd.Stderr = nil
   147  	if err := vcmd.Run(); err != nil {
   148  		t.Skipf("nomad version failed: %v", err)
   149  	}
   150  
   151  	dataDir, err := ioutil.TempDir("", "nomad")
   152  	if err != nil {
   153  		t.Fatalf("err: %s", err)
   154  	}
   155  
   156  	configFile, err := ioutil.TempFile(dataDir, "nomad")
   157  	if err != nil {
   158  		defer os.RemoveAll(dataDir)
   159  		t.Fatalf("err: %s", err)
   160  	}
   161  	defer configFile.Close()
   162  
   163  	nomadConfig := defaultServerConfig(t)
   164  	nomadConfig.DataDir = dataDir
   165  
   166  	if cb != nil {
   167  		cb(nomadConfig)
   168  	}
   169  
   170  	configContent, err := json.Marshal(nomadConfig)
   171  	if err != nil {
   172  		t.Fatalf("err: %s", err)
   173  	}
   174  
   175  	if _, err := configFile.Write(configContent); err != nil {
   176  		t.Fatalf("err: %s", err)
   177  	}
   178  	configFile.Close()
   179  
   180  	stdout := io.Writer(os.Stdout)
   181  	if nomadConfig.Stdout != nil {
   182  		stdout = nomadConfig.Stdout
   183  	}
   184  
   185  	stderr := io.Writer(os.Stderr)
   186  	if nomadConfig.Stderr != nil {
   187  		stderr = nomadConfig.Stderr
   188  	}
   189  
   190  	args := []string{"agent", "-config", configFile.Name()}
   191  	if nomadConfig.DevMode {
   192  		args = append(args, "-dev")
   193  	}
   194  
   195  	// Start the server
   196  	cmd := exec.Command(path, args...)
   197  	cmd.Stdout = stdout
   198  	cmd.Stderr = stderr
   199  	if err := cmd.Start(); err != nil {
   200  		t.Fatalf("err: %s", err)
   201  	}
   202  
   203  	client := cleanhttp.DefaultClient()
   204  
   205  	server := &TestServer{
   206  		Config: nomadConfig,
   207  		cmd:    cmd,
   208  		t:      t,
   209  
   210  		HTTPAddr:   fmt.Sprintf("127.0.0.1:%d", nomadConfig.Ports.HTTP),
   211  		SerfAddr:   fmt.Sprintf("127.0.0.1:%d", nomadConfig.Ports.Serf),
   212  		HTTPClient: client,
   213  	}
   214  
   215  	// Wait for the server to be ready
   216  	if nomadConfig.Server.Enabled && nomadConfig.Server.BootstrapExpect != 0 {
   217  		server.waitForLeader()
   218  	} else {
   219  		server.waitForAPI()
   220  	}
   221  
   222  	// Wait for the client to be ready
   223  	if nomadConfig.DevMode {
   224  		server.waitForClient()
   225  	}
   226  	return server
   227  }
   228  
   229  // Stop stops the test Nomad server, and removes the Nomad data
   230  // directory once we are done.
   231  func (s *TestServer) Stop() {
   232  	defer os.RemoveAll(s.Config.DataDir)
   233  
   234  	if err := s.cmd.Process.Kill(); err != nil {
   235  		s.t.Errorf("err: %s", err)
   236  	}
   237  
   238  	// wait for the process to exit to be sure that the data dir can be
   239  	// deleted on all platforms.
   240  	s.cmd.Wait()
   241  }
   242  
   243  // waitForAPI waits for only the agent HTTP endpoint to start
   244  // responding. This is an indication that the agent has started,
   245  // but will likely return before a leader is elected.
   246  func (s *TestServer) waitForAPI() {
   247  	WaitForResult(func() (bool, error) {
   248  		// Using this endpoint as it is does not have restricted access
   249  		resp, err := s.HTTPClient.Get(s.url("/v1/metrics"))
   250  		if err != nil {
   251  			return false, err
   252  		}
   253  		defer resp.Body.Close()
   254  		if err := s.requireOK(resp); err != nil {
   255  			return false, err
   256  		}
   257  		return true, nil
   258  	}, func(err error) {
   259  		defer s.Stop()
   260  		s.t.Fatalf("err: %s", err)
   261  	})
   262  }
   263  
   264  // waitForLeader waits for the Nomad server's HTTP API to become
   265  // available, and then waits for a known leader and an index of
   266  // 1 or more to be observed to confirm leader election is done.
   267  func (s *TestServer) waitForLeader() {
   268  	WaitForResult(func() (bool, error) {
   269  		// Query the API and check the status code
   270  		// Using this endpoint as it is does not have restricted access
   271  		resp, err := s.HTTPClient.Get(s.url("/v1/status/leader"))
   272  		if err != nil {
   273  			return false, err
   274  		}
   275  		defer resp.Body.Close()
   276  		if err := s.requireOK(resp); err != nil {
   277  			return false, err
   278  		}
   279  
   280  		return true, nil
   281  	}, func(err error) {
   282  		defer s.Stop()
   283  		s.t.Fatalf("err: %s", err)
   284  	})
   285  }
   286  
   287  // waitForClient waits for the Nomad client to be ready. The function returns
   288  // immediately if the server is not in dev mode.
   289  func (s *TestServer) waitForClient() {
   290  	if !s.Config.DevMode {
   291  		return
   292  	}
   293  
   294  	WaitForResult(func() (bool, error) {
   295  		resp, err := s.HTTPClient.Get(s.url("/v1/nodes"))
   296  		if err != nil {
   297  			return false, err
   298  		}
   299  		defer resp.Body.Close()
   300  		if err := s.requireOK(resp); err != nil {
   301  			return false, err
   302  		}
   303  
   304  		var decoded []struct {
   305  			ID     string
   306  			Status string
   307  		}
   308  
   309  		dec := json.NewDecoder(resp.Body)
   310  		if err := dec.Decode(&decoded); err != nil {
   311  			return false, err
   312  		}
   313  
   314  		if len(decoded) != 1 || decoded[0].Status != "ready" {
   315  			return false, fmt.Errorf("Node not ready: %v", decoded)
   316  		}
   317  
   318  		return true, nil
   319  	}, func(err error) {
   320  		defer s.Stop()
   321  		s.t.Fatalf("err: %s", err)
   322  	})
   323  }
   324  
   325  // url is a helper function which takes a relative URL and
   326  // makes it into a proper URL against the local Nomad server.
   327  func (s *TestServer) url(path string) string {
   328  	return fmt.Sprintf("http://%s%s", s.HTTPAddr, path)
   329  }
   330  
   331  // requireOK checks the HTTP response code and ensures it is acceptable.
   332  func (s *TestServer) requireOK(resp *http.Response) error {
   333  	if resp.StatusCode != 200 {
   334  		return fmt.Errorf("Bad status code: %d", resp.StatusCode)
   335  	}
   336  	return nil
   337  }
   338  
   339  // put performs a new HTTP PUT request.
   340  func (s *TestServer) put(path string, body io.Reader) *http.Response {
   341  	req, err := http.NewRequest("PUT", s.url(path), body)
   342  	if err != nil {
   343  		s.t.Fatalf("err: %s", err)
   344  	}
   345  	resp, err := s.HTTPClient.Do(req)
   346  	if err != nil {
   347  		s.t.Fatalf("err: %s", err)
   348  	}
   349  	if err := s.requireOK(resp); err != nil {
   350  		defer resp.Body.Close()
   351  		s.t.Fatal(err)
   352  	}
   353  	return resp
   354  }
   355  
   356  // get performs a new HTTP GET request.
   357  func (s *TestServer) get(path string) *http.Response {
   358  	resp, err := s.HTTPClient.Get(s.url(path))
   359  	if err != nil {
   360  		s.t.Fatalf("err: %s", err)
   361  	}
   362  	if err := s.requireOK(resp); err != nil {
   363  		defer resp.Body.Close()
   364  		s.t.Fatal(err)
   365  	}
   366  	return resp
   367  }
   368  
   369  // encodePayload returns a new io.Reader wrapping the encoded contents
   370  // of the payload, suitable for passing directly to a new request.
   371  func (s *TestServer) encodePayload(payload interface{}) io.Reader {
   372  	var encoded bytes.Buffer
   373  	enc := json.NewEncoder(&encoded)
   374  	if err := enc.Encode(payload); err != nil {
   375  		s.t.Fatalf("err: %s", err)
   376  	}
   377  	return &encoded
   378  }