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