github.com/cdoern/storage@v1.12.13/pkg/archive/changes_test.go (about)

     1  package archive
     2  
     3  import (
     4  	"io/ioutil"
     5  	"os"
     6  	"os/exec"
     7  	"path"
     8  	"runtime"
     9  	"sort"
    10  	"testing"
    11  	"time"
    12  
    13  	"github.com/containers/storage/pkg/idtools"
    14  	"github.com/containers/storage/pkg/system"
    15  	"github.com/stretchr/testify/require"
    16  )
    17  
    18  func max(x, y int) int {
    19  	if x >= y {
    20  		return x
    21  	}
    22  	return y
    23  }
    24  
    25  func copyDir(src, dst string) error {
    26  	cmd := exec.Command("cp", "-a", src, dst)
    27  	if runtime.GOOS == "solaris" {
    28  		cmd = exec.Command("gcp", "-a", src, dst)
    29  	}
    30  
    31  	if err := cmd.Run(); err != nil {
    32  		return err
    33  	}
    34  	return nil
    35  }
    36  
    37  type FileType uint32
    38  
    39  const (
    40  	Regular FileType = iota
    41  	Dir
    42  	Symlink
    43  )
    44  
    45  type FileData struct {
    46  	filetype    FileType
    47  	path        string
    48  	contents    string
    49  	permissions os.FileMode
    50  }
    51  
    52  func createSampleDir(t *testing.T, root string) {
    53  	files := []FileData{
    54  		{Regular, "file1", "file1\n", 0600},
    55  		{Regular, "file2", "file2\n", 0666},
    56  		{Regular, "file3", "file3\n", 0404},
    57  		{Regular, "file4", "file4\n", 0600},
    58  		{Regular, "file5", "file5\n", 0600},
    59  		{Regular, "file6", "file6\n", 0600},
    60  		{Regular, "file7", "file7\n", 0600},
    61  		{Dir, "dir1", "", 0740},
    62  		{Regular, "dir1/file1-1", "file1-1\n", 01444},
    63  		{Regular, "dir1/file1-2", "file1-2\n", 0666},
    64  		{Dir, "dir2", "", 0700},
    65  		{Regular, "dir2/file2-1", "file2-1\n", 0666},
    66  		{Regular, "dir2/file2-2", "file2-2\n", 0666},
    67  		{Dir, "dir3", "", 0700},
    68  		{Regular, "dir3/file3-1", "file3-1\n", 0666},
    69  		{Regular, "dir3/file3-2", "file3-2\n", 0666},
    70  		{Dir, "dir4", "", 0700},
    71  		{Regular, "dir4/file3-1", "file4-1\n", 0666},
    72  		{Regular, "dir4/file3-2", "file4-2\n", 0666},
    73  		{Symlink, "symlink1", "target1", 0666},
    74  		{Symlink, "symlink2", "target2", 0666},
    75  		{Symlink, "symlink3", root + "/file1", 0666},
    76  		{Symlink, "symlink4", root + "/symlink3", 0666},
    77  		{Symlink, "dirSymlink", root + "/dir1", 0740},
    78  	}
    79  
    80  	now := time.Now()
    81  	for _, info := range files {
    82  		p := path.Join(root, info.path)
    83  		if info.filetype == Dir {
    84  			err := os.MkdirAll(p, info.permissions)
    85  			require.NoError(t, err)
    86  		} else if info.filetype == Regular {
    87  			err := ioutil.WriteFile(p, []byte(info.contents), info.permissions)
    88  			require.NoError(t, err)
    89  		} else if info.filetype == Symlink {
    90  			err := os.Symlink(info.contents, p)
    91  			require.NoError(t, err)
    92  		}
    93  
    94  		if info.filetype != Symlink {
    95  			// Set a consistent ctime, atime for all files and dirs
    96  			err := system.Chtimes(p, now, now)
    97  			require.NoError(t, err)
    98  		}
    99  	}
   100  }
   101  
   102  func TestChangeString(t *testing.T) {
   103  	modifyChange := Change{"change", ChangeModify}
   104  	toString := modifyChange.String()
   105  	if toString != "C change" {
   106  		t.Fatalf("String() of a change with ChangeModify Kind should have been %s but was %s", "C change", toString)
   107  	}
   108  	addChange := Change{"change", ChangeAdd}
   109  	toString = addChange.String()
   110  	if toString != "A change" {
   111  		t.Fatalf("String() of a change with ChangeAdd Kind should have been %s but was %s", "A change", toString)
   112  	}
   113  	deleteChange := Change{"change", ChangeDelete}
   114  	toString = deleteChange.String()
   115  	if toString != "D change" {
   116  		t.Fatalf("String() of a change with ChangeDelete Kind should have been %s but was %s", "D change", toString)
   117  	}
   118  }
   119  
   120  func TestChangesWithNoChanges(t *testing.T) {
   121  	// TODO Windows. There may be a way of running this, but turning off for now
   122  	// as createSampleDir uses symlinks.
   123  	if runtime.GOOS == "windows" {
   124  		t.Skip("symlinks on Windows")
   125  	}
   126  	rwLayer, err := ioutil.TempDir("", "storage-changes-test")
   127  	require.NoError(t, err)
   128  	defer os.RemoveAll(rwLayer)
   129  	layer, err := ioutil.TempDir("", "storage-changes-test-layer")
   130  	require.NoError(t, err)
   131  	defer os.RemoveAll(layer)
   132  	createSampleDir(t, layer)
   133  	changes, err := Changes([]string{layer}, rwLayer)
   134  	require.NoError(t, err)
   135  	if len(changes) != 0 {
   136  		t.Fatalf("Changes with no difference should have detect no changes, but detected %d", len(changes))
   137  	}
   138  }
   139  
   140  func TestChangesWithChanges(t *testing.T) {
   141  	// TODO Windows. There may be a way of running this, but turning off for now
   142  	// as createSampleDir uses symlinks.
   143  	if runtime.GOOS == "windows" {
   144  		t.Skip("symlinks on Windows")
   145  	}
   146  	// Mock the readonly layer
   147  	layer, err := ioutil.TempDir("", "storage-changes-test-layer")
   148  	require.NoError(t, err)
   149  	defer os.RemoveAll(layer)
   150  	createSampleDir(t, layer)
   151  	os.MkdirAll(path.Join(layer, "dir1/subfolder"), 0740)
   152  
   153  	// Mock the RW layer
   154  	rwLayer, err := ioutil.TempDir("", "storage-changes-test")
   155  	require.NoError(t, err)
   156  	defer os.RemoveAll(rwLayer)
   157  
   158  	// Create a folder in RW layer
   159  	dir1 := path.Join(rwLayer, "dir1")
   160  	os.MkdirAll(dir1, 0740)
   161  	deletedFile := path.Join(dir1, ".wh.file1-2")
   162  	ioutil.WriteFile(deletedFile, []byte{}, 0600)
   163  	modifiedFile := path.Join(dir1, "file1-1")
   164  	ioutil.WriteFile(modifiedFile, []byte{0x00}, 01444)
   165  	// Let's add a subfolder for a newFile
   166  	subfolder := path.Join(dir1, "subfolder")
   167  	os.MkdirAll(subfolder, 0740)
   168  	newFile := path.Join(subfolder, "newFile")
   169  	ioutil.WriteFile(newFile, []byte{}, 0740)
   170  
   171  	changes, err := Changes([]string{layer}, rwLayer)
   172  	require.NoError(t, err)
   173  
   174  	expectedChanges := []Change{
   175  		{"/dir1", ChangeModify},
   176  		{"/dir1/file1-1", ChangeModify},
   177  		{"/dir1/file1-2", ChangeDelete},
   178  		{"/dir1/subfolder", ChangeModify},
   179  		{"/dir1/subfolder/newFile", ChangeAdd},
   180  	}
   181  	checkChanges(expectedChanges, changes, t)
   182  }
   183  
   184  // See https://github.com/docker/docker/pull/13590
   185  func TestChangesWithChangesGH13590(t *testing.T) {
   186  	// TODO Windows. There may be a way of running this, but turning off for now
   187  	// as createSampleDir uses symlinks.
   188  	if runtime.GOOS == "windows" {
   189  		t.Skip("symlinks on Windows")
   190  	}
   191  	baseLayer, err := ioutil.TempDir("", "storage-changes-test.")
   192  	defer os.RemoveAll(baseLayer)
   193  
   194  	dir3 := path.Join(baseLayer, "dir1/dir2/dir3")
   195  	os.MkdirAll(dir3, 07400)
   196  
   197  	file := path.Join(dir3, "file.txt")
   198  	ioutil.WriteFile(file, []byte("hello"), 0666)
   199  
   200  	layer, err := ioutil.TempDir("", "storage-changes-test2.")
   201  	defer os.RemoveAll(layer)
   202  
   203  	// Test creating a new file
   204  	if err := copyDir(baseLayer+"/dir1", layer+"/"); err != nil {
   205  		t.Fatalf("Cmd failed: %q", err)
   206  	}
   207  
   208  	os.Remove(path.Join(layer, "dir1/dir2/dir3/file.txt"))
   209  	file = path.Join(layer, "dir1/dir2/dir3/file1.txt")
   210  	ioutil.WriteFile(file, []byte("bye"), 0666)
   211  
   212  	changes, err := Changes([]string{baseLayer}, layer)
   213  	require.NoError(t, err)
   214  
   215  	expectedChanges := []Change{
   216  		{"/dir1", ChangeModify},
   217  		{"/dir1/dir2", ChangeModify},
   218  		{"/dir1/dir2/dir3", ChangeModify},
   219  		{"/dir1/dir2/dir3/file1.txt", ChangeAdd},
   220  	}
   221  	checkChanges(expectedChanges, changes, t)
   222  
   223  	// Now test changing a file
   224  	layer, err = ioutil.TempDir("", "storage-changes-test3.")
   225  	defer os.RemoveAll(layer)
   226  
   227  	if err := copyDir(baseLayer+"/dir1", layer+"/"); err != nil {
   228  		t.Fatalf("Cmd failed: %q", err)
   229  	}
   230  
   231  	file = path.Join(layer, "dir1/dir2/dir3/file.txt")
   232  	ioutil.WriteFile(file, []byte("bye"), 0666)
   233  
   234  	changes, err = Changes([]string{baseLayer}, layer)
   235  	require.NoError(t, err)
   236  
   237  	expectedChanges = []Change{
   238  		{"/dir1/dir2/dir3/file.txt", ChangeModify},
   239  	}
   240  	checkChanges(expectedChanges, changes, t)
   241  }
   242  
   243  // Create a directory, copy it, make sure we report no changes between the two
   244  func TestChangesDirsEmpty(t *testing.T) {
   245  	// TODO Windows. There may be a way of running this, but turning off for now
   246  	// as createSampleDir uses symlinks.
   247  	// TODO Should work for Solaris
   248  	if runtime.GOOS == "windows" || runtime.GOOS == "solaris" {
   249  		t.Skip("symlinks on Windows; gcp failure on Solaris")
   250  	}
   251  	src, err := ioutil.TempDir("", "storage-changes-test")
   252  	require.NoError(t, err)
   253  	defer os.RemoveAll(src)
   254  	createSampleDir(t, src)
   255  	dst := src + "-copy"
   256  	err = copyDir(src, dst)
   257  	require.NoError(t, err)
   258  	defer os.RemoveAll(dst)
   259  	changes, err := ChangesDirs(dst, &idtools.IDMappings{}, src, &idtools.IDMappings{})
   260  	require.NoError(t, err)
   261  
   262  	if len(changes) != 0 {
   263  		t.Fatalf("Reported changes for identical dirs: %v", changes)
   264  	}
   265  	os.RemoveAll(src)
   266  	os.RemoveAll(dst)
   267  }
   268  
   269  func mutateSampleDir(t *testing.T, root string) {
   270  	// Remove a regular file
   271  	err := os.RemoveAll(path.Join(root, "file1"))
   272  	require.NoError(t, err)
   273  
   274  	// Remove a directory
   275  	err = os.RemoveAll(path.Join(root, "dir1"))
   276  	require.NoError(t, err)
   277  
   278  	// Remove a symlink
   279  	err = os.RemoveAll(path.Join(root, "symlink1"))
   280  	require.NoError(t, err)
   281  
   282  	// Rewrite a file
   283  	err = ioutil.WriteFile(path.Join(root, "file2"), []byte("fileNN\n"), 0777)
   284  	require.NoError(t, err)
   285  
   286  	// Replace a file
   287  	err = os.RemoveAll(path.Join(root, "file3"))
   288  	require.NoError(t, err)
   289  	err = ioutil.WriteFile(path.Join(root, "file3"), []byte("fileMM\n"), 0404)
   290  	require.NoError(t, err)
   291  
   292  	// Touch file
   293  	err = system.Chtimes(path.Join(root, "file4"), time.Now().Add(time.Second), time.Now().Add(time.Second))
   294  	require.NoError(t, err)
   295  
   296  	// Replace file with dir
   297  	err = os.RemoveAll(path.Join(root, "file5"))
   298  	require.NoError(t, err)
   299  	err = os.MkdirAll(path.Join(root, "file5"), 0666)
   300  	require.NoError(t, err)
   301  
   302  	// Create new file
   303  	err = ioutil.WriteFile(path.Join(root, "filenew"), []byte("filenew\n"), 0777)
   304  	require.NoError(t, err)
   305  
   306  	// Create new dir
   307  	err = os.MkdirAll(path.Join(root, "dirnew"), 0766)
   308  	require.NoError(t, err)
   309  
   310  	// Create a new symlink
   311  	err = os.Symlink("targetnew", path.Join(root, "symlinknew"))
   312  	require.NoError(t, err)
   313  
   314  	// Change a symlink
   315  	err = os.RemoveAll(path.Join(root, "symlink2"))
   316  	require.NoError(t, err)
   317  
   318  	err = os.Symlink("target2change", path.Join(root, "symlink2"))
   319  	require.NoError(t, err)
   320  
   321  	// Replace dir with file
   322  	err = os.RemoveAll(path.Join(root, "dir2"))
   323  	require.NoError(t, err)
   324  	err = ioutil.WriteFile(path.Join(root, "dir2"), []byte("dir2\n"), 0777)
   325  	require.NoError(t, err)
   326  
   327  	// Touch dir
   328  	err = system.Chtimes(path.Join(root, "dir3"), time.Now().Add(time.Second), time.Now().Add(time.Second))
   329  	require.NoError(t, err)
   330  }
   331  
   332  func TestChangesDirsMutated(t *testing.T) {
   333  	// TODO Windows. There may be a way of running this, but turning off for now
   334  	// as createSampleDir uses symlinks.
   335  	// TODO Should work for Solaris
   336  	if runtime.GOOS == "windows" || runtime.GOOS == "solaris" {
   337  		t.Skip("symlinks on Windows; gcp failures on Solaris")
   338  	}
   339  	src, err := ioutil.TempDir("", "storage-changes-test")
   340  	require.NoError(t, err)
   341  	createSampleDir(t, src)
   342  	dst := src + "-copy"
   343  	err = copyDir(src, dst)
   344  	require.NoError(t, err)
   345  	defer os.RemoveAll(src)
   346  	defer os.RemoveAll(dst)
   347  
   348  	mutateSampleDir(t, dst)
   349  
   350  	changes, err := ChangesDirs(dst, &idtools.IDMappings{}, src, &idtools.IDMappings{})
   351  	require.NoError(t, err)
   352  
   353  	sort.Sort(changesByPath(changes))
   354  
   355  	expectedChanges := []Change{
   356  		{"/dir1", ChangeDelete},
   357  		{"/dir2", ChangeModify},
   358  		{"/dirnew", ChangeAdd},
   359  		{"/file1", ChangeDelete},
   360  		{"/file2", ChangeModify},
   361  		{"/file3", ChangeModify},
   362  		{"/file4", ChangeModify},
   363  		{"/file5", ChangeModify},
   364  		{"/filenew", ChangeAdd},
   365  		{"/symlink1", ChangeDelete},
   366  		{"/symlink2", ChangeModify},
   367  		{"/symlinknew", ChangeAdd},
   368  	}
   369  
   370  	for i := 0; i < max(len(changes), len(expectedChanges)); i++ {
   371  		if i >= len(expectedChanges) {
   372  			t.Fatalf("unexpected change %s\n", changes[i].String())
   373  		}
   374  		if i >= len(changes) {
   375  			t.Fatalf("no change for expected change %s\n", expectedChanges[i].String())
   376  		}
   377  		if changes[i].Path == expectedChanges[i].Path {
   378  			if changes[i] != expectedChanges[i] {
   379  				t.Fatalf("Wrong change for %s, expected %s, got %s\n", changes[i].Path, changes[i].String(), expectedChanges[i].String())
   380  			}
   381  		} else if changes[i].Path < expectedChanges[i].Path {
   382  			t.Fatalf("unexpected change %s\n", changes[i].String())
   383  		} else {
   384  			t.Fatalf("no change for expected change %s != %s\n", expectedChanges[i].String(), changes[i].String())
   385  		}
   386  	}
   387  }
   388  
   389  func TestApplyLayer(t *testing.T) {
   390  	// TODO Windows. There may be a way of running this, but turning off for now
   391  	// as createSampleDir uses symlinks.
   392  	// TODO Should work for Solaris
   393  	if runtime.GOOS == "windows" || runtime.GOOS == "solaris" {
   394  		t.Skip("symlinks on Windows; gcp failures on Solaris")
   395  	}
   396  	src, err := ioutil.TempDir("", "storage-changes-test")
   397  	require.NoError(t, err)
   398  	createSampleDir(t, src)
   399  	defer os.RemoveAll(src)
   400  	dst := src + "-copy"
   401  	err = copyDir(src, dst)
   402  	require.NoError(t, err)
   403  	mutateSampleDir(t, dst)
   404  	defer os.RemoveAll(dst)
   405  
   406  	changes, err := ChangesDirs(dst, &idtools.IDMappings{}, src, &idtools.IDMappings{})
   407  	require.NoError(t, err)
   408  
   409  	layer, err := ExportChanges(dst, changes, nil, nil)
   410  	require.NoError(t, err)
   411  
   412  	layerCopy, err := NewTempArchive(layer, "")
   413  	require.NoError(t, err)
   414  
   415  	_, err = ApplyLayer(src, layerCopy)
   416  	require.NoError(t, err)
   417  
   418  	changes2, err := ChangesDirs(src, &idtools.IDMappings{}, dst, &idtools.IDMappings{})
   419  	require.NoError(t, err)
   420  
   421  	if len(changes2) != 0 {
   422  		t.Fatalf("Unexpected differences after reapplying mutation: %v", changes2)
   423  	}
   424  }
   425  
   426  func TestChangesSizeWithHardlinks(t *testing.T) {
   427  	// TODO Windows. There may be a way of running this, but turning off for now
   428  	// as createSampleDir uses symlinks.
   429  	if runtime.GOOS == "windows" {
   430  		t.Skip("hardlinks on Windows")
   431  	}
   432  	srcDir, err := ioutil.TempDir("", "storage-test-srcDir")
   433  	require.NoError(t, err)
   434  	defer os.RemoveAll(srcDir)
   435  
   436  	destDir, err := ioutil.TempDir("", "storage-test-destDir")
   437  	require.NoError(t, err)
   438  	defer os.RemoveAll(destDir)
   439  
   440  	creationSize, err := prepareUntarSourceDirectory(100, destDir, true)
   441  	require.NoError(t, err)
   442  
   443  	changes, err := ChangesDirs(destDir, &idtools.IDMappings{}, srcDir, &idtools.IDMappings{})
   444  	require.NoError(t, err)
   445  
   446  	got := ChangesSize(destDir, changes)
   447  	if got != int64(creationSize) {
   448  		t.Errorf("Expected %d bytes of changes, got %d", creationSize, got)
   449  	}
   450  }
   451  
   452  func TestChangesSizeWithNoChanges(t *testing.T) {
   453  	size := ChangesSize("/tmp", nil)
   454  	if size != 0 {
   455  		t.Fatalf("ChangesSizes with no changes should be 0, was %d", size)
   456  	}
   457  }
   458  
   459  func TestChangesSizeWithOnlyDeleteChanges(t *testing.T) {
   460  	changes := []Change{
   461  		{Path: "deletedPath", Kind: ChangeDelete},
   462  	}
   463  	size := ChangesSize("/tmp", changes)
   464  	if size != 0 {
   465  		t.Fatalf("ChangesSizes with only delete changes should be 0, was %d", size)
   466  	}
   467  }
   468  
   469  func TestChangesSize(t *testing.T) {
   470  	parentPath, err := ioutil.TempDir("", "storage-changes-test")
   471  	defer os.RemoveAll(parentPath)
   472  	addition := path.Join(parentPath, "addition")
   473  	err = ioutil.WriteFile(addition, []byte{0x01, 0x01, 0x01}, 0744)
   474  	require.NoError(t, err)
   475  	modification := path.Join(parentPath, "modification")
   476  	err = ioutil.WriteFile(modification, []byte{0x01, 0x01, 0x01}, 0744)
   477  	require.NoError(t, err)
   478  
   479  	changes := []Change{
   480  		{Path: "addition", Kind: ChangeAdd},
   481  		{Path: "modification", Kind: ChangeModify},
   482  	}
   483  	size := ChangesSize(parentPath, changes)
   484  	if size != 6 {
   485  		t.Fatalf("Expected 6 bytes of changes, got %d", size)
   486  	}
   487  }
   488  
   489  func checkChanges(expectedChanges, changes []Change, t *testing.T) {
   490  	sort.Sort(changesByPath(expectedChanges))
   491  	sort.Sort(changesByPath(changes))
   492  	for i := 0; i < max(len(changes), len(expectedChanges)); i++ {
   493  		if i >= len(expectedChanges) {
   494  			t.Fatalf("unexpected change %s\n", changes[i].String())
   495  		}
   496  		if i >= len(changes) {
   497  			t.Fatalf("no change for expected change %s\n", expectedChanges[i].String())
   498  		}
   499  		if changes[i].Path == expectedChanges[i].Path {
   500  			if changes[i] != expectedChanges[i] {
   501  				t.Fatalf("Wrong change for %s, expected %s, got %s\n", changes[i].Path, changes[i].String(), expectedChanges[i].String())
   502  			}
   503  		} else if changes[i].Path < expectedChanges[i].Path {
   504  			t.Fatalf("unexpected change %s\n", changes[i].String())
   505  		} else {
   506  			t.Fatalf("no change for expected change %s != %s\n", expectedChanges[i].String(), changes[i].String())
   507  		}
   508  	}
   509  }