github.com/bigcommerce/nomad@v0.9.3-bc/drivers/exec/driver_test.go (about) 1 package exec 2 3 import ( 4 "bytes" 5 "context" 6 "fmt" 7 "io/ioutil" 8 "os" 9 "path/filepath" 10 "runtime" 11 "strings" 12 "sync" 13 "testing" 14 "time" 15 16 ctestutils "github.com/hashicorp/nomad/client/testutil" 17 "github.com/hashicorp/nomad/helper/pluginutils/hclutils" 18 "github.com/hashicorp/nomad/helper/testlog" 19 "github.com/hashicorp/nomad/helper/testtask" 20 "github.com/hashicorp/nomad/helper/uuid" 21 "github.com/hashicorp/nomad/nomad/structs" 22 "github.com/hashicorp/nomad/plugins/drivers" 23 dtestutil "github.com/hashicorp/nomad/plugins/drivers/testutils" 24 "github.com/hashicorp/nomad/testutil" 25 "github.com/stretchr/testify/require" 26 ) 27 28 func TestMain(m *testing.M) { 29 if !testtask.Run() { 30 os.Exit(m.Run()) 31 } 32 } 33 34 var testResources = &drivers.Resources{ 35 NomadResources: &structs.AllocatedTaskResources{ 36 Memory: structs.AllocatedMemoryResources{ 37 MemoryMB: 128, 38 }, 39 Cpu: structs.AllocatedCpuResources{ 40 CpuShares: 100, 41 }, 42 }, 43 LinuxResources: &drivers.LinuxResources{ 44 MemoryLimitBytes: 134217728, 45 CPUShares: 100, 46 }, 47 } 48 49 func TestExecDriver_Fingerprint_NonLinux(t *testing.T) { 50 if !testutil.IsCI() { 51 t.Parallel() 52 } 53 require := require.New(t) 54 if runtime.GOOS == "linux" { 55 t.Skip("Test only available not on Linux") 56 } 57 58 d := NewExecDriver(testlog.HCLogger(t)) 59 harness := dtestutil.NewDriverHarness(t, d) 60 61 fingerCh, err := harness.Fingerprint(context.Background()) 62 require.NoError(err) 63 select { 64 case finger := <-fingerCh: 65 require.Equal(drivers.HealthStateUndetected, finger.Health) 66 case <-time.After(time.Duration(testutil.TestMultiplier()*5) * time.Second): 67 require.Fail("timeout receiving fingerprint") 68 } 69 } 70 71 func TestExecDriver_Fingerprint(t *testing.T) { 72 t.Parallel() 73 require := require.New(t) 74 75 ctestutils.ExecCompatible(t) 76 77 d := NewExecDriver(testlog.HCLogger(t)) 78 harness := dtestutil.NewDriverHarness(t, d) 79 80 fingerCh, err := harness.Fingerprint(context.Background()) 81 require.NoError(err) 82 select { 83 case finger := <-fingerCh: 84 require.Equal(drivers.HealthStateHealthy, finger.Health) 85 require.True(finger.Attributes["driver.exec"].GetBool()) 86 case <-time.After(time.Duration(testutil.TestMultiplier()*5) * time.Second): 87 require.Fail("timeout receiving fingerprint") 88 } 89 } 90 91 func TestExecDriver_StartWait(t *testing.T) { 92 t.Parallel() 93 require := require.New(t) 94 ctestutils.ExecCompatible(t) 95 96 d := NewExecDriver(testlog.HCLogger(t)) 97 harness := dtestutil.NewDriverHarness(t, d) 98 task := &drivers.TaskConfig{ 99 ID: uuid.Generate(), 100 Name: "test", 101 Resources: testResources, 102 } 103 104 tc := &TaskConfig{ 105 Command: "cat", 106 Args: []string{"/proc/self/cgroup"}, 107 } 108 require.NoError(task.EncodeConcreteDriverConfig(&tc)) 109 110 cleanup := harness.MkAllocDir(task, false) 111 defer cleanup() 112 113 handle, _, err := harness.StartTask(task) 114 require.NoError(err) 115 116 ch, err := harness.WaitTask(context.Background(), handle.Config.ID) 117 require.NoError(err) 118 result := <-ch 119 require.Zero(result.ExitCode) 120 require.NoError(harness.DestroyTask(task.ID, true)) 121 } 122 123 func TestExecDriver_StartWaitStopKill(t *testing.T) { 124 t.Parallel() 125 require := require.New(t) 126 ctestutils.ExecCompatible(t) 127 128 d := NewExecDriver(testlog.HCLogger(t)) 129 harness := dtestutil.NewDriverHarness(t, d) 130 task := &drivers.TaskConfig{ 131 ID: uuid.Generate(), 132 Name: "test", 133 Resources: testResources, 134 } 135 136 tc := &TaskConfig{ 137 Command: "/bin/bash", 138 Args: []string{"-c", "echo hi; sleep 600"}, 139 } 140 require.NoError(task.EncodeConcreteDriverConfig(&tc)) 141 142 cleanup := harness.MkAllocDir(task, false) 143 defer cleanup() 144 145 handle, _, err := harness.StartTask(task) 146 require.NoError(err) 147 defer harness.DestroyTask(task.ID, true) 148 149 ch, err := harness.WaitTask(context.Background(), handle.Config.ID) 150 require.NoError(err) 151 152 require.NoError(harness.WaitUntilStarted(task.ID, 1*time.Second)) 153 154 go func() { 155 harness.StopTask(task.ID, 2*time.Second, "SIGINT") 156 }() 157 158 select { 159 case result := <-ch: 160 require.False(result.Successful()) 161 case <-time.After(10 * time.Second): 162 require.Fail("timeout waiting for task to shutdown") 163 } 164 165 // Ensure that the task is marked as dead, but account 166 // for WaitTask() closing channel before internal state is updated 167 testutil.WaitForResult(func() (bool, error) { 168 status, err := harness.InspectTask(task.ID) 169 if err != nil { 170 return false, fmt.Errorf("inspecting task failed: %v", err) 171 } 172 if status.State != drivers.TaskStateExited { 173 return false, fmt.Errorf("task hasn't exited yet; status: %v", status.State) 174 } 175 176 return true, nil 177 }, func(err error) { 178 require.NoError(err) 179 }) 180 181 require.NoError(harness.DestroyTask(task.ID, true)) 182 } 183 184 func TestExecDriver_StartWaitRecover(t *testing.T) { 185 t.Parallel() 186 require := require.New(t) 187 ctestutils.ExecCompatible(t) 188 189 d := NewExecDriver(testlog.HCLogger(t)) 190 harness := dtestutil.NewDriverHarness(t, d) 191 task := &drivers.TaskConfig{ 192 ID: uuid.Generate(), 193 Name: "test", 194 Resources: testResources, 195 } 196 197 tc := &TaskConfig{ 198 Command: "/bin/sleep", 199 Args: []string{"5"}, 200 } 201 require.NoError(task.EncodeConcreteDriverConfig(&tc)) 202 203 cleanup := harness.MkAllocDir(task, false) 204 defer cleanup() 205 206 handle, _, err := harness.StartTask(task) 207 require.NoError(err) 208 209 ctx, cancel := context.WithCancel(context.Background()) 210 211 ch, err := harness.WaitTask(ctx, handle.Config.ID) 212 require.NoError(err) 213 214 var wg sync.WaitGroup 215 wg.Add(1) 216 go func() { 217 defer wg.Done() 218 result := <-ch 219 require.Error(result.Err) 220 }() 221 222 require.NoError(harness.WaitUntilStarted(task.ID, 1*time.Second)) 223 cancel() 224 225 waitCh := make(chan struct{}) 226 go func() { 227 defer close(waitCh) 228 wg.Wait() 229 }() 230 231 select { 232 case <-waitCh: 233 status, err := harness.InspectTask(task.ID) 234 require.NoError(err) 235 require.Equal(drivers.TaskStateRunning, status.State) 236 case <-time.After(1 * time.Second): 237 require.Fail("timeout waiting for task wait to cancel") 238 } 239 240 // Loose task 241 d.(*Driver).tasks.Delete(task.ID) 242 _, err = harness.InspectTask(task.ID) 243 require.Error(err) 244 245 require.NoError(harness.RecoverTask(handle)) 246 status, err := harness.InspectTask(task.ID) 247 require.NoError(err) 248 require.Equal(drivers.TaskStateRunning, status.State) 249 250 require.NoError(harness.StopTask(task.ID, 0, "")) 251 require.NoError(harness.DestroyTask(task.ID, true)) 252 } 253 254 func TestExecDriver_Stats(t *testing.T) { 255 t.Parallel() 256 require := require.New(t) 257 ctestutils.ExecCompatible(t) 258 259 d := NewExecDriver(testlog.HCLogger(t)) 260 harness := dtestutil.NewDriverHarness(t, d) 261 task := &drivers.TaskConfig{ 262 ID: uuid.Generate(), 263 Name: "test", 264 Resources: testResources, 265 } 266 267 tc := &TaskConfig{ 268 Command: "/bin/sleep", 269 Args: []string{"5"}, 270 } 271 require.NoError(task.EncodeConcreteDriverConfig(&tc)) 272 273 cleanup := harness.MkAllocDir(task, false) 274 defer cleanup() 275 276 handle, _, err := harness.StartTask(task) 277 require.NoError(err) 278 require.NotNil(handle) 279 280 require.NoError(harness.WaitUntilStarted(task.ID, 1*time.Second)) 281 ctx, cancel := context.WithCancel(context.Background()) 282 defer cancel() 283 statsCh, err := harness.TaskStats(ctx, task.ID, time.Second*10) 284 require.NoError(err) 285 select { 286 case stats := <-statsCh: 287 require.NotZero(stats.ResourceUsage.MemoryStats.RSS) 288 require.NotZero(stats.Timestamp) 289 require.WithinDuration(time.Now(), time.Unix(0, stats.Timestamp), time.Second) 290 case <-time.After(time.Second): 291 require.Fail("timeout receiving from channel") 292 } 293 294 require.NoError(harness.DestroyTask(task.ID, true)) 295 } 296 297 func TestExecDriver_Start_Wait_AllocDir(t *testing.T) { 298 t.Parallel() 299 require := require.New(t) 300 ctestutils.ExecCompatible(t) 301 302 d := NewExecDriver(testlog.HCLogger(t)) 303 harness := dtestutil.NewDriverHarness(t, d) 304 task := &drivers.TaskConfig{ 305 ID: uuid.Generate(), 306 Name: "sleep", 307 Resources: testResources, 308 } 309 cleanup := harness.MkAllocDir(task, false) 310 defer cleanup() 311 312 exp := []byte{'w', 'i', 'n'} 313 file := "output.txt" 314 tc := &TaskConfig{ 315 Command: "/bin/bash", 316 Args: []string{ 317 "-c", 318 fmt.Sprintf(`sleep 1; echo -n %s > /alloc/%s`, string(exp), file), 319 }, 320 } 321 require.NoError(task.EncodeConcreteDriverConfig(&tc)) 322 323 handle, _, err := harness.StartTask(task) 324 require.NoError(err) 325 require.NotNil(handle) 326 327 // Task should terminate quickly 328 waitCh, err := harness.WaitTask(context.Background(), task.ID) 329 require.NoError(err) 330 select { 331 case res := <-waitCh: 332 require.True(res.Successful(), "task should have exited successfully: %v", res) 333 case <-time.After(time.Duration(testutil.TestMultiplier()*5) * time.Second): 334 require.Fail("timeout waiting for task") 335 } 336 337 // Check that data was written to the shared alloc directory. 338 outputFile := filepath.Join(task.TaskDir().SharedAllocDir, file) 339 act, err := ioutil.ReadFile(outputFile) 340 require.NoError(err) 341 require.Exactly(exp, act) 342 343 require.NoError(harness.DestroyTask(task.ID, true)) 344 } 345 346 func TestExecDriver_User(t *testing.T) { 347 t.Parallel() 348 require := require.New(t) 349 ctestutils.ExecCompatible(t) 350 351 d := NewExecDriver(testlog.HCLogger(t)) 352 harness := dtestutil.NewDriverHarness(t, d) 353 task := &drivers.TaskConfig{ 354 ID: uuid.Generate(), 355 Name: "sleep", 356 User: "alice", 357 Resources: testResources, 358 } 359 cleanup := harness.MkAllocDir(task, false) 360 defer cleanup() 361 362 tc := &TaskConfig{ 363 Command: "/bin/sleep", 364 Args: []string{"100"}, 365 } 366 require.NoError(task.EncodeConcreteDriverConfig(&tc)) 367 368 handle, _, err := harness.StartTask(task) 369 require.Error(err) 370 require.Nil(handle) 371 372 msg := "user alice" 373 if !strings.Contains(err.Error(), msg) { 374 t.Fatalf("Expecting '%v' in '%v'", msg, err) 375 } 376 } 377 378 // TestExecDriver_HandlerExec ensures the exec driver's handle properly 379 // executes commands inside the container. 380 func TestExecDriver_HandlerExec(t *testing.T) { 381 t.Parallel() 382 require := require.New(t) 383 ctestutils.ExecCompatible(t) 384 385 d := NewExecDriver(testlog.HCLogger(t)) 386 harness := dtestutil.NewDriverHarness(t, d) 387 task := &drivers.TaskConfig{ 388 ID: uuid.Generate(), 389 Name: "sleep", 390 Resources: testResources, 391 } 392 cleanup := harness.MkAllocDir(task, false) 393 defer cleanup() 394 395 tc := &TaskConfig{ 396 Command: "/bin/sleep", 397 Args: []string{"9000"}, 398 } 399 require.NoError(task.EncodeConcreteDriverConfig(&tc)) 400 401 handle, _, err := harness.StartTask(task) 402 require.NoError(err) 403 require.NotNil(handle) 404 405 // Exec a command that should work and dump the environment 406 // TODO: enable section when exec env is fully loaded 407 /*res, err := harness.ExecTask(task.ID, []string{"/bin/sh", "-c", "env | grep ^NOMAD"}, time.Second) 408 require.NoError(err) 409 require.True(res.ExitResult.Successful()) 410 411 // Assert exec'd commands are run in a task-like environment 412 scriptEnv := make(map[string]string) 413 for _, line := range strings.Split(string(res.Stdout), "\n") { 414 if line == "" { 415 continue 416 } 417 parts := strings.SplitN(string(line), "=", 2) 418 if len(parts) != 2 { 419 t.Fatalf("Invalid env var: %q", line) 420 } 421 scriptEnv[parts[0]] = parts[1] 422 } 423 if v, ok := scriptEnv["NOMAD_SECRETS_DIR"]; !ok || v != "/secrets" { 424 t.Errorf("Expected NOMAD_SECRETS_DIR=/secrets but found=%t value=%q", ok, v) 425 }*/ 426 427 // Assert cgroup membership 428 res, err := harness.ExecTask(task.ID, []string{"/bin/cat", "/proc/self/cgroup"}, time.Second) 429 require.NoError(err) 430 require.True(res.ExitResult.Successful()) 431 found := false 432 for _, line := range strings.Split(string(res.Stdout), "\n") { 433 // Every cgroup entry should be /nomad/$ALLOC_ID 434 if line == "" { 435 continue 436 } 437 // Skip rdma subsystem; rdma was added in most recent kernels and libcontainer/docker 438 // don't isolate it by default. 439 if strings.Contains(line, ":rdma:") { 440 continue 441 } 442 if !strings.Contains(line, ":/nomad/") { 443 t.Errorf("Not a member of the alloc's cgroup: expected=...:/nomad/... -- found=%q", line) 444 continue 445 } 446 found = true 447 } 448 require.True(found, "exec'd command isn't in the task's cgroup") 449 450 // Exec a command that should fail 451 res, err = harness.ExecTask(task.ID, []string{"/usr/bin/stat", "lkjhdsaflkjshowaisxmcvnlia"}, time.Second) 452 require.NoError(err) 453 require.False(res.ExitResult.Successful()) 454 if expected := "No such file or directory"; !bytes.Contains(res.Stdout, []byte(expected)) { 455 t.Fatalf("expected output to contain %q but found: %q", expected, res.Stdout) 456 } 457 458 require.NoError(harness.DestroyTask(task.ID, true)) 459 } 460 461 func TestExecDriver_DevicesAndMounts(t *testing.T) { 462 t.Parallel() 463 require := require.New(t) 464 ctestutils.ExecCompatible(t) 465 466 tmpDir, err := ioutil.TempDir("", "exec_binds_mounts") 467 require.NoError(err) 468 defer os.RemoveAll(tmpDir) 469 470 err = ioutil.WriteFile(filepath.Join(tmpDir, "testfile"), []byte("from-host"), 600) 471 require.NoError(err) 472 473 d := NewExecDriver(testlog.HCLogger(t)) 474 harness := dtestutil.NewDriverHarness(t, d) 475 task := &drivers.TaskConfig{ 476 ID: uuid.Generate(), 477 Name: "test", 478 User: "root", // need permission to read mounts paths 479 Resources: testResources, 480 StdoutPath: filepath.Join(tmpDir, "task-stdout"), 481 StderrPath: filepath.Join(tmpDir, "task-stderr"), 482 Devices: []*drivers.DeviceConfig{ 483 { 484 TaskPath: "/dev/inserted-random", 485 HostPath: "/dev/random", 486 Permissions: "rw", 487 }, 488 }, 489 Mounts: []*drivers.MountConfig{ 490 { 491 TaskPath: "/tmp/task-path-rw", 492 HostPath: tmpDir, 493 Readonly: false, 494 }, 495 { 496 TaskPath: "/tmp/task-path-ro", 497 HostPath: tmpDir, 498 Readonly: true, 499 }, 500 }, 501 } 502 503 require.NoError(ioutil.WriteFile(task.StdoutPath, []byte{}, 660)) 504 require.NoError(ioutil.WriteFile(task.StderrPath, []byte{}, 660)) 505 506 tc := &TaskConfig{ 507 Command: "/bin/bash", 508 Args: []string{"-c", ` 509 export LANG=en.UTF-8 510 echo "mounted device /inserted-random: $(stat -c '%t:%T' /dev/inserted-random)" 511 echo "reading from ro path: $(cat /tmp/task-path-ro/testfile)" 512 echo "reading from rw path: $(cat /tmp/task-path-rw/testfile)" 513 touch /tmp/task-path-rw/testfile && echo 'overwriting file in rw succeeded' 514 touch /tmp/task-path-rw/testfile-from-rw && echo from-exec > /tmp/task-path-rw/testfile-from-rw && echo 'writing new file in rw succeeded' 515 touch /tmp/task-path-ro/testfile && echo 'overwriting file in ro succeeded' 516 touch /tmp/task-path-ro/testfile-from-ro && echo from-exec > /tmp/task-path-ro/testfile-from-ro && echo 'writing new file in ro succeeded' 517 exit 0 518 `}, 519 } 520 require.NoError(task.EncodeConcreteDriverConfig(&tc)) 521 522 cleanup := harness.MkAllocDir(task, false) 523 defer cleanup() 524 525 handle, _, err := harness.StartTask(task) 526 require.NoError(err) 527 528 ch, err := harness.WaitTask(context.Background(), handle.Config.ID) 529 require.NoError(err) 530 result := <-ch 531 require.NoError(harness.DestroyTask(task.ID, true)) 532 533 stdout, err := ioutil.ReadFile(task.StdoutPath) 534 require.NoError(err) 535 require.Equal(`mounted device /inserted-random: 1:8 536 reading from ro path: from-host 537 reading from rw path: from-host 538 overwriting file in rw succeeded 539 writing new file in rw succeeded`, strings.TrimSpace(string(stdout))) 540 541 stderr, err := ioutil.ReadFile(task.StderrPath) 542 require.NoError(err) 543 require.Equal(`touch: cannot touch '/tmp/task-path-ro/testfile': Read-only file system 544 touch: cannot touch '/tmp/task-path-ro/testfile-from-ro': Read-only file system`, strings.TrimSpace(string(stderr))) 545 546 // testing exit code last so we can inspect output first 547 require.Zero(result.ExitCode) 548 549 fromRWContent, err := ioutil.ReadFile(filepath.Join(tmpDir, "testfile-from-rw")) 550 require.NoError(err) 551 require.Equal("from-exec", strings.TrimSpace(string(fromRWContent))) 552 } 553 554 func TestConfig_ParseAllHCL(t *testing.T) { 555 cfgStr := ` 556 config { 557 command = "/bin/bash" 558 args = ["-c", "echo hello"] 559 }` 560 561 expected := &TaskConfig{ 562 Command: "/bin/bash", 563 Args: []string{"-c", "echo hello"}, 564 } 565 566 var tc *TaskConfig 567 hclutils.NewConfigParser(taskConfigSpec).ParseHCL(t, cfgStr, &tc) 568 569 require.EqualValues(t, expected, tc) 570 }