github.com/taylorchu/nomad@v0.5.3-rc1.0.20170407200202-db11e7dd7b55/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 "github.com/hashicorp/nomad/helper/discover" 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 dataDir, err := ioutil.TempDir("", "nomad") 136 if err != nil { 137 t.Fatalf("err: %s", err) 138 } 139 140 configFile, err := ioutil.TempFile(dataDir, "nomad") 141 if err != nil { 142 defer os.RemoveAll(dataDir) 143 t.Fatalf("err: %s", err) 144 } 145 defer configFile.Close() 146 147 nomadConfig := defaultServerConfig() 148 nomadConfig.DataDir = dataDir 149 150 if cb != nil { 151 cb(nomadConfig) 152 } 153 154 configContent, err := json.Marshal(nomadConfig) 155 if err != nil { 156 t.Fatalf("err: %s", err) 157 } 158 159 if _, err := configFile.Write(configContent); err != nil { 160 t.Fatalf("err: %s", err) 161 } 162 configFile.Close() 163 164 stdout := io.Writer(os.Stdout) 165 if nomadConfig.Stdout != nil { 166 stdout = nomadConfig.Stdout 167 } 168 169 stderr := io.Writer(os.Stderr) 170 if nomadConfig.Stderr != nil { 171 stderr = nomadConfig.Stderr 172 } 173 174 args := []string{"agent", "-config", configFile.Name()} 175 if nomadConfig.DevMode { 176 args = append(args, "-dev") 177 } 178 179 // Start the server 180 cmd := exec.Command(path, args...) 181 cmd.Stdout = stdout 182 cmd.Stderr = stderr 183 if err := cmd.Start(); err != nil { 184 t.Fatalf("err: %s", err) 185 } 186 187 client := cleanhttp.DefaultClient() 188 189 server := &TestServer{ 190 Config: nomadConfig, 191 cmd: cmd, 192 t: t, 193 194 HTTPAddr: fmt.Sprintf("127.0.0.1:%d", nomadConfig.Ports.HTTP), 195 SerfAddr: fmt.Sprintf("127.0.0.1:%d", nomadConfig.Ports.Serf), 196 HTTPClient: client, 197 } 198 199 // Wait for the server to be ready 200 if nomadConfig.Server.Enabled && nomadConfig.Server.BootstrapExpect != 0 { 201 server.waitForLeader() 202 } else { 203 server.waitForAPI() 204 } 205 206 // Wait for the client to be ready 207 if nomadConfig.DevMode { 208 server.waitForClient() 209 } 210 return server 211 } 212 213 // Stop stops the test Nomad server, and removes the Nomad data 214 // directory once we are done. 215 func (s *TestServer) Stop() { 216 defer os.RemoveAll(s.Config.DataDir) 217 218 if err := s.cmd.Process.Kill(); err != nil { 219 s.t.Errorf("err: %s", err) 220 } 221 222 // wait for the process to exit to be sure that the data dir can be 223 // deleted on all platforms. 224 s.cmd.Wait() 225 } 226 227 // waitForAPI waits for only the agent HTTP endpoint to start 228 // responding. This is an indication that the agent has started, 229 // but will likely return before a leader is elected. 230 func (s *TestServer) waitForAPI() { 231 WaitForResult(func() (bool, error) { 232 resp, err := s.HTTPClient.Get(s.url("/v1/agent/self")) 233 if err != nil { 234 return false, err 235 } 236 defer resp.Body.Close() 237 if err := s.requireOK(resp); err != nil { 238 return false, err 239 } 240 return true, nil 241 }, func(err error) { 242 defer s.Stop() 243 s.t.Fatalf("err: %s", err) 244 }) 245 } 246 247 // waitForLeader waits for the Nomad server's HTTP API to become 248 // available, and then waits for a known leader and an index of 249 // 1 or more to be observed to confirm leader election is done. 250 func (s *TestServer) waitForLeader() { 251 WaitForResult(func() (bool, error) { 252 // Query the API and check the status code 253 resp, err := s.HTTPClient.Get(s.url("/v1/jobs")) 254 if err != nil { 255 return false, err 256 } 257 defer resp.Body.Close() 258 if err := s.requireOK(resp); err != nil { 259 return false, err 260 } 261 262 // Ensure we have a leader and a node registeration 263 if leader := resp.Header.Get("X-Nomad-KnownLeader"); leader != "true" { 264 return false, fmt.Errorf("Nomad leader status: %#v", leader) 265 } 266 return true, nil 267 }, func(err error) { 268 defer s.Stop() 269 s.t.Fatalf("err: %s", err) 270 }) 271 } 272 273 // waitForClient waits for the Nomad client to be ready. The function returns 274 // immediately if the server is not in dev mode. 275 func (s *TestServer) waitForClient() { 276 if !s.Config.DevMode { 277 return 278 } 279 280 WaitForResult(func() (bool, error) { 281 resp, err := s.HTTPClient.Get(s.url("/v1/nodes")) 282 if err != nil { 283 return false, err 284 } 285 defer resp.Body.Close() 286 if err := s.requireOK(resp); err != nil { 287 return false, err 288 } 289 290 var decoded []struct { 291 ID string 292 Status string 293 } 294 295 dec := json.NewDecoder(resp.Body) 296 if err := dec.Decode(&decoded); err != nil { 297 return false, err 298 } 299 300 if len(decoded) != 1 || decoded[0].Status != "ready" { 301 return false, fmt.Errorf("Node not ready: %v", decoded) 302 } 303 304 return true, nil 305 }, func(err error) { 306 defer s.Stop() 307 s.t.Fatalf("err: %s", err) 308 }) 309 } 310 311 // url is a helper function which takes a relative URL and 312 // makes it into a proper URL against the local Nomad server. 313 func (s *TestServer) url(path string) string { 314 return fmt.Sprintf("http://%s%s", s.HTTPAddr, path) 315 } 316 317 // requireOK checks the HTTP response code and ensures it is acceptable. 318 func (s *TestServer) requireOK(resp *http.Response) error { 319 if resp.StatusCode != 200 { 320 return fmt.Errorf("Bad status code: %d", resp.StatusCode) 321 } 322 return nil 323 } 324 325 // put performs a new HTTP PUT request. 326 func (s *TestServer) put(path string, body io.Reader) *http.Response { 327 req, err := http.NewRequest("PUT", s.url(path), body) 328 if err != nil { 329 s.t.Fatalf("err: %s", err) 330 } 331 resp, err := s.HTTPClient.Do(req) 332 if err != nil { 333 s.t.Fatalf("err: %s", err) 334 } 335 if err := s.requireOK(resp); err != nil { 336 defer resp.Body.Close() 337 s.t.Fatal(err) 338 } 339 return resp 340 } 341 342 // get performs a new HTTP GET request. 343 func (s *TestServer) get(path string) *http.Response { 344 resp, err := s.HTTPClient.Get(s.url(path)) 345 if err != nil { 346 s.t.Fatalf("err: %s", err) 347 } 348 if err := s.requireOK(resp); err != nil { 349 defer resp.Body.Close() 350 s.t.Fatal(err) 351 } 352 return resp 353 } 354 355 // encodePayload returns a new io.Reader wrapping the encoded contents 356 // of the payload, suitable for passing directly to a new request. 357 func (s *TestServer) encodePayload(payload interface{}) io.Reader { 358 var encoded bytes.Buffer 359 enc := json.NewEncoder(&encoded) 360 if err := enc.Encode(payload); err != nil { 361 s.t.Fatalf("err: %s", err) 362 } 363 return &encoded 364 }