github.com/hartzell/terraform@v0.8.6-0.20180503104400-0cc9e050ecd4/builtin/provisioners/local-exec/resource_provisioner.go (about)

     1  package localexec
     2  
     3  import (
     4  	"context"
     5  	"fmt"
     6  	"io"
     7  	"os"
     8  	"os/exec"
     9  	"runtime"
    10  
    11  	"github.com/armon/circbuf"
    12  	"github.com/hashicorp/terraform/helper/schema"
    13  	"github.com/hashicorp/terraform/terraform"
    14  	"github.com/mitchellh/go-linereader"
    15  )
    16  
    17  const (
    18  	// maxBufSize limits how much output we collect from a local
    19  	// invocation. This is to prevent TF memory usage from growing
    20  	// to an enormous amount due to a faulty process.
    21  	maxBufSize = 8 * 1024
    22  )
    23  
    24  func Provisioner() terraform.ResourceProvisioner {
    25  	return &schema.Provisioner{
    26  		Schema: map[string]*schema.Schema{
    27  			"command": &schema.Schema{
    28  				Type:     schema.TypeString,
    29  				Required: true,
    30  			},
    31  			"interpreter": &schema.Schema{
    32  				Type:     schema.TypeList,
    33  				Elem:     &schema.Schema{Type: schema.TypeString},
    34  				Optional: true,
    35  			},
    36  			"working_dir": &schema.Schema{
    37  				Type:     schema.TypeString,
    38  				Optional: true,
    39  			},
    40  			"environment": &schema.Schema{
    41  				Type:     schema.TypeMap,
    42  				Optional: true,
    43  			},
    44  		},
    45  
    46  		ApplyFunc: applyFn,
    47  	}
    48  }
    49  
    50  func applyFn(ctx context.Context) error {
    51  	data := ctx.Value(schema.ProvConfigDataKey).(*schema.ResourceData)
    52  	o := ctx.Value(schema.ProvOutputKey).(terraform.UIOutput)
    53  
    54  	command := data.Get("command").(string)
    55  	if command == "" {
    56  		return fmt.Errorf("local-exec provisioner command must be a non-empty string")
    57  	}
    58  
    59  	// Execute the command with env
    60  	environment := data.Get("environment").(map[string]interface{})
    61  
    62  	var env []string
    63  	env = make([]string, len(environment))
    64  	for k := range environment {
    65  		entry := fmt.Sprintf("%s=%s", k, environment[k].(string))
    66  		env = append(env, entry)
    67  	}
    68  
    69  	// Execute the command using a shell
    70  	interpreter := data.Get("interpreter").([]interface{})
    71  
    72  	var cmdargs []string
    73  	if len(interpreter) > 0 {
    74  		for _, i := range interpreter {
    75  			if arg, ok := i.(string); ok {
    76  				cmdargs = append(cmdargs, arg)
    77  			}
    78  		}
    79  	} else {
    80  		if runtime.GOOS == "windows" {
    81  			cmdargs = []string{"cmd", "/C"}
    82  		} else {
    83  			cmdargs = []string{"/bin/sh", "-c"}
    84  		}
    85  	}
    86  	cmdargs = append(cmdargs, command)
    87  
    88  	workingdir := data.Get("working_dir").(string)
    89  
    90  	// Setup the reader that will read the output from the command.
    91  	// We use an os.Pipe so that the *os.File can be passed directly to the
    92  	// process, and not rely on goroutines copying the data which may block.
    93  	// See golang.org/issue/18874
    94  	pr, pw, err := os.Pipe()
    95  	if err != nil {
    96  		return fmt.Errorf("failed to initialize pipe for output: %s", err)
    97  	}
    98  
    99  	var cmdEnv []string
   100  	cmdEnv = os.Environ()
   101  	cmdEnv = append(cmdEnv, env...)
   102  
   103  	// Setup the command
   104  	cmd := exec.CommandContext(ctx, cmdargs[0], cmdargs[1:]...)
   105  	cmd.Stderr = pw
   106  	cmd.Stdout = pw
   107  	// Dir specifies the working directory of the command.
   108  	// If Dir is the empty string (this is default), runs the command
   109  	// in the calling process's current directory.
   110  	cmd.Dir = workingdir
   111  	// Env specifies the environment of the command.
   112  	// By default will use the calling process's environment
   113  	cmd.Env = cmdEnv
   114  
   115  	output, _ := circbuf.NewBuffer(maxBufSize)
   116  
   117  	// Write everything we read from the pipe to the output buffer too
   118  	tee := io.TeeReader(pr, output)
   119  
   120  	// copy the teed output to the UI output
   121  	copyDoneCh := make(chan struct{})
   122  	go copyOutput(o, tee, copyDoneCh)
   123  
   124  	// Output what we're about to run
   125  	o.Output(fmt.Sprintf("Executing: %q", cmdargs))
   126  
   127  	// Start the command
   128  	err = cmd.Start()
   129  	if err == nil {
   130  		err = cmd.Wait()
   131  	}
   132  
   133  	// Close the write-end of the pipe so that the goroutine mirroring output
   134  	// ends properly.
   135  	pw.Close()
   136  
   137  	// Cancelling the command may block the pipe reader if the file descriptor
   138  	// was passed to a child process which hasn't closed it. In this case the
   139  	// copyOutput goroutine will just hang out until exit.
   140  	select {
   141  	case <-copyDoneCh:
   142  	case <-ctx.Done():
   143  	}
   144  
   145  	if err != nil {
   146  		return fmt.Errorf("Error running command '%s': %v. Output: %s",
   147  			command, err, output.Bytes())
   148  	}
   149  
   150  	return nil
   151  }
   152  
   153  func copyOutput(o terraform.UIOutput, r io.Reader, doneCh chan<- struct{}) {
   154  	defer close(doneCh)
   155  	lr := linereader.New(r)
   156  	for line := range lr.Ch {
   157  		o.Output(line)
   158  	}
   159  }