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