github.phpd.cn/hashicorp/packer@v1.3.2/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/hashicorp/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 ContainerUser string 27 lock sync.Mutex 28 } 29 30 func (c *Communicator) Start(remote *packer.RemoteCmd) error { 31 dockerArgs := []string{ 32 "exec", 33 "-i", 34 c.ContainerID, 35 "/bin/sh", 36 "-c", 37 fmt.Sprintf("(%s)", remote.Command), 38 } 39 40 if c.Config.Pty { 41 dockerArgs = append(dockerArgs[:2], append([]string{"-t"}, dockerArgs[2:]...)...) 42 } 43 44 if c.Config.ExecUser != "" { 45 dockerArgs = append(dockerArgs[:2], 46 append([]string{"-u", c.Config.ExecUser}, dockerArgs[2:]...)...) 47 } 48 49 cmd := exec.Command("docker", dockerArgs...) 50 51 var ( 52 stdin_w io.WriteCloser 53 err error 54 ) 55 56 stdin_w, err = cmd.StdinPipe() 57 if err != nil { 58 return err 59 } 60 61 stderr_r, err := cmd.StderrPipe() 62 if err != nil { 63 return err 64 } 65 66 stdout_r, err := cmd.StdoutPipe() 67 if err != nil { 68 return err 69 } 70 71 // Run the actual command in a goroutine so that Start doesn't block 72 go c.run(cmd, remote, stdin_w, stdout_r, stderr_r) 73 74 return nil 75 } 76 77 // Upload uploads a file to the docker container 78 func (c *Communicator) Upload(dst string, src io.Reader, fi *os.FileInfo) error { 79 if fi == nil { 80 return c.uploadReader(dst, src) 81 } 82 return c.uploadFile(dst, src, fi) 83 } 84 85 // uploadReader writes an io.Reader to a temporary file before uploading 86 func (c *Communicator) uploadReader(dst string, src io.Reader) error { 87 // Create a temporary file to store the upload 88 tempfile, err := ioutil.TempFile(c.HostDir, "upload") 89 if err != nil { 90 return fmt.Errorf("Failed to open temp file for writing: %s", err) 91 } 92 defer os.Remove(tempfile.Name()) 93 defer tempfile.Close() 94 95 if _, err := io.Copy(tempfile, src); err != nil { 96 return fmt.Errorf("Failed to copy upload file to tempfile: %s", err) 97 } 98 tempfile.Seek(0, 0) 99 fi, err := tempfile.Stat() 100 if err != nil { 101 return fmt.Errorf("Error getting tempfile info: %s", err) 102 } 103 return c.uploadFile(dst, tempfile, &fi) 104 } 105 106 // uploadFile uses docker cp to copy the file from the host to the container 107 func (c *Communicator) uploadFile(dst string, src io.Reader, fi *os.FileInfo) error { 108 // command format: docker cp /path/to/infile containerid:/path/to/outfile 109 log.Printf("Copying to %s on container %s.", dst, c.ContainerID) 110 111 localCmd := exec.Command("docker", "cp", "-", 112 fmt.Sprintf("%s:%s", c.ContainerID, filepath.Dir(dst))) 113 114 stderrP, err := localCmd.StderrPipe() 115 if err != nil { 116 return fmt.Errorf("Failed to open pipe: %s", err) 117 } 118 119 stdin, err := localCmd.StdinPipe() 120 if err != nil { 121 return fmt.Errorf("Failed to open pipe: %s", err) 122 } 123 124 if err := localCmd.Start(); err != nil { 125 return err 126 } 127 128 archive := tar.NewWriter(stdin) 129 header, err := tar.FileInfoHeader(*fi, "") 130 if err != nil { 131 return err 132 } 133 header.Name = filepath.Base(dst) 134 archive.WriteHeader(header) 135 numBytes, err := io.Copy(archive, src) 136 if err != nil { 137 return fmt.Errorf("Failed to pipe upload: %s", err) 138 } 139 log.Printf("Copied %d bytes for %s", numBytes, dst) 140 141 if err := archive.Close(); err != nil { 142 return fmt.Errorf("Failed to close archive: %s", err) 143 } 144 if err := stdin.Close(); err != nil { 145 return fmt.Errorf("Failed to close stdin: %s", err) 146 } 147 148 stderrOut, err := ioutil.ReadAll(stderrP) 149 if err != nil { 150 return err 151 } 152 153 if err := localCmd.Wait(); err != nil { 154 return fmt.Errorf("Failed to upload to '%s' in container: %s. %s.", dst, stderrOut, err) 155 } 156 157 if err := c.fixDestinationOwner(dst); err != nil { 158 return err 159 } 160 161 return nil 162 } 163 164 func (c *Communicator) UploadDir(dst string, src string, exclude []string) error { 165 /* 166 from https://docs.docker.com/engine/reference/commandline/cp/#extended-description 167 SRC_PATH specifies a directory 168 DEST_PATH does not exist 169 DEST_PATH is created as a directory and the contents of the source directory are copied into this directory 170 DEST_PATH exists and is a file 171 Error condition: cannot copy a directory to a file 172 DEST_PATH exists and is a directory 173 SRC_PATH does not end with /. (that is: slash followed by dot) 174 the source directory is copied into this directory 175 SRC_PATH does end with /. (that is: slash followed by dot) 176 the content of the source directory is copied into this directory 177 178 translating that in to our semantics: 179 180 if source ends in / 181 docker cp src. dest 182 otherwise, cp source dest 183 184 */ 185 186 var dockerSource string 187 188 if src[len(src)-1] == '/' { 189 dockerSource = fmt.Sprintf("%s.", src) 190 } else { 191 dockerSource = fmt.Sprintf("%s", src) 192 } 193 194 // Make the directory, then copy into it 195 localCmd := exec.Command("docker", "cp", dockerSource, fmt.Sprintf("%s:%s", c.ContainerID, dst)) 196 197 stderrP, err := localCmd.StderrPipe() 198 if err != nil { 199 return fmt.Errorf("Failed to open pipe: %s", err) 200 } 201 if err := localCmd.Start(); err != nil { 202 return fmt.Errorf("Failed to copy: %s", err) 203 } 204 stderrOut, err := ioutil.ReadAll(stderrP) 205 if err != nil { 206 return err 207 } 208 209 // Wait for the copy to complete 210 if err := localCmd.Wait(); err != nil { 211 return fmt.Errorf("Failed to upload to '%s' in container: %s. %s.", dst, stderrOut, err) 212 } 213 214 if err := c.fixDestinationOwner(dst); err != nil { 215 return err 216 } 217 218 return nil 219 } 220 221 // Download pulls a file out of a container using `docker cp`. We have a source 222 // path and want to write to an io.Writer, not a file. We use - to make docker 223 // cp to write to stdout, and then copy the stream to our destination io.Writer. 224 func (c *Communicator) Download(src string, dst io.Writer) error { 225 log.Printf("Downloading file from container: %s:%s", c.ContainerID, src) 226 localCmd := exec.Command("docker", "cp", fmt.Sprintf("%s:%s", c.ContainerID, src), "-") 227 228 pipe, err := localCmd.StdoutPipe() 229 if err != nil { 230 return fmt.Errorf("Failed to open pipe: %s", err) 231 } 232 233 if err = localCmd.Start(); err != nil { 234 return fmt.Errorf("Failed to start download: %s", err) 235 } 236 237 // When you use - to send docker cp to stdout it is streamed as a tar; this 238 // enables it to work with directories. We don't actually support 239 // directories in Download() but we still need to handle the tar format. 240 archive := tar.NewReader(pipe) 241 _, err = archive.Next() 242 if err != nil { 243 return fmt.Errorf("Failed to read header from tar stream: %s", err) 244 } 245 246 numBytes, err := io.Copy(dst, archive) 247 if err != nil { 248 return fmt.Errorf("Failed to pipe download: %s", err) 249 } 250 log.Printf("Copied %d bytes for %s", numBytes, src) 251 252 if err = localCmd.Wait(); err != nil { 253 return fmt.Errorf("Failed to download '%s' from container: %s", src, err) 254 } 255 256 return nil 257 } 258 259 func (c *Communicator) DownloadDir(src string, dst string, exclude []string) error { 260 return fmt.Errorf("DownloadDir is not implemented for docker") 261 } 262 263 // Runs the given command and blocks until completion 264 func (c *Communicator) run(cmd *exec.Cmd, remote *packer.RemoteCmd, stdin io.WriteCloser, stdout, stderr io.ReadCloser) { 265 // For Docker, remote communication must be serialized since it 266 // only supports single execution. 267 c.lock.Lock() 268 defer c.lock.Unlock() 269 270 wg := sync.WaitGroup{} 271 repeat := func(w io.Writer, r io.ReadCloser) { 272 io.Copy(w, r) 273 r.Close() 274 wg.Done() 275 } 276 277 if remote.Stdout != nil { 278 wg.Add(1) 279 go repeat(remote.Stdout, stdout) 280 } 281 282 if remote.Stderr != nil { 283 wg.Add(1) 284 go repeat(remote.Stderr, stderr) 285 } 286 287 // Start the command 288 log.Printf("Executing %s:", strings.Join(cmd.Args, " ")) 289 if err := cmd.Start(); err != nil { 290 log.Printf("Error executing: %s", err) 291 remote.SetExited(254) 292 return 293 } 294 295 var exitStatus int 296 297 if remote.Stdin != nil { 298 go func() { 299 io.Copy(stdin, remote.Stdin) 300 // close stdin to support commands that wait for stdin to be closed before exiting. 301 stdin.Close() 302 }() 303 } 304 305 wg.Wait() 306 err := cmd.Wait() 307 308 if exitErr, ok := err.(*exec.ExitError); ok { 309 exitStatus = 1 310 311 // There is no process-independent way to get the REAL 312 // exit status so we just try to go deeper. 313 if status, ok := exitErr.Sys().(syscall.WaitStatus); ok { 314 exitStatus = status.ExitStatus() 315 } 316 } 317 318 // Set the exit status which triggers waiters 319 remote.SetExited(exitStatus) 320 } 321 322 // TODO Workaround for #5307. Remove once #5409 is fixed. 323 func (c *Communicator) fixDestinationOwner(destination string) error { 324 if !c.Config.FixUploadOwner { 325 return nil 326 } 327 328 owner := c.ContainerUser 329 if owner == "" { 330 owner = "root" 331 } 332 333 chownArgs := []string{ 334 "docker", "exec", "--user", "root", c.ContainerID, "/bin/sh", "-c", 335 fmt.Sprintf("chown -R %s %s", owner, destination), 336 } 337 if output, err := exec.Command(chownArgs[0], chownArgs[1:]...).CombinedOutput(); err != nil { 338 return fmt.Errorf("Failed to set owner of the uploaded file: %s, %s", err, output) 339 } 340 341 return nil 342 }