Merge pull request #3476 from bhcleek/ansible-with-docker
fix docker builder with ansible provisioner
This commit is contained in:
commit
10cba9795f
|
@ -2,7 +2,6 @@ package docker
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"archive/tar"
|
"archive/tar"
|
||||||
"bytes"
|
|
||||||
"fmt"
|
"fmt"
|
||||||
"io"
|
"io"
|
||||||
"io/ioutil"
|
"io/ioutil"
|
||||||
|
@ -10,12 +9,10 @@ import (
|
||||||
"os"
|
"os"
|
||||||
"os/exec"
|
"os/exec"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
"strconv"
|
"strings"
|
||||||
"sync"
|
"sync"
|
||||||
"syscall"
|
"syscall"
|
||||||
"time"
|
|
||||||
|
|
||||||
"github.com/ActiveState/tail"
|
|
||||||
"github.com/hashicorp/go-version"
|
"github.com/hashicorp/go-version"
|
||||||
"github.com/mitchellh/packer/packer"
|
"github.com/mitchellh/packer/packer"
|
||||||
)
|
)
|
||||||
|
@ -30,42 +27,35 @@ type Communicator struct {
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *Communicator) Start(remote *packer.RemoteCmd) error {
|
func (c *Communicator) Start(remote *packer.RemoteCmd) error {
|
||||||
// Create a temporary file to store the output. Because of a bug in
|
var cmd *exec.Cmd
|
||||||
// Docker, sometimes all the output doesn't properly show up. This
|
if c.Config.Pty {
|
||||||
// file will capture ALL of the output, and we'll read that.
|
cmd = exec.Command("docker", "exec", "-i", "-t", c.ContainerId, "/bin/sh", "-c", fmt.Sprintf("(%s)", remote.Command))
|
||||||
//
|
} else {
|
||||||
// https://github.com/dotcloud/docker/issues/2625
|
cmd = exec.Command("docker", "exec", "-i", c.ContainerId, "/bin/sh", "-c", fmt.Sprintf("(%s)", remote.Command))
|
||||||
outputFile, err := ioutil.TempFile(c.HostDir, "cmd")
|
}
|
||||||
|
|
||||||
|
var (
|
||||||
|
stdin_w io.WriteCloser
|
||||||
|
err error
|
||||||
|
)
|
||||||
|
|
||||||
|
stdin_w, err = cmd.StdinPipe()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
outputFile.Close()
|
|
||||||
|
|
||||||
// This file will store the exit code of the command once it is complete.
|
stderr_r, err := cmd.StderrPipe()
|
||||||
exitCodePath := outputFile.Name() + "-exit"
|
|
||||||
|
|
||||||
var cmd *exec.Cmd
|
|
||||||
if c.canExec() {
|
|
||||||
if c.Config.Pty {
|
|
||||||
cmd = exec.Command("docker", "exec", "-i", "-t", c.ContainerId, "/bin/sh")
|
|
||||||
} else {
|
|
||||||
cmd = exec.Command("docker", "exec", "-i", c.ContainerId, "/bin/sh")
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
cmd = exec.Command("docker", "attach", c.ContainerId)
|
|
||||||
}
|
|
||||||
|
|
||||||
stdin_w, err := cmd.StdinPipe()
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
// We have to do some cleanup since run was never called
|
return err
|
||||||
os.Remove(outputFile.Name())
|
}
|
||||||
os.Remove(exitCodePath)
|
|
||||||
|
|
||||||
|
stdout_r, err := cmd.StdoutPipe()
|
||||||
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
// Run the actual command in a goroutine so that Start doesn't block
|
// Run the actual command in a goroutine so that Start doesn't block
|
||||||
go c.run(cmd, remote, stdin_w, outputFile, exitCodePath)
|
go c.run(cmd, remote, stdin_w, stdout_r, stderr_r)
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
@ -237,105 +227,51 @@ func (c *Communicator) DownloadDir(src string, dst string, exclude []string) err
|
||||||
return fmt.Errorf("DownloadDir is not implemented for docker")
|
return fmt.Errorf("DownloadDir is not implemented for docker")
|
||||||
}
|
}
|
||||||
|
|
||||||
// canExec tells us whether `docker exec` is supported
|
|
||||||
func (c *Communicator) canExec() bool {
|
|
||||||
execConstraint, err := version.NewConstraint(">= 1.4.0")
|
|
||||||
if err != nil {
|
|
||||||
panic(err)
|
|
||||||
}
|
|
||||||
return execConstraint.Check(c.Version)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Runs the given command and blocks until completion
|
// Runs the given command and blocks until completion
|
||||||
func (c *Communicator) run(cmd *exec.Cmd, remote *packer.RemoteCmd, stdin_w io.WriteCloser, outputFile *os.File, exitCodePath string) {
|
func (c *Communicator) run(cmd *exec.Cmd, remote *packer.RemoteCmd, stdin io.WriteCloser, stdout, stderr io.ReadCloser) {
|
||||||
// For Docker, remote communication must be serialized since it
|
// For Docker, remote communication must be serialized since it
|
||||||
// only supports single execution.
|
// only supports single execution.
|
||||||
c.lock.Lock()
|
c.lock.Lock()
|
||||||
defer c.lock.Unlock()
|
defer c.lock.Unlock()
|
||||||
|
|
||||||
// Clean up after ourselves by removing our temporary files
|
wg := sync.WaitGroup{}
|
||||||
defer os.Remove(outputFile.Name())
|
repeat := func(w io.Writer, r io.ReadCloser) {
|
||||||
defer os.Remove(exitCodePath)
|
io.Copy(w, r)
|
||||||
|
r.Close()
|
||||||
// Tail the output file and send the data to the stdout listener
|
wg.Done()
|
||||||
tail, err := tail.TailFile(outputFile.Name(), tail.Config{
|
|
||||||
Poll: true,
|
|
||||||
ReOpen: true,
|
|
||||||
Follow: true,
|
|
||||||
})
|
|
||||||
if err != nil {
|
|
||||||
log.Printf("Error tailing output file: %s", err)
|
|
||||||
remote.SetExited(254)
|
|
||||||
return
|
|
||||||
}
|
}
|
||||||
defer tail.Stop()
|
|
||||||
|
|
||||||
// Modify the remote command so that all the output of the commands
|
if remote.Stdout != nil {
|
||||||
// go to a single file and so that the exit code is redirected to
|
wg.Add(1)
|
||||||
// a single file. This lets us determine both when the command
|
go repeat(remote.Stdout, stdout)
|
||||||
// is truly complete (because the file will have data), what the
|
}
|
||||||
// exit status is (because Docker loses it because of the pty, not
|
|
||||||
// Docker's fault), and get the output (Docker bug).
|
if remote.Stderr != nil {
|
||||||
remoteCmd := fmt.Sprintf("(%s) >%s 2>&1; echo $? >%s",
|
wg.Add(1)
|
||||||
remote.Command,
|
go repeat(remote.Stderr, stderr)
|
||||||
filepath.Join(c.ContainerDir, filepath.Base(outputFile.Name())),
|
}
|
||||||
filepath.Join(c.ContainerDir, filepath.Base(exitCodePath)))
|
|
||||||
|
|
||||||
// Start the command
|
// Start the command
|
||||||
log.Printf("Executing in container %s: %#v", c.ContainerId, remoteCmd)
|
log.Printf("Executing %s:", strings.Join(cmd.Args, " "))
|
||||||
if err := cmd.Start(); err != nil {
|
if err := cmd.Start(); err != nil {
|
||||||
log.Printf("Error executing: %s", err)
|
log.Printf("Error executing: %s", err)
|
||||||
remote.SetExited(254)
|
remote.SetExited(254)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
go func() {
|
|
||||||
defer stdin_w.Close()
|
|
||||||
|
|
||||||
// This sleep needs to be here because of the issue linked to below.
|
|
||||||
// Basically, without it, Docker will hang on reading stdin forever,
|
|
||||||
// and won't see what we write, for some reason.
|
|
||||||
//
|
|
||||||
// https://github.com/dotcloud/docker/issues/2628
|
|
||||||
time.Sleep(2 * time.Second)
|
|
||||||
|
|
||||||
stdin_w.Write([]byte(remoteCmd + "\n"))
|
|
||||||
}()
|
|
||||||
|
|
||||||
// Start a goroutine to read all the lines out of the logs. These channels
|
|
||||||
// allow us to stop the go-routine and wait for it to be stopped.
|
|
||||||
stopTailCh := make(chan struct{})
|
|
||||||
doneCh := make(chan struct{})
|
|
||||||
go func() {
|
|
||||||
defer close(doneCh)
|
|
||||||
|
|
||||||
for {
|
|
||||||
select {
|
|
||||||
case <-tail.Dead():
|
|
||||||
return
|
|
||||||
case line := <-tail.Lines:
|
|
||||||
if remote.Stdout != nil {
|
|
||||||
remote.Stdout.Write([]byte(line.Text + "\n"))
|
|
||||||
} else {
|
|
||||||
log.Printf("Command stdout: %#v", line.Text)
|
|
||||||
}
|
|
||||||
case <-time.After(2 * time.Second):
|
|
||||||
// If we're done, then return. Otherwise, keep grabbing
|
|
||||||
// data. This gives us a chance to flush all the lines
|
|
||||||
// out of the tailed file.
|
|
||||||
select {
|
|
||||||
case <-stopTailCh:
|
|
||||||
return
|
|
||||||
default:
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}()
|
|
||||||
|
|
||||||
var exitRaw []byte
|
|
||||||
var exitStatus int
|
var exitStatus int
|
||||||
var exitStatusRaw int64
|
|
||||||
err = cmd.Wait()
|
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 {
|
if exitErr, ok := err.(*exec.ExitError); ok {
|
||||||
exitStatus = 1
|
exitStatus = 1
|
||||||
|
|
||||||
|
@ -344,45 +280,8 @@ func (c *Communicator) run(cmd *exec.Cmd, remote *packer.RemoteCmd, stdin_w io.W
|
||||||
if status, ok := exitErr.Sys().(syscall.WaitStatus); ok {
|
if status, ok := exitErr.Sys().(syscall.WaitStatus); ok {
|
||||||
exitStatus = status.ExitStatus()
|
exitStatus = status.ExitStatus()
|
||||||
}
|
}
|
||||||
|
|
||||||
// Say that we ended, since if Docker itself failed, then
|
|
||||||
// the command must've not run, or so we assume
|
|
||||||
goto REMOTE_EXIT
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Wait for the exit code to appear in our file...
|
|
||||||
log.Println("Waiting for exit code to appear for remote command...")
|
|
||||||
for {
|
|
||||||
fi, err := os.Stat(exitCodePath)
|
|
||||||
if err == nil && fi.Size() > 0 {
|
|
||||||
break
|
|
||||||
}
|
|
||||||
|
|
||||||
time.Sleep(1 * time.Second)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Read the exit code
|
|
||||||
exitRaw, err = ioutil.ReadFile(exitCodePath)
|
|
||||||
if err != nil {
|
|
||||||
log.Printf("Error executing: %s", err)
|
|
||||||
exitStatus = 254
|
|
||||||
goto REMOTE_EXIT
|
|
||||||
}
|
|
||||||
|
|
||||||
exitStatusRaw, err = strconv.ParseInt(string(bytes.TrimSpace(exitRaw)), 10, 0)
|
|
||||||
if err != nil {
|
|
||||||
log.Printf("Error executing: %s", err)
|
|
||||||
exitStatus = 254
|
|
||||||
goto REMOTE_EXIT
|
|
||||||
}
|
|
||||||
exitStatus = int(exitStatusRaw)
|
|
||||||
log.Printf("Executed command exit status: %d", exitStatus)
|
|
||||||
|
|
||||||
REMOTE_EXIT:
|
|
||||||
// Wait for the tail to finish
|
|
||||||
close(stopTailCh)
|
|
||||||
<-doneCh
|
|
||||||
|
|
||||||
// Set the exit status which triggers waiters
|
// Set the exit status which triggers waiters
|
||||||
remote.SetExited(exitStatus)
|
remote.SetExited(exitStatus)
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in New Issue