diff --git a/archive/tar.go b/archive/tar.go index fae023c55..7b1732c38 100644 --- a/archive/tar.go +++ b/archive/tar.go @@ -19,9 +19,7 @@ package archive import ( "archive/tar" "context" - "fmt" "io" - "io/ioutil" "os" "path/filepath" "runtime" @@ -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" @@ -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. @@ -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) { + 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 { @@ -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. @@ -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 } @@ -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 +} diff --git a/archive/tar_opts.go b/archive/tar_opts.go index a08bc102a..970c0f658 100644 --- a/archive/tar_opts.go +++ b/archive/tar_opts.go @@ -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 @@ -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 + } +} diff --git a/archive/tar_opts_linux.go b/archive/tar_opts_linux.go new file mode 100644 index 000000000..07ef64655 --- /dev/null +++ b/archive/tar_opts_linux.go @@ -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 +} diff --git a/archive/tar_opts_unix.go b/archive/tar_opts_other.go similarity index 83% rename from archive/tar_opts_unix.go rename to archive/tar_opts_other.go index 173826967..03c24cf8b 100644 --- a/archive/tar_opts_unix.go +++ b/archive/tar_opts_other.go @@ -1,4 +1,4 @@ -// +build !windows +// +build !linux,!windows /* Copyright The containerd Authors. @@ -20,5 +20,6 @@ package archive // ApplyOptions provides additional options for an Apply operation type ApplyOptions struct { - Filter Filter // Filter tar headers + Filter Filter // Filter tar headers + ConvertWhiteout ConvertWhiteout // Convert whiteout files } diff --git a/archive/tar_opts_windows.go b/archive/tar_opts_windows.go index e4b15a163..10d1453a2 100644 --- a/archive/tar_opts_windows.go +++ b/archive/tar_opts_windows.go @@ -20,9 +20,10 @@ package archive // ApplyOptions provides additional options for an Apply operation type ApplyOptions struct { - ParentLayerPaths []string // Parent layer paths used for Windows layer apply - IsWindowsContainerLayer bool // True if the tar stream to be applied is a Windows Container Layer - Filter Filter // Filter tar headers + ParentLayerPaths []string // Parent layer paths used for Windows layer apply + IsWindowsContainerLayer bool // True if the tar stream to be applied is a Windows Container Layer + Filter Filter // Filter tar headers + ConvertWhiteout ConvertWhiteout // Convert whiteout files } // WithParentLayers adds parent layers to the apply process this is required diff --git a/diff/apply/apply.go b/diff/apply/apply.go index 7a6b65c3e..97c065d8c 100644 --- a/diff/apply/apply.go +++ b/diff/apply/apply.go @@ -19,10 +19,8 @@ package apply import ( "context" "io" - "io/ioutil" "time" - "github.com/containerd/containerd/archive" "github.com/containerd/containerd/content" "github.com/containerd/containerd/diff" "github.com/containerd/containerd/log" @@ -94,15 +92,8 @@ func (s *fsApplier) Apply(ctx context.Context, desc ocispec.Descriptor, mounts [ rc := &readCounter{ r: io.TeeReader(processor, digester.Hash()), } - if err := mount.WithTempMount(ctx, mounts, func(root string) error { - if _, err := archive.Apply(ctx, root, rc); err != nil { - return err - } - // Read any trailing data - _, err := io.Copy(ioutil.Discard, rc) - return err - }); err != nil { + if err := apply(ctx, mounts, rc); err != nil { return emptyDesc, err } diff --git a/diff/apply/apply_linux.go b/diff/apply/apply_linux.go new file mode 100644 index 000000000..d055298b3 --- /dev/null +++ b/diff/apply/apply_linux.go @@ -0,0 +1,90 @@ +// +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 apply + +import ( + "context" + "io" + "strings" + + "github.com/containerd/containerd/archive" + "github.com/containerd/containerd/mount" + "github.com/pkg/errors" +) + +func apply(ctx context.Context, mounts []mount.Mount, r io.Reader) error { + switch { + case len(mounts) == 1 && mounts[0].Type == "overlay": + path, err := getOverlayPath(mounts[0].Options) + if err != nil { + return err + } + _, err = archive.Apply(ctx, path, r, + archive.WithConvertWhiteout(archive.OverlayConvertWhiteout)) + return err + case len(mounts) == 1 && mounts[0].Type == "aufs": + path, err := getAufsPath(mounts[0].Options) + if err != nil { + return err + } + _, err = archive.Apply(ctx, path, r, + archive.WithConvertWhiteout(archive.AufsConvertWhiteout)) + return err + default: + return mount.WithTempMount(ctx, mounts, func(root string) error { + _, err := archive.Apply(ctx, root, r) + return err + }) + } +} + +func getOverlayPath(options []string) (string, error) { + const upperdirPrefix = "upperdir=" + for _, o := range options { + if strings.HasPrefix(o, upperdirPrefix) { + return strings.TrimPrefix(o, upperdirPrefix), nil + } + } + return "", errors.New("upperdir not found") +} + +func getAufsPath(options []string) (string, error) { + const ( + sep = ":" + brPrefix1 = "br:" + brPrefix2 = "br=" + rwSuffix = "=rw" + ) + for _, o := range options { + if strings.HasPrefix(o, brPrefix1) { + o = strings.TrimPrefix(o, brPrefix1) + } else if strings.HasPrefix(o, brPrefix2) { + o = strings.TrimPrefix(o, brPrefix2) + } else { + continue + } + for _, b := range strings.Split(o, sep) { + if strings.HasSuffix(b, rwSuffix) { + return strings.TrimSuffix(b, rwSuffix), nil + } + } + break + } + return "", errors.New("rw branch not found") +} diff --git a/diff/apply/apply_linux_test.go b/diff/apply/apply_linux_test.go new file mode 100644 index 000000000..ee7c7a5fd --- /dev/null +++ b/diff/apply/apply_linux_test.go @@ -0,0 +1,79 @@ +// +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 apply + +import ( + "testing" +) + +func TestGetOverlayPath(t *testing.T) { + good := []string{"upperdir=/test/upper", "lowerdir=/test/lower", "workdir=/test/work"} + path, err := getOverlayPath(good) + if err != nil { + t.Fatalf("Get overlay path failed: %v", err) + } + if path != "/test/upper" { + t.Fatalf("Unexpected upperdir: %q", path) + } + + bad := []string{"lowerdir=/test/lower"} + _, err = getOverlayPath(bad) + if err == nil { + t.Fatalf("An error is expected") + } +} + +func TestGetAufsPath(t *testing.T) { + rwDir := "/test/rw" + for _, test := range []struct { + options []string + expectErr bool + }{ + { + options: []string{"random:option", "br:" + rwDir + "=rw:/test/ro=ro+wh"}, + expectErr: false, + }, + { + options: []string{"random:option", "br=" + rwDir + "=rw:/test/ro=ro+wh"}, + expectErr: false, + }, + { + options: []string{"random:option"}, + expectErr: true, + }, + { + options: []string{"br:/test/ro=ro+wh"}, + expectErr: true, + }, + } { + path, err := getAufsPath(test.options) + if test.expectErr { + if err == nil { + t.Fatalf("An error is expected") + } + continue + } + if err != nil { + t.Fatalf("Get aufs path failed: %v", err) + } + if path != rwDir { + t.Fatalf("Unexpected rw dir: %q", path) + } + } +} diff --git a/diff/apply/apply_other.go b/diff/apply/apply_other.go new file mode 100644 index 000000000..01e0f11bb --- /dev/null +++ b/diff/apply/apply_other.go @@ -0,0 +1,34 @@ +// +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 apply + +import ( + "context" + "io" + + "github.com/containerd/containerd/archive" + "github.com/containerd/containerd/mount" +) + +func apply(ctx context.Context, mounts []mount.Mount, r io.Reader) error { + return mount.WithTempMount(ctx, mounts, func(root string) error { + _, err := archive.Apply(ctx, root, r) + return err + }) +}