github.com/aclaygray/packer@v1.3.2/provisioner/chef-solo/provisioner.go (about)

     1  // This package implements a provisioner for Packer that uses
     2  // Chef to provision the remote machine, specifically with chef-solo (that is,
     3  // without a Chef server).
     4  package chefsolo
     5  
     6  import (
     7  	"bytes"
     8  	"encoding/json"
     9  	"fmt"
    10  	"io/ioutil"
    11  	"os"
    12  	"path/filepath"
    13  	"strings"
    14  
    15  	"github.com/hashicorp/packer/common"
    16  	"github.com/hashicorp/packer/helper/config"
    17  	"github.com/hashicorp/packer/packer"
    18  	"github.com/hashicorp/packer/provisioner"
    19  	"github.com/hashicorp/packer/template/interpolate"
    20  )
    21  
    22  type guestOSTypeConfig struct {
    23  	executeCommand string
    24  	installCommand string
    25  	stagingDir     string
    26  }
    27  
    28  var guestOSTypeConfigs = map[string]guestOSTypeConfig{
    29  	provisioner.UnixOSType: {
    30  		executeCommand: "{{if .Sudo}}sudo {{end}}chef-solo --no-color -c {{.ConfigPath}} -j {{.JsonPath}}",
    31  		installCommand: "curl -L https://omnitruck.chef.io/install.sh | {{if .Sudo}}sudo {{end}}bash -s --{{if .Version}} -v {{.Version}}{{end}}",
    32  		stagingDir:     "/tmp/packer-chef-solo",
    33  	},
    34  	provisioner.WindowsOSType: {
    35  		executeCommand: "c:/opscode/chef/bin/chef-solo.bat --no-color -c {{.ConfigPath}} -j {{.JsonPath}}",
    36  		installCommand: "powershell.exe -Command \". { iwr -useb https://omnitruck.chef.io/install.ps1 } | iex; Install-Project{{if .Version}} -version {{.Version}}{{end}}\"",
    37  		stagingDir:     "C:/Windows/Temp/packer-chef-solo",
    38  	},
    39  }
    40  
    41  type Config struct {
    42  	common.PackerConfig `mapstructure:",squash"`
    43  
    44  	ChefEnvironment            string   `mapstructure:"chef_environment"`
    45  	ConfigTemplate             string   `mapstructure:"config_template"`
    46  	CookbookPaths              []string `mapstructure:"cookbook_paths"`
    47  	RolesPath                  string   `mapstructure:"roles_path"`
    48  	DataBagsPath               string   `mapstructure:"data_bags_path"`
    49  	EncryptedDataBagSecretPath string   `mapstructure:"encrypted_data_bag_secret_path"`
    50  	EnvironmentsPath           string   `mapstructure:"environments_path"`
    51  	ExecuteCommand             string   `mapstructure:"execute_command"`
    52  	InstallCommand             string   `mapstructure:"install_command"`
    53  	RemoteCookbookPaths        []string `mapstructure:"remote_cookbook_paths"`
    54  	Json                       map[string]interface{}
    55  	PreventSudo                bool     `mapstructure:"prevent_sudo"`
    56  	RunList                    []string `mapstructure:"run_list"`
    57  	SkipInstall                bool     `mapstructure:"skip_install"`
    58  	StagingDir                 string   `mapstructure:"staging_directory"`
    59  	GuestOSType                string   `mapstructure:"guest_os_type"`
    60  	Version                    string   `mapstructure:"version"`
    61  
    62  	ctx interpolate.Context
    63  }
    64  
    65  type Provisioner struct {
    66  	config            Config
    67  	guestOSTypeConfig guestOSTypeConfig
    68  	guestCommands     *provisioner.GuestCommands
    69  }
    70  
    71  type ConfigTemplate struct {
    72  	CookbookPaths              string
    73  	DataBagsPath               string
    74  	EncryptedDataBagSecretPath string
    75  	RolesPath                  string
    76  	EnvironmentsPath           string
    77  	ChefEnvironment            string
    78  
    79  	// Templates don't support boolean statements until Go 1.2. In the
    80  	// mean time, we do this.
    81  	// TODO(mitchellh): Remove when Go 1.2 is released
    82  	HasDataBagsPath               bool
    83  	HasEncryptedDataBagSecretPath bool
    84  	HasRolesPath                  bool
    85  	HasEnvironmentsPath           bool
    86  }
    87  
    88  type ExecuteTemplate struct {
    89  	ConfigPath string
    90  	JsonPath   string
    91  	Sudo       bool
    92  }
    93  
    94  type InstallChefTemplate struct {
    95  	Sudo    bool
    96  	Version string
    97  }
    98  
    99  func (p *Provisioner) Prepare(raws ...interface{}) error {
   100  	err := config.Decode(&p.config, &config.DecodeOpts{
   101  		Interpolate:        true,
   102  		InterpolateContext: &p.config.ctx,
   103  		InterpolateFilter: &interpolate.RenderFilter{
   104  			Exclude: []string{
   105  				"execute_command",
   106  				"install_command",
   107  			},
   108  		},
   109  	}, raws...)
   110  	if err != nil {
   111  		return err
   112  	}
   113  
   114  	if p.config.GuestOSType == "" {
   115  		p.config.GuestOSType = provisioner.DefaultOSType
   116  	}
   117  	p.config.GuestOSType = strings.ToLower(p.config.GuestOSType)
   118  
   119  	var ok bool
   120  	p.guestOSTypeConfig, ok = guestOSTypeConfigs[p.config.GuestOSType]
   121  	if !ok {
   122  		return fmt.Errorf("Invalid guest_os_type: \"%s\"", p.config.GuestOSType)
   123  	}
   124  
   125  	p.guestCommands, err = provisioner.NewGuestCommands(p.config.GuestOSType, !p.config.PreventSudo)
   126  	if err != nil {
   127  		return fmt.Errorf("Invalid guest_os_type: \"%s\"", p.config.GuestOSType)
   128  	}
   129  
   130  	if p.config.ExecuteCommand == "" {
   131  		p.config.ExecuteCommand = p.guestOSTypeConfig.executeCommand
   132  	}
   133  
   134  	if p.config.InstallCommand == "" {
   135  		p.config.InstallCommand = p.guestOSTypeConfig.installCommand
   136  	}
   137  
   138  	if p.config.RunList == nil {
   139  		p.config.RunList = make([]string, 0)
   140  	}
   141  
   142  	if p.config.StagingDir == "" {
   143  		p.config.StagingDir = p.guestOSTypeConfig.stagingDir
   144  	}
   145  
   146  	var errs *packer.MultiError
   147  	if p.config.ConfigTemplate != "" {
   148  		fi, err := os.Stat(p.config.ConfigTemplate)
   149  		if err != nil {
   150  			errs = packer.MultiErrorAppend(
   151  				errs, fmt.Errorf("Bad config template path: %s", err))
   152  		} else if fi.IsDir() {
   153  			errs = packer.MultiErrorAppend(
   154  				errs, fmt.Errorf("Config template path must be a file: %s", err))
   155  		}
   156  	}
   157  
   158  	for _, path := range p.config.CookbookPaths {
   159  		pFileInfo, err := os.Stat(path)
   160  
   161  		if err != nil || !pFileInfo.IsDir() {
   162  			errs = packer.MultiErrorAppend(
   163  				errs, fmt.Errorf("Bad cookbook path '%s': %s", path, err))
   164  		}
   165  	}
   166  
   167  	if p.config.RolesPath != "" {
   168  		pFileInfo, err := os.Stat(p.config.RolesPath)
   169  
   170  		if err != nil || !pFileInfo.IsDir() {
   171  			errs = packer.MultiErrorAppend(
   172  				errs, fmt.Errorf("Bad roles path '%s': %s", p.config.RolesPath, err))
   173  		}
   174  	}
   175  
   176  	if p.config.DataBagsPath != "" {
   177  		pFileInfo, err := os.Stat(p.config.DataBagsPath)
   178  
   179  		if err != nil || !pFileInfo.IsDir() {
   180  			errs = packer.MultiErrorAppend(
   181  				errs, fmt.Errorf("Bad data bags path '%s': %s", p.config.DataBagsPath, err))
   182  		}
   183  	}
   184  
   185  	if p.config.EncryptedDataBagSecretPath != "" {
   186  		pFileInfo, err := os.Stat(p.config.EncryptedDataBagSecretPath)
   187  
   188  		if err != nil || pFileInfo.IsDir() {
   189  			errs = packer.MultiErrorAppend(
   190  				errs, fmt.Errorf("Bad encrypted data bag secret '%s': %s", p.config.EncryptedDataBagSecretPath, err))
   191  		}
   192  	}
   193  
   194  	if p.config.EnvironmentsPath != "" {
   195  		pFileInfo, err := os.Stat(p.config.EnvironmentsPath)
   196  
   197  		if err != nil || !pFileInfo.IsDir() {
   198  			errs = packer.MultiErrorAppend(
   199  				errs, fmt.Errorf("Bad environments path '%s': %s", p.config.EnvironmentsPath, err))
   200  		}
   201  	}
   202  
   203  	jsonValid := true
   204  	for k, v := range p.config.Json {
   205  		p.config.Json[k], err = p.deepJsonFix(k, v)
   206  		if err != nil {
   207  			errs = packer.MultiErrorAppend(
   208  				errs, fmt.Errorf("Error processing JSON: %s", err))
   209  			jsonValid = false
   210  		}
   211  	}
   212  
   213  	if jsonValid {
   214  		// Process the user variables within the JSON and set the JSON.
   215  		// Do this early so that we can validate and show errors.
   216  		p.config.Json, err = p.processJsonUserVars()
   217  		if err != nil {
   218  			errs = packer.MultiErrorAppend(
   219  				errs, fmt.Errorf("Error processing user variables in JSON: %s", err))
   220  		}
   221  	}
   222  
   223  	if errs != nil && len(errs.Errors) > 0 {
   224  		return errs
   225  	}
   226  
   227  	return nil
   228  }
   229  
   230  func (p *Provisioner) Provision(ui packer.Ui, comm packer.Communicator) error {
   231  	ui.Say("Provisioning with chef-solo")
   232  
   233  	if !p.config.SkipInstall {
   234  		if err := p.installChef(ui, comm, p.config.Version); err != nil {
   235  			return fmt.Errorf("Error installing Chef: %s", err)
   236  		}
   237  	}
   238  
   239  	if err := p.createDir(ui, comm, p.config.StagingDir); err != nil {
   240  		return fmt.Errorf("Error creating staging directory: %s", err)
   241  	}
   242  
   243  	cookbookPaths := make([]string, 0, len(p.config.CookbookPaths))
   244  	for i, path := range p.config.CookbookPaths {
   245  		targetPath := fmt.Sprintf("%s/cookbooks-%d", p.config.StagingDir, i)
   246  		if err := p.uploadDirectory(ui, comm, targetPath, path); err != nil {
   247  			return fmt.Errorf("Error uploading cookbooks: %s", err)
   248  		}
   249  
   250  		cookbookPaths = append(cookbookPaths, targetPath)
   251  	}
   252  
   253  	rolesPath := ""
   254  	if p.config.RolesPath != "" {
   255  		rolesPath = fmt.Sprintf("%s/roles", p.config.StagingDir)
   256  		if err := p.uploadDirectory(ui, comm, rolesPath, p.config.RolesPath); err != nil {
   257  			return fmt.Errorf("Error uploading roles: %s", err)
   258  		}
   259  	}
   260  
   261  	dataBagsPath := ""
   262  	if p.config.DataBagsPath != "" {
   263  		dataBagsPath = fmt.Sprintf("%s/data_bags", p.config.StagingDir)
   264  		if err := p.uploadDirectory(ui, comm, dataBagsPath, p.config.DataBagsPath); err != nil {
   265  			return fmt.Errorf("Error uploading data bags: %s", err)
   266  		}
   267  	}
   268  
   269  	encryptedDataBagSecretPath := ""
   270  	if p.config.EncryptedDataBagSecretPath != "" {
   271  		encryptedDataBagSecretPath = fmt.Sprintf("%s/encrypted_data_bag_secret", p.config.StagingDir)
   272  		if err := p.uploadFile(ui, comm, encryptedDataBagSecretPath, p.config.EncryptedDataBagSecretPath); err != nil {
   273  			return fmt.Errorf("Error uploading encrypted data bag secret: %s", err)
   274  		}
   275  	}
   276  
   277  	environmentsPath := ""
   278  	if p.config.EnvironmentsPath != "" {
   279  		environmentsPath = fmt.Sprintf("%s/environments", p.config.StagingDir)
   280  		if err := p.uploadDirectory(ui, comm, environmentsPath, p.config.EnvironmentsPath); err != nil {
   281  			return fmt.Errorf("Error uploading environments: %s", err)
   282  		}
   283  	}
   284  
   285  	configPath, err := p.createConfig(ui, comm, cookbookPaths, rolesPath, dataBagsPath, encryptedDataBagSecretPath, environmentsPath, p.config.ChefEnvironment)
   286  	if err != nil {
   287  		return fmt.Errorf("Error creating Chef config file: %s", err)
   288  	}
   289  
   290  	jsonPath, err := p.createJson(ui, comm)
   291  	if err != nil {
   292  		return fmt.Errorf("Error creating JSON attributes: %s", err)
   293  	}
   294  
   295  	if err := p.executeChef(ui, comm, configPath, jsonPath); err != nil {
   296  		return fmt.Errorf("Error executing Chef: %s", err)
   297  	}
   298  
   299  	return nil
   300  }
   301  
   302  func (p *Provisioner) Cancel() {
   303  	// Just hard quit. It isn't a big deal if what we're doing keeps
   304  	// running on the other side.
   305  	os.Exit(0)
   306  }
   307  
   308  func (p *Provisioner) uploadDirectory(ui packer.Ui, comm packer.Communicator, dst string, src string) error {
   309  	if err := p.createDir(ui, comm, dst); err != nil {
   310  		return err
   311  	}
   312  
   313  	// Make sure there is a trailing "/" so that the directory isn't
   314  	// created on the other side.
   315  	if src[len(src)-1] != '/' {
   316  		src = src + "/"
   317  	}
   318  
   319  	return comm.UploadDir(dst, src, nil)
   320  }
   321  
   322  func (p *Provisioner) uploadFile(ui packer.Ui, comm packer.Communicator, dst string, src string) error {
   323  	f, err := os.Open(src)
   324  	if err != nil {
   325  		return err
   326  	}
   327  	defer f.Close()
   328  
   329  	return comm.Upload(dst, f, nil)
   330  }
   331  
   332  func (p *Provisioner) createConfig(ui packer.Ui, comm packer.Communicator, localCookbooks []string, rolesPath string, dataBagsPath string, encryptedDataBagSecretPath string, environmentsPath string, chefEnvironment string) (string, error) {
   333  	ui.Message("Creating configuration file 'solo.rb'")
   334  
   335  	cookbook_paths := make([]string, len(p.config.RemoteCookbookPaths)+len(localCookbooks))
   336  	for i, path := range p.config.RemoteCookbookPaths {
   337  		cookbook_paths[i] = fmt.Sprintf(`"%s"`, path)
   338  	}
   339  
   340  	for i, path := range localCookbooks {
   341  		i = len(p.config.RemoteCookbookPaths) + i
   342  		cookbook_paths[i] = fmt.Sprintf(`"%s"`, path)
   343  	}
   344  
   345  	// Read the template
   346  	tpl := DefaultConfigTemplate
   347  	if p.config.ConfigTemplate != "" {
   348  		f, err := os.Open(p.config.ConfigTemplate)
   349  		if err != nil {
   350  			return "", err
   351  		}
   352  		defer f.Close()
   353  
   354  		tplBytes, err := ioutil.ReadAll(f)
   355  		if err != nil {
   356  			return "", err
   357  		}
   358  
   359  		tpl = string(tplBytes)
   360  	}
   361  
   362  	p.config.ctx.Data = &ConfigTemplate{
   363  		CookbookPaths:                 strings.Join(cookbook_paths, ","),
   364  		RolesPath:                     rolesPath,
   365  		DataBagsPath:                  dataBagsPath,
   366  		EncryptedDataBagSecretPath:    encryptedDataBagSecretPath,
   367  		EnvironmentsPath:              environmentsPath,
   368  		HasRolesPath:                  rolesPath != "",
   369  		HasDataBagsPath:               dataBagsPath != "",
   370  		HasEncryptedDataBagSecretPath: encryptedDataBagSecretPath != "",
   371  		HasEnvironmentsPath:           environmentsPath != "",
   372  		ChefEnvironment:               chefEnvironment,
   373  	}
   374  	configString, err := interpolate.Render(tpl, &p.config.ctx)
   375  	if err != nil {
   376  		return "", err
   377  	}
   378  
   379  	remotePath := filepath.ToSlash(filepath.Join(p.config.StagingDir, "solo.rb"))
   380  	if err := comm.Upload(remotePath, bytes.NewReader([]byte(configString)), nil); err != nil {
   381  		return "", err
   382  	}
   383  
   384  	return remotePath, nil
   385  }
   386  
   387  func (p *Provisioner) createJson(ui packer.Ui, comm packer.Communicator) (string, error) {
   388  	ui.Message("Creating JSON attribute file")
   389  
   390  	jsonData := make(map[string]interface{})
   391  
   392  	// Copy the configured JSON
   393  	for k, v := range p.config.Json {
   394  		jsonData[k] = v
   395  	}
   396  
   397  	// Set the run list if it was specified
   398  	if len(p.config.RunList) > 0 {
   399  		jsonData["run_list"] = p.config.RunList
   400  	}
   401  
   402  	jsonBytes, err := json.MarshalIndent(jsonData, "", "  ")
   403  	if err != nil {
   404  		return "", err
   405  	}
   406  
   407  	// Upload the bytes
   408  	remotePath := filepath.ToSlash(filepath.Join(p.config.StagingDir, "node.json"))
   409  	if err := comm.Upload(remotePath, bytes.NewReader(jsonBytes), nil); err != nil {
   410  		return "", err
   411  	}
   412  
   413  	return remotePath, nil
   414  }
   415  
   416  func (p *Provisioner) createDir(ui packer.Ui, comm packer.Communicator, dir string) error {
   417  	ui.Message(fmt.Sprintf("Creating directory: %s", dir))
   418  
   419  	cmd := &packer.RemoteCmd{Command: p.guestCommands.CreateDir(dir)}
   420  	if err := cmd.StartWithUi(comm, ui); err != nil {
   421  		return err
   422  	}
   423  	if cmd.ExitStatus != 0 {
   424  		return fmt.Errorf("Non-zero exit status. See output above for more info.")
   425  	}
   426  
   427  	// Chmod the directory to 0777 just so that we can access it as our user
   428  	cmd = &packer.RemoteCmd{Command: p.guestCommands.Chmod(dir, "0777")}
   429  	if err := cmd.StartWithUi(comm, ui); err != nil {
   430  		return err
   431  	}
   432  	if cmd.ExitStatus != 0 {
   433  		return fmt.Errorf("Non-zero exit status. See output above for more info.")
   434  	}
   435  
   436  	return nil
   437  }
   438  
   439  func (p *Provisioner) executeChef(ui packer.Ui, comm packer.Communicator, config string, json string) error {
   440  	p.config.ctx.Data = &ExecuteTemplate{
   441  		ConfigPath: config,
   442  		JsonPath:   json,
   443  		Sudo:       !p.config.PreventSudo,
   444  	}
   445  	command, err := interpolate.Render(p.config.ExecuteCommand, &p.config.ctx)
   446  	if err != nil {
   447  		return err
   448  	}
   449  
   450  	ui.Message(fmt.Sprintf("Executing Chef: %s", command))
   451  
   452  	cmd := &packer.RemoteCmd{
   453  		Command: command,
   454  	}
   455  
   456  	if err := cmd.StartWithUi(comm, ui); err != nil {
   457  		return err
   458  	}
   459  
   460  	if cmd.ExitStatus != 0 {
   461  		return fmt.Errorf("Non-zero exit status: %d", cmd.ExitStatus)
   462  	}
   463  
   464  	return nil
   465  }
   466  
   467  func (p *Provisioner) installChef(ui packer.Ui, comm packer.Communicator, version string) error {
   468  	ui.Message("Installing Chef...")
   469  
   470  	p.config.ctx.Data = &InstallChefTemplate{
   471  		Sudo:    !p.config.PreventSudo,
   472  		Version: version,
   473  	}
   474  	command, err := interpolate.Render(p.config.InstallCommand, &p.config.ctx)
   475  	if err != nil {
   476  		return err
   477  	}
   478  
   479  	cmd := &packer.RemoteCmd{Command: command}
   480  	if err := cmd.StartWithUi(comm, ui); err != nil {
   481  		return err
   482  	}
   483  
   484  	if cmd.ExitStatus != 0 {
   485  		return fmt.Errorf(
   486  			"Install script exited with non-zero exit status %d", cmd.ExitStatus)
   487  	}
   488  
   489  	return nil
   490  }
   491  
   492  func (p *Provisioner) deepJsonFix(key string, current interface{}) (interface{}, error) {
   493  	if current == nil {
   494  		return nil, nil
   495  	}
   496  
   497  	switch c := current.(type) {
   498  	case []interface{}:
   499  		val := make([]interface{}, len(c))
   500  		for i, v := range c {
   501  			var err error
   502  			val[i], err = p.deepJsonFix(fmt.Sprintf("%s[%d]", key, i), v)
   503  			if err != nil {
   504  				return nil, err
   505  			}
   506  		}
   507  
   508  		return val, nil
   509  	case []uint8:
   510  		return string(c), nil
   511  	case map[interface{}]interface{}:
   512  		val := make(map[string]interface{})
   513  		for k, v := range c {
   514  			ks, ok := k.(string)
   515  			if !ok {
   516  				return nil, fmt.Errorf("%s: key is not string", key)
   517  			}
   518  
   519  			var err error
   520  			val[ks], err = p.deepJsonFix(
   521  				fmt.Sprintf("%s.%s", key, ks), v)
   522  			if err != nil {
   523  				return nil, err
   524  			}
   525  		}
   526  
   527  		return val, nil
   528  	default:
   529  		return current, nil
   530  	}
   531  }
   532  
   533  func (p *Provisioner) processJsonUserVars() (map[string]interface{}, error) {
   534  	jsonBytes, err := json.Marshal(p.config.Json)
   535  	if err != nil {
   536  		// This really shouldn't happen since we literally just unmarshalled
   537  		panic(err)
   538  	}
   539  
   540  	// Copy the user variables so that we can restore them later, and
   541  	// make sure we make the quotes JSON-friendly in the user variables.
   542  	originalUserVars := make(map[string]string)
   543  	for k, v := range p.config.ctx.UserVariables {
   544  		originalUserVars[k] = v
   545  	}
   546  
   547  	// Make sure we reset them no matter what
   548  	defer func() {
   549  		p.config.ctx.UserVariables = originalUserVars
   550  	}()
   551  
   552  	// Make the current user variables JSON string safe.
   553  	for k, v := range p.config.ctx.UserVariables {
   554  		v = strings.Replace(v, `\`, `\\`, -1)
   555  		v = strings.Replace(v, `"`, `\"`, -1)
   556  		p.config.ctx.UserVariables[k] = v
   557  	}
   558  
   559  	// Process the bytes with the template processor
   560  	p.config.ctx.Data = nil
   561  	jsonBytesProcessed, err := interpolate.Render(string(jsonBytes), &p.config.ctx)
   562  	if err != nil {
   563  		return nil, err
   564  	}
   565  
   566  	var result map[string]interface{}
   567  	if err := json.Unmarshal([]byte(jsonBytesProcessed), &result); err != nil {
   568  		return nil, err
   569  	}
   570  
   571  	return result, nil
   572  }
   573  
   574  var DefaultConfigTemplate = `
   575  cookbook_path 	[{{.CookbookPaths}}]
   576  {{if .HasRolesPath}}
   577  role_path		"{{.RolesPath}}"
   578  {{end}}
   579  {{if .HasDataBagsPath}}
   580  data_bag_path	"{{.DataBagsPath}}"
   581  {{end}}
   582  {{if .HasEncryptedDataBagSecretPath}}
   583  encrypted_data_bag_secret "{{.EncryptedDataBagSecretPath}}"
   584  {{end}}
   585  {{if .HasEnvironmentsPath}}
   586  environment_path "{{.EnvironmentsPath}}"
   587  environment "{{.ChefEnvironment}}"
   588  {{end}}
   589  `