github.com/acm1/terraform@v0.6.2-0.20150729164239-1f314444f45c/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  	"text/template"
    15  	"time"
    16  
    17  	"github.com/hashicorp/terraform/communicator"
    18  	"github.com/hashicorp/terraform/communicator/remote"
    19  	"github.com/hashicorp/terraform/terraform"
    20  	"github.com/mitchellh/go-homedir"
    21  	"github.com/mitchellh/go-linereader"
    22  	"github.com/mitchellh/mapstructure"
    23  )
    24  
    25  const (
    26  	clienrb        = "client.rb"
    27  	defaultEnv     = "_default"
    28  	firstBoot      = "first-boot.json"
    29  	logfileDir     = "logfiles"
    30  	linuxChefCmd   = "chef-client"
    31  	linuxConfDir   = "/etc/chef"
    32  	secretKey      = "encrypted_data_bag_secret"
    33  	validationKey  = "validation.pem"
    34  	windowsChefCmd = "cmd /c chef-client"
    35  	windowsConfDir = "C:/chef"
    36  )
    37  
    38  const clientConf = `
    39  log_location            STDOUT
    40  chef_server_url         "{{ .ServerURL }}"
    41  validation_client_name  "{{ .ValidationClientName }}"
    42  node_name               "{{ .NodeName }}"
    43  
    44  {{ if .HTTPProxy }}
    45  http_proxy          "{{ .HTTPProxy }}"
    46  ENV['http_proxy'] = "{{ .HTTPProxy }}"
    47  ENV['HTTP_PROXY'] = "{{ .HTTPProxy }}"
    48  {{ end }}
    49  
    50  {{ if .HTTPSProxy }}
    51  https_proxy          "{{ .HTTPSProxy }}"
    52  ENV['https_proxy'] = "{{ .HTTPSProxy }}"
    53  ENV['HTTPS_PROXY'] = "{{ .HTTPSProxy }}"
    54  {{ end }}
    55  
    56  {{ if .NOProxy }}no_proxy "{{ join .NOProxy "," }}"{{ end }}
    57  {{ if .SSLVerifyMode }}ssl_verify_mode {{ .SSLVerifyMode }}{{ end }}
    58  `
    59  
    60  // Provisioner represents a specificly configured chef provisioner
    61  type Provisioner struct {
    62  	Attributes           interface{} `mapstructure:"attributes"`
    63  	Environment          string      `mapstructure:"environment"`
    64  	LogToFile            bool        `mapstructure:"log_to_file"`
    65  	HTTPProxy            string      `mapstructure:"http_proxy"`
    66  	HTTPSProxy           string      `mapstructure:"https_proxy"`
    67  	NOProxy              []string    `mapstructure:"no_proxy"`
    68  	NodeName             string      `mapstructure:"node_name"`
    69  	OhaiHints            []string    `mapstructure:"ohai_hints"`
    70  	OSType               string      `mapstructure:"os_type"`
    71  	PreventSudo          bool        `mapstructure:"prevent_sudo"`
    72  	RunList              []string    `mapstructure:"run_list"`
    73  	SecretKeyPath        string      `mapstructure:"secret_key_path"`
    74  	ServerURL            string      `mapstructure:"server_url"`
    75  	SkipInstall          bool        `mapstructure:"skip_install"`
    76  	SSLVerifyMode        string      `mapstructure:"ssl_verify_mode"`
    77  	ValidationClientName string      `mapstructure:"validation_client_name"`
    78  	ValidationKeyPath    string      `mapstructure:"validation_key_path"`
    79  	Version              string      `mapstructure:"version"`
    80  
    81  	installChefClient func(terraform.UIOutput, communicator.Communicator) error
    82  	createConfigFiles func(terraform.UIOutput, communicator.Communicator) error
    83  	runChefClient     func(terraform.UIOutput, communicator.Communicator) error
    84  	useSudo           bool
    85  }
    86  
    87  // ResourceProvisioner represents a generic chef provisioner
    88  type ResourceProvisioner struct{}
    89  
    90  // Apply executes the file provisioner
    91  func (r *ResourceProvisioner) Apply(
    92  	o terraform.UIOutput,
    93  	s *terraform.InstanceState,
    94  	c *terraform.ResourceConfig) error {
    95  	// Decode the raw config for this provisioner
    96  	p, err := r.decodeConfig(c)
    97  	if err != nil {
    98  		return err
    99  	}
   100  
   101  	if p.OSType == "" {
   102  		switch s.Ephemeral.ConnInfo["type"] {
   103  		case "ssh", "": // The default connection type is ssh, so if the type is empty assume ssh
   104  			p.OSType = "linux"
   105  		case "winrm":
   106  			p.OSType = "windows"
   107  		default:
   108  			return fmt.Errorf("Unsupported connection type: %s", s.Ephemeral.ConnInfo["type"])
   109  		}
   110  	}
   111  
   112  	// Set some values based on the targeted OS
   113  	switch p.OSType {
   114  	case "linux":
   115  		p.installChefClient = p.linuxInstallChefClient
   116  		p.createConfigFiles = p.linuxCreateConfigFiles
   117  		p.runChefClient = p.runChefClientFunc(linuxChefCmd, linuxConfDir)
   118  		p.useSudo = !p.PreventSudo && s.Ephemeral.ConnInfo["user"] != "root"
   119  	case "windows":
   120  		p.installChefClient = p.windowsInstallChefClient
   121  		p.createConfigFiles = p.windowsCreateConfigFiles
   122  		p.runChefClient = p.runChefClientFunc(windowsChefCmd, windowsConfDir)
   123  		p.useSudo = false
   124  	default:
   125  		return fmt.Errorf("Unsupported os type: %s", p.OSType)
   126  	}
   127  
   128  	// Get a new communicator
   129  	comm, err := communicator.New(s)
   130  	if err != nil {
   131  		return err
   132  	}
   133  
   134  	// Wait and retry until we establish the connection
   135  	err = retryFunc(comm.Timeout(), func() error {
   136  		err := comm.Connect(o)
   137  		return err
   138  	})
   139  	if err != nil {
   140  		return err
   141  	}
   142  	defer comm.Disconnect()
   143  
   144  	if !p.SkipInstall {
   145  		if err := p.installChefClient(o, comm); err != nil {
   146  			return err
   147  		}
   148  	}
   149  
   150  	o.Output("Creating configuration files...")
   151  	if err := p.createConfigFiles(o, comm); err != nil {
   152  		return err
   153  	}
   154  
   155  	o.Output("Starting initial Chef-Client run...")
   156  	if err := p.runChefClient(o, comm); err != nil {
   157  		return err
   158  	}
   159  
   160  	return nil
   161  }
   162  
   163  // Validate checks if the required arguments are configured
   164  func (r *ResourceProvisioner) Validate(c *terraform.ResourceConfig) (ws []string, es []error) {
   165  	p, err := r.decodeConfig(c)
   166  	if err != nil {
   167  		es = append(es, err)
   168  		return ws, es
   169  	}
   170  
   171  	if p.NodeName == "" {
   172  		es = append(es, fmt.Errorf("Key not found: node_name"))
   173  	}
   174  	if p.RunList == nil {
   175  		es = append(es, fmt.Errorf("Key not found: run_list"))
   176  	}
   177  	if p.ServerURL == "" {
   178  		es = append(es, fmt.Errorf("Key not found: server_url"))
   179  	}
   180  	if p.ValidationClientName == "" {
   181  		es = append(es, fmt.Errorf("Key not found: validation_client_name"))
   182  	}
   183  	if p.ValidationKeyPath == "" {
   184  		es = append(es, fmt.Errorf("Key not found: validation_key_path"))
   185  	}
   186  
   187  	return ws, es
   188  }
   189  
   190  func (r *ResourceProvisioner) decodeConfig(c *terraform.ResourceConfig) (*Provisioner, error) {
   191  	p := new(Provisioner)
   192  
   193  	decConf := &mapstructure.DecoderConfig{
   194  		ErrorUnused:      true,
   195  		WeaklyTypedInput: true,
   196  		Result:           p,
   197  	}
   198  	dec, err := mapstructure.NewDecoder(decConf)
   199  	if err != nil {
   200  		return nil, err
   201  	}
   202  
   203  	// We need to decode this twice. Once for the Raw config and once
   204  	// for the parsed Config. This makes sure that all values are there
   205  	// even if some still need to be interpolated later on.
   206  	// Without this the validation will fail when using a variable for
   207  	// a required parameter (the node_name for example).
   208  	if err := dec.Decode(c.Raw); err != nil {
   209  		return nil, err
   210  	}
   211  
   212  	if err := dec.Decode(c.Config); err != nil {
   213  		return nil, err
   214  	}
   215  
   216  	if p.Environment == "" {
   217  		p.Environment = defaultEnv
   218  	}
   219  
   220  	for i, hint := range p.OhaiHints {
   221  		hintPath, err := homedir.Expand(hint)
   222  		if err != nil {
   223  			return nil, fmt.Errorf("Error expanding the path %s: %v", hint, err)
   224  		}
   225  		p.OhaiHints[i] = hintPath
   226  	}
   227  
   228  	if p.ValidationKeyPath != "" {
   229  		keyPath, err := homedir.Expand(p.ValidationKeyPath)
   230  		if err != nil {
   231  			return nil, fmt.Errorf("Error expanding the validation key path: %v", err)
   232  		}
   233  		p.ValidationKeyPath = keyPath
   234  	}
   235  
   236  	if p.SecretKeyPath != "" {
   237  		keyPath, err := homedir.Expand(p.SecretKeyPath)
   238  		if err != nil {
   239  			return nil, fmt.Errorf("Error expanding the secret key path: %v", err)
   240  		}
   241  		p.SecretKeyPath = keyPath
   242  	}
   243  
   244  	if attrs, ok := c.Config["attributes"]; ok {
   245  		p.Attributes, err = rawToJSON(attrs)
   246  		if err != nil {
   247  			return nil, fmt.Errorf("Error parsing the attributes: %v", err)
   248  		}
   249  	}
   250  
   251  	return p, nil
   252  }
   253  
   254  func rawToJSON(raw interface{}) (interface{}, error) {
   255  	switch s := raw.(type) {
   256  	case []map[string]interface{}:
   257  		if len(s) != 1 {
   258  			return nil, errors.New("unexpected input while parsing raw config to JSON")
   259  		}
   260  
   261  		var err error
   262  		for k, v := range s[0] {
   263  			s[0][k], err = rawToJSON(v)
   264  			if err != nil {
   265  				return nil, err
   266  			}
   267  		}
   268  
   269  		return s[0], nil
   270  	default:
   271  		return raw, nil
   272  	}
   273  }
   274  
   275  // retryFunc is used to retry a function for a given duration
   276  func retryFunc(timeout time.Duration, f func() error) error {
   277  	finish := time.After(timeout)
   278  	for {
   279  		err := f()
   280  		if err == nil {
   281  			return nil
   282  		}
   283  		log.Printf("Retryable error: %v", err)
   284  
   285  		select {
   286  		case <-finish:
   287  			return err
   288  		case <-time.After(3 * time.Second):
   289  		}
   290  	}
   291  }
   292  
   293  func (p *Provisioner) runChefClientFunc(
   294  	chefCmd string,
   295  	confDir string) func(terraform.UIOutput, communicator.Communicator) error {
   296  	return func(o terraform.UIOutput, comm communicator.Communicator) error {
   297  		fb := path.Join(confDir, firstBoot)
   298  		cmd := fmt.Sprintf("%s -j %q -E %q", chefCmd, fb, p.Environment)
   299  
   300  		if p.LogToFile {
   301  			if err := os.MkdirAll(logfileDir, 0755); err != nil {
   302  				return fmt.Errorf("Error creating logfile directory %s: %v", logfileDir, err)
   303  			}
   304  
   305  			logFile := path.Join(logfileDir, p.NodeName)
   306  			f, err := os.Create(path.Join(logFile))
   307  			if err != nil {
   308  				return fmt.Errorf("Error creating logfile %s: %v", logFile, err)
   309  			}
   310  			f.Close()
   311  
   312  			o.Output("Writing Chef Client output to " + logFile)
   313  			o = p
   314  		}
   315  
   316  		return p.runCommand(o, comm, cmd)
   317  	}
   318  }
   319  
   320  // Output implementation of terraform.UIOutput interface
   321  func (p *Provisioner) Output(output string) {
   322  	logFile := path.Join(logfileDir, p.NodeName)
   323  	f, err := os.OpenFile(logFile, os.O_APPEND|os.O_WRONLY, 0666)
   324  	if err != nil {
   325  		log.Printf("Error creating logfile %s: %v", logFile, err)
   326  		return
   327  	}
   328  	defer f.Close()
   329  
   330  	// These steps are needed to remove any ANSI escape codes used to colorize
   331  	// the output and to make sure we have proper line endings before writing
   332  	// the string to the logfile.
   333  	re := regexp.MustCompile(`\x1b\[[0-9;]+m`)
   334  	output = re.ReplaceAllString(output, "")
   335  	output = strings.Replace(output, "\r", "\n", -1)
   336  
   337  	if _, err := f.WriteString(output); err != nil {
   338  		log.Printf("Error writing output to logfile %s: %v", logFile, err)
   339  	}
   340  
   341  	if err := f.Sync(); err != nil {
   342  		log.Printf("Error saving logfile %s to disk: %v", logFile, err)
   343  	}
   344  }
   345  
   346  func (p *Provisioner) deployConfigFiles(
   347  	o terraform.UIOutput,
   348  	comm communicator.Communicator,
   349  	confDir string) error {
   350  	// Open the validation key file
   351  	f, err := os.Open(p.ValidationKeyPath)
   352  	if err != nil {
   353  		return err
   354  	}
   355  	defer f.Close()
   356  
   357  	// Copy the validation key to the new instance
   358  	if err := comm.Upload(path.Join(confDir, validationKey), f); err != nil {
   359  		return fmt.Errorf("Uploading %s failed: %v", validationKey, err)
   360  	}
   361  
   362  	if p.SecretKeyPath != "" {
   363  		// Open the secret key file
   364  		s, err := os.Open(p.SecretKeyPath)
   365  		if err != nil {
   366  			return err
   367  		}
   368  		defer s.Close()
   369  
   370  		// Copy the secret key to the new instance
   371  		if err := comm.Upload(path.Join(confDir, secretKey), s); err != nil {
   372  			return fmt.Errorf("Uploading %s failed: %v", secretKey, err)
   373  		}
   374  	}
   375  
   376  	// Make strings.Join available for use within the template
   377  	funcMap := template.FuncMap{
   378  		"join": strings.Join,
   379  	}
   380  
   381  	// Create a new template and parse the client config into it
   382  	t := template.Must(template.New(clienrb).Funcs(funcMap).Parse(clientConf))
   383  
   384  	var buf bytes.Buffer
   385  	err = t.Execute(&buf, p)
   386  	if err != nil {
   387  		return fmt.Errorf("Error executing %s template: %s", clienrb, err)
   388  	}
   389  
   390  	// Copy the client config to the new instance
   391  	if err := comm.Upload(path.Join(confDir, clienrb), &buf); err != nil {
   392  		return fmt.Errorf("Uploading %s failed: %v", clienrb, err)
   393  	}
   394  
   395  	// Create a map with first boot settings
   396  	fb := make(map[string]interface{})
   397  	if p.Attributes != nil {
   398  		fb = p.Attributes.(map[string]interface{})
   399  	}
   400  
   401  	// Check if the run_list was also in the attributes and if so log a warning
   402  	// that it will be overwritten with the value of the run_list argument.
   403  	if _, found := fb["run_list"]; found {
   404  		log.Printf("[WARNING] Found a 'run_list' specified in the configured attributes! " +
   405  			"This value will be overwritten by the value of the `run_list` argument!")
   406  	}
   407  
   408  	// Add the initial runlist to the first boot settings
   409  	fb["run_list"] = p.RunList
   410  
   411  	// Marshal the first boot settings to JSON
   412  	d, err := json.Marshal(fb)
   413  	if err != nil {
   414  		return fmt.Errorf("Failed to create %s data: %s", firstBoot, err)
   415  	}
   416  
   417  	// Copy the first-boot.json to the new instance
   418  	if err := comm.Upload(path.Join(confDir, firstBoot), bytes.NewReader(d)); err != nil {
   419  		return fmt.Errorf("Uploading %s failed: %v", firstBoot, err)
   420  	}
   421  
   422  	return nil
   423  }
   424  
   425  func (p *Provisioner) deployOhaiHints(
   426  	o terraform.UIOutput,
   427  	comm communicator.Communicator,
   428  	hintDir string) error {
   429  	for _, hint := range p.OhaiHints {
   430  		// Open the hint file
   431  		f, err := os.Open(hint)
   432  		if err != nil {
   433  			return err
   434  		}
   435  		defer f.Close()
   436  
   437  		// Copy the hint to the new instance
   438  		if err := comm.Upload(path.Join(hintDir, path.Base(hint)), f); err != nil {
   439  			return fmt.Errorf("Uploading %s failed: %v", path.Base(hint), err)
   440  		}
   441  	}
   442  
   443  	return nil
   444  }
   445  
   446  // runCommand is used to run already prepared commands
   447  func (p *Provisioner) runCommand(
   448  	o terraform.UIOutput,
   449  	comm communicator.Communicator,
   450  	command string) error {
   451  	var err error
   452  
   453  	// Unless prevented, prefix the command with sudo
   454  	if p.useSudo {
   455  		command = "sudo " + command
   456  	}
   457  
   458  	outR, outW := io.Pipe()
   459  	errR, errW := io.Pipe()
   460  	outDoneCh := make(chan struct{})
   461  	errDoneCh := make(chan struct{})
   462  	go p.copyOutput(o, outR, outDoneCh)
   463  	go p.copyOutput(o, errR, errDoneCh)
   464  
   465  	cmd := &remote.Cmd{
   466  		Command: command,
   467  		Stdout:  outW,
   468  		Stderr:  errW,
   469  	}
   470  
   471  	if err := comm.Start(cmd); err != nil {
   472  		return fmt.Errorf("Error executing command %q: %v", cmd.Command, err)
   473  	}
   474  
   475  	cmd.Wait()
   476  	if cmd.ExitStatus != 0 {
   477  		err = fmt.Errorf(
   478  			"Command %q exited with non-zero exit status: %d", cmd.Command, cmd.ExitStatus)
   479  	}
   480  
   481  	// Wait for output to clean up
   482  	outW.Close()
   483  	errW.Close()
   484  	<-outDoneCh
   485  	<-errDoneCh
   486  
   487  	// If we have an error, return it out now that we've cleaned up
   488  	if err != nil {
   489  		return err
   490  	}
   491  
   492  	return nil
   493  }
   494  
   495  func (p *Provisioner) copyOutput(o terraform.UIOutput, r io.Reader, doneCh chan<- struct{}) {
   496  	defer close(doneCh)
   497  	lr := linereader.New(r)
   498  	for line := range lr.Ch {
   499  		o.Output(line)
   500  	}
   501  }