github.com/Ilhicas/nomad@v1.0.4-0.20210304152020-e86851182bc3/drivers/shared/executor/executor_linux_test.go (about)

     1  package executor
     2  
     3  import (
     4  	"context"
     5  	"fmt"
     6  	"io/ioutil"
     7  	"os"
     8  	"path/filepath"
     9  	"regexp"
    10  	"strconv"
    11  	"strings"
    12  	"testing"
    13  	"time"
    14  
    15  	"github.com/hashicorp/nomad/client/allocdir"
    16  	"github.com/hashicorp/nomad/client/taskenv"
    17  	"github.com/hashicorp/nomad/client/testutil"
    18  	"github.com/hashicorp/nomad/helper/testlog"
    19  	"github.com/hashicorp/nomad/nomad/mock"
    20  	"github.com/hashicorp/nomad/plugins/drivers"
    21  	tu "github.com/hashicorp/nomad/testutil"
    22  	"github.com/opencontainers/runc/libcontainer/cgroups"
    23  	lconfigs "github.com/opencontainers/runc/libcontainer/configs"
    24  	"github.com/stretchr/testify/require"
    25  	"golang.org/x/sys/unix"
    26  )
    27  
    28  func init() {
    29  	executorFactories["LibcontainerExecutor"] = libcontainerFactory
    30  }
    31  
    32  var libcontainerFactory = executorFactory{
    33  	new: NewExecutorWithIsolation,
    34  	configureExecCmd: func(t *testing.T, cmd *ExecCommand) {
    35  		cmd.ResourceLimits = true
    36  		setupRootfs(t, cmd.TaskDir)
    37  	},
    38  }
    39  
    40  // testExecutorContextWithChroot returns an ExecutorContext and AllocDir with
    41  // chroot. Use testExecutorContext if you don't need a chroot.
    42  //
    43  // The caller is responsible for calling AllocDir.Destroy() to cleanup.
    44  func testExecutorCommandWithChroot(t *testing.T) *testExecCmd {
    45  	chrootEnv := map[string]string{
    46  		"/etc/ld.so.cache":  "/etc/ld.so.cache",
    47  		"/etc/ld.so.conf":   "/etc/ld.so.conf",
    48  		"/etc/ld.so.conf.d": "/etc/ld.so.conf.d",
    49  		"/etc/passwd":       "/etc/passwd",
    50  		"/lib":              "/lib",
    51  		"/lib64":            "/lib64",
    52  		"/usr/lib":          "/usr/lib",
    53  		"/bin/ls":           "/bin/ls",
    54  		"/bin/cat":          "/bin/cat",
    55  		"/bin/echo":         "/bin/echo",
    56  		"/bin/bash":         "/bin/bash",
    57  		"/bin/sleep":        "/bin/sleep",
    58  		"/foobar":           "/does/not/exist",
    59  	}
    60  
    61  	alloc := mock.Alloc()
    62  	task := alloc.Job.TaskGroups[0].Tasks[0]
    63  	taskEnv := taskenv.NewBuilder(mock.Node(), alloc, task, "global").Build()
    64  
    65  	allocDir := allocdir.NewAllocDir(testlog.HCLogger(t), filepath.Join(os.TempDir(), alloc.ID))
    66  	if err := allocDir.Build(); err != nil {
    67  		t.Fatalf("AllocDir.Build() failed: %v", err)
    68  	}
    69  	if err := allocDir.NewTaskDir(task.Name).Build(true, chrootEnv); err != nil {
    70  		allocDir.Destroy()
    71  		t.Fatalf("allocDir.NewTaskDir(%q) failed: %v", task.Name, err)
    72  	}
    73  	td := allocDir.TaskDirs[task.Name]
    74  	cmd := &ExecCommand{
    75  		Env:     taskEnv.List(),
    76  		TaskDir: td.Dir,
    77  		Resources: &drivers.Resources{
    78  			NomadResources: alloc.AllocatedResources.Tasks[task.Name],
    79  		},
    80  	}
    81  
    82  	testCmd := &testExecCmd{
    83  		command:  cmd,
    84  		allocDir: allocDir,
    85  	}
    86  	configureTLogging(t, testCmd)
    87  	return testCmd
    88  }
    89  
    90  func TestExecutor_configureNamespaces(t *testing.T) {
    91  	t.Run("host host", func(t *testing.T) {
    92  		require.Equal(t, lconfigs.Namespaces{
    93  			{Type: lconfigs.NEWNS},
    94  		}, configureNamespaces("host", "host"))
    95  	})
    96  
    97  	t.Run("host private", func(t *testing.T) {
    98  		require.Equal(t, lconfigs.Namespaces{
    99  			{Type: lconfigs.NEWNS},
   100  			{Type: lconfigs.NEWIPC},
   101  		}, configureNamespaces("host", "private"))
   102  	})
   103  
   104  	t.Run("private host", func(t *testing.T) {
   105  		require.Equal(t, lconfigs.Namespaces{
   106  			{Type: lconfigs.NEWNS},
   107  			{Type: lconfigs.NEWPID},
   108  		}, configureNamespaces("private", "host"))
   109  	})
   110  
   111  	t.Run("private private", func(t *testing.T) {
   112  		require.Equal(t, lconfigs.Namespaces{
   113  			{Type: lconfigs.NEWNS},
   114  			{Type: lconfigs.NEWPID},
   115  			{Type: lconfigs.NEWIPC},
   116  		}, configureNamespaces("private", "private"))
   117  	})
   118  }
   119  
   120  func TestExecutor_Isolation_PID_and_IPC_hostMode(t *testing.T) {
   121  	t.Parallel()
   122  	r := require.New(t)
   123  	testutil.ExecCompatible(t)
   124  
   125  	testExecCmd := testExecutorCommandWithChroot(t)
   126  	execCmd, allocDir := testExecCmd.command, testExecCmd.allocDir
   127  	execCmd.Cmd = "/bin/ls"
   128  	execCmd.Args = []string{"-F", "/", "/etc/"}
   129  	defer allocDir.Destroy()
   130  
   131  	execCmd.ResourceLimits = true
   132  	execCmd.ModePID = "host" // disable PID namespace
   133  	execCmd.ModeIPC = "host" // disable IPC namespace
   134  
   135  	executor := NewExecutorWithIsolation(testlog.HCLogger(t))
   136  	defer executor.Shutdown("SIGKILL", 0)
   137  
   138  	ps, err := executor.Launch(execCmd)
   139  	r.NoError(err)
   140  	r.NotZero(ps.Pid)
   141  
   142  	estate, err := executor.Wait(context.Background())
   143  	r.NoError(err)
   144  	r.Zero(estate.ExitCode)
   145  
   146  	lexec, ok := executor.(*LibcontainerExecutor)
   147  	r.True(ok)
   148  
   149  	// Check that namespaces were applied to the container config
   150  	config := lexec.container.Config()
   151  
   152  	r.Contains(config.Namespaces, lconfigs.Namespace{Type: lconfigs.NEWNS})
   153  	r.NotContains(config.Namespaces, lconfigs.Namespace{Type: lconfigs.NEWPID})
   154  	r.NotContains(config.Namespaces, lconfigs.Namespace{Type: lconfigs.NEWIPC})
   155  
   156  	// Shut down executor
   157  	r.NoError(executor.Shutdown("", 0))
   158  	executor.Wait(context.Background())
   159  }
   160  
   161  func TestExecutor_IsolationAndConstraints(t *testing.T) {
   162  	t.Parallel()
   163  	r := require.New(t)
   164  	testutil.ExecCompatible(t)
   165  
   166  	testExecCmd := testExecutorCommandWithChroot(t)
   167  	execCmd, allocDir := testExecCmd.command, testExecCmd.allocDir
   168  	execCmd.Cmd = "/bin/ls"
   169  	execCmd.Args = []string{"-F", "/", "/etc/"}
   170  	defer allocDir.Destroy()
   171  
   172  	execCmd.ResourceLimits = true
   173  	execCmd.ModePID = "private"
   174  	execCmd.ModeIPC = "private"
   175  
   176  	executor := NewExecutorWithIsolation(testlog.HCLogger(t))
   177  	defer executor.Shutdown("SIGKILL", 0)
   178  
   179  	ps, err := executor.Launch(execCmd)
   180  	r.NoError(err)
   181  	r.NotZero(ps.Pid)
   182  
   183  	estate, err := executor.Wait(context.Background())
   184  	r.NoError(err)
   185  	r.Zero(estate.ExitCode)
   186  
   187  	lexec, ok := executor.(*LibcontainerExecutor)
   188  	r.True(ok)
   189  
   190  	// Check if the resource constraints were applied
   191  	state, err := lexec.container.State()
   192  	r.NoError(err)
   193  
   194  	memLimits := filepath.Join(state.CgroupPaths["memory"], "memory.limit_in_bytes")
   195  	data, err := ioutil.ReadFile(memLimits)
   196  	r.NoError(err)
   197  
   198  	expectedMemLim := strconv.Itoa(int(execCmd.Resources.NomadResources.Memory.MemoryMB * 1024 * 1024))
   199  	actualMemLim := strings.TrimSpace(string(data))
   200  	r.Equal(actualMemLim, expectedMemLim)
   201  
   202  	// Check that namespaces were applied to the container config
   203  	config := lexec.container.Config()
   204  
   205  	r.Contains(config.Namespaces, lconfigs.Namespace{Type: lconfigs.NEWNS})
   206  	r.Contains(config.Namespaces, lconfigs.Namespace{Type: lconfigs.NEWPID})
   207  	r.Contains(config.Namespaces, lconfigs.Namespace{Type: lconfigs.NEWIPC})
   208  
   209  	// Shut down executor
   210  	r.NoError(executor.Shutdown("", 0))
   211  	executor.Wait(context.Background())
   212  
   213  	// Check if Nomad has actually removed the cgroups
   214  	tu.WaitForResult(func() (bool, error) {
   215  		_, err = os.Stat(memLimits)
   216  		if err == nil {
   217  			return false, fmt.Errorf("expected an error from os.Stat %s", memLimits)
   218  		}
   219  		return true, nil
   220  	}, func(err error) { t.Error(err) })
   221  
   222  	expected := `/:
   223  alloc/
   224  bin/
   225  dev/
   226  etc/
   227  lib/
   228  lib64/
   229  local/
   230  proc/
   231  secrets/
   232  sys/
   233  tmp/
   234  usr/
   235  
   236  /etc/:
   237  ld.so.cache
   238  ld.so.conf
   239  ld.so.conf.d/
   240  passwd`
   241  	tu.WaitForResult(func() (bool, error) {
   242  		output := testExecCmd.stdout.String()
   243  		act := strings.TrimSpace(string(output))
   244  		if act != expected {
   245  			return false, fmt.Errorf("Command output incorrectly: want %v; got %v", expected, act)
   246  		}
   247  		return true, nil
   248  	}, func(err error) { t.Error(err) })
   249  }
   250  
   251  // TestExecutor_CgroupPaths asserts that process starts with independent cgroups
   252  // hierarchy created for this process
   253  func TestExecutor_CgroupPaths(t *testing.T) {
   254  	t.Parallel()
   255  	require := require.New(t)
   256  	testutil.ExecCompatible(t)
   257  
   258  	testExecCmd := testExecutorCommandWithChroot(t)
   259  	execCmd, allocDir := testExecCmd.command, testExecCmd.allocDir
   260  	execCmd.Cmd = "/bin/bash"
   261  	execCmd.Args = []string{"-c", "sleep 0.2; cat /proc/self/cgroup"}
   262  	defer allocDir.Destroy()
   263  
   264  	execCmd.ResourceLimits = true
   265  
   266  	executor := NewExecutorWithIsolation(testlog.HCLogger(t))
   267  	defer executor.Shutdown("SIGKILL", 0)
   268  
   269  	ps, err := executor.Launch(execCmd)
   270  	require.NoError(err)
   271  	require.NotZero(ps.Pid)
   272  
   273  	state, err := executor.Wait(context.Background())
   274  	require.NoError(err)
   275  	require.Zero(state.ExitCode)
   276  
   277  	tu.WaitForResult(func() (bool, error) {
   278  		output := strings.TrimSpace(testExecCmd.stdout.String())
   279  		// sanity check that we got some cgroups
   280  		if !strings.Contains(output, ":devices:") {
   281  			return false, fmt.Errorf("was expected cgroup files but found:\n%v", output)
   282  		}
   283  		lines := strings.Split(output, "\n")
   284  		for _, line := range lines {
   285  			// Every cgroup entry should be /nomad/$ALLOC_ID
   286  			if line == "" {
   287  				continue
   288  			}
   289  
   290  			// Skip rdma subsystem; rdma was added in most recent kernels and libcontainer/docker
   291  			// don't isolate it by default.
   292  			// :: filters out odd empty cgroup found in latest Ubuntu lines, e.g. 0::/user.slice/user-1000.slice/session-17.scope
   293  			// that is also not used for isolation
   294  			if strings.Contains(line, ":rdma:") || strings.Contains(line, "::") {
   295  				continue
   296  			}
   297  
   298  			if !strings.Contains(line, ":/nomad/") {
   299  				return false, fmt.Errorf("Not a member of the alloc's cgroup: expected=...:/nomad/... -- found=%q", line)
   300  			}
   301  		}
   302  		return true, nil
   303  	}, func(err error) { t.Error(err) })
   304  }
   305  
   306  // TestExecutor_CgroupPaths asserts that all cgroups created for a task
   307  // are destroyed on shutdown
   308  func TestExecutor_CgroupPathsAreDestroyed(t *testing.T) {
   309  	t.Parallel()
   310  	require := require.New(t)
   311  	testutil.ExecCompatible(t)
   312  
   313  	testExecCmd := testExecutorCommandWithChroot(t)
   314  	execCmd, allocDir := testExecCmd.command, testExecCmd.allocDir
   315  	execCmd.Cmd = "/bin/bash"
   316  	execCmd.Args = []string{"-c", "sleep 0.2; cat /proc/self/cgroup"}
   317  	defer allocDir.Destroy()
   318  
   319  	execCmd.ResourceLimits = true
   320  
   321  	executor := NewExecutorWithIsolation(testlog.HCLogger(t))
   322  	defer executor.Shutdown("SIGKILL", 0)
   323  
   324  	ps, err := executor.Launch(execCmd)
   325  	require.NoError(err)
   326  	require.NotZero(ps.Pid)
   327  
   328  	state, err := executor.Wait(context.Background())
   329  	require.NoError(err)
   330  	require.Zero(state.ExitCode)
   331  
   332  	var cgroupsPaths string
   333  	tu.WaitForResult(func() (bool, error) {
   334  		output := strings.TrimSpace(testExecCmd.stdout.String())
   335  		// sanity check that we got some cgroups
   336  		if !strings.Contains(output, ":devices:") {
   337  			return false, fmt.Errorf("was expected cgroup files but found:\n%v", output)
   338  		}
   339  		lines := strings.Split(output, "\n")
   340  		for _, line := range lines {
   341  			// Every cgroup entry should be /nomad/$ALLOC_ID
   342  			if line == "" {
   343  				continue
   344  			}
   345  
   346  			// Skip rdma subsystem; rdma was added in most recent kernels and libcontainer/docker
   347  			// don't isolate it by default.
   348  			if strings.Contains(line, ":rdma:") || strings.Contains(line, "::") {
   349  				continue
   350  			}
   351  
   352  			if !strings.Contains(line, ":/nomad/") {
   353  				return false, fmt.Errorf("Not a member of the alloc's cgroup: expected=...:/nomad/... -- found=%q", line)
   354  			}
   355  		}
   356  
   357  		cgroupsPaths = output
   358  		return true, nil
   359  	}, func(err error) { t.Error(err) })
   360  
   361  	// shutdown executor and test that cgroups are destroyed
   362  	executor.Shutdown("SIGKILL", 0)
   363  
   364  	// test that the cgroup paths are not visible
   365  	tmpFile, err := ioutil.TempFile("", "")
   366  	require.NoError(err)
   367  	defer os.Remove(tmpFile.Name())
   368  
   369  	_, err = tmpFile.WriteString(cgroupsPaths)
   370  	require.NoError(err)
   371  	tmpFile.Close()
   372  
   373  	subsystems, err := cgroups.ParseCgroupFile(tmpFile.Name())
   374  	require.NoError(err)
   375  
   376  	for subsystem, cgroup := range subsystems {
   377  		if !strings.Contains(cgroup, "nomad/") {
   378  			// this should only be rdma at this point
   379  			continue
   380  		}
   381  
   382  		p, err := getCgroupPathHelper(subsystem, cgroup)
   383  		require.NoError(err)
   384  		require.Falsef(cgroups.PathExists(p), "cgroup for %s %s still exists", subsystem, cgroup)
   385  	}
   386  }
   387  
   388  func TestUniversalExecutor_LookupTaskBin(t *testing.T) {
   389  	t.Parallel()
   390  	require := require.New(t)
   391  
   392  	// Create a temp dir
   393  	tmpDir, err := ioutil.TempDir("", "")
   394  	require.Nil(err)
   395  	defer os.Remove(tmpDir)
   396  
   397  	// Create the command
   398  	cmd := &ExecCommand{Env: []string{"PATH=/bin"}, TaskDir: tmpDir}
   399  
   400  	// Make a foo subdir
   401  	os.MkdirAll(filepath.Join(tmpDir, "foo"), 0700)
   402  
   403  	// Write a file under foo
   404  	filePath := filepath.Join(tmpDir, "foo", "tmp.txt")
   405  	err = ioutil.WriteFile(filePath, []byte{1, 2}, os.ModeAppend)
   406  	require.NoError(err)
   407  
   408  	// Lookout with an absolute path to the binary
   409  	cmd.Cmd = "/foo/tmp.txt"
   410  	_, err = lookupTaskBin(cmd)
   411  	require.NoError(err)
   412  
   413  	// Write a file under local subdir
   414  	os.MkdirAll(filepath.Join(tmpDir, "local"), 0700)
   415  	filePath2 := filepath.Join(tmpDir, "local", "tmp.txt")
   416  	ioutil.WriteFile(filePath2, []byte{1, 2}, os.ModeAppend)
   417  
   418  	// Lookup with file name, should find the one we wrote above
   419  	cmd.Cmd = "tmp.txt"
   420  	_, err = lookupTaskBin(cmd)
   421  	require.NoError(err)
   422  
   423  	// Lookup a host absolute path
   424  	cmd.Cmd = "/bin/sh"
   425  	_, err = lookupTaskBin(cmd)
   426  	require.Error(err)
   427  }
   428  
   429  // Exec Launch looks for the binary only inside the chroot
   430  func TestExecutor_EscapeContainer(t *testing.T) {
   431  	t.Parallel()
   432  	require := require.New(t)
   433  	testutil.ExecCompatible(t)
   434  
   435  	testExecCmd := testExecutorCommandWithChroot(t)
   436  	execCmd, allocDir := testExecCmd.command, testExecCmd.allocDir
   437  	execCmd.Cmd = "/bin/kill" // missing from the chroot container
   438  	defer allocDir.Destroy()
   439  
   440  	execCmd.ResourceLimits = true
   441  
   442  	executor := NewExecutorWithIsolation(testlog.HCLogger(t))
   443  	defer executor.Shutdown("SIGKILL", 0)
   444  
   445  	_, err := executor.Launch(execCmd)
   446  	require.Error(err)
   447  	require.Regexp("^file /bin/kill not found under path", err)
   448  
   449  	// Bare files are looked up using the system path, inside the container
   450  	allocDir.Destroy()
   451  	testExecCmd = testExecutorCommandWithChroot(t)
   452  	execCmd, allocDir = testExecCmd.command, testExecCmd.allocDir
   453  	execCmd.Cmd = "kill"
   454  	_, err = executor.Launch(execCmd)
   455  	require.Error(err)
   456  	require.Regexp("^file kill not found under path", err)
   457  
   458  	allocDir.Destroy()
   459  	testExecCmd = testExecutorCommandWithChroot(t)
   460  	execCmd, allocDir = testExecCmd.command, testExecCmd.allocDir
   461  	execCmd.Cmd = "echo"
   462  	_, err = executor.Launch(execCmd)
   463  	require.NoError(err)
   464  }
   465  
   466  func TestExecutor_Capabilities(t *testing.T) {
   467  	t.Parallel()
   468  	testutil.ExecCompatible(t)
   469  
   470  	cases := []struct {
   471  		user string
   472  		caps string
   473  	}{
   474  		{
   475  			user: "nobody",
   476  			caps: `
   477  CapInh: 0000000000000000
   478  CapPrm: 0000000000000000
   479  CapEff: 0000000000000000
   480  CapBnd: 0000003fffffffff
   481  CapAmb: 0000000000000000`,
   482  		},
   483  		{
   484  			user: "root",
   485  			caps: `
   486  CapInh: 0000000000000000
   487  CapPrm: 0000003fffffffff
   488  CapEff: 0000003fffffffff
   489  CapBnd: 0000003fffffffff
   490  CapAmb: 0000000000000000`,
   491  		},
   492  	}
   493  
   494  	for _, c := range cases {
   495  		t.Run(c.user, func(t *testing.T) {
   496  			require := require.New(t)
   497  
   498  			testExecCmd := testExecutorCommandWithChroot(t)
   499  			execCmd, allocDir := testExecCmd.command, testExecCmd.allocDir
   500  			defer allocDir.Destroy()
   501  
   502  			execCmd.User = c.user
   503  			execCmd.ResourceLimits = true
   504  			execCmd.Cmd = "/bin/bash"
   505  			execCmd.Args = []string{"-c", "cat /proc/$$/status"}
   506  
   507  			executor := NewExecutorWithIsolation(testlog.HCLogger(t))
   508  			defer executor.Shutdown("SIGKILL", 0)
   509  
   510  			_, err := executor.Launch(execCmd)
   511  			require.NoError(err)
   512  
   513  			ch := make(chan interface{})
   514  			go func() {
   515  				executor.Wait(context.Background())
   516  				close(ch)
   517  			}()
   518  
   519  			select {
   520  			case <-ch:
   521  				// all good
   522  			case <-time.After(5 * time.Second):
   523  				require.Fail("timeout waiting for exec to shutdown")
   524  			}
   525  
   526  			canonical := func(s string) string {
   527  				s = strings.TrimSpace(s)
   528  				s = regexp.MustCompile("[ \t]+").ReplaceAllString(s, " ")
   529  				s = regexp.MustCompile("[\n\r]+").ReplaceAllString(s, "\n")
   530  				return s
   531  			}
   532  
   533  			expected := canonical(c.caps)
   534  			tu.WaitForResult(func() (bool, error) {
   535  				output := canonical(testExecCmd.stdout.String())
   536  				if !strings.Contains(output, expected) {
   537  					return false, fmt.Errorf("capabilities didn't match: want\n%v\n; got:\n%v\n", expected, output)
   538  				}
   539  				return true, nil
   540  			}, func(err error) { require.NoError(err) })
   541  		})
   542  	}
   543  
   544  }
   545  
   546  func TestExecutor_ClientCleanup(t *testing.T) {
   547  	t.Parallel()
   548  	testutil.ExecCompatible(t)
   549  	require := require.New(t)
   550  
   551  	testExecCmd := testExecutorCommandWithChroot(t)
   552  	execCmd, allocDir := testExecCmd.command, testExecCmd.allocDir
   553  	defer allocDir.Destroy()
   554  
   555  	executor := NewExecutorWithIsolation(testlog.HCLogger(t))
   556  	defer executor.Shutdown("", 0)
   557  
   558  	// Need to run a command which will produce continuous output but not
   559  	// too quickly to ensure executor.Exit() stops the process.
   560  	execCmd.Cmd = "/bin/bash"
   561  	execCmd.Args = []string{"-c", "while true; do /bin/echo X; /bin/sleep 1; done"}
   562  	execCmd.ResourceLimits = true
   563  
   564  	ps, err := executor.Launch(execCmd)
   565  
   566  	require.NoError(err)
   567  	require.NotZero(ps.Pid)
   568  	time.Sleep(500 * time.Millisecond)
   569  	require.NoError(executor.Shutdown("SIGINT", 100*time.Millisecond))
   570  
   571  	ch := make(chan interface{})
   572  	go func() {
   573  		executor.Wait(context.Background())
   574  		close(ch)
   575  	}()
   576  
   577  	select {
   578  	case <-ch:
   579  		// all good
   580  	case <-time.After(5 * time.Second):
   581  		require.Fail("timeout waiting for exec to shutdown")
   582  	}
   583  
   584  	output := testExecCmd.stdout.String()
   585  	require.NotZero(len(output))
   586  	time.Sleep(2 * time.Second)
   587  	output1 := testExecCmd.stdout.String()
   588  	require.Equal(len(output), len(output1))
   589  }
   590  
   591  func TestExecutor_cmdDevices(t *testing.T) {
   592  	input := []*drivers.DeviceConfig{
   593  		{
   594  			HostPath:    "/dev/null",
   595  			TaskPath:    "/task/dev/null",
   596  			Permissions: "rwm",
   597  		},
   598  	}
   599  
   600  	expected := &lconfigs.Device{
   601  		DeviceRule: lconfigs.DeviceRule{
   602  			Type:        99,
   603  			Major:       1,
   604  			Minor:       3,
   605  			Permissions: "rwm",
   606  		},
   607  		Path: "/task/dev/null",
   608  	}
   609  
   610  	found, err := cmdDevices(input)
   611  	require.NoError(t, err)
   612  	require.Len(t, found, 1)
   613  
   614  	// ignore file permission and ownership
   615  	// as they are host specific potentially
   616  	d := found[0]
   617  	d.FileMode = 0
   618  	d.Uid = 0
   619  	d.Gid = 0
   620  
   621  	require.EqualValues(t, expected, d)
   622  }
   623  
   624  func TestExecutor_cmdMounts(t *testing.T) {
   625  	input := []*drivers.MountConfig{
   626  		{
   627  			HostPath: "/host/path-ro",
   628  			TaskPath: "/task/path-ro",
   629  			Readonly: true,
   630  		},
   631  		{
   632  			HostPath: "/host/path-rw",
   633  			TaskPath: "/task/path-rw",
   634  			Readonly: false,
   635  		},
   636  	}
   637  
   638  	expected := []*lconfigs.Mount{
   639  		{
   640  			Source:           "/host/path-ro",
   641  			Destination:      "/task/path-ro",
   642  			Flags:            unix.MS_BIND | unix.MS_RDONLY,
   643  			Device:           "bind",
   644  			PropagationFlags: []int{unix.MS_PRIVATE | unix.MS_REC},
   645  		},
   646  		{
   647  			Source:           "/host/path-rw",
   648  			Destination:      "/task/path-rw",
   649  			Flags:            unix.MS_BIND,
   650  			Device:           "bind",
   651  			PropagationFlags: []int{unix.MS_PRIVATE | unix.MS_REC},
   652  		},
   653  	}
   654  
   655  	require.EqualValues(t, expected, cmdMounts(input))
   656  }
   657  
   658  // TestUniversalExecutor_NoCgroup asserts that commands are executed in the
   659  // same cgroup as parent process
   660  func TestUniversalExecutor_NoCgroup(t *testing.T) {
   661  	t.Parallel()
   662  	testutil.ExecCompatible(t)
   663  
   664  	expectedBytes, err := ioutil.ReadFile("/proc/self/cgroup")
   665  	require.NoError(t, err)
   666  
   667  	expected := strings.TrimSpace(string(expectedBytes))
   668  
   669  	testExecCmd := testExecutorCommand(t)
   670  	execCmd, allocDir := testExecCmd.command, testExecCmd.allocDir
   671  	execCmd.Cmd = "/bin/cat"
   672  	execCmd.Args = []string{"/proc/self/cgroup"}
   673  	defer allocDir.Destroy()
   674  
   675  	execCmd.BasicProcessCgroup = false
   676  	execCmd.ResourceLimits = false
   677  
   678  	executor := NewExecutor(testlog.HCLogger(t))
   679  	defer executor.Shutdown("SIGKILL", 0)
   680  
   681  	_, err = executor.Launch(execCmd)
   682  	require.NoError(t, err)
   683  
   684  	_, err = executor.Wait(context.Background())
   685  	require.NoError(t, err)
   686  
   687  	tu.WaitForResult(func() (bool, error) {
   688  		act := strings.TrimSpace(string(testExecCmd.stdout.String()))
   689  		if expected != act {
   690  			return false, fmt.Errorf("expected:\n%s actual:\n%s", expected, act)
   691  		}
   692  		return true, nil
   693  	}, func(err error) {
   694  		stderr := strings.TrimSpace(string(testExecCmd.stderr.String()))
   695  		t.Logf("stderr: %v", stderr)
   696  		require.NoError(t, err)
   697  	})
   698  
   699  }