github.com/amanya/packer@v0.12.1-0.20161117214323-902ac5ab2eb6/builder/docker/communicator.go (about) 1 package docker 2 3 import ( 4 "archive/tar" 5 "fmt" 6 "io" 7 "io/ioutil" 8 "log" 9 "os" 10 "os/exec" 11 "path/filepath" 12 "strings" 13 "sync" 14 "syscall" 15 16 "github.com/hashicorp/go-version" 17 "github.com/mitchellh/packer/packer" 18 ) 19 20 type Communicator struct { 21 ContainerId string 22 HostDir string 23 ContainerDir string 24 Version *version.Version 25 Config *Config 26 lock sync.Mutex 27 } 28 29 func (c *Communicator) Start(remote *packer.RemoteCmd) error { 30 var cmd *exec.Cmd 31 if c.Config.Pty { 32 cmd = exec.Command("docker", "exec", "-i", "-t", c.ContainerId, "/bin/sh", "-c", fmt.Sprintf("(%s)", remote.Command)) 33 } else { 34 cmd = exec.Command("docker", "exec", "-i", c.ContainerId, "/bin/sh", "-c", fmt.Sprintf("(%s)", remote.Command)) 35 } 36 37 var ( 38 stdin_w io.WriteCloser 39 err error 40 ) 41 42 stdin_w, err = cmd.StdinPipe() 43 if err != nil { 44 return err 45 } 46 47 stderr_r, err := cmd.StderrPipe() 48 if err != nil { 49 return err 50 } 51 52 stdout_r, err := cmd.StdoutPipe() 53 if err != nil { 54 return err 55 } 56 57 // Run the actual command in a goroutine so that Start doesn't block 58 go c.run(cmd, remote, stdin_w, stdout_r, stderr_r) 59 60 return nil 61 } 62 63 func (c *Communicator) Upload(dst string, src io.Reader, fi *os.FileInfo) error { 64 // Create a temporary file to store the upload 65 tempfile, err := ioutil.TempFile(c.HostDir, "upload") 66 if err != nil { 67 return err 68 } 69 defer os.Remove(tempfile.Name()) 70 71 // Copy the contents to the temporary file 72 _, err = io.Copy(tempfile, src) 73 tempfile.Close() 74 if err != nil { 75 return err 76 } 77 78 // Copy the file into place by copying the temporary file we put 79 // into the shared folder into the proper location in the container 80 cmd := &packer.RemoteCmd{ 81 Command: fmt.Sprintf("command cp %s/%s %s", c.ContainerDir, 82 filepath.Base(tempfile.Name()), dst), 83 } 84 85 if err := c.Start(cmd); err != nil { 86 return err 87 } 88 89 // Wait for the copy to complete 90 cmd.Wait() 91 if cmd.ExitStatus != 0 { 92 return fmt.Errorf("Upload failed with non-zero exit status: %d", cmd.ExitStatus) 93 } 94 95 return nil 96 } 97 98 func (c *Communicator) UploadDir(dst string, src string, exclude []string) error { 99 // Create the temporary directory that will store the contents of "src" 100 // for copying into the container. 101 td, err := ioutil.TempDir(c.HostDir, "dirupload") 102 if err != nil { 103 return err 104 } 105 defer os.RemoveAll(td) 106 107 walkFn := func(path string, info os.FileInfo, err error) error { 108 if err != nil { 109 return err 110 } 111 112 relpath, err := filepath.Rel(src, path) 113 if err != nil { 114 return err 115 } 116 hostpath := filepath.Join(td, relpath) 117 118 // If it is a directory, just create it 119 if info.IsDir() { 120 return os.MkdirAll(hostpath, info.Mode()) 121 } 122 123 if info.Mode()&os.ModeSymlink == os.ModeSymlink { 124 dest, err := os.Readlink(path) 125 126 if err != nil { 127 return err 128 } 129 130 return os.Symlink(dest, hostpath) 131 } 132 133 // It is a file, copy it over, including mode. 134 src, err := os.Open(path) 135 if err != nil { 136 return err 137 } 138 defer src.Close() 139 140 dst, err := os.Create(hostpath) 141 if err != nil { 142 return err 143 } 144 defer dst.Close() 145 146 if _, err := io.Copy(dst, src); err != nil { 147 return err 148 } 149 150 si, err := src.Stat() 151 if err != nil { 152 return err 153 } 154 155 return dst.Chmod(si.Mode()) 156 } 157 158 // Copy the entire directory tree to the temporary directory 159 if err := filepath.Walk(src, walkFn); err != nil { 160 return err 161 } 162 163 // Determine the destination directory 164 containerSrc := filepath.Join(c.ContainerDir, filepath.Base(td)) 165 containerDst := dst 166 if src[len(src)-1] != '/' { 167 containerDst = filepath.Join(dst, filepath.Base(src)) 168 } 169 170 // Make the directory, then copy into it 171 cmd := &packer.RemoteCmd{ 172 Command: fmt.Sprintf("set -e; mkdir -p %s; cd %s; command cp -R `ls -A .` %s", 173 containerDst, containerSrc, containerDst), 174 } 175 if err := c.Start(cmd); err != nil { 176 return err 177 } 178 179 // Wait for the copy to complete 180 cmd.Wait() 181 if cmd.ExitStatus != 0 { 182 return fmt.Errorf("Upload failed with non-zero exit status: %d", cmd.ExitStatus) 183 } 184 185 return nil 186 } 187 188 // Download pulls a file out of a container using `docker cp`. We have a source 189 // path and want to write to an io.Writer, not a file. We use - to make docker 190 // cp to write to stdout, and then copy the stream to our destination io.Writer. 191 func (c *Communicator) Download(src string, dst io.Writer) error { 192 log.Printf("Downloading file from container: %s:%s", c.ContainerId, src) 193 localCmd := exec.Command("docker", "cp", fmt.Sprintf("%s:%s", c.ContainerId, src), "-") 194 195 pipe, err := localCmd.StdoutPipe() 196 if err != nil { 197 return fmt.Errorf("Failed to open pipe: %s", err) 198 } 199 200 if err = localCmd.Start(); err != nil { 201 return fmt.Errorf("Failed to start download: %s", err) 202 } 203 204 // When you use - to send docker cp to stdout it is streamed as a tar; this 205 // enables it to work with directories. We don't actually support 206 // directories in Download() but we still need to handle the tar format. 207 archive := tar.NewReader(pipe) 208 _, err = archive.Next() 209 if err != nil { 210 return fmt.Errorf("Failed to read header from tar stream: %s", err) 211 } 212 213 numBytes, err := io.Copy(dst, archive) 214 if err != nil { 215 return fmt.Errorf("Failed to pipe download: %s", err) 216 } 217 log.Printf("Copied %d bytes for %s", numBytes, src) 218 219 if err = localCmd.Wait(); err != nil { 220 return fmt.Errorf("Failed to download '%s' from container: %s", src, err) 221 } 222 223 return nil 224 } 225 226 func (c *Communicator) DownloadDir(src string, dst string, exclude []string) error { 227 return fmt.Errorf("DownloadDir is not implemented for docker") 228 } 229 230 // Runs the given command and blocks until completion 231 func (c *Communicator) run(cmd *exec.Cmd, remote *packer.RemoteCmd, stdin io.WriteCloser, stdout, stderr io.ReadCloser) { 232 // For Docker, remote communication must be serialized since it 233 // only supports single execution. 234 c.lock.Lock() 235 defer c.lock.Unlock() 236 237 wg := sync.WaitGroup{} 238 repeat := func(w io.Writer, r io.ReadCloser) { 239 io.Copy(w, r) 240 r.Close() 241 wg.Done() 242 } 243 244 if remote.Stdout != nil { 245 wg.Add(1) 246 go repeat(remote.Stdout, stdout) 247 } 248 249 if remote.Stderr != nil { 250 wg.Add(1) 251 go repeat(remote.Stderr, stderr) 252 } 253 254 // Start the command 255 log.Printf("Executing %s:", strings.Join(cmd.Args, " ")) 256 if err := cmd.Start(); err != nil { 257 log.Printf("Error executing: %s", err) 258 remote.SetExited(254) 259 return 260 } 261 262 var exitStatus int 263 264 if remote.Stdin != nil { 265 go func() { 266 io.Copy(stdin, remote.Stdin) 267 // close stdin to support commands that wait for stdin to be closed before exiting. 268 stdin.Close() 269 }() 270 } 271 272 wg.Wait() 273 err := cmd.Wait() 274 275 if exitErr, ok := err.(*exec.ExitError); ok { 276 exitStatus = 1 277 278 // There is no process-independent way to get the REAL 279 // exit status so we just try to go deeper. 280 if status, ok := exitErr.Sys().(syscall.WaitStatus); ok { 281 exitStatus = status.ExitStatus() 282 } 283 } 284 285 // Set the exit status which triggers waiters 286 remote.SetExited(exitStatus) 287 }