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