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 }