github.com/bhameyie/otto@v0.2.1-0.20160406174117-16052efa52ec/helper/terraform/terraform.go (about)

     1  package terraform
     2  
     3  import (
     4  	"encoding/json"
     5  	"fmt"
     6  	"io"
     7  	"io/ioutil"
     8  	"log"
     9  	"os"
    10  	"os/exec"
    11  	"path/filepath"
    12  
    13  	"github.com/hashicorp/go-version"
    14  	"github.com/hashicorp/otto/context"
    15  	"github.com/hashicorp/otto/directory"
    16  	execHelper "github.com/hashicorp/otto/helper/exec"
    17  	"github.com/hashicorp/otto/helper/hashitools"
    18  	"github.com/hashicorp/otto/ui"
    19  )
    20  
    21  var (
    22  	tfMinVersion = version.Must(version.NewVersion("0.6.3"))
    23  )
    24  
    25  // Project returns the hashitools Project for this.
    26  func Project(ctx *context.Shared) (*hashitools.Project, error) {
    27  	p := &hashitools.Project{
    28  		Name:       "terraform",
    29  		MinVersion: tfMinVersion,
    30  		Installer: &hashitools.GoInstaller{
    31  			Name: "terraform",
    32  			Dir:  filepath.Join(ctx.InstallDir),
    33  			Ui:   ctx.Ui,
    34  		},
    35  	}
    36  	return p, p.InstallIfNeeded()
    37  }
    38  
    39  // Terraform wraps `terraform` execution into an easy-to-use API
    40  type Terraform struct {
    41  	// Path is the path to Terraform itself. If empty, "terraform"
    42  	// will be used and looked up via the PATH var.
    43  	Path string
    44  
    45  	// Dir is the working directory where all Terraform commands are executed
    46  	Dir string
    47  
    48  	// Ui, if given, will be used to stream output from the Terraform commands.
    49  	// If this is nil, then the output will be logged but won't be visible
    50  	// to the user.
    51  	Ui ui.Ui
    52  
    53  	// Variables is a list of variables to pass to Terraform.
    54  	Variables map[string]string
    55  
    56  	// Directory can be set to point to a directory where data can be
    57  	// stored. If this is set, then the state will be loaded/stored here
    58  	// automatically.
    59  	//
    60  	// StateId is the identifier used to load/store the state from
    61  	// blob storage. If this is empty, state won't be loaded or stored
    62  	// automatically.
    63  	//
    64  	// It is highly recommended to use this instead of manually attempting
    65  	// to manage state since this will properly handle storing the state
    66  	// in the issue of an error and will put the state in the pwd in the
    67  	// case we can't write it to a directory.
    68  	Directory directory.Backend
    69  	StateId   string
    70  }
    71  
    72  // Execute executes a raw Terraform command
    73  func (t *Terraform) Execute(commandRaw ...string) error {
    74  	command := make([]string, 1, len(commandRaw)*2)
    75  	command[0] = commandRaw[0]
    76  	commandArgs := commandRaw[1:]
    77  
    78  	// Determine if we need to skip var flags or not.
    79  	varSkip := false
    80  	varSkip = command[0] == "get"
    81  
    82  	// If we have variables, create the var file
    83  	if !varSkip && len(t.Variables) > 0 {
    84  		varfile, err := t.varfile()
    85  		if err != nil {
    86  			return err
    87  		}
    88  		if execHelper.ShouldCleanup() {
    89  			defer os.Remove(varfile)
    90  		}
    91  
    92  		// Append the varfile onto our command.
    93  		command = append(command, "-var-file", varfile)
    94  
    95  		// Log some of the vars we're using
    96  		for k, _ := range t.Variables {
    97  			log.Printf("[DEBUG] setting TF var: %s", k)
    98  		}
    99  	}
   100  
   101  	// Determine if we need to skip state flags or not. This is just
   102  	// hardcoded for now.
   103  	stateSkip := false
   104  	stateSkip = command[0] == "get"
   105  
   106  	// Output needs state but not state-out; more hard-coding
   107  	stateOutSkip := false
   108  	stateOutSkip = command[0] == "output"
   109  
   110  	// If we care about state, then setup the state directory and
   111  	// load it up.
   112  	var stateDir, statePath string
   113  	if !stateSkip && t.StateId != "" && t.Directory != nil {
   114  		var err error
   115  		stateDir, err = ioutil.TempDir("", "otto-tf")
   116  		if err != nil {
   117  			return err
   118  		}
   119  		if execHelper.ShouldCleanup() {
   120  			defer os.RemoveAll(stateDir)
   121  		}
   122  
   123  		// State path
   124  		stateOldPath := filepath.Join(stateDir, "state.old")
   125  		statePath = filepath.Join(stateDir, "state")
   126  
   127  		// Load the state from the directory
   128  		data, err := t.Directory.GetBlob(t.StateId)
   129  		if err != nil {
   130  			return fmt.Errorf("Error loading Terraform state: %s", err)
   131  		}
   132  		if data == nil && command[0] == "destroy" {
   133  			// Destroy we can just execute, we don't need state
   134  			return nil
   135  		}
   136  		if data != nil {
   137  			err = data.WriteToFile(stateOldPath)
   138  			data.Close()
   139  		}
   140  		if err != nil {
   141  			return fmt.Errorf("Error writing Terraform state: %s", err)
   142  		}
   143  
   144  		// Append the state to the args
   145  		command = append(command, "-state", stateOldPath)
   146  		if !stateOutSkip {
   147  			command = append(command, "-state-out", statePath)
   148  		}
   149  	}
   150  
   151  	// Append all the final args
   152  	command = append(command, commandArgs...)
   153  
   154  	// Build the command to execute
   155  	log.Printf("[DEBUG] executing terraform: %v", command)
   156  	path := "terraform"
   157  	if t.Path != "" {
   158  		path = t.Path
   159  	}
   160  	cmd := exec.Command(path, command...)
   161  	cmd.Dir = t.Dir
   162  
   163  	// Start the Terraform command. If there is an error we just store
   164  	// the error but can't exit yet because we have to store partial
   165  	// state if there is any.
   166  	err := execHelper.Run(t.Ui, cmd)
   167  	if err != nil {
   168  		err = fmt.Errorf("Error running Terraform: %s", err)
   169  	}
   170  
   171  	// Save the state file if we have it.
   172  	if t.StateId != "" && t.Directory != nil && statePath != "" && !stateOutSkip {
   173  		f, ferr := os.Open(statePath)
   174  		if ferr != nil {
   175  			return fmt.Errorf(
   176  				"Error reading Terraform state for saving: %s", ferr)
   177  		}
   178  
   179  		// Store the state
   180  		derr := t.Directory.PutBlob(t.StateId, &directory.BlobData{
   181  			Data: f,
   182  		})
   183  
   184  		// Always close the file
   185  		f.Close()
   186  
   187  		// If we couldn't save the data, then note the error. This is a
   188  		// _really_ bad error to get since there isn't a good way to
   189  		// recover. For now, we just copy the state to the pwd and note
   190  		// the user.
   191  		if derr != nil {
   192  			// TODO: copy state
   193  
   194  			err = fmt.Errorf(
   195  				"Failed to save Terraform state: %s\n\n"+
   196  					"This means that Otto was unable to store the state of your infrastructure.\n"+
   197  					"At this time, Otto doesn't support gracefully recovering from this\n"+
   198  					"scenario. The state should be in the path below. Please ask the\n"+
   199  					"community for assistance.",
   200  				derr)
   201  		}
   202  	}
   203  
   204  	return err
   205  }
   206  
   207  // Outputs reads the outputs from the configured directory storage.
   208  func (t *Terraform) Outputs() (map[string]string, error) {
   209  	// Make a temporary file to store our state
   210  	tf, err := ioutil.TempFile("", "otto-tf")
   211  	if err != nil {
   212  		return nil, err
   213  	}
   214  	if execHelper.ShouldCleanup() {
   215  		defer os.Remove(tf.Name())
   216  	}
   217  
   218  	// Read the state from the directory and put it on disk. Lots of
   219  	// careful management of file handles here.
   220  	data, err := t.Directory.GetBlob(t.StateId)
   221  	if err == nil {
   222  		if data == nil {
   223  			return nil, nil
   224  		}
   225  
   226  		_, err = io.Copy(tf, data.Data)
   227  		data.Close()
   228  	}
   229  	tf.Close()
   230  	if err != nil {
   231  		return nil, fmt.Errorf("Error loading Terraform state: %s", err)
   232  	}
   233  
   234  	// Read the outputs as normal. Defers will clean up our temp file.
   235  	return Outputs(tf.Name())
   236  }
   237  
   238  func (t *Terraform) varfile() (string, error) {
   239  	f, err := ioutil.TempFile("", "otto-tf")
   240  	if err != nil {
   241  		return "", err
   242  	}
   243  
   244  	err = json.NewEncoder(f).Encode(t.Variables)
   245  	f.Close()
   246  	if err != nil {
   247  		os.Remove(f.Name())
   248  	}
   249  
   250  	return f.Name(), err
   251  }