github.com/emate/packer@v0.8.1-0.20150625195101-fe0fde195dc6/builder/docker/communicator.go (about)

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