github.com/bigcommerce/nomad@v0.9.3-bc/command/agent/fs_endpoint_test.go (about) 1 package agent 2 3 import ( 4 "encoding/base64" 5 "fmt" 6 "io" 7 "io/ioutil" 8 "net/http" 9 "net/http/httptest" 10 "strings" 11 "testing" 12 "time" 13 14 cstructs "github.com/hashicorp/nomad/client/structs" 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.Nil(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.Nil(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.Nil(err) 207 respW = httptest.NewRecorder() 208 209 _, err = s.Server.Stream(respW, req) 210 require.Nil(err) 211 }) 212 } 213 214 // TestHTTP_FS_Logs_MissingParams asserts proper error codes and messages are 215 // returned for incorrect parameters (eg missing tasks). 216 func TestHTTP_FS_Logs_MissingParams(t *testing.T) { 217 t.Parallel() 218 require := require.New(t) 219 httpTest(t, nil, func(s *TestAgent) { 220 // AllocID Not Present 221 req, err := http.NewRequest("GET", "/v1/client/fs/logs/", nil) 222 require.Nil(err) 223 respW := httptest.NewRecorder() 224 225 s.Server.mux.ServeHTTP(respW, req) 226 require.Equal(respW.Body.String(), allocIDNotPresentErr.Error()) 227 require.Equal(500, respW.Code) // 500 for backward compat 228 229 // Task Not Present 230 req, err = http.NewRequest("GET", "/v1/client/fs/logs/foo", nil) 231 require.Nil(err) 232 respW = httptest.NewRecorder() 233 234 s.Server.mux.ServeHTTP(respW, req) 235 require.Equal(respW.Body.String(), taskNotPresentErr.Error()) 236 require.Equal(500, respW.Code) // 500 for backward compat 237 238 // Log Type Not Present 239 req, err = http.NewRequest("GET", "/v1/client/fs/logs/foo?task=foo", nil) 240 require.Nil(err) 241 respW = httptest.NewRecorder() 242 243 s.Server.mux.ServeHTTP(respW, req) 244 require.Equal(respW.Body.String(), logTypeNotPresentErr.Error()) 245 require.Equal(500, respW.Code) // 500 for backward compat 246 247 // Ok 248 req, err = http.NewRequest("GET", "/v1/client/fs/logs/foo?task=foo&type=stdout", nil) 249 require.Nil(err) 250 respW = httptest.NewRecorder() 251 252 s.Server.mux.ServeHTTP(respW, req) 253 require.Equal(200, respW.Code) 254 }) 255 } 256 257 func TestHTTP_FS_List(t *testing.T) { 258 t.Parallel() 259 require := require.New(t) 260 httpTest(t, nil, func(s *TestAgent) { 261 a := mockFSAlloc(s.client.NodeID(), nil) 262 addAllocToClient(s, a, terminalClientAlloc) 263 264 req, err := http.NewRequest("GET", "/v1/client/fs/ls/"+a.ID, nil) 265 require.Nil(err) 266 respW := httptest.NewRecorder() 267 raw, err := s.Server.DirectoryListRequest(respW, req) 268 require.Nil(err) 269 270 files, ok := raw.([]*cstructs.AllocFileInfo) 271 require.True(ok) 272 require.NotEmpty(files) 273 require.True(files[0].IsDir) 274 }) 275 } 276 277 func TestHTTP_FS_Stat(t *testing.T) { 278 t.Parallel() 279 require := require.New(t) 280 httpTest(t, nil, func(s *TestAgent) { 281 a := mockFSAlloc(s.client.NodeID(), nil) 282 addAllocToClient(s, a, terminalClientAlloc) 283 284 path := fmt.Sprintf("/v1/client/fs/stat/%s?path=alloc/", a.ID) 285 req, err := http.NewRequest("GET", path, nil) 286 require.Nil(err) 287 respW := httptest.NewRecorder() 288 raw, err := s.Server.FileStatRequest(respW, req) 289 require.Nil(err) 290 291 info, ok := raw.(*cstructs.AllocFileInfo) 292 require.True(ok) 293 require.NotNil(info) 294 require.True(info.IsDir) 295 }) 296 } 297 298 func TestHTTP_FS_ReadAt(t *testing.T) { 299 t.Parallel() 300 require := require.New(t) 301 httpTest(t, nil, func(s *TestAgent) { 302 a := mockFSAlloc(s.client.NodeID(), nil) 303 addAllocToClient(s, a, terminalClientAlloc) 304 305 offset := 1 306 limit := 3 307 expectation := defaultLoggerMockDriverStdout[offset : offset+limit] 308 path := fmt.Sprintf("/v1/client/fs/readat/%s?path=alloc/logs/web.stdout.0&offset=%d&limit=%d", 309 a.ID, offset, limit) 310 311 req, err := http.NewRequest("GET", path, nil) 312 require.Nil(err) 313 respW := httptest.NewRecorder() 314 _, err = s.Server.FileReadAtRequest(respW, req) 315 require.Nil(err) 316 317 output, err := ioutil.ReadAll(respW.Result().Body) 318 require.Nil(err) 319 require.EqualValues(expectation, output) 320 }) 321 } 322 323 func TestHTTP_FS_Cat(t *testing.T) { 324 t.Parallel() 325 require := require.New(t) 326 httpTest(t, nil, func(s *TestAgent) { 327 a := mockFSAlloc(s.client.NodeID(), nil) 328 addAllocToClient(s, a, terminalClientAlloc) 329 330 path := fmt.Sprintf("/v1/client/fs/cat/%s?path=alloc/logs/web.stdout.0", a.ID) 331 332 req, err := http.NewRequest("GET", path, nil) 333 require.Nil(err) 334 respW := httptest.NewRecorder() 335 _, err = s.Server.FileCatRequest(respW, req) 336 require.Nil(err) 337 338 output, err := ioutil.ReadAll(respW.Result().Body) 339 require.Nil(err) 340 require.EqualValues(defaultLoggerMockDriverStdout, output) 341 }) 342 } 343 344 func TestHTTP_FS_Stream(t *testing.T) { 345 t.Parallel() 346 require := require.New(t) 347 httpTest(t, nil, func(s *TestAgent) { 348 a := mockFSAlloc(s.client.NodeID(), nil) 349 addAllocToClient(s, a, terminalClientAlloc) 350 351 offset := 4 352 expectation := base64.StdEncoding.EncodeToString( 353 []byte(defaultLoggerMockDriverStdout[len(defaultLoggerMockDriverStdout)-offset:])) 354 path := fmt.Sprintf("/v1/client/fs/stream/%s?path=alloc/logs/web.stdout.0&offset=%d&origin=end", 355 a.ID, offset) 356 357 p, _ := io.Pipe() 358 359 req, err := http.NewRequest("GET", path, p) 360 require.Nil(err) 361 respW := httptest.NewRecorder() 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.Body) 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 t.Fatal("shouldn't close") 385 case <-time.After(1 * time.Second): 386 } 387 388 p.Close() 389 }) 390 } 391 392 func TestHTTP_FS_Logs(t *testing.T) { 393 t.Parallel() 394 require := require.New(t) 395 httpTest(t, nil, func(s *TestAgent) { 396 a := mockFSAlloc(s.client.NodeID(), nil) 397 addAllocToClient(s, a, terminalClientAlloc) 398 399 offset := 4 400 expectation := defaultLoggerMockDriverStdout[len(defaultLoggerMockDriverStdout)-offset:] 401 path := fmt.Sprintf("/v1/client/fs/logs/%s?type=stdout&task=web&offset=%d&origin=end&plain=true", 402 a.ID, offset) 403 404 p, _ := io.Pipe() 405 req, err := http.NewRequest("GET", path, p) 406 require.Nil(err) 407 respW := testutil.NewResponseRecorder() 408 go func() { 409 _, err = s.Server.Logs(respW, req) 410 require.Nil(err) 411 }() 412 413 out := "" 414 testutil.WaitForResult(func() (bool, error) { 415 output, err := ioutil.ReadAll(respW) 416 if err != nil { 417 return false, err 418 } 419 420 out += string(output) 421 return out == expectation, fmt.Errorf("%q != %q", out, expectation) 422 }, func(err error) { 423 t.Fatal(err) 424 }) 425 426 p.Close() 427 }) 428 } 429 430 func TestHTTP_FS_Logs_Follow(t *testing.T) { 431 t.Parallel() 432 require := require.New(t) 433 httpTest(t, nil, func(s *TestAgent) { 434 a := mockFSAlloc(s.client.NodeID(), nil) 435 addAllocToClient(s, a, terminalClientAlloc) 436 437 offset := 4 438 expectation := defaultLoggerMockDriverStdout[len(defaultLoggerMockDriverStdout)-offset:] 439 path := fmt.Sprintf("/v1/client/fs/logs/%s?type=stdout&task=web&offset=%d&origin=end&plain=true&follow=true", 440 a.ID, offset) 441 442 p, _ := io.Pipe() 443 req, err := http.NewRequest("GET", path, p) 444 require.Nil(err) 445 respW := testutil.NewResponseRecorder() 446 errCh := make(chan error) 447 go func() { 448 _, err := s.Server.Logs(respW, req) 449 errCh <- err 450 }() 451 452 out := "" 453 testutil.WaitForResult(func() (bool, error) { 454 output, err := ioutil.ReadAll(respW) 455 if err != nil { 456 return false, err 457 } 458 459 out += string(output) 460 return out == expectation, fmt.Errorf("%q != %q", out, expectation) 461 }, func(err error) { 462 t.Fatal(err) 463 }) 464 465 select { 466 case err := <-errCh: 467 t.Fatalf("shouldn't exit: %v", err) 468 case <-time.After(1 * time.Second): 469 } 470 471 p.Close() 472 }) 473 }