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