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 }