github.com/anth0d/nomad@v0.0.0-20221214183521-ae3a0a2cad06/drivers/rawexec/driver_unix_test.go (about)

     1  //go:build !windows
     2  
     3  package rawexec
     4  
     5  import (
     6  	"context"
     7  	"fmt"
     8  	"io/ioutil"
     9  	"os"
    10  	"path/filepath"
    11  	"regexp"
    12  	"runtime"
    13  	"strconv"
    14  	"strings"
    15  	"syscall"
    16  	"testing"
    17  	"time"
    18  
    19  	"github.com/hashicorp/nomad/ci"
    20  	clienttestutil "github.com/hashicorp/nomad/client/testutil"
    21  	"github.com/hashicorp/nomad/helper/testtask"
    22  	"github.com/hashicorp/nomad/helper/uuid"
    23  	basePlug "github.com/hashicorp/nomad/plugins/base"
    24  	"github.com/hashicorp/nomad/plugins/drivers"
    25  	dtestutil "github.com/hashicorp/nomad/plugins/drivers/testutils"
    26  	"github.com/hashicorp/nomad/testutil"
    27  	"github.com/stretchr/testify/require"
    28  	"golang.org/x/sys/unix"
    29  )
    30  
    31  func TestRawExecDriver_User(t *testing.T) {
    32  	ci.Parallel(t)
    33  	clienttestutil.RequireLinux(t)
    34  	require := require.New(t)
    35  
    36  	d := newEnabledRawExecDriver(t)
    37  	harness := dtestutil.NewDriverHarness(t, d)
    38  
    39  	task := &drivers.TaskConfig{
    40  		ID:   uuid.Generate(),
    41  		Name: "sleep",
    42  		User: "alice",
    43  	}
    44  
    45  	cleanup := harness.MkAllocDir(task, false)
    46  	defer cleanup()
    47  
    48  	tc := &TaskConfig{
    49  		Command: testtask.Path(),
    50  		Args:    []string{"sleep", "45s"},
    51  	}
    52  	require.NoError(task.EncodeConcreteDriverConfig(&tc))
    53  	testtask.SetTaskConfigEnv(task)
    54  
    55  	_, _, err := harness.StartTask(task)
    56  	require.Error(err)
    57  	msg := "unknown user alice"
    58  	require.Contains(err.Error(), msg)
    59  }
    60  
    61  func TestRawExecDriver_Signal(t *testing.T) {
    62  	ci.Parallel(t)
    63  	clienttestutil.RequireLinux(t)
    64  
    65  	require := require.New(t)
    66  
    67  	d := newEnabledRawExecDriver(t)
    68  	harness := dtestutil.NewDriverHarness(t, d)
    69  
    70  	task := &drivers.TaskConfig{
    71  		AllocID: uuid.Generate(),
    72  		ID:      uuid.Generate(),
    73  		Name:    "signal",
    74  		Env:     defaultEnv(),
    75  	}
    76  
    77  	cleanup := harness.MkAllocDir(task, true)
    78  	defer cleanup()
    79  
    80  	tc := &TaskConfig{
    81  		Command: "/bin/bash",
    82  		Args:    []string{"test.sh"},
    83  	}
    84  	require.NoError(task.EncodeConcreteDriverConfig(&tc))
    85  	testtask.SetTaskConfigEnv(task)
    86  
    87  	testFile := filepath.Join(task.TaskDir().Dir, "test.sh")
    88  	testData := []byte(`
    89  at_term() {
    90      echo 'Terminated.'
    91      exit 3
    92  }
    93  trap at_term USR1
    94  while true; do
    95      sleep 1
    96  done
    97  	`)
    98  	require.NoError(ioutil.WriteFile(testFile, testData, 0777))
    99  
   100  	_, _, err := harness.StartTask(task)
   101  	require.NoError(err)
   102  
   103  	go func() {
   104  		time.Sleep(100 * time.Millisecond)
   105  		require.NoError(harness.SignalTask(task.ID, "SIGUSR1"))
   106  	}()
   107  
   108  	// Task should terminate quickly
   109  	waitCh, err := harness.WaitTask(context.Background(), task.ID)
   110  	require.NoError(err)
   111  	select {
   112  	case res := <-waitCh:
   113  		require.False(res.Successful())
   114  		require.Equal(3, res.ExitCode)
   115  	case <-time.After(time.Duration(testutil.TestMultiplier()*6) * time.Second):
   116  		require.Fail("WaitTask timeout")
   117  	}
   118  
   119  	// Check the log file to see it exited because of the signal
   120  	outputFile := filepath.Join(task.TaskDir().LogDir, "signal.stdout.0")
   121  	exp := "Terminated."
   122  	testutil.WaitForResult(func() (bool, error) {
   123  		act, err := ioutil.ReadFile(outputFile)
   124  		if err != nil {
   125  			return false, fmt.Errorf("Couldn't read expected output: %v", err)
   126  		}
   127  
   128  		if strings.TrimSpace(string(act)) != exp {
   129  			t.Logf("Read from %v", outputFile)
   130  			return false, fmt.Errorf("Command outputted %v; want %v", act, exp)
   131  		}
   132  		return true, nil
   133  	}, func(err error) { require.NoError(err) })
   134  }
   135  
   136  func TestRawExecDriver_StartWaitStop(t *testing.T) {
   137  	ci.Parallel(t)
   138  	require := require.New(t)
   139  
   140  	d := newEnabledRawExecDriver(t)
   141  	harness := dtestutil.NewDriverHarness(t, d)
   142  	defer harness.Kill()
   143  
   144  	// Disable cgroups so test works without root
   145  	config := &Config{NoCgroups: true, Enabled: true}
   146  	var data []byte
   147  	require.NoError(basePlug.MsgPackEncode(&data, config))
   148  	bconfig := &basePlug.Config{PluginConfig: data}
   149  	require.NoError(harness.SetConfig(bconfig))
   150  
   151  	task := &drivers.TaskConfig{
   152  		ID:   uuid.Generate(),
   153  		Name: "test",
   154  	}
   155  
   156  	taskConfig := map[string]interface{}{}
   157  	taskConfig["command"] = testtask.Path()
   158  	taskConfig["args"] = []string{"sleep", "100s"}
   159  
   160  	require.NoError(task.EncodeConcreteDriverConfig(&taskConfig))
   161  
   162  	cleanup := harness.MkAllocDir(task, false)
   163  	defer cleanup()
   164  
   165  	handle, _, err := harness.StartTask(task)
   166  	require.NoError(err)
   167  
   168  	ch, err := harness.WaitTask(context.Background(), handle.Config.ID)
   169  	require.NoError(err)
   170  
   171  	require.NoError(harness.WaitUntilStarted(task.ID, 1*time.Second))
   172  
   173  	go func() {
   174  		harness.StopTask(task.ID, 2*time.Second, "SIGINT")
   175  	}()
   176  
   177  	select {
   178  	case result := <-ch:
   179  		require.Equal(int(unix.SIGINT), result.Signal)
   180  	case <-time.After(10 * time.Second):
   181  		require.Fail("timeout waiting for task to shutdown")
   182  	}
   183  
   184  	// Ensure that the task is marked as dead, but account
   185  	// for WaitTask() closing channel before internal state is updated
   186  	testutil.WaitForResult(func() (bool, error) {
   187  		status, err := harness.InspectTask(task.ID)
   188  		if err != nil {
   189  			return false, fmt.Errorf("inspecting task failed: %v", err)
   190  		}
   191  		if status.State != drivers.TaskStateExited {
   192  			return false, fmt.Errorf("task hasn't exited yet; status: %v", status.State)
   193  		}
   194  
   195  		return true, nil
   196  	}, func(err error) {
   197  		require.NoError(err)
   198  	})
   199  
   200  	require.NoError(harness.DestroyTask(task.ID, true))
   201  }
   202  
   203  // TestRawExecDriver_DestroyKillsAll asserts that when TaskDestroy is called all
   204  // task processes are cleaned up.
   205  func TestRawExecDriver_DestroyKillsAll(t *testing.T) {
   206  	ci.Parallel(t)
   207  	clienttestutil.RequireLinux(t)
   208  
   209  	d := newEnabledRawExecDriver(t)
   210  	harness := dtestutil.NewDriverHarness(t, d)
   211  	defer harness.Kill()
   212  
   213  	task := &drivers.TaskConfig{
   214  		AllocID: uuid.Generate(),
   215  		ID:      uuid.Generate(),
   216  		Name:    "test",
   217  		Env:     defaultEnv(),
   218  	}
   219  
   220  	cleanup := harness.MkAllocDir(task, true)
   221  	defer cleanup()
   222  
   223  	taskConfig := map[string]interface{}{}
   224  	taskConfig["command"] = "/bin/sh"
   225  	taskConfig["args"] = []string{"-c", fmt.Sprintf(`sleep 3600 & echo "SLEEP_PID=$!"`)}
   226  
   227  	require.NoError(t, task.EncodeConcreteDriverConfig(&taskConfig))
   228  
   229  	handle, _, err := harness.StartTask(task)
   230  	require.NoError(t, err)
   231  	defer harness.DestroyTask(task.ID, true)
   232  
   233  	ch, err := harness.WaitTask(context.Background(), handle.Config.ID)
   234  	require.NoError(t, err)
   235  
   236  	select {
   237  	case result := <-ch:
   238  		require.True(t, result.Successful(), "command failed: %#v", result)
   239  	case <-time.After(10 * time.Second):
   240  		require.Fail(t, "timeout waiting for task to shutdown")
   241  	}
   242  
   243  	sleepPid := 0
   244  
   245  	// Ensure that the task is marked as dead, but account
   246  	// for WaitTask() closing channel before internal state is updated
   247  	testutil.WaitForResult(func() (bool, error) {
   248  		stdout, err := ioutil.ReadFile(filepath.Join(task.TaskDir().LogDir, "test.stdout.0"))
   249  		if err != nil {
   250  			return false, fmt.Errorf("failed to output pid file: %v", err)
   251  		}
   252  
   253  		pidMatch := regexp.MustCompile(`SLEEP_PID=(\d+)`).FindStringSubmatch(string(stdout))
   254  		if len(pidMatch) != 2 {
   255  			return false, fmt.Errorf("failed to find pid in %s", string(stdout))
   256  		}
   257  
   258  		pid, err := strconv.Atoi(pidMatch[1])
   259  		if err != nil {
   260  			return false, fmt.Errorf("pid parts aren't int: %s", pidMatch[1])
   261  		}
   262  
   263  		sleepPid = pid
   264  		return true, nil
   265  	}, func(err error) {
   266  		require.NoError(t, err)
   267  	})
   268  
   269  	// isProcessRunning returns an error if process is not running
   270  	isProcessRunning := func(pid int) error {
   271  		process, err := os.FindProcess(pid)
   272  		if err != nil {
   273  			return fmt.Errorf("failed to find process: %s", err)
   274  		}
   275  
   276  		err = process.Signal(syscall.Signal(0))
   277  		if err != nil {
   278  			return fmt.Errorf("failed to signal process: %s", err)
   279  		}
   280  
   281  		return nil
   282  	}
   283  
   284  	require.NoError(t, isProcessRunning(sleepPid))
   285  
   286  	require.NoError(t, harness.DestroyTask(task.ID, true))
   287  
   288  	testutil.WaitForResult(func() (bool, error) {
   289  		err := isProcessRunning(sleepPid)
   290  		if err == nil {
   291  			return false, fmt.Errorf("child process is still running")
   292  		}
   293  
   294  		if !strings.Contains(err.Error(), "failed to signal process") {
   295  			return false, fmt.Errorf("unexpected error: %v", err)
   296  		}
   297  
   298  		return true, nil
   299  	}, func(err error) {
   300  		require.NoError(t, err)
   301  	})
   302  }
   303  
   304  func TestRawExec_ExecTaskStreaming(t *testing.T) {
   305  	ci.Parallel(t)
   306  	if runtime.GOOS == "darwin" {
   307  		t.Skip("skip running exec tasks on darwin as darwin has restrictions on starting tty shells")
   308  	}
   309  	require := require.New(t)
   310  
   311  	d := newEnabledRawExecDriver(t)
   312  	harness := dtestutil.NewDriverHarness(t, d)
   313  	defer harness.Kill()
   314  
   315  	task := &drivers.TaskConfig{
   316  		AllocID: uuid.Generate(),
   317  		ID:      uuid.Generate(),
   318  		Name:    "sleep",
   319  		Env:     defaultEnv(),
   320  	}
   321  
   322  	cleanup := harness.MkAllocDir(task, false)
   323  	defer cleanup()
   324  
   325  	tc := &TaskConfig{
   326  		Command: testtask.Path(),
   327  		Args:    []string{"sleep", "9000s"},
   328  	}
   329  	require.NoError(task.EncodeConcreteDriverConfig(&tc))
   330  	testtask.SetTaskConfigEnv(task)
   331  
   332  	_, _, err := harness.StartTask(task)
   333  	require.NoError(err)
   334  	defer d.DestroyTask(task.ID, true)
   335  
   336  	dtestutil.ExecTaskStreamingConformanceTests(t, harness, task.ID)
   337  
   338  }
   339  
   340  func TestRawExec_ExecTaskStreaming_User(t *testing.T) {
   341  	ci.Parallel(t)
   342  	clienttestutil.RequireLinux(t)
   343  
   344  	d := newEnabledRawExecDriver(t)
   345  
   346  	// because we cannot set AllocID, see below
   347  	d.config.NoCgroups = true
   348  
   349  	harness := dtestutil.NewDriverHarness(t, d)
   350  	defer harness.Kill()
   351  
   352  	task := &drivers.TaskConfig{
   353  		// todo(shoenig) - Setting AllocID causes test to fail - with or without
   354  		//  cgroups, and with or without chroot. It has to do with MkAllocDir
   355  		//  creating the directory structure, but the actual root cause is still
   356  		//  TBD. The symptom is that any command you try to execute will result
   357  		//  in "permission denied" coming from os/exec. This test is imperfect,
   358  		//  the actual feature of running commands as another user works fine.
   359  		// AllocID: uuid.Generate()
   360  		ID:   uuid.Generate(),
   361  		Name: "sleep",
   362  		User: "nobody",
   363  	}
   364  
   365  	cleanup := harness.MkAllocDir(task, false)
   366  	defer cleanup()
   367  
   368  	err := os.Chmod(task.AllocDir, 0777)
   369  	require.NoError(t, err)
   370  
   371  	tc := &TaskConfig{
   372  		Command: "/bin/sleep",
   373  		Args:    []string{"9000"},
   374  	}
   375  	require.NoError(t, task.EncodeConcreteDriverConfig(&tc))
   376  	testtask.SetTaskConfigEnv(task)
   377  
   378  	_, _, err = harness.StartTask(task)
   379  	require.NoError(t, err)
   380  	defer d.DestroyTask(task.ID, true)
   381  
   382  	code, stdout, stderr := dtestutil.ExecTask(t, harness, task.ID, "whoami", false, "")
   383  	require.Zero(t, code)
   384  	require.Empty(t, stderr)
   385  	require.Contains(t, stdout, "nobody")
   386  }
   387  
   388  func TestRawExecDriver_NoCgroup(t *testing.T) {
   389  	ci.Parallel(t)
   390  	clienttestutil.RequireLinux(t)
   391  
   392  	expectedBytes, err := ioutil.ReadFile("/proc/self/cgroup")
   393  	require.NoError(t, err)
   394  	expected := strings.TrimSpace(string(expectedBytes))
   395  
   396  	d := newEnabledRawExecDriver(t)
   397  	d.config.NoCgroups = true
   398  	harness := dtestutil.NewDriverHarness(t, d)
   399  
   400  	task := &drivers.TaskConfig{
   401  		AllocID: uuid.Generate(),
   402  		ID:      uuid.Generate(),
   403  		Name:    "nocgroup",
   404  	}
   405  
   406  	cleanup := harness.MkAllocDir(task, true)
   407  	defer cleanup()
   408  
   409  	tc := &TaskConfig{
   410  		Command: "/bin/cat",
   411  		Args:    []string{"/proc/self/cgroup"},
   412  	}
   413  	require.NoError(t, task.EncodeConcreteDriverConfig(&tc))
   414  	testtask.SetTaskConfigEnv(task)
   415  
   416  	_, _, err = harness.StartTask(task)
   417  	require.NoError(t, err)
   418  
   419  	// Task should terminate quickly
   420  	waitCh, err := harness.WaitTask(context.Background(), task.ID)
   421  	require.NoError(t, err)
   422  	select {
   423  	case res := <-waitCh:
   424  		require.True(t, res.Successful())
   425  		require.Zero(t, res.ExitCode)
   426  	case <-time.After(time.Duration(testutil.TestMultiplier()*6) * time.Second):
   427  		require.Fail(t, "WaitTask timeout")
   428  	}
   429  
   430  	// Check the log file to see it exited because of the signal
   431  	outputFile := filepath.Join(task.TaskDir().LogDir, "nocgroup.stdout.0")
   432  	testutil.WaitForResult(func() (bool, error) {
   433  		act, err := ioutil.ReadFile(outputFile)
   434  		if err != nil {
   435  			return false, fmt.Errorf("Couldn't read expected output: %v", err)
   436  		}
   437  
   438  		if strings.TrimSpace(string(act)) != expected {
   439  			t.Logf("Read from %v", outputFile)
   440  			return false, fmt.Errorf("Command outputted\n%v; want\n%v", string(act), expected)
   441  		}
   442  		return true, nil
   443  	}, func(err error) { require.NoError(t, err) })
   444  }