github.com/sneal/packer@v0.5.2/builder/docker/communicator.go (about)

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