297 lines
8.6 KiB
Go
297 lines
8.6 KiB
Go
// 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"
|
|
)
|
|
|
|
type binaryPath string
|
|
|
|
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
|
|
|
|
// Location of the unpack binary inside of containers
|
|
containerBinaryPath binaryPath = "/.tether/unpack"
|
|
|
|
// Location of the unpack binary inside the endpoint VM
|
|
applianceBinaryPath binaryPath = "/bin/unpack"
|
|
)
|
|
|
|
// UnpackNoChroot 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 UnpackNoChroot(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
|
|
}
|
|
|
|
// OfflineUnpack wraps Unpack for usage in contexts without a childReaper, namely when copying to an offline container with docker cp
|
|
func OfflineUnpack(op trace.Operation, tarStream io.Reader, filter *FilterSpec, root string) error {
|
|
|
|
var cmd *exec.Cmd
|
|
var err error
|
|
if cmd, err = unpack(op, tarStream, filter, root, applianceBinaryPath); err != nil {
|
|
return err
|
|
}
|
|
|
|
if err = cmd.Wait(); err != nil {
|
|
return err
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// OnlineUnpack will extract a tar stream tarStream to folder root inside of a running container
|
|
func OnlineUnpack(op trace.Operation, tarStream io.Reader, filter *FilterSpec, root string) (*exec.Cmd, error) {
|
|
return unpack(op, tarStream, filter, root, containerBinaryPath)
|
|
}
|
|
|
|
func streamCopy(op trace.Operation, stdin io.WriteCloser, tarStream io.Reader) error {
|
|
// if we're passed a stream that doesn't cast to a tar.Reader copy the tarStream to the binary via stdin; the binary will stream it to InvokeUnpack unchanged
|
|
var err error
|
|
tr, ok := tarStream.(*tar.Reader)
|
|
if !ok {
|
|
defer stdin.Close()
|
|
if _, err := io.Copy(stdin, tarStream); err != nil {
|
|
op.Errorf("Error copying tarStream: %s", err.Error())
|
|
return err
|
|
}
|
|
return nil
|
|
}
|
|
|
|
tw := tar.NewWriter(stdin)
|
|
defer tw.Close()
|
|
var th *tar.Header
|
|
for {
|
|
th, err = tr.Next()
|
|
if err == io.EOF {
|
|
tw.Close()
|
|
return nil
|
|
}
|
|
if err != nil {
|
|
op.Errorf("error reading tar header %s", err)
|
|
return err
|
|
}
|
|
op.Debugf("processing tar header: asset(%s), size(%d)", th.Name, th.Size)
|
|
err = tw.WriteHeader(th)
|
|
if err != nil {
|
|
op.Errorf("error writing tar header %s", err)
|
|
return err
|
|
}
|
|
var k int64
|
|
k, err = io.Copy(tw, tr)
|
|
op.Debugf("wrote %d bytes", k)
|
|
if err != nil {
|
|
op.Errorf("error writing file body bytes to stdin %s", err)
|
|
return err
|
|
}
|
|
}
|
|
}
|
|
|
|
func DockerUnpack(op trace.Operation, root string, tarStream io.Reader) (int64, error) {
|
|
fi, err := os.Stat(root)
|
|
if err != nil {
|
|
// the target unpack path does not exist. We should not get here.
|
|
return 0, err
|
|
}
|
|
|
|
if !fi.IsDir() {
|
|
return 0, fmt.Errorf("unpack root target is not a directory: %s", root)
|
|
}
|
|
|
|
// #nosec: 193 applianceBinaryPath is a constant, not a variable
|
|
cmd := exec.Command(string(applianceBinaryPath), root)
|
|
|
|
stdin, err := cmd.StdinPipe()
|
|
|
|
if err != nil {
|
|
return 0, err
|
|
}
|
|
|
|
if stdin == nil {
|
|
err = errors.New("stdin was nil")
|
|
return 0, err
|
|
}
|
|
|
|
if err = cmd.Start(); err != nil {
|
|
return 0, err
|
|
}
|
|
|
|
bytesWritten := make(chan int64, 1)
|
|
go func() {
|
|
defer stdin.Close()
|
|
var n int64
|
|
if n, err = io.Copy(stdin, tarStream); err != nil {
|
|
op.Errorf("Error copying tarStream: %s", err.Error())
|
|
}
|
|
bytesWritten <- n
|
|
}()
|
|
|
|
if err = cmd.Wait(); err != nil {
|
|
return 0, err
|
|
}
|
|
|
|
return <-bytesWritten, nil
|
|
}
|
|
|
|
// Unpack runs the binary compiled in cmd/unpack.go which creates a chroot at `root` and passes `op`, `tarStream`, and `filter` to InvokeUnpack for extraction of the tar on the filesystem. `binPath` should be either ApplianceBinaryPath or ContainerBinaryPath. Unpack returns a `Cmd` to allow use in conjunction with the tether's `LaunchUtility`, so it is necessary to call `cmd.Wait` after `Unpack` exits e.g. OfflineUnpack, if not being used in conjunction with LaunchUtility and the childReaper.
|
|
func unpack(op trace.Operation, tarStream io.Reader, filter *FilterSpec, root string, binPath binaryPath) (*exec.Cmd, 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 nil, err
|
|
}
|
|
|
|
if !fi.IsDir() {
|
|
err := fmt.Errorf("unpack root target is not a directory: %s", root)
|
|
op.Error(err)
|
|
return nil, err
|
|
}
|
|
|
|
encodedFilter, err := EncodeFilterSpec(op, filter)
|
|
if err != nil {
|
|
op.Error(err)
|
|
return nil, 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
|
|
// "/bin/unpack" on appliance
|
|
// "/.tether/unpack" inside container
|
|
cmd := exec.Command(string(binPath), root, *encodedFilter)
|
|
|
|
stdin, err := cmd.StdinPipe()
|
|
|
|
if err != nil {
|
|
op.Error(err)
|
|
return nil, err
|
|
}
|
|
|
|
if stdin == nil {
|
|
err = errors.New("stdin was nil")
|
|
op.Error(err)
|
|
return nil, err
|
|
}
|
|
|
|
go streamCopy(op, stdin, tarStream)
|
|
|
|
return cmd, cmd.Start()
|
|
|
|
}
|