github.com/kikitux/packer@v0.10.1-0.20160322154024-6237df566f9f/builder/docker/communicator.go (about)

     1  package docker
     2  
     3  import (
     4  	"archive/tar"
     5  	"bytes"
     6  	"fmt"
     7  	"io"
     8  	"io/ioutil"
     9  	"log"
    10  	"os"
    11  	"os/exec"
    12  	"path/filepath"
    13  	"strconv"
    14  	"sync"
    15  	"syscall"
    16  	"time"
    17  
    18  	"github.com/ActiveState/tail"
    19  	"github.com/hashicorp/go-version"
    20  	"github.com/mitchellh/packer/packer"
    21  )
    22  
    23  type Communicator struct {
    24  	ContainerId  string
    25  	HostDir      string
    26  	ContainerDir string
    27  	Version      *version.Version
    28  	Config       *Config
    29  	lock         sync.Mutex
    30  }
    31  
    32  func (c *Communicator) Start(remote *packer.RemoteCmd) error {
    33  	// Create a temporary file to store the output. Because of a bug in
    34  	// Docker, sometimes all the output doesn't properly show up. This
    35  	// file will capture ALL of the output, and we'll read that.
    36  	//
    37  	// https://github.com/dotcloud/docker/issues/2625
    38  	outputFile, err := ioutil.TempFile(c.HostDir, "cmd")
    39  	if err != nil {
    40  		return err
    41  	}
    42  	outputFile.Close()
    43  
    44  	// This file will store the exit code of the command once it is complete.
    45  	exitCodePath := outputFile.Name() + "-exit"
    46  
    47  	var cmd *exec.Cmd
    48  	if c.canExec() {
    49  		if c.Config.Pty {
    50  			cmd = exec.Command("docker", "exec", "-i", "-t", c.ContainerId, "/bin/sh")
    51  		} else {
    52  			cmd = exec.Command("docker", "exec", "-i", c.ContainerId, "/bin/sh")
    53  		}
    54  	} else {
    55  		cmd = exec.Command("docker", "attach", c.ContainerId)
    56  	}
    57  
    58  	stdin_w, err := cmd.StdinPipe()
    59  	if err != nil {
    60  		// We have to do some cleanup since run was never called
    61  		os.Remove(outputFile.Name())
    62  		os.Remove(exitCodePath)
    63  
    64  		return err
    65  	}
    66  
    67  	// Run the actual command in a goroutine so that Start doesn't block
    68  	go c.run(cmd, remote, stdin_w, outputFile, exitCodePath)
    69  
    70  	return nil
    71  }
    72  
    73  func (c *Communicator) Upload(dst string, src io.Reader, fi *os.FileInfo) error {
    74  	// Create a temporary file to store the upload
    75  	tempfile, err := ioutil.TempFile(c.HostDir, "upload")
    76  	if err != nil {
    77  		return err
    78  	}
    79  	defer os.Remove(tempfile.Name())
    80  
    81  	// Copy the contents to the temporary file
    82  	_, err = io.Copy(tempfile, src)
    83  	tempfile.Close()
    84  	if err != nil {
    85  		return err
    86  	}
    87  
    88  	// Copy the file into place by copying the temporary file we put
    89  	// into the shared folder into the proper location in the container
    90  	cmd := &packer.RemoteCmd{
    91  		Command: fmt.Sprintf("command cp %s/%s %s", c.ContainerDir,
    92  			filepath.Base(tempfile.Name()), dst),
    93  	}
    94  
    95  	if err := c.Start(cmd); err != nil {
    96  		return err
    97  	}
    98  
    99  	// Wait for the copy to complete
   100  	cmd.Wait()
   101  	if cmd.ExitStatus != 0 {
   102  		return fmt.Errorf("Upload failed with non-zero exit status: %d", cmd.ExitStatus)
   103  	}
   104  
   105  	return nil
   106  }
   107  
   108  func (c *Communicator) UploadDir(dst string, src string, exclude []string) error {
   109  	// Create the temporary directory that will store the contents of "src"
   110  	// for copying into the container.
   111  	td, err := ioutil.TempDir(c.HostDir, "dirupload")
   112  	if err != nil {
   113  		return err
   114  	}
   115  	defer os.RemoveAll(td)
   116  
   117  	walkFn := func(path string, info os.FileInfo, err error) error {
   118  		if err != nil {
   119  			return err
   120  		}
   121  
   122  		relpath, err := filepath.Rel(src, path)
   123  		if err != nil {
   124  			return err
   125  		}
   126  		hostpath := filepath.Join(td, relpath)
   127  
   128  		// If it is a directory, just create it
   129  		if info.IsDir() {
   130  			return os.MkdirAll(hostpath, info.Mode())
   131  		}
   132  
   133  		if info.Mode()&os.ModeSymlink == os.ModeSymlink {
   134  			dest, err := os.Readlink(path)
   135  
   136  			if err != nil {
   137  				return err
   138  			}
   139  
   140  			return os.Symlink(dest, hostpath)
   141  		}
   142  
   143  		// It is a file, copy it over, including mode.
   144  		src, err := os.Open(path)
   145  		if err != nil {
   146  			return err
   147  		}
   148  		defer src.Close()
   149  
   150  		dst, err := os.Create(hostpath)
   151  		if err != nil {
   152  			return err
   153  		}
   154  		defer dst.Close()
   155  
   156  		if _, err := io.Copy(dst, src); err != nil {
   157  			return err
   158  		}
   159  
   160  		si, err := src.Stat()
   161  		if err != nil {
   162  			return err
   163  		}
   164  
   165  		return dst.Chmod(si.Mode())
   166  	}
   167  
   168  	// Copy the entire directory tree to the temporary directory
   169  	if err := filepath.Walk(src, walkFn); err != nil {
   170  		return err
   171  	}
   172  
   173  	// Determine the destination directory
   174  	containerSrc := filepath.Join(c.ContainerDir, filepath.Base(td))
   175  	containerDst := dst
   176  	if src[len(src)-1] != '/' {
   177  		containerDst = filepath.Join(dst, filepath.Base(src))
   178  	}
   179  
   180  	// Make the directory, then copy into it
   181  	cmd := &packer.RemoteCmd{
   182  		Command: fmt.Sprintf("set -e; mkdir -p %s; command cp -R %s/* %s",
   183  			containerDst, containerSrc, containerDst),
   184  	}
   185  	if err := c.Start(cmd); err != nil {
   186  		return err
   187  	}
   188  
   189  	// Wait for the copy to complete
   190  	cmd.Wait()
   191  	if cmd.ExitStatus != 0 {
   192  		return fmt.Errorf("Upload failed with non-zero exit status: %d", cmd.ExitStatus)
   193  	}
   194  
   195  	return nil
   196  }
   197  
   198  // Download pulls a file out of a container using `docker cp`. We have a source
   199  // path and want to write to an io.Writer, not a file. We use - to make docker
   200  // cp to write to stdout, and then copy the stream to our destination io.Writer.
   201  func (c *Communicator) Download(src string, dst io.Writer) error {
   202  	log.Printf("Downloading file from container: %s:%s", c.ContainerId, src)
   203  	localCmd := exec.Command("docker", "cp", fmt.Sprintf("%s:%s", c.ContainerId, src), "-")
   204  
   205  	pipe, err := localCmd.StdoutPipe()
   206  	if err != nil {
   207  		return fmt.Errorf("Failed to open pipe: %s", err)
   208  	}
   209  
   210  	if err = localCmd.Start(); err != nil {
   211  		return fmt.Errorf("Failed to start download: %s", err)
   212  	}
   213  
   214  	// When you use - to send docker cp to stdout it is streamed as a tar; this
   215  	// enables it to work with directories. We don't actually support
   216  	// directories in Download() but we still need to handle the tar format.
   217  	archive := tar.NewReader(pipe)
   218  	_, err = archive.Next()
   219  	if err != nil {
   220  		return fmt.Errorf("Failed to read header from tar stream: %s", err)
   221  	}
   222  
   223  	numBytes, err := io.Copy(dst, archive)
   224  	if err != nil {
   225  		return fmt.Errorf("Failed to pipe download: %s", err)
   226  	}
   227  	log.Printf("Copied %d bytes for %s", numBytes, src)
   228  
   229  	if err = localCmd.Wait(); err != nil {
   230  		return fmt.Errorf("Failed to download '%s' from container: %s", src, err)
   231  	}
   232  
   233  	return nil
   234  }
   235  
   236  func (c *Communicator) DownloadDir(src string, dst string, exclude []string) error {
   237  	return fmt.Errorf("DownloadDir is not implemented for docker")
   238  }
   239  
   240  // canExec tells us whether `docker exec` is supported
   241  func (c *Communicator) canExec() bool {
   242  	execConstraint, err := version.NewConstraint(">= 1.4.0")
   243  	if err != nil {
   244  		panic(err)
   245  	}
   246  	return execConstraint.Check(c.Version)
   247  }
   248  
   249  // Runs the given command and blocks until completion
   250  func (c *Communicator) run(cmd *exec.Cmd, remote *packer.RemoteCmd, stdin_w io.WriteCloser, outputFile *os.File, exitCodePath string) {
   251  	// For Docker, remote communication must be serialized since it
   252  	// only supports single execution.
   253  	c.lock.Lock()
   254  	defer c.lock.Unlock()
   255  
   256  	// Clean up after ourselves by removing our temporary files
   257  	defer os.Remove(outputFile.Name())
   258  	defer os.Remove(exitCodePath)
   259  
   260  	// Tail the output file and send the data to the stdout listener
   261  	tail, err := tail.TailFile(outputFile.Name(), tail.Config{
   262  		Poll:   true,
   263  		ReOpen: true,
   264  		Follow: true,
   265  	})
   266  	if err != nil {
   267  		log.Printf("Error tailing output file: %s", err)
   268  		remote.SetExited(254)
   269  		return
   270  	}
   271  	defer tail.Stop()
   272  
   273  	// Modify the remote command so that all the output of the commands
   274  	// go to a single file and so that the exit code is redirected to
   275  	// a single file. This lets us determine both when the command
   276  	// is truly complete (because the file will have data), what the
   277  	// exit status is (because Docker loses it because of the pty, not
   278  	// Docker's fault), and get the output (Docker bug).
   279  	remoteCmd := fmt.Sprintf("(%s) >%s 2>&1; echo $? >%s",
   280  		remote.Command,
   281  		filepath.Join(c.ContainerDir, filepath.Base(outputFile.Name())),
   282  		filepath.Join(c.ContainerDir, filepath.Base(exitCodePath)))
   283  
   284  	// Start the command
   285  	log.Printf("Executing in container %s: %#v", c.ContainerId, remoteCmd)
   286  	if err := cmd.Start(); err != nil {
   287  		log.Printf("Error executing: %s", err)
   288  		remote.SetExited(254)
   289  		return
   290  	}
   291  
   292  	go func() {
   293  		defer stdin_w.Close()
   294  
   295  		// This sleep needs to be here because of the issue linked to below.
   296  		// Basically, without it, Docker will hang on reading stdin forever,
   297  		// and won't see what we write, for some reason.
   298  		//
   299  		// https://github.com/dotcloud/docker/issues/2628
   300  		time.Sleep(2 * time.Second)
   301  
   302  		stdin_w.Write([]byte(remoteCmd + "\n"))
   303  	}()
   304  
   305  	// Start a goroutine to read all the lines out of the logs. These channels
   306  	// allow us to stop the go-routine and wait for it to be stopped.
   307  	stopTailCh := make(chan struct{})
   308  	doneCh := make(chan struct{})
   309  	go func() {
   310  		defer close(doneCh)
   311  
   312  		for {
   313  			select {
   314  			case <-tail.Dead():
   315  				return
   316  			case line := <-tail.Lines:
   317  				if remote.Stdout != nil {
   318  					remote.Stdout.Write([]byte(line.Text + "\n"))
   319  				} else {
   320  					log.Printf("Command stdout: %#v", line.Text)
   321  				}
   322  			case <-time.After(2 * time.Second):
   323  				// If we're done, then return. Otherwise, keep grabbing
   324  				// data. This gives us a chance to flush all the lines
   325  				// out of the tailed file.
   326  				select {
   327  				case <-stopTailCh:
   328  					return
   329  				default:
   330  				}
   331  			}
   332  		}
   333  	}()
   334  
   335  	var exitRaw []byte
   336  	var exitStatus int
   337  	var exitStatusRaw int64
   338  	err = cmd.Wait()
   339  	if exitErr, ok := err.(*exec.ExitError); ok {
   340  		exitStatus = 1
   341  
   342  		// There is no process-independent way to get the REAL
   343  		// exit status so we just try to go deeper.
   344  		if status, ok := exitErr.Sys().(syscall.WaitStatus); ok {
   345  			exitStatus = status.ExitStatus()
   346  		}
   347  
   348  		// Say that we ended, since if Docker itself failed, then
   349  		// the command must've not run, or so we assume
   350  		goto REMOTE_EXIT
   351  	}
   352  
   353  	// Wait for the exit code to appear in our file...
   354  	log.Println("Waiting for exit code to appear for remote command...")
   355  	for {
   356  		fi, err := os.Stat(exitCodePath)
   357  		if err == nil && fi.Size() > 0 {
   358  			break
   359  		}
   360  
   361  		time.Sleep(1 * time.Second)
   362  	}
   363  
   364  	// Read the exit code
   365  	exitRaw, err = ioutil.ReadFile(exitCodePath)
   366  	if err != nil {
   367  		log.Printf("Error executing: %s", err)
   368  		exitStatus = 254
   369  		goto REMOTE_EXIT
   370  	}
   371  
   372  	exitStatusRaw, err = strconv.ParseInt(string(bytes.TrimSpace(exitRaw)), 10, 0)
   373  	if err != nil {
   374  		log.Printf("Error executing: %s", err)
   375  		exitStatus = 254
   376  		goto REMOTE_EXIT
   377  	}
   378  	exitStatus = int(exitStatusRaw)
   379  	log.Printf("Executed command exit status: %d", exitStatus)
   380  
   381  REMOTE_EXIT:
   382  	// Wait for the tail to finish
   383  	close(stopTailCh)
   384  	<-doneCh
   385  
   386  	// Set the exit status which triggers waiters
   387  	remote.SetExited(exitStatus)
   388  }