github.com/sl1pm4t/consul@v1.4.5-0.20190325224627-74c31c540f9c/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 Consul 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 consul 8 // binary available on the $PATH. 9 // 10 // This package does not use Consul'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 "context" 16 "encoding/json" 17 "fmt" 18 "io" 19 "io/ioutil" 20 "log" 21 "net" 22 "net/http" 23 "os" 24 "os/exec" 25 "path/filepath" 26 "strconv" 27 "strings" 28 "testing" 29 "time" 30 31 "github.com/hashicorp/consul/lib/freeport" 32 "github.com/hashicorp/consul/testutil/retry" 33 "github.com/hashicorp/go-cleanhttp" 34 "github.com/hashicorp/go-uuid" 35 "github.com/pkg/errors" 36 ) 37 38 // TestPerformanceConfig configures the performance parameters. 39 type TestPerformanceConfig struct { 40 RaftMultiplier uint `json:"raft_multiplier,omitempty"` 41 } 42 43 // TestPortConfig configures the various ports used for services 44 // provided by the Consul server. 45 type TestPortConfig struct { 46 DNS int `json:"dns,omitempty"` 47 HTTP int `json:"http,omitempty"` 48 HTTPS int `json:"https,omitempty"` 49 SerfLan int `json:"serf_lan,omitempty"` 50 SerfWan int `json:"serf_wan,omitempty"` 51 Server int `json:"server,omitempty"` 52 ProxyMinPort int `json:"proxy_min_port,omitempty"` 53 ProxyMaxPort int `json:"proxy_max_port,omitempty"` 54 } 55 56 // TestAddressConfig contains the bind addresses for various 57 // components of the Consul server. 58 type TestAddressConfig struct { 59 HTTP string `json:"http,omitempty"` 60 } 61 62 // TestNetworkSegment contains the configuration for a network segment. 63 type TestNetworkSegment struct { 64 Name string `json:"name"` 65 Bind string `json:"bind"` 66 Port int `json:"port"` 67 Advertise string `json:"advertise"` 68 } 69 70 // TestServerConfig is the main server configuration struct. 71 type TestServerConfig struct { 72 NodeName string `json:"node_name"` 73 NodeID string `json:"node_id"` 74 NodeMeta map[string]string `json:"node_meta,omitempty"` 75 Performance *TestPerformanceConfig `json:"performance,omitempty"` 76 Bootstrap bool `json:"bootstrap,omitempty"` 77 Server bool `json:"server,omitempty"` 78 DataDir string `json:"data_dir,omitempty"` 79 Datacenter string `json:"datacenter,omitempty"` 80 Segments []TestNetworkSegment `json:"segments"` 81 DisableCheckpoint bool `json:"disable_update_check"` 82 LogLevel string `json:"log_level,omitempty"` 83 Bind string `json:"bind_addr,omitempty"` 84 Addresses *TestAddressConfig `json:"addresses,omitempty"` 85 Ports *TestPortConfig `json:"ports,omitempty"` 86 RaftProtocol int `json:"raft_protocol,omitempty"` 87 ACLMasterToken string `json:"acl_master_token,omitempty"` 88 ACLDatacenter string `json:"acl_datacenter,omitempty"` 89 PrimaryDatacenter string `json:"primary_datacenter,omitempty"` 90 ACLDefaultPolicy string `json:"acl_default_policy,omitempty"` 91 ACLEnforceVersion8 bool `json:"acl_enforce_version_8"` 92 ACL TestACLs `json:"acl,omitempty"` 93 Encrypt string `json:"encrypt,omitempty"` 94 CAFile string `json:"ca_file,omitempty"` 95 CertFile string `json:"cert_file,omitempty"` 96 KeyFile string `json:"key_file,omitempty"` 97 VerifyIncoming bool `json:"verify_incoming,omitempty"` 98 VerifyIncomingRPC bool `json:"verify_incoming_rpc,omitempty"` 99 VerifyIncomingHTTPS bool `json:"verify_incoming_https,omitempty"` 100 VerifyOutgoing bool `json:"verify_outgoing,omitempty"` 101 EnableScriptChecks bool `json:"enable_script_checks,omitempty"` 102 Connect map[string]interface{} `json:"connect,omitempty"` 103 EnableDebug bool `json:"enable_debug,omitempty"` 104 ReadyTimeout time.Duration `json:"-"` 105 Stdout, Stderr io.Writer `json:"-"` 106 Args []string `json:"-"` 107 } 108 109 type TestACLs struct { 110 Enabled bool `json:"enabled,omitempty"` 111 TokenReplication bool `json:"enable_token_replication,omitempty"` 112 PolicyTTL string `json:"policy_ttl,omitempty"` 113 TokenTTL string `json:"token_ttl,omitempty"` 114 DownPolicy string `json:"down_policy,omitempty"` 115 DefaultPolicy string `json:"default_policy,omitempty"` 116 EnableKeyListPolicy bool `json:"enable_key_list_policy,omitempty"` 117 Tokens TestTokens `json:"tokens,omitempty"` 118 DisabledTTL string `json:"disabled_ttl,omitempty"` 119 } 120 121 type TestTokens struct { 122 Master string `json:"master,omitempty"` 123 Replication string `json:"replication,omitempty"` 124 AgentMaster string `json:"agent_master,omitempty"` 125 Default string `json:"default,omitempty"` 126 Agent string `json:"agent,omitempty"` 127 } 128 129 // ServerConfigCallback is a function interface which can be 130 // passed to NewTestServerConfig to modify the server config. 131 type ServerConfigCallback func(c *TestServerConfig) 132 133 // defaultServerConfig returns a new TestServerConfig struct 134 // with all of the listen ports incremented by one. 135 func defaultServerConfig() *TestServerConfig { 136 nodeID, err := uuid.GenerateUUID() 137 if err != nil { 138 panic(err) 139 } 140 141 ports := freeport.Get(6) 142 return &TestServerConfig{ 143 NodeName: "node-" + nodeID, 144 NodeID: nodeID, 145 DisableCheckpoint: true, 146 Performance: &TestPerformanceConfig{ 147 RaftMultiplier: 1, 148 }, 149 Bootstrap: true, 150 Server: true, 151 LogLevel: "debug", 152 Bind: "127.0.0.1", 153 Addresses: &TestAddressConfig{}, 154 Ports: &TestPortConfig{ 155 DNS: ports[0], 156 HTTP: ports[1], 157 HTTPS: ports[2], 158 SerfLan: ports[3], 159 SerfWan: ports[4], 160 Server: ports[5], 161 }, 162 ReadyTimeout: 10 * time.Second, 163 Connect: map[string]interface{}{ 164 "enabled": true, 165 "ca_config": map[string]interface{}{ 166 // const TestClusterID causes import cycle so hard code it here. 167 "cluster_id": "11111111-2222-3333-4444-555555555555", 168 }, 169 "proxy": map[string]interface{}{ 170 "allow_managed_api_registration": true, 171 }, 172 }, 173 } 174 } 175 176 // TestService is used to serialize a service definition. 177 type TestService struct { 178 ID string `json:",omitempty"` 179 Name string `json:",omitempty"` 180 Tags []string `json:",omitempty"` 181 Address string `json:",omitempty"` 182 Port int `json:",omitempty"` 183 } 184 185 // TestCheck is used to serialize a check definition. 186 type TestCheck struct { 187 ID string `json:",omitempty"` 188 Name string `json:",omitempty"` 189 ServiceID string `json:",omitempty"` 190 TTL string `json:",omitempty"` 191 } 192 193 // TestKVResponse is what we use to decode KV data. 194 type TestKVResponse struct { 195 Value string 196 } 197 198 // TestServer is the main server wrapper struct. 199 type TestServer struct { 200 cmd *exec.Cmd 201 Config *TestServerConfig 202 203 HTTPAddr string 204 HTTPSAddr string 205 LANAddr string 206 WANAddr string 207 208 HTTPClient *http.Client 209 210 tmpdir string 211 } 212 213 // NewTestServer is an easy helper method to create a new Consul 214 // test server with the most basic configuration. 215 func NewTestServer() (*TestServer, error) { 216 return NewTestServerConfigT(nil, nil) 217 } 218 219 func NewTestServerConfig(cb ServerConfigCallback) (*TestServer, error) { 220 return NewTestServerConfigT(nil, cb) 221 } 222 223 // NewTestServerConfig creates a new TestServer, and makes a call to an optional 224 // callback function to modify the configuration. If there is an error 225 // configuring or starting the server, the server will NOT be running when the 226 // function returns (thus you do not need to stop it). 227 func NewTestServerConfigT(t *testing.T, cb ServerConfigCallback) (*TestServer, error) { 228 return newTestServerConfigT(t, cb) 229 } 230 231 // newTestServerConfigT is the internal helper for NewTestServerConfigT. 232 func newTestServerConfigT(t *testing.T, cb ServerConfigCallback) (*TestServer, error) { 233 path, err := exec.LookPath("consul") 234 if err != nil || path == "" { 235 return nil, fmt.Errorf("consul not found on $PATH - download and install " + 236 "consul or skip this test") 237 } 238 239 tmpdir := TempDir(t, "consul") 240 cfg := defaultServerConfig() 241 cfg.DataDir = filepath.Join(tmpdir, "data") 242 if cb != nil { 243 cb(cfg) 244 } 245 246 b, err := json.Marshal(cfg) 247 if err != nil { 248 return nil, errors.Wrap(err, "failed marshaling json") 249 } 250 251 log.Printf("CONFIG JSON: %s", string(b)) 252 configFile := filepath.Join(tmpdir, "config.json") 253 if err := ioutil.WriteFile(configFile, b, 0644); err != nil { 254 defer os.RemoveAll(tmpdir) 255 return nil, errors.Wrap(err, "failed writing config content") 256 } 257 258 stdout := io.Writer(os.Stdout) 259 if cfg.Stdout != nil { 260 stdout = cfg.Stdout 261 } 262 stderr := io.Writer(os.Stderr) 263 if cfg.Stderr != nil { 264 stderr = cfg.Stderr 265 } 266 267 // Start the server 268 args := []string{"agent", "-config-file", configFile} 269 args = append(args, cfg.Args...) 270 cmd := exec.Command("consul", args...) 271 cmd.Stdout = stdout 272 cmd.Stderr = stderr 273 if err := cmd.Start(); err != nil { 274 return nil, errors.Wrap(err, "failed starting command") 275 } 276 277 httpAddr := fmt.Sprintf("127.0.0.1:%d", cfg.Ports.HTTP) 278 client := cleanhttp.DefaultClient() 279 if strings.HasPrefix(cfg.Addresses.HTTP, "unix://") { 280 httpAddr = cfg.Addresses.HTTP 281 tr := cleanhttp.DefaultTransport() 282 tr.DialContext = func(_ context.Context, _, _ string) (net.Conn, error) { 283 return net.Dial("unix", httpAddr[len("unix://"):]) 284 } 285 client = &http.Client{Transport: tr} 286 } 287 288 server := &TestServer{ 289 Config: cfg, 290 cmd: cmd, 291 292 HTTPAddr: httpAddr, 293 HTTPSAddr: fmt.Sprintf("127.0.0.1:%d", cfg.Ports.HTTPS), 294 LANAddr: fmt.Sprintf("127.0.0.1:%d", cfg.Ports.SerfLan), 295 WANAddr: fmt.Sprintf("127.0.0.1:%d", cfg.Ports.SerfWan), 296 297 HTTPClient: client, 298 299 tmpdir: tmpdir, 300 } 301 302 // Wait for the server to be ready 303 if cfg.Bootstrap { 304 err = server.waitForLeader() 305 } else { 306 err = server.waitForAPI() 307 } 308 if err != nil { 309 defer server.Stop() 310 return nil, errors.Wrap(err, "failed waiting for server to start") 311 } 312 return server, nil 313 } 314 315 // Stop stops the test Consul server, and removes the Consul data 316 // directory once we are done. 317 func (s *TestServer) Stop() error { 318 defer os.RemoveAll(s.tmpdir) 319 320 // There was no process 321 if s.cmd == nil { 322 return nil 323 } 324 325 if s.cmd.Process != nil { 326 if err := s.cmd.Process.Signal(os.Interrupt); err != nil { 327 return errors.Wrap(err, "failed to kill consul server") 328 } 329 } 330 331 // wait for the process to exit to be sure that the data dir can be 332 // deleted on all platforms. 333 return s.cmd.Wait() 334 } 335 336 type failer struct { 337 failed bool 338 } 339 340 func (f *failer) Log(args ...interface{}) { fmt.Println(args...) } 341 func (f *failer) FailNow() { f.failed = true } 342 343 // waitForAPI waits for only the agent HTTP endpoint to start 344 // responding. This is an indication that the agent has started, 345 // but will likely return before a leader is elected. 346 func (s *TestServer) waitForAPI() error { 347 f := &failer{} 348 retry.Run(f, func(r *retry.R) { 349 resp, err := s.HTTPClient.Get(s.url("/v1/agent/self")) 350 if err != nil { 351 r.Fatal(err) 352 } 353 defer resp.Body.Close() 354 if err := s.requireOK(resp); err != nil { 355 r.Fatal("failed OK response", err) 356 } 357 }) 358 if f.failed { 359 return errors.New("failed waiting for API") 360 } 361 return nil 362 } 363 364 // waitForLeader waits for the Consul server's HTTP API to become 365 // available, and then waits for a known leader and an index of 366 // 1 or more to be observed to confirm leader election is done. 367 // It then waits to ensure the anti-entropy sync has completed. 368 func (s *TestServer) waitForLeader() error { 369 f := &failer{} 370 timer := &retry.Timer{ 371 Timeout: s.Config.ReadyTimeout, 372 Wait: 250 * time.Millisecond, 373 } 374 var index int64 375 retry.RunWith(timer, f, func(r *retry.R) { 376 // Query the API and check the status code. 377 url := s.url(fmt.Sprintf("/v1/catalog/nodes?index=%d", index)) 378 resp, err := s.HTTPClient.Get(url) 379 if err != nil { 380 r.Fatal("failed http get", err) 381 } 382 defer resp.Body.Close() 383 if err := s.requireOK(resp); err != nil { 384 r.Fatal("failed OK response", err) 385 } 386 387 // Ensure we have a leader and a node registration. 388 if leader := resp.Header.Get("X-Consul-KnownLeader"); leader != "true" { 389 r.Fatalf("Consul leader status: %#v", leader) 390 } 391 index, err = strconv.ParseInt(resp.Header.Get("X-Consul-Index"), 10, 64) 392 if err != nil { 393 r.Fatal("bad consul index", err) 394 } 395 if index == 0 { 396 r.Fatal("consul index is 0") 397 } 398 399 // Watch for the anti-entropy sync to finish. 400 var v []map[string]interface{} 401 dec := json.NewDecoder(resp.Body) 402 if err := dec.Decode(&v); err != nil { 403 r.Fatal(err) 404 } 405 if len(v) < 1 { 406 r.Fatal("No nodes") 407 } 408 taggedAddresses, ok := v[0]["TaggedAddresses"].(map[string]interface{}) 409 if !ok { 410 r.Fatal("Missing tagged addresses") 411 } 412 if _, ok := taggedAddresses["lan"]; !ok { 413 r.Fatal("No lan tagged addresses") 414 } 415 }) 416 if f.failed { 417 return errors.New("failed waiting for leader") 418 } 419 return nil 420 } 421 422 // WaitForSerfCheck ensures we have a node with serfHealth check registered 423 // Behavior mirrors testrpc.WaitForTestAgent but avoids the dependency cycle in api pkg 424 func (s *TestServer) WaitForSerfCheck(t *testing.T) { 425 retry.Run(t, func(r *retry.R) { 426 // Query the API and check the status code. 427 url := s.url("/v1/catalog/nodes?index=0") 428 resp, err := s.HTTPClient.Get(url) 429 if err != nil { 430 r.Fatal("failed http get", err) 431 } 432 defer resp.Body.Close() 433 if err := s.requireOK(resp); err != nil { 434 r.Fatal("failed OK response", err) 435 } 436 437 // Watch for the anti-entropy sync to finish. 438 var payload []map[string]interface{} 439 dec := json.NewDecoder(resp.Body) 440 if err := dec.Decode(&payload); err != nil { 441 r.Fatal(err) 442 } 443 if len(payload) < 1 { 444 r.Fatal("No nodes") 445 } 446 447 // Ensure the serfHealth check is registered 448 url = s.url(fmt.Sprintf("/v1/health/node/%s", payload[0]["Node"])) 449 resp, err = s.HTTPClient.Get(url) 450 if err != nil { 451 r.Fatal("failed http get", err) 452 } 453 defer resp.Body.Close() 454 if err := s.requireOK(resp); err != nil { 455 r.Fatal("failed OK response", err) 456 } 457 dec = json.NewDecoder(resp.Body) 458 if err = dec.Decode(&payload); err != nil { 459 r.Fatal(err) 460 } 461 462 var found bool 463 for _, check := range payload { 464 if check["CheckID"].(string) == "serfHealth" { 465 found = true 466 break 467 } 468 } 469 if !found { 470 r.Fatal("missing serfHealth registration") 471 } 472 }) 473 }