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