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