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