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 }