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