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 }