github.com/bigcommerce/nomad@v0.9.3-bc/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  	ctestutil "github.com/hashicorp/nomad/client/testutil"
    17  	"github.com/hashicorp/nomad/helper/pluginutils/hclutils"
    18  	"github.com/hashicorp/nomad/helper/testlog"
    19  	"github.com/hashicorp/nomad/helper/testtask"
    20  	"github.com/hashicorp/nomad/helper/uuid"
    21  	basePlug "github.com/hashicorp/nomad/plugins/base"
    22  	"github.com/hashicorp/nomad/plugins/drivers"
    23  	dtestutil "github.com/hashicorp/nomad/plugins/drivers/testutils"
    24  	pstructs "github.com/hashicorp/nomad/plugins/shared/structs"
    25  	"github.com/hashicorp/nomad/testutil"
    26  	"github.com/stretchr/testify/require"
    27  )
    28  
    29  func TestMain(m *testing.M) {
    30  	if !testtask.Run() {
    31  		os.Exit(m.Run())
    32  	}
    33  }
    34  
    35  func TestRawExecDriver_SetConfig(t *testing.T) {
    36  	t.Parallel()
    37  	require := require.New(t)
    38  
    39  	d := NewRawExecDriver(testlog.HCLogger(t))
    40  	harness := dtestutil.NewDriverHarness(t, d)
    41  	defer harness.Kill()
    42  
    43  	bconfig := &basePlug.Config{}
    44  
    45  	// Disable raw exec.
    46  	config := &Config{}
    47  
    48  	var data []byte
    49  	require.NoError(basePlug.MsgPackEncode(&data, config))
    50  	bconfig.PluginConfig = data
    51  	require.NoError(harness.SetConfig(bconfig))
    52  	require.Exactly(config, d.(*Driver).config)
    53  
    54  	config.Enabled = true
    55  	config.NoCgroups = true
    56  	data = []byte{}
    57  	require.NoError(basePlug.MsgPackEncode(&data, config))
    58  	bconfig.PluginConfig = data
    59  	require.NoError(harness.SetConfig(bconfig))
    60  	require.Exactly(config, d.(*Driver).config)
    61  
    62  	config.NoCgroups = false
    63  	data = []byte{}
    64  	require.NoError(basePlug.MsgPackEncode(&data, config))
    65  	bconfig.PluginConfig = data
    66  	require.NoError(harness.SetConfig(bconfig))
    67  	require.Exactly(config, d.(*Driver).config)
    68  }
    69  
    70  func TestRawExecDriver_Fingerprint(t *testing.T) {
    71  	t.Parallel()
    72  
    73  	fingerprintTest := func(config *Config, expected *drivers.Fingerprint) func(t *testing.T) {
    74  		return func(t *testing.T) {
    75  			require := require.New(t)
    76  			d := NewRawExecDriver(testlog.HCLogger(t)).(*Driver)
    77  			harness := dtestutil.NewDriverHarness(t, d)
    78  			defer harness.Kill()
    79  
    80  			var data []byte
    81  			require.NoError(basePlug.MsgPackEncode(&data, config))
    82  			bconfig := &basePlug.Config{
    83  				PluginConfig: data,
    84  			}
    85  			require.NoError(harness.SetConfig(bconfig))
    86  
    87  			fingerCh, err := harness.Fingerprint(context.Background())
    88  			require.NoError(err)
    89  			select {
    90  			case result := <-fingerCh:
    91  				require.Equal(expected, result)
    92  			case <-time.After(time.Duration(testutil.TestMultiplier()) * time.Second):
    93  				require.Fail("timeout receiving fingerprint")
    94  			}
    95  		}
    96  	}
    97  
    98  	cases := []struct {
    99  		Name     string
   100  		Conf     Config
   101  		Expected drivers.Fingerprint
   102  	}{
   103  		{
   104  			Name: "Disabled",
   105  			Conf: Config{
   106  				Enabled: false,
   107  			},
   108  			Expected: drivers.Fingerprint{
   109  				Attributes:        nil,
   110  				Health:            drivers.HealthStateUndetected,
   111  				HealthDescription: "disabled",
   112  			},
   113  		},
   114  		{
   115  			Name: "Enabled",
   116  			Conf: Config{
   117  				Enabled: true,
   118  			},
   119  			Expected: drivers.Fingerprint{
   120  				Attributes:        map[string]*pstructs.Attribute{"driver.raw_exec": pstructs.NewBoolAttribute(true)},
   121  				Health:            drivers.HealthStateHealthy,
   122  				HealthDescription: drivers.DriverHealthy,
   123  			},
   124  		},
   125  	}
   126  
   127  	for _, tc := range cases {
   128  		t.Run(tc.Name, fingerprintTest(&tc.Conf, &tc.Expected))
   129  	}
   130  }
   131  
   132  func TestRawExecDriver_StartWait(t *testing.T) {
   133  	t.Parallel()
   134  	require := require.New(t)
   135  
   136  	d := NewRawExecDriver(testlog.HCLogger(t))
   137  	harness := dtestutil.NewDriverHarness(t, d)
   138  	defer harness.Kill()
   139  	task := &drivers.TaskConfig{
   140  		ID:   uuid.Generate(),
   141  		Name: "test",
   142  	}
   143  
   144  	tc := &TaskConfig{
   145  		Command: testtask.Path(),
   146  		Args:    []string{"sleep", "10ms"},
   147  	}
   148  	require.NoError(task.EncodeConcreteDriverConfig(&tc))
   149  	testtask.SetTaskConfigEnv(task)
   150  
   151  	cleanup := harness.MkAllocDir(task, false)
   152  	defer cleanup()
   153  
   154  	handle, _, err := harness.StartTask(task)
   155  	require.NoError(err)
   156  
   157  	ch, err := harness.WaitTask(context.Background(), handle.Config.ID)
   158  	require.NoError(err)
   159  
   160  	var result *drivers.ExitResult
   161  	select {
   162  	case result = <-ch:
   163  	case <-time.After(5 * time.Second):
   164  		t.Fatal("timed out")
   165  	}
   166  
   167  	require.Zero(result.ExitCode)
   168  	require.Zero(result.Signal)
   169  	require.False(result.OOMKilled)
   170  	require.NoError(result.Err)
   171  	require.NoError(harness.DestroyTask(task.ID, true))
   172  }
   173  
   174  func TestRawExecDriver_StartWaitRecoverWaitStop(t *testing.T) {
   175  	t.Parallel()
   176  	require := require.New(t)
   177  
   178  	d := NewRawExecDriver(testlog.HCLogger(t))
   179  	harness := dtestutil.NewDriverHarness(t, d)
   180  	defer harness.Kill()
   181  
   182  	// Disable cgroups so test works without root
   183  	config := &Config{NoCgroups: true}
   184  	var data []byte
   185  	require.NoError(basePlug.MsgPackEncode(&data, config))
   186  	bconfig := &basePlug.Config{PluginConfig: data}
   187  	require.NoError(harness.SetConfig(bconfig))
   188  
   189  	task := &drivers.TaskConfig{
   190  		ID:   uuid.Generate(),
   191  		Name: "sleep",
   192  	}
   193  	tc := &TaskConfig{
   194  		Command: testtask.Path(),
   195  		Args:    []string{"sleep", "100s"},
   196  	}
   197  	require.NoError(task.EncodeConcreteDriverConfig(&tc))
   198  
   199  	testtask.SetTaskConfigEnv(task)
   200  	cleanup := harness.MkAllocDir(task, false)
   201  	defer cleanup()
   202  
   203  	handle, _, err := harness.StartTask(task)
   204  	require.NoError(err)
   205  
   206  	ch, err := harness.WaitTask(context.Background(), task.ID)
   207  	require.NoError(err)
   208  
   209  	var waitDone bool
   210  	var wg sync.WaitGroup
   211  	wg.Add(1)
   212  	go func() {
   213  		defer wg.Done()
   214  		result := <-ch
   215  		require.Error(result.Err)
   216  		waitDone = true
   217  	}()
   218  
   219  	originalStatus, err := d.InspectTask(task.ID)
   220  	require.NoError(err)
   221  
   222  	d.(*Driver).tasks.Delete(task.ID)
   223  
   224  	wg.Wait()
   225  	require.True(waitDone)
   226  	_, err = d.InspectTask(task.ID)
   227  	require.Equal(drivers.ErrTaskNotFound, err)
   228  
   229  	err = d.RecoverTask(handle)
   230  	require.NoError(err)
   231  
   232  	status, err := d.InspectTask(task.ID)
   233  	require.NoError(err)
   234  	require.Exactly(originalStatus, status)
   235  
   236  	ch, err = harness.WaitTask(context.Background(), task.ID)
   237  	require.NoError(err)
   238  
   239  	wg.Add(1)
   240  	waitDone = false
   241  	go func() {
   242  		defer wg.Done()
   243  		result := <-ch
   244  		require.NoError(result.Err)
   245  		require.NotZero(result.ExitCode)
   246  		require.Equal(9, result.Signal)
   247  		waitDone = true
   248  	}()
   249  
   250  	time.Sleep(300 * time.Millisecond)
   251  	require.NoError(d.StopTask(task.ID, 0, "SIGKILL"))
   252  	wg.Wait()
   253  	require.NoError(d.DestroyTask(task.ID, false))
   254  	require.True(waitDone)
   255  }
   256  
   257  func TestRawExecDriver_Start_Wait_AllocDir(t *testing.T) {
   258  	t.Parallel()
   259  	require := require.New(t)
   260  
   261  	d := NewRawExecDriver(testlog.HCLogger(t))
   262  	harness := dtestutil.NewDriverHarness(t, d)
   263  	defer harness.Kill()
   264  
   265  	task := &drivers.TaskConfig{
   266  		ID:   uuid.Generate(),
   267  		Name: "sleep",
   268  	}
   269  
   270  	cleanup := harness.MkAllocDir(task, false)
   271  	defer cleanup()
   272  
   273  	exp := []byte("win")
   274  	file := "output.txt"
   275  	outPath := fmt.Sprintf(`%s/%s`, task.TaskDir().SharedAllocDir, file)
   276  
   277  	tc := &TaskConfig{
   278  		Command: testtask.Path(),
   279  		Args:    []string{"sleep", "1s", "write", string(exp), outPath},
   280  	}
   281  	require.NoError(task.EncodeConcreteDriverConfig(&tc))
   282  	testtask.SetTaskConfigEnv(task)
   283  
   284  	_, _, err := harness.StartTask(task)
   285  	require.NoError(err)
   286  
   287  	// Task should terminate quickly
   288  	waitCh, err := harness.WaitTask(context.Background(), task.ID)
   289  	require.NoError(err)
   290  
   291  	select {
   292  	case res := <-waitCh:
   293  		require.NoError(res.Err)
   294  		require.True(res.Successful())
   295  	case <-time.After(time.Duration(testutil.TestMultiplier()*5) * time.Second):
   296  		require.Fail("WaitTask timeout")
   297  	}
   298  
   299  	// Check that data was written to the shared alloc directory.
   300  	outputFile := filepath.Join(task.TaskDir().SharedAllocDir, file)
   301  	act, err := ioutil.ReadFile(outputFile)
   302  	require.NoError(err)
   303  	require.Exactly(exp, act)
   304  	require.NoError(harness.DestroyTask(task.ID, true))
   305  }
   306  
   307  // This test creates a process tree such that without cgroups tracking the
   308  // processes cleanup of the children would not be possible. Thus the test
   309  // asserts that the processes get killed properly when using cgroups.
   310  func TestRawExecDriver_Start_Kill_Wait_Cgroup(t *testing.T) {
   311  	ctestutil.ExecCompatible(t)
   312  	t.Parallel()
   313  	require := require.New(t)
   314  	pidFile := "pid"
   315  
   316  	d := NewRawExecDriver(testlog.HCLogger(t))
   317  	harness := dtestutil.NewDriverHarness(t, d)
   318  	defer harness.Kill()
   319  
   320  	task := &drivers.TaskConfig{
   321  		ID:   uuid.Generate(),
   322  		Name: "sleep",
   323  		User: "root",
   324  	}
   325  
   326  	cleanup := harness.MkAllocDir(task, false)
   327  	defer cleanup()
   328  
   329  	tc := &TaskConfig{
   330  		Command: testtask.Path(),
   331  		Args:    []string{"fork/exec", pidFile, "pgrp", "0", "sleep", "20s"},
   332  	}
   333  	require.NoError(task.EncodeConcreteDriverConfig(&tc))
   334  	testtask.SetTaskConfigEnv(task)
   335  
   336  	_, _, err := harness.StartTask(task)
   337  	require.NoError(err)
   338  
   339  	// Find the process
   340  	var pidData []byte
   341  	testutil.WaitForResult(func() (bool, error) {
   342  		var err error
   343  		pidData, err = ioutil.ReadFile(filepath.Join(task.TaskDir().Dir, pidFile))
   344  		if err != nil {
   345  			return false, err
   346  		}
   347  
   348  		if len(pidData) == 0 {
   349  			return false, fmt.Errorf("pidFile empty")
   350  		}
   351  
   352  		return true, nil
   353  	}, func(err error) {
   354  		require.NoError(err)
   355  	})
   356  
   357  	pid, err := strconv.Atoi(string(pidData))
   358  	require.NoError(err, "failed to read pidData: %s", string(pidData))
   359  
   360  	// Check the pid is up
   361  	process, err := os.FindProcess(pid)
   362  	require.NoError(err)
   363  	require.NoError(process.Signal(syscall.Signal(0)))
   364  
   365  	var wg sync.WaitGroup
   366  	wg.Add(1)
   367  	go func() {
   368  		defer wg.Done()
   369  		time.Sleep(1 * time.Second)
   370  		err := harness.StopTask(task.ID, 0, "")
   371  
   372  		// Can't rely on the ordering between wait and kill on CI/travis...
   373  		if !testutil.IsCI() {
   374  			require.NoError(err)
   375  		}
   376  	}()
   377  
   378  	// Task should terminate quickly
   379  	waitCh, err := harness.WaitTask(context.Background(), task.ID)
   380  	require.NoError(err)
   381  	select {
   382  	case res := <-waitCh:
   383  		require.False(res.Successful())
   384  	case <-time.After(time.Duration(testutil.TestMultiplier()*5) * time.Second):
   385  		require.Fail("WaitTask timeout")
   386  	}
   387  
   388  	testutil.WaitForResult(func() (bool, error) {
   389  		if err := process.Signal(syscall.Signal(0)); err == nil {
   390  			return false, fmt.Errorf("process should not exist: %v", pid)
   391  		}
   392  
   393  		return true, nil
   394  	}, func(err error) {
   395  		require.NoError(err)
   396  	})
   397  
   398  	wg.Wait()
   399  	require.NoError(harness.DestroyTask(task.ID, true))
   400  }
   401  
   402  func TestRawExecDriver_Exec(t *testing.T) {
   403  	t.Parallel()
   404  	require := require.New(t)
   405  
   406  	d := NewRawExecDriver(testlog.HCLogger(t))
   407  	harness := dtestutil.NewDriverHarness(t, d)
   408  	defer harness.Kill()
   409  
   410  	task := &drivers.TaskConfig{
   411  		ID:   uuid.Generate(),
   412  		Name: "sleep",
   413  	}
   414  
   415  	cleanup := harness.MkAllocDir(task, false)
   416  	defer cleanup()
   417  
   418  	tc := &TaskConfig{
   419  		Command: testtask.Path(),
   420  		Args:    []string{"sleep", "9000s"},
   421  	}
   422  	require.NoError(task.EncodeConcreteDriverConfig(&tc))
   423  	testtask.SetTaskConfigEnv(task)
   424  
   425  	_, _, err := harness.StartTask(task)
   426  	require.NoError(err)
   427  
   428  	if runtime.GOOS == "windows" {
   429  		// Exec a command that should work
   430  		res, err := harness.ExecTask(task.ID, []string{"cmd.exe", "/c", "echo", "hello"}, 1*time.Second)
   431  		require.NoError(err)
   432  		require.True(res.ExitResult.Successful())
   433  		require.Equal(string(res.Stdout), "hello\r\n")
   434  
   435  		// Exec a command that should fail
   436  		res, err = harness.ExecTask(task.ID, []string{"cmd.exe", "/c", "stat", "notarealfile123abc"}, 1*time.Second)
   437  		require.NoError(err)
   438  		require.False(res.ExitResult.Successful())
   439  		require.Contains(string(res.Stdout), "not recognized")
   440  	} else {
   441  		// Exec a command that should work
   442  		res, err := harness.ExecTask(task.ID, []string{"/usr/bin/stat", "/tmp"}, 1*time.Second)
   443  		require.NoError(err)
   444  		require.True(res.ExitResult.Successful())
   445  		require.True(len(res.Stdout) > 100)
   446  
   447  		// Exec a command that should fail
   448  		res, err = harness.ExecTask(task.ID, []string{"/usr/bin/stat", "notarealfile123abc"}, 1*time.Second)
   449  		require.NoError(err)
   450  		require.False(res.ExitResult.Successful())
   451  		require.Contains(string(res.Stdout), "No such file or directory")
   452  	}
   453  
   454  	require.NoError(harness.DestroyTask(task.ID, true))
   455  }
   456  
   457  func TestConfig_ParseAllHCL(t *testing.T) {
   458  	cfgStr := `
   459  config {
   460    command = "/bin/bash"
   461    args = ["-c", "echo hello"]
   462  }`
   463  
   464  	expected := &TaskConfig{
   465  		Command: "/bin/bash",
   466  		Args:    []string{"-c", "echo hello"},
   467  	}
   468  
   469  	var tc *TaskConfig
   470  	hclutils.NewConfigParser(taskConfigSpec).ParseHCL(t, cfgStr, &tc)
   471  
   472  	require.EqualValues(t, expected, tc)
   473  }