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