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