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