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