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