github.com/danp/terraform@v0.9.5-0.20170426144147-39d740081351/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 }