github.phpd.cn/hashicorp/packer@v1.3.2/provisioner/converge/provisioner.go (about)

     1  // This package implements a provisioner for Packer that executes
     2  // Converge to provision a remote machine
     3  
     4  package converge
     5  
     6  import (
     7  	"bytes"
     8  	"errors"
     9  	"fmt"
    10  
    11  	"strings"
    12  
    13  	"encoding/json"
    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/template/interpolate"
    19  )
    20  
    21  // Config for Converge provisioner
    22  type Config struct {
    23  	common.PackerConfig `mapstructure:",squash"`
    24  
    25  	// Bootstrapping
    26  	Bootstrap            bool   `mapstructure:"bootstrap"`
    27  	Version              string `mapstructure:"version"`
    28  	BootstrapCommand     string `mapstructure:"bootstrap_command"`
    29  	PreventBootstrapSudo bool   `mapstructure:"prevent_bootstrap_sudo"`
    30  
    31  	// Modules
    32  	ModuleDirs []ModuleDir `mapstructure:"module_dirs"`
    33  
    34  	// Execution
    35  	Module           string            `mapstructure:"module"`
    36  	WorkingDirectory string            `mapstructure:"working_directory"`
    37  	Params           map[string]string `mapstructure:"params"`
    38  	ExecuteCommand   string            `mapstructure:"execute_command"`
    39  	PreventSudo      bool              `mapstructure:"prevent_sudo"`
    40  
    41  	ctx interpolate.Context
    42  }
    43  
    44  // ModuleDir is a directory to transfer to the remote system
    45  type ModuleDir struct {
    46  	Source      string   `mapstructure:"source"`
    47  	Destination string   `mapstructure:"destination"`
    48  	Exclude     []string `mapstructure:"exclude"`
    49  }
    50  
    51  // Provisioner for Converge
    52  type Provisioner struct {
    53  	config Config
    54  }
    55  
    56  // Prepare provisioner somehow. TODO: actual docs
    57  func (p *Provisioner) Prepare(raws ...interface{}) error {
    58  	err := config.Decode(
    59  		&p.config,
    60  		&config.DecodeOpts{
    61  			Interpolate:        true,
    62  			InterpolateContext: &p.config.ctx,
    63  			InterpolateFilter: &interpolate.RenderFilter{
    64  				Exclude: []string{
    65  					"execute_command",
    66  					"bootstrap_command",
    67  				},
    68  			},
    69  		},
    70  		raws...,
    71  	)
    72  	if err != nil {
    73  		return err
    74  	}
    75  
    76  	// require a single module
    77  	if p.config.Module == "" {
    78  		return errors.New("Converge requires a module to provision the system")
    79  	}
    80  
    81  	// set defaults
    82  	if p.config.WorkingDirectory == "" {
    83  		p.config.WorkingDirectory = "/tmp"
    84  	}
    85  
    86  	if p.config.ExecuteCommand == "" {
    87  		p.config.ExecuteCommand = "cd {{.WorkingDirectory}} && {{if .Sudo}}sudo {{end}}converge apply --local --log-level=WARNING --paramsJSON '{{.ParamsJSON}}' {{.Module}}"
    88  	}
    89  
    90  	if p.config.BootstrapCommand == "" {
    91  		p.config.BootstrapCommand = "curl -s https://get.converge.sh | {{if .Sudo}}sudo {{end}}sh {{if ne .Version \"\"}}-s -- -v {{.Version}}{{end}}"
    92  	}
    93  
    94  	// validate sources and destinations
    95  	for i, dir := range p.config.ModuleDirs {
    96  		if dir.Source == "" {
    97  			return fmt.Errorf("Source (\"source\" key) is required in Converge module dir #%d", i)
    98  		}
    99  		if dir.Destination == "" {
   100  			return fmt.Errorf("Destination (\"destination\" key) is required in Converge module dir #%d", i)
   101  		}
   102  	}
   103  
   104  	return err
   105  }
   106  
   107  // Provision node somehow. TODO: actual docs
   108  func (p *Provisioner) Provision(ui packer.Ui, comm packer.Communicator) error {
   109  	ui.Say("Provisioning with Converge")
   110  
   111  	// bootstrapping
   112  	if err := p.maybeBootstrap(ui, comm); err != nil {
   113  		return err // error messages are already user-friendly
   114  	}
   115  
   116  	// send module directories to the remote host
   117  	if err := p.sendModuleDirectories(ui, comm); err != nil {
   118  		return err // error messages are already user-friendly
   119  	}
   120  
   121  	// apply all the modules
   122  	if err := p.applyModules(ui, comm); err != nil {
   123  		return err // error messages are already user-friendly
   124  	}
   125  
   126  	return nil
   127  }
   128  
   129  func (p *Provisioner) maybeBootstrap(ui packer.Ui, comm packer.Communicator) error {
   130  	if !p.config.Bootstrap {
   131  		return nil
   132  	}
   133  	ui.Message("bootstrapping converge")
   134  
   135  	p.config.ctx.Data = struct {
   136  		Version string
   137  		Sudo    bool
   138  	}{
   139  		Version: p.config.Version,
   140  		Sudo:    !p.config.PreventBootstrapSudo,
   141  	}
   142  	command, err := interpolate.Render(p.config.BootstrapCommand, &p.config.ctx)
   143  	if err != nil {
   144  		return fmt.Errorf("Could not interpolate bootstrap command: %s", err)
   145  	}
   146  
   147  	var out, outErr bytes.Buffer
   148  	cmd := &packer.RemoteCmd{
   149  		Command: command,
   150  		Stdin:   nil,
   151  		Stdout:  &out,
   152  		Stderr:  &outErr,
   153  	}
   154  
   155  	if err = comm.Start(cmd); err != nil {
   156  		return fmt.Errorf("Error bootstrapping converge: %s", err)
   157  	}
   158  
   159  	cmd.Wait()
   160  	if cmd.ExitStatus != 0 {
   161  		ui.Error(out.String())
   162  		ui.Error(outErr.String())
   163  		return errors.New("Error bootstrapping converge")
   164  	}
   165  
   166  	ui.Message(strings.TrimSpace(out.String()))
   167  	return nil
   168  }
   169  
   170  func (p *Provisioner) sendModuleDirectories(ui packer.Ui, comm packer.Communicator) error {
   171  	for _, dir := range p.config.ModuleDirs {
   172  		if err := comm.UploadDir(dir.Destination, dir.Source, dir.Exclude); err != nil {
   173  			return fmt.Errorf("Could not upload %q: %s", dir.Source, err)
   174  		}
   175  		ui.Message(fmt.Sprintf("transferred %q to %q", dir.Source, dir.Destination))
   176  	}
   177  
   178  	return nil
   179  }
   180  
   181  func (p *Provisioner) applyModules(ui packer.Ui, comm packer.Communicator) error {
   182  	// create params JSON file
   183  	params, err := json.Marshal(p.config.Params)
   184  	if err != nil {
   185  		return fmt.Errorf("Could not marshal parameters as JSON: %s", err)
   186  	}
   187  
   188  	p.config.ctx.Data = struct {
   189  		ParamsJSON, WorkingDirectory, Module string
   190  		Sudo                                 bool
   191  	}{
   192  		ParamsJSON:       string(params),
   193  		WorkingDirectory: p.config.WorkingDirectory,
   194  		Module:           p.config.Module,
   195  		Sudo:             !p.config.PreventSudo,
   196  	}
   197  	command, err := interpolate.Render(p.config.ExecuteCommand, &p.config.ctx)
   198  	if err != nil {
   199  		return fmt.Errorf("Could not interpolate execute command: %s", err)
   200  	}
   201  
   202  	// run Converge in the specified directory
   203  	var runOut, runErr bytes.Buffer
   204  	cmd := &packer.RemoteCmd{
   205  		Command: command,
   206  		Stdin:   nil,
   207  		Stdout:  &runOut,
   208  		Stderr:  &runErr,
   209  	}
   210  	if err := comm.Start(cmd); err != nil {
   211  		return fmt.Errorf("Error applying %q: %s", p.config.Module, err)
   212  	}
   213  
   214  	cmd.Wait()
   215  	if cmd.ExitStatus == 127 {
   216  		ui.Error("Could not find Converge. Is it installed and in PATH?")
   217  		if !p.config.Bootstrap {
   218  			ui.Error("Bootstrapping was disabled for this run. That might be why Converge isn't present.")
   219  		}
   220  
   221  		return errors.New("Could not find Converge")
   222  
   223  	} else if cmd.ExitStatus != 0 {
   224  		ui.Error(strings.TrimSpace(runOut.String()))
   225  		ui.Error(strings.TrimSpace(runErr.String()))
   226  		ui.Error(fmt.Sprintf("Exited with error code %d.", cmd.ExitStatus))
   227  		return fmt.Errorf("Error applying %q", p.config.Module)
   228  	}
   229  
   230  	ui.Message(strings.TrimSpace(runOut.String()))
   231  
   232  	return nil
   233  }
   234  
   235  // Cancel the provisioning process
   236  func (p *Provisioner) Cancel() {
   237  	// there's not an awful lot we can do to cancel Converge at the moment.
   238  	// The default semantics are fine.
   239  }