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