Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

provisioners/remote-exec: output to UI #365

Merged
merged 1 commit into from
Oct 6, 2014
Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
81 changes: 53 additions & 28 deletions builtin/provisioners/remote-exec/resource_provisioner.go
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
package remoteexec

import (
"bufio"
"bytes"
"fmt"
"io"
Expand All @@ -13,6 +12,7 @@ import (

helper "github.com/hashicorp/terraform/helper/ssh"
"github.com/hashicorp/terraform/terraform"
"github.com/mitchellh/go-linereader"
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

👍 helpful

)

const (
Expand Down Expand Up @@ -47,7 +47,7 @@ func (p *ResourceProvisioner) Apply(
}

// Copy and execute each script
if err := p.runScripts(conf, scripts); err != nil {
if err := p.runScripts(o, conf, scripts); err != nil {
return err
}
return nil
Expand Down Expand Up @@ -163,26 +163,51 @@ func (p *ResourceProvisioner) collectScripts(c *terraform.ResourceConfig) ([]io.
}

// runScripts is used to copy and execute a set of scripts
func (p *ResourceProvisioner) runScripts(conf *helper.SSHConfig, scripts []io.ReadCloser) error {
func (p *ResourceProvisioner) runScripts(
o terraform.UIOutput,
conf *helper.SSHConfig,
scripts []io.ReadCloser) error {
// Get the SSH client config
config, err := helper.PrepareConfig(conf)
if err != nil {
return err
}

o.Output(fmt.Sprintf(
"Connecting to remote host via SSH...\n"+
" Host: %s\n"+
" User: %s\n"+
" Password: %v\n"+
" Private key: %v",
conf.Host, conf.User,
conf.Password != "",
conf.KeyFile != ""))

// Wait and retry until we establish the SSH connection
var comm *helper.SSHCommunicator
err = retryFunc(conf.TimeoutVal, func() error {
host := fmt.Sprintf("%s:%d", conf.Host, conf.Port)
comm, err = helper.New(host, config)
if err != nil {
o.Output(fmt.Sprintf("Connection error, will retry: %s", err))
}

return err
})
if err != nil {
return err
}

o.Output("Connected! Executing scripts...")
for _, script := range scripts {
var cmd *helper.RemoteCmd
outR, outW := io.Pipe()
errR, errW := io.Pipe()
outDoneCh := make(chan struct{})
errDoneCh := make(chan struct{})
go p.copyOutput(o, outR, outDoneCh)
go p.copyOutput(o, errR, errDoneCh)

err := retryFunc(conf.TimeoutVal, func() error {
if err := comm.Upload(conf.ScriptPath, script); err != nil {
return fmt.Errorf("Failed to upload script: %v", err)
Expand All @@ -197,34 +222,47 @@ func (p *ResourceProvisioner) runScripts(conf *helper.SSHConfig, scripts []io.Re
}
cmd.Wait()

stdOutReader, stdOutWriter := io.Pipe()
stdErrReader, stdErrWriter := io.Pipe()
go streamLogs(stdOutReader, "stdout")
go streamLogs(stdErrReader, "stderr")

cmd = &helper.RemoteCmd{
Command: conf.ScriptPath,
Stdout: stdOutWriter,
Stderr: stdErrWriter,
Stdout: outW,
Stderr: errW,
}
if err := comm.Start(cmd); err != nil {
return fmt.Errorf("Error starting script: %v", err)
}
return nil
})
if err != nil {
return err
if err == nil {
cmd.Wait()
if cmd.ExitStatus != 0 {
err = fmt.Errorf("Script exited with non-zero exit status: %d", cmd.ExitStatus)
}
}

cmd.Wait()
if cmd.ExitStatus != 0 {
return fmt.Errorf("Script exited with non-zero exit status: %d", cmd.ExitStatus)
// Wait for output to clean up
outW.Close()
errW.Close()
<-outDoneCh
<-errDoneCh

// If we have an error, return it out now that we've cleaned up
if err != nil {
return err
}
}

return nil
}

func (p *ResourceProvisioner) copyOutput(
o terraform.UIOutput, r io.Reader, doneCh chan<- struct{}) {
defer close(doneCh)
lr := linereader.New(r)
for line := range lr.Ch {
o.Output(line)
}
}

// retryFunc is used to retry a function for a given duration
func retryFunc(timeout time.Duration, f func() error) error {
finish := time.After(timeout)
Expand All @@ -242,16 +280,3 @@ func retryFunc(timeout time.Duration, f func() error) error {
}
}
}

// streamLogs is used to stream lines from stdout/stderr
// of a remote command to log output for users.
func streamLogs(r io.ReadCloser, name string) {
defer r.Close()
scanner := bufio.NewScanner(r)
for scanner.Scan() {
log.Printf("remote-exec: %s: %s", name, scanner.Text())
}
if err := scanner.Err(); err != nil {
return
}
}