github.com/adamar/terraform@v0.2.2-0.20141016210445-2e703afdad0e/command/apply.go (about) 1 package command 2 3 import ( 4 "bytes" 5 "fmt" 6 "log" 7 "os" 8 "sort" 9 "strings" 10 11 "github.com/hashicorp/terraform/config/module" 12 "github.com/hashicorp/terraform/terraform" 13 ) 14 15 // ApplyCommand is a Command implementation that applies a Terraform 16 // configuration and actually builds or changes infrastructure. 17 type ApplyCommand struct { 18 Meta 19 20 // If true, then this apply command will become the "destroy" 21 // command. It is just like apply but only processes a destroy. 22 Destroy bool 23 24 // When this channel is closed, the apply will be cancelled. 25 ShutdownCh <-chan struct{} 26 } 27 28 func (c *ApplyCommand) Run(args []string) int { 29 var destroyForce, refresh bool 30 var statePath, stateOutPath, backupPath string 31 32 args = c.Meta.process(args, true) 33 34 cmdName := "apply" 35 if c.Destroy { 36 cmdName = "destroy" 37 } 38 39 cmdFlags := c.Meta.flagSet(cmdName) 40 if c.Destroy { 41 cmdFlags.BoolVar(&destroyForce, "force", false, "force") 42 } 43 cmdFlags.BoolVar(&refresh, "refresh", true, "refresh") 44 cmdFlags.StringVar(&statePath, "state", DefaultStateFilename, "path") 45 cmdFlags.StringVar(&stateOutPath, "state-out", "", "path") 46 cmdFlags.StringVar(&backupPath, "backup", "", "path") 47 cmdFlags.Usage = func() { c.Ui.Error(c.Help()) } 48 if err := cmdFlags.Parse(args); err != nil { 49 return 1 50 } 51 52 pwd, err := os.Getwd() 53 if err != nil { 54 c.Ui.Error(fmt.Sprintf("Error getting pwd: %s", err)) 55 return 1 56 } 57 58 var configPath string 59 args = cmdFlags.Args() 60 if len(args) > 1 { 61 c.Ui.Error("The apply command expects at most one argument.") 62 cmdFlags.Usage() 63 return 1 64 } else if len(args) == 1 { 65 configPath = args[0] 66 } else { 67 configPath = pwd 68 } 69 70 // Prepare the extra hooks to count resources 71 countHook := new(CountHook) 72 c.Meta.extraHooks = []terraform.Hook{countHook} 73 74 // If we don't specify an output path, default to out normal state 75 // path. 76 if stateOutPath == "" { 77 stateOutPath = statePath 78 } 79 80 // If we don't specify a backup path, default to state out with 81 // the extension 82 if backupPath == "" { 83 backupPath = stateOutPath + DefaultBackupExtention 84 } 85 86 if !c.Destroy { 87 // Do a detect to determine if we need to do an init + apply. 88 if detected, err := module.Detect(configPath, pwd); err != nil { 89 c.Ui.Error(fmt.Sprintf( 90 "Invalid path: %s", err)) 91 return 1 92 } else if !strings.HasPrefix(detected, "file") { 93 // If this isn't a file URL then we're doing an init + 94 // apply. 95 var init InitCommand 96 init.Meta = c.Meta 97 if code := init.Run([]string{detected}); code != 0 { 98 return code 99 } 100 101 // Change the config path to be the cwd 102 configPath = pwd 103 } 104 } 105 106 // Build the context based on the arguments given 107 ctx, planned, err := c.Context(contextOpts{ 108 Path: configPath, 109 StatePath: statePath, 110 }) 111 if err != nil { 112 c.Ui.Error(err.Error()) 113 return 1 114 } 115 if c.Destroy && planned { 116 c.Ui.Error(fmt.Sprintf( 117 "Destroy can't be called with a plan file.")) 118 return 1 119 } 120 if !destroyForce && c.Destroy { 121 v, err := c.UIInput().Input(&terraform.InputOpts{ 122 Id: "destroy", 123 Query: "Do you really want to destroy?", 124 Description: "Terraform will delete all your managed infrastructure.\n" + 125 "There is no undo. Only 'yes' will be accepted to confirm.", 126 }) 127 if err != nil { 128 c.Ui.Error(fmt.Sprintf("Error asking for confirmation: %s", err)) 129 return 1 130 } 131 if v != "yes" { 132 c.Ui.Output("Destroy cancelled.") 133 return 1 134 } 135 } 136 if !planned { 137 if err := ctx.Input(c.InputMode()); err != nil { 138 c.Ui.Error(fmt.Sprintf("Error configuring: %s", err)) 139 return 1 140 } 141 } 142 if !validateContext(ctx, c.Ui) { 143 return 1 144 } 145 146 // Create a backup of the state before updating 147 if backupPath != "-" && c.state != nil { 148 log.Printf("[INFO] Writing backup state to: %s", backupPath) 149 f, err := os.Create(backupPath) 150 if err == nil { 151 err = terraform.WriteState(c.state, f) 152 f.Close() 153 } 154 if err != nil { 155 c.Ui.Error(fmt.Sprintf("Error writing backup state file: %s", err)) 156 return 1 157 } 158 } 159 160 // Plan if we haven't already 161 if !planned { 162 if refresh { 163 if _, err := ctx.Refresh(); err != nil { 164 c.Ui.Error(fmt.Sprintf("Error refreshing state: %s", err)) 165 return 1 166 } 167 } 168 169 var opts terraform.PlanOpts 170 if c.Destroy { 171 opts.Destroy = true 172 } 173 174 if _, err := ctx.Plan(&opts); err != nil { 175 c.Ui.Error(fmt.Sprintf( 176 "Error creating plan: %s", err)) 177 return 1 178 } 179 } 180 181 // Start the apply in a goroutine so that we can be interrupted. 182 var state *terraform.State 183 var applyErr error 184 doneCh := make(chan struct{}) 185 go func() { 186 defer close(doneCh) 187 state, applyErr = ctx.Apply() 188 }() 189 190 // Wait for the apply to finish or for us to be interrupted so 191 // we can handle it properly. 192 err = nil 193 select { 194 case <-c.ShutdownCh: 195 c.Ui.Output("Interrupt received. Gracefully shutting down...") 196 197 // Stop execution 198 go ctx.Stop() 199 200 // Still get the result, since there is still one 201 select { 202 case <-c.ShutdownCh: 203 c.Ui.Error( 204 "Two interrupts received. Exiting immediately. Note that data\n" + 205 "loss may have occurred.") 206 return 1 207 case <-doneCh: 208 } 209 case <-doneCh: 210 } 211 212 if state != nil { 213 // Write state out to the file 214 f, err := os.Create(stateOutPath) 215 if err == nil { 216 err = terraform.WriteState(state, f) 217 f.Close() 218 } 219 if err != nil { 220 c.Ui.Error(fmt.Sprintf("Failed to save state: %s", err)) 221 return 1 222 } 223 } 224 225 if applyErr != nil { 226 c.Ui.Error(fmt.Sprintf( 227 "Error applying plan:\n\n"+ 228 "%s\n\n"+ 229 "Terraform does not automatically rollback in the face of errors.\n"+ 230 "Instead, your Terraform state file has been partially updated with\n"+ 231 "any resources that successfully completed. Please address the error\n"+ 232 "above and apply again to incrementally change your infrastructure.", 233 applyErr)) 234 return 1 235 } 236 237 c.Ui.Output(c.Colorize().Color(fmt.Sprintf( 238 "[reset][bold][green]\n"+ 239 "Apply complete! Resources: %d added, %d changed, %d destroyed.", 240 countHook.Added, 241 countHook.Changed, 242 countHook.Removed))) 243 244 if countHook.Added > 0 || countHook.Changed > 0 { 245 c.Ui.Output(c.Colorize().Color(fmt.Sprintf( 246 "[reset]\n"+ 247 "The state of your infrastructure has been saved to the path\n"+ 248 "below. This state is required to modify and destroy your\n"+ 249 "infrastructure, so keep it safe. To inspect the complete state\n"+ 250 "use the `terraform show` command.\n\n"+ 251 "State path: %s", 252 stateOutPath))) 253 } 254 255 // If we have outputs, then output those at the end. 256 var outputs map[string]string 257 if !c.Destroy && state != nil { 258 outputs = state.RootModule().Outputs 259 } 260 if len(outputs) > 0 { 261 outputBuf := new(bytes.Buffer) 262 outputBuf.WriteString("[reset][bold][green]\nOutputs:\n\n") 263 264 // Output the outputs in alphabetical order 265 keyLen := 0 266 keys := make([]string, 0, len(outputs)) 267 for key, _ := range outputs { 268 keys = append(keys, key) 269 if len(key) > keyLen { 270 keyLen = len(key) 271 } 272 } 273 sort.Strings(keys) 274 275 for _, k := range keys { 276 v := outputs[k] 277 278 outputBuf.WriteString(fmt.Sprintf( 279 " %s%s = %s\n", 280 k, 281 strings.Repeat(" ", keyLen-len(k)), 282 v)) 283 } 284 285 c.Ui.Output(c.Colorize().Color( 286 strings.TrimSpace(outputBuf.String()))) 287 } 288 289 return 0 290 } 291 292 func (c *ApplyCommand) Help() string { 293 if c.Destroy { 294 return c.helpDestroy() 295 } 296 297 return c.helpApply() 298 } 299 300 func (c *ApplyCommand) Synopsis() string { 301 if c.Destroy { 302 return "Destroy Terraform-managed infrastructure" 303 } 304 305 return "Builds or changes infrastructure" 306 } 307 308 func (c *ApplyCommand) helpApply() string { 309 helpText := ` 310 Usage: terraform apply [options] [DIR] 311 312 Builds or changes infrastructure according to Terraform configuration 313 files in DIR. 314 315 DIR can also be a SOURCE as given to the "init" command. In this case, 316 apply behaves as though "init" was called followed by "apply". This only 317 works for sources that aren't files, and only if the current working 318 directory is empty of Terraform files. This is a shortcut for getting 319 started. 320 321 Options: 322 323 -backup=path Path to backup the existing state file before 324 modifying. Defaults to the "-state-out" path with 325 ".backup" extension. Set to "-" to disable backup. 326 327 -input=true Ask for input for variables if not directly set. 328 329 -no-color If specified, output won't contain any color. 330 331 -refresh=true Update state prior to checking for differences. This 332 has no effect if a plan file is given to apply. 333 334 -state=path Path to read and save state (unless state-out 335 is specified). Defaults to "terraform.tfstate". 336 337 -state-out=path Path to write state to that is different than 338 "-state". This can be used to preserve the old 339 state. 340 341 -var 'foo=bar' Set a variable in the Terraform configuration. This 342 flag can be set multiple times. 343 344 -var-file=foo Set variables in the Terraform configuration from 345 a file. If "terraform.tfvars" is present, it will be 346 automatically loaded if this flag is not specified. 347 348 349 ` 350 return strings.TrimSpace(helpText) 351 } 352 353 func (c *ApplyCommand) helpDestroy() string { 354 helpText := ` 355 Usage: terraform destroy [options] [DIR] 356 357 Destroy Terraform-managed infrastructure. 358 359 Options: 360 361 -backup=path Path to backup the existing state file before 362 modifying. Defaults to the "-state-out" path with 363 ".backup" extension. Set to "-" to disable backup. 364 365 -force Don't ask for input for destroy confirmation. 366 367 -no-color If specified, output won't contain any color. 368 369 -refresh=true Update state prior to checking for differences. This 370 has no effect if a plan file is given to apply. 371 372 -state=path Path to read and save state (unless state-out 373 is specified). Defaults to "terraform.tfstate". 374 375 -state-out=path Path to write state to that is different than 376 "-state". This can be used to preserve the old 377 state. 378 379 -var 'foo=bar' Set a variable in the Terraform configuration. This 380 flag can be set multiple times. 381 382 -var-file=foo Set variables in the Terraform configuration from 383 a file. If "terraform.tfvars" is present, it will be 384 automatically loaded if this flag is not specified. 385 386 387 ` 388 return strings.TrimSpace(helpText) 389 }