Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
122 changes: 46 additions & 76 deletions archive/tar.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,9 +19,7 @@ package archive
import (
"archive/tar"
"context"
"fmt"
"io"
"io/ioutil"
"os"
"path/filepath"
"runtime"
Expand Down Expand Up @@ -91,11 +89,6 @@ const (
// archives.
whiteoutMetaPrefix = whiteoutPrefix + whiteoutPrefix

// whiteoutLinkDir is a directory AUFS uses for storing hardlink links to other
// layers. Normally these should not go into exported archives and all changed
// hardlinks should be copied to the top layer.
whiteoutLinkDir = whiteoutMetaPrefix + "plnk"

// whiteoutOpaqueDir file means directory has been made opaque - meaning
// readdir calls to this directory do not follow to lower layers.
whiteoutOpaqueDir = whiteoutMetaPrefix + ".opq"
Expand Down Expand Up @@ -130,10 +123,6 @@ func applyNaive(ctx context.Context, root string, tr *tar.Reader, options ApplyO
// Used for handling opaque directory markers which
// may occur out of order
unpackedPaths = make(map[string]struct{})

// Used for aufs plink directory
aufsTempdir = ""
aufsHardlinks = make(map[string]*tar.Header)
)

// Iterate through the files in the archive.
Expand Down Expand Up @@ -201,40 +190,15 @@ func applyNaive(ctx context.Context, root string, tr *tar.Reader, options ApplyO
}
}

// Skip AUFS metadata dirs
if strings.HasPrefix(hdr.Name, whiteoutMetaPrefix) {
// Regular files inside /.wh..wh.plnk can be used as hardlink targets
// We don't want this directory, but we need the files in them so that
// such hardlinks can be resolved.
if strings.HasPrefix(hdr.Name, whiteoutLinkDir) && hdr.Typeflag == tar.TypeReg {
basename := filepath.Base(hdr.Name)
aufsHardlinks[basename] = hdr
if aufsTempdir == "" {
if aufsTempdir, err = ioutil.TempDir(os.Getenv("XDG_RUNTIME_DIR"), "dockerplnk"); err != nil {
return 0, err
}
defer os.RemoveAll(aufsTempdir)
}
p, err := fs.RootPath(aufsTempdir, basename)
if err != nil {
return 0, err
}
if err := createTarFile(ctx, p, root, hdr, tr); err != nil {
return 0, err
}
}

if hdr.Name != whiteoutOpaqueDir {
continue
}
}

if strings.HasPrefix(base, whiteoutPrefix) {
// Naive whiteout convert function which handles whiteout files by
// removing the target files.
convertWhiteout := func(hdr *tar.Header, path string) (bool, error) {
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we split this to a new function in the global scope

base := filepath.Base(path)
dir := filepath.Dir(path)
if base == whiteoutOpaqueDir {
_, err := os.Lstat(dir)
if err != nil {
return 0, err
return false, err
}
err = filepath.Walk(dir, func(path string, info os.FileInfo, err error) error {
if err != nil {
Expand All @@ -252,26 +216,29 @@ func applyNaive(ctx context.Context, root string, tr *tar.Reader, options ApplyO
}
return nil
})
if err != nil {
return 0, err
}
continue
return false, err
}

originalBase := base[len(whiteoutPrefix):]
originalPath := filepath.Join(dir, originalBase)
if strings.HasPrefix(base, whiteoutPrefix) {
originalBase := base[len(whiteoutPrefix):]
originalPath := filepath.Join(dir, originalBase)

// Ensure originalPath is under dir
if dir[len(dir)-1] != filepath.Separator {
dir += string(filepath.Separator)
}
if !strings.HasPrefix(originalPath, dir) {
return 0, errors.Wrapf(errInvalidArchive, "invalid whiteout name: %v", base)
return false, os.RemoveAll(originalPath)
}

if err := os.RemoveAll(originalPath); err != nil {
return 0, err
}
return true, nil
}
if options.ConvertWhiteout != nil {
convertWhiteout = options.ConvertWhiteout
}
if err := validateWhiteout(path); err != nil {
return 0, err
}
writeFile, err := convertWhiteout(hdr, path)
if err != nil {
return 0, errors.Wrapf(err, "failed to convert whiteout file %q", hdr.Name)
}
if !writeFile {
continue
}
// If path exits we almost always just want to remove and replace it.
Expand All @@ -289,26 +256,6 @@ func applyNaive(ctx context.Context, root string, tr *tar.Reader, options ApplyO
srcData := io.Reader(tr)
srcHdr := hdr

// Hard links into /.wh..wh.plnk don't work, as we don't extract that directory, so
// we manually retarget these into the temporary files we extracted them into
if hdr.Typeflag == tar.TypeLink && strings.HasPrefix(filepath.Clean(hdr.Linkname), whiteoutLinkDir) {
linkBasename := filepath.Base(hdr.Linkname)
srcHdr = aufsHardlinks[linkBasename]
if srcHdr == nil {
return 0, fmt.Errorf("invalid aufs hardlink")
}
p, err := fs.RootPath(aufsTempdir, linkBasename)
if err != nil {
return 0, err
}
tmpFile, err := os.Open(p)
if err != nil {
return 0, err
}
defer tmpFile.Close()
srcData = tmpFile
}

if err := createTarFile(ctx, path, root, srcHdr, srcData); err != nil {
return 0, err
}
Expand Down Expand Up @@ -684,3 +631,26 @@ func hardlinkRootPath(root, linkname string) (string, error) {
}
return targetPath, nil
}

func validateWhiteout(path string) error {
base := filepath.Base(path)
dir := filepath.Dir(path)

if base == whiteoutOpaqueDir {
return nil
}

if strings.HasPrefix(base, whiteoutPrefix) {
originalBase := base[len(whiteoutPrefix):]
originalPath := filepath.Join(dir, originalBase)

// Ensure originalPath is under dir
if dir[len(dir)-1] != filepath.Separator {
dir += string(filepath.Separator)
}
if !strings.HasPrefix(originalPath, dir) {
return errors.Wrapf(errInvalidArchive, "invalid whiteout name: %v", base)
}
}
return nil
}
122 changes: 122 additions & 0 deletions archive/tar_linux_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,122 @@
// +build linux

/*
Copyright The containerd Authors.

Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at

http://www.apache.org/licenses/LICENSE-2.0

Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/

package archive

import (
"context"
"fmt"
"io"
"io/ioutil"
"os"
"testing"

"github.com/containerd/aufs"
"github.com/containerd/containerd/archive/tartest"
"github.com/containerd/containerd/mount"
"github.com/containerd/containerd/pkg/testutil"
"github.com/containerd/containerd/snapshots/overlay"
"github.com/containerd/continuity/fs/fstest"
)

func runWriterToTarTest(t *testing.T, name string, wt tartest.WriterToTar, a fstest.Applier, validate func(string) error, applyErr error) {
t.Run(name, makeWriterToTarNaiveTest(wt, a, validate, applyErr))
t.Run(name+"Overlay", makeWriterToTarOverlayTest(wt, a, validate, applyErr))
t.Run(name+"Aufs", makeWriterToTarAufsTest(wt, a, validate, applyErr))
}

func makeWriterToTarOverlayTest(wt tartest.WriterToTar, a fstest.Applier, validate func(string) error, applyErr error) func(*testing.T) {
return func(t *testing.T) {
testutil.RequiresRoot(t)
if err := overlay.Supported("/"); err != nil {
t.Skipf("skipping because overlay is not supported %v", err)
}
ud, err := ioutil.TempDir("", "test-writer-to-tar-overlay-upper")
if err != nil {
t.Fatalf("Failed to create temp dir: %v", err)
}
defer os.RemoveAll(ud)
wd, err := ioutil.TempDir("", "test-writer-to-tar-overlay-work")
if err != nil {
t.Fatalf("Failed to create temp dir: %v", err)
}
defer os.RemoveAll(wd)

apply := func(ctx context.Context, _ string, tr io.Reader) error {
_, err := Apply(ctx, ud, tr, WithConvertWhiteout(OverlayConvertWhiteout))
return err
}

validate := func(td string) error {
mounts := []mount.Mount{
{
Type: "overlay",
Source: "overlay",
Options: []string{
fmt.Sprintf("workdir=%s", wd),
fmt.Sprintf("upperdir=%s", ud),
fmt.Sprintf("lowerdir=%s", td),
},
},
}

return mount.WithTempMount(context.Background(), mounts, func(root string) error {
return validate(root)
})
}

makeWriterToTarTest(wt, a, apply, validate, applyErr)(t)
}
}

func makeWriterToTarAufsTest(wt tartest.WriterToTar, a fstest.Applier, validate func(string) error, applyErr error) func(*testing.T) {
return func(t *testing.T) {
testutil.RequiresRoot(t)
if err := aufs.Supported(); err != nil {
t.Skipf("skipping because aufs is not supported %v", err)
}
d, err := ioutil.TempDir("", "test-writer-to-tar-aufs-rw")
if err != nil {
t.Fatalf("Failed to create temp dir: %v", err)
}
defer os.RemoveAll(d)

apply := func(ctx context.Context, _ string, tr io.Reader) error {
_, err := Apply(ctx, d, tr, WithConvertWhiteout(AufsConvertWhiteout))
return err
}

validate := func(td string) error {
mounts := []mount.Mount{
{
Type: "aufs",
Source: "none",
Options: []string{
fmt.Sprintf("br:%s=rw:%s=ro+wh", d, td),
},
},
}

return mount.WithTempMount(context.Background(), mounts, func(root string) error {
return validate(root)
})
}

makeWriterToTarTest(wt, a, apply, validate, applyErr)(t)
}
}
11 changes: 11 additions & 0 deletions archive/tar_opts.go
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,9 @@ type ApplyOpt func(options *ApplyOptions) error
// Filter specific files from the archive
type Filter func(*tar.Header) (bool, error)

// ConvertWhiteout converts whiteout files from the archive
type ConvertWhiteout func(*tar.Header, string) (bool, error)

// all allows all files
func all(_ *tar.Header) (bool, error) {
return true, nil
Expand All @@ -36,3 +39,11 @@ func WithFilter(f Filter) ApplyOpt {
return nil
}
}

// WithConvertWhiteout uses the convert function to convert the whiteout files.
func WithConvertWhiteout(c ConvertWhiteout) ApplyOpt {
return func(options *ApplyOptions) error {
options.ConvertWhiteout = c
return nil
}
}
65 changes: 65 additions & 0 deletions archive/tar_opts_linux.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
// +build linux

/*
Copyright The containerd Authors.

Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at

http://www.apache.org/licenses/LICENSE-2.0

Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/

package archive

import (
"archive/tar"
"os"
"path/filepath"
"strings"

"golang.org/x/sys/unix"
)

// ApplyOptions provides additional options for an Apply operation
type ApplyOptions struct {
Filter Filter // Filter tar headers
ConvertWhiteout ConvertWhiteout // Convert whiteout files
}

// AufsConvertWhiteout converts whiteout files for aufs.
func AufsConvertWhiteout(_ *tar.Header, _ string) (bool, error) {
return true, nil
}

// OverlayConvertWhiteout converts whiteout files for overlay.
func OverlayConvertWhiteout(hdr *tar.Header, path string) (bool, error) {
base := filepath.Base(path)
dir := filepath.Dir(path)

// if a directory is marked as opaque, we need to translate that to overlay
if base == whiteoutOpaqueDir {
// don't write the file itself
return false, unix.Setxattr(dir, "trusted.overlay.opaque", []byte{'y'}, 0)
}

// if a file was deleted and we are using overlay, we need to create a character device
if strings.HasPrefix(base, whiteoutPrefix) {
originalBase := base[len(whiteoutPrefix):]
originalPath := filepath.Join(dir, originalBase)

if err := unix.Mknod(originalPath, unix.S_IFCHR, 0); err != nil {
return false, err
}
// don't write the file itself
return false, os.Chown(originalPath, hdr.Uid, hdr.Gid)
}

return true, nil
}
Loading