github.com/hooklift/terraform@v0.11.0-beta1.0.20171117000744-6786c1361ffe/builtin/provisioners/remote-exec/resource_provisioner.go (about) 1 package remoteexec 2 3 import ( 4 "bytes" 5 "context" 6 "fmt" 7 "io" 8 "io/ioutil" 9 "log" 10 "os" 11 "strings" 12 "sync/atomic" 13 "time" 14 15 "github.com/hashicorp/terraform/communicator" 16 "github.com/hashicorp/terraform/communicator/remote" 17 "github.com/hashicorp/terraform/helper/schema" 18 "github.com/hashicorp/terraform/terraform" 19 "github.com/mitchellh/go-linereader" 20 ) 21 22 // maxBackoffDealy is the maximum delay between retry attempts 23 var maxBackoffDelay = 10 * time.Second 24 var initialBackoffDelay = time.Second 25 26 func Provisioner() terraform.ResourceProvisioner { 27 return &schema.Provisioner{ 28 Schema: map[string]*schema.Schema{ 29 "inline": { 30 Type: schema.TypeList, 31 Elem: &schema.Schema{Type: schema.TypeString}, 32 PromoteSingle: true, 33 Optional: true, 34 ConflictsWith: []string{"script", "scripts"}, 35 }, 36 37 "script": { 38 Type: schema.TypeString, 39 Optional: true, 40 ConflictsWith: []string{"inline", "scripts"}, 41 }, 42 43 "scripts": { 44 Type: schema.TypeList, 45 Elem: &schema.Schema{Type: schema.TypeString}, 46 Optional: true, 47 ConflictsWith: []string{"script", "inline"}, 48 }, 49 }, 50 51 ApplyFunc: applyFn, 52 } 53 } 54 55 // Apply executes the remote exec provisioner 56 func applyFn(ctx context.Context) error { 57 connState := ctx.Value(schema.ProvRawStateKey).(*terraform.InstanceState) 58 data := ctx.Value(schema.ProvConfigDataKey).(*schema.ResourceData) 59 o := ctx.Value(schema.ProvOutputKey).(terraform.UIOutput) 60 61 // Get a new communicator 62 comm, err := communicator.New(connState) 63 if err != nil { 64 return err 65 } 66 67 // Collect the scripts 68 scripts, err := collectScripts(data) 69 if err != nil { 70 return err 71 } 72 for _, s := range scripts { 73 defer s.Close() 74 } 75 76 // Copy and execute each script 77 if err := runScripts(ctx, o, comm, scripts); err != nil { 78 return err 79 } 80 81 return nil 82 } 83 84 // generateScripts takes the configuration and creates a script from each inline config 85 func generateScripts(d *schema.ResourceData) ([]string, error) { 86 var lines []string 87 for _, l := range d.Get("inline").([]interface{}) { 88 line, ok := l.(string) 89 if !ok { 90 return nil, fmt.Errorf("Error parsing %v as a string", l) 91 } 92 lines = append(lines, line) 93 } 94 lines = append(lines, "") 95 96 return []string{strings.Join(lines, "\n")}, nil 97 } 98 99 // collectScripts is used to collect all the scripts we need 100 // to execute in preparation for copying them. 101 func collectScripts(d *schema.ResourceData) ([]io.ReadCloser, error) { 102 // Check if inline 103 if _, ok := d.GetOk("inline"); ok { 104 scripts, err := generateScripts(d) 105 if err != nil { 106 return nil, err 107 } 108 109 var r []io.ReadCloser 110 for _, script := range scripts { 111 r = append(r, ioutil.NopCloser(bytes.NewReader([]byte(script)))) 112 } 113 114 return r, nil 115 } 116 117 // Collect scripts 118 var scripts []string 119 if script, ok := d.GetOk("script"); ok { 120 scr, ok := script.(string) 121 if !ok { 122 return nil, fmt.Errorf("Error parsing script %v as string", script) 123 } 124 scripts = append(scripts, scr) 125 } 126 127 if scriptList, ok := d.GetOk("scripts"); ok { 128 for _, script := range scriptList.([]interface{}) { 129 scr, ok := script.(string) 130 if !ok { 131 return nil, fmt.Errorf("Error parsing script %v as string", script) 132 } 133 scripts = append(scripts, scr) 134 } 135 } 136 137 // Open all the scripts 138 var fhs []io.ReadCloser 139 for _, s := range scripts { 140 fh, err := os.Open(s) 141 if err != nil { 142 for _, fh := range fhs { 143 fh.Close() 144 } 145 return nil, fmt.Errorf("Failed to open script '%s': %v", s, err) 146 } 147 fhs = append(fhs, fh) 148 } 149 150 // Done, return the file handles 151 return fhs, nil 152 } 153 154 // runScripts is used to copy and execute a set of scripts 155 func runScripts( 156 ctx context.Context, 157 o terraform.UIOutput, 158 comm communicator.Communicator, 159 scripts []io.ReadCloser) error { 160 // Wrap out context in a cancelation function that we use to 161 // kill the connection. 162 ctx, cancelFunc := context.WithCancel(ctx) 163 defer cancelFunc() 164 165 // Wait for the context to end and then disconnect 166 go func() { 167 <-ctx.Done() 168 comm.Disconnect() 169 }() 170 171 // Wait and retry until we establish the connection 172 err := retryFunc(ctx, comm.Timeout(), func() error { 173 err := comm.Connect(o) 174 return err 175 }) 176 if err != nil { 177 return err 178 } 179 180 for _, script := range scripts { 181 var cmd *remote.Cmd 182 outR, outW := io.Pipe() 183 errR, errW := io.Pipe() 184 outDoneCh := make(chan struct{}) 185 errDoneCh := make(chan struct{}) 186 go copyOutput(o, outR, outDoneCh) 187 go copyOutput(o, errR, errDoneCh) 188 189 remotePath := comm.ScriptPath() 190 err = retryFunc(ctx, comm.Timeout(), func() error { 191 if err := comm.UploadScript(remotePath, script); err != nil { 192 return fmt.Errorf("Failed to upload script: %v", err) 193 } 194 195 cmd = &remote.Cmd{ 196 Command: remotePath, 197 Stdout: outW, 198 Stderr: errW, 199 } 200 if err := comm.Start(cmd); err != nil { 201 return fmt.Errorf("Error starting script: %v", err) 202 } 203 204 return nil 205 }) 206 if err == nil { 207 cmd.Wait() 208 if cmd.ExitStatus != 0 { 209 err = fmt.Errorf("Script exited with non-zero exit status: %d", cmd.ExitStatus) 210 } 211 } 212 213 // If we have an error, end our context so the disconnect happens. 214 // This has to happen before the output cleanup below since during 215 // an interrupt this will cause the outputs to end. 216 if err != nil { 217 cancelFunc() 218 } 219 220 // Wait for output to clean up 221 outW.Close() 222 errW.Close() 223 <-outDoneCh 224 <-errDoneCh 225 226 // Upload a blank follow up file in the same path to prevent residual 227 // script contents from remaining on remote machine 228 empty := bytes.NewReader([]byte("")) 229 if err := comm.Upload(remotePath, empty); err != nil { 230 // This feature is best-effort. 231 log.Printf("[WARN] Failed to upload empty follow up script: %v", err) 232 } 233 234 // If we have an error, return it out now that we've cleaned up 235 if err != nil { 236 return err 237 } 238 } 239 240 return nil 241 } 242 243 func copyOutput( 244 o terraform.UIOutput, r io.Reader, doneCh chan<- struct{}) { 245 defer close(doneCh) 246 lr := linereader.New(r) 247 for line := range lr.Ch { 248 o.Output(line) 249 } 250 } 251 252 // retryFunc is used to retry a function for a given duration 253 func retryFunc(ctx context.Context, timeout time.Duration, f func() error) error { 254 // Build a new context with the timeout 255 ctx, done := context.WithTimeout(ctx, timeout) 256 defer done() 257 258 // container for atomic error value 259 type errWrap struct { 260 E error 261 } 262 263 // Try the function in a goroutine 264 var errVal atomic.Value 265 doneCh := make(chan struct{}) 266 go func() { 267 defer close(doneCh) 268 269 delay := time.Duration(0) 270 for { 271 // If our context ended, we want to exit right away. 272 select { 273 case <-ctx.Done(): 274 return 275 case <-time.After(delay): 276 } 277 278 // Try the function call 279 err := f() 280 errVal.Store(&errWrap{err}) 281 282 if err == nil { 283 return 284 } 285 286 log.Printf("[WARN] retryable error: %v", err) 287 288 delay *= 2 289 290 if delay == 0 { 291 delay = initialBackoffDelay 292 } 293 294 if delay > maxBackoffDelay { 295 delay = maxBackoffDelay 296 } 297 298 log.Printf("[INFO] sleeping for %s", delay) 299 } 300 }() 301 302 // Wait for completion 303 select { 304 case <-ctx.Done(): 305 case <-doneCh: 306 } 307 308 // Check if we have a context error to check if we're interrupted or timeout 309 switch ctx.Err() { 310 case context.Canceled: 311 return fmt.Errorf("interrupted") 312 case context.DeadlineExceeded: 313 return fmt.Errorf("timeout") 314 } 315 316 // Check if we got an error executing 317 if ev, ok := errVal.Load().(errWrap); ok { 318 return ev.E 319 } 320 321 return nil 322 }