can't use docker cp so call powershell to do this natively. Fix implementation for upload, uploadDir, and download in windows container communicator

This commit is contained in:
Megan Marsh 2019-03-28 09:38:17 -07:00
parent af01860fa9
commit 36f2634352
2 changed files with 34 additions and 179 deletions

View File

@ -33,13 +33,14 @@ func (s *StepConnectDocker) Run(_ context.Context, state multistep.StateBag) mul
// Create the communicator that talks to Docker via various // Create the communicator that talks to Docker via various
// os/exec tricks. // os/exec tricks.
if config.WindowsContainer { if config.WindowsContainer {
comm := &WindowsContainerCommunicator{ comm := &WindowsContainerCommunicator{Communicator{
ContainerID: containerId, ContainerID: containerId,
HostDir: tempDir, HostDir: tempDir,
ContainerDir: config.ContainerDir, ContainerDir: config.ContainerDir,
Version: version, Version: version,
Config: config, Config: config,
ContainerUser: containerUser, ContainerUser: containerUser,
},
} }
state.Put("communicator", comm) state.Put("communicator", comm)

View File

@ -1,7 +1,7 @@
package docker package docker
import ( import (
"archive/tar" "bytes"
"fmt" "fmt"
"io" "io"
"io/ioutil" "io/ioutil"
@ -9,22 +9,12 @@ import (
"os" "os"
"os/exec" "os/exec"
"path/filepath" "path/filepath"
"strings"
"sync"
"syscall"
"github.com/hashicorp/go-version"
"github.com/hashicorp/packer/packer" "github.com/hashicorp/packer/packer"
) )
type WindowsContainerCommunicator struct { type WindowsContainerCommunicator struct {
ContainerID string Communicator
HostDir string
ContainerDir string
Version *version.Version
Config *Config
ContainerUser string
lock sync.Mutex
} }
func (c *WindowsContainerCommunicator) Start(remote *packer.RemoteCmd) error { func (c *WindowsContainerCommunicator) Start(remote *packer.RemoteCmd) error {
@ -87,7 +77,6 @@ func (c *WindowsContainerCommunicator) Upload(dst string, src io.Reader, fi *os.
if err != nil { if err != nil {
return err return err
} }
if fi != nil { if fi != nil {
tempfile.Chmod((*fi).Mode()) tempfile.Chmod((*fi).Mode())
} }
@ -165,12 +154,7 @@ func (c *WindowsContainerCommunicator) UploadDir(dst string, src string, exclude
return err return err
} }
si, err := src.Stat() return nil
if err != nil {
return err
}
return dst.Chmod(si.Mode())
} }
// Copy the entire directory tree to the temporary directory // Copy the entire directory tree to the temporary directory
@ -187,8 +171,8 @@ func (c *WindowsContainerCommunicator) UploadDir(dst string, src string, exclude
// Make the directory, then copy into it // Make the directory, then copy into it
cmd := &packer.RemoteCmd{ cmd := &packer.RemoteCmd{
Command: fmt.Sprintf("set -e; mkdir -p %s; command cp -R %s/ %s", Command: fmt.Sprintf("Copy-Item %s -Destination %s -Recurse",
containerDst, containerSrc, containerDst), containerSrc, containerDst),
} }
if err := c.Start(cmd); err != nil { if err := c.Start(cmd); err != nil {
return err return err
@ -203,96 +187,40 @@ func (c *WindowsContainerCommunicator) UploadDir(dst string, src string, exclude
return nil return nil
} }
func (c *WindowsContainerCommunicator) uploadFileOld(dst string, src io.Reader, fi *os.FileInfo) error {
// command format: docker cp /path/to/infile containerid:/path/to/outfile
log.Printf("Copying to %s on container %s.", dst, c.ContainerID)
localCmd := exec.Command("docker", "cp", "-",
fmt.Sprintf("%s:%s", c.ContainerID, filepath.Dir(dst)))
stderrP, err := localCmd.StderrPipe()
if err != nil {
return fmt.Errorf("Failed to open pipe: %s", err)
}
stdin, err := localCmd.StdinPipe()
if err != nil {
return fmt.Errorf("Failed to open pipe: %s", err)
}
if err := localCmd.Start(); err != nil {
return err
}
archive := tar.NewWriter(stdin)
header, err := tar.FileInfoHeader(*fi, "")
if err != nil {
return err
}
header.Name = filepath.Base(dst)
archive.WriteHeader(header)
numBytes, err := io.Copy(archive, src)
if err != nil {
return fmt.Errorf("Failed to pipe upload: %s", err)
}
log.Printf("Copied %d bytes for %s", numBytes, dst)
if err := archive.Close(); err != nil {
return fmt.Errorf("Failed to close archive: %s", err)
}
if err := stdin.Close(); err != nil {
return fmt.Errorf("Failed to close stdin: %s", err)
}
stderrOut, err := ioutil.ReadAll(stderrP)
if err != nil {
return err
}
if err := localCmd.Wait(); err != nil {
return fmt.Errorf("Failed to upload to '%s' in container: %s. %s.", dst, stderrOut, err)
}
if err := c.fixDestinationOwner(dst); err != nil {
return err
}
return nil
}
// Download pulls a file out of a container using `docker cp`. We have a source // Download pulls a file out of a container using `docker cp`. We have a source
// path and want to write to an io.Writer, not a file. We use - to make docker // path and want to write to an io.Writer
// cp to write to stdout, and then copy the stream to our destination io.Writer.
func (c *WindowsContainerCommunicator) Download(src string, dst io.Writer) error { func (c *WindowsContainerCommunicator) Download(src string, dst io.Writer) error {
log.Printf("Downloading file from container: %s:%s", c.ContainerID, src) log.Printf("Downloading file from container: %s:%s", c.ContainerID, src)
localCmd := exec.Command("docker", "cp", fmt.Sprintf("%s:%s", c.ContainerID, src), "-") // Copy file onto temp file on mounted volume inside container
var stdout, stderr bytes.Buffer
cmd := &packer.RemoteCmd{
Command: fmt.Sprintf("Copy-Item -Path %s -Destination %s/%s", src, c.ContainerDir,
filepath.Base(src)),
Stdout: &stdout,
Stderr: &stderr,
}
if err := c.Start(cmd); err != nil {
return err
}
pipe, err := localCmd.StdoutPipe() // Wait for the copy to complete
cmd.Wait()
if cmd.ExitStatus != 0 {
return fmt.Errorf("Failed to copy file to shared drive: %s, %s, %d", stderr.String(), stdout.String(), cmd.ExitStatus)
}
// Read that copied file into a new file opened on host machine
fsrc, err := os.Open(filepath.Join(c.HostDir, filepath.Base(src)))
if err != nil { if err != nil {
return fmt.Errorf("Failed to open pipe: %s", err) return err
} }
defer fsrc.Close()
defer os.Remove(fsrc.Name())
if err = localCmd.Start(); err != nil { _, err = io.Copy(dst, fsrc)
return fmt.Errorf("Failed to start download: %s", err)
}
// When you use - to send docker cp to stdout it is streamed as a tar; this
// enables it to work with directories. We don't actually support
// directories in Download() but we still need to handle the tar format.
archive := tar.NewReader(pipe)
_, err = archive.Next()
if err != nil { if err != nil {
return fmt.Errorf("Failed to read header from tar stream: %s", err) return err
}
numBytes, err := io.Copy(dst, archive)
if err != nil {
return fmt.Errorf("Failed to pipe download: %s", err)
}
log.Printf("Copied %d bytes for %s", numBytes, src)
if err = localCmd.Wait(); err != nil {
return fmt.Errorf("Failed to download '%s' from container: %s", src, err)
} }
return nil return nil
@ -306,79 +234,5 @@ func (c *WindowsContainerCommunicator) DownloadDir(src string, dst string, exclu
func (c *WindowsContainerCommunicator) run(cmd *exec.Cmd, remote *packer.RemoteCmd, stdin io.WriteCloser, stdout, stderr io.ReadCloser) { func (c *WindowsContainerCommunicator) run(cmd *exec.Cmd, remote *packer.RemoteCmd, stdin io.WriteCloser, stdout, stderr io.ReadCloser) {
// For Docker, remote communication must be serialized since it // For Docker, remote communication must be serialized since it
// only supports single execution. // only supports single execution.
c.lock.Lock() c.Communicator.run(cmd, remote, stdin, stdout, stderr)
defer c.lock.Unlock()
wg := sync.WaitGroup{}
repeat := func(w io.Writer, r io.ReadCloser) {
io.Copy(w, r)
r.Close()
wg.Done()
}
if remote.Stdout != nil {
wg.Add(1)
go repeat(remote.Stdout, stdout)
}
if remote.Stderr != nil {
wg.Add(1)
go repeat(remote.Stderr, stderr)
}
// Start the command
log.Printf("Executing %s:", strings.Join(cmd.Args, " "))
if err := cmd.Start(); err != nil {
log.Printf("Error executing: %s", err)
remote.SetExited(254)
return
}
var exitStatus int
if remote.Stdin != nil {
go func() {
io.Copy(stdin, remote.Stdin)
// close stdin to support commands that wait for stdin to be closed before exiting.
stdin.Close()
}()
}
wg.Wait()
err := cmd.Wait()
if exitErr, ok := err.(*exec.ExitError); ok {
exitStatus = 1
// There is no process-independent way to get the REAL
// exit status so we just try to go deeper.
if status, ok := exitErr.Sys().(syscall.WaitStatus); ok {
exitStatus = status.ExitStatus()
}
}
// Set the exit status which triggers waiters
remote.SetExited(exitStatus)
}
// TODO Workaround for #5307. Remove once #5409 is fixed.
func (c *WindowsContainerCommunicator) fixDestinationOwner(destination string) error {
if !c.Config.FixUploadOwner {
return nil
}
owner := c.ContainerUser
if owner == "" {
owner = "root"
}
chownArgs := []string{
"docker", "exec", "--user", "root", c.ContainerID, "/bin/sh", "-c",
fmt.Sprintf("chown -R %s %s", owner, destination),
}
if output, err := exec.Command(chownArgs[0], chownArgs[1:]...).CombinedOutput(); err != nil {
return fmt.Errorf("Failed to set owner of the uploaded file: %s, %s", err, output)
}
return nil
} }