github.com/handlerbot/terraform@v0.10.0-beta1.0.20180726153736-26b68d98f9cb/builtin/provisioners/chef/resource_provisioner.go (about)

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