github.com/rvichery/terraform@v0.11.10/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  	o := ctx.Value(schema.ProvOutputKey).(terraform.UIOutput)
   118  	d := ctx.Value(schema.ProvConfigDataKey).(*schema.ResourceData)
   119  	connState := ctx.Value(schema.ProvRawStateKey).(*terraform.InstanceState)
   120  
   121  	p, err := decodeConfig(d)
   122  	if err != nil {
   123  		return err
   124  	}
   125  
   126  	// Get a new communicator
   127  	comm, err := communicator.New(connState)
   128  	if err != nil {
   129  		return err
   130  	}
   131  
   132  	retryCtx, cancel := context.WithTimeout(ctx, comm.Timeout())
   133  	defer cancel()
   134  
   135  	// Wait and retry until we establish the connection
   136  	err = communicator.Retry(retryCtx, func() error {
   137  		return comm.Connect(o)
   138  	})
   139  
   140  	if err != nil {
   141  		return err
   142  	}
   143  
   144  	// Wait for the context to end and then disconnect
   145  	go func() {
   146  		<-ctx.Done()
   147  		comm.Disconnect()
   148  	}()
   149  
   150  	var src, dst string
   151  
   152  	o.Output("Provisioning with Salt...")
   153  	if !p.SkipBootstrap {
   154  		cmd := &remote.Cmd{
   155  			// Fallback on wget if curl failed for any reason (such as not being installed)
   156  			Command: fmt.Sprintf("curl -L https://bootstrap.saltstack.com -o /tmp/install_salt.sh || wget -O /tmp/install_salt.sh https://bootstrap.saltstack.com"),
   157  		}
   158  		o.Output(fmt.Sprintf("Downloading saltstack bootstrap to /tmp/install_salt.sh"))
   159  		if err = comm.Start(cmd); err != nil {
   160  			err = fmt.Errorf("Unable to download Salt: %s", err)
   161  		}
   162  
   163  		if err := cmd.Wait(); err != nil {
   164  			return err
   165  		}
   166  
   167  		outR, outW := io.Pipe()
   168  		errR, errW := io.Pipe()
   169  		go copyOutput(o, outR)
   170  		go copyOutput(o, errR)
   171  		defer outW.Close()
   172  		defer errW.Close()
   173  
   174  		cmd = &remote.Cmd{
   175  			Command: fmt.Sprintf("%s /tmp/install_salt.sh %s", p.sudo("sh"), p.BootstrapArgs),
   176  			Stdout:  outW,
   177  			Stderr:  errW,
   178  		}
   179  
   180  		o.Output(fmt.Sprintf("Installing Salt with command %s", cmd.Command))
   181  		if err := comm.Start(cmd); err != nil {
   182  			return fmt.Errorf("Unable to install Salt: %s", err)
   183  		}
   184  
   185  		if err := cmd.Wait(); err != nil {
   186  			return err
   187  		}
   188  	}
   189  
   190  	o.Output(fmt.Sprintf("Creating remote temporary directory: %s", p.TempConfigDir))
   191  	if err := p.createDir(o, comm, p.TempConfigDir); err != nil {
   192  		return fmt.Errorf("Error creating remote temporary directory: %s", err)
   193  	}
   194  
   195  	if p.MinionConfig != "" {
   196  		o.Output(fmt.Sprintf("Uploading minion config: %s", p.MinionConfig))
   197  		src = p.MinionConfig
   198  		dst = filepath.ToSlash(filepath.Join(p.TempConfigDir, "minion"))
   199  		if err = p.uploadFile(o, comm, dst, src); err != nil {
   200  			return fmt.Errorf("Error uploading local minion config file to remote: %s", err)
   201  		}
   202  
   203  		// move minion config into /etc/salt
   204  		o.Output(fmt.Sprintf("Make sure directory %s exists", "/etc/salt"))
   205  		if err := p.createDir(o, comm, "/etc/salt"); err != nil {
   206  			return fmt.Errorf("Error creating remote salt configuration directory: %s", err)
   207  		}
   208  		src = filepath.ToSlash(filepath.Join(p.TempConfigDir, "minion"))
   209  		dst = "/etc/salt/minion"
   210  		if err = p.moveFile(o, comm, dst, src); err != nil {
   211  			return fmt.Errorf("Unable to move %s/minion to /etc/salt/minion: %s", p.TempConfigDir, err)
   212  		}
   213  	}
   214  
   215  	o.Output(fmt.Sprintf("Uploading local state tree: %s", p.LocalStateTree))
   216  	src = p.LocalStateTree
   217  	dst = filepath.ToSlash(filepath.Join(p.TempConfigDir, "states"))
   218  	if err = p.uploadDir(o, comm, dst, src, []string{".git"}); err != nil {
   219  		return fmt.Errorf("Error uploading local state tree to remote: %s", err)
   220  	}
   221  
   222  	// move state tree from temporary directory
   223  	src = filepath.ToSlash(filepath.Join(p.TempConfigDir, "states"))
   224  	dst = p.RemoteStateTree
   225  	if err = p.removeDir(o, comm, dst); err != nil {
   226  		return fmt.Errorf("Unable to clear salt tree: %s", err)
   227  	}
   228  	if err = p.moveFile(o, comm, dst, src); err != nil {
   229  		return fmt.Errorf("Unable to move %s/states to %s: %s", p.TempConfigDir, dst, err)
   230  	}
   231  
   232  	if p.LocalPillarRoots != "" {
   233  		o.Output(fmt.Sprintf("Uploading local pillar roots: %s", p.LocalPillarRoots))
   234  		src = p.LocalPillarRoots
   235  		dst = filepath.ToSlash(filepath.Join(p.TempConfigDir, "pillar"))
   236  		if err = p.uploadDir(o, comm, dst, src, []string{".git"}); err != nil {
   237  			return fmt.Errorf("Error uploading local pillar roots to remote: %s", err)
   238  		}
   239  
   240  		// move pillar root from temporary directory
   241  		src = filepath.ToSlash(filepath.Join(p.TempConfigDir, "pillar"))
   242  		dst = p.RemotePillarRoots
   243  
   244  		if err = p.removeDir(o, comm, dst); err != nil {
   245  			return fmt.Errorf("Unable to clear pillar root: %s", err)
   246  		}
   247  		if err = p.moveFile(o, comm, dst, src); err != nil {
   248  			return fmt.Errorf("Unable to move %s/pillar to %s: %s", p.TempConfigDir, dst, err)
   249  		}
   250  	}
   251  
   252  	outR, outW := io.Pipe()
   253  	errR, errW := io.Pipe()
   254  	go copyOutput(o, outR)
   255  	go copyOutput(o, errR)
   256  	defer outW.Close()
   257  	defer errW.Close()
   258  
   259  	o.Output(fmt.Sprintf("Running: salt-call --local %s", p.CmdArgs))
   260  	cmd := &remote.Cmd{
   261  		Command: p.sudo(fmt.Sprintf("salt-call --local %s", p.CmdArgs)),
   262  		Stdout:  outW,
   263  		Stderr:  errW,
   264  	}
   265  	if err = comm.Start(cmd); err != nil {
   266  		err = fmt.Errorf("Error executing salt-call: %s", err)
   267  	}
   268  
   269  	if err := cmd.Wait(); err != nil {
   270  		return err
   271  	}
   272  	return nil
   273  }
   274  
   275  // Prepends sudo to supplied command if config says to
   276  func (p *provisioner) sudo(cmd string) string {
   277  	if p.DisableSudo {
   278  		return cmd
   279  	}
   280  
   281  	return "sudo " + cmd
   282  }
   283  
   284  func validateDirConfig(path string, name string, required bool) error {
   285  	if required == true && path == "" {
   286  		return fmt.Errorf("%s cannot be empty", name)
   287  	} else if required == false && path == "" {
   288  		return nil
   289  	}
   290  	info, err := os.Stat(path)
   291  	if err != nil {
   292  		return fmt.Errorf("%s: path '%s' is invalid: %s", name, path, err)
   293  	} else if !info.IsDir() {
   294  		return fmt.Errorf("%s: path '%s' must point to a directory", name, path)
   295  	}
   296  	return nil
   297  }
   298  
   299  func validateFileConfig(path string, name string, required bool) error {
   300  	if required == true && path == "" {
   301  		return fmt.Errorf("%s cannot be empty", name)
   302  	} else if required == false && path == "" {
   303  		return nil
   304  	}
   305  	info, err := os.Stat(path)
   306  	if err != nil {
   307  		return fmt.Errorf("%s: path '%s' is invalid: %s", name, path, err)
   308  	} else if info.IsDir() {
   309  		return fmt.Errorf("%s: path '%s' must point to a file", name, path)
   310  	}
   311  	return nil
   312  }
   313  
   314  func (p *provisioner) uploadFile(o terraform.UIOutput, comm communicator.Communicator, dst, src string) error {
   315  	f, err := os.Open(src)
   316  	if err != nil {
   317  		return fmt.Errorf("Error opening: %s", err)
   318  	}
   319  	defer f.Close()
   320  
   321  	if err = comm.Upload(dst, f); err != nil {
   322  		return fmt.Errorf("Error uploading %s: %s", src, err)
   323  	}
   324  	return nil
   325  }
   326  
   327  func (p *provisioner) moveFile(o terraform.UIOutput, comm communicator.Communicator, dst, src string) error {
   328  	o.Output(fmt.Sprintf("Moving %s to %s", src, dst))
   329  	cmd := &remote.Cmd{Command: fmt.Sprintf(p.sudo("mv %s %s"), src, dst)}
   330  	if err := comm.Start(cmd); err != nil {
   331  		return fmt.Errorf("Unable to move %s to %s: %s", src, dst, err)
   332  	}
   333  	if err := cmd.Wait(); err != nil {
   334  		return err
   335  	}
   336  	return nil
   337  }
   338  
   339  func (p *provisioner) createDir(o terraform.UIOutput, comm communicator.Communicator, dir string) error {
   340  	o.Output(fmt.Sprintf("Creating directory: %s", dir))
   341  	cmd := &remote.Cmd{
   342  		Command: fmt.Sprintf("mkdir -p '%s'", dir),
   343  	}
   344  	if err := comm.Start(cmd); err != nil {
   345  		return err
   346  	}
   347  
   348  	if err := cmd.Wait(); err != nil {
   349  		return err
   350  	}
   351  	return nil
   352  }
   353  
   354  func (p *provisioner) removeDir(o terraform.UIOutput, comm communicator.Communicator, dir string) error {
   355  	o.Output(fmt.Sprintf("Removing directory: %s", dir))
   356  	cmd := &remote.Cmd{
   357  		Command: fmt.Sprintf("rm -rf '%s'", dir),
   358  	}
   359  	if err := comm.Start(cmd); err != nil {
   360  		return err
   361  	}
   362  	if err := cmd.Wait(); err != nil {
   363  		return err
   364  	}
   365  	return nil
   366  }
   367  
   368  func (p *provisioner) uploadDir(o terraform.UIOutput, comm communicator.Communicator, dst, src string, ignore []string) error {
   369  	if err := p.createDir(o, comm, dst); err != nil {
   370  		return err
   371  	}
   372  
   373  	// Make sure there is a trailing "/" so that the directory isn't
   374  	// created on the other side.
   375  	if src[len(src)-1] != '/' {
   376  		src = src + "/"
   377  	}
   378  	return comm.UploadDir(dst, src)
   379  }
   380  
   381  // Validate checks if the required arguments are configured
   382  func validateFn(c *terraform.ResourceConfig) (ws []string, es []error) {
   383  	// require a salt state tree
   384  	localStateTreeTmp, ok := c.Get("local_state_tree")
   385  	var localStateTree string
   386  	if !ok {
   387  		es = append(es,
   388  			errors.New("Required local_state_tree is not set"))
   389  	} else {
   390  		localStateTree = localStateTreeTmp.(string)
   391  	}
   392  	err := validateDirConfig(localStateTree, "local_state_tree", true)
   393  	if err != nil {
   394  		es = append(es, err)
   395  	}
   396  
   397  	var localPillarRoots string
   398  	localPillarRootsTmp, ok := c.Get("local_pillar_roots")
   399  	if !ok {
   400  		localPillarRoots = ""
   401  	} else {
   402  		localPillarRoots = localPillarRootsTmp.(string)
   403  	}
   404  
   405  	err = validateDirConfig(localPillarRoots, "local_pillar_roots", false)
   406  	if err != nil {
   407  		es = append(es, err)
   408  	}
   409  
   410  	var minionConfig string
   411  	minionConfigTmp, ok := c.Get("minion_config_file")
   412  	if !ok {
   413  		minionConfig = ""
   414  	} else {
   415  		minionConfig = minionConfigTmp.(string)
   416  	}
   417  	err = validateFileConfig(minionConfig, "minion_config_file", false)
   418  	if err != nil {
   419  		es = append(es, err)
   420  	}
   421  
   422  	var remoteStateTree string
   423  	remoteStateTreeTmp, ok := c.Get("remote_state_tree")
   424  	if !ok {
   425  		remoteStateTree = ""
   426  	} else {
   427  		remoteStateTree = remoteStateTreeTmp.(string)
   428  	}
   429  
   430  	var remotePillarRoots string
   431  	remotePillarRootsTmp, ok := c.Get("remote_pillar_roots")
   432  	if !ok {
   433  		remotePillarRoots = ""
   434  	} else {
   435  		remotePillarRoots = remotePillarRootsTmp.(string)
   436  	}
   437  
   438  	if minionConfig != "" && (remoteStateTree != "" || remotePillarRoots != "") {
   439  		es = append(es,
   440  			errors.New("remote_state_tree and remote_pillar_roots only apply when minion_config_file is not used"))
   441  	}
   442  
   443  	if len(es) > 0 {
   444  		return ws, es
   445  	}
   446  
   447  	return ws, es
   448  }
   449  
   450  func decodeConfig(d *schema.ResourceData) (*provisioner, error) {
   451  	p := &provisioner{
   452  		LocalStateTree:    d.Get("local_state_tree").(string),
   453  		LogLevel:          d.Get("log_level").(string),
   454  		SaltCallArgs:      d.Get("salt_call_args").(string),
   455  		CmdArgs:           d.Get("cmd_args").(string),
   456  		MinionConfig:      d.Get("minion_config_file").(string),
   457  		CustomState:       d.Get("custom_state").(string),
   458  		DisableSudo:       d.Get("disable_sudo").(bool),
   459  		BootstrapArgs:     d.Get("bootstrap_args").(string),
   460  		NoExitOnFailure:   d.Get("no_exit_on_failure").(bool),
   461  		SkipBootstrap:     d.Get("skip_bootstrap").(bool),
   462  		TempConfigDir:     d.Get("temp_config_dir").(string),
   463  		RemotePillarRoots: d.Get("remote_pillar_roots").(string),
   464  		RemoteStateTree:   d.Get("remote_state_tree").(string),
   465  		LocalPillarRoots:  d.Get("local_pillar_roots").(string),
   466  	}
   467  
   468  	// build the command line args to pass onto salt
   469  	var cmdArgs bytes.Buffer
   470  
   471  	if p.CustomState == "" {
   472  		cmdArgs.WriteString(" state.highstate")
   473  	} else {
   474  		cmdArgs.WriteString(" state.sls ")
   475  		cmdArgs.WriteString(p.CustomState)
   476  	}
   477  
   478  	if p.MinionConfig == "" {
   479  		// pass --file-root and --pillar-root if no minion_config_file is supplied
   480  		if p.RemoteStateTree != "" {
   481  			cmdArgs.WriteString(" --file-root=")
   482  			cmdArgs.WriteString(p.RemoteStateTree)
   483  		} else {
   484  			cmdArgs.WriteString(" --file-root=")
   485  			cmdArgs.WriteString(DefaultStateTreeDir)
   486  		}
   487  		if p.RemotePillarRoots != "" {
   488  			cmdArgs.WriteString(" --pillar-root=")
   489  			cmdArgs.WriteString(p.RemotePillarRoots)
   490  		} else {
   491  			cmdArgs.WriteString(" --pillar-root=")
   492  			cmdArgs.WriteString(DefaultPillarRootDir)
   493  		}
   494  	}
   495  
   496  	if !p.NoExitOnFailure {
   497  		cmdArgs.WriteString(" --retcode-passthrough")
   498  	}
   499  
   500  	if p.LogLevel == "" {
   501  		cmdArgs.WriteString(" -l info")
   502  	} else {
   503  		cmdArgs.WriteString(" -l ")
   504  		cmdArgs.WriteString(p.LogLevel)
   505  	}
   506  
   507  	if p.SaltCallArgs != "" {
   508  		cmdArgs.WriteString(" ")
   509  		cmdArgs.WriteString(p.SaltCallArgs)
   510  	}
   511  
   512  	p.CmdArgs = cmdArgs.String()
   513  
   514  	return p, nil
   515  }
   516  
   517  func copyOutput(
   518  	o terraform.UIOutput, r io.Reader) {
   519  	lr := linereader.New(r)
   520  	for line := range lr.Ch {
   521  		o.Output(line)
   522  	}
   523  }