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 }