From 8f3313d81e9712007db1d3202b8116522a7b5148 Mon Sep 17 00:00:00 2001 From: Megan Marsh Date: Wed, 27 Mar 2019 12:09:08 -0700 Subject: [PATCH 01/12] Create new docker communicator for windows containers --- builder/docker/builder.go | 3 +- builder/docker/config.go | 34 +- builder/docker/driver_docker.go | 12 +- .../docker/windows_container_communicator.go | 441 ++++++++++++++++++ helper/communicator/config.go | 2 +- 5 files changed, 469 insertions(+), 23 deletions(-) create mode 100644 builder/docker/windows_container_communicator.go diff --git a/builder/docker/builder.go b/builder/docker/builder.go index f6a6e4765..8d3382c2e 100644 --- a/builder/docker/builder.go +++ b/builder/docker/builder.go @@ -50,7 +50,8 @@ func (b *Builder) Run(ui packer.Ui, hook packer.Hook) (packer.Artifact, error) { Host: commHost, SSHConfig: b.config.Comm.SSHConfigFunc(), CustomConnect: map[string]multistep.Step{ - "docker": &StepConnectDocker{}, + "docker": &StepConnectDocker{}, + "dockerWindowsContainer": &StepConnectDocker{}, }, }, &common.StepProvision{}, diff --git a/builder/docker/config.go b/builder/docker/config.go index ef2d52a63..d7b2cd50c 100644 --- a/builder/docker/config.go +++ b/builder/docker/config.go @@ -23,21 +23,22 @@ type Config struct { common.PackerConfig `mapstructure:",squash"` Comm communicator.Config `mapstructure:",squash"` - Author string - Changes []string - Commit bool - ContainerDir string `mapstructure:"container_dir"` - Discard bool - ExecUser string `mapstructure:"exec_user"` - ExportPath string `mapstructure:"export_path"` - Image string - Message string - Privileged bool `mapstructure:"privileged"` - Pty bool - Pull bool - RunCommand []string `mapstructure:"run_command"` - Volumes map[string]string - FixUploadOwner bool `mapstructure:"fix_upload_owner"` + Author string + Changes []string + Commit bool + ContainerDir string `mapstructure:"container_dir"` + Discard bool + ExecUser string `mapstructure:"exec_user"` + ExportPath string `mapstructure:"export_path"` + Image string + Message string + Privileged bool `mapstructure:"privileged"` + Pty bool + Pull bool + RunCommand []string `mapstructure:"run_command"` + Volumes map[string]string + FixUploadOwner bool `mapstructure:"fix_upload_owner"` + WindowsContainer bool `windows_container` // This is used to login to dockerhub to pull a private base container. For // pushing to dockerhub, see the docker post-processors @@ -92,6 +93,9 @@ func NewConfig(raws ...interface{}) (*Config, []string, error) { // Default to the normal Docker type if c.Comm.Type == "" { c.Comm.Type = "docker" + if c.WindowsContainer { + c.Comm.Type = "dockerWindowsContainer" + } } var errs *packer.MultiError diff --git a/builder/docker/driver_docker.go b/builder/docker/driver_docker.go index 918dfdb2c..9b0a4556b 100644 --- a/builder/docker/driver_docker.go +++ b/builder/docker/driver_docker.go @@ -8,7 +8,7 @@ import ( "os" "os/exec" "regexp" - "runtime" + // "runtime" "strings" "sync" @@ -270,11 +270,11 @@ func (d *DockerDriver) StartContainer(config *ContainerConfig) (string, error) { args = append(args, "--privileged") } for host, guest := range config.Volumes { - if runtime.GOOS == "windows" { - // docker-toolbox can't handle the normal C:\filepath format in CLI - host = strings.Replace(host, "\\", "/", -1) - host = strings.Replace(host, "C:/", "/c/", 1) - } + // if runtime.GOOS == "windows" { + // // docker-toolbox can't handle the normal C:\filepath format in CLI + // host = strings.Replace(host, "\\", "/", -1) + // host = strings.Replace(host, "C:/", "/c/", 1) + // } args = append(args, "-v", fmt.Sprintf("%s:%s", host, guest)) } for _, v := range config.RunCommand { diff --git a/builder/docker/windows_container_communicator.go b/builder/docker/windows_container_communicator.go new file mode 100644 index 000000000..88054429c --- /dev/null +++ b/builder/docker/windows_container_communicator.go @@ -0,0 +1,441 @@ +package docker + +import ( + "archive/tar" + "fmt" + "io" + "io/ioutil" + "log" + "os" + "os/exec" + "path/filepath" + "strings" + "sync" + "syscall" + + "github.com/hashicorp/go-version" + "github.com/hashicorp/packer/packer" +) + +type WindowsContainerCommunicator struct { + ContainerID string + HostDir string + ContainerDir string + Version *version.Version + Config *Config + ContainerUser string + lock sync.Mutex +} + +func (c *WindowsContainerCommunicator) Start(remote *packer.RemoteCmd) error { + dockerArgs := []string{ + "exec", + "-i", + c.ContainerID, + "powershell", + fmt.Sprintf("(%s)", remote.Command), + } + + if c.Config.Pty { + dockerArgs = append(dockerArgs[:2], append([]string{"-t"}, dockerArgs[2:]...)...) + } + + if c.Config.ExecUser != "" { + dockerArgs = append(dockerArgs[:2], + append([]string{"-u", c.Config.ExecUser}, dockerArgs[2:]...)...) + } + + cmd := exec.Command("docker", dockerArgs...) + + var ( + stdin_w io.WriteCloser + err error + ) + + stdin_w, err = cmd.StdinPipe() + if err != nil { + return err + } + + stderr_r, err := cmd.StderrPipe() + if err != nil { + return err + } + + stdout_r, err := cmd.StdoutPipe() + if err != nil { + return err + } + + // Run the actual command in a goroutine so that Start doesn't block + go c.run(cmd, remote, stdin_w, stdout_r, stderr_r) + + return nil +} + +// Upload uses docker exec to copy the file from the host to the container +func (c *WindowsContainerCommunicator) Upload(dst string, src io.Reader, fi *os.FileInfo) error { + // Create a temporary file to store the upload + tempfile, err := ioutil.TempFile(c.HostDir, "upload") + if err != nil { + return err + } + defer os.Remove(tempfile.Name()) + + // Copy the contents to the temporary file + _, err = io.Copy(tempfile, src) + if err != nil { + return err + } + + if fi != nil { + tempfile.Chmod((*fi).Mode()) + } + tempfile.Close() + + // Copy the file into place by copying the temporary file we put + // into the shared folder into the proper location in the container + cmd := &packer.RemoteCmd{ + Command: fmt.Sprintf("Copy-Item -Path %s/%s -Destination %s", c.ContainerDir, + filepath.Base(tempfile.Name()), dst), + } + + if err := c.Start(cmd); err != nil { + return err + } + + // Wait for the copy to complete + cmd.Wait() + if cmd.ExitStatus != 0 { + return fmt.Errorf("Upload failed with non-zero exit status: %d", cmd.ExitStatus) + } + + return nil +} + +func (c *WindowsContainerCommunicator) UploadDir(dst string, src string, exclude []string) error { + // Create the temporary directory that will store the contents of "src" + // for copying into the container. + td, err := ioutil.TempDir(c.HostDir, "dirupload") + if err != nil { + return err + } + defer os.RemoveAll(td) + + walkFn := func(path string, info os.FileInfo, err error) error { + if err != nil { + return err + } + + relpath, err := filepath.Rel(src, path) + if err != nil { + return err + } + hostpath := filepath.Join(td, relpath) + + // If it is a directory, just create it + if info.IsDir() { + return os.MkdirAll(hostpath, info.Mode()) + } + + if info.Mode()&os.ModeSymlink == os.ModeSymlink { + dest, err := os.Readlink(path) + + if err != nil { + return err + } + + return os.Symlink(dest, hostpath) + } + + // It is a file, copy it over, including mode. + src, err := os.Open(path) + if err != nil { + return err + } + defer src.Close() + + dst, err := os.Create(hostpath) + if err != nil { + return err + } + defer dst.Close() + + if _, err := io.Copy(dst, src); err != nil { + return err + } + + si, err := src.Stat() + if err != nil { + return err + } + + return dst.Chmod(si.Mode()) + } + + // Copy the entire directory tree to the temporary directory + if err := filepath.Walk(src, walkFn); err != nil { + return err + } + + // Determine the destination directory + containerSrc := filepath.Join(c.ContainerDir, filepath.Base(td)) + containerDst := dst + if src[len(src)-1] != '/' { + containerDst = filepath.Join(dst, filepath.Base(src)) + } + + // Make the directory, then copy into it + cmd := &packer.RemoteCmd{ + Command: fmt.Sprintf("set -e; mkdir -p %s; command cp -R %s/ %s", + containerDst, containerSrc, containerDst), + } + if err := c.Start(cmd); err != nil { + return err + } + + // Wait for the copy to complete + cmd.Wait() + if cmd.ExitStatus != 0 { + return fmt.Errorf("Upload failed with non-zero exit status: %d", cmd.ExitStatus) + } + + return nil +} + +func (c *WindowsContainerCommunicator) uploadFileOld(dst string, src io.Reader, fi *os.FileInfo) error { + // command format: docker cp /path/to/infile containerid:/path/to/outfile + log.Printf("Copying to %s on container %s.", dst, c.ContainerID) + + localCmd := exec.Command("docker", "cp", "-", + fmt.Sprintf("%s:%s", c.ContainerID, filepath.Dir(dst))) + + stderrP, err := localCmd.StderrPipe() + if err != nil { + return fmt.Errorf("Failed to open pipe: %s", err) + } + + stdin, err := localCmd.StdinPipe() + if err != nil { + return fmt.Errorf("Failed to open pipe: %s", err) + } + + if err := localCmd.Start(); err != nil { + return err + } + + archive := tar.NewWriter(stdin) + header, err := tar.FileInfoHeader(*fi, "") + if err != nil { + return err + } + header.Name = filepath.Base(dst) + archive.WriteHeader(header) + numBytes, err := io.Copy(archive, src) + if err != nil { + return fmt.Errorf("Failed to pipe upload: %s", err) + } + log.Printf("Copied %d bytes for %s", numBytes, dst) + + if err := archive.Close(); err != nil { + return fmt.Errorf("Failed to close archive: %s", err) + } + if err := stdin.Close(); err != nil { + return fmt.Errorf("Failed to close stdin: %s", err) + } + + stderrOut, err := ioutil.ReadAll(stderrP) + if err != nil { + return err + } + + if err := localCmd.Wait(); err != nil { + return fmt.Errorf("Failed to upload to '%s' in container: %s. %s.", dst, stderrOut, err) + } + + if err := c.fixDestinationOwner(dst); err != nil { + return err + } + + return nil +} + +func (c *WindowsContainerCommunicator) UploadDir(dst string, src string, exclude []string) error { + /* + from https://docs.docker.com/engine/reference/commandline/cp/#extended-description + SRC_PATH specifies a directory + DEST_PATH does not exist + DEST_PATH is created as a directory and the contents of the source directory are copied into this directory + DEST_PATH exists and is a file + Error condition: cannot copy a directory to a file + DEST_PATH exists and is a directory + SRC_PATH does not end with /. (that is: slash followed by dot) + the source directory is copied into this directory + SRC_PATH does end with /. (that is: slash followed by dot) + the content of the source directory is copied into this directory + + translating that in to our semantics: + + if source ends in / + docker cp src. dest + otherwise, cp source dest + + */ + + var dockerSource string + + if src[len(src)-1] == '/' { + dockerSource = fmt.Sprintf("%s.", src) + } else { + dockerSource = fmt.Sprintf("%s", src) + } + + // Make the directory, then copy into it + localCmd := exec.Command("docker", "cp", dockerSource, fmt.Sprintf("%s:%s", c.ContainerID, dst)) + + stderrP, err := localCmd.StderrPipe() + if err != nil { + return fmt.Errorf("Failed to open pipe: %s", err) + } + if err := localCmd.Start(); err != nil { + return fmt.Errorf("Failed to copy: %s", err) + } + stderrOut, err := ioutil.ReadAll(stderrP) + if err != nil { + return err + } + + // Wait for the copy to complete + if err := localCmd.Wait(); err != nil { + return fmt.Errorf("Failed to upload to '%s' in container: %s. %s.", dst, stderrOut, err) + } + + if err := c.fixDestinationOwner(dst); err != nil { + return err + } + + return nil +} + +// Download pulls a file out of a container using `docker cp`. We have a source +// path and want to write to an io.Writer, not a file. We use - to make docker +// cp to write to stdout, and then copy the stream to our destination io.Writer. +func (c *WindowsContainerCommunicator) Download(src string, dst io.Writer) error { + log.Printf("Downloading file from container: %s:%s", c.ContainerID, src) + localCmd := exec.Command("docker", "cp", fmt.Sprintf("%s:%s", c.ContainerID, src), "-") + + pipe, err := localCmd.StdoutPipe() + if err != nil { + return fmt.Errorf("Failed to open pipe: %s", err) + } + + if err = localCmd.Start(); err != nil { + return fmt.Errorf("Failed to start download: %s", err) + } + + // When you use - to send docker cp to stdout it is streamed as a tar; this + // enables it to work with directories. We don't actually support + // directories in Download() but we still need to handle the tar format. + archive := tar.NewReader(pipe) + _, err = archive.Next() + if err != nil { + return fmt.Errorf("Failed to read header from tar stream: %s", err) + } + + numBytes, err := io.Copy(dst, archive) + if err != nil { + return fmt.Errorf("Failed to pipe download: %s", err) + } + log.Printf("Copied %d bytes for %s", numBytes, src) + + if err = localCmd.Wait(); err != nil { + return fmt.Errorf("Failed to download '%s' from container: %s", src, err) + } + + return nil +} + +func (c *WindowsContainerCommunicator) DownloadDir(src string, dst string, exclude []string) error { + return fmt.Errorf("DownloadDir is not implemented for docker") +} + +// Runs the given command and blocks until completion +func (c *WindowsContainerCommunicator) run(cmd *exec.Cmd, remote *packer.RemoteCmd, stdin io.WriteCloser, stdout, stderr io.ReadCloser) { + // For Docker, remote communication must be serialized since it + // only supports single execution. + c.lock.Lock() + defer c.lock.Unlock() + + wg := sync.WaitGroup{} + repeat := func(w io.Writer, r io.ReadCloser) { + io.Copy(w, r) + r.Close() + wg.Done() + } + + if remote.Stdout != nil { + wg.Add(1) + go repeat(remote.Stdout, stdout) + } + + if remote.Stderr != nil { + wg.Add(1) + go repeat(remote.Stderr, stderr) + } + + // Start the command + log.Printf("Executing %s:", strings.Join(cmd.Args, " ")) + if err := cmd.Start(); err != nil { + log.Printf("Error executing: %s", err) + remote.SetExited(254) + return + } + + var exitStatus int + + if remote.Stdin != nil { + go func() { + io.Copy(stdin, remote.Stdin) + // close stdin to support commands that wait for stdin to be closed before exiting. + stdin.Close() + }() + } + + wg.Wait() + err := cmd.Wait() + + if exitErr, ok := err.(*exec.ExitError); ok { + exitStatus = 1 + + // There is no process-independent way to get the REAL + // exit status so we just try to go deeper. + if status, ok := exitErr.Sys().(syscall.WaitStatus); ok { + exitStatus = status.ExitStatus() + } + } + + // Set the exit status which triggers waiters + remote.SetExited(exitStatus) +} + +// TODO Workaround for #5307. Remove once #5409 is fixed. +func (c *WindowsContainerCommunicator) fixDestinationOwner(destination string) error { + if !c.Config.FixUploadOwner { + return nil + } + + owner := c.ContainerUser + if owner == "" { + owner = "root" + } + + chownArgs := []string{ + "docker", "exec", "--user", "root", c.ContainerID, "/bin/sh", "-c", + fmt.Sprintf("chown -R %s %s", owner, destination), + } + if output, err := exec.Command(chownArgs[0], chownArgs[1:]...).CombinedOutput(); err != nil { + return fmt.Errorf("Failed to set owner of the uploaded file: %s, %s", err, output) + } + + return nil +} diff --git a/helper/communicator/config.go b/helper/communicator/config.go index fa02810d7..5488beccd 100644 --- a/helper/communicator/config.go +++ b/helper/communicator/config.go @@ -212,7 +212,7 @@ func (c *Config) Prepare(ctx *interpolate.Context) []error { if es := c.prepareWinRM(ctx); len(es) > 0 { errs = append(errs, es...) } - case "docker", "none": + case "docker", "dockerWindowsContainer", "none": break default: return []error{fmt.Errorf("Communicator type %s is invalid", c.Type)} From 3b87f2a5191d3fcfefb762521869384fc6a6f969 Mon Sep 17 00:00:00 2001 From: Megan Marsh Date: Wed, 27 Mar 2019 14:51:50 -0700 Subject: [PATCH 02/12] stop container before committing if windows --- builder/docker/config.go | 2 +- builder/docker/driver.go | 5 +- builder/docker/driver_docker.go | 7 +++ builder/docker/step_commit.go | 10 ++++ builder/docker/step_connect_docker.go | 30 +++++++--- builder/docker/step_run.go | 2 +- .../docker/windows_container_communicator.go | 57 ------------------- 7 files changed, 44 insertions(+), 69 deletions(-) diff --git a/builder/docker/config.go b/builder/docker/config.go index d7b2cd50c..684e88bd9 100644 --- a/builder/docker/config.go +++ b/builder/docker/config.go @@ -38,7 +38,7 @@ type Config struct { RunCommand []string `mapstructure:"run_command"` Volumes map[string]string FixUploadOwner bool `mapstructure:"fix_upload_owner"` - WindowsContainer bool `windows_container` + WindowsContainer bool `mapstructure:"windows_container"` // This is used to login to dockerhub to pull a private base container. For // pushing to dockerhub, see the docker post-processors diff --git a/builder/docker/driver.go b/builder/docker/driver.go index 7359a667d..73bf5e1a8 100644 --- a/builder/docker/driver.go +++ b/builder/docker/driver.go @@ -46,7 +46,10 @@ type Driver interface { // along with a potential error. StartContainer(*ContainerConfig) (string, error) - // StopContainer forcibly stops a container. + // KillContainer forcibly stops a container. + KillContainer(id string) error + + // StopContainer gently stops a container. StopContainer(id string) error // TagImage tags the image with the given ID diff --git a/builder/docker/driver_docker.go b/builder/docker/driver_docker.go index 9b0a4556b..6b946e954 100644 --- a/builder/docker/driver_docker.go +++ b/builder/docker/driver_docker.go @@ -314,6 +314,13 @@ func (d *DockerDriver) StartContainer(config *ContainerConfig) (string, error) { } func (d *DockerDriver) StopContainer(id string) error { + if err := exec.Command("docker", "stop", id).Run(); err != nil { + return err + } + return nil +} + +func (d *DockerDriver) KillContainer(id string) error { if err := exec.Command("docker", "kill", id).Run(); err != nil { return err } diff --git a/builder/docker/step_commit.go b/builder/docker/step_commit.go index d248ee64c..c75ab649c 100644 --- a/builder/docker/step_commit.go +++ b/builder/docker/step_commit.go @@ -19,6 +19,16 @@ func (s *StepCommit) Run(_ context.Context, state multistep.StateBag) multistep. config := state.Get("config").(*Config) ui := state.Get("ui").(packer.Ui) + if config.WindowsContainer { + // docker can't commit a running Windows container + err := driver.StopContainer(containerId) + if err != nil { + state.Put("error", err) + ui.Error(fmt.Sprintf("Error halting windows container for commit: %s", + err.Error())) + return multistep.ActionHalt + } + } ui.Say("Committing the container") imageId, err := driver.Commit(containerId, config.Author, config.Changes, config.Message) if err != nil { diff --git a/builder/docker/step_connect_docker.go b/builder/docker/step_connect_docker.go index ef0222a91..3cd4bc173 100644 --- a/builder/docker/step_connect_docker.go +++ b/builder/docker/step_connect_docker.go @@ -32,16 +32,28 @@ func (s *StepConnectDocker) Run(_ context.Context, state multistep.StateBag) mul // Create the communicator that talks to Docker via various // os/exec tricks. - comm := &Communicator{ - ContainerID: containerId, - HostDir: tempDir, - ContainerDir: config.ContainerDir, - Version: version, - Config: config, - ContainerUser: containerUser, - } + if config.WindowsContainer { + comm := &WindowsContainerCommunicator{ + ContainerID: containerId, + HostDir: tempDir, + ContainerDir: config.ContainerDir, + Version: version, + Config: config, + ContainerUser: containerUser, + } + state.Put("communicator", comm) - state.Put("communicator", comm) + } else { + comm := &Communicator{ + ContainerID: containerId, + HostDir: tempDir, + ContainerDir: config.ContainerDir, + Version: version, + Config: config, + ContainerUser: containerUser, + } + state.Put("communicator", comm) + } return multistep.ActionContinue } diff --git a/builder/docker/step_run.go b/builder/docker/step_run.go index 1d2ba6862..25c5dee0c 100644 --- a/builder/docker/step_run.go +++ b/builder/docker/step_run.go @@ -58,7 +58,7 @@ func (s *StepRun) Cleanup(state multistep.StateBag) { // just mean that the container doesn't exist anymore, which isn't a // big deal. ui.Say(fmt.Sprintf("Killing the container: %s", s.containerId)) - driver.StopContainer(s.containerId) + driver.KillContainer(s.containerId) // Reset the container ID so that we're idempotent s.containerId = "" diff --git a/builder/docker/windows_container_communicator.go b/builder/docker/windows_container_communicator.go index 88054429c..cc93975a4 100644 --- a/builder/docker/windows_container_communicator.go +++ b/builder/docker/windows_container_communicator.go @@ -260,63 +260,6 @@ func (c *WindowsContainerCommunicator) uploadFileOld(dst string, src io.Reader, return nil } -func (c *WindowsContainerCommunicator) UploadDir(dst string, src string, exclude []string) error { - /* - from https://docs.docker.com/engine/reference/commandline/cp/#extended-description - SRC_PATH specifies a directory - DEST_PATH does not exist - DEST_PATH is created as a directory and the contents of the source directory are copied into this directory - DEST_PATH exists and is a file - Error condition: cannot copy a directory to a file - DEST_PATH exists and is a directory - SRC_PATH does not end with /. (that is: slash followed by dot) - the source directory is copied into this directory - SRC_PATH does end with /. (that is: slash followed by dot) - the content of the source directory is copied into this directory - - translating that in to our semantics: - - if source ends in / - docker cp src. dest - otherwise, cp source dest - - */ - - var dockerSource string - - if src[len(src)-1] == '/' { - dockerSource = fmt.Sprintf("%s.", src) - } else { - dockerSource = fmt.Sprintf("%s", src) - } - - // Make the directory, then copy into it - localCmd := exec.Command("docker", "cp", dockerSource, fmt.Sprintf("%s:%s", c.ContainerID, dst)) - - stderrP, err := localCmd.StderrPipe() - if err != nil { - return fmt.Errorf("Failed to open pipe: %s", err) - } - if err := localCmd.Start(); err != nil { - return fmt.Errorf("Failed to copy: %s", err) - } - stderrOut, err := ioutil.ReadAll(stderrP) - if err != nil { - return err - } - - // Wait for the copy to complete - if err := localCmd.Wait(); err != nil { - return fmt.Errorf("Failed to upload to '%s' in container: %s. %s.", dst, stderrOut, err) - } - - if err := c.fixDestinationOwner(dst); err != nil { - return err - } - - return nil -} - // Download pulls a file out of a container using `docker cp`. We have a source // path and want to write to an io.Writer, not a file. We use - to make docker // cp to write to stdout, and then copy the stream to our destination io.Writer. From 0e6c779a88da7615f75afdd5f4a1df3cb6595fd6 Mon Sep 17 00:00:00 2001 From: Megan Marsh Date: Wed, 27 Mar 2019 15:22:59 -0700 Subject: [PATCH 03/12] allow user to access env_var_format in windows_shell call --- provisioner/windows-shell/provisioner.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/provisioner/windows-shell/provisioner.go b/provisioner/windows-shell/provisioner.go index 6d37e2514..993c92ab2 100644 --- a/provisioner/windows-shell/provisioner.go +++ b/provisioner/windows-shell/provisioner.go @@ -40,7 +40,7 @@ type Config struct { // This is used in the template generation to format environment variables // inside the `ExecuteCommand` template. - EnvVarFormat string + EnvVarFormat string `mapstructure:"env_var_format"` ctx interpolate.Context } From af01860fa9151c3e90bf6f0f2414f94c616d59f1 Mon Sep 17 00:00:00 2001 From: Megan Marsh Date: Wed, 27 Mar 2019 15:29:22 -0700 Subject: [PATCH 04/12] remove old docker-toolbox limitation. --- builder/docker/driver_docker.go | 6 ------ 1 file changed, 6 deletions(-) diff --git a/builder/docker/driver_docker.go b/builder/docker/driver_docker.go index 6b946e954..59b4c984d 100644 --- a/builder/docker/driver_docker.go +++ b/builder/docker/driver_docker.go @@ -8,7 +8,6 @@ import ( "os" "os/exec" "regexp" - // "runtime" "strings" "sync" @@ -270,11 +269,6 @@ func (d *DockerDriver) StartContainer(config *ContainerConfig) (string, error) { args = append(args, "--privileged") } for host, guest := range config.Volumes { - // if runtime.GOOS == "windows" { - // // docker-toolbox can't handle the normal C:\filepath format in CLI - // host = strings.Replace(host, "\\", "/", -1) - // host = strings.Replace(host, "C:/", "/c/", 1) - // } args = append(args, "-v", fmt.Sprintf("%s:%s", host, guest)) } for _, v := range config.RunCommand { From 36f26343528fce0b96521663935b11c2abbd507f Mon Sep 17 00:00:00 2001 From: Megan Marsh Date: Thu, 28 Mar 2019 09:38:17 -0700 Subject: [PATCH 05/12] can't use docker cp so call powershell to do this natively. Fix implementation for upload, uploadDir, and download in windows container communicator --- builder/docker/step_connect_docker.go | 3 +- .../docker/windows_container_communicator.go | 202 +++--------------- 2 files changed, 30 insertions(+), 175 deletions(-) diff --git a/builder/docker/step_connect_docker.go b/builder/docker/step_connect_docker.go index 3cd4bc173..6f08ac326 100644 --- a/builder/docker/step_connect_docker.go +++ b/builder/docker/step_connect_docker.go @@ -33,13 +33,14 @@ func (s *StepConnectDocker) Run(_ context.Context, state multistep.StateBag) mul // Create the communicator that talks to Docker via various // os/exec tricks. if config.WindowsContainer { - comm := &WindowsContainerCommunicator{ + comm := &WindowsContainerCommunicator{Communicator{ ContainerID: containerId, HostDir: tempDir, ContainerDir: config.ContainerDir, Version: version, Config: config, ContainerUser: containerUser, + }, } state.Put("communicator", comm) diff --git a/builder/docker/windows_container_communicator.go b/builder/docker/windows_container_communicator.go index cc93975a4..46650a103 100644 --- a/builder/docker/windows_container_communicator.go +++ b/builder/docker/windows_container_communicator.go @@ -1,7 +1,7 @@ package docker import ( - "archive/tar" + "bytes" "fmt" "io" "io/ioutil" @@ -9,22 +9,12 @@ import ( "os" "os/exec" "path/filepath" - "strings" - "sync" - "syscall" - "github.com/hashicorp/go-version" "github.com/hashicorp/packer/packer" ) type WindowsContainerCommunicator struct { - ContainerID string - HostDir string - ContainerDir string - Version *version.Version - Config *Config - ContainerUser string - lock sync.Mutex + Communicator } func (c *WindowsContainerCommunicator) Start(remote *packer.RemoteCmd) error { @@ -87,7 +77,6 @@ func (c *WindowsContainerCommunicator) Upload(dst string, src io.Reader, fi *os. if err != nil { return err } - if fi != nil { tempfile.Chmod((*fi).Mode()) } @@ -165,12 +154,7 @@ func (c *WindowsContainerCommunicator) UploadDir(dst string, src string, exclude return err } - si, err := src.Stat() - if err != nil { - return err - } - - return dst.Chmod(si.Mode()) + return nil } // Copy the entire directory tree to the temporary directory @@ -187,8 +171,8 @@ func (c *WindowsContainerCommunicator) UploadDir(dst string, src string, exclude // Make the directory, then copy into it cmd := &packer.RemoteCmd{ - Command: fmt.Sprintf("set -e; mkdir -p %s; command cp -R %s/ %s", - containerDst, containerSrc, containerDst), + Command: fmt.Sprintf("Copy-Item %s -Destination %s -Recurse", + containerSrc, containerDst), } if err := c.Start(cmd); err != nil { return err @@ -203,96 +187,40 @@ func (c *WindowsContainerCommunicator) UploadDir(dst string, src string, exclude return nil } -func (c *WindowsContainerCommunicator) uploadFileOld(dst string, src io.Reader, fi *os.FileInfo) error { - // command format: docker cp /path/to/infile containerid:/path/to/outfile - log.Printf("Copying to %s on container %s.", dst, c.ContainerID) - - localCmd := exec.Command("docker", "cp", "-", - fmt.Sprintf("%s:%s", c.ContainerID, filepath.Dir(dst))) - - stderrP, err := localCmd.StderrPipe() - if err != nil { - return fmt.Errorf("Failed to open pipe: %s", err) - } - - stdin, err := localCmd.StdinPipe() - if err != nil { - return fmt.Errorf("Failed to open pipe: %s", err) +// Download pulls a file out of a container using `docker cp`. We have a source +// path and want to write to an io.Writer +func (c *WindowsContainerCommunicator) Download(src string, dst io.Writer) error { + log.Printf("Downloading file from container: %s:%s", c.ContainerID, src) + // Copy file onto temp file on mounted volume inside container + var stdout, stderr bytes.Buffer + cmd := &packer.RemoteCmd{ + Command: fmt.Sprintf("Copy-Item -Path %s -Destination %s/%s", src, c.ContainerDir, + filepath.Base(src)), + Stdout: &stdout, + Stderr: &stderr, } - - if err := localCmd.Start(); err != nil { + if err := c.Start(cmd); err != nil { return err } - archive := tar.NewWriter(stdin) - header, err := tar.FileInfoHeader(*fi, "") - if err != nil { - return err - } - header.Name = filepath.Base(dst) - archive.WriteHeader(header) - numBytes, err := io.Copy(archive, src) - if err != nil { - return fmt.Errorf("Failed to pipe upload: %s", err) - } - log.Printf("Copied %d bytes for %s", numBytes, dst) + // Wait for the copy to complete + cmd.Wait() - if err := archive.Close(); err != nil { - return fmt.Errorf("Failed to close archive: %s", err) - } - if err := stdin.Close(); err != nil { - return fmt.Errorf("Failed to close stdin: %s", err) + if cmd.ExitStatus != 0 { + return fmt.Errorf("Failed to copy file to shared drive: %s, %s, %d", stderr.String(), stdout.String(), cmd.ExitStatus) } - stderrOut, err := ioutil.ReadAll(stderrP) + // Read that copied file into a new file opened on host machine + fsrc, err := os.Open(filepath.Join(c.HostDir, filepath.Base(src))) if err != nil { return err } + defer fsrc.Close() + defer os.Remove(fsrc.Name()) - if err := localCmd.Wait(); err != nil { - return fmt.Errorf("Failed to upload to '%s' in container: %s. %s.", dst, stderrOut, err) - } - - if err := c.fixDestinationOwner(dst); err != nil { - return err - } - - return nil -} - -// Download pulls a file out of a container using `docker cp`. We have a source -// path and want to write to an io.Writer, not a file. We use - to make docker -// cp to write to stdout, and then copy the stream to our destination io.Writer. -func (c *WindowsContainerCommunicator) Download(src string, dst io.Writer) error { - log.Printf("Downloading file from container: %s:%s", c.ContainerID, src) - localCmd := exec.Command("docker", "cp", fmt.Sprintf("%s:%s", c.ContainerID, src), "-") - - pipe, err := localCmd.StdoutPipe() + _, err = io.Copy(dst, fsrc) if err != nil { - return fmt.Errorf("Failed to open pipe: %s", err) - } - - if err = localCmd.Start(); err != nil { - return fmt.Errorf("Failed to start download: %s", err) - } - - // When you use - to send docker cp to stdout it is streamed as a tar; this - // enables it to work with directories. We don't actually support - // directories in Download() but we still need to handle the tar format. - archive := tar.NewReader(pipe) - _, err = archive.Next() - if err != nil { - return fmt.Errorf("Failed to read header from tar stream: %s", err) - } - - numBytes, err := io.Copy(dst, archive) - if err != nil { - return fmt.Errorf("Failed to pipe download: %s", err) - } - log.Printf("Copied %d bytes for %s", numBytes, src) - - if err = localCmd.Wait(); err != nil { - return fmt.Errorf("Failed to download '%s' from container: %s", src, err) + return err } return nil @@ -306,79 +234,5 @@ func (c *WindowsContainerCommunicator) DownloadDir(src string, dst string, exclu func (c *WindowsContainerCommunicator) run(cmd *exec.Cmd, remote *packer.RemoteCmd, stdin io.WriteCloser, stdout, stderr io.ReadCloser) { // For Docker, remote communication must be serialized since it // only supports single execution. - c.lock.Lock() - defer c.lock.Unlock() - - wg := sync.WaitGroup{} - repeat := func(w io.Writer, r io.ReadCloser) { - io.Copy(w, r) - r.Close() - wg.Done() - } - - if remote.Stdout != nil { - wg.Add(1) - go repeat(remote.Stdout, stdout) - } - - if remote.Stderr != nil { - wg.Add(1) - go repeat(remote.Stderr, stderr) - } - - // Start the command - log.Printf("Executing %s:", strings.Join(cmd.Args, " ")) - if err := cmd.Start(); err != nil { - log.Printf("Error executing: %s", err) - remote.SetExited(254) - return - } - - var exitStatus int - - if remote.Stdin != nil { - go func() { - io.Copy(stdin, remote.Stdin) - // close stdin to support commands that wait for stdin to be closed before exiting. - stdin.Close() - }() - } - - wg.Wait() - err := cmd.Wait() - - if exitErr, ok := err.(*exec.ExitError); ok { - exitStatus = 1 - - // There is no process-independent way to get the REAL - // exit status so we just try to go deeper. - if status, ok := exitErr.Sys().(syscall.WaitStatus); ok { - exitStatus = status.ExitStatus() - } - } - - // Set the exit status which triggers waiters - remote.SetExited(exitStatus) -} - -// TODO Workaround for #5307. Remove once #5409 is fixed. -func (c *WindowsContainerCommunicator) fixDestinationOwner(destination string) error { - if !c.Config.FixUploadOwner { - return nil - } - - owner := c.ContainerUser - if owner == "" { - owner = "root" - } - - chownArgs := []string{ - "docker", "exec", "--user", "root", c.ContainerID, "/bin/sh", "-c", - fmt.Sprintf("chown -R %s %s", owner, destination), - } - if output, err := exec.Command(chownArgs[0], chownArgs[1:]...).CombinedOutput(); err != nil { - return fmt.Errorf("Failed to set owner of the uploaded file: %s, %s", err, output) - } - - return nil + c.Communicator.run(cmd, remote, stdin, stdout, stderr) } From a01091952d2d4c71b549caf3aea4aa8e44f27715 Mon Sep 17 00:00:00 2001 From: Megan Marsh Date: Thu, 28 Mar 2019 16:26:38 -0700 Subject: [PATCH 06/12] add documentation for windows_container flag --- website/source/docs/builders/docker.html.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/website/source/docs/builders/docker.html.md b/website/source/docs/builders/docker.html.md index d5ef7c632..c7384ae3a 100644 --- a/website/source/docs/builders/docker.html.md +++ b/website/source/docs/builders/docker.html.md @@ -217,6 +217,10 @@ You must specify (only) one of `commit`, `discard`, or `export_path`. mount into this container. The key of the object is the host path, the value is the container path. +- `windows_container` (bool) - If "true", tells Packer that you are building a + Windows container running on a windows host. This is necessary for building + Windows containers, because our normal docker bindings do not work for them. + - `container_dir` (string) - The directory inside container to mount temp directory from host server for work [file provisioner](/docs/provisioners/file.html). By default this is set to From 70150ffa0fcd55413f636286c95ce21c406f1ad2 Mon Sep 17 00:00:00 2001 From: Megan Marsh Date: Thu, 28 Mar 2019 16:46:07 -0700 Subject: [PATCH 07/12] set powershell entrypoint for windows containers --- builder/docker/config.go | 3 +++ 1 file changed, 3 insertions(+) diff --git a/builder/docker/config.go b/builder/docker/config.go index 684e88bd9..8b41589e0 100644 --- a/builder/docker/config.go +++ b/builder/docker/config.go @@ -75,6 +75,9 @@ func NewConfig(raws ...interface{}) (*Config, []string, error) { // Defaults if len(c.RunCommand) == 0 { c.RunCommand = []string{"-d", "-i", "-t", "--entrypoint=/bin/sh", "--", "{{.Image}}"} + if c.WindowsContainer { + c.RunCommand = []string{"-d", "-i", "-t", "--entrypoint=powershell", "--", "{{.Image}}"} + } } // Default Pull if it wasn't set From 0860edeed80e373bee7a5d27bee6acb2e9750ee6 Mon Sep 17 00:00:00 2001 From: Megan Marsh Date: Thu, 28 Mar 2019 16:55:35 -0700 Subject: [PATCH 08/12] fix mocks --- builder/docker/driver_mock.go | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/builder/docker/driver_mock.go b/builder/docker/driver_mock.go index 4cca3325b..4ffc69299 100644 --- a/builder/docker/driver_mock.go +++ b/builder/docker/driver_mock.go @@ -28,6 +28,10 @@ type MockDriver struct { IPAddressResult string IPAddressErr error + KillCalled bool + KillID string + KillError error + LoginCalled bool LoginUsername string LoginPassword string @@ -160,6 +164,12 @@ func (d *MockDriver) StartContainer(config *ContainerConfig) (string, error) { return d.StartID, d.StartError } +func (d *MockDriver) KillContainer(id string) error { + d.KillCalled = true + d.KillID = id + return d.KillError +} + func (d *MockDriver) StopContainer(id string) error { d.StopCalled = true d.StopID = id From 12b9004c76e377a00317f093cd1555349b8efe2c Mon Sep 17 00:00:00 2001 From: Megan Marsh Date: Fri, 29 Mar 2019 11:14:01 -0700 Subject: [PATCH 09/12] reduce duplicated code --- builder/docker/communicator.go | 6 +- builder/docker/step_connect_docker.go | 2 + .../docker/windows_container_communicator.go | 58 ------------------- 3 files changed, 5 insertions(+), 61 deletions(-) diff --git a/builder/docker/communicator.go b/builder/docker/communicator.go index e898ecd1c..48b3de7c6 100644 --- a/builder/docker/communicator.go +++ b/builder/docker/communicator.go @@ -25,6 +25,7 @@ type Communicator struct { Config *Config ContainerUser string lock sync.Mutex + EntryPoint []string } func (c *Communicator) Start(remote *packer.RemoteCmd) error { @@ -32,10 +33,9 @@ func (c *Communicator) Start(remote *packer.RemoteCmd) error { "exec", "-i", c.ContainerID, - "/bin/sh", - "-c", - fmt.Sprintf("(%s)", remote.Command), } + dockerArgs = append(dockerArgs, c.EntryPoint...) + dockerArgs = append(dockerArgs, fmt.Sprintf("(%s)", remote.Command)) if c.Config.Pty { dockerArgs = append(dockerArgs[:2], append([]string{"-t"}, dockerArgs[2:]...)...) diff --git a/builder/docker/step_connect_docker.go b/builder/docker/step_connect_docker.go index 6f08ac326..583e69b36 100644 --- a/builder/docker/step_connect_docker.go +++ b/builder/docker/step_connect_docker.go @@ -40,6 +40,7 @@ func (s *StepConnectDocker) Run(_ context.Context, state multistep.StateBag) mul Version: version, Config: config, ContainerUser: containerUser, + EntryPoint: []string{"powershell"}, }, } state.Put("communicator", comm) @@ -52,6 +53,7 @@ func (s *StepConnectDocker) Run(_ context.Context, state multistep.StateBag) mul Version: version, Config: config, ContainerUser: containerUser, + EntryPoint: []string{"/bin/sh", "-c"}, } state.Put("communicator", comm) } diff --git a/builder/docker/windows_container_communicator.go b/builder/docker/windows_container_communicator.go index 46650a103..dbb74f406 100644 --- a/builder/docker/windows_container_communicator.go +++ b/builder/docker/windows_container_communicator.go @@ -7,7 +7,6 @@ import ( "io/ioutil" "log" "os" - "os/exec" "path/filepath" "github.com/hashicorp/packer/packer" @@ -17,52 +16,6 @@ type WindowsContainerCommunicator struct { Communicator } -func (c *WindowsContainerCommunicator) Start(remote *packer.RemoteCmd) error { - dockerArgs := []string{ - "exec", - "-i", - c.ContainerID, - "powershell", - fmt.Sprintf("(%s)", remote.Command), - } - - if c.Config.Pty { - dockerArgs = append(dockerArgs[:2], append([]string{"-t"}, dockerArgs[2:]...)...) - } - - if c.Config.ExecUser != "" { - dockerArgs = append(dockerArgs[:2], - append([]string{"-u", c.Config.ExecUser}, dockerArgs[2:]...)...) - } - - cmd := exec.Command("docker", dockerArgs...) - - var ( - stdin_w io.WriteCloser - err error - ) - - stdin_w, err = cmd.StdinPipe() - if err != nil { - return err - } - - stderr_r, err := cmd.StderrPipe() - if err != nil { - return err - } - - stdout_r, err := cmd.StdoutPipe() - if err != nil { - return err - } - - // Run the actual command in a goroutine so that Start doesn't block - go c.run(cmd, remote, stdin_w, stdout_r, stderr_r) - - return nil -} - // Upload uses docker exec to copy the file from the host to the container func (c *WindowsContainerCommunicator) Upload(dst string, src io.Reader, fi *os.FileInfo) error { // Create a temporary file to store the upload @@ -225,14 +178,3 @@ func (c *WindowsContainerCommunicator) Download(src string, dst io.Writer) error return nil } - -func (c *WindowsContainerCommunicator) DownloadDir(src string, dst string, exclude []string) error { - return fmt.Errorf("DownloadDir is not implemented for docker") -} - -// Runs the given command and blocks until completion -func (c *WindowsContainerCommunicator) run(cmd *exec.Cmd, remote *packer.RemoteCmd, stdin io.WriteCloser, stdout, stderr io.ReadCloser) { - // For Docker, remote communication must be serialized since it - // only supports single execution. - c.Communicator.run(cmd, remote, stdin, stdout, stderr) -} From 6407a579f0b4b5a0502f5062a2733f75ebeb1946 Mon Sep 17 00:00:00 2001 From: Megan Marsh Date: Fri, 29 Mar 2019 11:21:07 -0700 Subject: [PATCH 10/12] Document why we need windows communicator in code --- builder/docker/windows_container_communicator.go | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/builder/docker/windows_container_communicator.go b/builder/docker/windows_container_communicator.go index dbb74f406..3db7bbfbb 100644 --- a/builder/docker/windows_container_communicator.go +++ b/builder/docker/windows_container_communicator.go @@ -12,6 +12,14 @@ import ( "github.com/hashicorp/packer/packer" ) +// Windows containers are a special beast in Docker; you can't use docker cp +// to move files between the container and host. + +// This communicator works around that limitation by reusing all possible +// methods and fields of the normal Docker Communicator, but we overwrite the +// Upload, Download, and UploadDir methods to utilize a mounted directory and +// native powershell commands rather than relying on docker cp. + type WindowsContainerCommunicator struct { Communicator } From af063341142938941f2652c320f99e379dd45b7a Mon Sep 17 00:00:00 2001 From: Megan Marsh Date: Fri, 29 Mar 2019 11:37:23 -0700 Subject: [PATCH 11/12] fix tests --- builder/docker/step_run_test.go | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/builder/docker/step_run_test.go b/builder/docker/step_run_test.go index bc6639319..c76c7c637 100644 --- a/builder/docker/step_run_test.go +++ b/builder/docker/step_run_test.go @@ -52,16 +52,16 @@ func TestStepRun(t *testing.T) { } // Verify we haven't called stop yet - if driver.StopCalled { + if driver.KillCalled { t.Fatal("should not have stopped") } // Cleanup step.Cleanup(state) - if !driver.StopCalled { + if !driver.KillCalled { t.Fatal("should've stopped") } - if driver.StopID != id { + if driver.KillID != id { t.Fatalf("bad: %#v", driver.StopID) } } @@ -85,13 +85,13 @@ func TestStepRun_error(t *testing.T) { } // Verify we haven't called stop yet - if driver.StopCalled { + if driver.KillCalled { t.Fatal("should not have stopped") } // Cleanup step.Cleanup(state) - if driver.StopCalled { + if driver.KillCalled { t.Fatal("should not have stopped") } } From b079d7ba1210872919e8531c6883d23129f56d9f Mon Sep 17 00:00:00 2001 From: Megan Marsh Date: Fri, 29 Mar 2019 13:39:02 -0700 Subject: [PATCH 12/12] add windows container example --- website/source/docs/builders/docker.html.md | 34 +++++++++++++++++++-- 1 file changed, 32 insertions(+), 2 deletions(-) diff --git a/website/source/docs/builders/docker.html.md b/website/source/docs/builders/docker.html.md index c7384ae3a..83726aa33 100644 --- a/website/source/docs/builders/docker.html.md +++ b/website/source/docs/builders/docker.html.md @@ -210,8 +210,11 @@ You must specify (only) one of `commit`, `discard`, or `export_path`. - `run_command` (array of strings) - An array of arguments to pass to `docker run` in order to run the container. By default this is set to - `["-d", "-i", "-t", "{{.Image}}", "/bin/sh"]`. As you can see, you have a - couple template variables to customize, as well. + `["-d", "-i", "-t", "--entrypoint=/bin/sh", "--", "{{.Image}}"]` if you are + using a linux container, and + `["-d", "-i", "-t", "--entrypoint=powershell", "--", "{{.Image}}"]` if you + are running a windows container. {{.Image}} is a template variable that + corresponds to the `image` template option. - `volumes` (map of strings to strings) - A mapping of additional volumes to mount into this container. The key of the object is the host path, the @@ -338,6 +341,33 @@ nearly-identical sequence definitions, as demonstrated by the example below: +## Docker For Windows + +You should be able to run docker builds against both linux and Windows +containers. Windows containers use a different communicator than linux +containers, because Windows containers cannot use `docker cp`. + +If you are building a Windows container, you must set the template option +`"windows_container": true`. Please note that docker cannot export Windows +containers, so you must either commit or discard them. + +The following is a fully functional template for building a Windows +container. + +``` json +{ + "builders": [ + { + "type": "docker", + "image": "microsoft/windowsservercore:1709", + "container_dir": "c:/app", + "windows_container": true, + "commit": true + } + ] +} +``` + ## Amazon EC2 Container Registry Packer can tag and push images for use in [Amazon EC2 Container