github.com/tompao/terraform@v0.6.10-0.20180215233341-e41b29d0961b/builtin/provisioners/salt-masterless/resource_provisioner.go (about)

     1  // This package implements a provisioner for Terraform that executes a
     2  // saltstack state within the remote machine
     3  //
     4  // Adapted from gitub.com/hashicorp/packer/provisioner/salt-masterless
     5  
     6  package saltmasterless
     7  
     8  import (
     9  	"bytes"
    10  	"context"
    11  	"errors"
    12  	"fmt"
    13  	"io"
    14  	"os"
    15  	"path/filepath"
    16  
    17  	"github.com/hashicorp/terraform/communicator"
    18  	"github.com/hashicorp/terraform/communicator/remote"
    19  	"github.com/hashicorp/terraform/helper/schema"
    20  	"github.com/hashicorp/terraform/terraform"
    21  	linereader "github.com/mitchellh/go-linereader"
    22  )
    23  
    24  type provisionFn func(terraform.UIOutput, communicator.Communicator) error
    25  
    26  type provisioner struct {
    27  	SkipBootstrap     bool
    28  	BootstrapArgs     string
    29  	LocalStateTree    string
    30  	DisableSudo       bool
    31  	CustomState       string
    32  	MinionConfig      string
    33  	LocalPillarRoots  string
    34  	RemoteStateTree   string
    35  	RemotePillarRoots string
    36  	TempConfigDir     string
    37  	NoExitOnFailure   bool
    38  	LogLevel          string
    39  	SaltCallArgs      string
    40  	CmdArgs           string
    41  }
    42  
    43  const DefaultStateTreeDir = "/srv/salt"
    44  const DefaultPillarRootDir = "/srv/pillar"
    45  
    46  // Provisioner returns a salt-masterless provisioner
    47  func Provisioner() terraform.ResourceProvisioner {
    48  	return &schema.Provisioner{
    49  		Schema: map[string]*schema.Schema{
    50  			"local_state_tree": &schema.Schema{
    51  				Type:     schema.TypeString,
    52  				Required: true,
    53  			},
    54  			"local_pillar_roots": &schema.Schema{
    55  				Type:     schema.TypeString,
    56  				Optional: true,
    57  			},
    58  			"remote_state_tree": &schema.Schema{
    59  				Type:     schema.TypeString,
    60  				Optional: true,
    61  			},
    62  			"remote_pillar_roots": &schema.Schema{
    63  				Type:     schema.TypeString,
    64  				Optional: true,
    65  			},
    66  			"temp_config_dir": &schema.Schema{
    67  				Type:     schema.TypeString,
    68  				Optional: true,
    69  				Default:  "/tmp/salt",
    70  			},
    71  			"skip_bootstrap": &schema.Schema{
    72  				Type:     schema.TypeBool,
    73  				Optional: true,
    74  			},
    75  			"no_exit_on_failure": &schema.Schema{
    76  				Type:     schema.TypeBool,
    77  				Optional: true,
    78  			},
    79  			"bootstrap_args": &schema.Schema{
    80  				Type:     schema.TypeString,
    81  				Optional: true,
    82  			},
    83  			"disable_sudo": &schema.Schema{
    84  				Type:     schema.TypeBool,
    85  				Optional: true,
    86  			},
    87  			"custom_state": &schema.Schema{
    88  				Type:     schema.TypeString,
    89  				Optional: true,
    90  			},
    91  			"minion_config_file": &schema.Schema{
    92  				Type:     schema.TypeString,
    93  				Optional: true,
    94  			},
    95  			"cmd_args": &schema.Schema{
    96  				Type:     schema.TypeString,
    97  				Optional: true,
    98  			},
    99  			"salt_call_args": &schema.Schema{
   100  				Type:     schema.TypeString,
   101  				Optional: true,
   102  			},
   103  			"log_level": &schema.Schema{
   104  				Type:     schema.TypeString,
   105  				Optional: true,
   106  			},
   107  		},
   108  
   109  		ApplyFunc:    applyFn,
   110  		ValidateFunc: validateFn,
   111  	}
   112  }
   113  
   114  // Apply executes the file provisioner
   115  func applyFn(ctx context.Context) error {
   116  	// Decode the raw config for this provisioner
   117  	var err error
   118  
   119  	o := ctx.Value(schema.ProvOutputKey).(terraform.UIOutput)
   120  	d := ctx.Value(schema.ProvConfigDataKey).(*schema.ResourceData)
   121  	connState := ctx.Value(schema.ProvRawStateKey).(*terraform.InstanceState)
   122  
   123  	p, err := decodeConfig(d)
   124  	if err != nil {
   125  		return err
   126  	}
   127  
   128  	// Get a new communicator
   129  	comm, err := communicator.New(connState)
   130  	if err != nil {
   131  		return err
   132  	}
   133  
   134  	var src, dst string
   135  
   136  	o.Output("Provisioning with Salt...")
   137  	if !p.SkipBootstrap {
   138  		cmd := &remote.Cmd{
   139  			// Fallback on wget if curl failed for any reason (such as not being installed)
   140  			Command: fmt.Sprintf("curl -L https://bootstrap.saltstack.com -o /tmp/install_salt.sh || wget -O /tmp/install_salt.sh https://bootstrap.saltstack.com"),
   141  		}
   142  		o.Output(fmt.Sprintf("Downloading saltstack bootstrap to /tmp/install_salt.sh"))
   143  		if err = comm.Start(cmd); err != nil {
   144  			err = fmt.Errorf("Unable to download Salt: %s", err)
   145  		}
   146  
   147  		if err == nil {
   148  			cmd.Wait()
   149  			if cmd.ExitStatus != 0 {
   150  				err = fmt.Errorf("Script exited with non-zero exit status: %d", cmd.ExitStatus)
   151  			}
   152  		}
   153  
   154  		outR, outW := io.Pipe()
   155  		errR, errW := io.Pipe()
   156  		outDoneCh := make(chan struct{})
   157  		errDoneCh := make(chan struct{})
   158  		go copyOutput(o, outR, outDoneCh)
   159  		go copyOutput(o, errR, errDoneCh)
   160  		cmd = &remote.Cmd{
   161  			Command: fmt.Sprintf("%s /tmp/install_salt.sh %s", p.sudo("sh"), p.BootstrapArgs),
   162  			Stdout:  outW,
   163  			Stderr:  errW,
   164  		}
   165  
   166  		o.Output(fmt.Sprintf("Installing Salt with command %s", cmd.Command))
   167  		if err = comm.Start(cmd); err != nil {
   168  			err = fmt.Errorf("Unable to install Salt: %s", err)
   169  		}
   170  
   171  		if err == nil {
   172  			cmd.Wait()
   173  			if cmd.ExitStatus != 0 {
   174  				err = fmt.Errorf("Script exited with non-zero exit status: %d", cmd.ExitStatus)
   175  			}
   176  		}
   177  		// Wait for output to clean up
   178  		outW.Close()
   179  		errW.Close()
   180  		<-outDoneCh
   181  		<-errDoneCh
   182  		if err != nil {
   183  			return err
   184  		}
   185  	}
   186  
   187  	o.Output(fmt.Sprintf("Creating remote temporary directory: %s", p.TempConfigDir))
   188  	if err := p.createDir(o, comm, p.TempConfigDir); err != nil {
   189  		return fmt.Errorf("Error creating remote temporary directory: %s", err)
   190  	}
   191  
   192  	if p.MinionConfig != "" {
   193  		o.Output(fmt.Sprintf("Uploading minion config: %s", p.MinionConfig))
   194  		src = p.MinionConfig
   195  		dst = filepath.ToSlash(filepath.Join(p.TempConfigDir, "minion"))
   196  		if err = p.uploadFile(o, comm, dst, src); err != nil {
   197  			return fmt.Errorf("Error uploading local minion config file to remote: %s", err)
   198  		}
   199  
   200  		// move minion config into /etc/salt
   201  		o.Output(fmt.Sprintf("Make sure directory %s exists", "/etc/salt"))
   202  		if err := p.createDir(o, comm, "/etc/salt"); err != nil {
   203  			return fmt.Errorf("Error creating remote salt configuration directory: %s", err)
   204  		}
   205  		src = filepath.ToSlash(filepath.Join(p.TempConfigDir, "minion"))
   206  		dst = "/etc/salt/minion"
   207  		if err = p.moveFile(o, comm, dst, src); err != nil {
   208  			return fmt.Errorf("Unable to move %s/minion to /etc/salt/minion: %s", p.TempConfigDir, err)
   209  		}
   210  	}
   211  
   212  	o.Output(fmt.Sprintf("Uploading local state tree: %s", p.LocalStateTree))
   213  	src = p.LocalStateTree
   214  	dst = filepath.ToSlash(filepath.Join(p.TempConfigDir, "states"))
   215  	if err = p.uploadDir(o, comm, dst, src, []string{".git"}); err != nil {
   216  		return fmt.Errorf("Error uploading local state tree to remote: %s", err)
   217  	}
   218  
   219  	// move state tree from temporary directory
   220  	src = filepath.ToSlash(filepath.Join(p.TempConfigDir, "states"))
   221  	dst = p.RemoteStateTree
   222  	if err = p.removeDir(o, comm, dst); err != nil {
   223  		return fmt.Errorf("Unable to clear salt tree: %s", err)
   224  	}
   225  	if err = p.moveFile(o, comm, dst, src); err != nil {
   226  		return fmt.Errorf("Unable to move %s/states to %s: %s", p.TempConfigDir, dst, err)
   227  	}
   228  
   229  	if p.LocalPillarRoots != "" {
   230  		o.Output(fmt.Sprintf("Uploading local pillar roots: %s", p.LocalPillarRoots))
   231  		src = p.LocalPillarRoots
   232  		dst = filepath.ToSlash(filepath.Join(p.TempConfigDir, "pillar"))
   233  		if err = p.uploadDir(o, comm, dst, src, []string{".git"}); err != nil {
   234  			return fmt.Errorf("Error uploading local pillar roots to remote: %s", err)
   235  		}
   236  
   237  		// move pillar root from temporary directory
   238  		src = filepath.ToSlash(filepath.Join(p.TempConfigDir, "pillar"))
   239  		dst = p.RemotePillarRoots
   240  
   241  		if err = p.removeDir(o, comm, dst); err != nil {
   242  			return fmt.Errorf("Unable to clear pillar root: %s", err)
   243  		}
   244  		if err = p.moveFile(o, comm, dst, src); err != nil {
   245  			return fmt.Errorf("Unable to move %s/pillar to %s: %s", p.TempConfigDir, dst, err)
   246  		}
   247  	}
   248  
   249  	outR, outW := io.Pipe()
   250  	errR, errW := io.Pipe()
   251  	outDoneCh := make(chan struct{})
   252  	errDoneCh := make(chan struct{})
   253  
   254  	go copyOutput(o, outR, outDoneCh)
   255  	go copyOutput(o, errR, errDoneCh)
   256  	o.Output(fmt.Sprintf("Running: salt-call --local %s", p.CmdArgs))
   257  	cmd := &remote.Cmd{
   258  		Command: p.sudo(fmt.Sprintf("salt-call --local %s", p.CmdArgs)),
   259  		Stdout:  outW,
   260  		Stderr:  errW,
   261  	}
   262  	if err = comm.Start(cmd); err != nil || cmd.ExitStatus != 0 {
   263  		if err == nil {
   264  			err = fmt.Errorf("Bad exit status: %d", cmd.ExitStatus)
   265  		}
   266  
   267  		err = fmt.Errorf("Error executing salt-call: %s", err)
   268  	}
   269  	if err == nil {
   270  		cmd.Wait()
   271  		if cmd.ExitStatus != 0 {
   272  			err = fmt.Errorf("Script exited with non-zero exit status: %d", cmd.ExitStatus)
   273  		}
   274  	}
   275  	// Wait for output to clean up
   276  	outW.Close()
   277  	errW.Close()
   278  	<-outDoneCh
   279  	<-errDoneCh
   280  
   281  	return err
   282  }
   283  
   284  // Prepends sudo to supplied command if config says to
   285  func (p *provisioner) sudo(cmd string) string {
   286  	if p.DisableSudo {
   287  		return cmd
   288  	}
   289  
   290  	return "sudo " + cmd
   291  }
   292  
   293  func validateDirConfig(path string, name string, required bool) error {
   294  	if required == true && path == "" {
   295  		return fmt.Errorf("%s cannot be empty", name)
   296  	} else if required == false && path == "" {
   297  		return nil
   298  	}
   299  	info, err := os.Stat(path)
   300  	if err != nil {
   301  		return fmt.Errorf("%s: path '%s' is invalid: %s", name, path, err)
   302  	} else if !info.IsDir() {
   303  		return fmt.Errorf("%s: path '%s' must point to a directory", name, path)
   304  	}
   305  	return nil
   306  }
   307  
   308  func validateFileConfig(path string, name string, required bool) error {
   309  	if required == true && path == "" {
   310  		return fmt.Errorf("%s cannot be empty", name)
   311  	} else if required == false && path == "" {
   312  		return nil
   313  	}
   314  	info, err := os.Stat(path)
   315  	if err != nil {
   316  		return fmt.Errorf("%s: path '%s' is invalid: %s", name, path, err)
   317  	} else if info.IsDir() {
   318  		return fmt.Errorf("%s: path '%s' must point to a file", name, path)
   319  	}
   320  	return nil
   321  }
   322  
   323  func (p *provisioner) uploadFile(o terraform.UIOutput, comm communicator.Communicator, dst, src string) error {
   324  	f, err := os.Open(src)
   325  	if err != nil {
   326  		return fmt.Errorf("Error opening: %s", err)
   327  	}
   328  	defer f.Close()
   329  
   330  	if err = comm.Upload(dst, f); err != nil {
   331  		return fmt.Errorf("Error uploading %s: %s", src, err)
   332  	}
   333  	return nil
   334  }
   335  
   336  func (p *provisioner) moveFile(o terraform.UIOutput, comm communicator.Communicator, dst, src string) error {
   337  	o.Output(fmt.Sprintf("Moving %s to %s", src, dst))
   338  	cmd := &remote.Cmd{Command: fmt.Sprintf(p.sudo("mv %s %s"), src, dst)}
   339  	if err := comm.Start(cmd); err != nil || cmd.ExitStatus != 0 {
   340  		if err == nil {
   341  			err = fmt.Errorf("Bad exit status: %d", cmd.ExitStatus)
   342  		}
   343  
   344  		return fmt.Errorf("Unable to move %s to %s: %s", src, dst, err)
   345  	}
   346  	return nil
   347  }
   348  
   349  func (p *provisioner) createDir(o terraform.UIOutput, comm communicator.Communicator, dir string) error {
   350  	o.Output(fmt.Sprintf("Creating directory: %s", dir))
   351  	cmd := &remote.Cmd{
   352  		Command: fmt.Sprintf("mkdir -p '%s'", dir),
   353  	}
   354  	if err := comm.Start(cmd); err != nil {
   355  		return err
   356  	}
   357  	if cmd.ExitStatus != 0 {
   358  		return fmt.Errorf("Non-zero exit status.")
   359  	}
   360  	return nil
   361  }
   362  
   363  func (p *provisioner) removeDir(o terraform.UIOutput, comm communicator.Communicator, dir string) error {
   364  	o.Output(fmt.Sprintf("Removing directory: %s", dir))
   365  	cmd := &remote.Cmd{
   366  		Command: fmt.Sprintf("rm -rf '%s'", dir),
   367  	}
   368  	if err := comm.Start(cmd); err != nil {
   369  		return err
   370  	}
   371  	if cmd.ExitStatus != 0 {
   372  		return fmt.Errorf("Non-zero exit status.")
   373  	}
   374  	return nil
   375  }
   376  
   377  func (p *provisioner) uploadDir(o terraform.UIOutput, comm communicator.Communicator, dst, src string, ignore []string) error {
   378  	if err := p.createDir(o, comm, dst); err != nil {
   379  		return err
   380  	}
   381  
   382  	// Make sure there is a trailing "/" so that the directory isn't
   383  	// created on the other side.
   384  	if src[len(src)-1] != '/' {
   385  		src = src + "/"
   386  	}
   387  	return comm.UploadDir(dst, src)
   388  }
   389  
   390  // Validate checks if the required arguments are configured
   391  func validateFn(c *terraform.ResourceConfig) (ws []string, es []error) {
   392  	// require a salt state tree
   393  	localStateTreeTmp, ok := c.Get("local_state_tree")
   394  	var localStateTree string
   395  	if !ok {
   396  		es = append(es,
   397  			errors.New("Required local_state_tree is not set"))
   398  	} else {
   399  		localStateTree = localStateTreeTmp.(string)
   400  	}
   401  	err := validateDirConfig(localStateTree, "local_state_tree", true)
   402  	if err != nil {
   403  		es = append(es, err)
   404  	}
   405  
   406  	var localPillarRoots string
   407  	localPillarRootsTmp, ok := c.Get("local_pillar_roots")
   408  	if !ok {
   409  		localPillarRoots = ""
   410  	} else {
   411  		localPillarRoots = localPillarRootsTmp.(string)
   412  	}
   413  
   414  	err = validateDirConfig(localPillarRoots, "local_pillar_roots", false)
   415  	if err != nil {
   416  		es = append(es, err)
   417  	}
   418  
   419  	var minionConfig string
   420  	minionConfigTmp, ok := c.Get("minion_config_file")
   421  	if !ok {
   422  		minionConfig = ""
   423  	} else {
   424  		minionConfig = minionConfigTmp.(string)
   425  	}
   426  	err = validateFileConfig(minionConfig, "minion_config_file", false)
   427  	if err != nil {
   428  		es = append(es, err)
   429  	}
   430  
   431  	var remoteStateTree string
   432  	remoteStateTreeTmp, ok := c.Get("remote_state_tree")
   433  	if !ok {
   434  		remoteStateTree = ""
   435  	} else {
   436  		remoteStateTree = remoteStateTreeTmp.(string)
   437  	}
   438  
   439  	var remotePillarRoots string
   440  	remotePillarRootsTmp, ok := c.Get("remote_pillar_roots")
   441  	if !ok {
   442  		remotePillarRoots = ""
   443  	} else {
   444  		remotePillarRoots = remotePillarRootsTmp.(string)
   445  	}
   446  
   447  	if minionConfig != "" && (remoteStateTree != "" || remotePillarRoots != "") {
   448  		es = append(es,
   449  			errors.New("remote_state_tree and remote_pillar_roots only apply when minion_config_file is not used"))
   450  	}
   451  
   452  	if len(es) > 0 {
   453  		return ws, es
   454  	}
   455  
   456  	return ws, es
   457  }
   458  
   459  func decodeConfig(d *schema.ResourceData) (*provisioner, error) {
   460  	p := &provisioner{
   461  		LocalStateTree:    d.Get("local_state_tree").(string),
   462  		LogLevel:          d.Get("log_level").(string),
   463  		SaltCallArgs:      d.Get("salt_call_args").(string),
   464  		CmdArgs:           d.Get("cmd_args").(string),
   465  		MinionConfig:      d.Get("minion_config_file").(string),
   466  		CustomState:       d.Get("custom_state").(string),
   467  		DisableSudo:       d.Get("disable_sudo").(bool),
   468  		BootstrapArgs:     d.Get("bootstrap_args").(string),
   469  		NoExitOnFailure:   d.Get("no_exit_on_failure").(bool),
   470  		SkipBootstrap:     d.Get("skip_bootstrap").(bool),
   471  		TempConfigDir:     d.Get("temp_config_dir").(string),
   472  		RemotePillarRoots: d.Get("remote_pillar_roots").(string),
   473  		RemoteStateTree:   d.Get("remote_state_tree").(string),
   474  		LocalPillarRoots:  d.Get("local_pillar_roots").(string),
   475  	}
   476  
   477  	// build the command line args to pass onto salt
   478  	var cmdArgs bytes.Buffer
   479  
   480  	if p.CustomState == "" {
   481  		cmdArgs.WriteString(" state.highstate")
   482  	} else {
   483  		cmdArgs.WriteString(" state.sls ")
   484  		cmdArgs.WriteString(p.CustomState)
   485  	}
   486  
   487  	if p.MinionConfig == "" {
   488  		// pass --file-root and --pillar-root if no minion_config_file is supplied
   489  		if p.RemoteStateTree != "" {
   490  			cmdArgs.WriteString(" --file-root=")
   491  			cmdArgs.WriteString(p.RemoteStateTree)
   492  		} else {
   493  			cmdArgs.WriteString(" --file-root=")
   494  			cmdArgs.WriteString(DefaultStateTreeDir)
   495  		}
   496  		if p.RemotePillarRoots != "" {
   497  			cmdArgs.WriteString(" --pillar-root=")
   498  			cmdArgs.WriteString(p.RemotePillarRoots)
   499  		} else {
   500  			cmdArgs.WriteString(" --pillar-root=")
   501  			cmdArgs.WriteString(DefaultPillarRootDir)
   502  		}
   503  	}
   504  
   505  	if !p.NoExitOnFailure {
   506  		cmdArgs.WriteString(" --retcode-passthrough")
   507  	}
   508  
   509  	if p.LogLevel == "" {
   510  		cmdArgs.WriteString(" -l info")
   511  	} else {
   512  		cmdArgs.WriteString(" -l ")
   513  		cmdArgs.WriteString(p.LogLevel)
   514  	}
   515  
   516  	if p.SaltCallArgs != "" {
   517  		cmdArgs.WriteString(" ")
   518  		cmdArgs.WriteString(p.SaltCallArgs)
   519  	}
   520  
   521  	p.CmdArgs = cmdArgs.String()
   522  
   523  	return p, nil
   524  }
   525  
   526  func copyOutput(
   527  	o terraform.UIOutput, r io.Reader, doneCh chan<- struct{}) {
   528  	defer close(doneCh)
   529  	lr := linereader.New(r)
   530  	for line := range lr.Ch {
   531  		o.Output(line)
   532  	}
   533  }