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