Create new docker communicator for windows containers
This commit is contained in:
parent
113c04059e
commit
8f3313d81e
|
@ -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{},
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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
|
||||
}
|
|
@ -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)}
|
||||
|
|
Loading…
Reference in New Issue