github.com/janma/nomad@v0.11.3/command/agent/fs_endpoint_test.go (about) 1 package agent 2 3 import ( 4 "encoding/base64" 5 "fmt" 6 "io/ioutil" 7 "net/http" 8 "net/http/httptest" 9 "strings" 10 "testing" 11 "time" 12 13 cstructs "github.com/hashicorp/nomad/client/structs" 14 "github.com/hashicorp/nomad/helper/uuid" 15 "github.com/hashicorp/nomad/nomad/mock" 16 "github.com/hashicorp/nomad/nomad/structs" 17 "github.com/hashicorp/nomad/testutil" 18 "github.com/stretchr/testify/require" 19 ) 20 21 const ( 22 defaultLoggerMockDriverStdout = "Hello from the other side" 23 xssLoggerMockDriverStdout = "<script>alert(document.domain);</script>" 24 ) 25 26 var ( 27 defaultLoggerMockDriver = map[string]interface{}{ 28 "run_for": "2s", 29 "stdout_string": defaultLoggerMockDriverStdout, 30 } 31 xssLoggerMockDriver = map[string]interface{}{ 32 "run_for": "2s", 33 "stdout_string": xssLoggerMockDriverStdout, 34 } 35 ) 36 37 type clientAllocWaiter int 38 39 const ( 40 noWaitClientAlloc clientAllocWaiter = iota 41 runningClientAlloc 42 terminalClientAlloc 43 ) 44 45 func addAllocToClient(agent *TestAgent, alloc *structs.Allocation, wait clientAllocWaiter) { 46 require := require.New(agent.T) 47 48 // Wait for the client to connect 49 testutil.WaitForResult(func() (bool, error) { 50 node, err := agent.server.State().NodeByID(nil, agent.client.NodeID()) 51 if err != nil { 52 return false, err 53 } 54 if node == nil { 55 return false, fmt.Errorf("unknown node") 56 } 57 58 return node.Status == structs.NodeStatusReady, fmt.Errorf("bad node status") 59 }, func(err error) { 60 agent.T.Fatal(err) 61 }) 62 63 // Upsert the allocation 64 state := agent.server.State() 65 require.Nil(state.UpsertJob(999, alloc.Job)) 66 require.Nil(state.UpsertAllocs(1003, []*structs.Allocation{alloc})) 67 68 if wait == noWaitClientAlloc { 69 return 70 } 71 72 // Wait for the client to run the allocation 73 testutil.WaitForResult(func() (bool, error) { 74 alloc, err := state.AllocByID(nil, alloc.ID) 75 if err != nil { 76 return false, err 77 } 78 if alloc == nil { 79 return false, fmt.Errorf("unknown alloc") 80 } 81 82 expectation := alloc.ClientStatus == structs.AllocClientStatusComplete || 83 alloc.ClientStatus == structs.AllocClientStatusFailed 84 if wait == runningClientAlloc { 85 expectation = expectation || alloc.ClientStatus == structs.AllocClientStatusRunning 86 } 87 88 if !expectation { 89 return false, fmt.Errorf("alloc client status: %v", alloc.ClientStatus) 90 } 91 92 return true, nil 93 }, func(err error) { 94 agent.T.Fatal(err) 95 }) 96 } 97 98 // mockFSAlloc returns a suitable mock alloc for testing the fs system. If 99 // config isn't provided, the defaultLoggerMockDriver config is used. 100 func mockFSAlloc(nodeID string, config map[string]interface{}) *structs.Allocation { 101 a := mock.Alloc() 102 a.NodeID = nodeID 103 a.Job.Type = structs.JobTypeBatch 104 a.Job.TaskGroups[0].Count = 1 105 a.Job.TaskGroups[0].Tasks[0].Driver = "mock_driver" 106 107 if config != nil { 108 a.Job.TaskGroups[0].Tasks[0].Config = config 109 } else { 110 a.Job.TaskGroups[0].Tasks[0].Config = defaultLoggerMockDriver 111 } 112 113 return a 114 } 115 116 func TestHTTP_FS_List_MissingParams(t *testing.T) { 117 t.Parallel() 118 require := require.New(t) 119 httpTest(t, nil, func(s *TestAgent) { 120 req, err := http.NewRequest("GET", "/v1/client/fs/ls/", nil) 121 require.Nil(err) 122 respW := httptest.NewRecorder() 123 _, err = s.Server.DirectoryListRequest(respW, req) 124 require.EqualError(err, allocIDNotPresentErr.Error()) 125 }) 126 } 127 128 func TestHTTP_FS_Stat_MissingParams(t *testing.T) { 129 t.Parallel() 130 require := require.New(t) 131 httpTest(t, nil, func(s *TestAgent) { 132 req, err := http.NewRequest("GET", "/v1/client/fs/stat/", nil) 133 require.Nil(err) 134 respW := httptest.NewRecorder() 135 136 _, err = s.Server.FileStatRequest(respW, req) 137 require.EqualError(err, allocIDNotPresentErr.Error()) 138 139 req, err = http.NewRequest("GET", "/v1/client/fs/stat/foo", nil) 140 require.Nil(err) 141 respW = httptest.NewRecorder() 142 143 _, err = s.Server.FileStatRequest(respW, req) 144 require.EqualError(err, fileNameNotPresentErr.Error()) 145 }) 146 } 147 148 func TestHTTP_FS_ReadAt_MissingParams(t *testing.T) { 149 t.Parallel() 150 require := require.New(t) 151 httpTest(t, nil, func(s *TestAgent) { 152 req, err := http.NewRequest("GET", "/v1/client/fs/readat/", nil) 153 require.NoError(err) 154 155 _, err = s.Server.FileReadAtRequest(httptest.NewRecorder(), req) 156 require.Error(err) 157 158 req, err = http.NewRequest("GET", "/v1/client/fs/readat/foo", nil) 159 require.NoError(err) 160 161 _, err = s.Server.FileReadAtRequest(httptest.NewRecorder(), req) 162 require.Error(err) 163 164 req, err = http.NewRequest("GET", "/v1/client/fs/readat/foo?path=/path/to/file", nil) 165 require.NoError(err) 166 167 _, err = s.Server.FileReadAtRequest(httptest.NewRecorder(), req) 168 require.Error(err) 169 }) 170 } 171 172 func TestHTTP_FS_Cat_MissingParams(t *testing.T) { 173 t.Parallel() 174 require := require.New(t) 175 httpTest(t, nil, func(s *TestAgent) { 176 req, err := http.NewRequest("GET", "/v1/client/fs/cat/", nil) 177 require.Nil(err) 178 respW := httptest.NewRecorder() 179 180 _, err = s.Server.FileCatRequest(respW, req) 181 require.EqualError(err, allocIDNotPresentErr.Error()) 182 183 req, err = http.NewRequest("GET", "/v1/client/fs/stat/foo", nil) 184 require.Nil(err) 185 respW = httptest.NewRecorder() 186 187 _, err = s.Server.FileCatRequest(respW, req) 188 require.EqualError(err, fileNameNotPresentErr.Error()) 189 }) 190 } 191 192 func TestHTTP_FS_Stream_MissingParams(t *testing.T) { 193 t.Parallel() 194 require := require.New(t) 195 httpTest(t, nil, func(s *TestAgent) { 196 req, err := http.NewRequest("GET", "/v1/client/fs/stream/", nil) 197 require.NoError(err) 198 respW := httptest.NewRecorder() 199 200 _, err = s.Server.Stream(respW, req) 201 require.EqualError(err, allocIDNotPresentErr.Error()) 202 203 req, err = http.NewRequest("GET", "/v1/client/fs/stream/foo", nil) 204 require.NoError(err) 205 respW = httptest.NewRecorder() 206 207 _, err = s.Server.Stream(respW, req) 208 require.EqualError(err, fileNameNotPresentErr.Error()) 209 210 req, err = http.NewRequest("GET", "/v1/client/fs/stream/foo?path=/path/to/file", nil) 211 require.NoError(err) 212 respW = httptest.NewRecorder() 213 214 _, err = s.Server.Stream(respW, req) 215 require.Error(err) 216 require.Contains(err.Error(), "alloc lookup failed") 217 }) 218 } 219 220 // TestHTTP_FS_Logs_MissingParams asserts proper error codes and messages are 221 // returned for incorrect parameters (eg missing tasks). 222 func TestHTTP_FS_Logs_MissingParams(t *testing.T) { 223 t.Parallel() 224 require := require.New(t) 225 httpTest(t, nil, func(s *TestAgent) { 226 // AllocID Not Present 227 req, err := http.NewRequest("GET", "/v1/client/fs/logs/", nil) 228 require.NoError(err) 229 respW := httptest.NewRecorder() 230 231 s.Server.mux.ServeHTTP(respW, req) 232 require.Equal(respW.Body.String(), allocIDNotPresentErr.Error()) 233 require.Equal(400, respW.Code) 234 235 // Task Not Present 236 req, err = http.NewRequest("GET", "/v1/client/fs/logs/foo", nil) 237 require.NoError(err) 238 respW = httptest.NewRecorder() 239 240 s.Server.mux.ServeHTTP(respW, req) 241 require.Equal(respW.Body.String(), taskNotPresentErr.Error()) 242 require.Equal(400, respW.Code) 243 244 // Log Type Not Present 245 req, err = http.NewRequest("GET", "/v1/client/fs/logs/foo?task=foo", nil) 246 require.NoError(err) 247 respW = httptest.NewRecorder() 248 249 s.Server.mux.ServeHTTP(respW, req) 250 require.Equal(respW.Body.String(), logTypeNotPresentErr.Error()) 251 require.Equal(400, respW.Code) 252 253 // case where all parameters are set but alloc isn't found 254 req, err = http.NewRequest("GET", "/v1/client/fs/logs/foo?task=foo&type=stdout", nil) 255 require.NoError(err) 256 respW = httptest.NewRecorder() 257 258 s.Server.mux.ServeHTTP(respW, req) 259 require.Equal(500, respW.Code) 260 require.Contains(respW.Body.String(), "alloc lookup failed") 261 }) 262 } 263 264 func TestHTTP_FS_List(t *testing.T) { 265 t.Parallel() 266 require := require.New(t) 267 httpTest(t, nil, func(s *TestAgent) { 268 a := mockFSAlloc(s.client.NodeID(), nil) 269 addAllocToClient(s, a, terminalClientAlloc) 270 271 req, err := http.NewRequest("GET", "/v1/client/fs/ls/"+a.ID, nil) 272 require.Nil(err) 273 respW := httptest.NewRecorder() 274 raw, err := s.Server.DirectoryListRequest(respW, req) 275 require.Nil(err) 276 277 files, ok := raw.([]*cstructs.AllocFileInfo) 278 require.True(ok) 279 require.NotEmpty(files) 280 require.True(files[0].IsDir) 281 }) 282 } 283 284 func TestHTTP_FS_Stat(t *testing.T) { 285 t.Parallel() 286 require := require.New(t) 287 httpTest(t, nil, func(s *TestAgent) { 288 a := mockFSAlloc(s.client.NodeID(), nil) 289 addAllocToClient(s, a, terminalClientAlloc) 290 291 path := fmt.Sprintf("/v1/client/fs/stat/%s?path=alloc/", a.ID) 292 req, err := http.NewRequest("GET", path, nil) 293 require.Nil(err) 294 respW := httptest.NewRecorder() 295 raw, err := s.Server.FileStatRequest(respW, req) 296 require.Nil(err) 297 298 info, ok := raw.(*cstructs.AllocFileInfo) 299 require.True(ok) 300 require.NotNil(info) 301 require.True(info.IsDir) 302 }) 303 } 304 305 func TestHTTP_FS_ReadAt(t *testing.T) { 306 t.Parallel() 307 require := require.New(t) 308 httpTest(t, nil, func(s *TestAgent) { 309 a := mockFSAlloc(s.client.NodeID(), nil) 310 addAllocToClient(s, a, terminalClientAlloc) 311 312 offset := 1 313 limit := 3 314 expectation := defaultLoggerMockDriverStdout[offset : offset+limit] 315 path := fmt.Sprintf("/v1/client/fs/readat/%s?path=alloc/logs/web.stdout.0&offset=%d&limit=%d", 316 a.ID, offset, limit) 317 318 req, err := http.NewRequest("GET", path, nil) 319 require.Nil(err) 320 respW := httptest.NewRecorder() 321 _, err = s.Server.FileReadAtRequest(respW, req) 322 require.Nil(err) 323 324 output, err := ioutil.ReadAll(respW.Result().Body) 325 require.Nil(err) 326 require.EqualValues(expectation, output) 327 }) 328 } 329 330 // TestHTTP_FS_ReadAt_XSS asserts that the readat API is safe from XSS. 331 func TestHTTP_FS_ReadAt_XSS(t *testing.T) { 332 t.Parallel() 333 httpTest(t, nil, func(s *TestAgent) { 334 a := mockFSAlloc(s.client.NodeID(), xssLoggerMockDriver) 335 addAllocToClient(s, a, terminalClientAlloc) 336 337 path := fmt.Sprintf("%s/v1/client/fs/readat/%s?path=alloc/logs/web.stdout.0&offset=0&limit=%d", 338 s.HTTPAddr(), a.ID, len(xssLoggerMockDriverStdout)) 339 resp, err := http.DefaultClient.Get(path) 340 require.NoError(t, err) 341 defer resp.Body.Close() 342 343 buf, err := ioutil.ReadAll(resp.Body) 344 require.NoError(t, err) 345 require.Equal(t, xssLoggerMockDriverStdout, string(buf)) 346 347 require.Equal(t, []string{"text/plain"}, resp.Header.Values("Content-Type")) 348 require.Equal(t, []string{"nosniff"}, resp.Header.Values("X-Content-Type-Options")) 349 require.Equal(t, []string{"1; mode=block"}, resp.Header.Values("X-XSS-Protection")) 350 require.Equal(t, []string{"default-src 'none'; style-src 'unsafe-inline'; sandbox"}, 351 resp.Header.Values("Content-Security-Policy")) 352 }) 353 } 354 355 func TestHTTP_FS_Cat(t *testing.T) { 356 t.Parallel() 357 require := require.New(t) 358 httpTest(t, nil, func(s *TestAgent) { 359 a := mockFSAlloc(s.client.NodeID(), nil) 360 addAllocToClient(s, a, terminalClientAlloc) 361 362 path := fmt.Sprintf("/v1/client/fs/cat/%s?path=alloc/logs/web.stdout.0", a.ID) 363 364 req, err := http.NewRequest("GET", path, nil) 365 require.Nil(err) 366 respW := httptest.NewRecorder() 367 _, err = s.Server.FileCatRequest(respW, req) 368 require.Nil(err) 369 370 output, err := ioutil.ReadAll(respW.Result().Body) 371 require.Nil(err) 372 require.EqualValues(defaultLoggerMockDriverStdout, output) 373 }) 374 } 375 376 // TestHTTP_FS_Cat_XSS asserts that the cat API is safe from XSS. 377 func TestHTTP_FS_Cat_XSS(t *testing.T) { 378 t.Parallel() 379 httpTest(t, nil, func(s *TestAgent) { 380 a := mockFSAlloc(s.client.NodeID(), xssLoggerMockDriver) 381 addAllocToClient(s, a, terminalClientAlloc) 382 383 path := fmt.Sprintf("%s/v1/client/fs/cat/%s?path=alloc/logs/web.stdout.0", s.HTTPAddr(), a.ID) 384 resp, err := http.DefaultClient.Get(path) 385 require.NoError(t, err) 386 defer resp.Body.Close() 387 388 buf, err := ioutil.ReadAll(resp.Body) 389 require.NoError(t, err) 390 require.Equal(t, xssLoggerMockDriverStdout, string(buf)) 391 392 require.Equal(t, []string{"text/plain"}, resp.Header.Values("Content-Type")) 393 require.Equal(t, []string{"nosniff"}, resp.Header.Values("X-Content-Type-Options")) 394 require.Equal(t, []string{"1; mode=block"}, resp.Header.Values("X-XSS-Protection")) 395 require.Equal(t, []string{"default-src 'none'; style-src 'unsafe-inline'; sandbox"}, 396 resp.Header.Values("Content-Security-Policy")) 397 }) 398 } 399 400 func TestHTTP_FS_Stream_NoFollow(t *testing.T) { 401 t.Parallel() 402 require := require.New(t) 403 httpTest(t, nil, func(s *TestAgent) { 404 a := mockFSAlloc(s.client.NodeID(), nil) 405 addAllocToClient(s, a, terminalClientAlloc) 406 407 offset := 4 408 expectation := base64.StdEncoding.EncodeToString( 409 []byte(defaultLoggerMockDriverStdout[len(defaultLoggerMockDriverStdout)-offset:])) 410 path := fmt.Sprintf("/v1/client/fs/stream/%s?path=alloc/logs/web.stdout.0&offset=%d&origin=end&follow=false", 411 a.ID, offset) 412 413 req, err := http.NewRequest("GET", path, nil) 414 require.Nil(err) 415 respW := testutil.NewResponseRecorder() 416 doneCh := make(chan struct{}) 417 go func() { 418 _, err = s.Server.Stream(respW, req) 419 require.Nil(err) 420 close(doneCh) 421 }() 422 423 out := "" 424 testutil.WaitForResult(func() (bool, error) { 425 output, err := ioutil.ReadAll(respW) 426 if err != nil { 427 return false, err 428 } 429 430 out += string(output) 431 return strings.Contains(out, expectation), fmt.Errorf("%q doesn't contain %q", out, expectation) 432 }, func(err error) { 433 t.Fatal(err) 434 }) 435 436 select { 437 case <-doneCh: 438 case <-time.After(1 * time.Second): 439 t.Fatal("should close but did not") 440 } 441 }) 442 } 443 444 // TestHTTP_FS_Stream_NoFollow_XSS asserts that the stream API is safe from XSS. 445 func TestHTTP_FS_Stream_NoFollow_XSS(t *testing.T) { 446 t.Parallel() 447 httpTest(t, nil, func(s *TestAgent) { 448 a := mockFSAlloc(s.client.NodeID(), xssLoggerMockDriver) 449 addAllocToClient(s, a, terminalClientAlloc) 450 451 path := fmt.Sprintf("%s/v1/client/fs/stream/%s?path=alloc/logs/web.stdout.0&follow=false", 452 s.HTTPAddr(), a.ID) 453 resp, err := http.DefaultClient.Get(path) 454 require.NoError(t, err) 455 defer resp.Body.Close() 456 457 buf, err := ioutil.ReadAll(resp.Body) 458 require.NoError(t, err) 459 expected := `{"Data":"PHNjcmlwdD5hbGVydChkb2N1bWVudC5kb21haW4pOzwvc2NyaXB0Pg==","File":"alloc/logs/web.stdout.0","Offset":40}` 460 require.Equal(t, expected, string(buf)) 461 }) 462 } 463 464 func TestHTTP_FS_Stream_Follow(t *testing.T) { 465 t.Parallel() 466 require := require.New(t) 467 httpTest(t, nil, func(s *TestAgent) { 468 a := mockFSAlloc(s.client.NodeID(), nil) 469 addAllocToClient(s, a, terminalClientAlloc) 470 471 offset := 4 472 expectation := base64.StdEncoding.EncodeToString( 473 []byte(defaultLoggerMockDriverStdout[len(defaultLoggerMockDriverStdout)-offset:])) 474 path := fmt.Sprintf("/v1/client/fs/stream/%s?path=alloc/logs/web.stdout.0&offset=%d&origin=end", 475 a.ID, offset) 476 477 req, err := http.NewRequest("GET", path, nil) 478 require.Nil(err) 479 respW := httptest.NewRecorder() 480 doneCh := make(chan struct{}) 481 go func() { 482 _, err = s.Server.Stream(respW, req) 483 require.Nil(err) 484 close(doneCh) 485 }() 486 487 out := "" 488 testutil.WaitForResult(func() (bool, error) { 489 output, err := ioutil.ReadAll(respW.Body) 490 if err != nil { 491 return false, err 492 } 493 494 out += string(output) 495 return strings.Contains(out, expectation), fmt.Errorf("%q doesn't contain %q", out, expectation) 496 }, func(err error) { 497 t.Fatal(err) 498 }) 499 500 select { 501 case <-doneCh: 502 t.Fatal("shouldn't close") 503 case <-time.After(1 * time.Second): 504 } 505 }) 506 } 507 508 func TestHTTP_FS_Logs(t *testing.T) { 509 t.Parallel() 510 require := require.New(t) 511 httpTest(t, nil, func(s *TestAgent) { 512 a := mockFSAlloc(s.client.NodeID(), nil) 513 addAllocToClient(s, a, terminalClientAlloc) 514 515 offset := 4 516 expectation := defaultLoggerMockDriverStdout[len(defaultLoggerMockDriverStdout)-offset:] 517 path := fmt.Sprintf("/v1/client/fs/logs/%s?type=stdout&task=web&offset=%d&origin=end&plain=true", 518 a.ID, offset) 519 520 req, err := http.NewRequest("GET", path, nil) 521 require.Nil(err) 522 respW := testutil.NewResponseRecorder() 523 go func() { 524 _, err = s.Server.Logs(respW, req) 525 require.Nil(err) 526 }() 527 528 out := "" 529 testutil.WaitForResult(func() (bool, error) { 530 output, err := ioutil.ReadAll(respW) 531 if err != nil { 532 return false, err 533 } 534 535 out += string(output) 536 return out == expectation, fmt.Errorf("%q != %q", out, expectation) 537 }, func(err error) { 538 t.Fatal(err) 539 }) 540 }) 541 } 542 543 // TestHTTP_FS_Logs_XSS asserts that the logs endpoint always returns 544 // text/plain or application/json content regardless of whether the logs are 545 // HTML+Javascript or not. 546 func TestHTTP_FS_Logs_XSS(t *testing.T) { 547 t.Parallel() 548 httpTest(t, nil, func(s *TestAgent) { 549 a := mockFSAlloc(s.client.NodeID(), xssLoggerMockDriver) 550 addAllocToClient(s, a, terminalClientAlloc) 551 552 // Must make a "real" request to ensure Go's default content 553 // type detection does not detect text/html 554 path := fmt.Sprintf("%s/v1/client/fs/logs/%s?type=stdout&task=web&plain=true", s.HTTPAddr(), a.ID) 555 resp, err := http.DefaultClient.Get(path) 556 require.NoError(t, err) 557 defer resp.Body.Close() 558 559 buf, err := ioutil.ReadAll(resp.Body) 560 require.NoError(t, err) 561 require.Equal(t, xssLoggerMockDriverStdout, string(buf)) 562 563 require.Equal(t, []string{"text/plain"}, resp.Header.Values("Content-Type")) 564 }) 565 } 566 567 func TestHTTP_FS_Logs_Follow(t *testing.T) { 568 t.Parallel() 569 require := require.New(t) 570 httpTest(t, nil, func(s *TestAgent) { 571 a := mockFSAlloc(s.client.NodeID(), nil) 572 addAllocToClient(s, a, terminalClientAlloc) 573 574 offset := 4 575 expectation := defaultLoggerMockDriverStdout[len(defaultLoggerMockDriverStdout)-offset:] 576 path := fmt.Sprintf("/v1/client/fs/logs/%s?type=stdout&task=web&offset=%d&origin=end&plain=true&follow=true", 577 a.ID, offset) 578 579 req, err := http.NewRequest("GET", path, nil) 580 require.Nil(err) 581 respW := testutil.NewResponseRecorder() 582 errCh := make(chan error, 1) 583 go func() { 584 _, err := s.Server.Logs(respW, req) 585 errCh <- err 586 }() 587 588 out := "" 589 testutil.WaitForResult(func() (bool, error) { 590 output, err := ioutil.ReadAll(respW) 591 if err != nil { 592 return false, err 593 } 594 595 out += string(output) 596 return out == expectation, fmt.Errorf("%q != %q", out, expectation) 597 }, func(err error) { 598 t.Fatal(err) 599 }) 600 601 select { 602 case err := <-errCh: 603 t.Fatalf("shouldn't exit: %v", err) 604 case <-time.After(1 * time.Second): 605 } 606 }) 607 } 608 609 func TestHTTP_FS_Logs_PropagatesErrors(t *testing.T) { 610 t.Parallel() 611 httpTest(t, nil, func(s *TestAgent) { 612 path := fmt.Sprintf("/v1/client/fs/logs/%s?type=stdout&task=web&offset=0&origin=end&plain=true", 613 uuid.Generate()) 614 615 req, err := http.NewRequest("GET", path, nil) 616 require.NoError(t, err) 617 respW := testutil.NewResponseRecorder() 618 619 _, err = s.Server.Logs(respW, req) 620 require.Error(t, err) 621 622 _, ok := err.(HTTPCodedError) 623 require.Truef(t, ok, "expected a coded error but found: %#+v", err) 624 }) 625 }