VMware vSphere Integrated Containers provider (#206)

* Add Virtual Kubelet provider for VIC

Initial virtual kubelet provider for VMware VIC.  This provider currently
handles creating and starting of a pod VM via the VIC portlayer and persona
server.  Image store handling via the VIC persona server.  This provider
currently requires the feature/wolfpack branch of VIC.

* Added pod stop and delete.  Also added node capacity.

Added the ability to stop and delete pod VMs via VIC.  Also retrieve
node capacity information from the VCH.

* Cleanup and readme file

Some file clean up and added a Readme.md markdown file for the VIC
provider.

* Cleaned up errors, added function comments, moved operation code

1. Cleaned up error handling.  Set standard for creating errors.
2. Added method prototype comments for all interface functions.
3. Moved PodCreator, PodStarter, PodStopper, and PodDeleter to a new folder.

* Add mocking code and unit tests for podcache, podcreator, and podstarter

Used the unit test framework used in VIC to handle assertions in the provider's
unit test.  Mocking code generated using OSS project mockery, which is compatible
with the testify assertion framework.

* Vendored packages for the VIC provider

Requires feature/wolfpack branch of VIC and a few specific commit sha of
projects used within VIC.

* Implementation of POD Stopper and Deleter unit tests (#4)

* Updated files for initial PR
This commit is contained in:
Loc Nguyen
2018-06-04 15:41:32 -07:00
committed by Ria Bhatia
parent 98a111e8b7
commit 513cebe7b7
6296 changed files with 1123685 additions and 8 deletions

View File

@@ -0,0 +1,135 @@
// Copyright 2017 VMware, Inc. All Rights Reserved.
//
// 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.
// NOTE: This file is borrowed directly from docker in order to use unexported
// functions that it provides.
// https://github.com/moby/moby/blob/master/pkg/archive/archive_unix.go
// +build !windows
package archive
import (
"archive/tar"
"errors"
"os"
"path/filepath"
"syscall"
"github.com/docker/docker/pkg/system"
)
// fixVolumePathPrefix does platform specific processing to ensure that if
// the path being passed in is not in a volume path format, convert it to one.
func fixVolumePathPrefix(srcPath string) string {
return srcPath
}
// getWalkRoot calculates the root path when performing a TarWithOptions.
// We use a separate function as this is platform specific. On Linux, we
// can't use filepath.Join(srcPath,include) because this will clean away
// a trailing "." or "/" which may be important.
func getWalkRoot(srcPath string, include string) string {
return srcPath + string(filepath.Separator) + include
}
// CanonicalTarNameForPath returns platform-specific filepath
// to canonical posix-style path for tar archival. p is relative
// path.
func CanonicalTarNameForPath(p string) (string, error) {
return p, nil // already unix-style
}
// chmodTarEntry is used to adjust the file permissions used in tar header based
// on the platform the archival is done.
func chmodTarEntry(perm os.FileMode) os.FileMode {
return perm // noop for unix as golang APIs provide perm bits correctly
}
func getFileUIDGID(stat interface{}) (int, int, error) {
s, ok := stat.(*syscall.Stat_t)
if !ok {
return -1, -1, errors.New("cannot convert stat value to syscall.Stat_t")
}
return int(s.Uid), int(s.Gid), nil
}
func major(device uint64) uint64 {
return (device >> 8) & 0xfff
}
func minor(device uint64) uint64 {
return (device & 0xff) | ((device >> 12) & 0xfff00)
}
// handleTarTypeBlockCharFifo is an OS-specific helper function used by
// createTarFile to handle the following types of header: Block; Char; Fifo
func handleTarTypeBlockCharFifo(hdr *tar.Header, path string) error {
mode := uint32(hdr.Mode & 07777)
switch hdr.Typeflag {
case tar.TypeBlock:
mode |= syscall.S_IFBLK
case tar.TypeChar:
mode |= syscall.S_IFCHR
case tar.TypeFifo:
mode |= syscall.S_IFIFO
}
if err := system.Mknod(path, mode, int(system.Mkdev(hdr.Devmajor, hdr.Devminor))); err != nil {
return err
}
return nil
}
func handleLChmod(hdr *tar.Header, path string, hdrInfo os.FileInfo) error {
if hdr.Typeflag == tar.TypeLink {
if fi, err := os.Lstat(hdr.Linkname); err == nil && (fi.Mode()&os.ModeSymlink == 0) {
if err := os.Chmod(path, hdrInfo.Mode()); err != nil {
return err
}
}
} else if hdr.Typeflag != tar.TypeSymlink {
if err := os.Chmod(path, hdrInfo.Mode()); err != nil {
return err
}
}
return nil
}
func setHeaderForSpecialDevice(hdr *tar.Header, stat interface{}) (inode uint64, err error) {
s, ok := stat.(*syscall.Stat_t)
if !ok {
err = errors.New("cannot convert stat value to syscall.Stat_t")
return
}
inode = uint64(s.Ino)
// Currently go does not fill in the major/minors
if s.Mode&syscall.S_IFBLK != 0 ||
s.Mode&syscall.S_IFCHR != 0 {
hdr.Devmajor = int64(major(uint64(s.Rdev)))
hdr.Devminor = int64(minor(uint64(s.Rdev)))
}
return
}
func hasHardlinks(fi os.FileInfo) bool {
return fi.Sys().(*syscall.Stat_t).Nlink > 1
}

View File

@@ -0,0 +1,88 @@
// Copyright 2017 VMware, Inc. All Rights Reserved.
//
// 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.
// NOTE: This file is borrowed directly from docker in order to use unexported
// functions that it provides.
// https://github.com/moby/moby/blob/master/pkg/archive/archive_unix.go
// +build windows
package archive
import (
"archive/tar"
"fmt"
"os"
"path/filepath"
"strings"
"github.com/docker/docker/pkg/longpath"
)
// fixVolumePathPrefix does platform specific processing to ensure that if
// the path being passed in is not in a volume path format, convert it to one.
func fixVolumePathPrefix(srcPath string) string {
return longpath.AddPrefix(srcPath)
}
// getWalkRoot calculates the root path when performing a TarWithOptions.
// We use a separate function as this is platform specific.
func getWalkRoot(srcPath string, include string) string {
return filepath.Join(srcPath, include)
}
// CanonicalTarNameForPath returns platform-specific filepath
// to canonical posix-style path for tar archival. p is relative
// path.
func CanonicalTarNameForPath(p string) (string, error) {
// windows: convert windows style relative path with backslashes
// into forward slashes. Since windows does not allow '/' or '\'
// in file names, it is mostly safe to replace however we must
// check just in case
if strings.Contains(p, "/") {
return "", fmt.Errorf("Windows path contains forward slash: %s", p)
}
return strings.Replace(p, string(os.PathSeparator), "/", -1), nil
}
// chmodTarEntry is used to adjust the file permissions used in tar header based
// on the platform the archival is done.
func chmodTarEntry(perm os.FileMode) os.FileMode {
perm &= 0755
// Add the x bit: make everything +x from windows
perm |= 0111
return perm
}
func setHeaderForSpecialDevice(hdr *tar.Header, stat interface{}) (inode uint64, err error) {
// do nothing. no notion of Rdev, Inode, Nlink in stat on Windows
return
}
// handleTarTypeBlockCharFifo is an OS-specific helper function used by
// createTarFile to handle the following types of header: Block; Char; Fifo
func handleTarTypeBlockCharFifo(hdr *tar.Header, path string) error {
return nil
}
func handleLChmod(hdr *tar.Header, path string, hdrInfo os.FileInfo) error {
return nil
}
func getFileUIDGID(stat interface{}) (int, int, error) {
// no notion of file ownership mapping yet on Windows
return 0, 0, nil
}

196
vendor/github.com/vmware/vic/lib/archive/archiver.go generated vendored Normal file
View File

@@ -0,0 +1,196 @@
// Copyright 2017 VMware, Inc. All Rights Reserved.
//
// 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 (
"encoding/base64"
"encoding/json"
"fmt"
"io"
"net/url"
"strings"
"github.com/vmware/vic/pkg/trace"
)
// FilterType specifies what type of filter to apply in a FilterSpec
type FilterType int
const (
// Include specifies a path inclusion.
// Note: An included path that sits under an excluded path should be included.
// Example: if / is excluded, and /files is included, /files should be added to the archive.
Include = iota
// Exclude specifies a path exclusion.
Exclude
// Rebase specifies a path rebase.
// The rebase path is prepended to the path in the archive header.
// for an Export this will ensure proper headers on the way out.
// for an Import it will ensure that the tar unpacking occurs in
// right location
Rebase
// Strip specifies a path strip.
// The inverse of a rebase, the path is stripped from the header path
// before writing to disk.
Strip
)
// FilterSpec describes rules for handling specified paths during archival
type FilterSpec struct {
Inclusions map[string]struct{}
Exclusions map[string]struct{}
RebasePath string
StripPath string
}
// Archiver defines an API for creating archives consisting of data that
// exist between a child and parent layer, as well as unpacking archives
// to container filesystems
type Archiver interface {
// Export reads the delta between child and ancestor layers, returning
// the difference as a tar archive.
//
// store - the store containing the two layers
// id - must inherit from ancestor if ancestor is specified
// ancestor - the old layer against which to diff. If omitted, the entire filesystem will be included
// spec - describes filters on paths found in the data (include, exclude, rebase, strip)
// data - include file data in the tar archive if true, headers only otherwise
Export(op trace.Operation, store *url.URL, id, ancestor string, spec *FilterSpec, data bool) (io.ReadCloser, error)
// Import will process the input tar stream based on the supplied path spec and write the stream to the
// target device.
//
// store - the device store containing the target device
// id - the id for the device that is contained within the store
// spec - describes filters on paths found in the data (include, exclude, rebase, strip)
// tarstream - the tar stream that is to be written to the target on the device
Import(op trace.Operation, store *url.URL, id string, spec *FilterSpec, tarstream io.ReadCloser) error
}
// CreateFilterSpec creates a FilterSpec from a supplied map
func CreateFilterSpec(op trace.Operation, spec map[string]FilterType) (*FilterSpec, error) {
fs := &FilterSpec{
Inclusions: make(map[string]struct{}),
Exclusions: make(map[string]struct{}),
}
if spec == nil {
return fs, nil
}
for k, v := range spec {
switch v {
case Include:
fs.Inclusions[k] = struct{}{}
case Exclude:
fs.Exclusions[k] = struct{}{}
case Rebase:
if fs.RebasePath != "" {
return nil, fmt.Errorf("error creating filter spec: only one rebase path allowed")
}
fs.RebasePath = k
case Strip:
if fs.StripPath != "" {
return nil, fmt.Errorf("error creating filter spec: only one strip path allowed")
}
fs.StripPath = k
default:
return nil, fmt.Errorf("invalid filter specification: %d", v)
}
}
return fs, nil
}
// Decodes a base64 encoded string from EncodeFilterSpec into a FilterSpec
func DecodeFilterSpec(op trace.Operation, spec *string) (*FilterSpec, error) {
var filterSpec FilterSpec
// empty spec means don't apply any filtering
if spec != nil && len(*spec) > 0 {
decoded, err := base64.StdEncoding.DecodeString(*spec)
if err != nil {
op.Errorf("Unable to decode filter spec: %s", err)
return nil, err
}
op.Debugf("decoded spec: %+s", string(decoded))
if len(decoded) > 0 {
if err = json.Unmarshal(decoded, &filterSpec); err != nil {
op.Errorf("Unable to unmarshal decoded spec: %s", err)
return nil, err
}
}
}
// normalize empty spec
if filterSpec.Inclusions == nil {
op.Debugf("Empty inclusion set")
filterSpec.Inclusions = make(map[string]struct{})
}
if filterSpec.Exclusions == nil {
op.Debugf("Empty exclusion set")
filterSpec.Exclusions = make(map[string]struct{})
}
return &filterSpec, nil
}
// Encode the filter spec
func EncodeFilterSpec(op trace.Operation, spec *FilterSpec) (*string, error) {
mashalled, err := json.Marshal(spec)
if err != nil {
op.Errorf("Unable to encode filter spec: %s", err)
return nil, err
}
encoded := base64.StdEncoding.EncodeToString(mashalled)
op.Debugf("encodedFilter = %s", encoded)
return &encoded, nil
}
// Excludes returns true if the provided filter excludes the provided filepath
// If the spec is completely empty it will include everything.
// If an inclusion is set, but not exclusion, then we'll only return matches for the inclusions.
func (spec *FilterSpec) Excludes(op trace.Operation, filePath string) bool {
if spec == nil {
return false
}
inclusionLength := -1
exclusionLength := -1
for path := range spec.Inclusions {
if strings.HasPrefix(filePath, path) {
if len(path) > inclusionLength {
// more specific inclusion, so update
inclusionLength = len(path)
}
}
}
for path := range spec.Exclusions {
if strings.HasPrefix(filePath, path) {
if len(path) > exclusionLength {
// more specific exclusion, so update
exclusionLength = len(path)
}
}
}
return inclusionLength < exclusionLength
}

View File

@@ -0,0 +1,27 @@
// Copyright 2017 VMware, Inc. All Rights Reserved.
//
// 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.
// NOTE: This file is borrowed directly from docker in order to use unexported
// functions that it provides.
// https://github.com/moby/moby/blob/master/pkg/archive/archive_unix.go
package archive
import (
"os"
)
func hasHardlinks(fi os.FileInfo) bool {
return false
}

299
vendor/github.com/vmware/vic/lib/archive/diff.go generated vendored Normal file
View File

@@ -0,0 +1,299 @@
// Copyright 2017 VMware, Inc. All Rights Reserved.
//
// 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"
"io"
"os"
"path/filepath"
"sort"
"strings"
"sync"
"time"
docker "github.com/docker/docker/pkg/archive"
"github.com/vmware/vic/pkg/trace"
)
const (
// ChangeTypeKey defines the key for the type of diff change stored in the tar Xattrs header
ChangeTypeKey = "change_type"
)
// CancelNotifyKey allows for a notification when cancelation is complete
type CancelNotifyKey struct{}
var (
seenFiles map[uint64]string
)
func min(a, b int) int {
if a < b {
return a
}
return b
}
// for sort.Sort
type changesByPath []docker.Change
// This will walk the two path components up to min(i, j) length.
// If at any time those components do not match, a comparison of the non-matching
// components is returned.
// If the shorter path is a prefix of the longer path, the shorter path wins.
func (c changesByPath) Less(i, j int) bool {
a := strings.Split(c[i].Path[1:], "/")
b := strings.Split(c[j].Path[1:], "/")
m := min(len(a), len(b))
for x := 0; x < m; x++ {
if a[x] != b[x] {
return a[x] < b[x]
}
}
return len(a) < len(b)
}
func (c changesByPath) Len() int { return len(c) }
func (c changesByPath) Swap(i, j int) { c[j], c[i] = c[i], c[j] }
// Diff produces a tar archive containing the differences between two filesystems
func Diff(op trace.Operation, newDir, oldDir string, spec *FilterSpec, data bool, xattr bool) (io.ReadCloser, error) {
var err error
if spec == nil {
spec, err = CreateFilterSpec(op, nil)
if err != nil {
return nil, err
}
}
changes, err := docker.ChangesDirs(newDir, oldDir)
if err != nil {
return nil, err
}
sort.Sort(changesByPath(changes))
return Tar(op, newDir, changes, spec, data, xattr)
}
func Tar(op trace.Operation, dir string, changes []docker.Change, spec *FilterSpec, data bool, xattr bool) (io.ReadCloser, error) {
var (
err error
hdr *tar.Header
)
// NOTE(hickeng): I don't like this as we've not assurance that it's appropriately buffered, but cannot think of a go-idomatic way to
// do better right now
var notify *sync.WaitGroup
n := op.Value(CancelNotifyKey{})
if n != nil {
var ok bool
if notify, ok = n.(*sync.WaitGroup); ok {
// this lets us block the creator of the cancel-notifier until we've cleaned up
notify.Add(1)
}
}
seenFiles = make(map[uint64]string)
// Note: it is up to the caller to handle errors and the closing of the read side of the pipe
r, w := io.Pipe()
go func() {
tw := tar.NewWriter(w)
defer func() {
var cerr error
defer w.Close()
if notify != nil {
// inform waiters that we're done with cleanup
notify.Done()
}
if oerr := op.Err(); oerr != nil {
// don't close the archive if we're truncating the copy - it's misleading
// #nosec: Errors unhandled.
_ = w.CloseWithError(oerr)
return
}
if cerr = tw.Close(); cerr != nil {
op.Errorf("Error closing tar writer: %s", cerr.Error())
}
if err == nil {
if cerr != nil {
op.Errorf("Closing down tar writer with clean exit: %s", cerr)
} else {
op.Debugf("Closing down tar writer with pristine exit")
}
// #nosec: Errors unhandled.
_ = w.CloseWithError(cerr)
} else {
op.Errorf("Closing down tar writer with error during tar: %s", err)
// #nosec: Errors unhandled.
_ = w.CloseWithError(err)
}
}()
for _, change := range changes {
if cerr := op.Err(); cerr != nil {
// this will still trigger the defer to close the archive neatly
op.Warnf("Aborting tar due to cancellation: %s", cerr)
break
}
if spec.Excludes(op, strings.TrimPrefix(change.Path, "/")) {
continue
}
hdr, err = createHeader(op, dir, change, spec, xattr)
if err != nil {
op.Errorf("Error creating header from change: %s", err.Error())
return
}
var f *os.File
if !data {
hdr.Size = 0
}
// #nosec: Errors unhandled.
_ = tw.WriteHeader(hdr)
p := filepath.Join(dir, change.Path)
if (hdr.Typeflag == tar.TypeReg || hdr.Typeflag == tar.TypeRegA) && hdr.Size != 0 {
f, err = os.Open(p)
if err != nil {
if os.IsPermission(err) {
err = nil
}
return
}
if f != nil {
// make sure we get out of io.Copy if context is canceled
done := make(chan struct{})
go func() {
select {
case <-op.Done():
f.Close()
// force the io.Copy to exit whether it's in the read or write portion of the copy.
// If this causes problems with inflight data we can try moving the cancellation notifier
// Done call here and see if the other end of w/tw will be shut down.
w.Close()
case <-done:
}
}()
_, err = io.Copy(tw, f)
close(done)
// #nosec: Errors unhandled.
_ = f.Close()
if err != nil {
op.Errorf("Error writing archive data: %s", err.Error())
if err == io.EOF || err == io.ErrClosedPipe {
// no point in continuing
break
}
}
}
}
}
}()
return r, err
}
func createHeader(op trace.Operation, dir string, change docker.Change, spec *FilterSpec, xattr bool) (*tar.Header, error) {
var hdr *tar.Header
timestamp := time.Now()
switch change.Kind {
case docker.ChangeDelete:
whiteOutDir := filepath.Dir(change.Path)
whiteOutBase := filepath.Base(change.Path)
whiteOut := filepath.Join(whiteOutDir, docker.WhiteoutPrefix+whiteOutBase)
hdr = &tar.Header{
ModTime: timestamp,
Size: 0,
AccessTime: timestamp,
ChangeTime: timestamp,
}
whiteOut = strings.TrimPrefix(whiteOut, "/")
strippedName := strings.TrimPrefix(whiteOut, spec.StripPath)
hdr.Name = filepath.Join(spec.RebasePath, strippedName)
default:
path := filepath.Join(dir, change.Path)
fi, err := os.Lstat(path)
if err != nil {
op.Errorf("Error getting file info: %s", err.Error())
return nil, err
}
link := ""
if fi.Mode()&os.ModeSymlink != 0 {
if link, err = os.Readlink(path); err != nil {
return nil, err
}
}
hdr, err = tar.FileInfoHeader(fi, link)
if err != nil {
op.Errorf("Error getting file info header: %s", err.Error())
return nil, err
}
name := strings.TrimPrefix(change.Path, "/")
name = strings.TrimPrefix(name, spec.StripPath)
name = filepath.Join(spec.RebasePath, name)
if fi.IsDir() && !strings.HasSuffix(change.Path, "/") {
name += "/"
}
hdr.Name = strings.TrimPrefix(name, "/")
inode, err := setHeaderForSpecialDevice(hdr, fi.Sys())
if err != nil {
return nil, err
}
// if it's not a directory and has more than 1 link,
// it's hard linked, so set the type flag accordingly
if !fi.IsDir() && hasHardlinks(fi) {
// a link should have a name that it links to
// and that linked name should be first in the tar archive
if oldpath, ok := seenFiles[inode]; ok {
hdr.Typeflag = tar.TypeLink
hdr.Linkname = oldpath
hdr.Size = 0 // This Must be here for the writer math to add up!
} else {
seenFiles[inode] = strings.TrimPrefix(name, "/")
}
}
}
if xattr {
hdr.Xattrs = make(map[string]string)
hdr.Xattrs[ChangeTypeKey] = change.Kind.String()
}
return hdr, nil
}

629
vendor/github.com/vmware/vic/lib/archive/diff_test.go generated vendored Normal file
View File

@@ -0,0 +1,629 @@
// Copyright 2017 VMware, Inc. All Rights Reserved.
//
// 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"
"bytes"
"context"
"io"
"io/ioutil"
"os"
"path/filepath"
"strings"
"testing"
log "github.com/Sirupsen/logrus"
"github.com/stretchr/testify/assert"
"github.com/vmware/vic/pkg/trace"
)
var (
newDir, oldDir string
a, b []byte
files map[string][]string
directories map[string]struct{}
err error
)
func TestMain(m *testing.M) {
var err error
directories = make(map[string]struct{}, 4)
files = make(map[string][]string, 6)
files["original"] = []string{"file1", "file2", "file3", "file4",
"original/file1", "original/file2", "original/remove",
"exclude/excludeme", "exclude/includeme", "excludeme", "include/excludeme", "include/includeme"}
files["added"] = []string{"added1", "added2", "add/file1", "add/file2"}
files["changed"] = []string{"file1", "original/file2",
"exclude/excludeme", "exclude/includeme",
"include/excludeme", "include/includeme",
"excludeme"}
files["removed"] = []string{"file2", "original/file1", "original/remove"}
files["excluded"] = []string{"exclude/", "excludeme", "include/excludeme"}
files["included"] = []string{"exclude/includeme", "include/"}
newDir, err = ioutil.TempDir("", "mnt")
if err != nil {
return
}
defer os.RemoveAll(newDir)
oldDir, err = ioutil.TempDir("", "mnt")
if err != nil {
return
}
defer os.RemoveAll(oldDir)
a = []byte("The mollusk lingers with its wandering eye\n")
b = []byte("The waking of all creatures that live on the land\n")
for _, dir := range []string{"original/", "add/", "exclude/", "include/"} {
directories[dir] = struct{}{}
if err = os.Mkdir(filepath.Join(oldDir, dir), 0777); err != nil {
log.Errorf("Failed to add directory: %s", err.Error())
return
}
if err = os.Mkdir(filepath.Join(newDir, dir), 0777); err != nil {
log.Errorf("Failed to add directory: %s", err.Error())
return
}
}
for _, file := range files["original"] {
if err = ioutil.WriteFile(filepath.Join(oldDir, file), a, 0777); err != nil {
log.Errorf("Failed to add file: %s", err.Error())
return
}
if err = ioutil.WriteFile(filepath.Join(newDir, file), a, 0777); err != nil {
log.Errorf("Failed to add file: %s", err.Error())
return
}
}
for _, file := range append(files["added"], files["changed"]...) {
if err = ioutil.WriteFile(filepath.Join(newDir, file), b, 0777); err != nil {
log.Errorf("Failed to add file: %s", err.Error())
return
}
}
for _, file := range files["removed"] {
if err = os.Remove(filepath.Join(newDir, file)); err != nil {
log.Errorf("Failed to remove file: %s", err.Error())
return
}
}
os.Exit(m.Run())
}
func TestDiff(t *testing.T) {
op := trace.NewOperation(context.Background(), "TestDiff")
tarFile, err := Diff(op, newDir, oldDir, nil, true, true)
if !assert.NoError(t, err) {
return
}
tarredFiles := make(map[string][]byte)
tr := tar.NewReader(tarFile)
for {
hdr, err := tr.Next()
if err == io.EOF {
break
}
if err != nil {
op.Errorf("Error reading tar archive: %s", err.Error())
}
buf := bytes.NewBuffer([]byte{})
if _, err := io.Copy(buf, tr); !assert.NoError(t, err) {
return
}
tarredFiles[hdr.Name] = buf.Bytes()
}
all := append(files["added"], append(files["changed"], files["included"]...)...)
for _, file := range all {
if strings.HasSuffix(file, "/") {
continue
}
f, ok := tarredFiles[file]
assert.True(t, ok, "Expected to find %s in tar archive", file)
// don't try to check the contents if its a directory
if _, ok := directories[file]; !ok {
assert.Equal(t, b, f, "Expected file contents \"%s\", but found \"%s\"", b, f)
}
}
for _, file := range files["removed"] {
wh := filepath.Join(filepath.Dir(file), ".wh."+filepath.Base(file))
_, ok := tarredFiles[wh]
assert.True(t, ok, "Expected to find whiteout file in archive: %s", wh)
}
}
func TestDiffNoAncestor(t *testing.T) {
op := trace.NewOperation(context.Background(), "TestDiffNoParent")
// test without ancestor
tarFile, err := Diff(op, newDir, "", nil, true, true)
if !assert.NoError(t, err) {
return
}
tarredFiles := make(map[string][]byte)
tr := tar.NewReader(tarFile)
for {
hdr, err := tr.Next()
if err == io.EOF {
break
}
if err != nil {
op.Errorf("Error reading tar header: %s", err.Error())
}
buf := bytes.NewBuffer([]byte{})
if _, err := io.Copy(buf, tr); !assert.NoError(t, err) {
return
}
tarredFiles[hdr.Name] = buf.Bytes()
if hdr.Typeflag == tar.TypeDir {
directories[hdr.Name] = struct{}{}
}
}
all := append(files["added"], append(files["changed"], files["included"]...)...)
for _, file := range all {
f, ok := tarredFiles[file]
assert.True(t, ok, "Expected to find %s in tar archive", file)
// don't try to check the contents if its a directory
if _, ok := directories[file]; !ok {
assert.Equal(t, b, f, "Expected file contents \"%s\", but found \"%s\"", b, f)
}
}
for _, file := range files["removed"] {
wh := filepath.Join(filepath.Dir(file), ".wh."+filepath.Base(file))
_, ok := tarredFiles[wh]
assert.False(t, ok, "Expected not to find whiteout file in archive: %s", wh)
}
}
func TestDiffNoData(t *testing.T) {
op := trace.NewOperation(context.Background(), "TestDiffNoData")
tarFile, err := Diff(op, newDir, oldDir, nil, false, true)
if !assert.NoError(t, err) {
return
}
tarredFiles := make(map[string]string)
changeTypes := make(map[string]string)
tr := tar.NewReader(tarFile)
for {
hdr, err := tr.Next()
if err == io.EOF {
break
}
if !assert.NoError(t, err) {
op.Errorf("Error reading tar header: %s", err.Error())
}
buf := bytes.NewBuffer([]byte{})
n, err := io.Copy(buf, tr)
if !assert.NoError(t, err) {
return
}
assert.EqualValues(t, 0, n, "Expected 0 bytes copied, got %d instead", n)
tarredFiles[hdr.Name] = string(buf.Bytes())
changeTypes[hdr.Name] = hdr.Xattrs[ChangeTypeKey]
}
for _, file := range files["added"] {
f, ok := tarredFiles[file]
ctype := changeTypes[file]
assert.True(t, ok, "Expected to find %s in tar archive", file)
// don't try to check the contents if its a directory
if _, ok := directories[file]; !ok {
assert.Equal(t, "", f, "Expected file contents \"%s\", but found \"%s\"", "", f)
assert.Equal(t, "A", ctype, "Expected change type \"%s\", but found \"%s\"", "A", ctype)
}
}
for _, file := range append(files["changed"], files["included"]...) {
f, ok := tarredFiles[file]
ctype := changeTypes[file]
assert.True(t, ok, "expected to find %s in tar archive", file)
// don't try to check the contents if its a directory
if _, ok := directories[file]; !ok {
assert.Equal(t, "", f, "expected file contents \"%s\", but found \"%s\"", "", f)
assert.Equal(t, "C", ctype, "expected change type \"%s\", but found \"%s\"", "C", ctype)
}
}
for _, file := range files["removed"] {
wh := filepath.Join(filepath.Dir(file), ".wh."+filepath.Base(file))
_, ok := tarredFiles[wh]
ctype, cok := changeTypes[wh]
assert.True(t, ok, "Expected to find whiteout file in archive: %s", wh)
assert.True(t, cok, "Expected to find change type for %s", wh)
assert.Equal(t, "D", ctype, "Expected change type \"%s\" but found \"%s\"", "D", ctype)
}
}
func TestDiffFilterSpec(t *testing.T) {
op := trace.NewOperation(context.Background(), "TestDiffFilterSpec")
filter := make(map[string]FilterType)
for _, path := range files["excluded"] {
p := strings.TrimSuffix(path, "/")
filter[p] = Exclude
}
for _, path := range files["included"] {
filter[path] = Include
}
spec, err := CreateFilterSpec(op, filter)
if !assert.NoError(t, err) {
return
}
tarFile, err := Diff(op, newDir, oldDir, spec, true, true)
if !assert.NoError(t, err) {
return
}
tarredFiles := make(map[string][]byte)
tr := tar.NewReader(tarFile)
for {
hdr, err := tr.Next()
if err == io.EOF {
break
}
if err != nil {
op.Errorf("Error reading tar archive: %s", err.Error())
}
buf := bytes.NewBuffer([]byte{})
if _, err := io.Copy(buf, tr); !assert.NoError(t, err) {
return
}
tarredFiles[hdr.Name] = buf.Bytes()
}
all := append(files["added"], files["included"]...)
for _, file := range all {
if file == string(filepath.Separator) {
continue
}
f, ok := tarredFiles[file]
assert.True(t, ok, "Expected to find %s in tar archive", file)
// don't try to check the contents if its a directory
if _, ok := directories[file]; !ok {
assert.Equal(t, b, f, "Expected file contents \"%s\" for %s, but found \"%s\"", b, file, f)
}
}
for _, file := range files["removed"] {
wh := filepath.Join(filepath.Dir(file), ".wh."+filepath.Base(file))
_, ok := tarredFiles[wh]
assert.True(t, ok, "Expected to find whiteout file in archive: %s", wh)
}
for _, file := range files["excluded"] {
_, ok := tarredFiles[file]
assert.False(t, ok, "Expected excluded file to be missing from archive: %s", file)
}
}
func TestDiffFilterSpecNoAncestor(t *testing.T) {
op := trace.NewOperation(context.Background(), "TestDiffFilterSpecNoParent")
filter := make(map[string]FilterType)
for _, path := range files["excluded"] {
p := strings.TrimSuffix(path, "/")
filter[p] = Exclude
}
for _, path := range files["included"] {
filter[path] = Include
}
spec, err := CreateFilterSpec(op, filter)
if !assert.NoError(t, err) {
return
}
// test without ancestor
tarFile, err := Diff(op, newDir, "", spec, true, true)
if !assert.NoError(t, err) {
return
}
tarredFiles := make(map[string][]byte)
tr := tar.NewReader(tarFile)
for {
hdr, err := tr.Next()
if err == io.EOF {
break
}
if err != nil {
op.Errorf("Error reading tar archive: %s", err.Error())
}
buf := bytes.NewBuffer([]byte{})
if _, err := io.Copy(buf, tr); !assert.NoError(t, err) {
return
}
tarredFiles[hdr.Name] = buf.Bytes()
}
all := append(files["added"], files["included"]...)
for _, file := range all {
f, ok := tarredFiles[file]
if file == string(filepath.Separator) {
continue
}
assert.True(t, ok, "Expected to find %s in tar archive", file)
// don't try to check the contents if its a directory
if _, ok := directories[file]; !ok {
assert.Equal(t, b, f, "Expected file contents \"%s\", but found \"%s\" for target file (%s)", b, f, file)
}
}
for _, file := range files["removed"] {
wh := filepath.Join(filepath.Dir(file), ".wh."+filepath.Base(file))
_, ok := tarredFiles[wh]
assert.False(t, ok, "Expected not to find whiteout file in archive: %s", wh)
}
for _, file := range files["excluded"] {
_, ok := tarredFiles[file]
assert.False(t, ok, "Expected excluded file to be missing from archive: %s", file)
}
}
func TestDiffFilterSpecRebase(t *testing.T) {
op := trace.NewOperation(context.Background(), "TestDiffFilterSpecRebase")
filter := make(map[string]FilterType)
for _, path := range files["excluded"] {
filter[path] = Exclude
}
for _, path := range files["included"] {
filter[path] = Include
}
rebasePath := "path/to/prefix"
filter[rebasePath] = Rebase
spec, err := CreateFilterSpec(op, filter)
if !assert.NoError(t, err) {
return
}
// test without ancestor
tarFile, err := Diff(op, newDir, oldDir, spec, true, true)
if !assert.NoError(t, err) {
return
}
tarredFiles := make(map[string][]byte)
tr := tar.NewReader(tarFile)
for {
hdr, err := tr.Next()
if err == io.EOF {
break
}
if err != nil {
op.Errorf("Error reading tar archive: %s", err.Error())
}
buf := bytes.NewBuffer([]byte{})
if _, err := io.Copy(buf, tr); !assert.NoError(t, err) {
return
}
tarredFiles[strings.TrimSuffix(hdr.Name, "/")] = buf.Bytes()
}
all := append(files["added"], files["included"]...)
for _, file := range all {
if file == string(filepath.Separator) {
continue
}
rebasedPath := filepath.Join(rebasePath, file)
var isDir bool
_, isDir = directories[file]
f, ok := tarredFiles[rebasedPath]
assert.True(t, ok, "Expected to find %s in tar archive", rebasedPath)
if !isDir {
assert.Equal(t, b, f, "Expected file contents \"%s\", but found \"%s\" for target file (%s)", b, f, rebasedPath)
}
}
for _, file := range files["removed"] {
wh := filepath.Join(filepath.Dir(file), ".wh."+filepath.Base(file))
wh = filepath.Join(rebasePath, wh)
_, ok := tarredFiles[wh]
assert.True(t, ok, "Expected not to find whiteout file in archive: %s", wh)
}
for _, file := range files["excluded"] {
_, ok := tarredFiles[file]
assert.False(t, ok, "Expected excluded file to be missing from archive: %s", file)
}
}
func TestDiffFilterSpecRebaseNoData(t *testing.T) {
op := trace.NewOperation(context.Background(), "TestDiffFilterSpecRebase")
filter := make(map[string]FilterType)
for _, path := range files["excluded"] {
filter[path] = Exclude
}
for _, path := range files["included"] {
filter[path] = Include
}
rebasePath := "path/to/prefix"
filter[rebasePath] = Rebase
spec, err := CreateFilterSpec(op, filter)
if !assert.NoError(t, err) {
return
}
tarFile, err := Diff(op, newDir, oldDir, spec, false, true)
if !assert.NoError(t, err) {
return
}
tarredFiles := make(map[string]struct{})
changeTypes := make(map[string]string)
tr := tar.NewReader(tarFile)
for {
hdr, err := tr.Next()
if err == io.EOF {
break
}
if !assert.NoError(t, err) {
op.Errorf("Error reading tar header: %s", err.Error())
}
buf := bytes.NewBuffer([]byte{})
n, err := io.Copy(buf, tr)
if !assert.NoError(t, err) {
return
}
assert.EqualValues(t, 0, n, "Expected 0 bytes copied, got %d instead", n)
tarredFiles[strings.TrimSuffix(hdr.Name, "/")] = struct{}{}
changeTypes[hdr.Name] = hdr.Xattrs[ChangeTypeKey]
}
for _, file := range files["added"] {
rebasedPath := filepath.Join(rebasePath, file)
var isDir bool
_, isDir = directories[file]
_, ok := tarredFiles[rebasedPath]
ctype := changeTypes[rebasedPath]
assert.True(t, ok, "Expected to find %s in tar archive", file)
// don't try to check the contents if its a directory
if !isDir {
assert.Equal(t, "A", ctype, "Expected change type \"%s\", but found \"%s\"", "A", ctype)
}
}
for _, file := range append(files["changed"], files["included"]...) {
// don't check for excludes or whiteouts yet
base := filepath.Base(file)
if strings.HasSuffix(file, "excludeme") || strings.HasPrefix(base, ".wh.") {
continue
}
rebasedPath := filepath.Join(rebasePath, file)
var isDir bool
_, isDir = directories[file]
_, ok := tarredFiles[rebasedPath]
ctype := changeTypes[rebasedPath]
assert.True(t, ok, "expected to find %s in tar archive", file)
// don't try to check the contents if its a directory
if !isDir {
assert.Equal(t, "C", ctype, "expected change type \"%s\", but found \"%s\"", "C", ctype)
}
}
for _, file := range files["removed"] {
wh := filepath.Join(filepath.Dir(file), ".wh."+filepath.Base(file))
wh = filepath.Join(rebasePath, wh)
_, ok := tarredFiles[wh]
ctype, cok := changeTypes[wh]
assert.True(t, ok, "Expected to find whiteout file in archive: %s", wh)
assert.True(t, cok, "Expected to find change type for %s", wh)
assert.Equal(t, "D", ctype, "Expected change type \"%s\" but found \"%s\"", "D", ctype)
}
for _, file := range files["excluded"] {
_, ok := tarredFiles[file]
assert.False(t, ok, "Expected excluded file to be missing from archive: %s", file)
}
}
func TestDiffCreateFilterSpec(t *testing.T) {
op := trace.NewOperation(context.Background(), "TestDiffCreateFilterSpec")
filter := make(map[string]FilterType)
for _, path := range files["excluded"] {
filter[path] = Exclude
}
for _, path := range files["included"] {
filter[path] = Include
}
rebasePath := "/path/to/prefix"
filter[rebasePath] = Rebase
stripPath := "/path/to/strip"
filter[stripPath] = Strip
spec, err := CreateFilterSpec(op, filter)
if !assert.NoError(t, err) {
return
}
for _, path := range files["included"] {
_, ok := spec.Inclusions[path]
assert.True(t, ok, "Expected to find %s in inclusions set", path)
_, ok = spec.Exclusions[path]
assert.False(t, ok, "Expected not to find %s in exclusions set", path)
}
for _, path := range files["excluded"] {
_, ok := spec.Exclusions[path]
assert.True(t, ok, "Expected to find %s in exclusions set", path)
_, ok = spec.Inclusions[path]
assert.False(t, ok, "Expected not to find %s in inclusions set", path)
}
assert.Equal(t, rebasePath, spec.RebasePath)
assert.Equal(t, stripPath, spec.StripPath)
e := "/path/to/extra/rebase"
filter[e] = Rebase
spec, err = CreateFilterSpec(op, filter)
assert.Nil(t, spec, "Expected nil spec")
assert.Error(t, err)
assert.EqualError(t, err, "error creating filter spec: only one rebase path allowed")
delete(filter, e)
s := "/path/to/extra/strippath"
filter[s] = Strip
spec, err = CreateFilterSpec(op, filter)
assert.Nil(t, spec, "Expected nil spec")
assert.Error(t, err)
assert.EqualError(t, err, "error creating filter spec: only one strip path allowed")
delete(filter, s)
filter[s] = 20
spec, err = CreateFilterSpec(op, filter)
assert.Nil(t, spec, "Expected nil spec")
assert.Error(t, err)
assert.EqualError(t, err, "invalid filter specification: 20")
}

173
vendor/github.com/vmware/vic/lib/archive/stripper.go generated vendored Normal file
View File

@@ -0,0 +1,173 @@
// Copyright 2017 VMware, Inc. All Rights Reserved.
//
// 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"
"io"
"github.com/vmware/vic/pkg/trace"
)
// Stripper strips the end-of-archive entries from a tar stream
type Stripper struct {
// op allows threaded tracing
op trace.Operation
// be opinionated about the type of the source
source *tar.Reader
// close function to call if treated as io.Closer
closer func() error
}
// NewStripper returns a WriterTo that will strip the trailing end-of-archive bytes
// from the supplied tar stream.
// It implements io.Reader only so that it can be passed to io.Copy
func NewStripper(op trace.Operation, reader *tar.Reader, close func() error) *Stripper {
return &Stripper{
op: op,
source: reader,
closer: close,
}
}
// Read is implemented solely so this can be provided to io.Copy as an io.Reader.
// This works on the assumption of io.Copy making use of the WriterTo implementation.
func (s *Stripper) Read(b []byte) (int, error) {
panic("io.Reader usage not supported - intended use is as io.WriterTo")
}
// WriteTo is the primary function, allowing easy use of the underlying tar stream without
// requiring chunking and assocated tracking to another buffer size.
// Of note is that this returns the number of DATA bytes written, excluding the header bytes.
func (s *Stripper) WriteTo(w io.Writer) (sum int64, err error) {
// TODO: should we nil s.source on error then handle a post-error call? What's the expected
// semantic?
tw := tar.NewWriter(w)
for {
var header *tar.Header
header, err = s.source.Next()
if err == io.EOF {
// do NOT call tarwriter.Close() and drop the EOF for io.Copy behaviour
err = nil
s.op.Debugf("Stripper dropping end of archive")
return
}
if err != nil {
s.op.Errorf("Error reading archive header: %s", err)
return
}
err = tw.WriteHeader(header)
if err != nil {
s.op.Errorf("Error writing tar header: %s", err)
return
}
var n int64
n, err = io.Copy(tw, s.source)
sum += n
if err != nil {
s.op.Errorf("Error copying file data: %s", err)
return
}
// #nosec: Errors unhandled.
tw.Flush()
}
}
// Close allows us to proxy a close on the stripper to the wrapped input
func (s *Stripper) Close() error {
if s.closer != nil {
s.op.Debugf("Closing stripper source: %p", s)
return s.closer()
}
return nil
}
// eofReader copied from io package to support MultiWriterTo variant
type eofReader struct{}
func (eofReader) WriteTo(w io.Writer) (int64, error) {
return 0, io.EOF
}
func (eofReader) Read(b []byte) (int, error) {
return 0, io.EOF
}
func (eofReader) ReadFrom(r io.Reader) (int64, error) {
return 0, io.EOF
}
// multiStripper based off io.MultiReader but delegating to io.WriterTo
// instead of performing buffer copy
type multiReader struct {
readers []io.Reader
}
func (mr *multiReader) Read(p []byte) (n int, err error) {
panic("io.Reader usage not supported - intended use is as io.WriterTo")
}
func (mr *multiReader) WriteTo(w io.Writer) (sum int64, err error) {
for _, reader := range mr.readers {
var n int64
n, err = io.Copy(w, reader)
sum += n
// io.Copy never returns EOF so treat nil as EOF but keeping
// EOF for clarity
if err == io.EOF || err == nil {
continue
}
// err was non-nil/EOF and we read data - legitimate error scenario
if n > 0 {
return
}
}
err = nil
return
}
// Close allows this to be a Closer as well - specific to expected usage but necessary.
func (mr *multiReader) Close() error {
for _, r := range mr.readers {
// if it's a closer, close it
if closer, ok := r.(io.Closer); ok {
// #nosec: Errors unhandled.
closer.Close()
}
}
return nil
}
// MultiReader is based off the io.MultiReader but will make use of WriteTo or
// ReadFrom delegation and ONLY supports usage via the WriteTo method on itself.
// It is specifically intended to be passed to io.Copy
func MultiReader(readers ...io.Reader) io.ReadCloser {
r := make([]io.Reader, len(readers))
copy(r, readers)
return &multiReader{r}
}

View File

@@ -0,0 +1,320 @@
// Copyright 2017 VMware, Inc. All Rights Reserved.
//
// 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"
"context"
"fmt"
"io"
"sync"
"testing"
"github.com/stretchr/testify/assert"
"github.com/vmware/vic/pkg/trace"
"github.com/vmware/vic/pkg/uid"
)
// generateArchive takes a number of files and a file size and generates a tar archive
// based on that data. It returns:
// index of entry names in the archive
// archive byte stream
func generateArchive(name string, num, size int) ([]string, io.Reader) {
r, w := io.Pipe()
tw := tar.NewWriter(w)
// stable reference for expected archive entries
index := make([]string, num)
for i := 0; i < num; i++ {
index[i] = string(uid.New())
}
// our file contents are zeros - this is the worst case for stripping trailing headers
zeros := make([]byte, size)
go func() {
for i := 0; i < num; i++ {
// we only really care about two things in the header
hdr := &tar.Header{
Name: index[i],
Size: int64(size),
}
fmt.Printf("Writing header for file %s:%d\n", name, i)
err := tw.WriteHeader(hdr)
if err != nil {
panic(err)
}
fmt.Printf("Writing data for file %s:%d\n", name, i)
n, err := tw.Write(zeros)
if err != nil {
panic(err)
}
if n != size {
panic(fmt.Sprintf("Failed to write all bytes: %d != %d", n, size))
}
}
// add the end-of-archive
tw.Close()
w.Close()
}()
return index, r
}
// TestSingleStripper ensures that basic function (stripping end-of-archive footer) works as
// expected. I found no real way, when using the TarReader to actually assert that the footer
// has been dropped so this is left to assert correct passthrough of archive data and the
// dropping of the footer is asserted by the multistream cases.
func TestSingleStripper(t *testing.T) {
size := 2048
count := 5
index, reader := generateArchive("single", count, size)
source := tar.NewReader(reader)
stripper := NewStripper(trace.NewOperation(context.Background(), "strip"), source, nil)
pr, pw := io.Pipe()
tr := tar.NewReader(pr)
var wg sync.WaitGroup
wg.Add(1)
go func() {
n, err := io.Copy(pw, stripper)
pw.Close()
wg.Done()
fmt.Printf("Done copying from stripper: %d, %s\n", n, err)
if !assert.NoError(t, err, "Expected nil error from io.Copy on end-of-file") {
t.FailNow()
}
}()
for i := 0; i <= len(index); i++ {
fmt.Printf("Reading header for file %d\n", i)
header, err := tr.Next()
if err == io.EOF {
fmt.Printf("End-of-file")
// TODO: is this pass or fail?
return
}
if err != nil {
fmt.Printf("Error from archive: %s\n", err)
t.FailNow()
}
if !assert.NotEqual(t, i, len(index), "Expected EOF after index exhausted") {
t.FailNow()
}
if !assert.Equal(t, header.Name, index[i], "Expected header name to match index") {
t.FailNow()
}
if !assert.Equal(t, header.Size, int64(size), "Expected file size in header to match target generated size") {
t.FailNow()
}
// make the buf just that little bit bigger to allow for errrors in the copy if they would occur
buf := make([]byte, size+10)
fmt.Printf("Reading data for file %d\n", i)
n, err := tr.Read(buf)
if !assert.NoError(t, err, "No expected errors from file data copy") {
t.FailNow()
}
if !assert.Equal(t, n, size, "Expected file data size to match target generated size") {
t.FailNow()
}
}
wg.Wait()
}
// TestConjoinedStrippers ensures that the footer is correctly dropped from a stripped archive
// and that a TarReader continues to provide headers from the following conjoined streams.
func TestConjoinedStrippers(t *testing.T) {
size := 2048
count := 3
multiplicity := 3
indices := make([][]string, multiplicity)
strippers := make([]io.Reader, multiplicity)
for m := 0; m < multiplicity; m++ {
var reader io.Reader
indices[m], reader = generateArchive(fmt.Sprintf("archive-%d", m), count, size)
source := tar.NewReader(reader)
strippers[m] = NewStripper(trace.NewOperation(context.Background(), fmt.Sprintf("strip-%d", m)), source, nil)
}
conjoined := MultiReader(strippers...)
pr, pw := io.Pipe()
tr := tar.NewReader(pr)
var wg sync.WaitGroup
wg.Add(1)
go func() {
n, err := io.Copy(pw, conjoined)
pw.Close()
wg.Done()
fmt.Printf("Done copying from strippers: %d, %s\n", n, err)
if !assert.NoError(t, err, "Expected nil error from io.Copy on end-of-file") {
t.FailNow()
}
}()
expectedEntries := count * multiplicity
for i := 0; i <= expectedEntries; i++ {
fmt.Printf("Reading header for file %d\n", i)
header, err := tr.Next()
if err == io.EOF {
fmt.Printf("End-of-file\n")
// TODO: is this pass or fail?
return
}
if err != nil {
fmt.Printf("Error from archive: %s\n", err)
t.FailNow()
}
if !assert.NotEqual(t, i, expectedEntries, "Expected EOF after index exhausted") {
t.FailNow()
}
member := i / count
entry := i % count
if !assert.Equal(t, header.Name, indices[member][entry], "Expected header name to match index") {
t.FailNow()
}
if !assert.Equal(t, header.Size, int64(size), "Expected file size in header to match target generated size") {
t.FailNow()
}
// make the buf just that little bit bigger to allow for errrors in the copy if they would occur
buf := make([]byte, size+10)
fmt.Printf("Reading data for file %d\n", i)
n, err := tr.Read(buf)
if !assert.NoError(t, err, "No expected errors from file data copy") {
t.FailNow()
}
if !assert.Equal(t, n, size, "Expected file data size to match target generated size") {
t.FailNow()
}
}
wg.Wait()
}
// TestConjoinedStrippersWithCloser ensures that we can conjoin readers, multiple strippers and a regular, in order to get
// the end-of-archive footer as the finale. We have a wait group to ensure that all routines have finished before declaring
// success.
func TestConjoinedStrippersWithCloser(t *testing.T) {
size := 2048
count := 3
multiplicity := 3
indices := make([][]string, multiplicity)
readers := make([]io.Reader, multiplicity)
for m := 0; m < multiplicity; m++ {
var reader io.Reader
indices[m], reader = generateArchive(fmt.Sprintf("archive-%d", m), count, size)
source := tar.NewReader(reader)
if m < multiplicity-1 {
fmt.Printf("added stripper\n")
readers[m] = NewStripper(trace.NewOperation(context.Background(), fmt.Sprintf("strip-%d", m)), source, nil)
} else {
fmt.Printf("added raw\n")
readers[m] = reader
}
}
conjoined := MultiReader(readers...)
pr, pw := io.Pipe()
tr := tar.NewReader(pr)
var wg sync.WaitGroup
wg.Add(1)
go func() {
n, err := io.Copy(pw, conjoined)
pw.Close()
wg.Done()
fmt.Printf("Done copying from all sources: %d, %s\n", n, err)
if !assert.NoError(t, err, "Expected nil error from io.Copy on end-of-file") {
t.FailNow()
}
}()
expectedEntries := count * multiplicity
for i := 0; i <= expectedEntries; i++ {
fmt.Printf("Reading header for file %d\n", i)
header, err := tr.Next()
if err == io.EOF {
fmt.Printf("End-of-file\n")
wg.Wait()
return
}
if err != nil {
fmt.Printf("Error from archive: %s\n", err)
t.FailNow()
}
if !assert.NotEqual(t, i, expectedEntries, "Expected EOF after index exhausted") {
t.FailNow()
}
member := i / count
entry := i % count
if !assert.Equal(t, header.Name, indices[member][entry], "Expected header name to match index") {
t.FailNow()
}
if !assert.Equal(t, header.Size, int64(size), "Expected file size in header to match target generated size") {
t.FailNow()
}
// make the buf just that little bit bigger to allow for errrors in the copy if they would occur
buf := make([]byte, size+10)
fmt.Printf("Reading data for file %d\n", i)
n, err := tr.Read(buf)
if !assert.NoError(t, err, "No expected errors from file data copy") {
t.FailNow()
}
if !assert.Equal(t, n, size, "Expected file data size to match target generated size") {
t.FailNow()
}
}
wg.Wait()
}

201
vendor/github.com/vmware/vic/lib/archive/unpack_unix.go generated vendored Normal file
View File

@@ -0,0 +1,201 @@
// Copyright 2016-2017 VMware, Inc. All Rights Reserved.
//
// 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.
// +build !windows
package archive
import (
"archive/tar"
"fmt"
"io"
"os"
"os/exec"
"path/filepath"
"strings"
"errors"
"github.com/vmware/vic/pkg/trace"
)
const (
// fileWriteFlags is a collection of flags configuring our writes for general tar behavior
//
// O_CREATE = Create file if it does not exist
// O_TRUNC = truncate file to 0 length if it does exist(overwrite the file)
// O_WRONLY = We use this since we do not intend to read, we only need to write.
fileWriteFlags = os.O_CREATE | os.O_TRUNC | os.O_WRONLY
)
// InvokeUnpack will unpack the given tarstream(if it is a tar stream) on the local filesystem based on the specified root
// combined with any rebase from the path spec
//
// the pathSpec will include the following elements
// - include : any tar entry that has a path below(after stripping) the include path will be written
// - strip : The strip string will indicate the
// - exlude : marks paths that are to be excluded from the write
// - rebase : marks the the write path that will be tacked onto (appended or prepended? TODO improve this comment) the "root". e.g /tmp/unpack + /my/target/path = /tmp/unpack/my/target/path
// N.B. tarStream MUST BE TERMINATED WITH EOF or this function will hang forever!
func InvokeUnpack(op trace.Operation, tarStream io.Reader, filter *FilterSpec, root string) error {
op.Debugf("unpacking archive to root: %s, filter: %+v", root, filter)
// Online datasource is sending a tar reader instead of an io reader.
// Type check here to see if we actually need to create a tar reader.
var tr *tar.Reader
if trCheck, ok := tarStream.(*tar.Reader); ok {
tr = trCheck
} else {
tr = tar.NewReader(tarStream)
}
fi, err := os.Stat(root)
if err != nil {
// the target unpack path does not exist. We should not get here.
op.Errorf("tar unpack target does not exist: %s", root)
return err
}
if !fi.IsDir() {
err := fmt.Errorf("unpack root target is not a directory: %s", root)
op.Error(err)
return err
}
op.Debugf("using FilterSpec : (%#v)", *filter)
// process the tarball onto the filesystem
for {
header, err := tr.Next()
if err == io.EOF {
// This indicates the end of the archive
break
}
if err != nil {
op.Errorf("Error reading tar header: %s", err)
return err
}
op.Debugf("processing tar header: asset(%s), size(%d)", header.Name, header.Size)
// skip excluded elements unless explicitly included
if filter.Excludes(op, header.Name) {
continue
}
// fix up path
stripped := strings.TrimPrefix(header.Name, filter.StripPath)
rebased := filepath.Join(filter.RebasePath, stripped)
absPath := filepath.Join(root, rebased)
switch header.Typeflag {
case tar.TypeDir:
err = os.MkdirAll(absPath, header.FileInfo().Mode())
if err != nil {
op.Errorf("Failed to create directory%s: %s", absPath, err)
return err
}
case tar.TypeSymlink:
err := os.Symlink(header.Linkname, absPath)
if err != nil {
op.Errorf("Failed to create symlink %s->%s: %s", absPath, header.Linkname, err)
return err
}
case tar.TypeReg:
f, err := os.OpenFile(absPath, fileWriteFlags, header.FileInfo().Mode())
if err != nil {
op.Errorf("Failed to open file %s: %s", absPath, err)
return err
}
_, err = io.Copy(f, tr)
// TODO: add ctx.Done cancellation
f.Close()
if err != nil {
return err
}
default:
// TODO: add support for special file types - otherwise we will do absurd things such as read infinitely from /dev/random
}
op.Debugf("Finished writing to: %s", absPath)
}
return nil
}
// Unpack hooks into a binary present in the appliance vm called unpack in order to execute InvokeUnpack inside of a chroot. This method works identically to InvokeUnpack, except that it will not function in areas where the binary is not present at /bin/unpack
func Unpack(op trace.Operation, tarStream io.Reader, filter *FilterSpec, root string) error {
fi, err := os.Stat(root)
if err != nil {
// the target unpack path does not exist. We should not get here.
op.Errorf("tar unpack target does not exist: %s", root)
return err
}
if !fi.IsDir() {
err := fmt.Errorf("unpack root target is not a directory: %s", root)
op.Error(err)
return err
}
encodedFilter, err := EncodeFilterSpec(op, filter)
if err != nil {
op.Error(err)
return err
}
// Prepare to launch the binary, which will create a chroot at root and then invoke InvokeUnpack
// #nosec: Subprocess launching with variable. -- neither variable is user input & both are bounded inputs so this is fine
cmd := exec.Command("/bin/unpack", op.ID(), root, *encodedFilter)
//stdin
stdin, err := cmd.StdinPipe()
if err != nil {
op.Error(err)
return err
}
if stdin == nil {
err = errors.New("stdin was nil")
op.Error(err)
return err
}
done := make(chan error)
go func() {
// copy the tarStream to the binary via stdin; the binary will stream it to InvokeUnpack unchanged
defer stdin.Close()
if _, err := io.Copy(stdin, tarStream); err != nil {
op.Errorf("Error copying tarStream: %s", err.Error())
}
done <- err
}()
out, err := cmd.CombinedOutput()
if len(out) == 0 {
op.Debug("No output from command")
} else {
// output should just be trace messages
op.Debugf("%s", string(out))
}
if err != nil {
stdin.Close()
op.Errorf("Command returned error %s", err.Error())
return err
}
// This error gets logged by the goroutine if it is non-nil.
// This receive is just functioning as a wait
err = <-done
return err
}

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,127 @@
// Copyright 2016-2017 VMware, Inc. All Rights Reserved.
//
// 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.
// +build windows
package archive
import (
"archive/tar"
"fmt"
"io"
"os"
"path/filepath"
"strings"
"github.com/vmware/vic/pkg/trace"
)
const (
// fileWriteFlags is a collection of flags configuring our writes for general tar behavior
//
// O_CREATE = Create file if it does not exist
// O_TRUNC = truncate file to 0 length if it does exist(overwrite the file)
// O_WRONLY = We use this since we do not intend to read, we only need to write.
fileWriteFlags = os.O_CREATE | os.O_TRUNC | os.O_WRONLY
)
// combined with any rebase from the path spec
//
// the pathSpec will include the following elements
// - include : any tar entry that has a path below(after stripping) the include path will be written
// - strip : The strip string will indicate the
// - exlude : marks paths that are to be excluded from the write operation
// - rebase : marks the the write path that will be tacked onto the "root". e.g /tmp/unpack + /my/target/path = /tmp/unpack/my/target/path
func Unpack(op trace.Operation, tarStream io.Reader, filter *FilterSpec, root string) error {
op.Debugf("unpacking archive to root: %s, filter: %+v", root, filter)
// Online datasource is sending a tar reader instead of an io reader.
// Type check here to see if we actually need to create a tar reader.
var tr *tar.Reader
if trCheck, ok := tarStream.(*tar.Reader); ok {
tr = trCheck
} else {
tr = tar.NewReader(tarStream)
}
fi, err := os.Stat(root)
if err != nil {
// the target unpack path does not exist. We should not get here.
op.Errorf("tar unpack target does not exist: %s", root)
return err
}
if !fi.IsDir() {
err := fmt.Errorf("unpack root target is not a directory: %s", root)
op.Error(err)
return err
}
op.Debugf("using FilterSpec : (%#v)", *filter)
// process the tarball onto the filesystem
for {
header, err := tr.Next()
if err == io.EOF {
// This indicates the end of the archive
break
}
if err != nil {
op.Errorf("Error reading tar header: %s", err)
return err
}
op.Debugf("processing tar header: asset(%s), size(%d)", header.Name, header.Size)
// skip excluded elements unless explicitly included
if filter.Excludes(op, header.Name) {
continue
}
// fix up path
stripped := strings.TrimPrefix(header.Name, filter.StripPath)
rebased := filepath.Join(filter.RebasePath, stripped)
absPath := filepath.Join(root, rebased)
switch header.Typeflag {
case tar.TypeDir:
err = os.MkdirAll(absPath, header.FileInfo().Mode())
if err != nil {
op.Errorf("Failed to create directory%s: %s", absPath, err)
return err
}
case tar.TypeSymlink:
err := os.Symlink(header.Linkname, absPath)
if err != nil {
op.Errorf("Failed to create symlink %s->%s: %s", absPath, header.Linkname, err)
return err
}
case tar.TypeReg:
f, err := os.OpenFile(absPath, fileWriteFlags, header.FileInfo().Mode())
if err != nil {
op.Errorf("Failed to open file %s: %s", absPath, err)
return err
}
_, err = io.Copy(f, tr)
// TODO: add ctx.Done cancellation
f.Close()
if err != nil {
return err
}
default:
// TODO: add support for special file types - otherwise we will do absurd things such as read infinitely from /dev/random
}
op.Debugf("Finished writing to: %s", absPath)
}
return nil
}

File diff suppressed because it is too large Load Diff

147
vendor/github.com/vmware/vic/lib/archive/util.go generated vendored Normal file
View File

@@ -0,0 +1,147 @@
// Copyright 2016-2017 VMware, Inc. All Rights Reserved.
//
// 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 (
"fmt"
"path/filepath"
"strings"
)
const (
// CopyTo is used to indicate that the desired filter spec is for a CopyTo direction
CopyTo = true
// CopyFrom is used to indicate that the desired filter spec is for the CopyFrom direction
CopyFrom = false
)
// GenerateFilterSpec will populate the appropriate relative Rebase and Strip paths based on the supplied scenarios.
// Inclusion/Exclusion should be constructed separately. Please also note that any mount that exists before the copy
// target that is not primary and comes before the primary target will have a bogus filterspec, since it would not be
// written or read to.
func GenerateFilterSpec(copyPath string, mountPoint string, primaryTarget bool, direction bool) FilterSpec {
var filter FilterSpec
// NOTE: this solidifies that this function is very heavily designed for docker cp behavior.
// which is why this should mainly only be used by the docker personality
// scrub trailing '/' before passing copyPath along and create a Clean fs path
copyPath = strings.TrimSuffix(copyPath, "/")
// Note I know they are just booleans, if that changes then this statement will not need to.
if direction == CopyTo {
filter = generateCopyToFilterSpec(copyPath, mountPoint, primaryTarget)
} else {
filter = generateCopyFromFilterSpec(copyPath, mountPoint, primaryTarget)
}
filter.Exclusions = make(map[string]struct{})
filter.Inclusions = make(map[string]struct{})
return filter
}
func generateCopyFromFilterSpec(copyPath string, mountPoint string, primaryTarget bool) FilterSpec {
var filter FilterSpec
// If the copyPath ends with '/.', we don't want the content's directory name
// to be included in the resulting tar header. we do this by removing the directory
// portion of the rebase path.
// if the copyPath does not end with '/.', we only need the right most element of copyPath.
first := filepath.Base(copyPath)
if first == "." {
first = ""
copyPath = Clean(copyPath, false)
}
// primary target was provided so we wil need to split the target and take the right most element for the rebase.
// then strip set the strip as the target path.
if primaryTarget {
filter.RebasePath = Clean(first, true)
filter.StripPath = Clean(strings.TrimPrefix(copyPath, mountPoint), true)
return filter
}
// non primary target was provided. in this case we will need rebase to include the right most member of the target(or "/") joined to the front of the mountPath - the target path. 3
filter.RebasePath = Clean(filepath.Join(first, strings.TrimPrefix(mountPoint, copyPath)), true)
filter.StripPath = ""
return filter
}
func generateCopyToFilterSpec(copyPath string, mountPoint string, primaryTarget bool) FilterSpec {
var filter FilterSpec
// primary target was provided so we will need to rebase header assets for this mount to have the target in front for the write.
if primaryTarget {
filter.RebasePath = Clean(strings.TrimPrefix(copyPath, mountPoint), true)
filter.StripPath = ""
return filter
}
// non primary target, this implies that the asset header has part of the mount point path in it. We must strip out that part since the non primary target will be mounted and be looking at the world from it's own root "/"
filter.RebasePath = ""
filter.StripPath = Clean(strings.TrimPrefix(mountPoint, copyPath), true)
return filter
}
func AddMountInclusionsExclusions(currentMount string, filter *FilterSpec, mounts []string, copyTarget string) error {
if filter == nil {
return fmt.Errorf("filterSpec for (%s) was nil, cannot add exclusions or inclusions", currentMount)
}
if filter.Exclusions == nil || filter.Inclusions == nil {
return fmt.Errorf("either the inclusions or exclusions map was nil for (%s)", currentMount)
}
if strings.HasPrefix(copyTarget, currentMount) && copyTarget != currentMount {
inclusion := Clean(strings.TrimPrefix(copyTarget, currentMount), true)
if filepath.Base(copyTarget) == "." {
inclusion += "/"
}
filter.Inclusions[inclusion] = struct{}{}
filter.Exclusions[""] = struct{}{}
} else {
// this would be a mount that is after the target. It would mean we have to include root. then exclude any mounts after root.
filter.Inclusions[""] = struct{}{}
}
for _, mount := range mounts {
if strings.HasPrefix(mount, currentMount) && currentMount != mount {
// exclusions are relative to the mount so the leading `/` should be removed unless we decide otherwise.
exclusion := Clean(strings.TrimPrefix(mount, currentMount), true) + "/"
filter.Exclusions[exclusion] = struct{}{}
}
}
return nil
}
// Clean run filepath.Clean on the target and will remove leading
// and trailing slashes from the target path corresponding to supplied booleans
func Clean(path string, leading bool) string {
path = filepath.Clean(path)
// path returns '.' if the result of the Clean was an empty string.
if path == "." {
return ""
}
if leading {
path = strings.TrimPrefix(path, "/")
}
return path
}

1112
vendor/github.com/vmware/vic/lib/archive/util_test.go generated vendored Normal file

File diff suppressed because it is too large Load Diff