github.com/minamijoyo/terraform@v0.7.8-0.20161029001309-18b3736ba44b/builtin/provisioners/chef/resource_provisioner.go (about) 1 package chef 2 3 import ( 4 "bytes" 5 "encoding/json" 6 "errors" 7 "fmt" 8 "io" 9 "log" 10 "os" 11 "path" 12 "regexp" 13 "strings" 14 "sync" 15 "text/template" 16 "time" 17 18 "github.com/hashicorp/terraform/communicator" 19 "github.com/hashicorp/terraform/communicator/remote" 20 "github.com/hashicorp/terraform/terraform" 21 "github.com/mitchellh/go-homedir" 22 "github.com/mitchellh/go-linereader" 23 "github.com/mitchellh/mapstructure" 24 ) 25 26 const ( 27 clienrb = "client.rb" 28 defaultEnv = "_default" 29 firstBoot = "first-boot.json" 30 logfileDir = "logfiles" 31 linuxChefCmd = "chef-client" 32 linuxConfDir = "/etc/chef" 33 linuxNoOutput = "> /dev/null 2>&1" 34 linuxGemCmd = "/opt/chef/embedded/bin/gem" 35 linuxKnifeCmd = "knife" 36 secretKey = "encrypted_data_bag_secret" 37 windowsChefCmd = "cmd /c chef-client" 38 windowsConfDir = "C:/chef" 39 windowsNoOutput = "> nul 2>&1" 40 windowsGemCmd = "C:/opscode/chef/embedded/bin/gem" 41 windowsKnifeCmd = "cmd /c knife" 42 ) 43 44 const clientConf = ` 45 log_location STDOUT 46 chef_server_url "{{ .ServerURL }}" 47 node_name "{{ .NodeName }}" 48 {{ if .UsePolicyfile }} 49 use_policyfile true 50 policy_group "{{ .PolicyGroup }}" 51 policy_name "{{ .PolicyName }}" 52 {{ end -}} 53 54 {{ if .HTTPProxy }} 55 http_proxy "{{ .HTTPProxy }}" 56 ENV['http_proxy'] = "{{ .HTTPProxy }}" 57 ENV['HTTP_PROXY'] = "{{ .HTTPProxy }}" 58 {{ end -}} 59 60 {{ if .HTTPSProxy }} 61 https_proxy "{{ .HTTPSProxy }}" 62 ENV['https_proxy'] = "{{ .HTTPSProxy }}" 63 ENV['HTTPS_PROXY'] = "{{ .HTTPSProxy }}" 64 {{ end -}} 65 66 {{ if .NOProxy }} 67 no_proxy "{{ join .NOProxy "," }}" 68 ENV['no_proxy'] = "{{ join .NOProxy "," }}" 69 {{ end -}} 70 71 {{ if .SSLVerifyMode }} 72 ssl_verify_mode {{ .SSLVerifyMode }} 73 {{- end -}} 74 75 {{ if .DisableReporting }} 76 enable_reporting false 77 {{ end -}} 78 79 {{ if .ClientOptions }} 80 {{ join .ClientOptions "\n" }} 81 {{ end }} 82 ` 83 84 // Provisioner represents a Chef provisioner 85 type Provisioner struct { 86 AttributesJSON string `mapstructure:"attributes_json"` 87 ClientOptions []string `mapstructure:"client_options"` 88 DisableReporting bool `mapstructure:"disable_reporting"` 89 Environment string `mapstructure:"environment"` 90 FetchChefCertificates bool `mapstructure:"fetch_chef_certificates"` 91 LogToFile bool `mapstructure:"log_to_file"` 92 UsePolicyfile bool `mapstructure:"use_policyfile"` 93 PolicyGroup string `mapstructure:"policy_group"` 94 PolicyName string `mapstructure:"policy_name"` 95 HTTPProxy string `mapstructure:"http_proxy"` 96 HTTPSProxy string `mapstructure:"https_proxy"` 97 NOProxy []string `mapstructure:"no_proxy"` 98 NodeName string `mapstructure:"node_name"` 99 OhaiHints []string `mapstructure:"ohai_hints"` 100 OSType string `mapstructure:"os_type"` 101 RecreateClient bool `mapstructure:"recreate_client"` 102 PreventSudo bool `mapstructure:"prevent_sudo"` 103 RunList []string `mapstructure:"run_list"` 104 SecretKey string `mapstructure:"secret_key"` 105 ServerURL string `mapstructure:"server_url"` 106 SkipInstall bool `mapstructure:"skip_install"` 107 SkipRegister bool `mapstructure:"skip_register"` 108 SSLVerifyMode string `mapstructure:"ssl_verify_mode"` 109 UserName string `mapstructure:"user_name"` 110 UserKey string `mapstructure:"user_key"` 111 VaultJSON string `mapstructure:"vault_json"` 112 Version string `mapstructure:"version"` 113 114 attributes map[string]interface{} 115 vaults map[string][]string 116 117 cleanupUserKeyCmd string 118 createConfigFiles func(terraform.UIOutput, communicator.Communicator) error 119 installChefClient func(terraform.UIOutput, communicator.Communicator) error 120 fetchChefCertificates func(terraform.UIOutput, communicator.Communicator) error 121 generateClientKey func(terraform.UIOutput, communicator.Communicator) error 122 configureVaults func(terraform.UIOutput, communicator.Communicator) error 123 runChefClient func(terraform.UIOutput, communicator.Communicator) error 124 useSudo bool 125 126 // Deprecated Fields 127 ValidationClientName string `mapstructure:"validation_client_name"` 128 ValidationKey string `mapstructure:"validation_key"` 129 } 130 131 // ResourceProvisioner represents a generic chef provisioner 132 type ResourceProvisioner struct{} 133 134 // Apply executes the file provisioner 135 func (r *ResourceProvisioner) Apply( 136 o terraform.UIOutput, 137 s *terraform.InstanceState, 138 c *terraform.ResourceConfig) error { 139 // Decode the raw config for this provisioner 140 p, err := r.decodeConfig(c) 141 if err != nil { 142 return err 143 } 144 145 if p.OSType == "" { 146 switch s.Ephemeral.ConnInfo["type"] { 147 case "ssh", "": // The default connection type is ssh, so if the type is empty assume ssh 148 p.OSType = "linux" 149 case "winrm": 150 p.OSType = "windows" 151 default: 152 return fmt.Errorf("Unsupported connection type: %s", s.Ephemeral.ConnInfo["type"]) 153 } 154 } 155 156 // Set some values based on the targeted OS 157 switch p.OSType { 158 case "linux": 159 p.cleanupUserKeyCmd = fmt.Sprintf("rm -f %s", path.Join(linuxConfDir, p.UserName+".pem")) 160 p.createConfigFiles = p.linuxCreateConfigFiles 161 p.installChefClient = p.linuxInstallChefClient 162 p.fetchChefCertificates = p.fetchChefCertificatesFunc(linuxKnifeCmd, linuxConfDir) 163 p.generateClientKey = p.generateClientKeyFunc(linuxKnifeCmd, linuxConfDir, linuxNoOutput) 164 p.configureVaults = p.configureVaultsFunc(linuxGemCmd, linuxKnifeCmd, linuxConfDir) 165 p.runChefClient = p.runChefClientFunc(linuxChefCmd, linuxConfDir) 166 p.useSudo = !p.PreventSudo && s.Ephemeral.ConnInfo["user"] != "root" 167 case "windows": 168 p.cleanupUserKeyCmd = fmt.Sprintf("cd %s && del /F /Q %s", windowsConfDir, p.UserName+".pem") 169 p.createConfigFiles = p.windowsCreateConfigFiles 170 p.installChefClient = p.windowsInstallChefClient 171 p.fetchChefCertificates = p.fetchChefCertificatesFunc(windowsKnifeCmd, windowsConfDir) 172 p.generateClientKey = p.generateClientKeyFunc(windowsKnifeCmd, windowsConfDir, windowsNoOutput) 173 p.configureVaults = p.configureVaultsFunc(windowsGemCmd, windowsKnifeCmd, windowsConfDir) 174 p.runChefClient = p.runChefClientFunc(windowsChefCmd, windowsConfDir) 175 p.useSudo = false 176 default: 177 return fmt.Errorf("Unsupported os type: %s", p.OSType) 178 } 179 180 // Get a new communicator 181 comm, err := communicator.New(s) 182 if err != nil { 183 return err 184 } 185 186 // Wait and retry until we establish the connection 187 err = retryFunc(comm.Timeout(), func() error { 188 err := comm.Connect(o) 189 return err 190 }) 191 if err != nil { 192 return err 193 } 194 defer comm.Disconnect() 195 196 // Make sure we always delete the user key from the new node! 197 var once sync.Once 198 cleanupUserKey := func() { 199 o.Output("Cleanup user key...") 200 if err := p.runCommand(o, comm, p.cleanupUserKeyCmd); err != nil { 201 o.Output("WARNING: Failed to cleanup user key on new node: " + err.Error()) 202 } 203 } 204 defer once.Do(cleanupUserKey) 205 206 if !p.SkipInstall { 207 if err := p.installChefClient(o, comm); err != nil { 208 return err 209 } 210 } 211 212 o.Output("Creating configuration files...") 213 if err := p.createConfigFiles(o, comm); err != nil { 214 return err 215 } 216 217 if !p.SkipRegister { 218 if p.FetchChefCertificates { 219 o.Output("Fetch Chef certificates...") 220 if err := p.fetchChefCertificates(o, comm); err != nil { 221 return err 222 } 223 } 224 225 o.Output("Generate the private key...") 226 if err := p.generateClientKey(o, comm); err != nil { 227 return err 228 } 229 } 230 231 if p.VaultJSON != "" { 232 o.Output("Configure Chef vaults...") 233 if err := p.configureVaults(o, comm); err != nil { 234 return err 235 } 236 } 237 238 // Cleanup the user key before we run Chef-Client to prevent issues 239 // with rights caused by changing settings during the run. 240 once.Do(cleanupUserKey) 241 242 o.Output("Starting initial Chef-Client run...") 243 if err := p.runChefClient(o, comm); err != nil { 244 return err 245 } 246 247 return nil 248 } 249 250 // Validate checks if the required arguments are configured 251 func (r *ResourceProvisioner) Validate(c *terraform.ResourceConfig) (ws []string, es []error) { 252 p, err := r.decodeConfig(c) 253 if err != nil { 254 es = append(es, err) 255 return ws, es 256 } 257 258 if p.NodeName == "" { 259 es = append(es, errors.New("Key not found: node_name")) 260 } 261 if !p.UsePolicyfile && p.RunList == nil { 262 es = append(es, errors.New("Key not found: run_list")) 263 } 264 if p.ServerURL == "" { 265 es = append(es, errors.New("Key not found: server_url")) 266 } 267 if p.UsePolicyfile && p.PolicyName == "" { 268 es = append(es, errors.New("Policyfile enabled but key not found: policy_name")) 269 } 270 if p.UsePolicyfile && p.PolicyGroup == "" { 271 es = append(es, errors.New("Policyfile enabled but key not found: policy_group")) 272 } 273 if p.UserName == "" && p.ValidationClientName == "" { 274 es = append(es, errors.New( 275 "One of user_name or the deprecated validation_client_name must be provided")) 276 } 277 if p.UserKey == "" && p.ValidationKey == "" { 278 es = append(es, errors.New( 279 "One of user_key or the deprecated validation_key must be provided")) 280 } 281 if p.ValidationClientName != "" { 282 ws = append(ws, "validation_client_name is deprecated, please use user_name instead") 283 } 284 if p.ValidationKey != "" { 285 ws = append(ws, "validation_key is deprecated, please use user_key instead") 286 287 if p.RecreateClient { 288 es = append(es, errors.New( 289 "Cannot use recreate_client=true with the deprecated validation_key, please provide a user_key")) 290 } 291 if p.VaultJSON != "" { 292 es = append(es, errors.New( 293 "Cannot configure chef vaults using the deprecated validation_key, please provide a user_key")) 294 } 295 } 296 297 return ws, es 298 } 299 300 func (r *ResourceProvisioner) decodeConfig(c *terraform.ResourceConfig) (*Provisioner, error) { 301 p := new(Provisioner) 302 303 decConf := &mapstructure.DecoderConfig{ 304 ErrorUnused: true, 305 WeaklyTypedInput: true, 306 Result: p, 307 } 308 dec, err := mapstructure.NewDecoder(decConf) 309 if err != nil { 310 return nil, err 311 } 312 313 // We need to merge both configs into a single map first. Order is 314 // important as we need to make sure interpolated values are used 315 // over raw values. This makes sure that all values are there even 316 // if some still need to be interpolated later on. Without this 317 // the validation will fail when using a variable for a required 318 // parameter (the node_name for example). 319 m := make(map[string]interface{}) 320 321 for k, v := range c.Raw { 322 m[k] = v 323 } 324 325 for k, v := range c.Config { 326 m[k] = v 327 } 328 329 if err := dec.Decode(m); err != nil { 330 return nil, err 331 } 332 333 // Make sure the supplied URL has a trailing slash 334 p.ServerURL = strings.TrimSuffix(p.ServerURL, "/") + "/" 335 336 if p.Environment == "" { 337 p.Environment = defaultEnv 338 } 339 340 for i, hint := range p.OhaiHints { 341 hintPath, err := homedir.Expand(hint) 342 if err != nil { 343 return nil, fmt.Errorf("Error expanding the path %s: %v", hint, err) 344 } 345 p.OhaiHints[i] = hintPath 346 } 347 348 if p.UserName == "" && p.ValidationClientName != "" { 349 p.UserName = p.ValidationClientName 350 } 351 352 if p.UserKey == "" && p.ValidationKey != "" { 353 p.UserKey = p.ValidationKey 354 } 355 356 if attrs, ok := c.Config["attributes_json"].(string); ok { 357 var m map[string]interface{} 358 if err := json.Unmarshal([]byte(attrs), &m); err != nil { 359 return nil, fmt.Errorf("Error parsing attributes_json: %v", err) 360 } 361 p.attributes = m 362 } 363 364 if vaults, ok := c.Config["vault_json"].(string); ok { 365 var m map[string]interface{} 366 if err := json.Unmarshal([]byte(vaults), &m); err != nil { 367 return nil, fmt.Errorf("Error parsing vault_json: %v", err) 368 } 369 370 v := make(map[string][]string) 371 for vault, items := range m { 372 switch items := items.(type) { 373 case []interface{}: 374 for _, item := range items { 375 if item, ok := item.(string); ok { 376 v[vault] = append(v[vault], item) 377 } 378 } 379 case interface{}: 380 if item, ok := items.(string); ok { 381 v[vault] = append(v[vault], item) 382 } 383 } 384 } 385 386 p.vaults = v 387 } 388 389 return p, nil 390 } 391 392 func (p *Provisioner) deployConfigFiles( 393 o terraform.UIOutput, 394 comm communicator.Communicator, 395 confDir string) error { 396 // Copy the user key to the new instance 397 pk := strings.NewReader(p.UserKey) 398 if err := comm.Upload(path.Join(confDir, p.UserName+".pem"), pk); err != nil { 399 return fmt.Errorf("Uploading user key failed: %v", err) 400 } 401 402 if p.SecretKey != "" { 403 // Copy the secret key to the new instance 404 s := strings.NewReader(p.SecretKey) 405 if err := comm.Upload(path.Join(confDir, secretKey), s); err != nil { 406 return fmt.Errorf("Uploading %s failed: %v", secretKey, err) 407 } 408 } 409 410 // Make sure the SSLVerifyMode value is written as a symbol 411 if p.SSLVerifyMode != "" && !strings.HasPrefix(p.SSLVerifyMode, ":") { 412 p.SSLVerifyMode = fmt.Sprintf(":%s", p.SSLVerifyMode) 413 } 414 415 // Make strings.Join available for use within the template 416 funcMap := template.FuncMap{ 417 "join": strings.Join, 418 } 419 420 // Create a new template and parse the client config into it 421 t := template.Must(template.New(clienrb).Funcs(funcMap).Parse(clientConf)) 422 423 var buf bytes.Buffer 424 err := t.Execute(&buf, p) 425 if err != nil { 426 return fmt.Errorf("Error executing %s template: %s", clienrb, err) 427 } 428 429 // Copy the client config to the new instance 430 if err := comm.Upload(path.Join(confDir, clienrb), &buf); err != nil { 431 return fmt.Errorf("Uploading %s failed: %v", clienrb, err) 432 } 433 434 // Create a map with first boot settings 435 fb := make(map[string]interface{}) 436 if p.attributes != nil { 437 fb = p.attributes 438 } 439 440 // Check if the run_list was also in the attributes and if so log a warning 441 // that it will be overwritten with the value of the run_list argument. 442 if _, found := fb["run_list"]; found { 443 log.Printf("[WARNING] Found a 'run_list' specified in the configured attributes! " + 444 "This value will be overwritten by the value of the `run_list` argument!") 445 } 446 447 // Add the initial runlist to the first boot settings 448 if !p.UsePolicyfile { 449 fb["run_list"] = p.RunList 450 } 451 452 // Marshal the first boot settings to JSON 453 d, err := json.Marshal(fb) 454 if err != nil { 455 return fmt.Errorf("Failed to create %s data: %s", firstBoot, err) 456 } 457 458 // Copy the first-boot.json to the new instance 459 if err := comm.Upload(path.Join(confDir, firstBoot), bytes.NewReader(d)); err != nil { 460 return fmt.Errorf("Uploading %s failed: %v", firstBoot, err) 461 } 462 463 return nil 464 } 465 466 func (p *Provisioner) deployOhaiHints( 467 o terraform.UIOutput, 468 comm communicator.Communicator, 469 hintDir string) error { 470 for _, hint := range p.OhaiHints { 471 // Open the hint file 472 f, err := os.Open(hint) 473 if err != nil { 474 return err 475 } 476 defer f.Close() 477 478 // Copy the hint to the new instance 479 if err := comm.Upload(path.Join(hintDir, path.Base(hint)), f); err != nil { 480 return fmt.Errorf("Uploading %s failed: %v", path.Base(hint), err) 481 } 482 } 483 484 return nil 485 } 486 487 func (p *Provisioner) fetchChefCertificatesFunc( 488 knifeCmd string, 489 confDir string) func(terraform.UIOutput, communicator.Communicator) error { 490 return func(o terraform.UIOutput, comm communicator.Communicator) error { 491 clientrb := path.Join(confDir, clienrb) 492 cmd := fmt.Sprintf("%s ssl fetch -c %s", knifeCmd, clientrb) 493 494 return p.runCommand(o, comm, cmd) 495 } 496 } 497 498 func (p *Provisioner) generateClientKeyFunc( 499 knifeCmd string, 500 confDir string, 501 noOutput string) func(terraform.UIOutput, communicator.Communicator) error { 502 return func(o terraform.UIOutput, comm communicator.Communicator) error { 503 options := fmt.Sprintf("-c %s -u %s --key %s", 504 path.Join(confDir, clienrb), 505 p.UserName, 506 path.Join(confDir, p.UserName+".pem"), 507 ) 508 509 // See if we already have a node object 510 getNodeCmd := fmt.Sprintf("%s node show %s %s %s", knifeCmd, p.NodeName, options, noOutput) 511 node := p.runCommand(o, comm, getNodeCmd) == nil 512 513 // See if we already have a client object 514 getClientCmd := fmt.Sprintf("%s client show %s %s %s", knifeCmd, p.NodeName, options, noOutput) 515 client := p.runCommand(o, comm, getClientCmd) == nil 516 517 // If we have a client, we can only continue if we are to recreate the client 518 if client && !p.RecreateClient { 519 return fmt.Errorf( 520 "Chef client %q already exists, set recreate_client=true to automatically recreate the client", p.NodeName) 521 } 522 523 // If the node exists, try to delete it 524 if node { 525 deleteNodeCmd := fmt.Sprintf("%s node delete %s -y %s", 526 knifeCmd, 527 p.NodeName, 528 options, 529 ) 530 if err := p.runCommand(o, comm, deleteNodeCmd); err != nil { 531 return err 532 } 533 } 534 535 // If the client exists, try to delete it 536 if client { 537 deleteClientCmd := fmt.Sprintf("%s client delete %s -y %s", 538 knifeCmd, 539 p.NodeName, 540 options, 541 ) 542 if err := p.runCommand(o, comm, deleteClientCmd); err != nil { 543 return err 544 } 545 } 546 547 // Create the new client object 548 createClientCmd := fmt.Sprintf("%s client create %s -d -f %s %s", 549 knifeCmd, 550 p.NodeName, 551 path.Join(confDir, "client.pem"), 552 options, 553 ) 554 555 return p.runCommand(o, comm, createClientCmd) 556 } 557 } 558 559 func (p *Provisioner) configureVaultsFunc( 560 gemCmd string, 561 knifeCmd string, 562 confDir string) func(terraform.UIOutput, communicator.Communicator) error { 563 return func(o terraform.UIOutput, comm communicator.Communicator) error { 564 if err := p.runCommand(o, comm, fmt.Sprintf("%s install chef-vault", gemCmd)); err != nil { 565 return err 566 } 567 568 options := fmt.Sprintf("-c %s -u %s --key %s", 569 path.Join(confDir, clienrb), 570 p.UserName, 571 path.Join(confDir, p.UserName+".pem"), 572 ) 573 574 for vault, items := range p.vaults { 575 for _, item := range items { 576 updateCmd := fmt.Sprintf("%s vault update %s %s -A %s -M client %s", 577 knifeCmd, 578 vault, 579 item, 580 p.NodeName, 581 options, 582 ) 583 if err := p.runCommand(o, comm, updateCmd); err != nil { 584 return err 585 } 586 } 587 } 588 589 return nil 590 } 591 } 592 593 func (p *Provisioner) runChefClientFunc( 594 chefCmd string, 595 confDir string) func(terraform.UIOutput, communicator.Communicator) error { 596 return func(o terraform.UIOutput, comm communicator.Communicator) error { 597 fb := path.Join(confDir, firstBoot) 598 var cmd string 599 600 // Policyfiles do not support chef environments, so don't pass the `-E` flag. 601 if p.UsePolicyfile { 602 cmd = fmt.Sprintf("%s -j %q", chefCmd, fb) 603 } else { 604 cmd = fmt.Sprintf("%s -j %q -E %q", chefCmd, fb, p.Environment) 605 } 606 607 if p.LogToFile { 608 if err := os.MkdirAll(logfileDir, 0755); err != nil { 609 return fmt.Errorf("Error creating logfile directory %s: %v", logfileDir, err) 610 } 611 612 logFile := path.Join(logfileDir, p.NodeName) 613 f, err := os.Create(path.Join(logFile)) 614 if err != nil { 615 return fmt.Errorf("Error creating logfile %s: %v", logFile, err) 616 } 617 f.Close() 618 619 o.Output("Writing Chef Client output to " + logFile) 620 o = p 621 } 622 623 return p.runCommand(o, comm, cmd) 624 } 625 } 626 627 // Output implementation of terraform.UIOutput interface 628 func (p *Provisioner) Output(output string) { 629 logFile := path.Join(logfileDir, p.NodeName) 630 f, err := os.OpenFile(logFile, os.O_APPEND|os.O_WRONLY, 0666) 631 if err != nil { 632 log.Printf("Error creating logfile %s: %v", logFile, err) 633 return 634 } 635 defer f.Close() 636 637 // These steps are needed to remove any ANSI escape codes used to colorize 638 // the output and to make sure we have proper line endings before writing 639 // the string to the logfile. 640 re := regexp.MustCompile(`\x1b\[[0-9;]+m`) 641 output = re.ReplaceAllString(output, "") 642 output = strings.Replace(output, "\r", "\n", -1) 643 644 if _, err := f.WriteString(output); err != nil { 645 log.Printf("Error writing output to logfile %s: %v", logFile, err) 646 } 647 648 if err := f.Sync(); err != nil { 649 log.Printf("Error saving logfile %s to disk: %v", logFile, err) 650 } 651 } 652 653 // runCommand is used to run already prepared commands 654 func (p *Provisioner) runCommand( 655 o terraform.UIOutput, 656 comm communicator.Communicator, 657 command string) error { 658 // Unless prevented, prefix the command with sudo 659 if p.useSudo { 660 command = "sudo " + command 661 } 662 663 outR, outW := io.Pipe() 664 errR, errW := io.Pipe() 665 outDoneCh := make(chan struct{}) 666 errDoneCh := make(chan struct{}) 667 go p.copyOutput(o, outR, outDoneCh) 668 go p.copyOutput(o, errR, errDoneCh) 669 670 cmd := &remote.Cmd{ 671 Command: command, 672 Stdout: outW, 673 Stderr: errW, 674 } 675 676 err := comm.Start(cmd) 677 if err != nil { 678 return fmt.Errorf("Error executing command %q: %v", cmd.Command, err) 679 } 680 681 cmd.Wait() 682 if cmd.ExitStatus != 0 { 683 err = fmt.Errorf( 684 "Command %q exited with non-zero exit status: %d", cmd.Command, cmd.ExitStatus) 685 } 686 687 // Wait for output to clean up 688 outW.Close() 689 errW.Close() 690 <-outDoneCh 691 <-errDoneCh 692 693 return err 694 } 695 696 func (p *Provisioner) copyOutput(o terraform.UIOutput, r io.Reader, doneCh chan<- struct{}) { 697 defer close(doneCh) 698 lr := linereader.New(r) 699 for line := range lr.Ch { 700 o.Output(line) 701 } 702 } 703 704 // retryFunc is used to retry a function for a given duration 705 func retryFunc(timeout time.Duration, f func() error) error { 706 finish := time.After(timeout) 707 for { 708 err := f() 709 if err == nil { 710 return nil 711 } 712 log.Printf("Retryable error: %v", err) 713 714 select { 715 case <-finish: 716 return err 717 case <-time.After(3 * time.Second): 718 } 719 } 720 }