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

     1  package rawexec
     2  
     3  import (
     4  	"context"
     5  	"fmt"
     6  	"io/ioutil"
     7  	"os"
     8  	"path/filepath"
     9  	"runtime"
    10  	"strconv"
    11  	"sync"
    12  	"syscall"
    13  	"testing"
    14  	"time"
    15  
    16  	"github.com/hashicorp/nomad/ci"
    17  	"github.com/hashicorp/nomad/client/lib/cgutil"
    18  	ctestutil "github.com/hashicorp/nomad/client/testutil"
    19  	"github.com/hashicorp/nomad/helper/pluginutils/hclutils"
    20  	"github.com/hashicorp/nomad/helper/testlog"
    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  	pstructs "github.com/hashicorp/nomad/plugins/shared/structs"
    27  	"github.com/hashicorp/nomad/testutil"
    28  	"github.com/stretchr/testify/require"
    29  )
    30  
    31  // defaultEnv creates the default environment for raw exec tasks
    32  func defaultEnv() map[string]string {
    33  	m := make(map[string]string)
    34  	if cgutil.UseV2 {
    35  		// normally the taskenv.Builder will set this automatically from the
    36  		// Node object which gets created via Client configuration, but none of
    37  		// that exists in the test harness so just set it here.
    38  		m["NOMAD_PARENT_CGROUP"] = "nomad.slice"
    39  	}
    40  	return m
    41  }
    42  
    43  func TestMain(m *testing.M) {
    44  	if !testtask.Run() {
    45  		os.Exit(m.Run())
    46  	}
    47  }
    48  
    49  func newEnabledRawExecDriver(t *testing.T) *Driver {
    50  	ctx, cancel := context.WithCancel(context.Background())
    51  	t.Cleanup(cancel)
    52  
    53  	logger := testlog.HCLogger(t)
    54  	d := NewRawExecDriver(ctx, logger).(*Driver)
    55  	d.config.Enabled = true
    56  
    57  	return d
    58  }
    59  
    60  func TestRawExecDriver_SetConfig(t *testing.T) {
    61  	ci.Parallel(t)
    62  	require := require.New(t)
    63  
    64  	ctx, cancel := context.WithCancel(context.Background())
    65  	defer cancel()
    66  
    67  	logger := testlog.HCLogger(t)
    68  
    69  	d := NewRawExecDriver(ctx, logger)
    70  	harness := dtestutil.NewDriverHarness(t, d)
    71  	defer harness.Kill()
    72  
    73  	var (
    74  		bconfig = new(basePlug.Config)
    75  		config  = new(Config)
    76  		data    = make([]byte, 0)
    77  	)
    78  
    79  	// Default is raw_exec is disabled.
    80  	require.NoError(basePlug.MsgPackEncode(&data, config))
    81  	bconfig.PluginConfig = data
    82  	require.NoError(harness.SetConfig(bconfig))
    83  	require.Exactly(config, d.(*Driver).config)
    84  
    85  	// Enable raw_exec, but disable cgroups.
    86  	config.Enabled = true
    87  	config.NoCgroups = true
    88  	data = []byte{}
    89  	require.NoError(basePlug.MsgPackEncode(&data, config))
    90  	bconfig.PluginConfig = data
    91  	require.NoError(harness.SetConfig(bconfig))
    92  	require.Exactly(config, d.(*Driver).config)
    93  
    94  	// Enable raw_exec, enable cgroups.
    95  	config.NoCgroups = false
    96  	data = []byte{}
    97  	require.NoError(basePlug.MsgPackEncode(&data, config))
    98  	bconfig.PluginConfig = data
    99  	require.NoError(harness.SetConfig(bconfig))
   100  	require.Exactly(config, d.(*Driver).config)
   101  }
   102  
   103  func TestRawExecDriver_Fingerprint(t *testing.T) {
   104  	ci.Parallel(t)
   105  
   106  	fingerprintTest := func(config *Config, expected *drivers.Fingerprint) func(t *testing.T) {
   107  		return func(t *testing.T) {
   108  			require := require.New(t)
   109  			d := newEnabledRawExecDriver(t)
   110  			harness := dtestutil.NewDriverHarness(t, d)
   111  			defer harness.Kill()
   112  
   113  			var data []byte
   114  			require.NoError(basePlug.MsgPackEncode(&data, config))
   115  			bconfig := &basePlug.Config{
   116  				PluginConfig: data,
   117  			}
   118  			require.NoError(harness.SetConfig(bconfig))
   119  
   120  			fingerCh, err := harness.Fingerprint(context.Background())
   121  			require.NoError(err)
   122  			select {
   123  			case result := <-fingerCh:
   124  				require.Equal(expected, result)
   125  			case <-time.After(time.Duration(testutil.TestMultiplier()) * time.Second):
   126  				require.Fail("timeout receiving fingerprint")
   127  			}
   128  		}
   129  	}
   130  
   131  	cases := []struct {
   132  		Name     string
   133  		Conf     Config
   134  		Expected drivers.Fingerprint
   135  	}{
   136  		{
   137  			Name: "Disabled",
   138  			Conf: Config{
   139  				Enabled: false,
   140  			},
   141  			Expected: drivers.Fingerprint{
   142  				Attributes:        nil,
   143  				Health:            drivers.HealthStateUndetected,
   144  				HealthDescription: "disabled",
   145  			},
   146  		},
   147  		{
   148  			Name: "Enabled",
   149  			Conf: Config{
   150  				Enabled: true,
   151  			},
   152  			Expected: drivers.Fingerprint{
   153  				Attributes:        map[string]*pstructs.Attribute{"driver.raw_exec": pstructs.NewBoolAttribute(true)},
   154  				Health:            drivers.HealthStateHealthy,
   155  				HealthDescription: drivers.DriverHealthy,
   156  			},
   157  		},
   158  	}
   159  
   160  	for _, tc := range cases {
   161  		t.Run(tc.Name, fingerprintTest(&tc.Conf, &tc.Expected))
   162  	}
   163  }
   164  
   165  func TestRawExecDriver_StartWait(t *testing.T) {
   166  	ci.Parallel(t)
   167  	require := require.New(t)
   168  
   169  	d := newEnabledRawExecDriver(t)
   170  	harness := dtestutil.NewDriverHarness(t, d)
   171  	defer harness.Kill()
   172  	task := &drivers.TaskConfig{
   173  		AllocID: uuid.Generate(),
   174  		ID:      uuid.Generate(),
   175  		Name:    "test",
   176  		Env:     defaultEnv(),
   177  	}
   178  
   179  	tc := &TaskConfig{
   180  		Command: testtask.Path(),
   181  		Args:    []string{"sleep", "10ms"},
   182  	}
   183  	require.NoError(task.EncodeConcreteDriverConfig(&tc))
   184  	testtask.SetTaskConfigEnv(task)
   185  
   186  	cleanup := harness.MkAllocDir(task, false)
   187  	defer cleanup()
   188  
   189  	handle, _, err := harness.StartTask(task)
   190  	require.NoError(err)
   191  
   192  	ch, err := harness.WaitTask(context.Background(), handle.Config.ID)
   193  	require.NoError(err)
   194  
   195  	var result *drivers.ExitResult
   196  	select {
   197  	case result = <-ch:
   198  	case <-time.After(5 * time.Second):
   199  		t.Fatal("timed out")
   200  	}
   201  
   202  	require.Zero(result.ExitCode)
   203  	require.Zero(result.Signal)
   204  	require.False(result.OOMKilled)
   205  	require.NoError(result.Err)
   206  	require.NoError(harness.DestroyTask(task.ID, true))
   207  }
   208  
   209  func TestRawExecDriver_StartWaitRecoverWaitStop(t *testing.T) {
   210  	ci.Parallel(t)
   211  	require := require.New(t)
   212  
   213  	d := newEnabledRawExecDriver(t)
   214  	harness := dtestutil.NewDriverHarness(t, d)
   215  	defer harness.Kill()
   216  
   217  	// Disable cgroups so test works without root
   218  	config := &Config{NoCgroups: true, Enabled: true}
   219  	var data []byte
   220  	require.NoError(basePlug.MsgPackEncode(&data, config))
   221  	bconfig := &basePlug.Config{PluginConfig: data}
   222  	require.NoError(harness.SetConfig(bconfig))
   223  
   224  	task := &drivers.TaskConfig{
   225  		AllocID: uuid.Generate(),
   226  		ID:      uuid.Generate(),
   227  		Name:    "sleep",
   228  		Env:     defaultEnv(),
   229  	}
   230  	tc := &TaskConfig{
   231  		Command: testtask.Path(),
   232  		Args:    []string{"sleep", "100s"},
   233  	}
   234  	require.NoError(task.EncodeConcreteDriverConfig(&tc))
   235  
   236  	testtask.SetTaskConfigEnv(task)
   237  	cleanup := harness.MkAllocDir(task, false)
   238  	defer cleanup()
   239  
   240  	handle, _, err := harness.StartTask(task)
   241  	require.NoError(err)
   242  
   243  	ch, err := harness.WaitTask(context.Background(), task.ID)
   244  	require.NoError(err)
   245  
   246  	var waitDone bool
   247  	var wg sync.WaitGroup
   248  	wg.Add(1)
   249  	go func() {
   250  		defer wg.Done()
   251  		result := <-ch
   252  		require.Error(result.Err)
   253  		waitDone = true
   254  	}()
   255  
   256  	originalStatus, err := d.InspectTask(task.ID)
   257  	require.NoError(err)
   258  
   259  	d.tasks.Delete(task.ID)
   260  
   261  	wg.Wait()
   262  	require.True(waitDone)
   263  	_, err = d.InspectTask(task.ID)
   264  	require.Equal(drivers.ErrTaskNotFound, err)
   265  
   266  	err = d.RecoverTask(handle)
   267  	require.NoError(err)
   268  
   269  	status, err := d.InspectTask(task.ID)
   270  	require.NoError(err)
   271  	require.Exactly(originalStatus, status)
   272  
   273  	ch, err = harness.WaitTask(context.Background(), task.ID)
   274  	require.NoError(err)
   275  
   276  	wg.Add(1)
   277  	waitDone = false
   278  	go func() {
   279  		defer wg.Done()
   280  		result := <-ch
   281  		require.NoError(result.Err)
   282  		require.NotZero(result.ExitCode)
   283  		require.Equal(9, result.Signal)
   284  		waitDone = true
   285  	}()
   286  
   287  	time.Sleep(300 * time.Millisecond)
   288  	require.NoError(d.StopTask(task.ID, 0, "SIGKILL"))
   289  	wg.Wait()
   290  	require.NoError(d.DestroyTask(task.ID, false))
   291  	require.True(waitDone)
   292  }
   293  
   294  func TestRawExecDriver_Start_Wait_AllocDir(t *testing.T) {
   295  	ci.Parallel(t)
   296  	require := require.New(t)
   297  
   298  	d := newEnabledRawExecDriver(t)
   299  	harness := dtestutil.NewDriverHarness(t, d)
   300  	defer harness.Kill()
   301  
   302  	task := &drivers.TaskConfig{
   303  		AllocID: uuid.Generate(),
   304  		ID:      uuid.Generate(),
   305  		Name:    "sleep",
   306  		Env:     defaultEnv(),
   307  	}
   308  
   309  	cleanup := harness.MkAllocDir(task, false)
   310  	defer cleanup()
   311  
   312  	exp := []byte("win")
   313  	file := "output.txt"
   314  	outPath := fmt.Sprintf(`%s/%s`, task.TaskDir().SharedAllocDir, file)
   315  
   316  	tc := &TaskConfig{
   317  		Command: testtask.Path(),
   318  		Args:    []string{"sleep", "1s", "write", string(exp), outPath},
   319  	}
   320  	require.NoError(task.EncodeConcreteDriverConfig(&tc))
   321  	testtask.SetTaskConfigEnv(task)
   322  
   323  	_, _, err := harness.StartTask(task)
   324  	require.NoError(err)
   325  
   326  	// Task should terminate quickly
   327  	waitCh, err := harness.WaitTask(context.Background(), task.ID)
   328  	require.NoError(err)
   329  
   330  	select {
   331  	case res := <-waitCh:
   332  		require.NoError(res.Err)
   333  		require.True(res.Successful())
   334  	case <-time.After(time.Duration(testutil.TestMultiplier()*5) * time.Second):
   335  		require.Fail("WaitTask timeout")
   336  	}
   337  
   338  	// Check that data was written to the shared alloc directory.
   339  	outputFile := filepath.Join(task.TaskDir().SharedAllocDir, file)
   340  	act, err := ioutil.ReadFile(outputFile)
   341  	require.NoError(err)
   342  	require.Exactly(exp, act)
   343  	require.NoError(harness.DestroyTask(task.ID, true))
   344  }
   345  
   346  // This test creates a process tree such that without cgroups tracking the
   347  // processes cleanup of the children would not be possible. Thus the test
   348  // asserts that the processes get killed properly when using cgroups.
   349  func TestRawExecDriver_Start_Kill_Wait_Cgroup(t *testing.T) {
   350  	ci.Parallel(t)
   351  	ctestutil.ExecCompatible(t)
   352  
   353  	require := require.New(t)
   354  	pidFile := "pid"
   355  
   356  	d := newEnabledRawExecDriver(t)
   357  	harness := dtestutil.NewDriverHarness(t, d)
   358  	defer harness.Kill()
   359  
   360  	task := &drivers.TaskConfig{
   361  		AllocID: uuid.Generate(),
   362  		ID:      uuid.Generate(),
   363  		Name:    "sleep",
   364  		User:    "root",
   365  		Env:     defaultEnv(),
   366  	}
   367  
   368  	cleanup := harness.MkAllocDir(task, false)
   369  	defer cleanup()
   370  
   371  	tc := &TaskConfig{
   372  		Command: testtask.Path(),
   373  		Args:    []string{"fork/exec", pidFile, "pgrp", "0", "sleep", "20s"},
   374  	}
   375  	require.NoError(task.EncodeConcreteDriverConfig(&tc))
   376  	testtask.SetTaskConfigEnv(task)
   377  
   378  	_, _, err := harness.StartTask(task)
   379  	require.NoError(err)
   380  
   381  	// Find the process
   382  	var pidData []byte
   383  	testutil.WaitForResult(func() (bool, error) {
   384  		var err error
   385  		pidData, err = ioutil.ReadFile(filepath.Join(task.TaskDir().Dir, pidFile))
   386  		if err != nil {
   387  			return false, err
   388  		}
   389  
   390  		if len(pidData) == 0 {
   391  			return false, fmt.Errorf("pidFile empty")
   392  		}
   393  
   394  		return true, nil
   395  	}, func(err error) {
   396  		require.NoError(err)
   397  	})
   398  
   399  	pid, err := strconv.Atoi(string(pidData))
   400  	require.NoError(err, "failed to read pidData: %s", string(pidData))
   401  
   402  	// Check the pid is up
   403  	process, err := os.FindProcess(pid)
   404  	require.NoError(err)
   405  	require.NoError(process.Signal(syscall.Signal(0)))
   406  
   407  	var wg sync.WaitGroup
   408  	wg.Add(1)
   409  	go func() {
   410  		defer wg.Done()
   411  		time.Sleep(1 * time.Second)
   412  		err := harness.StopTask(task.ID, 0, "")
   413  
   414  		// Can't rely on the ordering between wait and kill on CI/travis...
   415  		if !testutil.IsCI() {
   416  			require.NoError(err)
   417  		}
   418  	}()
   419  
   420  	// Task should terminate quickly
   421  	waitCh, err := harness.WaitTask(context.Background(), task.ID)
   422  	require.NoError(err)
   423  	select {
   424  	case res := <-waitCh:
   425  		require.False(res.Successful())
   426  	case <-time.After(time.Duration(testutil.TestMultiplier()*5) * time.Second):
   427  		require.Fail("WaitTask timeout")
   428  	}
   429  
   430  	testutil.WaitForResult(func() (bool, error) {
   431  		if err := process.Signal(syscall.Signal(0)); err == nil {
   432  			return false, fmt.Errorf("process should not exist: %v", pid)
   433  		}
   434  
   435  		return true, nil
   436  	}, func(err error) {
   437  		require.NoError(err)
   438  	})
   439  
   440  	wg.Wait()
   441  	require.NoError(harness.DestroyTask(task.ID, true))
   442  }
   443  
   444  func TestRawExecDriver_ParentCgroup(t *testing.T) {
   445  	ci.Parallel(t)
   446  	ctestutil.ExecCompatible(t)
   447  	ctestutil.CgroupsCompatibleV2(t)
   448  
   449  	d := newEnabledRawExecDriver(t)
   450  	harness := dtestutil.NewDriverHarness(t, d)
   451  	defer harness.Kill()
   452  
   453  	task := &drivers.TaskConfig{
   454  		AllocID: uuid.Generate(),
   455  		ID:      uuid.Generate(),
   456  		Name:    "sleep",
   457  		Env: map[string]string{
   458  			"NOMAD_PARENT_CGROUP": "custom.slice",
   459  		},
   460  	}
   461  
   462  	cleanup := harness.MkAllocDir(task, false)
   463  	defer cleanup()
   464  
   465  	// run sleep task
   466  	tc := &TaskConfig{
   467  		Command: testtask.Path(),
   468  		Args:    []string{"sleep", "9000s"},
   469  	}
   470  	require.NoError(t, task.EncodeConcreteDriverConfig(&tc))
   471  	testtask.SetTaskConfigEnv(task)
   472  	_, _, err := harness.StartTask(task)
   473  	require.NoError(t, err)
   474  
   475  	// inspect environment variable
   476  	res, execErr := harness.ExecTask(task.ID, []string{"/usr/bin/env"}, 1*time.Second)
   477  	require.NoError(t, execErr)
   478  	require.True(t, res.ExitResult.Successful())
   479  	require.Contains(t, string(res.Stdout), "custom.slice")
   480  
   481  	// inspect /proc/self/cgroup
   482  	res2, execErr2 := harness.ExecTask(task.ID, []string{"cat", "/proc/self/cgroup"}, 1*time.Second)
   483  	require.NoError(t, execErr2)
   484  	require.True(t, res2.ExitResult.Successful())
   485  	require.Contains(t, string(res2.Stdout), "custom.slice")
   486  
   487  	// kill the sleep task
   488  	require.NoError(t, harness.DestroyTask(task.ID, true))
   489  }
   490  
   491  func TestRawExecDriver_Exec(t *testing.T) {
   492  	ci.Parallel(t)
   493  	ctestutil.ExecCompatible(t)
   494  
   495  	require := require.New(t)
   496  
   497  	d := newEnabledRawExecDriver(t)
   498  	harness := dtestutil.NewDriverHarness(t, d)
   499  	defer harness.Kill()
   500  
   501  	task := &drivers.TaskConfig{
   502  		AllocID: uuid.Generate(),
   503  		ID:      uuid.Generate(),
   504  		Name:    "sleep",
   505  		Env:     defaultEnv(),
   506  	}
   507  
   508  	cleanup := harness.MkAllocDir(task, false)
   509  	defer cleanup()
   510  
   511  	tc := &TaskConfig{
   512  		Command: testtask.Path(),
   513  		Args:    []string{"sleep", "9000s"},
   514  	}
   515  	require.NoError(task.EncodeConcreteDriverConfig(&tc))
   516  	testtask.SetTaskConfigEnv(task)
   517  
   518  	_, _, err := harness.StartTask(task)
   519  	require.NoError(err)
   520  
   521  	if runtime.GOOS == "windows" {
   522  		// Exec a command that should work
   523  		res, err := harness.ExecTask(task.ID, []string{"cmd.exe", "/c", "echo", "hello"}, 1*time.Second)
   524  		require.NoError(err)
   525  		require.True(res.ExitResult.Successful())
   526  		require.Equal(string(res.Stdout), "hello\r\n")
   527  
   528  		// Exec a command that should fail
   529  		res, err = harness.ExecTask(task.ID, []string{"cmd.exe", "/c", "stat", "notarealfile123abc"}, 1*time.Second)
   530  		require.NoError(err)
   531  		require.False(res.ExitResult.Successful())
   532  		require.Contains(string(res.Stdout), "not recognized")
   533  	} else {
   534  		// Exec a command that should work
   535  		res, err := harness.ExecTask(task.ID, []string{"/usr/bin/stat", "/tmp"}, 1*time.Second)
   536  		require.NoError(err)
   537  		require.True(res.ExitResult.Successful())
   538  		require.True(len(res.Stdout) > 100)
   539  
   540  		// Exec a command that should fail
   541  		res, err = harness.ExecTask(task.ID, []string{"/usr/bin/stat", "notarealfile123abc"}, 1*time.Second)
   542  		require.NoError(err)
   543  		require.False(res.ExitResult.Successful())
   544  		require.Contains(string(res.Stdout), "No such file or directory")
   545  	}
   546  
   547  	require.NoError(harness.DestroyTask(task.ID, true))
   548  }
   549  
   550  func TestConfig_ParseAllHCL(t *testing.T) {
   551  	ci.Parallel(t)
   552  
   553  	cfgStr := `
   554  config {
   555    command = "/bin/bash"
   556    args = ["-c", "echo hello"]
   557  }`
   558  
   559  	expected := &TaskConfig{
   560  		Command: "/bin/bash",
   561  		Args:    []string{"-c", "echo hello"},
   562  	}
   563  
   564  	var tc *TaskConfig
   565  	hclutils.NewConfigParser(taskConfigSpec).ParseHCL(t, cfgStr, &tc)
   566  
   567  	require.EqualValues(t, expected, tc)
   568  }
   569  
   570  func TestRawExecDriver_Disabled(t *testing.T) {
   571  	ci.Parallel(t)
   572  	require := require.New(t)
   573  
   574  	d := newEnabledRawExecDriver(t)
   575  	d.config.Enabled = false
   576  
   577  	harness := dtestutil.NewDriverHarness(t, d)
   578  	defer harness.Kill()
   579  	task := &drivers.TaskConfig{
   580  		AllocID: uuid.Generate(),
   581  		ID:      uuid.Generate(),
   582  		Name:    "test",
   583  		Env:     defaultEnv(),
   584  	}
   585  
   586  	handle, _, err := harness.StartTask(task)
   587  	require.Error(err)
   588  	require.Contains(err.Error(), errDisabledDriver.Error())
   589  	require.Nil(handle)
   590  }