github.com/lymingtonprecision/terraform@v0.9.9-0.20170613092852-62acef9611a9/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  		},
    32  
    33  		ApplyFunc: applyFn,
    34  	}
    35  }
    36  
    37  func applyFn(ctx context.Context) error {
    38  	data := ctx.Value(schema.ProvConfigDataKey).(*schema.ResourceData)
    39  	o := ctx.Value(schema.ProvOutputKey).(terraform.UIOutput)
    40  
    41  	command := data.Get("command").(string)
    42  	if command == "" {
    43  		return fmt.Errorf("local-exec provisioner command must be a non-empty string")
    44  	}
    45  
    46  	// Execute the command using a shell
    47  	var shell, flag string
    48  	if runtime.GOOS == "windows" {
    49  		shell = "cmd"
    50  		flag = "/C"
    51  	} else {
    52  		shell = "/bin/sh"
    53  		flag = "-c"
    54  	}
    55  
    56  	// Setup the reader that will read the output from the command.
    57  	// We use an os.Pipe so that the *os.File can be passed directly to the
    58  	// process, and not rely on goroutines copying the data which may block.
    59  	// See golang.org/issue/18874
    60  	pr, pw, err := os.Pipe()
    61  	if err != nil {
    62  		return fmt.Errorf("failed to initialize pipe for output: %s", err)
    63  	}
    64  
    65  	// Setup the command
    66  	cmd := exec.CommandContext(ctx, shell, flag, command)
    67  	cmd.Stderr = pw
    68  	cmd.Stdout = pw
    69  
    70  	output, _ := circbuf.NewBuffer(maxBufSize)
    71  
    72  	// Write everything we read from the pipe to the output buffer too
    73  	tee := io.TeeReader(pr, output)
    74  
    75  	// copy the teed output to the UI output
    76  	copyDoneCh := make(chan struct{})
    77  	go copyOutput(o, tee, copyDoneCh)
    78  
    79  	// Output what we're about to run
    80  	o.Output(fmt.Sprintf(
    81  		"Executing: %s %s \"%s\"",
    82  		shell, flag, command))
    83  
    84  	// Start the command
    85  	err = cmd.Start()
    86  	if err == nil {
    87  		err = cmd.Wait()
    88  	}
    89  
    90  	// Close the write-end of the pipe so that the goroutine mirroring output
    91  	// ends properly.
    92  	pw.Close()
    93  
    94  	// Cancelling the command may block the pipe reader if the file descriptor
    95  	// was passed to a child process which hasn't closed it. In this case the
    96  	// copyOutput goroutine will just hang out until exit.
    97  	select {
    98  	case <-copyDoneCh:
    99  	case <-ctx.Done():
   100  	}
   101  
   102  	if err != nil {
   103  		return fmt.Errorf("Error running command '%s': %v. Output: %s",
   104  			command, err, output.Bytes())
   105  	}
   106  
   107  	return nil
   108  }
   109  
   110  func copyOutput(o terraform.UIOutput, r io.Reader, doneCh chan<- struct{}) {
   111  	defer close(doneCh)
   112  	lr := linereader.New(r)
   113  	for line := range lr.Ch {
   114  		o.Output(line)
   115  	}
   116  }