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