github.com/hooklift/terraform@v0.11.0-beta1.0.20171117000744-6786c1361ffe/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  	"strings"
    15  	"sync"
    16  	"text/template"
    17  	"time"
    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  	// Wait and retry until we establish the connection
   310  	err = retryFunc(comm.Timeout(), func() error {
   311  		return comm.Connect(o)
   312  	})
   313  	if err != nil {
   314  		return err
   315  	}
   316  	defer comm.Disconnect()
   317  
   318  	// Make sure we always delete the user key from the new node!
   319  	var once sync.Once
   320  	cleanupUserKey := func() {
   321  		o.Output("Cleanup user key...")
   322  		if err := p.runCommand(o, comm, p.cleanupUserKeyCmd); err != nil {
   323  			o.Output("WARNING: Failed to cleanup user key on new node: " + err.Error())
   324  		}
   325  	}
   326  	defer once.Do(cleanupUserKey)
   327  
   328  	if !p.SkipInstall {
   329  		if err := p.installChefClient(o, comm); err != nil {
   330  			return err
   331  		}
   332  	}
   333  
   334  	o.Output("Creating configuration files...")
   335  	if err := p.createConfigFiles(o, comm); err != nil {
   336  		return err
   337  	}
   338  
   339  	if !p.SkipRegister {
   340  		if p.FetchChefCertificates {
   341  			o.Output("Fetch Chef certificates...")
   342  			if err := p.fetchChefCertificates(o, comm); err != nil {
   343  				return err
   344  			}
   345  		}
   346  
   347  		o.Output("Generate the private key...")
   348  		if err := p.generateClientKey(o, comm); err != nil {
   349  			return err
   350  		}
   351  	}
   352  
   353  	if p.Vaults != nil {
   354  		o.Output("Configure Chef vaults...")
   355  		if err := p.configureVaults(o, comm); err != nil {
   356  			return err
   357  		}
   358  	}
   359  
   360  	// Cleanup the user key before we run Chef-Client to prevent issues
   361  	// with rights caused by changing settings during the run.
   362  	once.Do(cleanupUserKey)
   363  
   364  	o.Output("Starting initial Chef-Client run...")
   365  	if err := p.runChefClient(o, comm); err != nil {
   366  		return err
   367  	}
   368  
   369  	return nil
   370  }
   371  
   372  func validateFn(c *terraform.ResourceConfig) (ws []string, es []error) {
   373  	usePolicyFile, ok := c.Get("use_policyfile")
   374  	if !ok {
   375  		usePolicyFile = false
   376  	}
   377  
   378  	if !usePolicyFile.(bool) && !c.IsSet("run_list") {
   379  		es = append(es, errors.New("\"run_list\": required field is not set"))
   380  	}
   381  	if usePolicyFile.(bool) && !c.IsSet("policy_name") {
   382  		es = append(es, errors.New("using policyfile, but \"policy_name\" not set"))
   383  	}
   384  	if usePolicyFile.(bool) && !c.IsSet("policy_group") {
   385  		es = append(es, errors.New("using policyfile, but \"policy_group\" not set"))
   386  	}
   387  
   388  	return ws, es
   389  }
   390  
   391  func (p *provisioner) deployConfigFiles(o terraform.UIOutput, comm communicator.Communicator, confDir string) error {
   392  	// Copy the user key to the new instance
   393  	pk := strings.NewReader(p.UserKey)
   394  	if err := comm.Upload(path.Join(confDir, p.UserName+".pem"), pk); err != nil {
   395  		return fmt.Errorf("Uploading user key failed: %v", err)
   396  	}
   397  
   398  	if p.SecretKey != "" {
   399  		// Copy the secret key to the new instance
   400  		s := strings.NewReader(p.SecretKey)
   401  		if err := comm.Upload(path.Join(confDir, secretKey), s); err != nil {
   402  			return fmt.Errorf("Uploading %s failed: %v", secretKey, err)
   403  		}
   404  	}
   405  
   406  	// Make sure the SSLVerifyMode value is written as a symbol
   407  	if p.SSLVerifyMode != "" && !strings.HasPrefix(p.SSLVerifyMode, ":") {
   408  		p.SSLVerifyMode = fmt.Sprintf(":%s", p.SSLVerifyMode)
   409  	}
   410  
   411  	// Make strings.Join available for use within the template
   412  	funcMap := template.FuncMap{
   413  		"join": strings.Join,
   414  	}
   415  
   416  	// Create a new template and parse the client config into it
   417  	t := template.Must(template.New(clienrb).Funcs(funcMap).Parse(clientConf))
   418  
   419  	var buf bytes.Buffer
   420  	err := t.Execute(&buf, p)
   421  	if err != nil {
   422  		return fmt.Errorf("Error executing %s template: %s", clienrb, err)
   423  	}
   424  
   425  	// Copy the client config to the new instance
   426  	if err = comm.Upload(path.Join(confDir, clienrb), &buf); err != nil {
   427  		return fmt.Errorf("Uploading %s failed: %v", clienrb, err)
   428  	}
   429  
   430  	// Create a map with first boot settings
   431  	fb := make(map[string]interface{})
   432  	if p.Attributes != nil {
   433  		fb = p.Attributes
   434  	}
   435  
   436  	// Check if the run_list was also in the attributes and if so log a warning
   437  	// that it will be overwritten with the value of the run_list argument.
   438  	if _, found := fb["run_list"]; found {
   439  		log.Printf("[WARNING] Found a 'run_list' specified in the configured attributes! " +
   440  			"This value will be overwritten by the value of the `run_list` argument!")
   441  	}
   442  
   443  	// Add the initial runlist to the first boot settings
   444  	if !p.UsePolicyfile {
   445  		fb["run_list"] = p.RunList
   446  	}
   447  
   448  	// Marshal the first boot settings to JSON
   449  	d, err := json.Marshal(fb)
   450  	if err != nil {
   451  		return fmt.Errorf("Failed to create %s data: %s", firstBoot, err)
   452  	}
   453  
   454  	// Copy the first-boot.json to the new instance
   455  	if err := comm.Upload(path.Join(confDir, firstBoot), bytes.NewReader(d)); err != nil {
   456  		return fmt.Errorf("Uploading %s failed: %v", firstBoot, err)
   457  	}
   458  
   459  	return nil
   460  }
   461  
   462  func (p *provisioner) deployOhaiHints(o terraform.UIOutput, comm communicator.Communicator, hintDir string) error {
   463  	for _, hint := range p.OhaiHints {
   464  		// Open the hint file
   465  		f, err := os.Open(hint)
   466  		if err != nil {
   467  			return err
   468  		}
   469  		defer f.Close()
   470  
   471  		// Copy the hint to the new instance
   472  		if err := comm.Upload(path.Join(hintDir, path.Base(hint)), f); err != nil {
   473  			return fmt.Errorf("Uploading %s failed: %v", path.Base(hint), err)
   474  		}
   475  	}
   476  
   477  	return nil
   478  }
   479  
   480  func (p *provisioner) fetchChefCertificatesFunc(
   481  	knifeCmd string,
   482  	confDir string) func(terraform.UIOutput, communicator.Communicator) error {
   483  	return func(o terraform.UIOutput, comm communicator.Communicator) error {
   484  		clientrb := path.Join(confDir, clienrb)
   485  		cmd := fmt.Sprintf("%s ssl fetch -c %s", knifeCmd, clientrb)
   486  
   487  		return p.runCommand(o, comm, cmd)
   488  	}
   489  }
   490  
   491  func (p *provisioner) generateClientKeyFunc(knifeCmd string, confDir string, noOutput string) provisionFn {
   492  	return func(o terraform.UIOutput, comm communicator.Communicator) error {
   493  		options := fmt.Sprintf("-c %s -u %s --key %s",
   494  			path.Join(confDir, clienrb),
   495  			p.UserName,
   496  			path.Join(confDir, p.UserName+".pem"),
   497  		)
   498  
   499  		// See if we already have a node object
   500  		getNodeCmd := fmt.Sprintf("%s node show %s %s %s", knifeCmd, p.NodeName, options, noOutput)
   501  		node := p.runCommand(o, comm, getNodeCmd) == nil
   502  
   503  		// See if we already have a client object
   504  		getClientCmd := fmt.Sprintf("%s client show %s %s %s", knifeCmd, p.NodeName, options, noOutput)
   505  		client := p.runCommand(o, comm, getClientCmd) == nil
   506  
   507  		// If we have a client, we can only continue if we are to recreate the client
   508  		if client && !p.RecreateClient {
   509  			return fmt.Errorf(
   510  				"Chef client %q already exists, set recreate_client=true to automatically recreate the client", p.NodeName)
   511  		}
   512  
   513  		// If the node exists, try to delete it
   514  		if node {
   515  			deleteNodeCmd := fmt.Sprintf("%s node delete %s -y %s",
   516  				knifeCmd,
   517  				p.NodeName,
   518  				options,
   519  			)
   520  			if err := p.runCommand(o, comm, deleteNodeCmd); err != nil {
   521  				return err
   522  			}
   523  		}
   524  
   525  		// If the client exists, try to delete it
   526  		if client {
   527  			deleteClientCmd := fmt.Sprintf("%s client delete %s -y %s",
   528  				knifeCmd,
   529  				p.NodeName,
   530  				options,
   531  			)
   532  			if err := p.runCommand(o, comm, deleteClientCmd); err != nil {
   533  				return err
   534  			}
   535  		}
   536  
   537  		// Create the new client object
   538  		createClientCmd := fmt.Sprintf("%s client create %s -d -f %s %s",
   539  			knifeCmd,
   540  			p.NodeName,
   541  			path.Join(confDir, "client.pem"),
   542  			options,
   543  		)
   544  
   545  		return p.runCommand(o, comm, createClientCmd)
   546  	}
   547  }
   548  
   549  func (p *provisioner) configureVaultsFunc(gemCmd string, knifeCmd string, confDir string) provisionFn {
   550  	return func(o terraform.UIOutput, comm communicator.Communicator) error {
   551  		if err := p.runCommand(o, comm, fmt.Sprintf("%s install chef-vault", gemCmd)); err != nil {
   552  			return err
   553  		}
   554  
   555  		options := fmt.Sprintf("-c %s -u %s --key %s",
   556  			path.Join(confDir, clienrb),
   557  			p.UserName,
   558  			path.Join(confDir, p.UserName+".pem"),
   559  		)
   560  
   561  		// if client gets recreated, remove (old) client (with old keys) from vaults/items
   562  		// otherwise, the (new) client (with new keys) will not be able to decrypt the vault
   563  		if p.RecreateClient {
   564  			for vault, items := range p.Vaults {
   565  				for _, item := range items {
   566  					deleteCmd := fmt.Sprintf("%s vault remove %s %s -C \"%s\" -M client %s",
   567  						knifeCmd,
   568  						vault,
   569  						item,
   570  						p.NodeName,
   571  						options,
   572  					)
   573  					if err := p.runCommand(o, comm, deleteCmd); err != nil {
   574  						return err
   575  					}
   576  				}
   577  			}
   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(chefCmd string, confDir string) provisionFn {
   600  	return func(o terraform.UIOutput, comm communicator.Communicator) error {
   601  		fb := path.Join(confDir, firstBoot)
   602  		var cmd string
   603  
   604  		// Policyfiles do not support chef environments, so don't pass the `-E` flag.
   605  		switch {
   606  		case p.UsePolicyfile && p.NamedRunList == "":
   607  			cmd = fmt.Sprintf("%s -j %q", chefCmd, fb)
   608  		case p.UsePolicyfile && p.NamedRunList != "":
   609  			cmd = fmt.Sprintf("%s -j %q -n %q", chefCmd, fb, p.NamedRunList)
   610  		default:
   611  			cmd = fmt.Sprintf("%s -j %q -E %q", chefCmd, fb, p.Environment)
   612  		}
   613  
   614  		if p.LogToFile {
   615  			if err := os.MkdirAll(logfileDir, 0755); err != nil {
   616  				return fmt.Errorf("Error creating logfile directory %s: %v", logfileDir, err)
   617  			}
   618  
   619  			logFile := path.Join(logfileDir, p.NodeName)
   620  			f, err := os.Create(path.Join(logFile))
   621  			if err != nil {
   622  				return fmt.Errorf("Error creating logfile %s: %v", logFile, err)
   623  			}
   624  			f.Close()
   625  
   626  			o.Output("Writing Chef Client output to " + logFile)
   627  			o = p
   628  		}
   629  
   630  		return p.runCommand(o, comm, cmd)
   631  	}
   632  }
   633  
   634  // Output implementation of terraform.UIOutput interface
   635  func (p *provisioner) Output(output string) {
   636  	logFile := path.Join(logfileDir, p.NodeName)
   637  	f, err := os.OpenFile(logFile, os.O_APPEND|os.O_WRONLY, 0666)
   638  	if err != nil {
   639  		log.Printf("Error creating logfile %s: %v", logFile, err)
   640  		return
   641  	}
   642  	defer f.Close()
   643  
   644  	// These steps are needed to remove any ANSI escape codes used to colorize
   645  	// the output and to make sure we have proper line endings before writing
   646  	// the string to the logfile.
   647  	re := regexp.MustCompile(`\x1b\[[0-9;]+m`)
   648  	output = re.ReplaceAllString(output, "")
   649  	output = strings.Replace(output, "\r", "\n", -1)
   650  
   651  	if _, err := f.WriteString(output); err != nil {
   652  		log.Printf("Error writing output to logfile %s: %v", logFile, err)
   653  	}
   654  
   655  	if err := f.Sync(); err != nil {
   656  		log.Printf("Error saving logfile %s to disk: %v", logFile, err)
   657  	}
   658  }
   659  
   660  // runCommand is used to run already prepared commands
   661  func (p *provisioner) runCommand(o terraform.UIOutput, comm communicator.Communicator, 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  }
   725  
   726  func decodeConfig(d *schema.ResourceData) (*provisioner, error) {
   727  	p := &provisioner{
   728  		ClientOptions:         getStringList(d.Get("client_options")),
   729  		DisableReporting:      d.Get("disable_reporting").(bool),
   730  		Environment:           d.Get("environment").(string),
   731  		FetchChefCertificates: d.Get("fetch_chef_certificates").(bool),
   732  		LogToFile:             d.Get("log_to_file").(bool),
   733  		UsePolicyfile:         d.Get("use_policyfile").(bool),
   734  		PolicyGroup:           d.Get("policy_group").(string),
   735  		PolicyName:            d.Get("policy_name").(string),
   736  		HTTPProxy:             d.Get("http_proxy").(string),
   737  		HTTPSProxy:            d.Get("https_proxy").(string),
   738  		NOProxy:               getStringList(d.Get("no_proxy")),
   739  		NamedRunList:          d.Get("named_run_list").(string),
   740  		NodeName:              d.Get("node_name").(string),
   741  		OhaiHints:             getStringList(d.Get("ohai_hints")),
   742  		OSType:                d.Get("os_type").(string),
   743  		RecreateClient:        d.Get("recreate_client").(bool),
   744  		PreventSudo:           d.Get("prevent_sudo").(bool),
   745  		RunList:               getStringList(d.Get("run_list")),
   746  		SecretKey:             d.Get("secret_key").(string),
   747  		ServerURL:             d.Get("server_url").(string),
   748  		SkipInstall:           d.Get("skip_install").(bool),
   749  		SkipRegister:          d.Get("skip_register").(bool),
   750  		SSLVerifyMode:         d.Get("ssl_verify_mode").(string),
   751  		UserName:              d.Get("user_name").(string),
   752  		UserKey:               d.Get("user_key").(string),
   753  		Version:               d.Get("version").(string),
   754  	}
   755  
   756  	// Make sure the supplied URL has a trailing slash
   757  	p.ServerURL = strings.TrimSuffix(p.ServerURL, "/") + "/"
   758  
   759  	for i, hint := range p.OhaiHints {
   760  		hintPath, err := homedir.Expand(hint)
   761  		if err != nil {
   762  			return nil, fmt.Errorf("Error expanding the path %s: %v", hint, err)
   763  		}
   764  		p.OhaiHints[i] = hintPath
   765  	}
   766  
   767  	if attrs, ok := d.GetOk("attributes_json"); ok {
   768  		var m map[string]interface{}
   769  		if err := json.Unmarshal([]byte(attrs.(string)), &m); err != nil {
   770  			return nil, fmt.Errorf("Error parsing attributes_json: %v", err)
   771  		}
   772  		p.Attributes = m
   773  	}
   774  
   775  	if vaults, ok := d.GetOk("vault_json"); ok {
   776  		var m map[string]interface{}
   777  		if err := json.Unmarshal([]byte(vaults.(string)), &m); err != nil {
   778  			return nil, fmt.Errorf("Error parsing vault_json: %v", err)
   779  		}
   780  
   781  		v := make(map[string][]string)
   782  		for vault, items := range m {
   783  			switch items := items.(type) {
   784  			case []interface{}:
   785  				for _, item := range items {
   786  					if item, ok := item.(string); ok {
   787  						v[vault] = append(v[vault], item)
   788  					}
   789  				}
   790  			case interface{}:
   791  				if item, ok := items.(string); ok {
   792  					v[vault] = append(v[vault], item)
   793  				}
   794  			}
   795  		}
   796  
   797  		p.Vaults = v
   798  	}
   799  
   800  	return p, nil
   801  }
   802  
   803  func getStringList(v interface{}) []string {
   804  	var result []string
   805  
   806  	switch v := v.(type) {
   807  	case nil:
   808  		return result
   809  	case []interface{}:
   810  		for _, vv := range v {
   811  			if vv, ok := vv.(string); ok {
   812  				result = append(result, vv)
   813  			}
   814  		}
   815  		return result
   816  	default:
   817  		panic(fmt.Sprintf("Unsupported type: %T", v))
   818  	}
   819  }