github.com/iqoqo/nomad@v0.11.3-0.20200911112621-d7021c74d101/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_IsolationAndConstraints(t *testing.T) {
    91  	t.Parallel()
    92  	require := require.New(t)
    93  	testutil.ExecCompatible(t)
    94  
    95  	testExecCmd := testExecutorCommandWithChroot(t)
    96  	execCmd, allocDir := testExecCmd.command, testExecCmd.allocDir
    97  	execCmd.Cmd = "/bin/ls"
    98  	execCmd.Args = []string{"-F", "/", "/etc/"}
    99  	defer allocDir.Destroy()
   100  
   101  	execCmd.ResourceLimits = true
   102  
   103  	executor := NewExecutorWithIsolation(testlog.HCLogger(t))
   104  	defer executor.Shutdown("SIGKILL", 0)
   105  
   106  	ps, err := executor.Launch(execCmd)
   107  	require.NoError(err)
   108  	require.NotZero(ps.Pid)
   109  
   110  	state, err := executor.Wait(context.Background())
   111  	require.NoError(err)
   112  	require.Zero(state.ExitCode)
   113  
   114  	// Check if the resource constraints were applied
   115  	if lexec, ok := executor.(*LibcontainerExecutor); ok {
   116  		state, err := lexec.container.State()
   117  		require.NoError(err)
   118  
   119  		memLimits := filepath.Join(state.CgroupPaths["memory"], "memory.limit_in_bytes")
   120  		data, err := ioutil.ReadFile(memLimits)
   121  		require.NoError(err)
   122  
   123  		expectedMemLim := strconv.Itoa(int(execCmd.Resources.NomadResources.Memory.MemoryMB * 1024 * 1024))
   124  		actualMemLim := strings.TrimSpace(string(data))
   125  		require.Equal(actualMemLim, expectedMemLim)
   126  		require.NoError(executor.Shutdown("", 0))
   127  		executor.Wait(context.Background())
   128  
   129  		// Check if Nomad has actually removed the cgroups
   130  		tu.WaitForResult(func() (bool, error) {
   131  			_, err = os.Stat(memLimits)
   132  			if err == nil {
   133  				return false, fmt.Errorf("expected an error from os.Stat %s", memLimits)
   134  			}
   135  			return true, nil
   136  		}, func(err error) { t.Error(err) })
   137  
   138  	}
   139  	expected := `/:
   140  alloc/
   141  bin/
   142  dev/
   143  etc/
   144  lib/
   145  lib64/
   146  local/
   147  proc/
   148  secrets/
   149  sys/
   150  tmp/
   151  usr/
   152  
   153  /etc/:
   154  ld.so.cache
   155  ld.so.conf
   156  ld.so.conf.d/
   157  passwd`
   158  	tu.WaitForResult(func() (bool, error) {
   159  		output := testExecCmd.stdout.String()
   160  		act := strings.TrimSpace(string(output))
   161  		if act != expected {
   162  			return false, fmt.Errorf("Command output incorrectly: want %v; got %v", expected, act)
   163  		}
   164  		return true, nil
   165  	}, func(err error) { t.Error(err) })
   166  }
   167  
   168  // TestExecutor_CgroupPaths asserts that process starts with independent cgroups
   169  // hierarchy created for this process
   170  func TestExecutor_CgroupPaths(t *testing.T) {
   171  	t.Parallel()
   172  	require := require.New(t)
   173  	testutil.ExecCompatible(t)
   174  
   175  	testExecCmd := testExecutorCommandWithChroot(t)
   176  	execCmd, allocDir := testExecCmd.command, testExecCmd.allocDir
   177  	execCmd.Cmd = "/bin/bash"
   178  	execCmd.Args = []string{"-c", "sleep 0.2; cat /proc/self/cgroup"}
   179  	defer allocDir.Destroy()
   180  
   181  	execCmd.ResourceLimits = true
   182  
   183  	executor := NewExecutorWithIsolation(testlog.HCLogger(t))
   184  	defer executor.Shutdown("SIGKILL", 0)
   185  
   186  	ps, err := executor.Launch(execCmd)
   187  	require.NoError(err)
   188  	require.NotZero(ps.Pid)
   189  
   190  	state, err := executor.Wait(context.Background())
   191  	require.NoError(err)
   192  	require.Zero(state.ExitCode)
   193  
   194  	tu.WaitForResult(func() (bool, error) {
   195  		output := strings.TrimSpace(testExecCmd.stdout.String())
   196  		// sanity check that we got some cgroups
   197  		if !strings.Contains(output, ":devices:") {
   198  			return false, fmt.Errorf("was expected cgroup files but found:\n%v", output)
   199  		}
   200  		lines := strings.Split(output, "\n")
   201  		for _, line := range lines {
   202  			// Every cgroup entry should be /nomad/$ALLOC_ID
   203  			if line == "" {
   204  				continue
   205  			}
   206  
   207  			// Skip rdma subsystem; rdma was added in most recent kernels and libcontainer/docker
   208  			// don't isolate it by default.
   209  			if strings.Contains(line, ":rdma:") {
   210  				continue
   211  			}
   212  
   213  			if !strings.Contains(line, ":/nomad/") {
   214  				return false, fmt.Errorf("Not a member of the alloc's cgroup: expected=...:/nomad/... -- found=%q", line)
   215  			}
   216  		}
   217  		return true, nil
   218  	}, func(err error) { t.Error(err) })
   219  }
   220  
   221  // TestExecutor_CgroupPaths asserts that all cgroups created for a task
   222  // are destroyed on shutdown
   223  func TestExecutor_CgroupPathsAreDestroyed(t *testing.T) {
   224  	t.Parallel()
   225  	require := require.New(t)
   226  	testutil.ExecCompatible(t)
   227  
   228  	testExecCmd := testExecutorCommandWithChroot(t)
   229  	execCmd, allocDir := testExecCmd.command, testExecCmd.allocDir
   230  	execCmd.Cmd = "/bin/bash"
   231  	execCmd.Args = []string{"-c", "sleep 0.2; cat /proc/self/cgroup"}
   232  	defer allocDir.Destroy()
   233  
   234  	execCmd.ResourceLimits = true
   235  
   236  	executor := NewExecutorWithIsolation(testlog.HCLogger(t))
   237  	defer executor.Shutdown("SIGKILL", 0)
   238  
   239  	ps, err := executor.Launch(execCmd)
   240  	require.NoError(err)
   241  	require.NotZero(ps.Pid)
   242  
   243  	state, err := executor.Wait(context.Background())
   244  	require.NoError(err)
   245  	require.Zero(state.ExitCode)
   246  
   247  	var cgroupsPaths string
   248  	tu.WaitForResult(func() (bool, error) {
   249  		output := strings.TrimSpace(testExecCmd.stdout.String())
   250  		// sanity check that we got some cgroups
   251  		if !strings.Contains(output, ":devices:") {
   252  			return false, fmt.Errorf("was expected cgroup files but found:\n%v", output)
   253  		}
   254  		lines := strings.Split(output, "\n")
   255  		for _, line := range lines {
   256  			// Every cgroup entry should be /nomad/$ALLOC_ID
   257  			if line == "" {
   258  				continue
   259  			}
   260  
   261  			// Skip rdma subsystem; rdma was added in most recent kernels and libcontainer/docker
   262  			// don't isolate it by default.
   263  			if strings.Contains(line, ":rdma:") {
   264  				continue
   265  			}
   266  
   267  			if !strings.Contains(line, ":/nomad/") {
   268  				return false, fmt.Errorf("Not a member of the alloc's cgroup: expected=...:/nomad/... -- found=%q", line)
   269  			}
   270  		}
   271  
   272  		cgroupsPaths = output
   273  		return true, nil
   274  	}, func(err error) { t.Error(err) })
   275  
   276  	// shutdown executor and test that cgroups are destroyed
   277  	executor.Shutdown("SIGKILL", 0)
   278  
   279  	// test that the cgroup paths are not visible
   280  	tmpFile, err := ioutil.TempFile("", "")
   281  	require.NoError(err)
   282  	defer os.Remove(tmpFile.Name())
   283  
   284  	_, err = tmpFile.WriteString(cgroupsPaths)
   285  	require.NoError(err)
   286  	tmpFile.Close()
   287  
   288  	subsystems, err := cgroups.ParseCgroupFile(tmpFile.Name())
   289  	require.NoError(err)
   290  
   291  	for subsystem, cgroup := range subsystems {
   292  		if !strings.Contains(cgroup, "nomad/") {
   293  			// this should only be rdma at this point
   294  			continue
   295  		}
   296  
   297  		p, err := getCgroupPathHelper(subsystem, cgroup)
   298  		require.NoError(err)
   299  		require.Falsef(cgroups.PathExists(p), "cgroup for %s %s still exists", subsystem, cgroup)
   300  	}
   301  }
   302  
   303  func TestUniversalExecutor_LookupTaskBin(t *testing.T) {
   304  	t.Parallel()
   305  	require := require.New(t)
   306  
   307  	// Create a temp dir
   308  	tmpDir, err := ioutil.TempDir("", "")
   309  	require.Nil(err)
   310  	defer os.Remove(tmpDir)
   311  
   312  	// Create the command
   313  	cmd := &ExecCommand{Env: []string{"PATH=/bin"}, TaskDir: tmpDir}
   314  
   315  	// Make a foo subdir
   316  	os.MkdirAll(filepath.Join(tmpDir, "foo"), 0700)
   317  
   318  	// Write a file under foo
   319  	filePath := filepath.Join(tmpDir, "foo", "tmp.txt")
   320  	err = ioutil.WriteFile(filePath, []byte{1, 2}, os.ModeAppend)
   321  	require.NoError(err)
   322  
   323  	// Lookout with an absolute path to the binary
   324  	cmd.Cmd = "/foo/tmp.txt"
   325  	_, err = lookupTaskBin(cmd)
   326  	require.NoError(err)
   327  
   328  	// Write a file under local subdir
   329  	os.MkdirAll(filepath.Join(tmpDir, "local"), 0700)
   330  	filePath2 := filepath.Join(tmpDir, "local", "tmp.txt")
   331  	ioutil.WriteFile(filePath2, []byte{1, 2}, os.ModeAppend)
   332  
   333  	// Lookup with file name, should find the one we wrote above
   334  	cmd.Cmd = "tmp.txt"
   335  	_, err = lookupTaskBin(cmd)
   336  	require.NoError(err)
   337  
   338  	// Lookup a host absolute path
   339  	cmd.Cmd = "/bin/sh"
   340  	_, err = lookupTaskBin(cmd)
   341  	require.Error(err)
   342  }
   343  
   344  // Exec Launch looks for the binary only inside the chroot
   345  func TestExecutor_EscapeContainer(t *testing.T) {
   346  	t.Parallel()
   347  	require := require.New(t)
   348  	testutil.ExecCompatible(t)
   349  
   350  	testExecCmd := testExecutorCommandWithChroot(t)
   351  	execCmd, allocDir := testExecCmd.command, testExecCmd.allocDir
   352  	execCmd.Cmd = "/bin/kill" // missing from the chroot container
   353  	defer allocDir.Destroy()
   354  
   355  	execCmd.ResourceLimits = true
   356  
   357  	executor := NewExecutorWithIsolation(testlog.HCLogger(t))
   358  	defer executor.Shutdown("SIGKILL", 0)
   359  
   360  	_, err := executor.Launch(execCmd)
   361  	require.Error(err)
   362  	require.Regexp("^file /bin/kill not found under path", err)
   363  
   364  	// Bare files are looked up using the system path, inside the container
   365  	allocDir.Destroy()
   366  	testExecCmd = testExecutorCommandWithChroot(t)
   367  	execCmd, allocDir = testExecCmd.command, testExecCmd.allocDir
   368  	execCmd.Cmd = "kill"
   369  	_, err = executor.Launch(execCmd)
   370  	require.Error(err)
   371  	require.Regexp("^file kill not found under path", err)
   372  
   373  	allocDir.Destroy()
   374  	testExecCmd = testExecutorCommandWithChroot(t)
   375  	execCmd, allocDir = testExecCmd.command, testExecCmd.allocDir
   376  	execCmd.Cmd = "echo"
   377  	_, err = executor.Launch(execCmd)
   378  	require.NoError(err)
   379  }
   380  
   381  func TestExecutor_Capabilities(t *testing.T) {
   382  	t.Parallel()
   383  	testutil.ExecCompatible(t)
   384  
   385  	cases := []struct {
   386  		user string
   387  		caps string
   388  	}{
   389  		{
   390  			user: "nobody",
   391  			caps: `
   392  CapInh: 0000000000000000
   393  CapPrm: 0000000000000000
   394  CapEff: 0000000000000000
   395  CapBnd: 0000003fffffffff
   396  CapAmb: 0000000000000000`,
   397  		},
   398  		{
   399  			user: "root",
   400  			caps: `
   401  CapInh: 0000000000000000
   402  CapPrm: 0000003fffffffff
   403  CapEff: 0000003fffffffff
   404  CapBnd: 0000003fffffffff
   405  CapAmb: 0000000000000000`,
   406  		},
   407  	}
   408  
   409  	for _, c := range cases {
   410  		t.Run(c.user, func(t *testing.T) {
   411  			require := require.New(t)
   412  
   413  			testExecCmd := testExecutorCommandWithChroot(t)
   414  			execCmd, allocDir := testExecCmd.command, testExecCmd.allocDir
   415  			defer allocDir.Destroy()
   416  
   417  			execCmd.User = c.user
   418  			execCmd.ResourceLimits = true
   419  			execCmd.Cmd = "/bin/bash"
   420  			execCmd.Args = []string{"-c", "cat /proc/$$/status"}
   421  
   422  			executor := NewExecutorWithIsolation(testlog.HCLogger(t))
   423  			defer executor.Shutdown("SIGKILL", 0)
   424  
   425  			_, err := executor.Launch(execCmd)
   426  			require.NoError(err)
   427  
   428  			ch := make(chan interface{})
   429  			go func() {
   430  				executor.Wait(context.Background())
   431  				close(ch)
   432  			}()
   433  
   434  			select {
   435  			case <-ch:
   436  				// all good
   437  			case <-time.After(5 * time.Second):
   438  				require.Fail("timeout waiting for exec to shutdown")
   439  			}
   440  
   441  			canonical := func(s string) string {
   442  				s = strings.TrimSpace(s)
   443  				s = regexp.MustCompile("[ \t]+").ReplaceAllString(s, " ")
   444  				s = regexp.MustCompile("[\n\r]+").ReplaceAllString(s, "\n")
   445  				return s
   446  			}
   447  
   448  			expected := canonical(c.caps)
   449  			tu.WaitForResult(func() (bool, error) {
   450  				output := canonical(testExecCmd.stdout.String())
   451  				if !strings.Contains(output, expected) {
   452  					return false, fmt.Errorf("capabilities didn't match: want\n%v\n; got:\n%v\n", expected, output)
   453  				}
   454  				return true, nil
   455  			}, func(err error) { require.NoError(err) })
   456  		})
   457  	}
   458  
   459  }
   460  
   461  func TestExecutor_ClientCleanup(t *testing.T) {
   462  	t.Parallel()
   463  	testutil.ExecCompatible(t)
   464  	require := require.New(t)
   465  
   466  	testExecCmd := testExecutorCommandWithChroot(t)
   467  	execCmd, allocDir := testExecCmd.command, testExecCmd.allocDir
   468  	defer allocDir.Destroy()
   469  
   470  	executor := NewExecutorWithIsolation(testlog.HCLogger(t))
   471  	defer executor.Shutdown("", 0)
   472  
   473  	// Need to run a command which will produce continuous output but not
   474  	// too quickly to ensure executor.Exit() stops the process.
   475  	execCmd.Cmd = "/bin/bash"
   476  	execCmd.Args = []string{"-c", "while true; do /bin/echo X; /bin/sleep 1; done"}
   477  	execCmd.ResourceLimits = true
   478  
   479  	ps, err := executor.Launch(execCmd)
   480  
   481  	require.NoError(err)
   482  	require.NotZero(ps.Pid)
   483  	time.Sleep(500 * time.Millisecond)
   484  	require.NoError(executor.Shutdown("SIGINT", 100*time.Millisecond))
   485  
   486  	ch := make(chan interface{})
   487  	go func() {
   488  		executor.Wait(context.Background())
   489  		close(ch)
   490  	}()
   491  
   492  	select {
   493  	case <-ch:
   494  		// all good
   495  	case <-time.After(5 * time.Second):
   496  		require.Fail("timeout waiting for exec to shutdown")
   497  	}
   498  
   499  	output := testExecCmd.stdout.String()
   500  	require.NotZero(len(output))
   501  	time.Sleep(2 * time.Second)
   502  	output1 := testExecCmd.stdout.String()
   503  	require.Equal(len(output), len(output1))
   504  }
   505  
   506  func TestExecutor_cmdDevices(t *testing.T) {
   507  	input := []*drivers.DeviceConfig{
   508  		{
   509  			HostPath:    "/dev/null",
   510  			TaskPath:    "/task/dev/null",
   511  			Permissions: "rwm",
   512  		},
   513  	}
   514  
   515  	expected := &lconfigs.Device{
   516  		Path:        "/task/dev/null",
   517  		Type:        99,
   518  		Major:       1,
   519  		Minor:       3,
   520  		Permissions: "rwm",
   521  	}
   522  
   523  	found, err := cmdDevices(input)
   524  	require.NoError(t, err)
   525  	require.Len(t, found, 1)
   526  
   527  	// ignore file permission and ownership
   528  	// as they are host specific potentially
   529  	d := found[0]
   530  	d.FileMode = 0
   531  	d.Uid = 0
   532  	d.Gid = 0
   533  
   534  	require.EqualValues(t, expected, d)
   535  }
   536  
   537  func TestExecutor_cmdMounts(t *testing.T) {
   538  	input := []*drivers.MountConfig{
   539  		{
   540  			HostPath: "/host/path-ro",
   541  			TaskPath: "/task/path-ro",
   542  			Readonly: true,
   543  		},
   544  		{
   545  			HostPath: "/host/path-rw",
   546  			TaskPath: "/task/path-rw",
   547  			Readonly: false,
   548  		},
   549  	}
   550  
   551  	expected := []*lconfigs.Mount{
   552  		{
   553  			Source:           "/host/path-ro",
   554  			Destination:      "/task/path-ro",
   555  			Flags:            unix.MS_BIND | unix.MS_RDONLY,
   556  			Device:           "bind",
   557  			PropagationFlags: []int{unix.MS_PRIVATE | unix.MS_REC},
   558  		},
   559  		{
   560  			Source:           "/host/path-rw",
   561  			Destination:      "/task/path-rw",
   562  			Flags:            unix.MS_BIND,
   563  			Device:           "bind",
   564  			PropagationFlags: []int{unix.MS_PRIVATE | unix.MS_REC},
   565  		},
   566  	}
   567  
   568  	require.EqualValues(t, expected, cmdMounts(input))
   569  }