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