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