github.com/jrxfive/nomad@v0.6.1-0.20170802162750-1fef470e89bf/command/agent/http_test.go (about) 1 package agent 2 3 import ( 4 "bytes" 5 "crypto/tls" 6 "crypto/x509" 7 "encoding/json" 8 "fmt" 9 "io" 10 "io/ioutil" 11 "net" 12 "net/http" 13 "net/http/httptest" 14 "net/url" 15 "strconv" 16 "testing" 17 "time" 18 19 "github.com/hashicorp/nomad/nomad/mock" 20 "github.com/hashicorp/nomad/nomad/structs" 21 "github.com/hashicorp/nomad/nomad/structs/config" 22 "github.com/hashicorp/nomad/testutil" 23 ) 24 25 // makeHTTPServer returns a test server whose logs will be written to 26 // the passed writer. If the writer is nil, the logs are written to stderr. 27 func makeHTTPServer(t testing.TB, cb func(c *Config)) *TestAgent { 28 return NewTestAgent(t.Name(), cb) 29 } 30 31 func BenchmarkHTTPRequests(b *testing.B) { 32 s := makeHTTPServer(b, func(c *Config) { 33 c.Client.Enabled = false 34 }) 35 defer s.Shutdown() 36 37 job := mock.Job() 38 var allocs []*structs.Allocation 39 count := 1000 40 for i := 0; i < count; i++ { 41 alloc := mock.Alloc() 42 alloc.Job = job 43 alloc.JobID = job.ID 44 alloc.Name = fmt.Sprintf("my-job.web[%d]", i) 45 allocs = append(allocs, alloc) 46 } 47 48 handler := func(resp http.ResponseWriter, req *http.Request) (interface{}, error) { 49 return allocs[:count], nil 50 } 51 b.ResetTimer() 52 53 b.RunParallel(func(pb *testing.PB) { 54 for pb.Next() { 55 resp := httptest.NewRecorder() 56 req, _ := http.NewRequest("GET", "/v1/kv/key", nil) 57 s.Server.wrap(handler)(resp, req) 58 } 59 }) 60 } 61 62 func TestSetIndex(t *testing.T) { 63 t.Parallel() 64 resp := httptest.NewRecorder() 65 setIndex(resp, 1000) 66 header := resp.Header().Get("X-Nomad-Index") 67 if header != "1000" { 68 t.Fatalf("Bad: %v", header) 69 } 70 setIndex(resp, 2000) 71 if v := resp.Header()["X-Nomad-Index"]; len(v) != 1 { 72 t.Fatalf("bad: %#v", v) 73 } 74 } 75 76 func TestSetKnownLeader(t *testing.T) { 77 t.Parallel() 78 resp := httptest.NewRecorder() 79 setKnownLeader(resp, true) 80 header := resp.Header().Get("X-Nomad-KnownLeader") 81 if header != "true" { 82 t.Fatalf("Bad: %v", header) 83 } 84 resp = httptest.NewRecorder() 85 setKnownLeader(resp, false) 86 header = resp.Header().Get("X-Nomad-KnownLeader") 87 if header != "false" { 88 t.Fatalf("Bad: %v", header) 89 } 90 } 91 92 func TestSetLastContact(t *testing.T) { 93 t.Parallel() 94 resp := httptest.NewRecorder() 95 setLastContact(resp, 123456*time.Microsecond) 96 header := resp.Header().Get("X-Nomad-LastContact") 97 if header != "123" { 98 t.Fatalf("Bad: %v", header) 99 } 100 } 101 102 func TestSetMeta(t *testing.T) { 103 t.Parallel() 104 meta := structs.QueryMeta{ 105 Index: 1000, 106 KnownLeader: true, 107 LastContact: 123456 * time.Microsecond, 108 } 109 resp := httptest.NewRecorder() 110 setMeta(resp, &meta) 111 header := resp.Header().Get("X-Nomad-Index") 112 if header != "1000" { 113 t.Fatalf("Bad: %v", header) 114 } 115 header = resp.Header().Get("X-Nomad-KnownLeader") 116 if header != "true" { 117 t.Fatalf("Bad: %v", header) 118 } 119 header = resp.Header().Get("X-Nomad-LastContact") 120 if header != "123" { 121 t.Fatalf("Bad: %v", header) 122 } 123 } 124 125 func TestSetHeaders(t *testing.T) { 126 t.Parallel() 127 s := makeHTTPServer(t, nil) 128 s.Agent.config.HTTPAPIResponseHeaders = map[string]string{"foo": "bar"} 129 defer s.Shutdown() 130 131 resp := httptest.NewRecorder() 132 handler := func(resp http.ResponseWriter, req *http.Request) (interface{}, error) { 133 return &structs.Job{Name: "foo"}, nil 134 } 135 136 req, _ := http.NewRequest("GET", "/v1/kv/key", nil) 137 s.Server.wrap(handler)(resp, req) 138 header := resp.Header().Get("foo") 139 140 if header != "bar" { 141 t.Fatalf("expected header: %v, actual: %v", "bar", header) 142 } 143 144 } 145 146 func TestContentTypeIsJSON(t *testing.T) { 147 t.Parallel() 148 s := makeHTTPServer(t, nil) 149 defer s.Shutdown() 150 151 resp := httptest.NewRecorder() 152 153 handler := func(resp http.ResponseWriter, req *http.Request) (interface{}, error) { 154 return &structs.Job{Name: "foo"}, nil 155 } 156 157 req, _ := http.NewRequest("GET", "/v1/kv/key", nil) 158 s.Server.wrap(handler)(resp, req) 159 160 contentType := resp.Header().Get("Content-Type") 161 162 if contentType != "application/json" { 163 t.Fatalf("Content-Type header was not 'application/json'") 164 } 165 } 166 167 func TestPrettyPrint(t *testing.T) { 168 t.Parallel() 169 testPrettyPrint("pretty=1", true, t) 170 } 171 172 func TestPrettyPrintOff(t *testing.T) { 173 t.Parallel() 174 testPrettyPrint("pretty=0", false, t) 175 } 176 177 func TestPrettyPrintBare(t *testing.T) { 178 t.Parallel() 179 testPrettyPrint("pretty", true, t) 180 } 181 182 func testPrettyPrint(pretty string, prettyFmt bool, t *testing.T) { 183 s := makeHTTPServer(t, nil) 184 defer s.Shutdown() 185 186 r := &structs.Job{Name: "foo"} 187 188 resp := httptest.NewRecorder() 189 handler := func(resp http.ResponseWriter, req *http.Request) (interface{}, error) { 190 return r, nil 191 } 192 193 urlStr := "/v1/job/foo?" + pretty 194 req, _ := http.NewRequest("GET", urlStr, nil) 195 s.Server.wrap(handler)(resp, req) 196 197 var expected []byte 198 if prettyFmt { 199 expected, _ = json.MarshalIndent(r, "", " ") 200 expected = append(expected, "\n"...) 201 } else { 202 expected, _ = json.Marshal(r) 203 } 204 actual, err := ioutil.ReadAll(resp.Body) 205 if err != nil { 206 t.Fatalf("err: %s", err) 207 } 208 209 if !bytes.Equal(expected, actual) { 210 t.Fatalf("bad:\nexpected:\t%q\nactual:\t\t%q", string(expected), string(actual)) 211 } 212 } 213 214 func TestParseWait(t *testing.T) { 215 t.Parallel() 216 resp := httptest.NewRecorder() 217 var b structs.QueryOptions 218 219 req, err := http.NewRequest("GET", 220 "/v1/catalog/nodes?wait=60s&index=1000", nil) 221 if err != nil { 222 t.Fatalf("err: %v", err) 223 } 224 225 if d := parseWait(resp, req, &b); d { 226 t.Fatalf("unexpected done") 227 } 228 229 if b.MinQueryIndex != 1000 { 230 t.Fatalf("Bad: %v", b) 231 } 232 if b.MaxQueryTime != 60*time.Second { 233 t.Fatalf("Bad: %v", b) 234 } 235 } 236 237 func TestParseWait_InvalidTime(t *testing.T) { 238 t.Parallel() 239 resp := httptest.NewRecorder() 240 var b structs.QueryOptions 241 242 req, err := http.NewRequest("GET", 243 "/v1/catalog/nodes?wait=60foo&index=1000", nil) 244 if err != nil { 245 t.Fatalf("err: %v", err) 246 } 247 248 if d := parseWait(resp, req, &b); !d { 249 t.Fatalf("expected done") 250 } 251 252 if resp.Code != 400 { 253 t.Fatalf("bad code: %v", resp.Code) 254 } 255 } 256 257 func TestParseWait_InvalidIndex(t *testing.T) { 258 t.Parallel() 259 resp := httptest.NewRecorder() 260 var b structs.QueryOptions 261 262 req, err := http.NewRequest("GET", 263 "/v1/catalog/nodes?wait=60s&index=foo", nil) 264 if err != nil { 265 t.Fatalf("err: %v", err) 266 } 267 268 if d := parseWait(resp, req, &b); !d { 269 t.Fatalf("expected done") 270 } 271 272 if resp.Code != 400 { 273 t.Fatalf("bad code: %v", resp.Code) 274 } 275 } 276 277 func TestParseConsistency(t *testing.T) { 278 t.Parallel() 279 var b structs.QueryOptions 280 281 req, err := http.NewRequest("GET", 282 "/v1/catalog/nodes?stale", nil) 283 if err != nil { 284 t.Fatalf("err: %v", err) 285 } 286 287 parseConsistency(req, &b) 288 if !b.AllowStale { 289 t.Fatalf("Bad: %v", b) 290 } 291 292 b = structs.QueryOptions{} 293 req, err = http.NewRequest("GET", 294 "/v1/catalog/nodes?consistent", nil) 295 if err != nil { 296 t.Fatalf("err: %v", err) 297 } 298 299 parseConsistency(req, &b) 300 if b.AllowStale { 301 t.Fatalf("Bad: %v", b) 302 } 303 } 304 305 func TestParseRegion(t *testing.T) { 306 t.Parallel() 307 s := makeHTTPServer(t, nil) 308 defer s.Shutdown() 309 310 req, err := http.NewRequest("GET", 311 "/v1/jobs?region=foo", nil) 312 if err != nil { 313 t.Fatalf("err: %v", err) 314 } 315 316 var region string 317 s.Server.parseRegion(req, ®ion) 318 if region != "foo" { 319 t.Fatalf("bad %s", region) 320 } 321 322 region = "" 323 req, err = http.NewRequest("GET", "/v1/jobs", nil) 324 if err != nil { 325 t.Fatalf("err: %v", err) 326 } 327 328 s.Server.parseRegion(req, ®ion) 329 if region != "global" { 330 t.Fatalf("bad %s", region) 331 } 332 } 333 334 // TestHTTP_VerifyHTTPSClient asserts that a client certificate signed by the 335 // appropriate CA is required when VerifyHTTPSClient=true. 336 func TestHTTP_VerifyHTTPSClient(t *testing.T) { 337 t.Parallel() 338 const ( 339 cafile = "../../helper/tlsutil/testdata/ca.pem" 340 foocert = "../../helper/tlsutil/testdata/nomad-foo.pem" 341 fookey = "../../helper/tlsutil/testdata/nomad-foo-key.pem" 342 ) 343 s := makeHTTPServer(t, func(c *Config) { 344 c.Region = "foo" // match the region on foocert 345 c.TLSConfig = &config.TLSConfig{ 346 EnableHTTP: true, 347 VerifyHTTPSClient: true, 348 CAFile: cafile, 349 CertFile: foocert, 350 KeyFile: fookey, 351 } 352 }) 353 defer s.Shutdown() 354 355 reqURL := fmt.Sprintf("https://%s/v1/agent/self", s.Agent.config.AdvertiseAddrs.HTTP) 356 357 // FAIL: Requests that expect 127.0.0.1 as the name should fail 358 resp, err := http.Get(reqURL) 359 if err == nil { 360 resp.Body.Close() 361 t.Fatalf("expected non-nil error but received: %v", resp.StatusCode) 362 } 363 urlErr, ok := err.(*url.Error) 364 if !ok { 365 t.Fatalf("expected a *url.Error but received: %T -> %v", err, err) 366 } 367 hostErr, ok := urlErr.Err.(x509.HostnameError) 368 if !ok { 369 t.Fatalf("expected a x509.HostnameError but received: %T -> %v", urlErr.Err, urlErr.Err) 370 } 371 if expected := "127.0.0.1"; hostErr.Host != expected { 372 t.Fatalf("expected hostname on error to be %q but found %q", expected, hostErr.Host) 373 } 374 375 // FAIL: Requests that specify a valid hostname but not the CA should 376 // fail 377 tlsConf := &tls.Config{ 378 ServerName: "client.regionFoo.nomad", 379 } 380 transport := &http.Transport{TLSClientConfig: tlsConf} 381 client := &http.Client{Transport: transport} 382 req, err := http.NewRequest("GET", reqURL, nil) 383 if err != nil { 384 t.Fatalf("error creating request: %v", err) 385 } 386 resp, err = client.Do(req) 387 if err == nil { 388 resp.Body.Close() 389 t.Fatalf("expected non-nil error but received: %v", resp.StatusCode) 390 } 391 urlErr, ok = err.(*url.Error) 392 if !ok { 393 t.Fatalf("expected a *url.Error but received: %T -> %v", err, err) 394 } 395 _, ok = urlErr.Err.(x509.UnknownAuthorityError) 396 if !ok { 397 t.Fatalf("expected a x509.UnknownAuthorityError but received: %T -> %v", urlErr.Err, urlErr.Err) 398 } 399 400 // FAIL: Requests that specify a valid hostname and CA cert but lack a 401 // client certificate should fail 402 cacertBytes, err := ioutil.ReadFile(cafile) 403 if err != nil { 404 t.Fatalf("error reading cacert: %v", err) 405 } 406 tlsConf.RootCAs = x509.NewCertPool() 407 tlsConf.RootCAs.AppendCertsFromPEM(cacertBytes) 408 req, err = http.NewRequest("GET", reqURL, nil) 409 if err != nil { 410 t.Fatalf("error creating request: %v", err) 411 } 412 resp, err = client.Do(req) 413 if err == nil { 414 resp.Body.Close() 415 t.Fatalf("expected non-nil error but received: %v", resp.StatusCode) 416 } 417 urlErr, ok = err.(*url.Error) 418 if !ok { 419 t.Fatalf("expected a *url.Error but received: %T -> %v", err, err) 420 } 421 opErr, ok := urlErr.Err.(*net.OpError) 422 if !ok { 423 t.Fatalf("expected a *net.OpErr but received: %T -> %v", urlErr.Err, urlErr.Err) 424 } 425 const badCertificate = "tls: bad certificate" // from crypto/tls/alert.go:52 and RFC 5246 ยง A.3 426 if opErr.Err.Error() != badCertificate { 427 t.Fatalf("expected tls.alert bad_certificate but received: %q", opErr.Err.Error()) 428 } 429 430 // PASS: Requests that specify a valid hostname, CA cert, and client 431 // certificate succeed. 432 tlsConf.GetClientCertificate = func(*tls.CertificateRequestInfo) (*tls.Certificate, error) { 433 c, err := tls.LoadX509KeyPair(foocert, fookey) 434 if err != nil { 435 return nil, err 436 } 437 return &c, nil 438 } 439 transport = &http.Transport{TLSClientConfig: tlsConf} 440 client = &http.Client{Transport: transport} 441 req, err = http.NewRequest("GET", reqURL, nil) 442 if err != nil { 443 t.Fatalf("error creating request: %v", err) 444 } 445 resp, err = client.Do(req) 446 if err != nil { 447 t.Fatalf("unexpected error: %v", err) 448 } 449 resp.Body.Close() 450 if resp.StatusCode != 200 { 451 t.Fatalf("expected 200 status code but got: %d", resp.StatusCode) 452 } 453 } 454 455 // assertIndex tests that X-Nomad-Index is set and non-zero 456 func assertIndex(t *testing.T, resp *httptest.ResponseRecorder) { 457 header := resp.Header().Get("X-Nomad-Index") 458 if header == "" || header == "0" { 459 t.Fatalf("Bad: %v", header) 460 } 461 } 462 463 // checkIndex is like assertIndex but returns an error 464 func checkIndex(resp *httptest.ResponseRecorder) error { 465 header := resp.Header().Get("X-Nomad-Index") 466 if header == "" || header == "0" { 467 return fmt.Errorf("Bad: %v", header) 468 } 469 return nil 470 } 471 472 // getIndex parses X-Nomad-Index 473 func getIndex(t *testing.T, resp *httptest.ResponseRecorder) uint64 { 474 header := resp.Header().Get("X-Nomad-Index") 475 if header == "" { 476 t.Fatalf("Bad: %v", header) 477 } 478 val, err := strconv.Atoi(header) 479 if err != nil { 480 t.Fatalf("Bad: %v", header) 481 } 482 return uint64(val) 483 } 484 485 func httpTest(t testing.TB, cb func(c *Config), f func(srv *TestAgent)) { 486 s := makeHTTPServer(t, cb) 487 defer s.Shutdown() 488 testutil.WaitForLeader(t, s.Agent.RPC) 489 f(s) 490 } 491 492 func encodeReq(obj interface{}) io.ReadCloser { 493 buf := bytes.NewBuffer(nil) 494 enc := json.NewEncoder(buf) 495 enc.Encode(obj) 496 return ioutil.NopCloser(buf) 497 }