packer-cn/builder/docker/communicator.go

337 lines
8.1 KiB
Go

package docker
import (
"archive/tar"
"fmt"
"io"
"io/ioutil"
"log"
"os"
"os/exec"
"path/filepath"
"strings"
"sync"
"syscall"
"github.com/hashicorp/go-version"
"github.com/hashicorp/packer/packer"
)
type Communicator struct {
ContainerId string
HostDir string
ContainerDir string
Version *version.Version
Config *Config
lock sync.Mutex
}
func (c *Communicator) Start(remote *packer.RemoteCmd) error {
var cmd *exec.Cmd
if c.Config.Pty {
cmd = exec.Command("docker", "exec", "-i", "-t", c.ContainerId, "/bin/sh", "-c", fmt.Sprintf("(%s)", remote.Command))
} else {
cmd = exec.Command("docker", "exec", "-i", c.ContainerId, "/bin/sh", "-c", fmt.Sprintf("(%s)", remote.Command))
}
var (
stdin_w io.WriteCloser
err error
)
stdin_w, err = cmd.StdinPipe()
if err != nil {
return err
}
stderr_r, err := cmd.StderrPipe()
if err != nil {
return err
}
stdout_r, err := cmd.StdoutPipe()
if err != nil {
return err
}
// Run the actual command in a goroutine so that Start doesn't block
go c.run(cmd, remote, stdin_w, stdout_r, stderr_r)
return nil
}
// Upload uploads a file to the docker container
func (c *Communicator) Upload(dst string, src io.Reader, fi *os.FileInfo) error {
if fi == nil {
return c.uploadReader(dst, src)
}
return c.uploadFile(dst, src, fi)
}
// uploadReader writes an io.Reader to a temporary file before uploading
func (c *Communicator) uploadReader(dst string, src io.Reader) error {
// Create a temporary file to store the upload
tempfile, err := ioutil.TempFile(c.HostDir, "upload")
if err != nil {
return fmt.Errorf("Failed to open temp file for writing: %s", err)
}
defer os.Remove(tempfile.Name())
defer tempfile.Close()
if _, err := io.Copy(tempfile, src); err != nil {
return fmt.Errorf("Failed to copy upload file to tempfile: %s", err)
}
tempfile.Seek(0, 0)
fi, err := tempfile.Stat()
if err != nil {
return fmt.Errorf("Error getting tempfile info: %s", err)
}
return c.uploadFile(dst, tempfile, &fi)
}
// uploadFile uses docker cp to copy the file from the host to the container
func (c *Communicator) uploadFile(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)
}
return nil
}
func (c *Communicator) UploadDir(dst string, src string, exclude []string) error {
// Create the temporary directory that will store the contents of "src"
// for copying into the container.
td, err := ioutil.TempDir(c.HostDir, "dirupload")
if err != nil {
return err
}
defer os.RemoveAll(td)
walkFn := func(path string, info os.FileInfo, err error) error {
if err != nil {
return err
}
relpath, err := filepath.Rel(src, path)
if err != nil {
return err
}
hostpath := filepath.Join(td, relpath)
// If it is a directory, just create it
if info.IsDir() {
return os.MkdirAll(hostpath, info.Mode())
}
if info.Mode()&os.ModeSymlink == os.ModeSymlink {
dest, err := os.Readlink(path)
if err != nil {
return err
}
return os.Symlink(dest, hostpath)
}
// It is a file, copy it over, including mode.
src, err := os.Open(path)
if err != nil {
return err
}
defer src.Close()
dst, err := os.Create(hostpath)
if err != nil {
return err
}
defer dst.Close()
if _, err := io.Copy(dst, src); err != nil {
return err
}
si, err := src.Stat()
if err != nil {
return err
}
return dst.Chmod(si.Mode())
}
// Copy the entire directory tree to the temporary directory
if err := filepath.Walk(src, walkFn); err != nil {
return err
}
// Determine the destination directory
containerSrc := filepath.Join(c.ContainerDir, filepath.Base(td))
containerDst := dst
if src[len(src)-1] != '/' {
containerDst = filepath.Join(dst, filepath.Base(src))
}
// Make the directory, then copy into it
cmd := &packer.RemoteCmd{
Command: fmt.Sprintf("set -e; mkdir -p %s; command cp -R %s/. %s",
containerDst, containerSrc, containerDst),
}
if err := c.Start(cmd); err != nil {
return err
}
// Wait for the copy to complete
cmd.Wait()
if cmd.ExitStatus != 0 {
return fmt.Errorf("Upload failed with non-zero exit status: %d", cmd.ExitStatus)
}
return nil
}
// 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
// cp to write to stdout, and then copy the stream to our destination io.Writer.
func (c *Communicator) Download(src string, dst io.Writer) error {
log.Printf("Downloading file from container: %s:%s", c.ContainerId, src)
localCmd := exec.Command("docker", "cp", fmt.Sprintf("%s:%s", c.ContainerId, src), "-")
pipe, err := localCmd.StdoutPipe()
if err != nil {
return fmt.Errorf("Failed to open pipe: %s", err)
}
if err = localCmd.Start(); err != nil {
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 {
return fmt.Errorf("Failed to read header from tar stream: %s", 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
}
func (c *Communicator) DownloadDir(src string, dst string, exclude []string) error {
return fmt.Errorf("DownloadDir is not implemented for docker")
}
// Runs the given command and blocks until completion
func (c *Communicator) run(cmd *exec.Cmd, remote *packer.RemoteCmd, stdin io.WriteCloser, stdout, stderr io.ReadCloser) {
// For Docker, remote communication must be serialized since it
// only supports single execution.
c.lock.Lock()
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)
}