github.com/graywolf-at-work-2/terraform-vendor@v1.4.5/internal/builtin/provisioners/remote-exec/resource_provisioner.go (about) 1 package remoteexec 2 3 import ( 4 "bytes" 5 "context" 6 "errors" 7 "fmt" 8 "io" 9 "io/ioutil" 10 "log" 11 "os" 12 "strings" 13 14 "github.com/hashicorp/terraform/internal/communicator" 15 "github.com/hashicorp/terraform/internal/communicator/remote" 16 "github.com/hashicorp/terraform/internal/configs/configschema" 17 "github.com/hashicorp/terraform/internal/provisioners" 18 "github.com/hashicorp/terraform/internal/tfdiags" 19 "github.com/mitchellh/go-linereader" 20 "github.com/zclconf/go-cty/cty" 21 ) 22 23 func New() provisioners.Interface { 24 ctx, cancel := context.WithCancel(context.Background()) 25 return &provisioner{ 26 ctx: ctx, 27 cancel: cancel, 28 } 29 } 30 31 type provisioner struct { 32 // We store a context here tied to the lifetime of the provisioner. 33 // This allows the Stop method to cancel any in-flight requests. 34 ctx context.Context 35 cancel context.CancelFunc 36 } 37 38 func (p *provisioner) GetSchema() (resp provisioners.GetSchemaResponse) { 39 schema := &configschema.Block{ 40 Attributes: map[string]*configschema.Attribute{ 41 "inline": { 42 Type: cty.List(cty.String), 43 Optional: true, 44 }, 45 "script": { 46 Type: cty.String, 47 Optional: true, 48 }, 49 "scripts": { 50 Type: cty.List(cty.String), 51 Optional: true, 52 }, 53 }, 54 } 55 56 resp.Provisioner = schema 57 return resp 58 } 59 60 func (p *provisioner) ValidateProvisionerConfig(req provisioners.ValidateProvisionerConfigRequest) (resp provisioners.ValidateProvisionerConfigResponse) { 61 cfg, err := p.GetSchema().Provisioner.CoerceValue(req.Config) 62 if err != nil { 63 resp.Diagnostics = resp.Diagnostics.Append(tfdiags.WholeContainingBody( 64 tfdiags.Error, 65 "Invalid remote-exec provisioner configuration", 66 err.Error(), 67 )) 68 return resp 69 } 70 71 inline := cfg.GetAttr("inline") 72 script := cfg.GetAttr("script") 73 scripts := cfg.GetAttr("scripts") 74 75 set := 0 76 if !inline.IsNull() { 77 set++ 78 } 79 if !script.IsNull() { 80 set++ 81 } 82 if !scripts.IsNull() { 83 set++ 84 } 85 if set != 1 { 86 resp.Diagnostics = resp.Diagnostics.Append(tfdiags.WholeContainingBody( 87 tfdiags.Error, 88 "Invalid remote-exec provisioner configuration", 89 `Only one of "inline", "script", or "scripts" must be set`, 90 )) 91 } 92 return resp 93 } 94 95 func (p *provisioner) ProvisionResource(req provisioners.ProvisionResourceRequest) (resp provisioners.ProvisionResourceResponse) { 96 if req.Connection.IsNull() { 97 resp.Diagnostics = resp.Diagnostics.Append(tfdiags.WholeContainingBody( 98 tfdiags.Error, 99 "remote-exec provisioner error", 100 "Missing connection configuration for provisioner.", 101 )) 102 return resp 103 } 104 105 comm, err := communicator.New(req.Connection) 106 if err != nil { 107 resp.Diagnostics = resp.Diagnostics.Append(tfdiags.WholeContainingBody( 108 tfdiags.Error, 109 "remote-exec provisioner error", 110 err.Error(), 111 )) 112 return resp 113 } 114 115 // Collect the scripts 116 scripts, err := collectScripts(req.Config) 117 if err != nil { 118 resp.Diagnostics = resp.Diagnostics.Append(tfdiags.WholeContainingBody( 119 tfdiags.Error, 120 "remote-exec provisioner error", 121 err.Error(), 122 )) 123 return resp 124 } 125 for _, s := range scripts { 126 defer s.Close() 127 } 128 129 // Copy and execute each script 130 if err := runScripts(p.ctx, req.UIOutput, comm, scripts); err != nil { 131 resp.Diagnostics = resp.Diagnostics.Append(tfdiags.WholeContainingBody( 132 tfdiags.Error, 133 "remote-exec provisioner error", 134 err.Error(), 135 )) 136 return resp 137 } 138 139 return resp 140 } 141 142 func (p *provisioner) Stop() error { 143 p.cancel() 144 return nil 145 } 146 147 func (p *provisioner) Close() error { 148 return nil 149 } 150 151 // generateScripts takes the configuration and creates a script from each inline config 152 func generateScripts(inline cty.Value) ([]string, error) { 153 var lines []string 154 for _, l := range inline.AsValueSlice() { 155 if l.IsNull() { 156 return nil, errors.New("invalid null string in 'scripts'") 157 } 158 159 s := l.AsString() 160 if s == "" { 161 return nil, errors.New("invalid empty string in 'scripts'") 162 } 163 lines = append(lines, s) 164 } 165 lines = append(lines, "") 166 167 return []string{strings.Join(lines, "\n")}, nil 168 } 169 170 // collectScripts is used to collect all the scripts we need 171 // to execute in preparation for copying them. 172 func collectScripts(v cty.Value) ([]io.ReadCloser, error) { 173 // Check if inline 174 if inline := v.GetAttr("inline"); !inline.IsNull() { 175 scripts, err := generateScripts(inline) 176 if err != nil { 177 return nil, err 178 } 179 180 var r []io.ReadCloser 181 for _, script := range scripts { 182 r = append(r, ioutil.NopCloser(bytes.NewReader([]byte(script)))) 183 } 184 185 return r, nil 186 } 187 188 // Collect scripts 189 var scripts []string 190 if script := v.GetAttr("script"); !script.IsNull() { 191 s := script.AsString() 192 if s == "" { 193 return nil, errors.New("invalid empty string in 'script'") 194 } 195 scripts = append(scripts, s) 196 } 197 198 if scriptList := v.GetAttr("scripts"); !scriptList.IsNull() { 199 for _, script := range scriptList.AsValueSlice() { 200 if script.IsNull() { 201 return nil, errors.New("invalid null string in 'script'") 202 } 203 s := script.AsString() 204 if s == "" { 205 return nil, errors.New("invalid empty string in 'script'") 206 } 207 scripts = append(scripts, s) 208 } 209 } 210 211 // Open all the scripts 212 var fhs []io.ReadCloser 213 for _, s := range scripts { 214 fh, err := os.Open(s) 215 if err != nil { 216 for _, fh := range fhs { 217 fh.Close() 218 } 219 return nil, fmt.Errorf("Failed to open script '%s': %v", s, err) 220 } 221 fhs = append(fhs, fh) 222 } 223 224 // Done, return the file handles 225 return fhs, nil 226 } 227 228 // runScripts is used to copy and execute a set of scripts 229 func runScripts(ctx context.Context, o provisioners.UIOutput, comm communicator.Communicator, scripts []io.ReadCloser) error { 230 retryCtx, cancel := context.WithTimeout(ctx, comm.Timeout()) 231 defer cancel() 232 233 // Wait and retry until we establish the connection 234 err := communicator.Retry(retryCtx, func() error { 235 return comm.Connect(o) 236 }) 237 if err != nil { 238 return err 239 } 240 241 // Wait for the context to end and then disconnect 242 go func() { 243 <-ctx.Done() 244 comm.Disconnect() 245 }() 246 247 for _, script := range scripts { 248 var cmd *remote.Cmd 249 250 outR, outW := io.Pipe() 251 errR, errW := io.Pipe() 252 defer outW.Close() 253 defer errW.Close() 254 255 go copyUIOutput(o, outR) 256 go copyUIOutput(o, errR) 257 258 remotePath := comm.ScriptPath() 259 260 if err := comm.UploadScript(remotePath, script); err != nil { 261 return fmt.Errorf("Failed to upload script: %v", err) 262 } 263 264 cmd = &remote.Cmd{ 265 Command: remotePath, 266 Stdout: outW, 267 Stderr: errW, 268 } 269 if err := comm.Start(cmd); err != nil { 270 return fmt.Errorf("Error starting script: %v", err) 271 } 272 273 if err := cmd.Wait(); err != nil { 274 return err 275 } 276 277 // Upload a blank follow up file in the same path to prevent residual 278 // script contents from remaining on remote machine 279 empty := bytes.NewReader([]byte("")) 280 if err := comm.Upload(remotePath, empty); err != nil { 281 // This feature is best-effort. 282 log.Printf("[WARN] Failed to upload empty follow up script: %v", err) 283 } 284 } 285 286 return nil 287 } 288 289 func copyUIOutput(o provisioners.UIOutput, r io.Reader) { 290 lr := linereader.New(r) 291 for line := range lr.Ch { 292 o.Output(line) 293 } 294 }