github.com/Ilhicas/nomad@v1.0.4-0.20210304152020-e86851182bc3/drivers/shared/executor/executor_linux_test.go (about) 1 package executor 2 3 import ( 4 "context" 5 "fmt" 6 "io/ioutil" 7 "os" 8 "path/filepath" 9 "regexp" 10 "strconv" 11 "strings" 12 "testing" 13 "time" 14 15 "github.com/hashicorp/nomad/client/allocdir" 16 "github.com/hashicorp/nomad/client/taskenv" 17 "github.com/hashicorp/nomad/client/testutil" 18 "github.com/hashicorp/nomad/helper/testlog" 19 "github.com/hashicorp/nomad/nomad/mock" 20 "github.com/hashicorp/nomad/plugins/drivers" 21 tu "github.com/hashicorp/nomad/testutil" 22 "github.com/opencontainers/runc/libcontainer/cgroups" 23 lconfigs "github.com/opencontainers/runc/libcontainer/configs" 24 "github.com/stretchr/testify/require" 25 "golang.org/x/sys/unix" 26 ) 27 28 func init() { 29 executorFactories["LibcontainerExecutor"] = libcontainerFactory 30 } 31 32 var libcontainerFactory = executorFactory{ 33 new: NewExecutorWithIsolation, 34 configureExecCmd: func(t *testing.T, cmd *ExecCommand) { 35 cmd.ResourceLimits = true 36 setupRootfs(t, cmd.TaskDir) 37 }, 38 } 39 40 // testExecutorContextWithChroot returns an ExecutorContext and AllocDir with 41 // chroot. Use testExecutorContext if you don't need a chroot. 42 // 43 // The caller is responsible for calling AllocDir.Destroy() to cleanup. 44 func testExecutorCommandWithChroot(t *testing.T) *testExecCmd { 45 chrootEnv := map[string]string{ 46 "/etc/ld.so.cache": "/etc/ld.so.cache", 47 "/etc/ld.so.conf": "/etc/ld.so.conf", 48 "/etc/ld.so.conf.d": "/etc/ld.so.conf.d", 49 "/etc/passwd": "/etc/passwd", 50 "/lib": "/lib", 51 "/lib64": "/lib64", 52 "/usr/lib": "/usr/lib", 53 "/bin/ls": "/bin/ls", 54 "/bin/cat": "/bin/cat", 55 "/bin/echo": "/bin/echo", 56 "/bin/bash": "/bin/bash", 57 "/bin/sleep": "/bin/sleep", 58 "/foobar": "/does/not/exist", 59 } 60 61 alloc := mock.Alloc() 62 task := alloc.Job.TaskGroups[0].Tasks[0] 63 taskEnv := taskenv.NewBuilder(mock.Node(), alloc, task, "global").Build() 64 65 allocDir := allocdir.NewAllocDir(testlog.HCLogger(t), filepath.Join(os.TempDir(), alloc.ID)) 66 if err := allocDir.Build(); err != nil { 67 t.Fatalf("AllocDir.Build() failed: %v", err) 68 } 69 if err := allocDir.NewTaskDir(task.Name).Build(true, chrootEnv); err != nil { 70 allocDir.Destroy() 71 t.Fatalf("allocDir.NewTaskDir(%q) failed: %v", task.Name, err) 72 } 73 td := allocDir.TaskDirs[task.Name] 74 cmd := &ExecCommand{ 75 Env: taskEnv.List(), 76 TaskDir: td.Dir, 77 Resources: &drivers.Resources{ 78 NomadResources: alloc.AllocatedResources.Tasks[task.Name], 79 }, 80 } 81 82 testCmd := &testExecCmd{ 83 command: cmd, 84 allocDir: allocDir, 85 } 86 configureTLogging(t, testCmd) 87 return testCmd 88 } 89 90 func TestExecutor_configureNamespaces(t *testing.T) { 91 t.Run("host host", func(t *testing.T) { 92 require.Equal(t, lconfigs.Namespaces{ 93 {Type: lconfigs.NEWNS}, 94 }, configureNamespaces("host", "host")) 95 }) 96 97 t.Run("host private", func(t *testing.T) { 98 require.Equal(t, lconfigs.Namespaces{ 99 {Type: lconfigs.NEWNS}, 100 {Type: lconfigs.NEWIPC}, 101 }, configureNamespaces("host", "private")) 102 }) 103 104 t.Run("private host", func(t *testing.T) { 105 require.Equal(t, lconfigs.Namespaces{ 106 {Type: lconfigs.NEWNS}, 107 {Type: lconfigs.NEWPID}, 108 }, configureNamespaces("private", "host")) 109 }) 110 111 t.Run("private private", func(t *testing.T) { 112 require.Equal(t, lconfigs.Namespaces{ 113 {Type: lconfigs.NEWNS}, 114 {Type: lconfigs.NEWPID}, 115 {Type: lconfigs.NEWIPC}, 116 }, configureNamespaces("private", "private")) 117 }) 118 } 119 120 func TestExecutor_Isolation_PID_and_IPC_hostMode(t *testing.T) { 121 t.Parallel() 122 r := require.New(t) 123 testutil.ExecCompatible(t) 124 125 testExecCmd := testExecutorCommandWithChroot(t) 126 execCmd, allocDir := testExecCmd.command, testExecCmd.allocDir 127 execCmd.Cmd = "/bin/ls" 128 execCmd.Args = []string{"-F", "/", "/etc/"} 129 defer allocDir.Destroy() 130 131 execCmd.ResourceLimits = true 132 execCmd.ModePID = "host" // disable PID namespace 133 execCmd.ModeIPC = "host" // disable IPC namespace 134 135 executor := NewExecutorWithIsolation(testlog.HCLogger(t)) 136 defer executor.Shutdown("SIGKILL", 0) 137 138 ps, err := executor.Launch(execCmd) 139 r.NoError(err) 140 r.NotZero(ps.Pid) 141 142 estate, err := executor.Wait(context.Background()) 143 r.NoError(err) 144 r.Zero(estate.ExitCode) 145 146 lexec, ok := executor.(*LibcontainerExecutor) 147 r.True(ok) 148 149 // Check that namespaces were applied to the container config 150 config := lexec.container.Config() 151 152 r.Contains(config.Namespaces, lconfigs.Namespace{Type: lconfigs.NEWNS}) 153 r.NotContains(config.Namespaces, lconfigs.Namespace{Type: lconfigs.NEWPID}) 154 r.NotContains(config.Namespaces, lconfigs.Namespace{Type: lconfigs.NEWIPC}) 155 156 // Shut down executor 157 r.NoError(executor.Shutdown("", 0)) 158 executor.Wait(context.Background()) 159 } 160 161 func TestExecutor_IsolationAndConstraints(t *testing.T) { 162 t.Parallel() 163 r := require.New(t) 164 testutil.ExecCompatible(t) 165 166 testExecCmd := testExecutorCommandWithChroot(t) 167 execCmd, allocDir := testExecCmd.command, testExecCmd.allocDir 168 execCmd.Cmd = "/bin/ls" 169 execCmd.Args = []string{"-F", "/", "/etc/"} 170 defer allocDir.Destroy() 171 172 execCmd.ResourceLimits = true 173 execCmd.ModePID = "private" 174 execCmd.ModeIPC = "private" 175 176 executor := NewExecutorWithIsolation(testlog.HCLogger(t)) 177 defer executor.Shutdown("SIGKILL", 0) 178 179 ps, err := executor.Launch(execCmd) 180 r.NoError(err) 181 r.NotZero(ps.Pid) 182 183 estate, err := executor.Wait(context.Background()) 184 r.NoError(err) 185 r.Zero(estate.ExitCode) 186 187 lexec, ok := executor.(*LibcontainerExecutor) 188 r.True(ok) 189 190 // Check if the resource constraints were applied 191 state, err := lexec.container.State() 192 r.NoError(err) 193 194 memLimits := filepath.Join(state.CgroupPaths["memory"], "memory.limit_in_bytes") 195 data, err := ioutil.ReadFile(memLimits) 196 r.NoError(err) 197 198 expectedMemLim := strconv.Itoa(int(execCmd.Resources.NomadResources.Memory.MemoryMB * 1024 * 1024)) 199 actualMemLim := strings.TrimSpace(string(data)) 200 r.Equal(actualMemLim, expectedMemLim) 201 202 // Check that namespaces were applied to the container config 203 config := lexec.container.Config() 204 205 r.Contains(config.Namespaces, lconfigs.Namespace{Type: lconfigs.NEWNS}) 206 r.Contains(config.Namespaces, lconfigs.Namespace{Type: lconfigs.NEWPID}) 207 r.Contains(config.Namespaces, lconfigs.Namespace{Type: lconfigs.NEWIPC}) 208 209 // Shut down executor 210 r.NoError(executor.Shutdown("", 0)) 211 executor.Wait(context.Background()) 212 213 // Check if Nomad has actually removed the cgroups 214 tu.WaitForResult(func() (bool, error) { 215 _, err = os.Stat(memLimits) 216 if err == nil { 217 return false, fmt.Errorf("expected an error from os.Stat %s", memLimits) 218 } 219 return true, nil 220 }, func(err error) { t.Error(err) }) 221 222 expected := `/: 223 alloc/ 224 bin/ 225 dev/ 226 etc/ 227 lib/ 228 lib64/ 229 local/ 230 proc/ 231 secrets/ 232 sys/ 233 tmp/ 234 usr/ 235 236 /etc/: 237 ld.so.cache 238 ld.so.conf 239 ld.so.conf.d/ 240 passwd` 241 tu.WaitForResult(func() (bool, error) { 242 output := testExecCmd.stdout.String() 243 act := strings.TrimSpace(string(output)) 244 if act != expected { 245 return false, fmt.Errorf("Command output incorrectly: want %v; got %v", expected, act) 246 } 247 return true, nil 248 }, func(err error) { t.Error(err) }) 249 } 250 251 // TestExecutor_CgroupPaths asserts that process starts with independent cgroups 252 // hierarchy created for this process 253 func TestExecutor_CgroupPaths(t *testing.T) { 254 t.Parallel() 255 require := require.New(t) 256 testutil.ExecCompatible(t) 257 258 testExecCmd := testExecutorCommandWithChroot(t) 259 execCmd, allocDir := testExecCmd.command, testExecCmd.allocDir 260 execCmd.Cmd = "/bin/bash" 261 execCmd.Args = []string{"-c", "sleep 0.2; cat /proc/self/cgroup"} 262 defer allocDir.Destroy() 263 264 execCmd.ResourceLimits = true 265 266 executor := NewExecutorWithIsolation(testlog.HCLogger(t)) 267 defer executor.Shutdown("SIGKILL", 0) 268 269 ps, err := executor.Launch(execCmd) 270 require.NoError(err) 271 require.NotZero(ps.Pid) 272 273 state, err := executor.Wait(context.Background()) 274 require.NoError(err) 275 require.Zero(state.ExitCode) 276 277 tu.WaitForResult(func() (bool, error) { 278 output := strings.TrimSpace(testExecCmd.stdout.String()) 279 // sanity check that we got some cgroups 280 if !strings.Contains(output, ":devices:") { 281 return false, fmt.Errorf("was expected cgroup files but found:\n%v", output) 282 } 283 lines := strings.Split(output, "\n") 284 for _, line := range lines { 285 // Every cgroup entry should be /nomad/$ALLOC_ID 286 if line == "" { 287 continue 288 } 289 290 // Skip rdma subsystem; rdma was added in most recent kernels and libcontainer/docker 291 // don't isolate it by default. 292 // :: filters out odd empty cgroup found in latest Ubuntu lines, e.g. 0::/user.slice/user-1000.slice/session-17.scope 293 // that is also not used for isolation 294 if strings.Contains(line, ":rdma:") || strings.Contains(line, "::") { 295 continue 296 } 297 298 if !strings.Contains(line, ":/nomad/") { 299 return false, fmt.Errorf("Not a member of the alloc's cgroup: expected=...:/nomad/... -- found=%q", line) 300 } 301 } 302 return true, nil 303 }, func(err error) { t.Error(err) }) 304 } 305 306 // TestExecutor_CgroupPaths asserts that all cgroups created for a task 307 // are destroyed on shutdown 308 func TestExecutor_CgroupPathsAreDestroyed(t *testing.T) { 309 t.Parallel() 310 require := require.New(t) 311 testutil.ExecCompatible(t) 312 313 testExecCmd := testExecutorCommandWithChroot(t) 314 execCmd, allocDir := testExecCmd.command, testExecCmd.allocDir 315 execCmd.Cmd = "/bin/bash" 316 execCmd.Args = []string{"-c", "sleep 0.2; cat /proc/self/cgroup"} 317 defer allocDir.Destroy() 318 319 execCmd.ResourceLimits = true 320 321 executor := NewExecutorWithIsolation(testlog.HCLogger(t)) 322 defer executor.Shutdown("SIGKILL", 0) 323 324 ps, err := executor.Launch(execCmd) 325 require.NoError(err) 326 require.NotZero(ps.Pid) 327 328 state, err := executor.Wait(context.Background()) 329 require.NoError(err) 330 require.Zero(state.ExitCode) 331 332 var cgroupsPaths string 333 tu.WaitForResult(func() (bool, error) { 334 output := strings.TrimSpace(testExecCmd.stdout.String()) 335 // sanity check that we got some cgroups 336 if !strings.Contains(output, ":devices:") { 337 return false, fmt.Errorf("was expected cgroup files but found:\n%v", output) 338 } 339 lines := strings.Split(output, "\n") 340 for _, line := range lines { 341 // Every cgroup entry should be /nomad/$ALLOC_ID 342 if line == "" { 343 continue 344 } 345 346 // Skip rdma subsystem; rdma was added in most recent kernels and libcontainer/docker 347 // don't isolate it by default. 348 if strings.Contains(line, ":rdma:") || strings.Contains(line, "::") { 349 continue 350 } 351 352 if !strings.Contains(line, ":/nomad/") { 353 return false, fmt.Errorf("Not a member of the alloc's cgroup: expected=...:/nomad/... -- found=%q", line) 354 } 355 } 356 357 cgroupsPaths = output 358 return true, nil 359 }, func(err error) { t.Error(err) }) 360 361 // shutdown executor and test that cgroups are destroyed 362 executor.Shutdown("SIGKILL", 0) 363 364 // test that the cgroup paths are not visible 365 tmpFile, err := ioutil.TempFile("", "") 366 require.NoError(err) 367 defer os.Remove(tmpFile.Name()) 368 369 _, err = tmpFile.WriteString(cgroupsPaths) 370 require.NoError(err) 371 tmpFile.Close() 372 373 subsystems, err := cgroups.ParseCgroupFile(tmpFile.Name()) 374 require.NoError(err) 375 376 for subsystem, cgroup := range subsystems { 377 if !strings.Contains(cgroup, "nomad/") { 378 // this should only be rdma at this point 379 continue 380 } 381 382 p, err := getCgroupPathHelper(subsystem, cgroup) 383 require.NoError(err) 384 require.Falsef(cgroups.PathExists(p), "cgroup for %s %s still exists", subsystem, cgroup) 385 } 386 } 387 388 func TestUniversalExecutor_LookupTaskBin(t *testing.T) { 389 t.Parallel() 390 require := require.New(t) 391 392 // Create a temp dir 393 tmpDir, err := ioutil.TempDir("", "") 394 require.Nil(err) 395 defer os.Remove(tmpDir) 396 397 // Create the command 398 cmd := &ExecCommand{Env: []string{"PATH=/bin"}, TaskDir: tmpDir} 399 400 // Make a foo subdir 401 os.MkdirAll(filepath.Join(tmpDir, "foo"), 0700) 402 403 // Write a file under foo 404 filePath := filepath.Join(tmpDir, "foo", "tmp.txt") 405 err = ioutil.WriteFile(filePath, []byte{1, 2}, os.ModeAppend) 406 require.NoError(err) 407 408 // Lookout with an absolute path to the binary 409 cmd.Cmd = "/foo/tmp.txt" 410 _, err = lookupTaskBin(cmd) 411 require.NoError(err) 412 413 // Write a file under local subdir 414 os.MkdirAll(filepath.Join(tmpDir, "local"), 0700) 415 filePath2 := filepath.Join(tmpDir, "local", "tmp.txt") 416 ioutil.WriteFile(filePath2, []byte{1, 2}, os.ModeAppend) 417 418 // Lookup with file name, should find the one we wrote above 419 cmd.Cmd = "tmp.txt" 420 _, err = lookupTaskBin(cmd) 421 require.NoError(err) 422 423 // Lookup a host absolute path 424 cmd.Cmd = "/bin/sh" 425 _, err = lookupTaskBin(cmd) 426 require.Error(err) 427 } 428 429 // Exec Launch looks for the binary only inside the chroot 430 func TestExecutor_EscapeContainer(t *testing.T) { 431 t.Parallel() 432 require := require.New(t) 433 testutil.ExecCompatible(t) 434 435 testExecCmd := testExecutorCommandWithChroot(t) 436 execCmd, allocDir := testExecCmd.command, testExecCmd.allocDir 437 execCmd.Cmd = "/bin/kill" // missing from the chroot container 438 defer allocDir.Destroy() 439 440 execCmd.ResourceLimits = true 441 442 executor := NewExecutorWithIsolation(testlog.HCLogger(t)) 443 defer executor.Shutdown("SIGKILL", 0) 444 445 _, err := executor.Launch(execCmd) 446 require.Error(err) 447 require.Regexp("^file /bin/kill not found under path", err) 448 449 // Bare files are looked up using the system path, inside the container 450 allocDir.Destroy() 451 testExecCmd = testExecutorCommandWithChroot(t) 452 execCmd, allocDir = testExecCmd.command, testExecCmd.allocDir 453 execCmd.Cmd = "kill" 454 _, err = executor.Launch(execCmd) 455 require.Error(err) 456 require.Regexp("^file kill not found under path", err) 457 458 allocDir.Destroy() 459 testExecCmd = testExecutorCommandWithChroot(t) 460 execCmd, allocDir = testExecCmd.command, testExecCmd.allocDir 461 execCmd.Cmd = "echo" 462 _, err = executor.Launch(execCmd) 463 require.NoError(err) 464 } 465 466 func TestExecutor_Capabilities(t *testing.T) { 467 t.Parallel() 468 testutil.ExecCompatible(t) 469 470 cases := []struct { 471 user string 472 caps string 473 }{ 474 { 475 user: "nobody", 476 caps: ` 477 CapInh: 0000000000000000 478 CapPrm: 0000000000000000 479 CapEff: 0000000000000000 480 CapBnd: 0000003fffffffff 481 CapAmb: 0000000000000000`, 482 }, 483 { 484 user: "root", 485 caps: ` 486 CapInh: 0000000000000000 487 CapPrm: 0000003fffffffff 488 CapEff: 0000003fffffffff 489 CapBnd: 0000003fffffffff 490 CapAmb: 0000000000000000`, 491 }, 492 } 493 494 for _, c := range cases { 495 t.Run(c.user, func(t *testing.T) { 496 require := require.New(t) 497 498 testExecCmd := testExecutorCommandWithChroot(t) 499 execCmd, allocDir := testExecCmd.command, testExecCmd.allocDir 500 defer allocDir.Destroy() 501 502 execCmd.User = c.user 503 execCmd.ResourceLimits = true 504 execCmd.Cmd = "/bin/bash" 505 execCmd.Args = []string{"-c", "cat /proc/$$/status"} 506 507 executor := NewExecutorWithIsolation(testlog.HCLogger(t)) 508 defer executor.Shutdown("SIGKILL", 0) 509 510 _, err := executor.Launch(execCmd) 511 require.NoError(err) 512 513 ch := make(chan interface{}) 514 go func() { 515 executor.Wait(context.Background()) 516 close(ch) 517 }() 518 519 select { 520 case <-ch: 521 // all good 522 case <-time.After(5 * time.Second): 523 require.Fail("timeout waiting for exec to shutdown") 524 } 525 526 canonical := func(s string) string { 527 s = strings.TrimSpace(s) 528 s = regexp.MustCompile("[ \t]+").ReplaceAllString(s, " ") 529 s = regexp.MustCompile("[\n\r]+").ReplaceAllString(s, "\n") 530 return s 531 } 532 533 expected := canonical(c.caps) 534 tu.WaitForResult(func() (bool, error) { 535 output := canonical(testExecCmd.stdout.String()) 536 if !strings.Contains(output, expected) { 537 return false, fmt.Errorf("capabilities didn't match: want\n%v\n; got:\n%v\n", expected, output) 538 } 539 return true, nil 540 }, func(err error) { require.NoError(err) }) 541 }) 542 } 543 544 } 545 546 func TestExecutor_ClientCleanup(t *testing.T) { 547 t.Parallel() 548 testutil.ExecCompatible(t) 549 require := require.New(t) 550 551 testExecCmd := testExecutorCommandWithChroot(t) 552 execCmd, allocDir := testExecCmd.command, testExecCmd.allocDir 553 defer allocDir.Destroy() 554 555 executor := NewExecutorWithIsolation(testlog.HCLogger(t)) 556 defer executor.Shutdown("", 0) 557 558 // Need to run a command which will produce continuous output but not 559 // too quickly to ensure executor.Exit() stops the process. 560 execCmd.Cmd = "/bin/bash" 561 execCmd.Args = []string{"-c", "while true; do /bin/echo X; /bin/sleep 1; done"} 562 execCmd.ResourceLimits = true 563 564 ps, err := executor.Launch(execCmd) 565 566 require.NoError(err) 567 require.NotZero(ps.Pid) 568 time.Sleep(500 * time.Millisecond) 569 require.NoError(executor.Shutdown("SIGINT", 100*time.Millisecond)) 570 571 ch := make(chan interface{}) 572 go func() { 573 executor.Wait(context.Background()) 574 close(ch) 575 }() 576 577 select { 578 case <-ch: 579 // all good 580 case <-time.After(5 * time.Second): 581 require.Fail("timeout waiting for exec to shutdown") 582 } 583 584 output := testExecCmd.stdout.String() 585 require.NotZero(len(output)) 586 time.Sleep(2 * time.Second) 587 output1 := testExecCmd.stdout.String() 588 require.Equal(len(output), len(output1)) 589 } 590 591 func TestExecutor_cmdDevices(t *testing.T) { 592 input := []*drivers.DeviceConfig{ 593 { 594 HostPath: "/dev/null", 595 TaskPath: "/task/dev/null", 596 Permissions: "rwm", 597 }, 598 } 599 600 expected := &lconfigs.Device{ 601 DeviceRule: lconfigs.DeviceRule{ 602 Type: 99, 603 Major: 1, 604 Minor: 3, 605 Permissions: "rwm", 606 }, 607 Path: "/task/dev/null", 608 } 609 610 found, err := cmdDevices(input) 611 require.NoError(t, err) 612 require.Len(t, found, 1) 613 614 // ignore file permission and ownership 615 // as they are host specific potentially 616 d := found[0] 617 d.FileMode = 0 618 d.Uid = 0 619 d.Gid = 0 620 621 require.EqualValues(t, expected, d) 622 } 623 624 func TestExecutor_cmdMounts(t *testing.T) { 625 input := []*drivers.MountConfig{ 626 { 627 HostPath: "/host/path-ro", 628 TaskPath: "/task/path-ro", 629 Readonly: true, 630 }, 631 { 632 HostPath: "/host/path-rw", 633 TaskPath: "/task/path-rw", 634 Readonly: false, 635 }, 636 } 637 638 expected := []*lconfigs.Mount{ 639 { 640 Source: "/host/path-ro", 641 Destination: "/task/path-ro", 642 Flags: unix.MS_BIND | unix.MS_RDONLY, 643 Device: "bind", 644 PropagationFlags: []int{unix.MS_PRIVATE | unix.MS_REC}, 645 }, 646 { 647 Source: "/host/path-rw", 648 Destination: "/task/path-rw", 649 Flags: unix.MS_BIND, 650 Device: "bind", 651 PropagationFlags: []int{unix.MS_PRIVATE | unix.MS_REC}, 652 }, 653 } 654 655 require.EqualValues(t, expected, cmdMounts(input)) 656 } 657 658 // TestUniversalExecutor_NoCgroup asserts that commands are executed in the 659 // same cgroup as parent process 660 func TestUniversalExecutor_NoCgroup(t *testing.T) { 661 t.Parallel() 662 testutil.ExecCompatible(t) 663 664 expectedBytes, err := ioutil.ReadFile("/proc/self/cgroup") 665 require.NoError(t, err) 666 667 expected := strings.TrimSpace(string(expectedBytes)) 668 669 testExecCmd := testExecutorCommand(t) 670 execCmd, allocDir := testExecCmd.command, testExecCmd.allocDir 671 execCmd.Cmd = "/bin/cat" 672 execCmd.Args = []string{"/proc/self/cgroup"} 673 defer allocDir.Destroy() 674 675 execCmd.BasicProcessCgroup = false 676 execCmd.ResourceLimits = false 677 678 executor := NewExecutor(testlog.HCLogger(t)) 679 defer executor.Shutdown("SIGKILL", 0) 680 681 _, err = executor.Launch(execCmd) 682 require.NoError(t, err) 683 684 _, err = executor.Wait(context.Background()) 685 require.NoError(t, err) 686 687 tu.WaitForResult(func() (bool, error) { 688 act := strings.TrimSpace(string(testExecCmd.stdout.String())) 689 if expected != act { 690 return false, fmt.Errorf("expected:\n%s actual:\n%s", expected, act) 691 } 692 return true, nil 693 }, func(err error) { 694 stderr := strings.TrimSpace(string(testExecCmd.stderr.String())) 695 t.Logf("stderr: %v", stderr) 696 require.NoError(t, err) 697 }) 698 699 }