github.com/djenriquez/nomad-1@v0.8.1/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 "github.com/stretchr/testify/assert" 24 "github.com/ugorji/go/codec" 25 ) 26 27 // makeHTTPServer returns a test server whose logs will be written to 28 // the passed writer. If the writer is nil, the logs are written to stderr. 29 func makeHTTPServer(t testing.TB, cb func(c *Config)) *TestAgent { 30 return NewTestAgent(t, t.Name(), cb) 31 } 32 33 func BenchmarkHTTPRequests(b *testing.B) { 34 s := makeHTTPServer(b, func(c *Config) { 35 c.Client.Enabled = false 36 }) 37 defer s.Shutdown() 38 39 job := mock.Job() 40 var allocs []*structs.Allocation 41 count := 1000 42 for i := 0; i < count; i++ { 43 alloc := mock.Alloc() 44 alloc.Job = job 45 alloc.JobID = job.ID 46 alloc.Name = fmt.Sprintf("my-job.web[%d]", i) 47 allocs = append(allocs, alloc) 48 } 49 50 handler := func(resp http.ResponseWriter, req *http.Request) (interface{}, error) { 51 return allocs[:count], nil 52 } 53 b.ResetTimer() 54 55 b.RunParallel(func(pb *testing.PB) { 56 for pb.Next() { 57 resp := httptest.NewRecorder() 58 req, _ := http.NewRequest("GET", "/v1/kv/key", nil) 59 s.Server.wrap(handler)(resp, req) 60 } 61 }) 62 } 63 64 func TestSetIndex(t *testing.T) { 65 t.Parallel() 66 resp := httptest.NewRecorder() 67 setIndex(resp, 1000) 68 header := resp.Header().Get("X-Nomad-Index") 69 if header != "1000" { 70 t.Fatalf("Bad: %v", header) 71 } 72 setIndex(resp, 2000) 73 if v := resp.Header()["X-Nomad-Index"]; len(v) != 1 { 74 t.Fatalf("bad: %#v", v) 75 } 76 } 77 78 func TestSetKnownLeader(t *testing.T) { 79 t.Parallel() 80 resp := httptest.NewRecorder() 81 setKnownLeader(resp, true) 82 header := resp.Header().Get("X-Nomad-KnownLeader") 83 if header != "true" { 84 t.Fatalf("Bad: %v", header) 85 } 86 resp = httptest.NewRecorder() 87 setKnownLeader(resp, false) 88 header = resp.Header().Get("X-Nomad-KnownLeader") 89 if header != "false" { 90 t.Fatalf("Bad: %v", header) 91 } 92 } 93 94 func TestSetLastContact(t *testing.T) { 95 t.Parallel() 96 resp := httptest.NewRecorder() 97 setLastContact(resp, 123456*time.Microsecond) 98 header := resp.Header().Get("X-Nomad-LastContact") 99 if header != "123" { 100 t.Fatalf("Bad: %v", header) 101 } 102 } 103 104 func TestSetMeta(t *testing.T) { 105 t.Parallel() 106 meta := structs.QueryMeta{ 107 Index: 1000, 108 KnownLeader: true, 109 LastContact: 123456 * time.Microsecond, 110 } 111 resp := httptest.NewRecorder() 112 setMeta(resp, &meta) 113 header := resp.Header().Get("X-Nomad-Index") 114 if header != "1000" { 115 t.Fatalf("Bad: %v", header) 116 } 117 header = resp.Header().Get("X-Nomad-KnownLeader") 118 if header != "true" { 119 t.Fatalf("Bad: %v", header) 120 } 121 header = resp.Header().Get("X-Nomad-LastContact") 122 if header != "123" { 123 t.Fatalf("Bad: %v", header) 124 } 125 } 126 127 func TestSetHeaders(t *testing.T) { 128 t.Parallel() 129 s := makeHTTPServer(t, nil) 130 s.Agent.config.HTTPAPIResponseHeaders = map[string]string{"foo": "bar"} 131 defer s.Shutdown() 132 133 resp := httptest.NewRecorder() 134 handler := func(resp http.ResponseWriter, req *http.Request) (interface{}, error) { 135 return &structs.Job{Name: "foo"}, nil 136 } 137 138 req, _ := http.NewRequest("GET", "/v1/kv/key", nil) 139 s.Server.wrap(handler)(resp, req) 140 header := resp.Header().Get("foo") 141 142 if header != "bar" { 143 t.Fatalf("expected header: %v, actual: %v", "bar", header) 144 } 145 146 } 147 148 func TestContentTypeIsJSON(t *testing.T) { 149 t.Parallel() 150 s := makeHTTPServer(t, nil) 151 defer s.Shutdown() 152 153 resp := httptest.NewRecorder() 154 155 handler := func(resp http.ResponseWriter, req *http.Request) (interface{}, error) { 156 return &structs.Job{Name: "foo"}, nil 157 } 158 159 req, _ := http.NewRequest("GET", "/v1/kv/key", nil) 160 s.Server.wrap(handler)(resp, req) 161 162 contentType := resp.Header().Get("Content-Type") 163 164 if contentType != "application/json" { 165 t.Fatalf("Content-Type header was not 'application/json'") 166 } 167 } 168 169 func TestPrettyPrint(t *testing.T) { 170 t.Parallel() 171 testPrettyPrint("pretty=1", true, t) 172 } 173 174 func TestPrettyPrintOff(t *testing.T) { 175 t.Parallel() 176 testPrettyPrint("pretty=0", false, t) 177 } 178 179 func TestPrettyPrintBare(t *testing.T) { 180 t.Parallel() 181 testPrettyPrint("pretty", true, t) 182 } 183 184 func testPrettyPrint(pretty string, prettyFmt bool, t *testing.T) { 185 s := makeHTTPServer(t, nil) 186 defer s.Shutdown() 187 188 r := &structs.Job{Name: "foo"} 189 190 resp := httptest.NewRecorder() 191 handler := func(resp http.ResponseWriter, req *http.Request) (interface{}, error) { 192 return r, nil 193 } 194 195 urlStr := "/v1/job/foo?" + pretty 196 req, _ := http.NewRequest("GET", urlStr, nil) 197 s.Server.wrap(handler)(resp, req) 198 199 var expected bytes.Buffer 200 var err error 201 if prettyFmt { 202 enc := codec.NewEncoder(&expected, structs.JsonHandlePretty) 203 err = enc.Encode(r) 204 expected.WriteByte('\n') 205 } else { 206 enc := codec.NewEncoder(&expected, structs.JsonHandle) 207 err = enc.Encode(r) 208 } 209 if err != nil { 210 t.Fatalf("failed to encode: %v", err) 211 } 212 actual, err := ioutil.ReadAll(resp.Body) 213 if err != nil { 214 t.Fatalf("err: %s", err) 215 } 216 217 if !bytes.Equal(expected.Bytes(), actual) { 218 t.Fatalf("bad:\nexpected:\t%q\nactual:\t\t%q", expected.String(), string(actual)) 219 } 220 } 221 222 func TestPermissionDenied(t *testing.T) { 223 s := makeHTTPServer(t, func(c *Config) { 224 c.ACL.Enabled = true 225 }) 226 defer s.Shutdown() 227 228 { 229 resp := httptest.NewRecorder() 230 handler := func(resp http.ResponseWriter, req *http.Request) (interface{}, error) { 231 return nil, structs.ErrPermissionDenied 232 } 233 234 req, _ := http.NewRequest("GET", "/v1/job/foo", nil) 235 s.Server.wrap(handler)(resp, req) 236 assert.Equal(t, resp.Code, 403) 237 } 238 239 // When remote RPC is used the errors have "rpc error: " prependend 240 { 241 resp := httptest.NewRecorder() 242 handler := func(resp http.ResponseWriter, req *http.Request) (interface{}, error) { 243 return nil, fmt.Errorf("rpc error: %v", structs.ErrPermissionDenied) 244 } 245 246 req, _ := http.NewRequest("GET", "/v1/job/foo", nil) 247 s.Server.wrap(handler)(resp, req) 248 assert.Equal(t, resp.Code, 403) 249 } 250 } 251 252 func TestTokenNotFound(t *testing.T) { 253 s := makeHTTPServer(t, func(c *Config) { 254 c.ACL.Enabled = true 255 }) 256 defer s.Shutdown() 257 258 resp := httptest.NewRecorder() 259 handler := func(resp http.ResponseWriter, req *http.Request) (interface{}, error) { 260 return nil, structs.ErrTokenNotFound 261 } 262 263 urlStr := "/v1/job/foo" 264 req, _ := http.NewRequest("GET", urlStr, nil) 265 s.Server.wrap(handler)(resp, req) 266 assert.Equal(t, resp.Code, 403) 267 } 268 269 func TestParseWait(t *testing.T) { 270 t.Parallel() 271 resp := httptest.NewRecorder() 272 var b structs.QueryOptions 273 274 req, err := http.NewRequest("GET", 275 "/v1/catalog/nodes?wait=60s&index=1000", nil) 276 if err != nil { 277 t.Fatalf("err: %v", err) 278 } 279 280 if d := parseWait(resp, req, &b); d { 281 t.Fatalf("unexpected done") 282 } 283 284 if b.MinQueryIndex != 1000 { 285 t.Fatalf("Bad: %v", b) 286 } 287 if b.MaxQueryTime != 60*time.Second { 288 t.Fatalf("Bad: %v", b) 289 } 290 } 291 292 func TestParseWait_InvalidTime(t *testing.T) { 293 t.Parallel() 294 resp := httptest.NewRecorder() 295 var b structs.QueryOptions 296 297 req, err := http.NewRequest("GET", 298 "/v1/catalog/nodes?wait=60foo&index=1000", nil) 299 if err != nil { 300 t.Fatalf("err: %v", err) 301 } 302 303 if d := parseWait(resp, req, &b); !d { 304 t.Fatalf("expected done") 305 } 306 307 if resp.Code != 400 { 308 t.Fatalf("bad code: %v", resp.Code) 309 } 310 } 311 312 func TestParseWait_InvalidIndex(t *testing.T) { 313 t.Parallel() 314 resp := httptest.NewRecorder() 315 var b structs.QueryOptions 316 317 req, err := http.NewRequest("GET", 318 "/v1/catalog/nodes?wait=60s&index=foo", nil) 319 if err != nil { 320 t.Fatalf("err: %v", err) 321 } 322 323 if d := parseWait(resp, req, &b); !d { 324 t.Fatalf("expected done") 325 } 326 327 if resp.Code != 400 { 328 t.Fatalf("bad code: %v", resp.Code) 329 } 330 } 331 332 func TestParseConsistency(t *testing.T) { 333 t.Parallel() 334 var b structs.QueryOptions 335 336 req, err := http.NewRequest("GET", 337 "/v1/catalog/nodes?stale", nil) 338 if err != nil { 339 t.Fatalf("err: %v", err) 340 } 341 342 parseConsistency(req, &b) 343 if !b.AllowStale { 344 t.Fatalf("Bad: %v", b) 345 } 346 347 b = structs.QueryOptions{} 348 req, err = http.NewRequest("GET", 349 "/v1/catalog/nodes?consistent", nil) 350 if err != nil { 351 t.Fatalf("err: %v", err) 352 } 353 354 parseConsistency(req, &b) 355 if b.AllowStale { 356 t.Fatalf("Bad: %v", b) 357 } 358 } 359 360 func TestParseRegion(t *testing.T) { 361 t.Parallel() 362 s := makeHTTPServer(t, nil) 363 defer s.Shutdown() 364 365 req, err := http.NewRequest("GET", 366 "/v1/jobs?region=foo", nil) 367 if err != nil { 368 t.Fatalf("err: %v", err) 369 } 370 371 var region string 372 s.Server.parseRegion(req, ®ion) 373 if region != "foo" { 374 t.Fatalf("bad %s", region) 375 } 376 377 region = "" 378 req, err = http.NewRequest("GET", "/v1/jobs", nil) 379 if err != nil { 380 t.Fatalf("err: %v", err) 381 } 382 383 s.Server.parseRegion(req, ®ion) 384 if region != "global" { 385 t.Fatalf("bad %s", region) 386 } 387 } 388 389 func TestParseToken(t *testing.T) { 390 t.Parallel() 391 s := makeHTTPServer(t, nil) 392 defer s.Shutdown() 393 394 req, err := http.NewRequest("GET", "/v1/jobs", nil) 395 req.Header.Add("X-Nomad-Token", "foobar") 396 if err != nil { 397 t.Fatalf("err: %v", err) 398 } 399 400 var token string 401 s.Server.parseToken(req, &token) 402 if token != "foobar" { 403 t.Fatalf("bad %s", token) 404 } 405 } 406 407 // TestHTTP_VerifyHTTPSClient asserts that a client certificate signed by the 408 // appropriate CA is required when VerifyHTTPSClient=true. 409 func TestHTTP_VerifyHTTPSClient(t *testing.T) { 410 t.Parallel() 411 const ( 412 cafile = "../../helper/tlsutil/testdata/ca.pem" 413 foocert = "../../helper/tlsutil/testdata/nomad-foo.pem" 414 fookey = "../../helper/tlsutil/testdata/nomad-foo-key.pem" 415 ) 416 s := makeHTTPServer(t, func(c *Config) { 417 c.Region = "foo" // match the region on foocert 418 c.TLSConfig = &config.TLSConfig{ 419 EnableHTTP: true, 420 VerifyHTTPSClient: true, 421 CAFile: cafile, 422 CertFile: foocert, 423 KeyFile: fookey, 424 } 425 }) 426 defer s.Shutdown() 427 428 reqURL := fmt.Sprintf("https://%s/v1/agent/self", s.Agent.config.AdvertiseAddrs.HTTP) 429 430 // FAIL: Requests that expect 127.0.0.1 as the name should fail 431 resp, err := http.Get(reqURL) 432 if err == nil { 433 resp.Body.Close() 434 t.Fatalf("expected non-nil error but received: %v", resp.StatusCode) 435 } 436 urlErr, ok := err.(*url.Error) 437 if !ok { 438 t.Fatalf("expected a *url.Error but received: %T -> %v", err, err) 439 } 440 hostErr, ok := urlErr.Err.(x509.HostnameError) 441 if !ok { 442 t.Fatalf("expected a x509.HostnameError but received: %T -> %v", urlErr.Err, urlErr.Err) 443 } 444 if expected := "127.0.0.1"; hostErr.Host != expected { 445 t.Fatalf("expected hostname on error to be %q but found %q", expected, hostErr.Host) 446 } 447 448 // FAIL: Requests that specify a valid hostname but not the CA should 449 // fail 450 tlsConf := &tls.Config{ 451 ServerName: "client.regionFoo.nomad", 452 } 453 transport := &http.Transport{TLSClientConfig: tlsConf} 454 client := &http.Client{Transport: transport} 455 req, err := http.NewRequest("GET", reqURL, nil) 456 if err != nil { 457 t.Fatalf("error creating request: %v", err) 458 } 459 resp, err = client.Do(req) 460 if err == nil { 461 resp.Body.Close() 462 t.Fatalf("expected non-nil error but received: %v", resp.StatusCode) 463 } 464 urlErr, ok = err.(*url.Error) 465 if !ok { 466 t.Fatalf("expected a *url.Error but received: %T -> %v", err, err) 467 } 468 _, ok = urlErr.Err.(x509.UnknownAuthorityError) 469 if !ok { 470 t.Fatalf("expected a x509.UnknownAuthorityError but received: %T -> %v", urlErr.Err, urlErr.Err) 471 } 472 473 // FAIL: Requests that specify a valid hostname and CA cert but lack a 474 // client certificate should fail 475 cacertBytes, err := ioutil.ReadFile(cafile) 476 if err != nil { 477 t.Fatalf("error reading cacert: %v", err) 478 } 479 tlsConf.RootCAs = x509.NewCertPool() 480 tlsConf.RootCAs.AppendCertsFromPEM(cacertBytes) 481 req, err = http.NewRequest("GET", reqURL, nil) 482 if err != nil { 483 t.Fatalf("error creating request: %v", err) 484 } 485 resp, err = client.Do(req) 486 if err == nil { 487 resp.Body.Close() 488 t.Fatalf("expected non-nil error but received: %v", resp.StatusCode) 489 } 490 urlErr, ok = err.(*url.Error) 491 if !ok { 492 t.Fatalf("expected a *url.Error but received: %T -> %v", err, err) 493 } 494 opErr, ok := urlErr.Err.(*net.OpError) 495 if !ok { 496 t.Fatalf("expected a *net.OpErr but received: %T -> %v", urlErr.Err, urlErr.Err) 497 } 498 const badCertificate = "tls: bad certificate" // from crypto/tls/alert.go:52 and RFC 5246 ยง A.3 499 if opErr.Err.Error() != badCertificate { 500 t.Fatalf("expected tls.alert bad_certificate but received: %q", opErr.Err.Error()) 501 } 502 503 // PASS: Requests that specify a valid hostname, CA cert, and client 504 // certificate succeed. 505 tlsConf.GetClientCertificate = func(*tls.CertificateRequestInfo) (*tls.Certificate, error) { 506 c, err := tls.LoadX509KeyPair(foocert, fookey) 507 if err != nil { 508 return nil, err 509 } 510 return &c, nil 511 } 512 transport = &http.Transport{TLSClientConfig: tlsConf} 513 client = &http.Client{Transport: transport} 514 req, err = http.NewRequest("GET", reqURL, nil) 515 if err != nil { 516 t.Fatalf("error creating request: %v", err) 517 } 518 resp, err = client.Do(req) 519 if err != nil { 520 t.Fatalf("unexpected error: %v", err) 521 } 522 resp.Body.Close() 523 if resp.StatusCode != 200 { 524 t.Fatalf("expected 200 status code but got: %d", resp.StatusCode) 525 } 526 } 527 528 // assertIndex tests that X-Nomad-Index is set and non-zero 529 func assertIndex(t *testing.T, resp *httptest.ResponseRecorder) { 530 header := resp.Header().Get("X-Nomad-Index") 531 if header == "" || header == "0" { 532 t.Fatalf("Bad: %v", header) 533 } 534 } 535 536 // checkIndex is like assertIndex but returns an error 537 func checkIndex(resp *httptest.ResponseRecorder) error { 538 header := resp.Header().Get("X-Nomad-Index") 539 if header == "" || header == "0" { 540 return fmt.Errorf("Bad: %v", header) 541 } 542 return nil 543 } 544 545 func TestHTTP_VerifyHTTPSClient_AfterConfigReload(t *testing.T) { 546 t.Parallel() 547 assert := assert.New(t) 548 549 const ( 550 cafile = "../../helper/tlsutil/testdata/ca.pem" 551 foocert = "../../helper/tlsutil/testdata/nomad-bad.pem" 552 fookey = "../../helper/tlsutil/testdata/nomad-bad-key.pem" 553 foocert2 = "../../helper/tlsutil/testdata/nomad-foo.pem" 554 fookey2 = "../../helper/tlsutil/testdata/nomad-foo-key.pem" 555 ) 556 557 agentConfig := &Config{ 558 TLSConfig: &config.TLSConfig{ 559 EnableHTTP: true, 560 VerifyHTTPSClient: true, 561 CAFile: cafile, 562 CertFile: foocert, 563 KeyFile: fookey, 564 }, 565 } 566 567 newConfig := &Config{ 568 TLSConfig: &config.TLSConfig{ 569 EnableHTTP: true, 570 VerifyHTTPSClient: true, 571 CAFile: cafile, 572 CertFile: foocert2, 573 KeyFile: fookey2, 574 }, 575 } 576 577 s := makeHTTPServer(t, func(c *Config) { 578 c.TLSConfig = agentConfig.TLSConfig 579 }) 580 defer s.Shutdown() 581 582 // Make an initial request that should fail. 583 // Requests that specify a valid hostname, CA cert, and client 584 // certificate succeed. 585 tlsConf := &tls.Config{ 586 ServerName: "client.regionFoo.nomad", 587 RootCAs: x509.NewCertPool(), 588 GetClientCertificate: func(*tls.CertificateRequestInfo) (*tls.Certificate, error) { 589 c, err := tls.LoadX509KeyPair(foocert, fookey) 590 if err != nil { 591 return nil, err 592 } 593 return &c, nil 594 }, 595 } 596 597 // HTTPS request should succeed 598 httpsReqURL := fmt.Sprintf("https://%s/v1/agent/self", s.Agent.config.AdvertiseAddrs.HTTP) 599 600 cacertBytes, err := ioutil.ReadFile(cafile) 601 assert.Nil(err) 602 tlsConf.RootCAs.AppendCertsFromPEM(cacertBytes) 603 604 transport := &http.Transport{TLSClientConfig: tlsConf} 605 client := &http.Client{Transport: transport} 606 req, err := http.NewRequest("GET", httpsReqURL, nil) 607 assert.Nil(err) 608 609 // Check that we get an error that the certificate isn't valid for the 610 // region we are contacting. 611 _, err = client.Do(req) 612 assert.Contains(err.Error(), "certificate is valid for") 613 614 // Reload the TLS configuration== 615 assert.Nil(s.Agent.Reload(newConfig)) 616 617 // Requests that specify a valid hostname, CA cert, and client 618 // certificate succeed. 619 tlsConf = &tls.Config{ 620 ServerName: "client.regionFoo.nomad", 621 RootCAs: x509.NewCertPool(), 622 GetClientCertificate: func(*tls.CertificateRequestInfo) (*tls.Certificate, error) { 623 c, err := tls.LoadX509KeyPair(foocert2, fookey2) 624 if err != nil { 625 return nil, err 626 } 627 return &c, nil 628 }, 629 } 630 631 cacertBytes, err = ioutil.ReadFile(cafile) 632 assert.Nil(err) 633 tlsConf.RootCAs.AppendCertsFromPEM(cacertBytes) 634 635 transport = &http.Transport{TLSClientConfig: tlsConf} 636 client = &http.Client{Transport: transport} 637 req, err = http.NewRequest("GET", httpsReqURL, nil) 638 assert.Nil(err) 639 640 resp, err := client.Do(req) 641 if assert.Nil(err) { 642 resp.Body.Close() 643 assert.Equal(resp.StatusCode, 200) 644 } 645 } 646 647 // getIndex parses X-Nomad-Index 648 func getIndex(t *testing.T, resp *httptest.ResponseRecorder) uint64 { 649 header := resp.Header().Get("X-Nomad-Index") 650 if header == "" { 651 t.Fatalf("Bad: %v", header) 652 } 653 val, err := strconv.Atoi(header) 654 if err != nil { 655 t.Fatalf("Bad: %v", header) 656 } 657 return uint64(val) 658 } 659 660 func httpTest(t testing.TB, cb func(c *Config), f func(srv *TestAgent)) { 661 s := makeHTTPServer(t, cb) 662 defer s.Shutdown() 663 testutil.WaitForLeader(t, s.Agent.RPC) 664 f(s) 665 } 666 667 func httpACLTest(t testing.TB, cb func(c *Config), f func(srv *TestAgent)) { 668 s := makeHTTPServer(t, func(c *Config) { 669 c.ACL.Enabled = true 670 if cb != nil { 671 cb(c) 672 } 673 }) 674 defer s.Shutdown() 675 testutil.WaitForLeader(t, s.Agent.RPC) 676 f(s) 677 } 678 679 func setToken(req *http.Request, token *structs.ACLToken) { 680 req.Header.Set("X-Nomad-Token", token.SecretID) 681 } 682 683 func encodeReq(obj interface{}) io.ReadCloser { 684 buf := bytes.NewBuffer(nil) 685 enc := json.NewEncoder(buf) 686 enc.Encode(obj) 687 return ioutil.NopCloser(buf) 688 }