github.com/kjmkznr/terraform@v0.5.2-0.20180216194316-1d0f5fdac99e/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  	// Wrap out context in a cancelation function that we use to
   160  	// kill the connection.
   161  	ctx, cancelFunc := context.WithTimeout(ctx, comm.Timeout())
   162  	defer cancelFunc()
   163  
   164  	// Wait for the context to end and then disconnect
   165  	go func() {
   166  		<-ctx.Done()
   167  		comm.Disconnect()
   168  	}()
   169  
   170  	// Wait and retry until we establish the connection
   171  	err := communicator.Retry(ctx, func() error {
   172  		return comm.Connect(o)
   173  	})
   174  	if err != nil {
   175  		return err
   176  	}
   177  
   178  	for _, script := range scripts {
   179  		var cmd *remote.Cmd
   180  
   181  		outR, outW := io.Pipe()
   182  		errR, errW := io.Pipe()
   183  		defer outW.Close()
   184  		defer errW.Close()
   185  
   186  		go copyOutput(o, outR)
   187  		go copyOutput(o, errR)
   188  
   189  		remotePath := comm.ScriptPath()
   190  
   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  		cmd.Wait()
   205  		if cmd.ExitStatus != 0 {
   206  			err = fmt.Errorf("Script exited with non-zero exit status: %d", cmd.ExitStatus)
   207  		}
   208  
   209  		// Upload a blank follow up file in the same path to prevent residual
   210  		// script contents from remaining on remote machine
   211  		empty := bytes.NewReader([]byte(""))
   212  		if err := comm.Upload(remotePath, empty); err != nil {
   213  			// This feature is best-effort.
   214  			log.Printf("[WARN] Failed to upload empty follow up script: %v", err)
   215  		}
   216  	}
   217  
   218  	return nil
   219  }
   220  
   221  func copyOutput(
   222  	o terraform.UIOutput, r io.Reader) {
   223  	lr := linereader.New(r)
   224  	for line := range lr.Ch {
   225  		o.Output(line)
   226  	}
   227  }