github.com/docker/docker@v299999999.0.0-20200612211812-aaf470eca7b5+incompatible/pkg/archive/archive_unix_test.go (about)

     1  // +build !windows
     2  
     3  package archive // import "github.com/docker/docker/pkg/archive"
     4  
     5  import (
     6  	"bytes"
     7  	"fmt"
     8  	"io/ioutil"
     9  	"os"
    10  	"os/exec"
    11  	"path/filepath"
    12  	"strings"
    13  	"syscall"
    14  	"testing"
    15  
    16  	"github.com/docker/docker/pkg/system"
    17  	rsystem "github.com/opencontainers/runc/libcontainer/system"
    18  	"golang.org/x/sys/unix"
    19  	"gotest.tools/v3/assert"
    20  	is "gotest.tools/v3/assert/cmp"
    21  	"gotest.tools/v3/skip"
    22  )
    23  
    24  func TestCanonicalTarNameForPath(t *testing.T) {
    25  	cases := []struct{ in, expected string }{
    26  		{"foo", "foo"},
    27  		{"foo/bar", "foo/bar"},
    28  		{"foo/dir/", "foo/dir/"},
    29  	}
    30  	for _, v := range cases {
    31  		if CanonicalTarNameForPath(v.in) != v.expected {
    32  			t.Fatalf("wrong canonical tar name. expected:%s got:%s", v.expected, CanonicalTarNameForPath(v.in))
    33  		}
    34  	}
    35  }
    36  
    37  func TestCanonicalTarName(t *testing.T) {
    38  	cases := []struct {
    39  		in       string
    40  		isDir    bool
    41  		expected string
    42  	}{
    43  		{"foo", false, "foo"},
    44  		{"foo", true, "foo/"},
    45  		{"foo/bar", false, "foo/bar"},
    46  		{"foo/bar", true, "foo/bar/"},
    47  	}
    48  	for _, v := range cases {
    49  		if canonicalTarName(v.in, v.isDir) != v.expected {
    50  			t.Fatalf("wrong canonical tar name. expected:%s got:%s", v.expected, canonicalTarName(v.in, v.isDir))
    51  		}
    52  	}
    53  }
    54  
    55  func TestChmodTarEntry(t *testing.T) {
    56  	cases := []struct {
    57  		in, expected os.FileMode
    58  	}{
    59  		{0000, 0000},
    60  		{0777, 0777},
    61  		{0644, 0644},
    62  		{0755, 0755},
    63  		{0444, 0444},
    64  	}
    65  	for _, v := range cases {
    66  		if out := chmodTarEntry(v.in); out != v.expected {
    67  			t.Fatalf("wrong chmod. expected:%v got:%v", v.expected, out)
    68  		}
    69  	}
    70  }
    71  
    72  func TestTarWithHardLink(t *testing.T) {
    73  	origin, err := ioutil.TempDir("", "docker-test-tar-hardlink")
    74  	assert.NilError(t, err)
    75  	defer os.RemoveAll(origin)
    76  
    77  	err = ioutil.WriteFile(filepath.Join(origin, "1"), []byte("hello world"), 0700)
    78  	assert.NilError(t, err)
    79  
    80  	err = os.Link(filepath.Join(origin, "1"), filepath.Join(origin, "2"))
    81  	assert.NilError(t, err)
    82  
    83  	var i1, i2 uint64
    84  	i1, err = getNlink(filepath.Join(origin, "1"))
    85  	assert.NilError(t, err)
    86  
    87  	// sanity check that we can hardlink
    88  	if i1 != 2 {
    89  		t.Skipf("skipping since hardlinks don't work here; expected 2 links, got %d", i1)
    90  	}
    91  
    92  	dest, err := ioutil.TempDir("", "docker-test-tar-hardlink-dest")
    93  	assert.NilError(t, err)
    94  	defer os.RemoveAll(dest)
    95  
    96  	// we'll do this in two steps to separate failure
    97  	fh, err := Tar(origin, Uncompressed)
    98  	assert.NilError(t, err)
    99  
   100  	// ensure we can read the whole thing with no error, before writing back out
   101  	buf, err := ioutil.ReadAll(fh)
   102  	assert.NilError(t, err)
   103  
   104  	bRdr := bytes.NewReader(buf)
   105  	err = Untar(bRdr, dest, &TarOptions{Compression: Uncompressed})
   106  	assert.NilError(t, err)
   107  
   108  	i1, err = getInode(filepath.Join(dest, "1"))
   109  	assert.NilError(t, err)
   110  
   111  	i2, err = getInode(filepath.Join(dest, "2"))
   112  	assert.NilError(t, err)
   113  
   114  	assert.Check(t, is.Equal(i1, i2))
   115  }
   116  
   117  func TestTarWithHardLinkAndRebase(t *testing.T) {
   118  	tmpDir, err := ioutil.TempDir("", "docker-test-tar-hardlink-rebase")
   119  	assert.NilError(t, err)
   120  	defer os.RemoveAll(tmpDir)
   121  
   122  	origin := filepath.Join(tmpDir, "origin")
   123  	err = os.Mkdir(origin, 0700)
   124  	assert.NilError(t, err)
   125  
   126  	err = ioutil.WriteFile(filepath.Join(origin, "1"), []byte("hello world"), 0700)
   127  	assert.NilError(t, err)
   128  
   129  	err = os.Link(filepath.Join(origin, "1"), filepath.Join(origin, "2"))
   130  	assert.NilError(t, err)
   131  
   132  	var i1, i2 uint64
   133  	i1, err = getNlink(filepath.Join(origin, "1"))
   134  	assert.NilError(t, err)
   135  
   136  	// sanity check that we can hardlink
   137  	if i1 != 2 {
   138  		t.Skipf("skipping since hardlinks don't work here; expected 2 links, got %d", i1)
   139  	}
   140  
   141  	dest := filepath.Join(tmpDir, "dest")
   142  	bRdr, err := TarResourceRebase(origin, "origin")
   143  	assert.NilError(t, err)
   144  
   145  	dstDir, srcBase := SplitPathDirEntry(origin)
   146  	_, dstBase := SplitPathDirEntry(dest)
   147  	content := RebaseArchiveEntries(bRdr, srcBase, dstBase)
   148  	err = Untar(content, dstDir, &TarOptions{Compression: Uncompressed, NoLchown: true, NoOverwriteDirNonDir: true})
   149  	assert.NilError(t, err)
   150  
   151  	i1, err = getInode(filepath.Join(dest, "1"))
   152  	assert.NilError(t, err)
   153  	i2, err = getInode(filepath.Join(dest, "2"))
   154  	assert.NilError(t, err)
   155  
   156  	assert.Check(t, is.Equal(i1, i2))
   157  }
   158  
   159  func getNlink(path string) (uint64, error) {
   160  	stat, err := os.Stat(path)
   161  	if err != nil {
   162  		return 0, err
   163  	}
   164  	statT, ok := stat.Sys().(*syscall.Stat_t)
   165  	if !ok {
   166  		return 0, fmt.Errorf("expected type *syscall.Stat_t, got %t", stat.Sys())
   167  	}
   168  	// We need this conversion on ARM64
   169  	// nolint: unconvert
   170  	return uint64(statT.Nlink), nil
   171  }
   172  
   173  func getInode(path string) (uint64, error) {
   174  	stat, err := os.Stat(path)
   175  	if err != nil {
   176  		return 0, err
   177  	}
   178  	statT, ok := stat.Sys().(*syscall.Stat_t)
   179  	if !ok {
   180  		return 0, fmt.Errorf("expected type *syscall.Stat_t, got %t", stat.Sys())
   181  	}
   182  	return statT.Ino, nil
   183  }
   184  
   185  func TestTarWithBlockCharFifo(t *testing.T) {
   186  	skip.If(t, os.Getuid() != 0, "skipping test that requires root")
   187  	skip.If(t, rsystem.RunningInUserNS(), "skipping test that requires initial userns")
   188  	origin, err := ioutil.TempDir("", "docker-test-tar-hardlink")
   189  	assert.NilError(t, err)
   190  
   191  	defer os.RemoveAll(origin)
   192  	err = ioutil.WriteFile(filepath.Join(origin, "1"), []byte("hello world"), 0700)
   193  	assert.NilError(t, err)
   194  
   195  	err = system.Mknod(filepath.Join(origin, "2"), unix.S_IFBLK, int(system.Mkdev(int64(12), int64(5))))
   196  	assert.NilError(t, err)
   197  	err = system.Mknod(filepath.Join(origin, "3"), unix.S_IFCHR, int(system.Mkdev(int64(12), int64(5))))
   198  	assert.NilError(t, err)
   199  	err = system.Mknod(filepath.Join(origin, "4"), unix.S_IFIFO, int(system.Mkdev(int64(12), int64(5))))
   200  	assert.NilError(t, err)
   201  
   202  	dest, err := ioutil.TempDir("", "docker-test-tar-hardlink-dest")
   203  	assert.NilError(t, err)
   204  	defer os.RemoveAll(dest)
   205  
   206  	// we'll do this in two steps to separate failure
   207  	fh, err := Tar(origin, Uncompressed)
   208  	assert.NilError(t, err)
   209  
   210  	// ensure we can read the whole thing with no error, before writing back out
   211  	buf, err := ioutil.ReadAll(fh)
   212  	assert.NilError(t, err)
   213  
   214  	bRdr := bytes.NewReader(buf)
   215  	err = Untar(bRdr, dest, &TarOptions{Compression: Uncompressed})
   216  	assert.NilError(t, err)
   217  
   218  	changes, err := ChangesDirs(origin, dest)
   219  	assert.NilError(t, err)
   220  
   221  	if len(changes) > 0 {
   222  		t.Fatalf("Tar with special device (block, char, fifo) should keep them (recreate them when untar) : %v", changes)
   223  	}
   224  }
   225  
   226  // TestTarUntarWithXattr is Unix as Lsetxattr is not supported on Windows
   227  func TestTarUntarWithXattr(t *testing.T) {
   228  	skip.If(t, os.Getuid() != 0, "skipping test that requires root")
   229  	if _, err := exec.LookPath("setcap"); err != nil {
   230  		t.Skip("setcap not installed")
   231  	}
   232  	if _, err := exec.LookPath("getcap"); err != nil {
   233  		t.Skip("getcap not installed")
   234  	}
   235  
   236  	origin, err := ioutil.TempDir("", "docker-test-untar-origin")
   237  	assert.NilError(t, err)
   238  	defer os.RemoveAll(origin)
   239  	err = ioutil.WriteFile(filepath.Join(origin, "1"), []byte("hello world"), 0700)
   240  	assert.NilError(t, err)
   241  
   242  	err = ioutil.WriteFile(filepath.Join(origin, "2"), []byte("welcome!"), 0700)
   243  	assert.NilError(t, err)
   244  	err = ioutil.WriteFile(filepath.Join(origin, "3"), []byte("will be ignored"), 0700)
   245  	assert.NilError(t, err)
   246  	// there is no known Go implementation of setcap/getcap with support for v3 file capability
   247  	out, err := exec.Command("setcap", "cap_block_suspend+ep", filepath.Join(origin, "2")).CombinedOutput()
   248  	assert.NilError(t, err, string(out))
   249  
   250  	for _, c := range []Compression{
   251  		Uncompressed,
   252  		Gzip,
   253  	} {
   254  		changes, err := tarUntar(t, origin, &TarOptions{
   255  			Compression:     c,
   256  			ExcludePatterns: []string{"3"},
   257  		})
   258  
   259  		if err != nil {
   260  			t.Fatalf("Error tar/untar for compression %s: %s", c.Extension(), err)
   261  		}
   262  
   263  		if len(changes) != 1 || changes[0].Path != "/3" {
   264  			t.Fatalf("Unexpected differences after tarUntar: %v", changes)
   265  		}
   266  		out, err := exec.Command("getcap", filepath.Join(origin, "2")).CombinedOutput()
   267  		assert.NilError(t, err, string(out))
   268  		assert.Check(t, is.Contains(string(out), "= cap_block_suspend+ep"), "untar should have kept the 'security.capability' xattr")
   269  	}
   270  }
   271  
   272  func TestCopyInfoDestinationPathSymlink(t *testing.T) {
   273  	tmpDir, _ := getTestTempDirs(t)
   274  	defer removeAllPaths(tmpDir)
   275  
   276  	root := strings.TrimRight(tmpDir, "/") + "/"
   277  
   278  	type FileTestData struct {
   279  		resource FileData
   280  		file     string
   281  		expected CopyInfo
   282  	}
   283  
   284  	testData := []FileTestData{
   285  		// Create a directory: /tmp/archive-copy-test*/dir1
   286  		// Test will "copy" file1 to dir1
   287  		{resource: FileData{filetype: Dir, path: "dir1", permissions: 0740}, file: "file1", expected: CopyInfo{Path: root + "dir1/file1", Exists: false, IsDir: false}},
   288  
   289  		// Create a symlink directory to dir1: /tmp/archive-copy-test*/dirSymlink -> dir1
   290  		// Test will "copy" file2 to dirSymlink
   291  		{resource: FileData{filetype: Symlink, path: "dirSymlink", contents: root + "dir1", permissions: 0600}, file: "file2", expected: CopyInfo{Path: root + "dirSymlink/file2", Exists: false, IsDir: false}},
   292  
   293  		// Create a file in tmp directory: /tmp/archive-copy-test*/file1
   294  		// Test to cover when the full file path already exists.
   295  		{resource: FileData{filetype: Regular, path: "file1", permissions: 0600}, file: "", expected: CopyInfo{Path: root + "file1", Exists: true}},
   296  
   297  		// Create a directory: /tmp/archive-copy*/dir2
   298  		// Test to cover when the full directory path already exists
   299  		{resource: FileData{filetype: Dir, path: "dir2", permissions: 0740}, file: "", expected: CopyInfo{Path: root + "dir2", Exists: true, IsDir: true}},
   300  
   301  		// Create a symlink to a non-existent target: /tmp/archive-copy*/symlink1 -> noSuchTarget
   302  		// Negative test to cover symlinking to a target that does not exit
   303  		{resource: FileData{filetype: Symlink, path: "symlink1", contents: "noSuchTarget", permissions: 0600}, file: "", expected: CopyInfo{Path: root + "noSuchTarget", Exists: false}},
   304  
   305  		// Create a file in tmp directory for next test: /tmp/existingfile
   306  		{resource: FileData{filetype: Regular, path: "existingfile", permissions: 0600}, file: "", expected: CopyInfo{Path: root + "existingfile", Exists: true}},
   307  
   308  		// Create a symlink to an existing file: /tmp/archive-copy*/symlink2 -> /tmp/existingfile
   309  		// Test to cover when the parent directory of a new file is a symlink
   310  		{resource: FileData{filetype: Symlink, path: "symlink2", contents: "existingfile", permissions: 0600}, file: "", expected: CopyInfo{Path: root + "existingfile", Exists: true}},
   311  	}
   312  
   313  	var dirs []FileData
   314  	for _, data := range testData {
   315  		dirs = append(dirs, data.resource)
   316  	}
   317  	provisionSampleDir(t, tmpDir, dirs)
   318  
   319  	for _, info := range testData {
   320  		p := filepath.Join(tmpDir, info.resource.path, info.file)
   321  		ci, err := CopyInfoDestinationPath(p)
   322  		assert.Check(t, err)
   323  		assert.Check(t, is.DeepEqual(info.expected, ci))
   324  	}
   325  }