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