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