github.com/nathanielks/terraform@v0.6.1-0.20170509030759-13e1a62319dc/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 -C %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  }