github.com/graywolf-at-work-2/terraform-vendor@v1.4.5/internal/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/internal/configs/configschema" 13 "github.com/hashicorp/terraform/internal/provisioners" 14 "github.com/hashicorp/terraform/internal/tfdiags" 15 "github.com/mitchellh/go-linereader" 16 "github.com/zclconf/go-cty/cty" 17 ) 18 19 const ( 20 // maxBufSize limits how much output we collect from a local 21 // invocation. This is to prevent TF memory usage from growing 22 // to an enormous amount due to a faulty process. 23 maxBufSize = 8 * 1024 24 ) 25 26 func New() provisioners.Interface { 27 ctx, cancel := context.WithCancel(context.Background()) 28 return &provisioner{ 29 ctx: ctx, 30 cancel: cancel, 31 } 32 } 33 34 type provisioner struct { 35 // We store a context here tied to the lifetime of the provisioner. 36 // This allows the Stop method to cancel any in-flight requests. 37 ctx context.Context 38 cancel context.CancelFunc 39 } 40 41 func (p *provisioner) GetSchema() (resp provisioners.GetSchemaResponse) { 42 schema := &configschema.Block{ 43 Attributes: map[string]*configschema.Attribute{ 44 "command": { 45 Type: cty.String, 46 Required: true, 47 }, 48 "interpreter": { 49 Type: cty.List(cty.String), 50 Optional: true, 51 }, 52 "working_dir": { 53 Type: cty.String, 54 Optional: true, 55 }, 56 "environment": { 57 Type: cty.Map(cty.String), 58 Optional: true, 59 }, 60 "quiet": { 61 Type: cty.Bool, 62 Optional: true, 63 }, 64 }, 65 } 66 67 resp.Provisioner = schema 68 return resp 69 } 70 71 func (p *provisioner) ValidateProvisionerConfig(req provisioners.ValidateProvisionerConfigRequest) (resp provisioners.ValidateProvisionerConfigResponse) { 72 if _, err := p.GetSchema().Provisioner.CoerceValue(req.Config); err != nil { 73 resp.Diagnostics = resp.Diagnostics.Append(tfdiags.WholeContainingBody( 74 tfdiags.Error, 75 "Invalid local-exec provisioner configuration", 76 err.Error(), 77 )) 78 } 79 return resp 80 } 81 82 func (p *provisioner) ProvisionResource(req provisioners.ProvisionResourceRequest) (resp provisioners.ProvisionResourceResponse) { 83 command := req.Config.GetAttr("command").AsString() 84 if command == "" { 85 resp.Diagnostics = resp.Diagnostics.Append(tfdiags.WholeContainingBody( 86 tfdiags.Error, 87 "Invalid local-exec provisioner command", 88 "The command must be a non-empty string.", 89 )) 90 return resp 91 } 92 93 envVal := req.Config.GetAttr("environment") 94 var env []string 95 96 if !envVal.IsNull() { 97 for k, v := range envVal.AsValueMap() { 98 if !v.IsNull() { 99 entry := fmt.Sprintf("%s=%s", k, v.AsString()) 100 env = append(env, entry) 101 } 102 } 103 } 104 105 // Execute the command using a shell 106 intrVal := req.Config.GetAttr("interpreter") 107 108 var cmdargs []string 109 if !intrVal.IsNull() && intrVal.LengthInt() > 0 { 110 for _, v := range intrVal.AsValueSlice() { 111 if !v.IsNull() { 112 cmdargs = append(cmdargs, v.AsString()) 113 } 114 } 115 } else { 116 if runtime.GOOS == "windows" { 117 cmdargs = []string{"cmd", "/C"} 118 } else { 119 cmdargs = []string{"/bin/sh", "-c"} 120 } 121 } 122 123 cmdargs = append(cmdargs, command) 124 125 workingdir := "" 126 if wdVal := req.Config.GetAttr("working_dir"); !wdVal.IsNull() { 127 workingdir = wdVal.AsString() 128 } 129 130 // Set up the reader that will read the output from the command. 131 // We use an os.Pipe so that the *os.File can be passed directly to the 132 // process, and not rely on goroutines copying the data which may block. 133 // See golang.org/issue/18874 134 pr, pw, err := os.Pipe() 135 if err != nil { 136 resp.Diagnostics = resp.Diagnostics.Append(tfdiags.WholeContainingBody( 137 tfdiags.Error, 138 "local-exec provisioner error", 139 fmt.Sprintf("Failed to initialize pipe for output: %s", err), 140 )) 141 return resp 142 } 143 144 var cmdEnv []string 145 cmdEnv = os.Environ() 146 cmdEnv = append(cmdEnv, env...) 147 148 // Set up the command 149 cmd := exec.CommandContext(p.ctx, cmdargs[0], cmdargs[1:]...) 150 cmd.Stderr = pw 151 cmd.Stdout = pw 152 // Dir specifies the working directory of the command. 153 // If Dir is the empty string (this is default), runs the command 154 // in the calling process's current directory. 155 cmd.Dir = workingdir 156 // Env specifies the environment of the command. 157 // By default will use the calling process's environment 158 cmd.Env = cmdEnv 159 160 output, _ := circbuf.NewBuffer(maxBufSize) 161 162 // Write everything we read from the pipe to the output buffer too 163 tee := io.TeeReader(pr, output) 164 165 // copy the teed output to the UI output 166 copyDoneCh := make(chan struct{}) 167 go copyUIOutput(req.UIOutput, tee, copyDoneCh) 168 169 // Output what we're about to run 170 if quietVal := req.Config.GetAttr("quiet"); !quietVal.IsNull() && quietVal.True() { 171 req.UIOutput.Output("local-exec: Executing: Suppressed by quiet=true") 172 } else { 173 req.UIOutput.Output(fmt.Sprintf("Executing: %q", cmdargs)) 174 } 175 176 // Start the command 177 err = cmd.Start() 178 if err == nil { 179 err = cmd.Wait() 180 } 181 182 // Close the write-end of the pipe so that the goroutine mirroring output 183 // ends properly. 184 pw.Close() 185 186 // Cancelling the command may block the pipe reader if the file descriptor 187 // was passed to a child process which hasn't closed it. In this case the 188 // copyOutput goroutine will just hang out until exit. 189 select { 190 case <-copyDoneCh: 191 case <-p.ctx.Done(): 192 } 193 194 if err != nil { 195 resp.Diagnostics = resp.Diagnostics.Append(tfdiags.WholeContainingBody( 196 tfdiags.Error, 197 "local-exec provisioner error", 198 fmt.Sprintf("Error running command '%s': %v. Output: %s", command, err, output.Bytes()), 199 )) 200 return resp 201 } 202 203 return resp 204 } 205 206 func (p *provisioner) Stop() error { 207 p.cancel() 208 return nil 209 } 210 211 func (p *provisioner) Close() error { 212 return nil 213 } 214 215 func copyUIOutput(o provisioners.UIOutput, r io.Reader, doneCh chan<- struct{}) { 216 defer close(doneCh) 217 lr := linereader.New(r) 218 for line := range lr.Ch { 219 o.Output(line) 220 } 221 }