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