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