github.com/peterbale/terraform@v0.9.0-beta2.0.20170315142748-5723acd55547/builtin/provisioners/remote-exec/resource_provisioner.go (about)

     1  package remoteexec
     2  
     3  import (
     4  	"bytes"
     5  	"context"
     6  	"fmt"
     7  	"io"
     8  	"io/ioutil"
     9  	"log"
    10  	"os"
    11  	"strings"
    12  	"sync/atomic"
    13  	"time"
    14  
    15  	"github.com/hashicorp/terraform/communicator"
    16  	"github.com/hashicorp/terraform/communicator/remote"
    17  	"github.com/hashicorp/terraform/helper/schema"
    18  	"github.com/hashicorp/terraform/terraform"
    19  	"github.com/mitchellh/go-linereader"
    20  )
    21  
    22  func Provisioner() terraform.ResourceProvisioner {
    23  	return &schema.Provisioner{
    24  		Schema: map[string]*schema.Schema{
    25  			"inline": &schema.Schema{
    26  				Type:          schema.TypeList,
    27  				Elem:          &schema.Schema{Type: schema.TypeString},
    28  				PromoteSingle: true,
    29  				Optional:      true,
    30  				ConflictsWith: []string{"script", "scripts"},
    31  			},
    32  
    33  			"script": &schema.Schema{
    34  				Type:          schema.TypeString,
    35  				Optional:      true,
    36  				ConflictsWith: []string{"inline", "scripts"},
    37  			},
    38  
    39  			"scripts": &schema.Schema{
    40  				Type:          schema.TypeList,
    41  				Elem:          &schema.Schema{Type: schema.TypeString},
    42  				Optional:      true,
    43  				ConflictsWith: []string{"script", "inline"},
    44  			},
    45  		},
    46  
    47  		ApplyFunc: applyFn,
    48  	}
    49  }
    50  
    51  // Apply executes the remote exec provisioner
    52  func applyFn(ctx context.Context) error {
    53  	connState := ctx.Value(schema.ProvRawStateKey).(*terraform.InstanceState)
    54  	data := ctx.Value(schema.ProvConfigDataKey).(*schema.ResourceData)
    55  	o := ctx.Value(schema.ProvOutputKey).(terraform.UIOutput)
    56  
    57  	// Get a new communicator
    58  	comm, err := communicator.New(connState)
    59  	if err != nil {
    60  		return err
    61  	}
    62  
    63  	// Collect the scripts
    64  	scripts, err := collectScripts(data)
    65  	if err != nil {
    66  		return err
    67  	}
    68  	for _, s := range scripts {
    69  		defer s.Close()
    70  	}
    71  
    72  	// Copy and execute each script
    73  	if err := runScripts(ctx, o, comm, scripts); err != nil {
    74  		return err
    75  	}
    76  
    77  	return nil
    78  }
    79  
    80  // generateScripts takes the configuration and creates a script from each inline config
    81  func generateScripts(d *schema.ResourceData) ([]string, error) {
    82  	var lines []string
    83  	for _, l := range d.Get("inline").([]interface{}) {
    84  		lines = append(lines, l.(string))
    85  	}
    86  	lines = append(lines, "")
    87  
    88  	return []string{strings.Join(lines, "\n")}, nil
    89  }
    90  
    91  // collectScripts is used to collect all the scripts we need
    92  // to execute in preparation for copying them.
    93  func collectScripts(d *schema.ResourceData) ([]io.ReadCloser, error) {
    94  	// Check if inline
    95  	if _, ok := d.GetOk("inline"); ok {
    96  		scripts, err := generateScripts(d)
    97  		if err != nil {
    98  			return nil, err
    99  		}
   100  
   101  		var r []io.ReadCloser
   102  		for _, script := range scripts {
   103  			r = append(r, ioutil.NopCloser(bytes.NewReader([]byte(script))))
   104  		}
   105  
   106  		return r, nil
   107  	}
   108  
   109  	// Collect scripts
   110  	var scripts []string
   111  	if script, ok := d.GetOk("script"); ok {
   112  		scripts = append(scripts, script.(string))
   113  	}
   114  
   115  	if scriptList, ok := d.GetOk("scripts"); ok {
   116  		for _, script := range scriptList.([]interface{}) {
   117  			scripts = append(scripts, script.(string))
   118  		}
   119  	}
   120  
   121  	// Open all the scripts
   122  	var fhs []io.ReadCloser
   123  	for _, s := range scripts {
   124  		fh, err := os.Open(s)
   125  		if err != nil {
   126  			for _, fh := range fhs {
   127  				fh.Close()
   128  			}
   129  			return nil, fmt.Errorf("Failed to open script '%s': %v", s, err)
   130  		}
   131  		fhs = append(fhs, fh)
   132  	}
   133  
   134  	// Done, return the file handles
   135  	return fhs, nil
   136  }
   137  
   138  // runScripts is used to copy and execute a set of scripts
   139  func runScripts(
   140  	ctx context.Context,
   141  	o terraform.UIOutput,
   142  	comm communicator.Communicator,
   143  	scripts []io.ReadCloser) error {
   144  	// Wrap out context in a cancelation function that we use to
   145  	// kill the connection.
   146  	ctx, cancelFunc := context.WithCancel(ctx)
   147  	defer cancelFunc()
   148  
   149  	// Wait for the context to end and then disconnect
   150  	go func() {
   151  		<-ctx.Done()
   152  		comm.Disconnect()
   153  	}()
   154  
   155  	// Wait and retry until we establish the connection
   156  	err := retryFunc(ctx, comm.Timeout(), func() error {
   157  		err := comm.Connect(o)
   158  		return err
   159  	})
   160  	if err != nil {
   161  		return err
   162  	}
   163  
   164  	for _, script := range scripts {
   165  		var cmd *remote.Cmd
   166  		outR, outW := io.Pipe()
   167  		errR, errW := io.Pipe()
   168  		outDoneCh := make(chan struct{})
   169  		errDoneCh := make(chan struct{})
   170  		go copyOutput(o, outR, outDoneCh)
   171  		go copyOutput(o, errR, errDoneCh)
   172  
   173  		remotePath := comm.ScriptPath()
   174  		err = retryFunc(ctx, comm.Timeout(), func() error {
   175  			if err := comm.UploadScript(remotePath, script); err != nil {
   176  				return fmt.Errorf("Failed to upload script: %v", err)
   177  			}
   178  
   179  			cmd = &remote.Cmd{
   180  				Command: remotePath,
   181  				Stdout:  outW,
   182  				Stderr:  errW,
   183  			}
   184  			if err := comm.Start(cmd); err != nil {
   185  				return fmt.Errorf("Error starting script: %v", err)
   186  			}
   187  
   188  			return nil
   189  		})
   190  		if err == nil {
   191  			cmd.Wait()
   192  			if cmd.ExitStatus != 0 {
   193  				err = fmt.Errorf("Script exited with non-zero exit status: %d", cmd.ExitStatus)
   194  			}
   195  		}
   196  
   197  		// If we have an error, end our context so the disconnect happens.
   198  		// This has to happen before the output cleanup below since during
   199  		// an interrupt this will cause the outputs to end.
   200  		if err != nil {
   201  			cancelFunc()
   202  		}
   203  
   204  		// Wait for output to clean up
   205  		outW.Close()
   206  		errW.Close()
   207  		<-outDoneCh
   208  		<-errDoneCh
   209  
   210  		// Upload a blank follow up file in the same path to prevent residual
   211  		// script contents from remaining on remote machine
   212  		empty := bytes.NewReader([]byte(""))
   213  		if err := comm.Upload(remotePath, empty); err != nil {
   214  			// This feature is best-effort.
   215  			log.Printf("[WARN] Failed to upload empty follow up script: %v", err)
   216  		}
   217  
   218  		// If we have an error, return it out now that we've cleaned up
   219  		if err != nil {
   220  			return err
   221  		}
   222  	}
   223  
   224  	return nil
   225  }
   226  
   227  func copyOutput(
   228  	o terraform.UIOutput, r io.Reader, doneCh chan<- struct{}) {
   229  	defer close(doneCh)
   230  	lr := linereader.New(r)
   231  	for line := range lr.Ch {
   232  		o.Output(line)
   233  	}
   234  }
   235  
   236  // retryFunc is used to retry a function for a given duration
   237  // TODO: this should probably backoff too
   238  func retryFunc(ctx context.Context, timeout time.Duration, f func() error) error {
   239  	// Build a new context with the timeout
   240  	ctx, done := context.WithTimeout(ctx, timeout)
   241  	defer done()
   242  
   243  	// container for atomic error value
   244  	type errWrap struct {
   245  		E error
   246  	}
   247  
   248  	// Try the function in a goroutine
   249  	var errVal atomic.Value
   250  	doneCh := make(chan struct{})
   251  	go func() {
   252  		defer close(doneCh)
   253  
   254  		for {
   255  			// If our context ended, we want to exit right away.
   256  			select {
   257  			case <-ctx.Done():
   258  				return
   259  			default:
   260  			}
   261  
   262  			// Try the function call
   263  			err := f()
   264  			errVal.Store(&errWrap{err})
   265  
   266  			if err == nil {
   267  				return
   268  			}
   269  
   270  			log.Printf("Retryable error: %v", err)
   271  		}
   272  	}()
   273  
   274  	// Wait for completion
   275  	select {
   276  	case <-ctx.Done():
   277  	case <-doneCh:
   278  	}
   279  
   280  	// Check if we have a context error to check if we're interrupted or timeout
   281  	switch ctx.Err() {
   282  	case context.Canceled:
   283  		return fmt.Errorf("interrupted")
   284  	case context.DeadlineExceeded:
   285  		return fmt.Errorf("timeout")
   286  	}
   287  
   288  	// Check if we got an error executing
   289  	if ev, ok := errVal.Load().(errWrap); ok {
   290  		return ev.E
   291  	}
   292  
   293  	return nil
   294  }