diff --git a/builder/docker/exec.go b/builder/docker/exec.go index 8aa2c019d..e04461bb6 100644 --- a/builder/docker/exec.go +++ b/builder/docker/exec.go @@ -1,114 +1,26 @@ package docker import ( - "fmt" - "io" - "log" "os/exec" - "regexp" - "strings" - "sync" - "syscall" - "github.com/hashicorp/packer/common/iochan" + "github.com/hashicorp/packer/helper/builder/localexec" "github.com/hashicorp/packer/packer" ) func runAndStream(cmd *exec.Cmd, ui packer.Ui) error { - stdout_r, stdout_w := io.Pipe() - stderr_r, stderr_w := io.Pipe() - defer stdout_w.Close() - defer stderr_w.Close() args := make([]string, len(cmd.Args)-1) copy(args, cmd.Args[1:]) // Scrub password from the log output. + capturedPassword := "" for i, v := range args { if v == "-p" || v == "--password" { - args[i+1] = "" + capturedPassword = args[i+1] break } } - log.Printf("Executing: %s %v", cmd.Path, args) - cmd.Stdout = stdout_w - cmd.Stderr = stderr_w - if err := cmd.Start(); err != nil { - return err - } - - // Create the channels we'll use for data - exitCh := make(chan int, 1) - stdoutCh := iochan.LineReader(stdout_r) - stderrCh := iochan.LineReader(stderr_r) - - // Start the goroutine to watch for the exit - go func() { - defer stdout_w.Close() - defer stderr_w.Close() - exitStatus := 0 - - 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() - } - } - - exitCh <- exitStatus - }() - - // This waitgroup waits for the streaming to end - var streamWg sync.WaitGroup - streamWg.Add(2) - - streamFunc := func(ch <-chan string) { - defer streamWg.Done() - - for data := range ch { - data = cleanOutputLine(data) - if data != "" { - ui.Message(data) - } - } - } - - // Stream stderr/stdout - go streamFunc(stderrCh) - go streamFunc(stdoutCh) - - // Wait for the process to end and then wait for the streaming to end - exitStatus := <-exitCh - streamWg.Wait() - - if exitStatus != 0 { - return fmt.Errorf("Bad exit status: %d", exitStatus) - } - - return nil -} - -// cleanOutputLine cleans up a line so that '\r' don't muck up the -// UI output when we're reading from a remote command. -func cleanOutputLine(line string) string { - // Build a regular expression that will get rid of shell codes - re := regexp.MustCompile("(?i)\x1b\\[([0-9]{1,2}(;[0-9]{1,2})?)?[a|b|m|k]") - line = re.ReplaceAllString(line, "") - - // Trim surrounding whitespace - line = strings.TrimSpace(line) - - // Trim up to the first carriage return, since that text would be - // lost anyways. - idx := strings.LastIndex(line, "\r") - if idx > -1 { - line = line[idx+1:] - } - - return line + // run local command and stream output to UI. + return localexec.RunAndStream(cmd, ui, []string{capturedPassword}) } diff --git a/helper/builder/localexec/run_and_stream.go b/helper/builder/localexec/run_and_stream.go new file mode 100644 index 000000000..f40076cdb --- /dev/null +++ b/helper/builder/localexec/run_and_stream.go @@ -0,0 +1,109 @@ +package localexec + +import ( + "fmt" + "io" + "log" + "os/exec" + "regexp" + "strings" + "sync" + "syscall" + + "github.com/hashicorp/packer/common/iochan" + "github.com/hashicorp/packer/packer" +) + +func RunAndStream(cmd *exec.Cmd, ui packer.Ui, sensitive []string) error { + stdout_r, stdout_w := io.Pipe() + stderr_r, stderr_w := io.Pipe() + defer stdout_w.Close() + defer stderr_w.Close() + + // Scrub any sensitive values from being printed to Packer ui. + packer.LogSecretFilter.Set(sensitive...) + + args := make([]string, len(cmd.Args)-1) + copy(args, cmd.Args[1:]) + + log.Printf("Executing: %s %v", cmd.Path, args) + cmd.Stdout = stdout_w + cmd.Stderr = stderr_w + if err := cmd.Start(); err != nil { + return err + } + + // Create the channels we'll use for data + exitCh := make(chan int, 1) + stdoutCh := iochan.LineReader(stdout_r) + stderrCh := iochan.LineReader(stderr_r) + + // Start the goroutine to watch for the exit + go func() { + defer stdout_w.Close() + defer stderr_w.Close() + exitStatus := 0 + + 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() + } + } + + exitCh <- exitStatus + }() + + // This waitgroup waits for the streaming to end + var streamWg sync.WaitGroup + streamWg.Add(2) + + streamFunc := func(ch <-chan string) { + defer streamWg.Done() + + for data := range ch { + data = cleanOutputLine(data) + if data != "" { + ui.Message(data) + } + } + } + + // Stream stderr/stdout + go streamFunc(stderrCh) + go streamFunc(stdoutCh) + + // Wait for the process to end and then wait for the streaming to end + exitStatus := <-exitCh + streamWg.Wait() + + if exitStatus != 0 { + return fmt.Errorf("Bad exit status: %d", exitStatus) + } + + return nil +} + +// cleanOutputLine cleans up a line so that '\r' don't muck up the +// UI output when we're reading from a remote command. +func cleanOutputLine(line string) string { + // Build a regular expression that will get rid of shell codes + re := regexp.MustCompile("(?i)\x1b\\[([0-9]{1,2}(;[0-9]{1,2})?)?[a|b|m|k]") + line = re.ReplaceAllString(line, "") + + // Trim surrounding whitespace + line = strings.TrimSpace(line) + + // Trim up to the first carriage return, since that text would be + // lost anyways. + idx := strings.LastIndex(line, "\r") + if idx > -1 { + line = line[idx+1:] + } + + return line +} diff --git a/builder/docker/exec_test.go b/helper/builder/localexec/run_and_stream_test.go similarity index 97% rename from builder/docker/exec_test.go rename to helper/builder/localexec/run_and_stream_test.go index c035c9eaf..533b72801 100644 --- a/builder/docker/exec_test.go +++ b/helper/builder/localexec/run_and_stream_test.go @@ -1,4 +1,4 @@ -package docker +package localexec import ( "testing"