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 }