github.com/askholme/packer@v0.7.2-0.20140924152349-70d9566a6852/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  		// It is a file, copy it over, including mode.
   121  		src, err := os.Open(path)
   122  		if err != nil {
   123  			return err
   124  		}
   125  		defer src.Close()
   126  
   127  		dst, err := os.Create(hostpath)
   128  		if err != nil {
   129  			return err
   130  		}
   131  		defer dst.Close()
   132  
   133  		if _, err := io.Copy(dst, src); err != nil {
   134  			return err
   135  		}
   136  
   137  		si, err := src.Stat()
   138  		if err != nil {
   139  			return err
   140  		}
   141  
   142  		return dst.Chmod(si.Mode())
   143  	}
   144  
   145  	// Copy the entire directory tree to the temporary directory
   146  	if err := filepath.Walk(src, walkFn); err != nil {
   147  		return err
   148  	}
   149  
   150  	// Determine the destination directory
   151  	containerSrc := filepath.Join(c.ContainerDir, filepath.Base(td))
   152  	containerDst := dst
   153  	if src[len(src)-1] != '/' {
   154  		containerDst = filepath.Join(dst, filepath.Base(src))
   155  	}
   156  
   157  	// Make the directory, then copy into it
   158  	cmd := &packer.RemoteCmd{
   159  		Command: fmt.Sprintf("set -e; mkdir -p %s; cp -R %s/* %s",
   160  			containerDst, containerSrc, containerDst),
   161  	}
   162  	if err := c.Start(cmd); err != nil {
   163  		return err
   164  	}
   165  
   166  	// Wait for the copy to complete
   167  	cmd.Wait()
   168  	if cmd.ExitStatus != 0 {
   169  		return fmt.Errorf("Upload failed with non-zero exit status: %d", cmd.ExitStatus)
   170  	}
   171  
   172  	return nil
   173  }
   174  
   175  func (c *Communicator) Download(src string, dst io.Writer) error {
   176  	panic("not implemented")
   177  }
   178  
   179  // Runs the given command and blocks until completion
   180  func (c *Communicator) run(cmd *exec.Cmd, remote *packer.RemoteCmd, stdin_w io.WriteCloser, outputFile *os.File, exitCodePath string) {
   181  	// For Docker, remote communication must be serialized since it
   182  	// only supports single execution.
   183  	c.lock.Lock()
   184  	defer c.lock.Unlock()
   185  
   186  	// Clean up after ourselves by removing our temporary files
   187  	defer os.Remove(outputFile.Name())
   188  	defer os.Remove(exitCodePath)
   189  
   190  	// Tail the output file and send the data to the stdout listener
   191  	tail, err := tail.TailFile(outputFile.Name(), tail.Config{
   192  		Poll:   true,
   193  		ReOpen: true,
   194  		Follow: true,
   195  	})
   196  	if err != nil {
   197  		log.Printf("Error tailing output file: %s", err)
   198  		remote.SetExited(254)
   199  		return
   200  	}
   201  	defer tail.Stop()
   202  
   203  	// Modify the remote command so that all the output of the commands
   204  	// go to a single file and so that the exit code is redirected to
   205  	// a single file. This lets us determine both when the command
   206  	// is truly complete (because the file will have data), what the
   207  	// exit status is (because Docker loses it because of the pty, not
   208  	// Docker's fault), and get the output (Docker bug).
   209  	remoteCmd := fmt.Sprintf("(%s) >%s 2>&1; echo $? >%s",
   210  		remote.Command,
   211  		filepath.Join(c.ContainerDir, filepath.Base(outputFile.Name())),
   212  		filepath.Join(c.ContainerDir, filepath.Base(exitCodePath)))
   213  
   214  	// Start the command
   215  	log.Printf("Executing in container %s: %#v", c.ContainerId, remoteCmd)
   216  	if err := cmd.Start(); err != nil {
   217  		log.Printf("Error executing: %s", err)
   218  		remote.SetExited(254)
   219  		return
   220  	}
   221  
   222  	go func() {
   223  		defer stdin_w.Close()
   224  
   225  		// This sleep needs to be here because of the issue linked to below.
   226  		// Basically, without it, Docker will hang on reading stdin forever,
   227  		// and won't see what we write, for some reason.
   228  		//
   229  		// https://github.com/dotcloud/docker/issues/2628
   230  		time.Sleep(2 * time.Second)
   231  
   232  		stdin_w.Write([]byte(remoteCmd + "\n"))
   233  	}()
   234  
   235  	// Start a goroutine to read all the lines out of the logs. These channels
   236  	// allow us to stop the go-routine and wait for it to be stopped.
   237  	stopTailCh := make(chan struct{})
   238  	doneCh := make(chan struct{})
   239  	go func() {
   240  		defer close(doneCh)
   241  
   242  		for {
   243  			select {
   244  			case <-tail.Dead():
   245  				return
   246  			case line := <-tail.Lines:
   247  				if remote.Stdout != nil {
   248  					remote.Stdout.Write([]byte(line.Text + "\n"))
   249  				} else {
   250  					log.Printf("Command stdout: %#v", line.Text)
   251  				}
   252  			case <-time.After(2 * time.Second):
   253  				// If we're done, then return. Otherwise, keep grabbing
   254  				// data. This gives us a chance to flush all the lines
   255  				// out of the tailed file.
   256  				select {
   257  				case <-stopTailCh:
   258  					return
   259  				default:
   260  				}
   261  			}
   262  		}
   263  	}()
   264  
   265  	var exitRaw []byte
   266  	var exitStatus int
   267  	var exitStatusRaw int64
   268  	err = cmd.Wait()
   269  	if exitErr, ok := err.(*exec.ExitError); ok {
   270  		exitStatus = 1
   271  
   272  		// There is no process-independent way to get the REAL
   273  		// exit status so we just try to go deeper.
   274  		if status, ok := exitErr.Sys().(syscall.WaitStatus); ok {
   275  			exitStatus = status.ExitStatus()
   276  		}
   277  
   278  		// Say that we ended, since if Docker itself failed, then
   279  		// the command must've not run, or so we assume
   280  		goto REMOTE_EXIT
   281  	}
   282  
   283  	// Wait for the exit code to appear in our file...
   284  	log.Println("Waiting for exit code to appear for remote command...")
   285  	for {
   286  		fi, err := os.Stat(exitCodePath)
   287  		if err == nil && fi.Size() > 0 {
   288  			break
   289  		}
   290  
   291  		time.Sleep(1 * time.Second)
   292  	}
   293  
   294  	// Read the exit code
   295  	exitRaw, err = ioutil.ReadFile(exitCodePath)
   296  	if err != nil {
   297  		log.Printf("Error executing: %s", err)
   298  		exitStatus = 254
   299  		goto REMOTE_EXIT
   300  	}
   301  
   302  	exitStatusRaw, err = strconv.ParseInt(string(bytes.TrimSpace(exitRaw)), 10, 0)
   303  	if err != nil {
   304  		log.Printf("Error executing: %s", err)
   305  		exitStatus = 254
   306  		goto REMOTE_EXIT
   307  	}
   308  	exitStatus = int(exitStatusRaw)
   309  	log.Printf("Executed command exit status: %d", exitStatus)
   310  
   311  REMOTE_EXIT:
   312  	// Wait for the tail to finish
   313  	close(stopTailCh)
   314  	<-doneCh
   315  
   316  	// Set the exit status which triggers waiters
   317  	remote.SetExited(exitStatus)
   318  }