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  }