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 = &copyDone
   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  }