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