github.com/mgoltzsche/ctnr@v0.7.1-alpha/pkg/fs/tree/fsbuilder_test.go (about)

     1  package tree
     2  
     3  import (
     4  	"bytes"
     5  	"fmt"
     6  	"io/ioutil"
     7  	"log"
     8  	"os"
     9  	"path/filepath"
    10  	"strings"
    11  	"testing"
    12  
    13  	"github.com/mgoltzsche/ctnr/pkg/fs"
    14  	"github.com/mgoltzsche/ctnr/pkg/fs/source"
    15  	"github.com/mgoltzsche/ctnr/pkg/fs/testutils"
    16  	"github.com/mgoltzsche/ctnr/pkg/fs/writer"
    17  	"github.com/stretchr/testify/assert"
    18  	"github.com/stretchr/testify/require"
    19  	"github.com/vbatts/go-mtree"
    20  )
    21  
    22  func absDirs(baseDir string, file []string) []string {
    23  	files := make([]string, len(file))
    24  	for i, f := range file {
    25  		files[i] = filepath.Join(baseDir, f)
    26  	}
    27  	return files
    28  }
    29  
    30  func TestFsBuilder(t *testing.T) {
    31  	tmpDir, err := ioutil.TempDir("", "fsbuilder-test-")
    32  	require.NoError(t, err)
    33  	defer os.RemoveAll(tmpDir)
    34  	rootfs := filepath.Join(tmpDir, "rootfs")
    35  	opts := fs.NewFSOptions(true)
    36  	warn := log.New(os.Stdout, "warn: ", 0)
    37  	testutils.WriteTestFileSystem(t, writer.NewDirWriter(rootfs, opts, warn))
    38  
    39  	//
    40  	// FILE SYSTEM TREE CONSTRUCTION TESTS
    41  	//
    42  
    43  	// Test AddAll() - source creation and mapping to destination path
    44  	expectedFileA := map[string]bool{"/": true, "/dir": true, "/dir/fileA": true}
    45  	expectedFilesAB := map[string]bool{
    46  		"/":          true,
    47  		"/dir":       true,
    48  		"/dir/fileA": true,
    49  		"/dir/fileB": true,
    50  	}
    51  	expectedFileCopyOps := map[string]bool{
    52  		"/":                             true,
    53  		"/dir":                          true,
    54  		"/dir/x":                        true,
    55  		"/dir/x/fifo":                   true,
    56  		"/dir/x/fileA":                  true,
    57  		"/dir/x/link-abs":               true,
    58  		"/dir/x/nesteddir":              true,
    59  		"/dir/x/nestedsymlink":          true,
    60  		"/dir/x/symlinkResolved1":       true,
    61  		"/dir/x/dirA1/symlinkResolved2": true,
    62  		"/dir/x/dirA1":                  true,
    63  	}
    64  	expectedDir1CopyOps := map[string]bool{
    65  		"/":                            true,
    66  		"/dest":                        true,
    67  		"/dest/dir":                    true,
    68  		"/dest/dir/file1":              true,
    69  		"/dest/dir/file2":              true,
    70  		"/dest/dir/sdir":               true,
    71  		"/dest/dir/sdir/nesteddir":     true,
    72  		"/dest/dir/sdir/nestedsymlink": true,
    73  	}
    74  	expectedRootfsOps := mtreeToExpectedPathSet(t, "/all", testutils.ExpectedTestfsState)
    75  	for _, c := range []struct {
    76  		src           []string
    77  		dest          string
    78  		expand        bool
    79  		expectedPaths map[string]bool
    80  	}{
    81  		{[]string{"rootfs/etc/fileA", "rootfs/etc/link-abs", "rootfs/etc/symlink-rel", "rootfs/dir1/sdir", "rootfs/etc/fifo"}, "dir/x", false, expectedFileCopyOps},
    82  		{[]string{"rootfs/etc/fileA", "rootfs/etc/link-abs", "rootfs/etc/symlink-rel", "rootfs/dir1/sdir", "rootfs/etc/fifo"}, "dir/x/", false, expectedFileCopyOps},
    83  		{[]string{"rootfs/etc/fileA"}, "dir/fileX", false, map[string]bool{"/": true, "/dir": true, "/dir/fileX": true}},
    84  		{[]string{"rootfs/etc/fileA"}, "dir/", false, expectedFileA},
    85  		//{[]string{filepath.Join(tmpDir, "rootfs/etc/fileA")}, "dir/", false, expectedFileA},
    86  		{[]string{"rootfs/etc/file*"}, "dir", false, expectedFilesAB},
    87  		{[]string{"rootfs/dir1"}, "dest/dir", false, expectedDir1CopyOps},
    88  		{[]string{"rootfs/dir1/"}, "dest/dir", false, expectedDir1CopyOps},
    89  		{[]string{"rootfs/dir1"}, "dest/dir/", false, expectedDir1CopyOps},
    90  		{[]string{"rootfs/dir1/"}, "dest/dir/", false, expectedDir1CopyOps},
    91  		{[]string{"rootfs"}, "/all/", false, expectedRootfsOps},
    92  		// TODO: add URL source test case
    93  	} {
    94  		label := fmt.Sprintf("AddAll(ctx, %+v, %s)", c.src, c.dest)
    95  		rootfs := newFS()
    96  		testee := NewFsBuilder(rootfs, opts)
    97  		testee.AddAll(tmpDir, c.src, c.dest, nil)
    98  		w := testutils.NewWriterMock(t, fs.AttrsAll)
    99  		err := testee.Write(w)
   100  		require.NoError(t, err, label)
   101  		rootfs.MockDevices()
   102  		// need to assert against path map since archive content write order is not guaranteed
   103  		if !assert.Equal(t, c.expectedPaths, w.WrittenPaths, label) {
   104  			t.FailNow()
   105  		}
   106  		_, err = testee.Hash(fs.AttrsAll)
   107  		require.NoError(t, err)
   108  	}
   109  
   110  	// Test error
   111  	testee := NewFsBuilder(newFS(), opts)
   112  	testee.AddAll(tmpDir, []string{"not-existing"}, "/", nil)
   113  	err = testee.Write(fs.HashingNilWriter())
   114  	require.Error(t, err, "using not existing file as source should yield error")
   115  
   116  	//
   117  	// CONSISTENCY TESTS
   118  	//
   119  
   120  	// Test written node tree equals original
   121  	testee = NewFsBuilder(newFS(), opts)
   122  	testee.CopyDir(rootfs, "/", nil)
   123  	var buf bytes.Buffer
   124  	tree, err := testee.FS()
   125  	require.NoError(t, err)
   126  	err = tree.WriteTo(&buf, fs.AttrsCompare)
   127  	require.NoError(t, err)
   128  	expectedStr := buf.String()
   129  	expectedWritten := testutils.MockWrites(t, tree).Written
   130  	expectedWritten2 := testutils.MockWrites(t, tree).Written
   131  	if !assert.Equal(t, expectedWritten, expectedWritten2, "Write() should be idempotent") {
   132  		t.FailNow()
   133  	}
   134  	// Create tar
   135  	tarFile := filepath.Join(tmpDir, "archive.tar")
   136  	f, err := os.OpenFile(tarFile, os.O_CREATE|os.O_RDWR, 0640)
   137  	require.NoError(t, err)
   138  	defer f.Close()
   139  	err = testee.Write(writer.NewTarWriter(f))
   140  	require.NoError(t, err)
   141  	f.Close()
   142  
   143  	// Read, extract and compare cases
   144  	for i, c := range []string{"rootfs", "archive.tar"} {
   145  		testee := NewFsBuilder(newFS(), opts)
   146  		if i == 0 {
   147  			// rootfs dir
   148  			testee.CopyDir(filepath.Join(tmpDir, c), "/", nil)
   149  		} else {
   150  			// archive
   151  			testee.AddAll(tmpDir, []string{c}, "/", nil)
   152  		}
   153  		// Normalize
   154  		rootfs := filepath.Join(tmpDir, "rootfs"+fmt.Sprintf("%d", i))
   155  		dirWriter := writer.NewDirWriter(rootfs, opts, warn)
   156  		nodes := newFS()
   157  		nodeWriter := writer.NewFsNodeWriter(nodes, dirWriter)
   158  		err = testee.Write(&fs.ExpandingWriter{nodeWriter})
   159  		require.NoError(t, err)
   160  		err = dirWriter.Close()
   161  		require.NoError(t, err)
   162  		// Assert normalized string representation equals original
   163  		buf.Reset()
   164  		err = nodes.WriteTo(&buf, fs.AttrsCompare)
   165  		require.NoError(t, err)
   166  		if !assert.Equal(t, strings.Split(expectedStr, "\n"), strings.Split(buf.String(), "\n"), "string(expand("+c+")) != string(sourcedir{"+c+"})") {
   167  			t.FailNow()
   168  		}
   169  		// Write nodes written by FsNodeWriter should equal original
   170  		if !assert.Equal(t, expectedWritten, testutils.MockWrites(t, nodes).Written, "a.Write(nodeWriter); nodeWriter.Write() should write same as original") {
   171  			t.FailNow()
   172  		}
   173  		// Assert fs.Diff(fs) should return empty tree
   174  		diff, err := tree.Diff(nodes)
   175  		require.NoError(t, err)
   176  		if !assert.Equal(t, []string{}, testutils.MockWrites(t, diff).Written, "a.Diff(a) should be empty") {
   177  			t.FailNow()
   178  		}
   179  		// Assert fs.Diff(changedFs) == change
   180  		_, err = nodes.AddUpper("/etc/dir1", source.NewSourceDir(fs.FileAttrs{Mode: os.ModeDir | 0740}))
   181  		require.NoError(t, err)
   182  		diff, err = tree.Diff(nodes)
   183  		require.NoError(t, err)
   184  		w := testutils.NewWriterMock(t, fs.AttrsHash)
   185  		err = diff.Write(w)
   186  		expectedOps := []string{
   187  			"/ type=dir mode=775",
   188  			"/etc type=dir mode=775",
   189  			"/etc/dir1 type=dir mode=740",
   190  		}
   191  		if !assert.Equal(t, expectedOps, w.Written, "fs.Diff(changedFs) == changes") {
   192  			t.FailNow()
   193  		}
   194  	}
   195  
   196  	// Test Hash()
   197  	testee = NewFsBuilder(newFS(), opts)
   198  	testee.AddFiles(filepath.Join(rootfs, "etc/fileA"), "fileA", nil)
   199  	hash1, err := testee.Hash(fs.AttrsHash)
   200  	require.NoError(t, err)
   201  	testee = NewFsBuilder(newFS(), opts)
   202  	testee.AddFiles(filepath.Join(rootfs, "etc/fileA"), "fileA", nil)
   203  	hash2, err := testee.Hash(fs.AttrsHash)
   204  	require.NoError(t, err)
   205  	if hash1 != hash2 {
   206  		t.Errorf("Hash(): should be idempotent")
   207  		t.FailNow()
   208  	}
   209  	testee.AddFiles(filepath.Join(rootfs, "etc/fileB"), "fileA", nil)
   210  	hash2, err = testee.Hash(fs.AttrsCompare)
   211  	require.NoError(t, err)
   212  	if hash1 == hash2 {
   213  		t.Errorf("Hash(): must return changed value when content changed")
   214  		t.FailNow()
   215  	}
   216  }
   217  
   218  // TODO: enable again
   219  /*func TestFileSystemBuilderRootfsBoundsViolation(t *testing.T) {
   220  	for _, c := range []struct {
   221  		src  string
   222  		dest string
   223  		msg  string
   224  	}{
   225  		{"/dir2", "../outsiderootfs", "destination outside rootfs directory was not rejected"},
   226  		{"dir1/sdir1/linkInvalid", "/dirx", "linking outside rootfs directory was not rejected"},
   227  		//{"/dir2"}, "/dirx", "source path outside context directory was not rejected"},
   228  		//{"../outsidectx", "dirx", "relative source pattern outside context directory was not rejected"},
   229  	} {
   230  		ctxDir, rootfs := createFiles(t)
   231  		defer deleteFiles(ctxDir, rootfs)
   232  		opts := NewFSOptions(true)
   233  		testee := NewFsBuilder(opts)
   234  		testee.AddFiles(filepath.Join(ctxDir, c.src), c.dest, nil)
   235  		if err := testee.Write(newWriterMock(t)); err == nil {
   236  			t.Errorf(c.msg + ": " + c.src + " -> " + c.dest)
   237  		}
   238  	}
   239  }*/
   240  
   241  func mtreeToExpectedPathSet(t *testing.T, rootPath, dhStr string) (r map[string]bool) {
   242  	r = map[string]bool{}
   243  	r["/"] = true
   244  	if rootPath != "" {
   245  		// Add root dirs
   246  		names := strings.Split(filepath.Clean(rootPath), string(filepath.Separator))[1:]
   247  		for i, _ := range names {
   248  			r[string(filepath.Separator)+filepath.Join(names[:i]...)] = true
   249  		}
   250  	}
   251  	dhStr = strings.Replace(dhStr, "$ROOT", rootPath, -1)
   252  	dh, err := mtree.ParseSpec(strings.NewReader(dhStr))
   253  	require.NoError(t, err)
   254  	diff, err := mtree.Compare(&mtree.DirectoryHierarchy{}, dh, testutils.MtreeTestkeywords)
   255  	require.NoError(t, err)
   256  	for _, e := range diff {
   257  		r[filepath.Join(rootPath, string(filepath.Separator)+e.Path())] = true
   258  	}
   259  	return r
   260  }