github.com/pbthorste/terraform@v0.8.6-0.20170127005045-deb56bd93da2/builtin/provisioners/remote-exec/resource_provisioner.go (about) 1 package remoteexec 2 3 import ( 4 "bytes" 5 "fmt" 6 "io" 7 "io/ioutil" 8 "log" 9 "os" 10 "time" 11 12 "github.com/hashicorp/terraform/communicator" 13 "github.com/hashicorp/terraform/communicator/remote" 14 "github.com/hashicorp/terraform/terraform" 15 "github.com/mitchellh/go-linereader" 16 ) 17 18 // ResourceProvisioner represents a remote exec provisioner 19 type ResourceProvisioner struct{} 20 21 // Apply executes the remote exec provisioner 22 func (p *ResourceProvisioner) Apply( 23 o terraform.UIOutput, 24 s *terraform.InstanceState, 25 c *terraform.ResourceConfig) error { 26 // Get a new communicator 27 comm, err := communicator.New(s) 28 if err != nil { 29 return err 30 } 31 32 // Collect the scripts 33 scripts, err := p.collectScripts(c) 34 if err != nil { 35 return err 36 } 37 for _, s := range scripts { 38 defer s.Close() 39 } 40 41 // Copy and execute each script 42 if err := p.runScripts(o, comm, scripts); err != nil { 43 return err 44 } 45 return nil 46 } 47 48 // Validate checks if the required arguments are configured 49 func (p *ResourceProvisioner) Validate(c *terraform.ResourceConfig) (ws []string, es []error) { 50 num := 0 51 for name := range c.Raw { 52 switch name { 53 case "scripts", "script", "inline": 54 num++ 55 default: 56 es = append(es, fmt.Errorf("Unknown configuration '%s'", name)) 57 } 58 } 59 if num != 1 { 60 es = append(es, fmt.Errorf("Must provide one of 'scripts', 'script' or 'inline' to remote-exec")) 61 } 62 return 63 } 64 65 // generateScripts takes the configuration and creates a script from each inline config 66 func (p *ResourceProvisioner) generateScripts(c *terraform.ResourceConfig) ([]string, error) { 67 var scripts []string 68 command, ok := c.Config["inline"] 69 if ok { 70 switch cmd := command.(type) { 71 case string: 72 scripts = append(scripts, cmd) 73 case []string: 74 scripts = append(scripts, cmd...) 75 case []interface{}: 76 for _, l := range cmd { 77 lStr, ok := l.(string) 78 if ok { 79 scripts = append(scripts, lStr) 80 } else { 81 return nil, fmt.Errorf("Unsupported 'inline' type! Must be string, or list of strings.") 82 } 83 } 84 default: 85 return nil, fmt.Errorf("Unsupported 'inline' type! Must be string, or list of strings.") 86 } 87 } 88 return scripts, nil 89 } 90 91 // collectScripts is used to collect all the scripts we need 92 // to execute in preparation for copying them. 93 func (p *ResourceProvisioner) collectScripts(c *terraform.ResourceConfig) ([]io.ReadCloser, error) { 94 // Check if inline 95 _, ok := c.Config["inline"] 96 if ok { 97 scripts, err := p.generateScripts(c) 98 if err != nil { 99 return nil, err 100 } 101 102 r := []io.ReadCloser{} 103 for _, script := range scripts { 104 r = append(r, ioutil.NopCloser(bytes.NewReader([]byte(script)))) 105 } 106 107 return r, nil 108 } 109 110 // Collect scripts 111 var scripts []string 112 s, ok := c.Config["script"] 113 if ok { 114 sStr, ok := s.(string) 115 if !ok { 116 return nil, fmt.Errorf("Unsupported 'script' type! Must be a string.") 117 } 118 scripts = append(scripts, sStr) 119 } 120 121 sl, ok := c.Config["scripts"] 122 if ok { 123 switch slt := sl.(type) { 124 case []string: 125 scripts = append(scripts, slt...) 126 case []interface{}: 127 for _, l := range slt { 128 lStr, ok := l.(string) 129 if ok { 130 scripts = append(scripts, lStr) 131 } else { 132 return nil, fmt.Errorf("Unsupported 'scripts' type! Must be list of strings.") 133 } 134 } 135 default: 136 return nil, fmt.Errorf("Unsupported 'scripts' type! Must be list of strings.") 137 } 138 } 139 140 // Open all the scripts 141 var fhs []io.ReadCloser 142 for _, s := range scripts { 143 fh, err := os.Open(s) 144 if err != nil { 145 for _, fh := range fhs { 146 fh.Close() 147 } 148 return nil, fmt.Errorf("Failed to open script '%s': %v", s, err) 149 } 150 fhs = append(fhs, fh) 151 } 152 153 // Done, return the file handles 154 return fhs, nil 155 } 156 157 // runScripts is used to copy and execute a set of scripts 158 func (p *ResourceProvisioner) runScripts( 159 o terraform.UIOutput, 160 comm communicator.Communicator, 161 scripts []io.ReadCloser) error { 162 // Wait and retry until we establish the connection 163 err := retryFunc(comm.Timeout(), func() error { 164 err := comm.Connect(o) 165 return err 166 }) 167 if err != nil { 168 return err 169 } 170 defer comm.Disconnect() 171 172 for _, script := range scripts { 173 var cmd *remote.Cmd 174 outR, outW := io.Pipe() 175 errR, errW := io.Pipe() 176 outDoneCh := make(chan struct{}) 177 errDoneCh := make(chan struct{}) 178 go p.copyOutput(o, outR, outDoneCh) 179 go p.copyOutput(o, errR, errDoneCh) 180 181 remotePath := comm.ScriptPath() 182 err = retryFunc(comm.Timeout(), func() error { 183 if err := comm.UploadScript(remotePath, script); err != nil { 184 return fmt.Errorf("Failed to upload script: %v", err) 185 } 186 187 cmd = &remote.Cmd{ 188 Command: remotePath, 189 Stdout: outW, 190 Stderr: errW, 191 } 192 if err := comm.Start(cmd); err != nil { 193 return fmt.Errorf("Error starting script: %v", err) 194 } 195 196 return nil 197 }) 198 if err == nil { 199 cmd.Wait() 200 if cmd.ExitStatus != 0 { 201 err = fmt.Errorf("Script exited with non-zero exit status: %d", cmd.ExitStatus) 202 } 203 } 204 205 // Wait for output to clean up 206 outW.Close() 207 errW.Close() 208 <-outDoneCh 209 <-errDoneCh 210 211 // Upload a blank follow up file in the same path to prevent residual 212 // script contents from remaining on remote machine 213 empty := bytes.NewReader([]byte("")) 214 if err := comm.Upload(remotePath, empty); err != nil { 215 // This feature is best-effort. 216 log.Printf("[WARN] Failed to upload empty follow up script: %v", err) 217 } 218 219 // If we have an error, return it out now that we've cleaned up 220 if err != nil { 221 return err 222 } 223 } 224 225 return nil 226 } 227 228 func (p *ResourceProvisioner) copyOutput( 229 o terraform.UIOutput, r io.Reader, doneCh chan<- struct{}) { 230 defer close(doneCh) 231 lr := linereader.New(r) 232 for line := range lr.Ch { 233 o.Output(line) 234 } 235 } 236 237 // retryFunc is used to retry a function for a given duration 238 func retryFunc(timeout time.Duration, f func() error) error { 239 finish := time.After(timeout) 240 for { 241 err := f() 242 if err == nil { 243 return nil 244 } 245 log.Printf("Retryable error: %v", err) 246 247 select { 248 case <-finish: 249 return err 250 case <-time.After(3 * time.Second): 251 } 252 } 253 }