github.com/handlerbot/terraform@v0.10.0-beta1.0.20180726153736-26b68d98f9cb/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 "time" 13 14 "github.com/hashicorp/terraform/communicator" 15 "github.com/hashicorp/terraform/communicator/remote" 16 "github.com/hashicorp/terraform/helper/schema" 17 "github.com/hashicorp/terraform/terraform" 18 "github.com/mitchellh/go-linereader" 19 ) 20 21 // maxBackoffDealy is the maximum delay between retry attempts 22 var maxBackoffDelay = 10 * time.Second 23 var initialBackoffDelay = time.Second 24 25 func Provisioner() terraform.ResourceProvisioner { 26 return &schema.Provisioner{ 27 Schema: map[string]*schema.Schema{ 28 "inline": { 29 Type: schema.TypeList, 30 Elem: &schema.Schema{Type: schema.TypeString}, 31 PromoteSingle: true, 32 Optional: true, 33 ConflictsWith: []string{"script", "scripts"}, 34 }, 35 36 "script": { 37 Type: schema.TypeString, 38 Optional: true, 39 ConflictsWith: []string{"inline", "scripts"}, 40 }, 41 42 "scripts": { 43 Type: schema.TypeList, 44 Elem: &schema.Schema{Type: schema.TypeString}, 45 Optional: true, 46 ConflictsWith: []string{"script", "inline"}, 47 }, 48 }, 49 50 ApplyFunc: applyFn, 51 } 52 } 53 54 // Apply executes the remote exec provisioner 55 func applyFn(ctx context.Context) error { 56 connState := ctx.Value(schema.ProvRawStateKey).(*terraform.InstanceState) 57 data := ctx.Value(schema.ProvConfigDataKey).(*schema.ResourceData) 58 o := ctx.Value(schema.ProvOutputKey).(terraform.UIOutput) 59 60 // Get a new communicator 61 comm, err := communicator.New(connState) 62 if err != nil { 63 return err 64 } 65 66 // Collect the scripts 67 scripts, err := collectScripts(data) 68 if err != nil { 69 return err 70 } 71 for _, s := range scripts { 72 defer s.Close() 73 } 74 75 // Copy and execute each script 76 if err := runScripts(ctx, o, comm, scripts); err != nil { 77 return err 78 } 79 80 return nil 81 } 82 83 // generateScripts takes the configuration and creates a script from each inline config 84 func generateScripts(d *schema.ResourceData) ([]string, error) { 85 var lines []string 86 for _, l := range d.Get("inline").([]interface{}) { 87 line, ok := l.(string) 88 if !ok { 89 return nil, fmt.Errorf("Error parsing %v as a string", l) 90 } 91 lines = append(lines, line) 92 } 93 lines = append(lines, "") 94 95 return []string{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 collectScripts(d *schema.ResourceData) ([]io.ReadCloser, error) { 101 // Check if inline 102 if _, ok := d.GetOk("inline"); ok { 103 scripts, err := generateScripts(d) 104 if err != nil { 105 return nil, err 106 } 107 108 var r []io.ReadCloser 109 for _, script := range scripts { 110 r = append(r, ioutil.NopCloser(bytes.NewReader([]byte(script)))) 111 } 112 113 return r, nil 114 } 115 116 // Collect scripts 117 var scripts []string 118 if script, ok := d.GetOk("script"); ok { 119 scr, ok := script.(string) 120 if !ok { 121 return nil, fmt.Errorf("Error parsing script %v as string", script) 122 } 123 scripts = append(scripts, scr) 124 } 125 126 if scriptList, ok := d.GetOk("scripts"); ok { 127 for _, script := range scriptList.([]interface{}) { 128 scr, ok := script.(string) 129 if !ok { 130 return nil, fmt.Errorf("Error parsing script %v as string", script) 131 } 132 scripts = append(scripts, scr) 133 } 134 } 135 136 // Open all the scripts 137 var fhs []io.ReadCloser 138 for _, s := range scripts { 139 fh, err := os.Open(s) 140 if err != nil { 141 for _, fh := range fhs { 142 fh.Close() 143 } 144 return nil, fmt.Errorf("Failed to open script '%s': %v", s, err) 145 } 146 fhs = append(fhs, fh) 147 } 148 149 // Done, return the file handles 150 return fhs, nil 151 } 152 153 // runScripts is used to copy and execute a set of scripts 154 func runScripts( 155 ctx context.Context, 156 o terraform.UIOutput, 157 comm communicator.Communicator, 158 scripts []io.ReadCloser) error { 159 160 retryCtx, cancel := context.WithTimeout(ctx, comm.Timeout()) 161 defer cancel() 162 163 // Wait and retry until we establish the connection 164 err := communicator.Retry(retryCtx, func() error { 165 return comm.Connect(o) 166 }) 167 if err != nil { 168 return err 169 } 170 171 // Wait for the context to end and then disconnect 172 go func() { 173 <-ctx.Done() 174 comm.Disconnect() 175 }() 176 177 for _, script := range scripts { 178 var cmd *remote.Cmd 179 180 outR, outW := io.Pipe() 181 errR, errW := io.Pipe() 182 defer outW.Close() 183 defer errW.Close() 184 185 go copyOutput(o, outR) 186 go copyOutput(o, errR) 187 188 remotePath := comm.ScriptPath() 189 190 if err := comm.UploadScript(remotePath, script); err != nil { 191 return fmt.Errorf("Failed to upload script: %v", err) 192 } 193 194 cmd = &remote.Cmd{ 195 Command: remotePath, 196 Stdout: outW, 197 Stderr: errW, 198 } 199 if err := comm.Start(cmd); err != nil { 200 return fmt.Errorf("Error starting script: %v", err) 201 } 202 203 if err := cmd.Wait(); err != nil { 204 return err 205 } 206 207 // Upload a blank follow up file in the same path to prevent residual 208 // script contents from remaining on remote machine 209 empty := bytes.NewReader([]byte("")) 210 if err := comm.Upload(remotePath, empty); err != nil { 211 // This feature is best-effort. 212 log.Printf("[WARN] Failed to upload empty follow up script: %v", err) 213 } 214 } 215 216 return nil 217 } 218 219 func copyOutput( 220 o terraform.UIOutput, r io.Reader) { 221 lr := linereader.New(r) 222 for line := range lr.Ch { 223 o.Output(line) 224 } 225 }