github.com/nathanielks/terraform@v0.6.1-0.20170509030759-13e1a62319dc/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": {
    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": {
    34  				Type:          schema.TypeString,
    35  				Optional:      true,
    36  				ConflictsWith: []string{"inline", "scripts"},
    37  			},
    38  
    39  			"scripts": {
    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  		line, ok := l.(string)
    85  		if !ok {
    86  			return nil, fmt.Errorf("Error parsing %v as a string", l)
    87  		}
    88  		lines = append(lines, line)
    89  	}
    90  	lines = append(lines, "")
    91  
    92  	return []string{strings.Join(lines, "\n")}, nil
    93  }
    94  
    95  // collectScripts is used to collect all the scripts we need
    96  // to execute in preparation for copying them.
    97  func collectScripts(d *schema.ResourceData) ([]io.ReadCloser, error) {
    98  	// Check if inline
    99  	if _, ok := d.GetOk("inline"); ok {
   100  		scripts, err := generateScripts(d)
   101  		if err != nil {
   102  			return nil, err
   103  		}
   104  
   105  		var r []io.ReadCloser
   106  		for _, script := range scripts {
   107  			r = append(r, ioutil.NopCloser(bytes.NewReader([]byte(script))))
   108  		}
   109  
   110  		return r, nil
   111  	}
   112  
   113  	// Collect scripts
   114  	var scripts []string
   115  	if script, ok := d.GetOk("script"); ok {
   116  		scr, ok := script.(string)
   117  		if !ok {
   118  			return nil, fmt.Errorf("Error parsing script %v as string", script)
   119  		}
   120  		scripts = append(scripts, scr)
   121  	}
   122  
   123  	if scriptList, ok := d.GetOk("scripts"); ok {
   124  		for _, script := range scriptList.([]interface{}) {
   125  			scr, ok := script.(string)
   126  			if !ok {
   127  				return nil, fmt.Errorf("Error parsing script %v as string", script)
   128  			}
   129  			scripts = append(scripts, scr)
   130  		}
   131  	}
   132  
   133  	// Open all the scripts
   134  	var fhs []io.ReadCloser
   135  	for _, s := range scripts {
   136  		fh, err := os.Open(s)
   137  		if err != nil {
   138  			for _, fh := range fhs {
   139  				fh.Close()
   140  			}
   141  			return nil, fmt.Errorf("Failed to open script '%s': %v", s, err)
   142  		}
   143  		fhs = append(fhs, fh)
   144  	}
   145  
   146  	// Done, return the file handles
   147  	return fhs, nil
   148  }
   149  
   150  // runScripts is used to copy and execute a set of scripts
   151  func runScripts(
   152  	ctx context.Context,
   153  	o terraform.UIOutput,
   154  	comm communicator.Communicator,
   155  	scripts []io.ReadCloser) error {
   156  	// Wrap out context in a cancelation function that we use to
   157  	// kill the connection.
   158  	ctx, cancelFunc := context.WithCancel(ctx)
   159  	defer cancelFunc()
   160  
   161  	// Wait for the context to end and then disconnect
   162  	go func() {
   163  		<-ctx.Done()
   164  		comm.Disconnect()
   165  	}()
   166  
   167  	// Wait and retry until we establish the connection
   168  	err := retryFunc(ctx, comm.Timeout(), func() error {
   169  		err := comm.Connect(o)
   170  		return err
   171  	})
   172  	if err != nil {
   173  		return err
   174  	}
   175  
   176  	for _, script := range scripts {
   177  		var cmd *remote.Cmd
   178  		outR, outW := io.Pipe()
   179  		errR, errW := io.Pipe()
   180  		outDoneCh := make(chan struct{})
   181  		errDoneCh := make(chan struct{})
   182  		go copyOutput(o, outR, outDoneCh)
   183  		go copyOutput(o, errR, errDoneCh)
   184  
   185  		remotePath := comm.ScriptPath()
   186  		err = retryFunc(ctx, comm.Timeout(), func() error {
   187  			if err := comm.UploadScript(remotePath, script); err != nil {
   188  				return fmt.Errorf("Failed to upload script: %v", err)
   189  			}
   190  
   191  			cmd = &remote.Cmd{
   192  				Command: remotePath,
   193  				Stdout:  outW,
   194  				Stderr:  errW,
   195  			}
   196  			if err := comm.Start(cmd); err != nil {
   197  				return fmt.Errorf("Error starting script: %v", err)
   198  			}
   199  
   200  			return nil
   201  		})
   202  		if err == nil {
   203  			cmd.Wait()
   204  			if cmd.ExitStatus != 0 {
   205  				err = fmt.Errorf("Script exited with non-zero exit status: %d", cmd.ExitStatus)
   206  			}
   207  		}
   208  
   209  		// If we have an error, end our context so the disconnect happens.
   210  		// This has to happen before the output cleanup below since during
   211  		// an interrupt this will cause the outputs to end.
   212  		if err != nil {
   213  			cancelFunc()
   214  		}
   215  
   216  		// Wait for output to clean up
   217  		outW.Close()
   218  		errW.Close()
   219  		<-outDoneCh
   220  		<-errDoneCh
   221  
   222  		// Upload a blank follow up file in the same path to prevent residual
   223  		// script contents from remaining on remote machine
   224  		empty := bytes.NewReader([]byte(""))
   225  		if err := comm.Upload(remotePath, empty); err != nil {
   226  			// This feature is best-effort.
   227  			log.Printf("[WARN] Failed to upload empty follow up script: %v", err)
   228  		}
   229  
   230  		// If we have an error, return it out now that we've cleaned up
   231  		if err != nil {
   232  			return err
   233  		}
   234  	}
   235  
   236  	return nil
   237  }
   238  
   239  func copyOutput(
   240  	o terraform.UIOutput, r io.Reader, doneCh chan<- struct{}) {
   241  	defer close(doneCh)
   242  	lr := linereader.New(r)
   243  	for line := range lr.Ch {
   244  		o.Output(line)
   245  	}
   246  }
   247  
   248  // retryFunc is used to retry a function for a given duration
   249  // TODO: this should probably backoff too
   250  func retryFunc(ctx context.Context, timeout time.Duration, f func() error) error {
   251  	// Build a new context with the timeout
   252  	ctx, done := context.WithTimeout(ctx, timeout)
   253  	defer done()
   254  
   255  	// container for atomic error value
   256  	type errWrap struct {
   257  		E error
   258  	}
   259  
   260  	// Try the function in a goroutine
   261  	var errVal atomic.Value
   262  	doneCh := make(chan struct{})
   263  	go func() {
   264  		defer close(doneCh)
   265  
   266  		for {
   267  			// If our context ended, we want to exit right away.
   268  			select {
   269  			case <-ctx.Done():
   270  				return
   271  			default:
   272  			}
   273  
   274  			// Try the function call
   275  			err := f()
   276  			errVal.Store(&errWrap{err})
   277  
   278  			if err == nil {
   279  				return
   280  			}
   281  
   282  			log.Printf("Retryable error: %v", err)
   283  		}
   284  	}()
   285  
   286  	// Wait for completion
   287  	select {
   288  	case <-ctx.Done():
   289  	case <-doneCh:
   290  	}
   291  
   292  	// Check if we have a context error to check if we're interrupted or timeout
   293  	switch ctx.Err() {
   294  	case context.Canceled:
   295  		return fmt.Errorf("interrupted")
   296  	case context.DeadlineExceeded:
   297  		return fmt.Errorf("timeout")
   298  	}
   299  
   300  	// Check if we got an error executing
   301  	if ev, ok := errVal.Load().(errWrap); ok {
   302  		return ev.E
   303  	}
   304  
   305  	return nil
   306  }