github.com/pbthorste/terraform@v0.8.6-0.20170127005045-deb56bd93da2/builtin/provisioners/remote-exec/resource_provisioner.go (about)

     1  package remoteexec
     2  
     3  import (
     4  	"bytes"
     5  	"fmt"
     6  	"io"
     7  	"io/ioutil"
     8  	"log"
     9  	"os"
    10  	"time"
    11  
    12  	"github.com/hashicorp/terraform/communicator"
    13  	"github.com/hashicorp/terraform/communicator/remote"
    14  	"github.com/hashicorp/terraform/terraform"
    15  	"github.com/mitchellh/go-linereader"
    16  )
    17  
    18  // ResourceProvisioner represents a remote exec provisioner
    19  type ResourceProvisioner struct{}
    20  
    21  // Apply executes the remote exec provisioner
    22  func (p *ResourceProvisioner) Apply(
    23  	o terraform.UIOutput,
    24  	s *terraform.InstanceState,
    25  	c *terraform.ResourceConfig) error {
    26  	// Get a new communicator
    27  	comm, err := communicator.New(s)
    28  	if err != nil {
    29  		return err
    30  	}
    31  
    32  	// Collect the scripts
    33  	scripts, err := p.collectScripts(c)
    34  	if err != nil {
    35  		return err
    36  	}
    37  	for _, s := range scripts {
    38  		defer s.Close()
    39  	}
    40  
    41  	// Copy and execute each script
    42  	if err := p.runScripts(o, comm, scripts); err != nil {
    43  		return err
    44  	}
    45  	return nil
    46  }
    47  
    48  // Validate checks if the required arguments are configured
    49  func (p *ResourceProvisioner) Validate(c *terraform.ResourceConfig) (ws []string, es []error) {
    50  	num := 0
    51  	for name := range c.Raw {
    52  		switch name {
    53  		case "scripts", "script", "inline":
    54  			num++
    55  		default:
    56  			es = append(es, fmt.Errorf("Unknown configuration '%s'", name))
    57  		}
    58  	}
    59  	if num != 1 {
    60  		es = append(es, fmt.Errorf("Must provide one of 'scripts', 'script' or 'inline' to remote-exec"))
    61  	}
    62  	return
    63  }
    64  
    65  // generateScripts takes the configuration and creates a script from each inline config
    66  func (p *ResourceProvisioner) generateScripts(c *terraform.ResourceConfig) ([]string, error) {
    67  	var scripts []string
    68  	command, ok := c.Config["inline"]
    69  	if ok {
    70  		switch cmd := command.(type) {
    71  		case string:
    72  			scripts = append(scripts, cmd)
    73  		case []string:
    74  			scripts = append(scripts, cmd...)
    75  		case []interface{}:
    76  			for _, l := range cmd {
    77  				lStr, ok := l.(string)
    78  				if ok {
    79  					scripts = append(scripts, lStr)
    80  				} else {
    81  					return nil, fmt.Errorf("Unsupported 'inline' type! Must be string, or list of strings.")
    82  				}
    83  			}
    84  		default:
    85  			return nil, fmt.Errorf("Unsupported 'inline' type! Must be string, or list of strings.")
    86  		}
    87  	}
    88  	return scripts, nil
    89  }
    90  
    91  // collectScripts is used to collect all the scripts we need
    92  // to execute in preparation for copying them.
    93  func (p *ResourceProvisioner) collectScripts(c *terraform.ResourceConfig) ([]io.ReadCloser, error) {
    94  	// Check if inline
    95  	_, ok := c.Config["inline"]
    96  	if ok {
    97  		scripts, err := p.generateScripts(c)
    98  		if err != nil {
    99  			return nil, err
   100  		}
   101  
   102  		r := []io.ReadCloser{}
   103  		for _, script := range scripts {
   104  			r = append(r, ioutil.NopCloser(bytes.NewReader([]byte(script))))
   105  		}
   106  
   107  		return r, nil
   108  	}
   109  
   110  	// Collect scripts
   111  	var scripts []string
   112  	s, ok := c.Config["script"]
   113  	if ok {
   114  		sStr, ok := s.(string)
   115  		if !ok {
   116  			return nil, fmt.Errorf("Unsupported 'script' type! Must be a string.")
   117  		}
   118  		scripts = append(scripts, sStr)
   119  	}
   120  
   121  	sl, ok := c.Config["scripts"]
   122  	if ok {
   123  		switch slt := sl.(type) {
   124  		case []string:
   125  			scripts = append(scripts, slt...)
   126  		case []interface{}:
   127  			for _, l := range slt {
   128  				lStr, ok := l.(string)
   129  				if ok {
   130  					scripts = append(scripts, lStr)
   131  				} else {
   132  					return nil, fmt.Errorf("Unsupported 'scripts' type! Must be list of strings.")
   133  				}
   134  			}
   135  		default:
   136  			return nil, fmt.Errorf("Unsupported 'scripts' type! Must be list of strings.")
   137  		}
   138  	}
   139  
   140  	// Open all the scripts
   141  	var fhs []io.ReadCloser
   142  	for _, s := range scripts {
   143  		fh, err := os.Open(s)
   144  		if err != nil {
   145  			for _, fh := range fhs {
   146  				fh.Close()
   147  			}
   148  			return nil, fmt.Errorf("Failed to open script '%s': %v", s, err)
   149  		}
   150  		fhs = append(fhs, fh)
   151  	}
   152  
   153  	// Done, return the file handles
   154  	return fhs, nil
   155  }
   156  
   157  // runScripts is used to copy and execute a set of scripts
   158  func (p *ResourceProvisioner) runScripts(
   159  	o terraform.UIOutput,
   160  	comm communicator.Communicator,
   161  	scripts []io.ReadCloser) error {
   162  	// Wait and retry until we establish the connection
   163  	err := retryFunc(comm.Timeout(), func() error {
   164  		err := comm.Connect(o)
   165  		return err
   166  	})
   167  	if err != nil {
   168  		return err
   169  	}
   170  	defer comm.Disconnect()
   171  
   172  	for _, script := range scripts {
   173  		var cmd *remote.Cmd
   174  		outR, outW := io.Pipe()
   175  		errR, errW := io.Pipe()
   176  		outDoneCh := make(chan struct{})
   177  		errDoneCh := make(chan struct{})
   178  		go p.copyOutput(o, outR, outDoneCh)
   179  		go p.copyOutput(o, errR, errDoneCh)
   180  
   181  		remotePath := comm.ScriptPath()
   182  		err = retryFunc(comm.Timeout(), func() error {
   183  			if err := comm.UploadScript(remotePath, script); err != nil {
   184  				return fmt.Errorf("Failed to upload script: %v", err)
   185  			}
   186  
   187  			cmd = &remote.Cmd{
   188  				Command: remotePath,
   189  				Stdout:  outW,
   190  				Stderr:  errW,
   191  			}
   192  			if err := comm.Start(cmd); err != nil {
   193  				return fmt.Errorf("Error starting script: %v", err)
   194  			}
   195  
   196  			return nil
   197  		})
   198  		if err == nil {
   199  			cmd.Wait()
   200  			if cmd.ExitStatus != 0 {
   201  				err = fmt.Errorf("Script exited with non-zero exit status: %d", cmd.ExitStatus)
   202  			}
   203  		}
   204  
   205  		// Wait for output to clean up
   206  		outW.Close()
   207  		errW.Close()
   208  		<-outDoneCh
   209  		<-errDoneCh
   210  
   211  		// Upload a blank follow up file in the same path to prevent residual
   212  		// script contents from remaining on remote machine
   213  		empty := bytes.NewReader([]byte(""))
   214  		if err := comm.Upload(remotePath, empty); err != nil {
   215  			// This feature is best-effort.
   216  			log.Printf("[WARN] Failed to upload empty follow up script: %v", err)
   217  		}
   218  
   219  		// If we have an error, return it out now that we've cleaned up
   220  		if err != nil {
   221  			return err
   222  		}
   223  	}
   224  
   225  	return nil
   226  }
   227  
   228  func (p *ResourceProvisioner) copyOutput(
   229  	o terraform.UIOutput, r io.Reader, doneCh chan<- struct{}) {
   230  	defer close(doneCh)
   231  	lr := linereader.New(r)
   232  	for line := range lr.Ch {
   233  		o.Output(line)
   234  	}
   235  }
   236  
   237  // retryFunc is used to retry a function for a given duration
   238  func retryFunc(timeout time.Duration, f func() error) error {
   239  	finish := time.After(timeout)
   240  	for {
   241  		err := f()
   242  		if err == nil {
   243  			return nil
   244  		}
   245  		log.Printf("Retryable error: %v", err)
   246  
   247  		select {
   248  		case <-finish:
   249  			return err
   250  		case <-time.After(3 * time.Second):
   251  		}
   252  	}
   253  }