github.com/handlerbot/terraform@v0.10.0-beta1.0.20180726153736-26b68d98f9cb/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  	"time"
    13  
    14  	"github.com/hashicorp/terraform/communicator"
    15  	"github.com/hashicorp/terraform/communicator/remote"
    16  	"github.com/hashicorp/terraform/helper/schema"
    17  	"github.com/hashicorp/terraform/terraform"
    18  	"github.com/mitchellh/go-linereader"
    19  )
    20  
    21  // maxBackoffDealy is the maximum delay between retry attempts
    22  var maxBackoffDelay = 10 * time.Second
    23  var initialBackoffDelay = time.Second
    24  
    25  func Provisioner() terraform.ResourceProvisioner {
    26  	return &schema.Provisioner{
    27  		Schema: map[string]*schema.Schema{
    28  			"inline": {
    29  				Type:          schema.TypeList,
    30  				Elem:          &schema.Schema{Type: schema.TypeString},
    31  				PromoteSingle: true,
    32  				Optional:      true,
    33  				ConflictsWith: []string{"script", "scripts"},
    34  			},
    35  
    36  			"script": {
    37  				Type:          schema.TypeString,
    38  				Optional:      true,
    39  				ConflictsWith: []string{"inline", "scripts"},
    40  			},
    41  
    42  			"scripts": {
    43  				Type:          schema.TypeList,
    44  				Elem:          &schema.Schema{Type: schema.TypeString},
    45  				Optional:      true,
    46  				ConflictsWith: []string{"script", "inline"},
    47  			},
    48  		},
    49  
    50  		ApplyFunc: applyFn,
    51  	}
    52  }
    53  
    54  // Apply executes the remote exec provisioner
    55  func applyFn(ctx context.Context) error {
    56  	connState := ctx.Value(schema.ProvRawStateKey).(*terraform.InstanceState)
    57  	data := ctx.Value(schema.ProvConfigDataKey).(*schema.ResourceData)
    58  	o := ctx.Value(schema.ProvOutputKey).(terraform.UIOutput)
    59  
    60  	// Get a new communicator
    61  	comm, err := communicator.New(connState)
    62  	if err != nil {
    63  		return err
    64  	}
    65  
    66  	// Collect the scripts
    67  	scripts, err := collectScripts(data)
    68  	if err != nil {
    69  		return err
    70  	}
    71  	for _, s := range scripts {
    72  		defer s.Close()
    73  	}
    74  
    75  	// Copy and execute each script
    76  	if err := runScripts(ctx, o, comm, scripts); err != nil {
    77  		return err
    78  	}
    79  
    80  	return nil
    81  }
    82  
    83  // generateScripts takes the configuration and creates a script from each inline config
    84  func generateScripts(d *schema.ResourceData) ([]string, error) {
    85  	var lines []string
    86  	for _, l := range d.Get("inline").([]interface{}) {
    87  		line, ok := l.(string)
    88  		if !ok {
    89  			return nil, fmt.Errorf("Error parsing %v as a string", l)
    90  		}
    91  		lines = append(lines, line)
    92  	}
    93  	lines = append(lines, "")
    94  
    95  	return []string{strings.Join(lines, "\n")}, nil
    96  }
    97  
    98  // collectScripts is used to collect all the scripts we need
    99  // to execute in preparation for copying them.
   100  func collectScripts(d *schema.ResourceData) ([]io.ReadCloser, error) {
   101  	// Check if inline
   102  	if _, ok := d.GetOk("inline"); ok {
   103  		scripts, err := generateScripts(d)
   104  		if err != nil {
   105  			return nil, err
   106  		}
   107  
   108  		var r []io.ReadCloser
   109  		for _, script := range scripts {
   110  			r = append(r, ioutil.NopCloser(bytes.NewReader([]byte(script))))
   111  		}
   112  
   113  		return r, nil
   114  	}
   115  
   116  	// Collect scripts
   117  	var scripts []string
   118  	if script, ok := d.GetOk("script"); ok {
   119  		scr, ok := script.(string)
   120  		if !ok {
   121  			return nil, fmt.Errorf("Error parsing script %v as string", script)
   122  		}
   123  		scripts = append(scripts, scr)
   124  	}
   125  
   126  	if scriptList, ok := d.GetOk("scripts"); ok {
   127  		for _, script := range scriptList.([]interface{}) {
   128  			scr, ok := script.(string)
   129  			if !ok {
   130  				return nil, fmt.Errorf("Error parsing script %v as string", script)
   131  			}
   132  			scripts = append(scripts, scr)
   133  		}
   134  	}
   135  
   136  	// Open all the scripts
   137  	var fhs []io.ReadCloser
   138  	for _, s := range scripts {
   139  		fh, err := os.Open(s)
   140  		if err != nil {
   141  			for _, fh := range fhs {
   142  				fh.Close()
   143  			}
   144  			return nil, fmt.Errorf("Failed to open script '%s': %v", s, err)
   145  		}
   146  		fhs = append(fhs, fh)
   147  	}
   148  
   149  	// Done, return the file handles
   150  	return fhs, nil
   151  }
   152  
   153  // runScripts is used to copy and execute a set of scripts
   154  func runScripts(
   155  	ctx context.Context,
   156  	o terraform.UIOutput,
   157  	comm communicator.Communicator,
   158  	scripts []io.ReadCloser) error {
   159  
   160  	retryCtx, cancel := context.WithTimeout(ctx, comm.Timeout())
   161  	defer cancel()
   162  
   163  	// Wait and retry until we establish the connection
   164  	err := communicator.Retry(retryCtx, func() error {
   165  		return comm.Connect(o)
   166  	})
   167  	if err != nil {
   168  		return err
   169  	}
   170  
   171  	// Wait for the context to end and then disconnect
   172  	go func() {
   173  		<-ctx.Done()
   174  		comm.Disconnect()
   175  	}()
   176  
   177  	for _, script := range scripts {
   178  		var cmd *remote.Cmd
   179  
   180  		outR, outW := io.Pipe()
   181  		errR, errW := io.Pipe()
   182  		defer outW.Close()
   183  		defer errW.Close()
   184  
   185  		go copyOutput(o, outR)
   186  		go copyOutput(o, errR)
   187  
   188  		remotePath := comm.ScriptPath()
   189  
   190  		if err := comm.UploadScript(remotePath, script); err != nil {
   191  			return fmt.Errorf("Failed to upload script: %v", err)
   192  		}
   193  
   194  		cmd = &remote.Cmd{
   195  			Command: remotePath,
   196  			Stdout:  outW,
   197  			Stderr:  errW,
   198  		}
   199  		if err := comm.Start(cmd); err != nil {
   200  			return fmt.Errorf("Error starting script: %v", err)
   201  		}
   202  
   203  		if err := cmd.Wait(); err != nil {
   204  			return err
   205  		}
   206  
   207  		// Upload a blank follow up file in the same path to prevent residual
   208  		// script contents from remaining on remote machine
   209  		empty := bytes.NewReader([]byte(""))
   210  		if err := comm.Upload(remotePath, empty); err != nil {
   211  			// This feature is best-effort.
   212  			log.Printf("[WARN] Failed to upload empty follow up script: %v", err)
   213  		}
   214  	}
   215  
   216  	return nil
   217  }
   218  
   219  func copyOutput(
   220  	o terraform.UIOutput, r io.Reader) {
   221  	lr := linereader.New(r)
   222  	for line := range lr.Ch {
   223  		o.Output(line)
   224  	}
   225  }