github.com/karlem/nomad@v0.10.2-rc1/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 ) 24 25 var ( 26 defaultLoggerMockDriver = map[string]interface{}{ 27 "run_for": "2s", 28 "stdout_string": defaultLoggerMockDriverStdout, 29 } 30 ) 31 32 type clientAllocWaiter int 33 34 const ( 35 noWaitClientAlloc clientAllocWaiter = iota 36 runningClientAlloc 37 terminalClientAlloc 38 ) 39 40 func addAllocToClient(agent *TestAgent, alloc *structs.Allocation, wait clientAllocWaiter) { 41 require := require.New(agent.T) 42 43 // Wait for the client to connect 44 testutil.WaitForResult(func() (bool, error) { 45 node, err := agent.server.State().NodeByID(nil, agent.client.NodeID()) 46 if err != nil { 47 return false, err 48 } 49 if node == nil { 50 return false, fmt.Errorf("unknown node") 51 } 52 53 return node.Status == structs.NodeStatusReady, fmt.Errorf("bad node status") 54 }, func(err error) { 55 agent.T.Fatal(err) 56 }) 57 58 // Upsert the allocation 59 state := agent.server.State() 60 require.Nil(state.UpsertJob(999, alloc.Job)) 61 require.Nil(state.UpsertAllocs(1003, []*structs.Allocation{alloc})) 62 63 if wait == noWaitClientAlloc { 64 return 65 } 66 67 // Wait for the client to run the allocation 68 testutil.WaitForResult(func() (bool, error) { 69 alloc, err := state.AllocByID(nil, alloc.ID) 70 if err != nil { 71 return false, err 72 } 73 if alloc == nil { 74 return false, fmt.Errorf("unknown alloc") 75 } 76 77 expectation := alloc.ClientStatus == structs.AllocClientStatusComplete || 78 alloc.ClientStatus == structs.AllocClientStatusFailed 79 if wait == runningClientAlloc { 80 expectation = expectation || alloc.ClientStatus == structs.AllocClientStatusRunning 81 } 82 83 if !expectation { 84 return false, fmt.Errorf("alloc client status: %v", alloc.ClientStatus) 85 } 86 87 return true, nil 88 }, func(err error) { 89 agent.T.Fatal(err) 90 }) 91 } 92 93 // mockFSAlloc returns a suitable mock alloc for testing the fs system. If 94 // config isn't provided, the defaultLoggerMockDriver config is used. 95 func mockFSAlloc(nodeID string, config map[string]interface{}) *structs.Allocation { 96 a := mock.Alloc() 97 a.NodeID = nodeID 98 a.Job.Type = structs.JobTypeBatch 99 a.Job.TaskGroups[0].Count = 1 100 a.Job.TaskGroups[0].Tasks[0].Driver = "mock_driver" 101 102 if config != nil { 103 a.Job.TaskGroups[0].Tasks[0].Config = config 104 } else { 105 a.Job.TaskGroups[0].Tasks[0].Config = defaultLoggerMockDriver 106 } 107 108 return a 109 } 110 111 func TestHTTP_FS_List_MissingParams(t *testing.T) { 112 t.Parallel() 113 require := require.New(t) 114 httpTest(t, nil, func(s *TestAgent) { 115 req, err := http.NewRequest("GET", "/v1/client/fs/ls/", nil) 116 require.Nil(err) 117 respW := httptest.NewRecorder() 118 _, err = s.Server.DirectoryListRequest(respW, req) 119 require.EqualError(err, allocIDNotPresentErr.Error()) 120 }) 121 } 122 123 func TestHTTP_FS_Stat_MissingParams(t *testing.T) { 124 t.Parallel() 125 require := require.New(t) 126 httpTest(t, nil, func(s *TestAgent) { 127 req, err := http.NewRequest("GET", "/v1/client/fs/stat/", nil) 128 require.Nil(err) 129 respW := httptest.NewRecorder() 130 131 _, err = s.Server.FileStatRequest(respW, req) 132 require.EqualError(err, allocIDNotPresentErr.Error()) 133 134 req, err = http.NewRequest("GET", "/v1/client/fs/stat/foo", nil) 135 require.Nil(err) 136 respW = httptest.NewRecorder() 137 138 _, err = s.Server.FileStatRequest(respW, req) 139 require.EqualError(err, fileNameNotPresentErr.Error()) 140 }) 141 } 142 143 func TestHTTP_FS_ReadAt_MissingParams(t *testing.T) { 144 t.Parallel() 145 require := require.New(t) 146 httpTest(t, nil, func(s *TestAgent) { 147 req, err := http.NewRequest("GET", "/v1/client/fs/readat/", nil) 148 require.NoError(err) 149 150 _, err = s.Server.FileReadAtRequest(httptest.NewRecorder(), req) 151 require.Error(err) 152 153 req, err = http.NewRequest("GET", "/v1/client/fs/readat/foo", nil) 154 require.NoError(err) 155 156 _, err = s.Server.FileReadAtRequest(httptest.NewRecorder(), req) 157 require.Error(err) 158 159 req, err = http.NewRequest("GET", "/v1/client/fs/readat/foo?path=/path/to/file", nil) 160 require.NoError(err) 161 162 _, err = s.Server.FileReadAtRequest(httptest.NewRecorder(), req) 163 require.Error(err) 164 }) 165 } 166 167 func TestHTTP_FS_Cat_MissingParams(t *testing.T) { 168 t.Parallel() 169 require := require.New(t) 170 httpTest(t, nil, func(s *TestAgent) { 171 req, err := http.NewRequest("GET", "/v1/client/fs/cat/", nil) 172 require.Nil(err) 173 respW := httptest.NewRecorder() 174 175 _, err = s.Server.FileCatRequest(respW, req) 176 require.EqualError(err, allocIDNotPresentErr.Error()) 177 178 req, err = http.NewRequest("GET", "/v1/client/fs/stat/foo", nil) 179 require.Nil(err) 180 respW = httptest.NewRecorder() 181 182 _, err = s.Server.FileCatRequest(respW, req) 183 require.EqualError(err, fileNameNotPresentErr.Error()) 184 }) 185 } 186 187 func TestHTTP_FS_Stream_MissingParams(t *testing.T) { 188 t.Parallel() 189 require := require.New(t) 190 httpTest(t, nil, func(s *TestAgent) { 191 req, err := http.NewRequest("GET", "/v1/client/fs/stream/", nil) 192 require.NoError(err) 193 respW := httptest.NewRecorder() 194 195 _, err = s.Server.Stream(respW, req) 196 require.EqualError(err, allocIDNotPresentErr.Error()) 197 198 req, err = http.NewRequest("GET", "/v1/client/fs/stream/foo", nil) 199 require.NoError(err) 200 respW = httptest.NewRecorder() 201 202 _, err = s.Server.Stream(respW, req) 203 require.EqualError(err, fileNameNotPresentErr.Error()) 204 205 req, err = http.NewRequest("GET", "/v1/client/fs/stream/foo?path=/path/to/file", nil) 206 require.NoError(err) 207 respW = httptest.NewRecorder() 208 209 _, err = s.Server.Stream(respW, req) 210 require.Error(err) 211 require.Contains(err.Error(), "alloc lookup failed") 212 }) 213 } 214 215 // TestHTTP_FS_Logs_MissingParams asserts proper error codes and messages are 216 // returned for incorrect parameters (eg missing tasks). 217 func TestHTTP_FS_Logs_MissingParams(t *testing.T) { 218 t.Parallel() 219 require := require.New(t) 220 httpTest(t, nil, func(s *TestAgent) { 221 // AllocID Not Present 222 req, err := http.NewRequest("GET", "/v1/client/fs/logs/", nil) 223 require.NoError(err) 224 respW := httptest.NewRecorder() 225 226 s.Server.mux.ServeHTTP(respW, req) 227 require.Equal(respW.Body.String(), allocIDNotPresentErr.Error()) 228 require.Equal(400, respW.Code) 229 230 // Task Not Present 231 req, err = http.NewRequest("GET", "/v1/client/fs/logs/foo", nil) 232 require.NoError(err) 233 respW = httptest.NewRecorder() 234 235 s.Server.mux.ServeHTTP(respW, req) 236 require.Equal(respW.Body.String(), taskNotPresentErr.Error()) 237 require.Equal(400, respW.Code) 238 239 // Log Type Not Present 240 req, err = http.NewRequest("GET", "/v1/client/fs/logs/foo?task=foo", nil) 241 require.NoError(err) 242 respW = httptest.NewRecorder() 243 244 s.Server.mux.ServeHTTP(respW, req) 245 require.Equal(respW.Body.String(), logTypeNotPresentErr.Error()) 246 require.Equal(400, respW.Code) 247 248 // case where all parameters are set but alloc isn't found 249 req, err = http.NewRequest("GET", "/v1/client/fs/logs/foo?task=foo&type=stdout", nil) 250 require.NoError(err) 251 respW = httptest.NewRecorder() 252 253 s.Server.mux.ServeHTTP(respW, req) 254 require.Equal(500, respW.Code) 255 require.Contains(respW.Body.String(), "alloc lookup failed") 256 }) 257 } 258 259 func TestHTTP_FS_List(t *testing.T) { 260 t.Parallel() 261 require := require.New(t) 262 httpTest(t, nil, func(s *TestAgent) { 263 a := mockFSAlloc(s.client.NodeID(), nil) 264 addAllocToClient(s, a, terminalClientAlloc) 265 266 req, err := http.NewRequest("GET", "/v1/client/fs/ls/"+a.ID, nil) 267 require.Nil(err) 268 respW := httptest.NewRecorder() 269 raw, err := s.Server.DirectoryListRequest(respW, req) 270 require.Nil(err) 271 272 files, ok := raw.([]*cstructs.AllocFileInfo) 273 require.True(ok) 274 require.NotEmpty(files) 275 require.True(files[0].IsDir) 276 }) 277 } 278 279 func TestHTTP_FS_Stat(t *testing.T) { 280 t.Parallel() 281 require := require.New(t) 282 httpTest(t, nil, func(s *TestAgent) { 283 a := mockFSAlloc(s.client.NodeID(), nil) 284 addAllocToClient(s, a, terminalClientAlloc) 285 286 path := fmt.Sprintf("/v1/client/fs/stat/%s?path=alloc/", a.ID) 287 req, err := http.NewRequest("GET", path, nil) 288 require.Nil(err) 289 respW := httptest.NewRecorder() 290 raw, err := s.Server.FileStatRequest(respW, req) 291 require.Nil(err) 292 293 info, ok := raw.(*cstructs.AllocFileInfo) 294 require.True(ok) 295 require.NotNil(info) 296 require.True(info.IsDir) 297 }) 298 } 299 300 func TestHTTP_FS_ReadAt(t *testing.T) { 301 t.Parallel() 302 require := require.New(t) 303 httpTest(t, nil, func(s *TestAgent) { 304 a := mockFSAlloc(s.client.NodeID(), nil) 305 addAllocToClient(s, a, terminalClientAlloc) 306 307 offset := 1 308 limit := 3 309 expectation := defaultLoggerMockDriverStdout[offset : offset+limit] 310 path := fmt.Sprintf("/v1/client/fs/readat/%s?path=alloc/logs/web.stdout.0&offset=%d&limit=%d", 311 a.ID, offset, limit) 312 313 req, err := http.NewRequest("GET", path, nil) 314 require.Nil(err) 315 respW := httptest.NewRecorder() 316 _, err = s.Server.FileReadAtRequest(respW, req) 317 require.Nil(err) 318 319 output, err := ioutil.ReadAll(respW.Result().Body) 320 require.Nil(err) 321 require.EqualValues(expectation, output) 322 }) 323 } 324 325 func TestHTTP_FS_Cat(t *testing.T) { 326 t.Parallel() 327 require := require.New(t) 328 httpTest(t, nil, func(s *TestAgent) { 329 a := mockFSAlloc(s.client.NodeID(), nil) 330 addAllocToClient(s, a, terminalClientAlloc) 331 332 path := fmt.Sprintf("/v1/client/fs/cat/%s?path=alloc/logs/web.stdout.0", a.ID) 333 334 req, err := http.NewRequest("GET", path, nil) 335 require.Nil(err) 336 respW := httptest.NewRecorder() 337 _, err = s.Server.FileCatRequest(respW, req) 338 require.Nil(err) 339 340 output, err := ioutil.ReadAll(respW.Result().Body) 341 require.Nil(err) 342 require.EqualValues(defaultLoggerMockDriverStdout, output) 343 }) 344 } 345 346 func TestHTTP_FS_Stream_NoFollow(t *testing.T) { 347 t.Parallel() 348 require := require.New(t) 349 httpTest(t, nil, func(s *TestAgent) { 350 a := mockFSAlloc(s.client.NodeID(), nil) 351 addAllocToClient(s, a, terminalClientAlloc) 352 353 offset := 4 354 expectation := base64.StdEncoding.EncodeToString( 355 []byte(defaultLoggerMockDriverStdout[len(defaultLoggerMockDriverStdout)-offset:])) 356 path := fmt.Sprintf("/v1/client/fs/stream/%s?path=alloc/logs/web.stdout.0&offset=%d&origin=end&follow=false", 357 a.ID, offset) 358 359 req, err := http.NewRequest("GET", path, nil) 360 require.Nil(err) 361 respW := testutil.NewResponseRecorder() 362 doneCh := make(chan struct{}) 363 go func() { 364 _, err = s.Server.Stream(respW, req) 365 require.Nil(err) 366 close(doneCh) 367 }() 368 369 out := "" 370 testutil.WaitForResult(func() (bool, error) { 371 output, err := ioutil.ReadAll(respW) 372 if err != nil { 373 return false, err 374 } 375 376 out += string(output) 377 return strings.Contains(out, expectation), fmt.Errorf("%q doesn't contain %q", out, expectation) 378 }, func(err error) { 379 t.Fatal(err) 380 }) 381 382 select { 383 case <-doneCh: 384 case <-time.After(1 * time.Second): 385 t.Fatal("should close but did not") 386 } 387 }) 388 } 389 390 func TestHTTP_FS_Stream_Follow(t *testing.T) { 391 t.Parallel() 392 require := require.New(t) 393 httpTest(t, nil, func(s *TestAgent) { 394 a := mockFSAlloc(s.client.NodeID(), nil) 395 addAllocToClient(s, a, terminalClientAlloc) 396 397 offset := 4 398 expectation := base64.StdEncoding.EncodeToString( 399 []byte(defaultLoggerMockDriverStdout[len(defaultLoggerMockDriverStdout)-offset:])) 400 path := fmt.Sprintf("/v1/client/fs/stream/%s?path=alloc/logs/web.stdout.0&offset=%d&origin=end", 401 a.ID, offset) 402 403 req, err := http.NewRequest("GET", path, nil) 404 require.Nil(err) 405 respW := httptest.NewRecorder() 406 doneCh := make(chan struct{}) 407 go func() { 408 _, err = s.Server.Stream(respW, req) 409 require.Nil(err) 410 close(doneCh) 411 }() 412 413 out := "" 414 testutil.WaitForResult(func() (bool, error) { 415 output, err := ioutil.ReadAll(respW.Body) 416 if err != nil { 417 return false, err 418 } 419 420 out += string(output) 421 return strings.Contains(out, expectation), fmt.Errorf("%q doesn't contain %q", out, expectation) 422 }, func(err error) { 423 t.Fatal(err) 424 }) 425 426 select { 427 case <-doneCh: 428 t.Fatal("shouldn't close") 429 case <-time.After(1 * time.Second): 430 } 431 }) 432 } 433 434 func TestHTTP_FS_Logs(t *testing.T) { 435 t.Parallel() 436 require := require.New(t) 437 httpTest(t, nil, func(s *TestAgent) { 438 a := mockFSAlloc(s.client.NodeID(), nil) 439 addAllocToClient(s, a, terminalClientAlloc) 440 441 offset := 4 442 expectation := defaultLoggerMockDriverStdout[len(defaultLoggerMockDriverStdout)-offset:] 443 path := fmt.Sprintf("/v1/client/fs/logs/%s?type=stdout&task=web&offset=%d&origin=end&plain=true", 444 a.ID, offset) 445 446 req, err := http.NewRequest("GET", path, nil) 447 require.Nil(err) 448 respW := testutil.NewResponseRecorder() 449 go func() { 450 _, err = s.Server.Logs(respW, req) 451 require.Nil(err) 452 }() 453 454 out := "" 455 testutil.WaitForResult(func() (bool, error) { 456 output, err := ioutil.ReadAll(respW) 457 if err != nil { 458 return false, err 459 } 460 461 out += string(output) 462 return out == expectation, fmt.Errorf("%q != %q", out, expectation) 463 }, func(err error) { 464 t.Fatal(err) 465 }) 466 }) 467 } 468 469 func TestHTTP_FS_Logs_Follow(t *testing.T) { 470 t.Parallel() 471 require := require.New(t) 472 httpTest(t, nil, func(s *TestAgent) { 473 a := mockFSAlloc(s.client.NodeID(), nil) 474 addAllocToClient(s, a, terminalClientAlloc) 475 476 offset := 4 477 expectation := defaultLoggerMockDriverStdout[len(defaultLoggerMockDriverStdout)-offset:] 478 path := fmt.Sprintf("/v1/client/fs/logs/%s?type=stdout&task=web&offset=%d&origin=end&plain=true&follow=true", 479 a.ID, offset) 480 481 req, err := http.NewRequest("GET", path, nil) 482 require.Nil(err) 483 respW := testutil.NewResponseRecorder() 484 errCh := make(chan error) 485 go func() { 486 _, err := s.Server.Logs(respW, req) 487 errCh <- err 488 }() 489 490 out := "" 491 testutil.WaitForResult(func() (bool, error) { 492 output, err := ioutil.ReadAll(respW) 493 if err != nil { 494 return false, err 495 } 496 497 out += string(output) 498 return out == expectation, fmt.Errorf("%q != %q", out, expectation) 499 }, func(err error) { 500 t.Fatal(err) 501 }) 502 503 select { 504 case err := <-errCh: 505 t.Fatalf("shouldn't exit: %v", err) 506 case <-time.After(1 * time.Second): 507 } 508 }) 509 } 510 511 func TestHTTP_FS_Logs_PropagatesErrors(t *testing.T) { 512 t.Parallel() 513 httpTest(t, nil, func(s *TestAgent) { 514 path := fmt.Sprintf("/v1/client/fs/logs/%s?type=stdout&task=web&offset=0&origin=end&plain=true", 515 uuid.Generate()) 516 517 req, err := http.NewRequest("GET", path, nil) 518 require.NoError(t, err) 519 respW := testutil.NewResponseRecorder() 520 521 _, err = s.Server.Logs(respW, req) 522 require.Error(t, err) 523 524 _, ok := err.(HTTPCodedError) 525 require.Truef(t, ok, "expected a coded error but found: %#+v", err) 526 }) 527 }