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 }