github.com/kimor79/packer@v0.8.7-0.20151221212622-d507b18eb4cf/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  // canExec tells us whether `docker exec` is supported
   237  func (c *Communicator) canExec() bool {
   238  	execConstraint, err := version.NewConstraint(">= 1.4.0")
   239  	if err != nil {
   240  		panic(err)
   241  	}
   242  	return execConstraint.Check(c.Version)
   243  }
   244  
   245  // Runs the given command and blocks until completion
   246  func (c *Communicator) run(cmd *exec.Cmd, remote *packer.RemoteCmd, stdin_w io.WriteCloser, outputFile *os.File, exitCodePath string) {
   247  	// For Docker, remote communication must be serialized since it
   248  	// only supports single execution.
   249  	c.lock.Lock()
   250  	defer c.lock.Unlock()
   251  
   252  	// Clean up after ourselves by removing our temporary files
   253  	defer os.Remove(outputFile.Name())
   254  	defer os.Remove(exitCodePath)
   255  
   256  	// Tail the output file and send the data to the stdout listener
   257  	tail, err := tail.TailFile(outputFile.Name(), tail.Config{
   258  		Poll:   true,
   259  		ReOpen: true,
   260  		Follow: true,
   261  	})
   262  	if err != nil {
   263  		log.Printf("Error tailing output file: %s", err)
   264  		remote.SetExited(254)
   265  		return
   266  	}
   267  	defer tail.Stop()
   268  
   269  	// Modify the remote command so that all the output of the commands
   270  	// go to a single file and so that the exit code is redirected to
   271  	// a single file. This lets us determine both when the command
   272  	// is truly complete (because the file will have data), what the
   273  	// exit status is (because Docker loses it because of the pty, not
   274  	// Docker's fault), and get the output (Docker bug).
   275  	remoteCmd := fmt.Sprintf("(%s) >%s 2>&1; echo $? >%s",
   276  		remote.Command,
   277  		filepath.Join(c.ContainerDir, filepath.Base(outputFile.Name())),
   278  		filepath.Join(c.ContainerDir, filepath.Base(exitCodePath)))
   279  
   280  	// Start the command
   281  	log.Printf("Executing in container %s: %#v", c.ContainerId, remoteCmd)
   282  	if err := cmd.Start(); err != nil {
   283  		log.Printf("Error executing: %s", err)
   284  		remote.SetExited(254)
   285  		return
   286  	}
   287  
   288  	go func() {
   289  		defer stdin_w.Close()
   290  
   291  		// This sleep needs to be here because of the issue linked to below.
   292  		// Basically, without it, Docker will hang on reading stdin forever,
   293  		// and won't see what we write, for some reason.
   294  		//
   295  		// https://github.com/dotcloud/docker/issues/2628
   296  		time.Sleep(2 * time.Second)
   297  
   298  		stdin_w.Write([]byte(remoteCmd + "\n"))
   299  	}()
   300  
   301  	// Start a goroutine to read all the lines out of the logs. These channels
   302  	// allow us to stop the go-routine and wait for it to be stopped.
   303  	stopTailCh := make(chan struct{})
   304  	doneCh := make(chan struct{})
   305  	go func() {
   306  		defer close(doneCh)
   307  
   308  		for {
   309  			select {
   310  			case <-tail.Dead():
   311  				return
   312  			case line := <-tail.Lines:
   313  				if remote.Stdout != nil {
   314  					remote.Stdout.Write([]byte(line.Text + "\n"))
   315  				} else {
   316  					log.Printf("Command stdout: %#v", line.Text)
   317  				}
   318  			case <-time.After(2 * time.Second):
   319  				// If we're done, then return. Otherwise, keep grabbing
   320  				// data. This gives us a chance to flush all the lines
   321  				// out of the tailed file.
   322  				select {
   323  				case <-stopTailCh:
   324  					return
   325  				default:
   326  				}
   327  			}
   328  		}
   329  	}()
   330  
   331  	var exitRaw []byte
   332  	var exitStatus int
   333  	var exitStatusRaw int64
   334  	err = cmd.Wait()
   335  	if exitErr, ok := err.(*exec.ExitError); ok {
   336  		exitStatus = 1
   337  
   338  		// There is no process-independent way to get the REAL
   339  		// exit status so we just try to go deeper.
   340  		if status, ok := exitErr.Sys().(syscall.WaitStatus); ok {
   341  			exitStatus = status.ExitStatus()
   342  		}
   343  
   344  		// Say that we ended, since if Docker itself failed, then
   345  		// the command must've not run, or so we assume
   346  		goto REMOTE_EXIT
   347  	}
   348  
   349  	// Wait for the exit code to appear in our file...
   350  	log.Println("Waiting for exit code to appear for remote command...")
   351  	for {
   352  		fi, err := os.Stat(exitCodePath)
   353  		if err == nil && fi.Size() > 0 {
   354  			break
   355  		}
   356  
   357  		time.Sleep(1 * time.Second)
   358  	}
   359  
   360  	// Read the exit code
   361  	exitRaw, err = ioutil.ReadFile(exitCodePath)
   362  	if err != nil {
   363  		log.Printf("Error executing: %s", err)
   364  		exitStatus = 254
   365  		goto REMOTE_EXIT
   366  	}
   367  
   368  	exitStatusRaw, err = strconv.ParseInt(string(bytes.TrimSpace(exitRaw)), 10, 0)
   369  	if err != nil {
   370  		log.Printf("Error executing: %s", err)
   371  		exitStatus = 254
   372  		goto REMOTE_EXIT
   373  	}
   374  	exitStatus = int(exitStatusRaw)
   375  	log.Printf("Executed command exit status: %d", exitStatus)
   376  
   377  REMOTE_EXIT:
   378  	// Wait for the tail to finish
   379  	close(stopTailCh)
   380  	<-doneCh
   381  
   382  	// Set the exit status which triggers waiters
   383  	remote.SetExited(exitStatus)
   384  }