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