github.com/anth0d/nomad@v0.0.0-20221214183521-ae3a0a2cad06/client/allocdir/alloc_dir_test.go (about)

     1  package allocdir
     2  
     3  import (
     4  	"archive/tar"
     5  	"bytes"
     6  	"context"
     7  	"io"
     8  	"io/fs"
     9  	"io/ioutil"
    10  	"os"
    11  	"path/filepath"
    12  	"runtime"
    13  	"strings"
    14  	"syscall"
    15  	"testing"
    16  
    17  	"github.com/hashicorp/nomad/ci"
    18  	"github.com/hashicorp/nomad/helper/testlog"
    19  	"github.com/hashicorp/nomad/nomad/structs"
    20  	"github.com/stretchr/testify/require"
    21  )
    22  
    23  var (
    24  	t1 = &structs.Task{
    25  		Name:   "web",
    26  		Driver: "exec",
    27  		Config: map[string]interface{}{
    28  			"command": "/bin/date",
    29  			"args":    "+%s",
    30  		},
    31  		Resources: &structs.Resources{
    32  			DiskMB: 1,
    33  		},
    34  	}
    35  
    36  	t2 = &structs.Task{
    37  		Name:   "web2",
    38  		Driver: "exec",
    39  		Config: map[string]interface{}{
    40  			"command": "/bin/date",
    41  			"args":    "+%s",
    42  		},
    43  		Resources: &structs.Resources{
    44  			DiskMB: 1,
    45  		},
    46  	}
    47  )
    48  
    49  // Test that AllocDir.Build builds just the alloc directory.
    50  func TestAllocDir_BuildAlloc(t *testing.T) {
    51  	ci.Parallel(t)
    52  
    53  	tmp := t.TempDir()
    54  
    55  	d := NewAllocDir(testlog.HCLogger(t), tmp, "test")
    56  	defer d.Destroy()
    57  	d.NewTaskDir(t1.Name)
    58  	d.NewTaskDir(t2.Name)
    59  	if err := d.Build(); err != nil {
    60  		t.Fatalf("Build() failed: %v", err)
    61  	}
    62  
    63  	// Check that the AllocDir and each of the task directories exist.
    64  	if _, err := os.Stat(d.AllocDir); os.IsNotExist(err) {
    65  		t.Fatalf("Build() didn't create AllocDir %v", d.AllocDir)
    66  	}
    67  
    68  	for _, task := range []*structs.Task{t1, t2} {
    69  		tDir, ok := d.TaskDirs[task.Name]
    70  		if !ok {
    71  			t.Fatalf("Task directory not found for %v", task.Name)
    72  		}
    73  
    74  		if stat, _ := os.Stat(tDir.Dir); stat != nil {
    75  			t.Fatalf("Build() created TaskDir %v", tDir.Dir)
    76  		}
    77  
    78  		if stat, _ := os.Stat(tDir.SecretsDir); stat != nil {
    79  			t.Fatalf("Build() created secret dir %v", tDir.Dir)
    80  		}
    81  	}
    82  }
    83  
    84  // HACK: This function is copy/pasted from client.testutil to prevent a test
    85  //
    86  //	import cycle, due to testutil transitively importing allocdir. This
    87  //	should be fixed after DriverManager is implemented.
    88  func MountCompatible(t *testing.T) {
    89  	if runtime.GOOS == "windows" {
    90  		t.Skip("Windows does not support mount")
    91  	}
    92  
    93  	if syscall.Geteuid() != 0 {
    94  		t.Skip("Must be root to run test")
    95  	}
    96  }
    97  
    98  func TestAllocDir_MountSharedAlloc(t *testing.T) {
    99  	ci.Parallel(t)
   100  	MountCompatible(t)
   101  
   102  	tmp := t.TempDir()
   103  
   104  	d := NewAllocDir(testlog.HCLogger(t), tmp, "test")
   105  	defer d.Destroy()
   106  	if err := d.Build(); err != nil {
   107  		t.Fatalf("Build() failed: %v", err)
   108  	}
   109  
   110  	// Build 2 task dirs
   111  	td1 := d.NewTaskDir(t1.Name)
   112  	if err := td1.Build(true, nil); err != nil {
   113  		t.Fatalf("error build task=%q dir: %v", t1.Name, err)
   114  	}
   115  	td2 := d.NewTaskDir(t2.Name)
   116  	if err := td2.Build(true, nil); err != nil {
   117  		t.Fatalf("error build task=%q dir: %v", t2.Name, err)
   118  	}
   119  
   120  	// Write a file to the shared dir.
   121  	contents := []byte("foo")
   122  	const filename = "bar"
   123  	if err := ioutil.WriteFile(filepath.Join(d.SharedDir, filename), contents, 0666); err != nil {
   124  		t.Fatalf("Couldn't write file to shared directory: %v", err)
   125  	}
   126  
   127  	// Check that the file exists in the task directories
   128  	for _, td := range []*TaskDir{td1, td2} {
   129  		taskFile := filepath.Join(td.SharedTaskDir, filename)
   130  		act, err := ioutil.ReadFile(taskFile)
   131  		if err != nil {
   132  			t.Errorf("Failed to read shared alloc file from task dir: %v", err)
   133  			continue
   134  		}
   135  
   136  		if !bytes.Equal(act, contents) {
   137  			t.Errorf("Incorrect data read from task dir: want %v; got %v", contents, act)
   138  		}
   139  	}
   140  }
   141  
   142  func TestAllocDir_Snapshot(t *testing.T) {
   143  	ci.Parallel(t)
   144  
   145  	tmp := t.TempDir()
   146  
   147  	d := NewAllocDir(testlog.HCLogger(t), tmp, "test")
   148  	defer d.Destroy()
   149  	if err := d.Build(); err != nil {
   150  		t.Fatalf("Build() failed: %v", err)
   151  	}
   152  
   153  	// Build 2 task dirs
   154  	td1 := d.NewTaskDir(t1.Name)
   155  	if err := td1.Build(false, nil); err != nil {
   156  		t.Fatalf("error build task=%q dir: %v", t1.Name, err)
   157  	}
   158  	td2 := d.NewTaskDir(t2.Name)
   159  	if err := td2.Build(false, nil); err != nil {
   160  		t.Fatalf("error build task=%q dir: %v", t2.Name, err)
   161  	}
   162  
   163  	// Write a file to the shared dir.
   164  	exp := []byte{'f', 'o', 'o'}
   165  	file := "bar"
   166  	if err := ioutil.WriteFile(filepath.Join(d.SharedDir, "data", file), exp, 0666); err != nil {
   167  		t.Fatalf("Couldn't write file to shared directory: %v", err)
   168  	}
   169  
   170  	// Write a symlink to the shared dir
   171  	link := "qux"
   172  	if err := os.Symlink("foo", filepath.Join(d.SharedDir, "data", link)); err != nil {
   173  		t.Fatalf("Couldn't write symlink to shared directory: %v", err)
   174  	}
   175  
   176  	// Write a file to the task local
   177  	exp = []byte{'b', 'a', 'r'}
   178  	file1 := "lol"
   179  	if err := ioutil.WriteFile(filepath.Join(td1.LocalDir, file1), exp, 0666); err != nil {
   180  		t.Fatalf("couldn't write file to task local directory: %v", err)
   181  	}
   182  
   183  	// Write a symlink to the task local
   184  	link1 := "baz"
   185  	if err := os.Symlink("bar", filepath.Join(td1.LocalDir, link1)); err != nil {
   186  		t.Fatalf("couldn't write symlink to task local directory :%v", err)
   187  	}
   188  
   189  	var b bytes.Buffer
   190  	if err := d.Snapshot(&b); err != nil {
   191  		t.Fatalf("err: %v", err)
   192  	}
   193  
   194  	tr := tar.NewReader(&b)
   195  	var files []string
   196  	var links []string
   197  	for {
   198  		hdr, err := tr.Next()
   199  		if err != nil && err != io.EOF {
   200  			t.Fatalf("err: %v", err)
   201  		}
   202  		if err == io.EOF {
   203  			break
   204  		}
   205  		if hdr.Typeflag == tar.TypeReg {
   206  			files = append(files, hdr.FileInfo().Name())
   207  		} else if hdr.Typeflag == tar.TypeSymlink {
   208  			links = append(links, hdr.FileInfo().Name())
   209  		}
   210  	}
   211  
   212  	if len(files) != 2 {
   213  		t.Fatalf("bad files: %#v", files)
   214  	}
   215  	if len(links) != 2 {
   216  		t.Fatalf("bad links: %#v", links)
   217  	}
   218  }
   219  
   220  func TestAllocDir_Move(t *testing.T) {
   221  	ci.Parallel(t)
   222  
   223  	tmp1 := t.TempDir()
   224  	tmp2 := t.TempDir()
   225  
   226  	// Create two alloc dirs
   227  	d1 := NewAllocDir(testlog.HCLogger(t), tmp1, "test")
   228  	if err := d1.Build(); err != nil {
   229  		t.Fatalf("Build() failed: %v", err)
   230  	}
   231  	defer d1.Destroy()
   232  
   233  	d2 := NewAllocDir(testlog.HCLogger(t), tmp2, "test")
   234  	if err := d2.Build(); err != nil {
   235  		t.Fatalf("Build() failed: %v", err)
   236  	}
   237  	defer d2.Destroy()
   238  
   239  	td1 := d1.NewTaskDir(t1.Name)
   240  	if err := td1.Build(false, nil); err != nil {
   241  		t.Fatalf("TaskDir.Build() faild: %v", err)
   242  	}
   243  
   244  	// Create but don't build second task dir to mimic alloc/task runner
   245  	// behavior (AllocDir.Move() is called pre-TaskDir.Build).
   246  	d2.NewTaskDir(t1.Name)
   247  
   248  	dataDir := filepath.Join(d1.SharedDir, SharedDataDir)
   249  
   250  	// Write a file to the shared dir.
   251  	exp1 := []byte("foo")
   252  	file1 := "bar"
   253  	if err := ioutil.WriteFile(filepath.Join(dataDir, file1), exp1, 0666); err != nil {
   254  		t.Fatalf("Couldn't write file to shared directory: %v", err)
   255  	}
   256  
   257  	// Write a file to the task local
   258  	exp2 := []byte("bar")
   259  	file2 := "lol"
   260  	if err := ioutil.WriteFile(filepath.Join(td1.LocalDir, file2), exp2, 0666); err != nil {
   261  		t.Fatalf("couldn't write to task local directory: %v", err)
   262  	}
   263  
   264  	// Move the d1 allocdir to d2
   265  	if err := d2.Move(d1, []*structs.Task{t1}); err != nil {
   266  		t.Fatalf("err: %v", err)
   267  	}
   268  
   269  	// Ensure the files in d1 are present in d2
   270  	fi, err := os.Stat(filepath.Join(d2.SharedDir, SharedDataDir, file1))
   271  	if err != nil || fi == nil {
   272  		t.Fatalf("data dir was not moved")
   273  	}
   274  
   275  	fi, err = os.Stat(filepath.Join(d2.TaskDirs[t1.Name].LocalDir, file2))
   276  	if err != nil || fi == nil {
   277  		t.Fatalf("task local dir was not moved")
   278  	}
   279  }
   280  
   281  func TestAllocDir_EscapeChecking(t *testing.T) {
   282  	ci.Parallel(t)
   283  
   284  	tmp := t.TempDir()
   285  
   286  	d := NewAllocDir(testlog.HCLogger(t), tmp, "test")
   287  	if err := d.Build(); err != nil {
   288  		t.Fatalf("Build() failed: %v", err)
   289  	}
   290  	defer d.Destroy()
   291  
   292  	// Check that issuing calls that escape the alloc dir returns errors
   293  	// List
   294  	if _, err := d.List(".."); err == nil || !strings.Contains(err.Error(), "escapes") {
   295  		t.Fatalf("List of escaping path didn't error: %v", err)
   296  	}
   297  
   298  	// Stat
   299  	if _, err := d.Stat("../foo"); err == nil || !strings.Contains(err.Error(), "escapes") {
   300  		t.Fatalf("Stat of escaping path didn't error: %v", err)
   301  	}
   302  
   303  	// ReadAt
   304  	if _, err := d.ReadAt("../foo", 0); err == nil || !strings.Contains(err.Error(), "escapes") {
   305  		t.Fatalf("ReadAt of escaping path didn't error: %v", err)
   306  	}
   307  
   308  	// BlockUntilExists
   309  	if _, err := d.BlockUntilExists(context.Background(), "../foo"); err == nil || !strings.Contains(err.Error(), "escapes") {
   310  		t.Fatalf("BlockUntilExists of escaping path didn't error: %v", err)
   311  	}
   312  
   313  	// ChangeEvents
   314  	if _, err := d.ChangeEvents(context.Background(), "../foo", 0); err == nil || !strings.Contains(err.Error(), "escapes") {
   315  		t.Fatalf("ChangeEvents of escaping path didn't error: %v", err)
   316  	}
   317  }
   318  
   319  // Test that `nomad fs` can't read secrets
   320  func TestAllocDir_ReadAt_SecretDir(t *testing.T) {
   321  	ci.Parallel(t)
   322  	tmp := t.TempDir()
   323  
   324  	d := NewAllocDir(testlog.HCLogger(t), tmp, "test")
   325  	err := d.Build()
   326  	require.NoError(t, err)
   327  	defer func() {
   328  		_ = d.Destroy()
   329  	}()
   330  
   331  	td := d.NewTaskDir(t1.Name)
   332  	err = td.Build(false, nil)
   333  	require.NoError(t, err)
   334  
   335  	// something to write and test reading
   336  	target := filepath.Join(t1.Name, TaskSecrets, "test_file")
   337  
   338  	// create target file in the task secrets dir
   339  	full := filepath.Join(d.AllocDir, target)
   340  	err = ioutil.WriteFile(full, []byte("hi"), 0600)
   341  	require.NoError(t, err)
   342  
   343  	// ReadAt of a file in the task secrets dir should fail
   344  	_, err = d.ReadAt(target, 0)
   345  	require.EqualError(t, err, "Reading secret file prohibited: web/secrets/test_file")
   346  }
   347  
   348  func TestAllocDir_SplitPath(t *testing.T) {
   349  	ci.Parallel(t)
   350  
   351  	dir := t.TempDir()
   352  
   353  	dest := filepath.Join(dir, "/foo/bar/baz")
   354  	if err := os.MkdirAll(dest, os.ModePerm); err != nil {
   355  		t.Fatalf("err: %v", err)
   356  	}
   357  
   358  	info, err := splitPath(dest)
   359  	if err != nil {
   360  		t.Fatalf("err: %v", err)
   361  	}
   362  	// Testing that is 6 or more rather than 6 because on osx, the temp dir is
   363  	// randomized.
   364  	if len(info) < 6 {
   365  		t.Fatalf("expected more than: %v, actual: %v", 6, len(info))
   366  	}
   367  }
   368  
   369  func TestAllocDir_CreateDir(t *testing.T) {
   370  	ci.Parallel(t)
   371  	if syscall.Geteuid() != 0 {
   372  		t.Skip("Must be root to run test")
   373  	}
   374  
   375  	dir := t.TempDir()
   376  
   377  	// create a subdir and a file
   378  	subdir := filepath.Join(dir, "subdir")
   379  	if err := os.MkdirAll(subdir, 0760); err != nil {
   380  		t.Fatalf("err: %v", err)
   381  	}
   382  	subdirMode, err := os.Stat(subdir)
   383  	if err != nil {
   384  		t.Fatalf("err: %v", err)
   385  	}
   386  
   387  	// Create the above hierarchy under another destination
   388  	dir1 := t.TempDir()
   389  
   390  	if err := createDir(dir1, subdir); err != nil {
   391  		t.Fatalf("err: %v", err)
   392  	}
   393  
   394  	// Ensure that the subdir had the right perm
   395  	fi, err := os.Stat(filepath.Join(dir1, dir, "subdir"))
   396  	if err != nil {
   397  		t.Fatalf("err: %v", err)
   398  	}
   399  	if fi.Mode() != subdirMode.Mode() {
   400  		t.Fatalf("wrong file mode: %v, expected: %v", fi.Mode(), subdirMode.Mode())
   401  	}
   402  }
   403  
   404  func TestPathFuncs(t *testing.T) {
   405  	ci.Parallel(t)
   406  
   407  	dir := t.TempDir()
   408  
   409  	missingDir := filepath.Join(dir, "does-not-exist")
   410  
   411  	if !pathExists(dir) {
   412  		t.Errorf("%q exists", dir)
   413  	}
   414  	if pathExists(missingDir) {
   415  		t.Errorf("%q does not exist", missingDir)
   416  	}
   417  
   418  	if empty, err := pathEmpty(dir); err != nil || !empty {
   419  		t.Errorf("%q is empty and exists. empty=%v error=%v", dir, empty, err)
   420  	}
   421  	if empty, err := pathEmpty(missingDir); err == nil || empty {
   422  		t.Errorf("%q is missing. empty=%v error=%v", missingDir, empty, err)
   423  	}
   424  
   425  	filename := filepath.Join(dir, "just-some-file")
   426  	f, err := os.Create(filename)
   427  	if err != nil {
   428  		t.Fatalf("could not create %q: %v", filename, err)
   429  	}
   430  	f.Close()
   431  
   432  	if empty, err := pathEmpty(dir); err != nil || empty {
   433  		t.Errorf("%q is not empty. empty=%v error=%v", dir, empty, err)
   434  	}
   435  }
   436  
   437  func TestAllocDir_DetectContentType(t *testing.T) {
   438  	ci.Parallel(t)
   439  	require := require.New(t)
   440  
   441  	inputPath := "input/"
   442  	var testFiles []string
   443  	err := filepath.Walk(inputPath, func(path string, info os.FileInfo, err error) error {
   444  		if !info.IsDir() {
   445  			testFiles = append(testFiles, path)
   446  		}
   447  		return err
   448  	})
   449  	require.Nil(err)
   450  
   451  	expectedEncodings := map[string]string{
   452  		"input/happy.gif": "image/gif",
   453  		"input/image.png": "image/png",
   454  		"input/nomad.jpg": "image/jpeg",
   455  		"input/test.bin":  "application/octet-stream",
   456  		"input/test.json": "application/json",
   457  		"input/test.txt":  "text/plain; charset=utf-8",
   458  		"input/test.go":   "text/plain; charset=utf-8",
   459  		"input/test.hcl":  "text/plain; charset=utf-8",
   460  	}
   461  	for _, file := range testFiles {
   462  		fileInfo, err := os.Stat(file)
   463  		require.Nil(err)
   464  		res := detectContentType(fileInfo, file)
   465  		require.Equal(expectedEncodings[file], res, "unexpected output for %v", file)
   466  	}
   467  }
   468  
   469  // TestAllocDir_SkipAllocDir asserts that building a chroot which contains
   470  // itself will *not* infinitely recurse. AllocDirs should always skip embedding
   471  // themselves into chroots.
   472  //
   473  // Warning: If this test fails it may fill your disk before failing, so be
   474  // careful and/or confident.
   475  func TestAllocDir_SkipAllocDir(t *testing.T) {
   476  	ci.Parallel(t)
   477  	MountCompatible(t)
   478  
   479  	// Create root, alloc, and other dirs
   480  	rootDir := t.TempDir()
   481  
   482  	clientAllocDir := filepath.Join(rootDir, "nomad")
   483  	require.NoError(t, os.Mkdir(clientAllocDir, fs.ModeDir|0o777))
   484  
   485  	otherDir := filepath.Join(rootDir, "etc")
   486  	require.NoError(t, os.Mkdir(otherDir, fs.ModeDir|0o777))
   487  
   488  	// chroot contains client.alloc_dir! This could cause infinite
   489  	// recursion.
   490  	chroot := map[string]string{
   491  		rootDir: "/",
   492  	}
   493  
   494  	allocDir := NewAllocDir(testlog.HCLogger(t), clientAllocDir, "test")
   495  	taskDir := allocDir.NewTaskDir("testtask")
   496  
   497  	require.NoError(t, allocDir.Build())
   498  	defer allocDir.Destroy()
   499  
   500  	// Build chroot
   501  	err := taskDir.Build(true, chroot)
   502  	require.NoError(t, err)
   503  
   504  	// Assert other directory *was* embedded
   505  	embeddedOtherDir := filepath.Join(clientAllocDir, "test", "testtask", "etc")
   506  	if _, err := os.Stat(embeddedOtherDir); os.IsNotExist(err) {
   507  		t.Fatalf("expected other directory to exist at: %q", embeddedOtherDir)
   508  	}
   509  
   510  	// Assert client.alloc_dir was *not* embedded
   511  	embeddedChroot := filepath.Join(clientAllocDir, "test", "testtask", "nomad")
   512  	s, err := os.Stat(embeddedChroot)
   513  	if s != nil {
   514  		t.Logf("somehow you managed to embed the chroot without causing infinite recursion!")
   515  		t.Fatalf("expected chroot to not exist at: %q", embeddedChroot)
   516  	}
   517  	if !os.IsNotExist(err) {
   518  		t.Fatalf("expected chroot to not exist but error is: %v", err)
   519  	}
   520  }