github.com/bigcommerce/nomad@v0.9.3-bc/drivers/exec/driver_test.go (about)

     1  package exec
     2  
     3  import (
     4  	"bytes"
     5  	"context"
     6  	"fmt"
     7  	"io/ioutil"
     8  	"os"
     9  	"path/filepath"
    10  	"runtime"
    11  	"strings"
    12  	"sync"
    13  	"testing"
    14  	"time"
    15  
    16  	ctestutils "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  	"github.com/hashicorp/nomad/nomad/structs"
    22  	"github.com/hashicorp/nomad/plugins/drivers"
    23  	dtestutil "github.com/hashicorp/nomad/plugins/drivers/testutils"
    24  	"github.com/hashicorp/nomad/testutil"
    25  	"github.com/stretchr/testify/require"
    26  )
    27  
    28  func TestMain(m *testing.M) {
    29  	if !testtask.Run() {
    30  		os.Exit(m.Run())
    31  	}
    32  }
    33  
    34  var testResources = &drivers.Resources{
    35  	NomadResources: &structs.AllocatedTaskResources{
    36  		Memory: structs.AllocatedMemoryResources{
    37  			MemoryMB: 128,
    38  		},
    39  		Cpu: structs.AllocatedCpuResources{
    40  			CpuShares: 100,
    41  		},
    42  	},
    43  	LinuxResources: &drivers.LinuxResources{
    44  		MemoryLimitBytes: 134217728,
    45  		CPUShares:        100,
    46  	},
    47  }
    48  
    49  func TestExecDriver_Fingerprint_NonLinux(t *testing.T) {
    50  	if !testutil.IsCI() {
    51  		t.Parallel()
    52  	}
    53  	require := require.New(t)
    54  	if runtime.GOOS == "linux" {
    55  		t.Skip("Test only available not on Linux")
    56  	}
    57  
    58  	d := NewExecDriver(testlog.HCLogger(t))
    59  	harness := dtestutil.NewDriverHarness(t, d)
    60  
    61  	fingerCh, err := harness.Fingerprint(context.Background())
    62  	require.NoError(err)
    63  	select {
    64  	case finger := <-fingerCh:
    65  		require.Equal(drivers.HealthStateUndetected, finger.Health)
    66  	case <-time.After(time.Duration(testutil.TestMultiplier()*5) * time.Second):
    67  		require.Fail("timeout receiving fingerprint")
    68  	}
    69  }
    70  
    71  func TestExecDriver_Fingerprint(t *testing.T) {
    72  	t.Parallel()
    73  	require := require.New(t)
    74  
    75  	ctestutils.ExecCompatible(t)
    76  
    77  	d := NewExecDriver(testlog.HCLogger(t))
    78  	harness := dtestutil.NewDriverHarness(t, d)
    79  
    80  	fingerCh, err := harness.Fingerprint(context.Background())
    81  	require.NoError(err)
    82  	select {
    83  	case finger := <-fingerCh:
    84  		require.Equal(drivers.HealthStateHealthy, finger.Health)
    85  		require.True(finger.Attributes["driver.exec"].GetBool())
    86  	case <-time.After(time.Duration(testutil.TestMultiplier()*5) * time.Second):
    87  		require.Fail("timeout receiving fingerprint")
    88  	}
    89  }
    90  
    91  func TestExecDriver_StartWait(t *testing.T) {
    92  	t.Parallel()
    93  	require := require.New(t)
    94  	ctestutils.ExecCompatible(t)
    95  
    96  	d := NewExecDriver(testlog.HCLogger(t))
    97  	harness := dtestutil.NewDriverHarness(t, d)
    98  	task := &drivers.TaskConfig{
    99  		ID:        uuid.Generate(),
   100  		Name:      "test",
   101  		Resources: testResources,
   102  	}
   103  
   104  	tc := &TaskConfig{
   105  		Command: "cat",
   106  		Args:    []string{"/proc/self/cgroup"},
   107  	}
   108  	require.NoError(task.EncodeConcreteDriverConfig(&tc))
   109  
   110  	cleanup := harness.MkAllocDir(task, false)
   111  	defer cleanup()
   112  
   113  	handle, _, err := harness.StartTask(task)
   114  	require.NoError(err)
   115  
   116  	ch, err := harness.WaitTask(context.Background(), handle.Config.ID)
   117  	require.NoError(err)
   118  	result := <-ch
   119  	require.Zero(result.ExitCode)
   120  	require.NoError(harness.DestroyTask(task.ID, true))
   121  }
   122  
   123  func TestExecDriver_StartWaitStopKill(t *testing.T) {
   124  	t.Parallel()
   125  	require := require.New(t)
   126  	ctestutils.ExecCompatible(t)
   127  
   128  	d := NewExecDriver(testlog.HCLogger(t))
   129  	harness := dtestutil.NewDriverHarness(t, d)
   130  	task := &drivers.TaskConfig{
   131  		ID:        uuid.Generate(),
   132  		Name:      "test",
   133  		Resources: testResources,
   134  	}
   135  
   136  	tc := &TaskConfig{
   137  		Command: "/bin/bash",
   138  		Args:    []string{"-c", "echo hi; sleep 600"},
   139  	}
   140  	require.NoError(task.EncodeConcreteDriverConfig(&tc))
   141  
   142  	cleanup := harness.MkAllocDir(task, false)
   143  	defer cleanup()
   144  
   145  	handle, _, err := harness.StartTask(task)
   146  	require.NoError(err)
   147  	defer harness.DestroyTask(task.ID, true)
   148  
   149  	ch, err := harness.WaitTask(context.Background(), handle.Config.ID)
   150  	require.NoError(err)
   151  
   152  	require.NoError(harness.WaitUntilStarted(task.ID, 1*time.Second))
   153  
   154  	go func() {
   155  		harness.StopTask(task.ID, 2*time.Second, "SIGINT")
   156  	}()
   157  
   158  	select {
   159  	case result := <-ch:
   160  		require.False(result.Successful())
   161  	case <-time.After(10 * time.Second):
   162  		require.Fail("timeout waiting for task to shutdown")
   163  	}
   164  
   165  	// Ensure that the task is marked as dead, but account
   166  	// for WaitTask() closing channel before internal state is updated
   167  	testutil.WaitForResult(func() (bool, error) {
   168  		status, err := harness.InspectTask(task.ID)
   169  		if err != nil {
   170  			return false, fmt.Errorf("inspecting task failed: %v", err)
   171  		}
   172  		if status.State != drivers.TaskStateExited {
   173  			return false, fmt.Errorf("task hasn't exited yet; status: %v", status.State)
   174  		}
   175  
   176  		return true, nil
   177  	}, func(err error) {
   178  		require.NoError(err)
   179  	})
   180  
   181  	require.NoError(harness.DestroyTask(task.ID, true))
   182  }
   183  
   184  func TestExecDriver_StartWaitRecover(t *testing.T) {
   185  	t.Parallel()
   186  	require := require.New(t)
   187  	ctestutils.ExecCompatible(t)
   188  
   189  	d := NewExecDriver(testlog.HCLogger(t))
   190  	harness := dtestutil.NewDriverHarness(t, d)
   191  	task := &drivers.TaskConfig{
   192  		ID:        uuid.Generate(),
   193  		Name:      "test",
   194  		Resources: testResources,
   195  	}
   196  
   197  	tc := &TaskConfig{
   198  		Command: "/bin/sleep",
   199  		Args:    []string{"5"},
   200  	}
   201  	require.NoError(task.EncodeConcreteDriverConfig(&tc))
   202  
   203  	cleanup := harness.MkAllocDir(task, false)
   204  	defer cleanup()
   205  
   206  	handle, _, err := harness.StartTask(task)
   207  	require.NoError(err)
   208  
   209  	ctx, cancel := context.WithCancel(context.Background())
   210  
   211  	ch, err := harness.WaitTask(ctx, handle.Config.ID)
   212  	require.NoError(err)
   213  
   214  	var wg sync.WaitGroup
   215  	wg.Add(1)
   216  	go func() {
   217  		defer wg.Done()
   218  		result := <-ch
   219  		require.Error(result.Err)
   220  	}()
   221  
   222  	require.NoError(harness.WaitUntilStarted(task.ID, 1*time.Second))
   223  	cancel()
   224  
   225  	waitCh := make(chan struct{})
   226  	go func() {
   227  		defer close(waitCh)
   228  		wg.Wait()
   229  	}()
   230  
   231  	select {
   232  	case <-waitCh:
   233  		status, err := harness.InspectTask(task.ID)
   234  		require.NoError(err)
   235  		require.Equal(drivers.TaskStateRunning, status.State)
   236  	case <-time.After(1 * time.Second):
   237  		require.Fail("timeout waiting for task wait to cancel")
   238  	}
   239  
   240  	// Loose task
   241  	d.(*Driver).tasks.Delete(task.ID)
   242  	_, err = harness.InspectTask(task.ID)
   243  	require.Error(err)
   244  
   245  	require.NoError(harness.RecoverTask(handle))
   246  	status, err := harness.InspectTask(task.ID)
   247  	require.NoError(err)
   248  	require.Equal(drivers.TaskStateRunning, status.State)
   249  
   250  	require.NoError(harness.StopTask(task.ID, 0, ""))
   251  	require.NoError(harness.DestroyTask(task.ID, true))
   252  }
   253  
   254  func TestExecDriver_Stats(t *testing.T) {
   255  	t.Parallel()
   256  	require := require.New(t)
   257  	ctestutils.ExecCompatible(t)
   258  
   259  	d := NewExecDriver(testlog.HCLogger(t))
   260  	harness := dtestutil.NewDriverHarness(t, d)
   261  	task := &drivers.TaskConfig{
   262  		ID:        uuid.Generate(),
   263  		Name:      "test",
   264  		Resources: testResources,
   265  	}
   266  
   267  	tc := &TaskConfig{
   268  		Command: "/bin/sleep",
   269  		Args:    []string{"5"},
   270  	}
   271  	require.NoError(task.EncodeConcreteDriverConfig(&tc))
   272  
   273  	cleanup := harness.MkAllocDir(task, false)
   274  	defer cleanup()
   275  
   276  	handle, _, err := harness.StartTask(task)
   277  	require.NoError(err)
   278  	require.NotNil(handle)
   279  
   280  	require.NoError(harness.WaitUntilStarted(task.ID, 1*time.Second))
   281  	ctx, cancel := context.WithCancel(context.Background())
   282  	defer cancel()
   283  	statsCh, err := harness.TaskStats(ctx, task.ID, time.Second*10)
   284  	require.NoError(err)
   285  	select {
   286  	case stats := <-statsCh:
   287  		require.NotZero(stats.ResourceUsage.MemoryStats.RSS)
   288  		require.NotZero(stats.Timestamp)
   289  		require.WithinDuration(time.Now(), time.Unix(0, stats.Timestamp), time.Second)
   290  	case <-time.After(time.Second):
   291  		require.Fail("timeout receiving from channel")
   292  	}
   293  
   294  	require.NoError(harness.DestroyTask(task.ID, true))
   295  }
   296  
   297  func TestExecDriver_Start_Wait_AllocDir(t *testing.T) {
   298  	t.Parallel()
   299  	require := require.New(t)
   300  	ctestutils.ExecCompatible(t)
   301  
   302  	d := NewExecDriver(testlog.HCLogger(t))
   303  	harness := dtestutil.NewDriverHarness(t, d)
   304  	task := &drivers.TaskConfig{
   305  		ID:        uuid.Generate(),
   306  		Name:      "sleep",
   307  		Resources: testResources,
   308  	}
   309  	cleanup := harness.MkAllocDir(task, false)
   310  	defer cleanup()
   311  
   312  	exp := []byte{'w', 'i', 'n'}
   313  	file := "output.txt"
   314  	tc := &TaskConfig{
   315  		Command: "/bin/bash",
   316  		Args: []string{
   317  			"-c",
   318  			fmt.Sprintf(`sleep 1; echo -n %s > /alloc/%s`, string(exp), file),
   319  		},
   320  	}
   321  	require.NoError(task.EncodeConcreteDriverConfig(&tc))
   322  
   323  	handle, _, err := harness.StartTask(task)
   324  	require.NoError(err)
   325  	require.NotNil(handle)
   326  
   327  	// Task should terminate quickly
   328  	waitCh, err := harness.WaitTask(context.Background(), task.ID)
   329  	require.NoError(err)
   330  	select {
   331  	case res := <-waitCh:
   332  		require.True(res.Successful(), "task should have exited successfully: %v", res)
   333  	case <-time.After(time.Duration(testutil.TestMultiplier()*5) * time.Second):
   334  		require.Fail("timeout waiting for task")
   335  	}
   336  
   337  	// Check that data was written to the shared alloc directory.
   338  	outputFile := filepath.Join(task.TaskDir().SharedAllocDir, file)
   339  	act, err := ioutil.ReadFile(outputFile)
   340  	require.NoError(err)
   341  	require.Exactly(exp, act)
   342  
   343  	require.NoError(harness.DestroyTask(task.ID, true))
   344  }
   345  
   346  func TestExecDriver_User(t *testing.T) {
   347  	t.Parallel()
   348  	require := require.New(t)
   349  	ctestutils.ExecCompatible(t)
   350  
   351  	d := NewExecDriver(testlog.HCLogger(t))
   352  	harness := dtestutil.NewDriverHarness(t, d)
   353  	task := &drivers.TaskConfig{
   354  		ID:        uuid.Generate(),
   355  		Name:      "sleep",
   356  		User:      "alice",
   357  		Resources: testResources,
   358  	}
   359  	cleanup := harness.MkAllocDir(task, false)
   360  	defer cleanup()
   361  
   362  	tc := &TaskConfig{
   363  		Command: "/bin/sleep",
   364  		Args:    []string{"100"},
   365  	}
   366  	require.NoError(task.EncodeConcreteDriverConfig(&tc))
   367  
   368  	handle, _, err := harness.StartTask(task)
   369  	require.Error(err)
   370  	require.Nil(handle)
   371  
   372  	msg := "user alice"
   373  	if !strings.Contains(err.Error(), msg) {
   374  		t.Fatalf("Expecting '%v' in '%v'", msg, err)
   375  	}
   376  }
   377  
   378  // TestExecDriver_HandlerExec ensures the exec driver's handle properly
   379  // executes commands inside the container.
   380  func TestExecDriver_HandlerExec(t *testing.T) {
   381  	t.Parallel()
   382  	require := require.New(t)
   383  	ctestutils.ExecCompatible(t)
   384  
   385  	d := NewExecDriver(testlog.HCLogger(t))
   386  	harness := dtestutil.NewDriverHarness(t, d)
   387  	task := &drivers.TaskConfig{
   388  		ID:        uuid.Generate(),
   389  		Name:      "sleep",
   390  		Resources: testResources,
   391  	}
   392  	cleanup := harness.MkAllocDir(task, false)
   393  	defer cleanup()
   394  
   395  	tc := &TaskConfig{
   396  		Command: "/bin/sleep",
   397  		Args:    []string{"9000"},
   398  	}
   399  	require.NoError(task.EncodeConcreteDriverConfig(&tc))
   400  
   401  	handle, _, err := harness.StartTask(task)
   402  	require.NoError(err)
   403  	require.NotNil(handle)
   404  
   405  	// Exec a command that should work and dump the environment
   406  	// TODO: enable section when exec env is fully loaded
   407  	/*res, err := harness.ExecTask(task.ID, []string{"/bin/sh", "-c", "env | grep ^NOMAD"}, time.Second)
   408  	require.NoError(err)
   409  	require.True(res.ExitResult.Successful())
   410  
   411  	// Assert exec'd commands are run in a task-like environment
   412  	scriptEnv := make(map[string]string)
   413  	for _, line := range strings.Split(string(res.Stdout), "\n") {
   414  		if line == "" {
   415  			continue
   416  		}
   417  		parts := strings.SplitN(string(line), "=", 2)
   418  		if len(parts) != 2 {
   419  			t.Fatalf("Invalid env var: %q", line)
   420  		}
   421  		scriptEnv[parts[0]] = parts[1]
   422  	}
   423  	if v, ok := scriptEnv["NOMAD_SECRETS_DIR"]; !ok || v != "/secrets" {
   424  		t.Errorf("Expected NOMAD_SECRETS_DIR=/secrets but found=%t value=%q", ok, v)
   425  	}*/
   426  
   427  	// Assert cgroup membership
   428  	res, err := harness.ExecTask(task.ID, []string{"/bin/cat", "/proc/self/cgroup"}, time.Second)
   429  	require.NoError(err)
   430  	require.True(res.ExitResult.Successful())
   431  	found := false
   432  	for _, line := range strings.Split(string(res.Stdout), "\n") {
   433  		// Every cgroup entry should be /nomad/$ALLOC_ID
   434  		if line == "" {
   435  			continue
   436  		}
   437  		// Skip rdma subsystem; rdma was added in most recent kernels and libcontainer/docker
   438  		// don't isolate it by default.
   439  		if strings.Contains(line, ":rdma:") {
   440  			continue
   441  		}
   442  		if !strings.Contains(line, ":/nomad/") {
   443  			t.Errorf("Not a member of the alloc's cgroup: expected=...:/nomad/... -- found=%q", line)
   444  			continue
   445  		}
   446  		found = true
   447  	}
   448  	require.True(found, "exec'd command isn't in the task's cgroup")
   449  
   450  	// Exec a command that should fail
   451  	res, err = harness.ExecTask(task.ID, []string{"/usr/bin/stat", "lkjhdsaflkjshowaisxmcvnlia"}, time.Second)
   452  	require.NoError(err)
   453  	require.False(res.ExitResult.Successful())
   454  	if expected := "No such file or directory"; !bytes.Contains(res.Stdout, []byte(expected)) {
   455  		t.Fatalf("expected output to contain %q but found: %q", expected, res.Stdout)
   456  	}
   457  
   458  	require.NoError(harness.DestroyTask(task.ID, true))
   459  }
   460  
   461  func TestExecDriver_DevicesAndMounts(t *testing.T) {
   462  	t.Parallel()
   463  	require := require.New(t)
   464  	ctestutils.ExecCompatible(t)
   465  
   466  	tmpDir, err := ioutil.TempDir("", "exec_binds_mounts")
   467  	require.NoError(err)
   468  	defer os.RemoveAll(tmpDir)
   469  
   470  	err = ioutil.WriteFile(filepath.Join(tmpDir, "testfile"), []byte("from-host"), 600)
   471  	require.NoError(err)
   472  
   473  	d := NewExecDriver(testlog.HCLogger(t))
   474  	harness := dtestutil.NewDriverHarness(t, d)
   475  	task := &drivers.TaskConfig{
   476  		ID:         uuid.Generate(),
   477  		Name:       "test",
   478  		User:       "root", // need permission to read mounts paths
   479  		Resources:  testResources,
   480  		StdoutPath: filepath.Join(tmpDir, "task-stdout"),
   481  		StderrPath: filepath.Join(tmpDir, "task-stderr"),
   482  		Devices: []*drivers.DeviceConfig{
   483  			{
   484  				TaskPath:    "/dev/inserted-random",
   485  				HostPath:    "/dev/random",
   486  				Permissions: "rw",
   487  			},
   488  		},
   489  		Mounts: []*drivers.MountConfig{
   490  			{
   491  				TaskPath: "/tmp/task-path-rw",
   492  				HostPath: tmpDir,
   493  				Readonly: false,
   494  			},
   495  			{
   496  				TaskPath: "/tmp/task-path-ro",
   497  				HostPath: tmpDir,
   498  				Readonly: true,
   499  			},
   500  		},
   501  	}
   502  
   503  	require.NoError(ioutil.WriteFile(task.StdoutPath, []byte{}, 660))
   504  	require.NoError(ioutil.WriteFile(task.StderrPath, []byte{}, 660))
   505  
   506  	tc := &TaskConfig{
   507  		Command: "/bin/bash",
   508  		Args: []string{"-c", `
   509  export LANG=en.UTF-8
   510  echo "mounted device /inserted-random: $(stat -c '%t:%T' /dev/inserted-random)"
   511  echo "reading from ro path: $(cat /tmp/task-path-ro/testfile)"
   512  echo "reading from rw path: $(cat /tmp/task-path-rw/testfile)"
   513  touch /tmp/task-path-rw/testfile && echo 'overwriting file in rw succeeded'
   514  touch /tmp/task-path-rw/testfile-from-rw && echo from-exec >  /tmp/task-path-rw/testfile-from-rw && echo 'writing new file in rw succeeded'
   515  touch /tmp/task-path-ro/testfile && echo 'overwriting file in ro succeeded'
   516  touch /tmp/task-path-ro/testfile-from-ro && echo from-exec >  /tmp/task-path-ro/testfile-from-ro && echo 'writing new file in ro succeeded'
   517  exit 0
   518  `},
   519  	}
   520  	require.NoError(task.EncodeConcreteDriverConfig(&tc))
   521  
   522  	cleanup := harness.MkAllocDir(task, false)
   523  	defer cleanup()
   524  
   525  	handle, _, err := harness.StartTask(task)
   526  	require.NoError(err)
   527  
   528  	ch, err := harness.WaitTask(context.Background(), handle.Config.ID)
   529  	require.NoError(err)
   530  	result := <-ch
   531  	require.NoError(harness.DestroyTask(task.ID, true))
   532  
   533  	stdout, err := ioutil.ReadFile(task.StdoutPath)
   534  	require.NoError(err)
   535  	require.Equal(`mounted device /inserted-random: 1:8
   536  reading from ro path: from-host
   537  reading from rw path: from-host
   538  overwriting file in rw succeeded
   539  writing new file in rw succeeded`, strings.TrimSpace(string(stdout)))
   540  
   541  	stderr, err := ioutil.ReadFile(task.StderrPath)
   542  	require.NoError(err)
   543  	require.Equal(`touch: cannot touch '/tmp/task-path-ro/testfile': Read-only file system
   544  touch: cannot touch '/tmp/task-path-ro/testfile-from-ro': Read-only file system`, strings.TrimSpace(string(stderr)))
   545  
   546  	// testing exit code last so we can inspect output first
   547  	require.Zero(result.ExitCode)
   548  
   549  	fromRWContent, err := ioutil.ReadFile(filepath.Join(tmpDir, "testfile-from-rw"))
   550  	require.NoError(err)
   551  	require.Equal("from-exec", strings.TrimSpace(string(fromRWContent)))
   552  }
   553  
   554  func TestConfig_ParseAllHCL(t *testing.T) {
   555  	cfgStr := `
   556  config {
   557    command = "/bin/bash"
   558    args = ["-c", "echo hello"]
   559  }`
   560  
   561  	expected := &TaskConfig{
   562  		Command: "/bin/bash",
   563  		Args:    []string{"-c", "echo hello"},
   564  	}
   565  
   566  	var tc *TaskConfig
   567  	hclutils.NewConfigParser(taskConfigSpec).ParseHCL(t, cfgStr, &tc)
   568  
   569  	require.EqualValues(t, expected, tc)
   570  }