github.com/amanya/packer@v0.12.1-0.20161117214323-902ac5ab2eb6/builder/docker/communicator.go (about)

     1  package docker
     2  
     3  import (
     4  	"archive/tar"
     5  	"fmt"
     6  	"io"
     7  	"io/ioutil"
     8  	"log"
     9  	"os"
    10  	"os/exec"
    11  	"path/filepath"
    12  	"strings"
    13  	"sync"
    14  	"syscall"
    15  
    16  	"github.com/hashicorp/go-version"
    17  	"github.com/mitchellh/packer/packer"
    18  )
    19  
    20  type Communicator struct {
    21  	ContainerId  string
    22  	HostDir      string
    23  	ContainerDir string
    24  	Version      *version.Version
    25  	Config       *Config
    26  	lock         sync.Mutex
    27  }
    28  
    29  func (c *Communicator) Start(remote *packer.RemoteCmd) error {
    30  	var cmd *exec.Cmd
    31  	if c.Config.Pty {
    32  		cmd = exec.Command("docker", "exec", "-i", "-t", c.ContainerId, "/bin/sh", "-c", fmt.Sprintf("(%s)", remote.Command))
    33  	} else {
    34  		cmd = exec.Command("docker", "exec", "-i", c.ContainerId, "/bin/sh", "-c", fmt.Sprintf("(%s)", remote.Command))
    35  	}
    36  
    37  	var (
    38  		stdin_w io.WriteCloser
    39  		err     error
    40  	)
    41  
    42  	stdin_w, err = cmd.StdinPipe()
    43  	if err != nil {
    44  		return err
    45  	}
    46  
    47  	stderr_r, err := cmd.StderrPipe()
    48  	if err != nil {
    49  		return err
    50  	}
    51  
    52  	stdout_r, err := cmd.StdoutPipe()
    53  	if err != nil {
    54  		return err
    55  	}
    56  
    57  	// Run the actual command in a goroutine so that Start doesn't block
    58  	go c.run(cmd, remote, stdin_w, stdout_r, stderr_r)
    59  
    60  	return nil
    61  }
    62  
    63  func (c *Communicator) Upload(dst string, src io.Reader, fi *os.FileInfo) error {
    64  	// Create a temporary file to store the upload
    65  	tempfile, err := ioutil.TempFile(c.HostDir, "upload")
    66  	if err != nil {
    67  		return err
    68  	}
    69  	defer os.Remove(tempfile.Name())
    70  
    71  	// Copy the contents to the temporary file
    72  	_, err = io.Copy(tempfile, src)
    73  	tempfile.Close()
    74  	if err != nil {
    75  		return err
    76  	}
    77  
    78  	// Copy the file into place by copying the temporary file we put
    79  	// into the shared folder into the proper location in the container
    80  	cmd := &packer.RemoteCmd{
    81  		Command: fmt.Sprintf("command cp %s/%s %s", c.ContainerDir,
    82  			filepath.Base(tempfile.Name()), dst),
    83  	}
    84  
    85  	if err := c.Start(cmd); err != nil {
    86  		return err
    87  	}
    88  
    89  	// Wait for the copy to complete
    90  	cmd.Wait()
    91  	if cmd.ExitStatus != 0 {
    92  		return fmt.Errorf("Upload failed with non-zero exit status: %d", cmd.ExitStatus)
    93  	}
    94  
    95  	return nil
    96  }
    97  
    98  func (c *Communicator) UploadDir(dst string, src string, exclude []string) error {
    99  	// Create the temporary directory that will store the contents of "src"
   100  	// for copying into the container.
   101  	td, err := ioutil.TempDir(c.HostDir, "dirupload")
   102  	if err != nil {
   103  		return err
   104  	}
   105  	defer os.RemoveAll(td)
   106  
   107  	walkFn := func(path string, info os.FileInfo, err error) error {
   108  		if err != nil {
   109  			return err
   110  		}
   111  
   112  		relpath, err := filepath.Rel(src, path)
   113  		if err != nil {
   114  			return err
   115  		}
   116  		hostpath := filepath.Join(td, relpath)
   117  
   118  		// If it is a directory, just create it
   119  		if info.IsDir() {
   120  			return os.MkdirAll(hostpath, info.Mode())
   121  		}
   122  
   123  		if info.Mode()&os.ModeSymlink == os.ModeSymlink {
   124  			dest, err := os.Readlink(path)
   125  
   126  			if err != nil {
   127  				return err
   128  			}
   129  
   130  			return os.Symlink(dest, hostpath)
   131  		}
   132  
   133  		// It is a file, copy it over, including mode.
   134  		src, err := os.Open(path)
   135  		if err != nil {
   136  			return err
   137  		}
   138  		defer src.Close()
   139  
   140  		dst, err := os.Create(hostpath)
   141  		if err != nil {
   142  			return err
   143  		}
   144  		defer dst.Close()
   145  
   146  		if _, err := io.Copy(dst, src); err != nil {
   147  			return err
   148  		}
   149  
   150  		si, err := src.Stat()
   151  		if err != nil {
   152  			return err
   153  		}
   154  
   155  		return dst.Chmod(si.Mode())
   156  	}
   157  
   158  	// Copy the entire directory tree to the temporary directory
   159  	if err := filepath.Walk(src, walkFn); err != nil {
   160  		return err
   161  	}
   162  
   163  	// Determine the destination directory
   164  	containerSrc := filepath.Join(c.ContainerDir, filepath.Base(td))
   165  	containerDst := dst
   166  	if src[len(src)-1] != '/' {
   167  		containerDst = filepath.Join(dst, filepath.Base(src))
   168  	}
   169  
   170  	// Make the directory, then copy into it
   171  	cmd := &packer.RemoteCmd{
   172  		Command: fmt.Sprintf("set -e; mkdir -p %s; cd %s; command cp -R `ls -A .` %s",
   173  			containerDst, containerSrc, containerDst),
   174  	}
   175  	if err := c.Start(cmd); err != nil {
   176  		return err
   177  	}
   178  
   179  	// Wait for the copy to complete
   180  	cmd.Wait()
   181  	if cmd.ExitStatus != 0 {
   182  		return fmt.Errorf("Upload failed with non-zero exit status: %d", cmd.ExitStatus)
   183  	}
   184  
   185  	return nil
   186  }
   187  
   188  // Download pulls a file out of a container using `docker cp`. We have a source
   189  // path and want to write to an io.Writer, not a file. We use - to make docker
   190  // cp to write to stdout, and then copy the stream to our destination io.Writer.
   191  func (c *Communicator) Download(src string, dst io.Writer) error {
   192  	log.Printf("Downloading file from container: %s:%s", c.ContainerId, src)
   193  	localCmd := exec.Command("docker", "cp", fmt.Sprintf("%s:%s", c.ContainerId, src), "-")
   194  
   195  	pipe, err := localCmd.StdoutPipe()
   196  	if err != nil {
   197  		return fmt.Errorf("Failed to open pipe: %s", err)
   198  	}
   199  
   200  	if err = localCmd.Start(); err != nil {
   201  		return fmt.Errorf("Failed to start download: %s", err)
   202  	}
   203  
   204  	// When you use - to send docker cp to stdout it is streamed as a tar; this
   205  	// enables it to work with directories. We don't actually support
   206  	// directories in Download() but we still need to handle the tar format.
   207  	archive := tar.NewReader(pipe)
   208  	_, err = archive.Next()
   209  	if err != nil {
   210  		return fmt.Errorf("Failed to read header from tar stream: %s", err)
   211  	}
   212  
   213  	numBytes, err := io.Copy(dst, archive)
   214  	if err != nil {
   215  		return fmt.Errorf("Failed to pipe download: %s", err)
   216  	}
   217  	log.Printf("Copied %d bytes for %s", numBytes, src)
   218  
   219  	if err = localCmd.Wait(); err != nil {
   220  		return fmt.Errorf("Failed to download '%s' from container: %s", src, err)
   221  	}
   222  
   223  	return nil
   224  }
   225  
   226  func (c *Communicator) DownloadDir(src string, dst string, exclude []string) error {
   227  	return fmt.Errorf("DownloadDir is not implemented for docker")
   228  }
   229  
   230  // Runs the given command and blocks until completion
   231  func (c *Communicator) run(cmd *exec.Cmd, remote *packer.RemoteCmd, stdin io.WriteCloser, stdout, stderr io.ReadCloser) {
   232  	// For Docker, remote communication must be serialized since it
   233  	// only supports single execution.
   234  	c.lock.Lock()
   235  	defer c.lock.Unlock()
   236  
   237  	wg := sync.WaitGroup{}
   238  	repeat := func(w io.Writer, r io.ReadCloser) {
   239  		io.Copy(w, r)
   240  		r.Close()
   241  		wg.Done()
   242  	}
   243  
   244  	if remote.Stdout != nil {
   245  		wg.Add(1)
   246  		go repeat(remote.Stdout, stdout)
   247  	}
   248  
   249  	if remote.Stderr != nil {
   250  		wg.Add(1)
   251  		go repeat(remote.Stderr, stderr)
   252  	}
   253  
   254  	// Start the command
   255  	log.Printf("Executing %s:", strings.Join(cmd.Args, " "))
   256  	if err := cmd.Start(); err != nil {
   257  		log.Printf("Error executing: %s", err)
   258  		remote.SetExited(254)
   259  		return
   260  	}
   261  
   262  	var exitStatus int
   263  
   264  	if remote.Stdin != nil {
   265  		go func() {
   266  			io.Copy(stdin, remote.Stdin)
   267  			// close stdin to support commands that wait for stdin to be closed before exiting.
   268  			stdin.Close()
   269  		}()
   270  	}
   271  
   272  	wg.Wait()
   273  	err := cmd.Wait()
   274  
   275  	if exitErr, ok := err.(*exec.ExitError); ok {
   276  		exitStatus = 1
   277  
   278  		// There is no process-independent way to get the REAL
   279  		// exit status so we just try to go deeper.
   280  		if status, ok := exitErr.Sys().(syscall.WaitStatus); ok {
   281  			exitStatus = status.ExitStatus()
   282  		}
   283  	}
   284  
   285  	// Set the exit status which triggers waiters
   286  	remote.SetExited(exitStatus)
   287  }