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