github.com/anth0d/nomad@v0.0.0-20221214183521-ae3a0a2cad06/drivers/shared/executor/executor_test.go (about) 1 package executor 2 3 import ( 4 "bytes" 5 "context" 6 "fmt" 7 "io" 8 "io/ioutil" 9 "os" 10 "path/filepath" 11 "runtime" 12 "strings" 13 "sync" 14 "syscall" 15 "testing" 16 "time" 17 18 "github.com/hashicorp/go-hclog" 19 "github.com/hashicorp/nomad/ci" 20 "github.com/hashicorp/nomad/client/allocdir" 21 "github.com/hashicorp/nomad/client/lib/cgutil" 22 "github.com/hashicorp/nomad/client/taskenv" 23 "github.com/hashicorp/nomad/client/testutil" 24 "github.com/hashicorp/nomad/helper/testlog" 25 "github.com/hashicorp/nomad/nomad/mock" 26 "github.com/hashicorp/nomad/nomad/structs" 27 "github.com/hashicorp/nomad/plugins/drivers" 28 tu "github.com/hashicorp/nomad/testutil" 29 ps "github.com/mitchellh/go-ps" 30 "github.com/stretchr/testify/assert" 31 "github.com/stretchr/testify/require" 32 ) 33 34 var executorFactories = map[string]executorFactory{} 35 36 type executorFactory struct { 37 new func(hclog.Logger) Executor 38 configureExecCmd func(*testing.T, *ExecCommand) 39 } 40 41 var universalFactory = executorFactory{ 42 new: NewExecutor, 43 configureExecCmd: func(*testing.T, *ExecCommand) {}, 44 } 45 46 func init() { 47 executorFactories["UniversalExecutor"] = universalFactory 48 } 49 50 type testExecCmd struct { 51 command *ExecCommand 52 allocDir *allocdir.AllocDir 53 54 stdout *bytes.Buffer 55 stderr *bytes.Buffer 56 outputCopyDone *sync.WaitGroup 57 } 58 59 // testExecutorContext returns an ExecutorContext and AllocDir. 60 // 61 // The caller is responsible for calling AllocDir.Destroy() to cleanup. 62 func testExecutorCommand(t *testing.T) *testExecCmd { 63 alloc := mock.Alloc() 64 task := alloc.Job.TaskGroups[0].Tasks[0] 65 taskEnv := taskenv.NewBuilder(mock.Node(), alloc, task, "global").Build() 66 67 allocDir := allocdir.NewAllocDir(testlog.HCLogger(t), t.TempDir(), alloc.ID) 68 if err := allocDir.Build(); err != nil { 69 t.Fatalf("AllocDir.Build() failed: %v", err) 70 } 71 if err := allocDir.NewTaskDir(task.Name).Build(false, nil); err != nil { 72 allocDir.Destroy() 73 t.Fatalf("allocDir.NewTaskDir(%q) failed: %v", task.Name, err) 74 } 75 td := allocDir.TaskDirs[task.Name] 76 cmd := &ExecCommand{ 77 Env: taskEnv.List(), 78 TaskDir: td.Dir, 79 Resources: &drivers.Resources{ 80 NomadResources: &structs.AllocatedTaskResources{ 81 Cpu: structs.AllocatedCpuResources{ 82 CpuShares: 500, 83 }, 84 Memory: structs.AllocatedMemoryResources{ 85 MemoryMB: 256, 86 }, 87 }, 88 LinuxResources: &drivers.LinuxResources{ 89 CPUShares: 500, 90 MemoryLimitBytes: 256 * 1024 * 1024, 91 }, 92 }, 93 } 94 95 if cgutil.UseV2 { 96 cmd.Resources.LinuxResources.CpusetCgroupPath = filepath.Join(cgutil.CgroupRoot, "testing.scope", cgutil.CgroupScope(alloc.ID, task.Name)) 97 } 98 99 testCmd := &testExecCmd{ 100 command: cmd, 101 allocDir: allocDir, 102 } 103 configureTLogging(t, testCmd) 104 return testCmd 105 } 106 107 // configureTLogging configures a test command executor with buffer as Std{out|err} 108 // but using os.Pipe so it mimics non-test case where cmd is set with files as Std{out|err} 109 // the buffers can be used to read command output 110 func configureTLogging(t *testing.T, testcmd *testExecCmd) { 111 var stdout, stderr bytes.Buffer 112 var copyDone sync.WaitGroup 113 114 stdoutPr, stdoutPw, err := os.Pipe() 115 require.NoError(t, err) 116 117 stderrPr, stderrPw, err := os.Pipe() 118 require.NoError(t, err) 119 120 copyDone.Add(2) 121 go func() { 122 defer copyDone.Done() 123 io.Copy(&stdout, stdoutPr) 124 }() 125 go func() { 126 defer copyDone.Done() 127 io.Copy(&stderr, stderrPr) 128 }() 129 130 testcmd.stdout = &stdout 131 testcmd.stderr = &stderr 132 testcmd.outputCopyDone = ©Done 133 134 testcmd.command.stdout = stdoutPw 135 testcmd.command.stderr = stderrPw 136 return 137 } 138 139 func TestExecutor_Start_Invalid(t *testing.T) { 140 ci.Parallel(t) 141 invalid := "/bin/foobar" 142 for name, factory := range executorFactories { 143 t.Run(name, func(t *testing.T) { 144 require := require.New(t) 145 testExecCmd := testExecutorCommand(t) 146 execCmd, allocDir := testExecCmd.command, testExecCmd.allocDir 147 execCmd.Cmd = invalid 148 execCmd.Args = []string{"1"} 149 factory.configureExecCmd(t, execCmd) 150 defer allocDir.Destroy() 151 executor := factory.new(testlog.HCLogger(t)) 152 defer executor.Shutdown("", 0) 153 154 _, err := executor.Launch(execCmd) 155 require.Error(err) 156 }) 157 } 158 } 159 160 func TestExecutor_Start_Wait_Failure_Code(t *testing.T) { 161 ci.Parallel(t) 162 for name, factory := range executorFactories { 163 t.Run(name, func(t *testing.T) { 164 require := require.New(t) 165 testExecCmd := testExecutorCommand(t) 166 execCmd, allocDir := testExecCmd.command, testExecCmd.allocDir 167 execCmd.Cmd = "/bin/sh" 168 execCmd.Args = []string{"-c", "sleep 1; /bin/date fail"} 169 factory.configureExecCmd(t, execCmd) 170 defer allocDir.Destroy() 171 executor := factory.new(testlog.HCLogger(t)) 172 defer executor.Shutdown("", 0) 173 174 ps, err := executor.Launch(execCmd) 175 require.NoError(err) 176 require.NotZero(ps.Pid) 177 ps, _ = executor.Wait(context.Background()) 178 require.NotZero(ps.ExitCode, "expected exit code to be non zero") 179 require.NoError(executor.Shutdown("SIGINT", 100*time.Millisecond)) 180 }) 181 } 182 } 183 184 func TestExecutor_Start_Wait(t *testing.T) { 185 ci.Parallel(t) 186 for name, factory := range executorFactories { 187 t.Run(name, func(t *testing.T) { 188 require := require.New(t) 189 testExecCmd := testExecutorCommand(t) 190 execCmd, allocDir := testExecCmd.command, testExecCmd.allocDir 191 execCmd.Cmd = "/bin/echo" 192 execCmd.Args = []string{"hello world"} 193 factory.configureExecCmd(t, execCmd) 194 195 defer allocDir.Destroy() 196 executor := factory.new(testlog.HCLogger(t)) 197 defer executor.Shutdown("", 0) 198 199 ps, err := executor.Launch(execCmd) 200 require.NoError(err) 201 require.NotZero(ps.Pid) 202 203 ps, err = executor.Wait(context.Background()) 204 require.NoError(err) 205 require.NoError(executor.Shutdown("SIGINT", 100*time.Millisecond)) 206 207 expected := "hello world" 208 tu.WaitForResult(func() (bool, error) { 209 act := strings.TrimSpace(string(testExecCmd.stdout.String())) 210 if expected != act { 211 return false, fmt.Errorf("expected: '%s' actual: '%s'", expected, act) 212 } 213 return true, nil 214 }, func(err error) { 215 require.NoError(err) 216 }) 217 }) 218 } 219 } 220 221 func TestExecutor_Start_Wait_Children(t *testing.T) { 222 ci.Parallel(t) 223 for name, factory := range executorFactories { 224 t.Run(name, func(t *testing.T) { 225 require := require.New(t) 226 testExecCmd := testExecutorCommand(t) 227 execCmd, allocDir := testExecCmd.command, testExecCmd.allocDir 228 execCmd.Cmd = "/bin/sh" 229 execCmd.Args = []string{"-c", "(sleep 30 > /dev/null & ) ; exec sleep 1"} 230 factory.configureExecCmd(t, execCmd) 231 232 defer allocDir.Destroy() 233 executor := factory.new(testlog.HCLogger(t)) 234 defer executor.Shutdown("SIGKILL", 0) 235 236 ps, err := executor.Launch(execCmd) 237 require.NoError(err) 238 require.NotZero(ps.Pid) 239 240 ch := make(chan error) 241 242 go func() { 243 ps, err = executor.Wait(context.Background()) 244 t.Logf("Processe completed with %#v error: %#v", ps, err) 245 ch <- err 246 }() 247 248 timeout := 7 * time.Second 249 select { 250 case <-ch: 251 require.NoError(err) 252 //good 253 case <-time.After(timeout): 254 require.Fail(fmt.Sprintf("process is running after timeout: %v", timeout)) 255 } 256 }) 257 } 258 } 259 260 func TestExecutor_WaitExitSignal(t *testing.T) { 261 ci.Parallel(t) 262 testutil.CgroupsCompatibleV1(t) // todo(shoenig) #12351 263 264 for name, factory := range executorFactories { 265 t.Run(name, func(t *testing.T) { 266 testExecCmd := testExecutorCommand(t) 267 execCmd, allocDir := testExecCmd.command, testExecCmd.allocDir 268 execCmd.Cmd = "/bin/sleep" 269 execCmd.Args = []string{"10000"} 270 execCmd.ResourceLimits = true 271 factory.configureExecCmd(t, execCmd) 272 273 defer allocDir.Destroy() 274 executor := factory.new(testlog.HCLogger(t)) 275 defer executor.Shutdown("", 0) 276 277 pState, err := executor.Launch(execCmd) 278 require.NoError(t, err) 279 280 go func() { 281 tu.WaitForResult(func() (bool, error) { 282 ch, err := executor.Stats(context.Background(), time.Second) 283 if err != nil { 284 return false, err 285 } 286 select { 287 case <-time.After(time.Second): 288 return false, fmt.Errorf("stats failed to send on interval") 289 case ru := <-ch: 290 assert.NotEmpty(t, ru.Pids, "no pids recorded in stats") 291 292 // just checking we measured something; each executor type has its own abilities, 293 // and e.g. cgroup v2 provides different information than cgroup v1 294 assert.NotEmpty(t, ru.ResourceUsage.MemoryStats.Measured) 295 296 assert.WithinDuration(t, time.Now(), time.Unix(0, ru.Timestamp), time.Second) 297 } 298 proc, err := os.FindProcess(pState.Pid) 299 if err != nil { 300 return false, err 301 } 302 err = proc.Signal(syscall.SIGKILL) 303 if err != nil { 304 return false, err 305 } 306 return true, nil 307 }, func(err error) { 308 assert.NoError(t, executor.Signal(os.Kill)) 309 assert.NoError(t, err) 310 }) 311 }() 312 313 pState, err = executor.Wait(context.Background()) 314 require.NoError(t, err) 315 require.Equal(t, pState.Signal, int(syscall.SIGKILL)) 316 }) 317 } 318 } 319 320 func TestExecutor_Start_Kill(t *testing.T) { 321 ci.Parallel(t) 322 for name, factory := range executorFactories { 323 t.Run(name, func(t *testing.T) { 324 require := require.New(t) 325 testExecCmd := testExecutorCommand(t) 326 execCmd, allocDir := testExecCmd.command, testExecCmd.allocDir 327 execCmd.Cmd = "/bin/sleep" 328 execCmd.Args = []string{"10"} 329 factory.configureExecCmd(t, execCmd) 330 331 defer allocDir.Destroy() 332 executor := factory.new(testlog.HCLogger(t)) 333 defer executor.Shutdown("", 0) 334 335 ps, err := executor.Launch(execCmd) 336 require.NoError(err) 337 require.NotZero(ps.Pid) 338 339 require.NoError(executor.Shutdown("SIGINT", 100*time.Millisecond)) 340 341 time.Sleep(time.Duration(tu.TestMultiplier()*2) * time.Second) 342 output := testExecCmd.stdout.String() 343 expected := "" 344 act := strings.TrimSpace(string(output)) 345 if act != expected { 346 t.Fatalf("Command output incorrectly: want %v; got %v", expected, act) 347 } 348 }) 349 } 350 } 351 352 func TestExecutor_Shutdown_Exit(t *testing.T) { 353 ci.Parallel(t) 354 require := require.New(t) 355 testExecCmd := testExecutorCommand(t) 356 execCmd, allocDir := testExecCmd.command, testExecCmd.allocDir 357 execCmd.Cmd = "/bin/sleep" 358 execCmd.Args = []string{"100"} 359 cfg := &ExecutorConfig{ 360 LogFile: "/dev/null", 361 } 362 executor, pluginClient, err := CreateExecutor(testlog.HCLogger(t), nil, cfg) 363 require.NoError(err) 364 365 proc, err := executor.Launch(execCmd) 366 require.NoError(err) 367 require.NotZero(proc.Pid) 368 369 executor.Shutdown("", 0) 370 pluginClient.Kill() 371 tu.WaitForResult(func() (bool, error) { 372 p, err := ps.FindProcess(proc.Pid) 373 if err != nil { 374 return false, err 375 } 376 return p == nil, fmt.Errorf("process found: %d", proc.Pid) 377 }, func(err error) { 378 require.NoError(err) 379 }) 380 require.NoError(allocDir.Destroy()) 381 } 382 383 func TestUniversalExecutor_MakeExecutable(t *testing.T) { 384 ci.Parallel(t) 385 // Create a temp file 386 f, err := ioutil.TempFile("", "") 387 if err != nil { 388 t.Fatal(err) 389 } 390 defer f.Close() 391 defer os.Remove(f.Name()) 392 393 // Set its permissions to be non-executable 394 f.Chmod(os.FileMode(0610)) 395 396 err = makeExecutable(f.Name()) 397 if err != nil { 398 t.Fatalf("makeExecutable() failed: %v", err) 399 } 400 401 // Check the permissions 402 stat, err := f.Stat() 403 if err != nil { 404 t.Fatalf("Stat() failed: %v", err) 405 } 406 407 act := stat.Mode().Perm() 408 exp := os.FileMode(0755) 409 if act != exp { 410 t.Fatalf("expected permissions %v; got %v", exp, act) 411 } 412 } 413 414 func TestUniversalExecutor_LookupPath(t *testing.T) { 415 ci.Parallel(t) 416 require := require.New(t) 417 // Create a temp dir 418 tmpDir := t.TempDir() 419 420 // Make a foo subdir 421 os.MkdirAll(filepath.Join(tmpDir, "foo"), 0700) 422 423 // Write a file under foo 424 filePath := filepath.Join(tmpDir, "foo", "tmp.txt") 425 err := ioutil.WriteFile(filePath, []byte{1, 2}, os.ModeAppend) 426 require.Nil(err) 427 428 // Lookup with full path on host to binary 429 path, err := lookupBin("not_tmpDir", filePath) 430 require.Nil(err) 431 require.Equal(filePath, path) 432 433 // Lookout with an absolute path to the binary 434 _, err = lookupBin(tmpDir, "/foo/tmp.txt") 435 require.Nil(err) 436 437 // Write a file under task dir 438 filePath3 := filepath.Join(tmpDir, "tmp.txt") 439 ioutil.WriteFile(filePath3, []byte{1, 2}, os.ModeAppend) 440 441 // Lookup with file name, should find the one we wrote above 442 path, err = lookupBin(tmpDir, "tmp.txt") 443 require.Nil(err) 444 require.Equal(filepath.Join(tmpDir, "tmp.txt"), path) 445 446 // Write a file under local subdir 447 os.MkdirAll(filepath.Join(tmpDir, "local"), 0700) 448 filePath2 := filepath.Join(tmpDir, "local", "tmp.txt") 449 ioutil.WriteFile(filePath2, []byte{1, 2}, os.ModeAppend) 450 451 // Lookup with file name, should find the one we wrote above 452 path, err = lookupBin(tmpDir, "tmp.txt") 453 require.Nil(err) 454 require.Equal(filepath.Join(tmpDir, "local", "tmp.txt"), path) 455 456 // Lookup a host path 457 _, err = lookupBin(tmpDir, "/bin/sh") 458 require.NoError(err) 459 460 // Lookup a host path via $PATH 461 _, err = lookupBin(tmpDir, "sh") 462 require.NoError(err) 463 } 464 465 // setupRoootfs setups the rootfs for libcontainer executor 466 // It uses busybox to make some binaries available - somewhat cheaper 467 // than mounting the underlying host filesystem 468 func setupRootfs(t *testing.T, rootfs string) { 469 paths := []string{ 470 "/bin/sh", 471 "/bin/sleep", 472 "/bin/echo", 473 "/bin/date", 474 } 475 476 for _, p := range paths { 477 setupRootfsBinary(t, rootfs, p) 478 } 479 } 480 481 // setupRootfsBinary installs a busybox link in the desired path 482 func setupRootfsBinary(t *testing.T, rootfs, path string) { 483 t.Helper() 484 485 dst := filepath.Join(rootfs, path) 486 err := os.MkdirAll(filepath.Dir(dst), 0755) 487 require.NoError(t, err) 488 489 src := filepath.Join( 490 "test-resources", "busybox", 491 fmt.Sprintf("busybox-%s", runtime.GOARCH), 492 ) 493 494 err = os.Link(src, dst) 495 if err != nil { 496 // On failure, fallback to copying the file directly. 497 // Linking may fail if the test source code lives on a separate 498 // volume/partition from the temp dir used for testing 499 copyFile(t, src, dst) 500 } 501 } 502 503 func copyFile(t *testing.T, src, dst string) { 504 in, err := os.Open(src) 505 require.NoErrorf(t, err, "copying %v -> %v", src, dst) 506 defer in.Close() 507 508 ins, err := in.Stat() 509 require.NoErrorf(t, err, "copying %v -> %v", src, dst) 510 511 out, err := os.OpenFile(dst, os.O_RDWR|os.O_CREATE, ins.Mode()) 512 require.NoErrorf(t, err, "copying %v -> %v", src, dst) 513 defer func() { 514 if err := out.Close(); err != nil { 515 t.Fatalf("copying %v -> %v failed: %v", src, dst, err) 516 } 517 }() 518 519 _, err = io.Copy(out, in) 520 require.NoErrorf(t, err, "copying %v -> %v", src, dst) 521 } 522 523 // TestExecutor_Start_Kill_Immediately_NoGrace asserts that executors shutdown 524 // immediately when sent a kill signal with no grace period. 525 func TestExecutor_Start_Kill_Immediately_NoGrace(t *testing.T) { 526 ci.Parallel(t) 527 for name, factory := range executorFactories { 528 529 t.Run(name, func(t *testing.T) { 530 require := require.New(t) 531 testExecCmd := testExecutorCommand(t) 532 execCmd, allocDir := testExecCmd.command, testExecCmd.allocDir 533 execCmd.Cmd = "/bin/sleep" 534 execCmd.Args = []string{"100"} 535 factory.configureExecCmd(t, execCmd) 536 defer allocDir.Destroy() 537 executor := factory.new(testlog.HCLogger(t)) 538 defer executor.Shutdown("", 0) 539 540 ps, err := executor.Launch(execCmd) 541 require.NoError(err) 542 require.NotZero(ps.Pid) 543 544 waitCh := make(chan interface{}) 545 go func() { 546 defer close(waitCh) 547 executor.Wait(context.Background()) 548 }() 549 550 require.NoError(executor.Shutdown("SIGKILL", 0)) 551 552 select { 553 case <-waitCh: 554 // all good! 555 case <-time.After(4 * time.Second * time.Duration(tu.TestMultiplier())): 556 require.Fail("process did not terminate despite SIGKILL") 557 } 558 }) 559 } 560 } 561 562 func TestExecutor_Start_Kill_Immediately_WithGrace(t *testing.T) { 563 ci.Parallel(t) 564 for name, factory := range executorFactories { 565 t.Run(name, func(t *testing.T) { 566 require := require.New(t) 567 testExecCmd := testExecutorCommand(t) 568 execCmd, allocDir := testExecCmd.command, testExecCmd.allocDir 569 execCmd.Cmd = "/bin/sleep" 570 execCmd.Args = []string{"100"} 571 factory.configureExecCmd(t, execCmd) 572 defer allocDir.Destroy() 573 executor := factory.new(testlog.HCLogger(t)) 574 defer executor.Shutdown("", 0) 575 576 ps, err := executor.Launch(execCmd) 577 require.NoError(err) 578 require.NotZero(ps.Pid) 579 580 waitCh := make(chan interface{}) 581 go func() { 582 defer close(waitCh) 583 executor.Wait(context.Background()) 584 }() 585 586 require.NoError(executor.Shutdown("SIGKILL", 100*time.Millisecond)) 587 588 select { 589 case <-waitCh: 590 // all good! 591 case <-time.After(4 * time.Second * time.Duration(tu.TestMultiplier())): 592 require.Fail("process did not terminate despite SIGKILL") 593 } 594 }) 595 } 596 } 597 598 // TestExecutor_Start_NonExecutableBinaries asserts that executor marks binary as executable 599 // before starting 600 func TestExecutor_Start_NonExecutableBinaries(t *testing.T) { 601 ci.Parallel(t) 602 603 for name, factory := range executorFactories { 604 t.Run(name, func(t *testing.T) { 605 require := require.New(t) 606 607 tmpDir := t.TempDir() 608 609 nonExecutablePath := filepath.Join(tmpDir, "nonexecutablefile") 610 ioutil.WriteFile(nonExecutablePath, 611 []byte("#!/bin/sh\necho hello world"), 612 0600) 613 614 testExecCmd := testExecutorCommand(t) 615 execCmd, allocDir := testExecCmd.command, testExecCmd.allocDir 616 execCmd.Cmd = nonExecutablePath 617 factory.configureExecCmd(t, execCmd) 618 619 executor := factory.new(testlog.HCLogger(t)) 620 defer executor.Shutdown("", 0) 621 622 // need to configure path in chroot with that file if using isolation executor 623 if _, ok := executor.(*UniversalExecutor); !ok { 624 taskName := filepath.Base(testExecCmd.command.TaskDir) 625 err := allocDir.NewTaskDir(taskName).Build(true, map[string]string{ 626 tmpDir: tmpDir, 627 }) 628 require.NoError(err) 629 } 630 631 defer allocDir.Destroy() 632 ps, err := executor.Launch(execCmd) 633 require.NoError(err) 634 require.NotZero(ps.Pid) 635 636 ps, err = executor.Wait(context.Background()) 637 require.NoError(err) 638 require.NoError(executor.Shutdown("SIGINT", 100*time.Millisecond)) 639 640 expected := "hello world" 641 tu.WaitForResult(func() (bool, error) { 642 act := strings.TrimSpace(string(testExecCmd.stdout.String())) 643 if expected != act { 644 return false, fmt.Errorf("expected: '%s' actual: '%s'", expected, act) 645 } 646 return true, nil 647 }, func(err error) { 648 stderr := strings.TrimSpace(string(testExecCmd.stderr.String())) 649 t.Logf("stderr: %v", stderr) 650 require.NoError(err) 651 }) 652 }) 653 } 654 }