github.com/opsidian/terraform@v0.7.8-0.20161104123224-27c39cdfba5b/command/apply.go (about) 1 package command 2 3 import ( 4 "bytes" 5 "fmt" 6 "os" 7 "sort" 8 "strings" 9 10 "github.com/hashicorp/go-getter" 11 "github.com/hashicorp/go-multierror" 12 "github.com/hashicorp/terraform/config" 13 "github.com/hashicorp/terraform/helper/experiment" 14 "github.com/hashicorp/terraform/terraform" 15 ) 16 17 // ApplyCommand is a Command implementation that applies a Terraform 18 // configuration and actually builds or changes infrastructure. 19 type ApplyCommand struct { 20 Meta 21 22 // If true, then this apply command will become the "destroy" 23 // command. It is just like apply but only processes a destroy. 24 Destroy bool 25 26 // When this channel is closed, the apply will be cancelled. 27 ShutdownCh <-chan struct{} 28 } 29 30 func (c *ApplyCommand) Run(args []string) int { 31 var destroyForce, refresh bool 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.IntVar( 45 &c.Meta.parallelism, "parallelism", DefaultParallelism, "parallelism") 46 cmdFlags.StringVar(&c.Meta.statePath, "state", DefaultStateFilename, "path") 47 cmdFlags.StringVar(&c.Meta.stateOutPath, "state-out", "", "path") 48 cmdFlags.StringVar(&c.Meta.backupPath, "backup", "", "path") 49 cmdFlags.Usage = func() { c.Ui.Error(c.Help()) } 50 if err := cmdFlags.Parse(args); err != nil { 51 return 1 52 } 53 54 pwd, err := os.Getwd() 55 if err != nil { 56 c.Ui.Error(fmt.Sprintf("Error getting pwd: %s", err)) 57 return 1 58 } 59 60 var configPath string 61 maybeInit := true 62 args = cmdFlags.Args() 63 if len(args) > 1 { 64 c.Ui.Error("The apply command expects at most one argument.") 65 cmdFlags.Usage() 66 return 1 67 } else if len(args) == 1 { 68 configPath = args[0] 69 } else { 70 configPath = pwd 71 maybeInit = false 72 } 73 74 // Prepare the extra hooks to count resources 75 countHook := new(CountHook) 76 stateHook := new(StateHook) 77 c.Meta.extraHooks = []terraform.Hook{countHook, stateHook} 78 79 if !c.Destroy && maybeInit { 80 // Do a detect to determine if we need to do an init + apply. 81 if detected, err := getter.Detect(configPath, pwd, getter.Detectors); err != nil { 82 c.Ui.Error(fmt.Sprintf( 83 "Invalid path: %s", err)) 84 return 1 85 } else if !strings.HasPrefix(detected, "file") { 86 // If this isn't a file URL then we're doing an init + 87 // apply. 88 var init InitCommand 89 init.Meta = c.Meta 90 if code := init.Run([]string{detected}); code != 0 { 91 return code 92 } 93 94 // Change the config path to be the cwd 95 configPath = pwd 96 } 97 } 98 99 // Check for the new apply 100 if experiment.Enabled(experiment.X_newApply) && !experiment.Force() { 101 desc := "Experimental new apply graph has been enabled. This may still\n" + 102 "have bugs, and should be used with care. If you'd like to continue,\n" + 103 "you must enter exactly 'yes' as a response." 104 v, err := c.UIInput().Input(&terraform.InputOpts{ 105 Id: "Xnew-apply", 106 Query: "Experimental feature enabled: new apply graph. Continue?", 107 Description: desc, 108 }) 109 if err != nil { 110 c.Ui.Error(fmt.Sprintf("Error asking for confirmation: %s", err)) 111 return 1 112 } 113 if v != "yes" { 114 c.Ui.Output("Apply cancelled.") 115 return 1 116 } 117 } 118 119 // Check for the new destroy 120 if experiment.Enabled(experiment.X_newDestroy) && !experiment.Force() { 121 desc := "Experimental new destroy graph has been enabled. This may still\n" + 122 "have bugs, and should be used with care. If you'd like to continue,\n" + 123 "you must enter exactly 'yes' as a response." 124 v, err := c.UIInput().Input(&terraform.InputOpts{ 125 Id: "Xnew-destroy", 126 Query: "Experimental feature enabled: new destroy graph. Continue?", 127 Description: desc, 128 }) 129 if err != nil { 130 c.Ui.Error(fmt.Sprintf("Error asking for confirmation: %s", err)) 131 return 1 132 } 133 if v != "yes" { 134 c.Ui.Output("Apply cancelled.") 135 return 1 136 } 137 } 138 139 // Build the context based on the arguments given 140 ctx, planned, err := c.Context(contextOpts{ 141 Destroy: c.Destroy, 142 Path: configPath, 143 StatePath: c.Meta.statePath, 144 Parallelism: c.Meta.parallelism, 145 }) 146 if err != nil { 147 c.Ui.Error(err.Error()) 148 return 1 149 } 150 if c.Destroy && planned { 151 c.Ui.Error(fmt.Sprintf( 152 "Destroy can't be called with a plan file.")) 153 return 1 154 } 155 if !destroyForce && c.Destroy { 156 // Default destroy message 157 desc := "Terraform will delete all your managed infrastructure.\n" + 158 "There is no undo. Only 'yes' will be accepted to confirm." 159 160 // If targets are specified, list those to user 161 if c.Meta.targets != nil { 162 var descBuffer bytes.Buffer 163 descBuffer.WriteString("Terraform will delete the following infrastructure:\n") 164 for _, target := range c.Meta.targets { 165 descBuffer.WriteString("\t") 166 descBuffer.WriteString(target) 167 descBuffer.WriteString("\n") 168 } 169 descBuffer.WriteString("There is no undo. Only 'yes' will be accepted to confirm") 170 desc = descBuffer.String() 171 } 172 173 v, err := c.UIInput().Input(&terraform.InputOpts{ 174 Id: "destroy", 175 Query: "Do you really want to destroy?", 176 Description: desc, 177 }) 178 if err != nil { 179 c.Ui.Error(fmt.Sprintf("Error asking for confirmation: %s", err)) 180 return 1 181 } 182 if v != "yes" { 183 c.Ui.Output("Destroy cancelled.") 184 return 1 185 } 186 } 187 if !planned { 188 if err := ctx.Input(c.InputMode()); err != nil { 189 c.Ui.Error(fmt.Sprintf("Error configuring: %s", err)) 190 return 1 191 } 192 } 193 if !validateContext(ctx, c.Ui) { 194 return 1 195 } 196 197 // Plan if we haven't already 198 if !planned { 199 if refresh { 200 if _, err := ctx.Refresh(); err != nil { 201 c.Ui.Error(fmt.Sprintf("Error refreshing state: %s", err)) 202 return 1 203 } 204 } 205 206 if _, err := ctx.Plan(); err != nil { 207 c.Ui.Error(fmt.Sprintf( 208 "Error creating plan: %s", err)) 209 return 1 210 } 211 } 212 213 // Setup the state hook for continuous state updates 214 { 215 state, err := c.State() 216 if err != nil { 217 c.Ui.Error(fmt.Sprintf( 218 "Error reading state: %s", err)) 219 return 1 220 } 221 222 stateHook.State = state 223 } 224 225 // Start the apply in a goroutine so that we can be interrupted. 226 var state *terraform.State 227 var applyErr error 228 doneCh := make(chan struct{}) 229 go func() { 230 defer close(doneCh) 231 state, applyErr = ctx.Apply() 232 }() 233 234 // Wait for the apply to finish or for us to be interrupted so 235 // we can handle it properly. 236 err = nil 237 select { 238 case <-c.ShutdownCh: 239 c.Ui.Output("Interrupt received. Gracefully shutting down...") 240 241 // Stop execution 242 go ctx.Stop() 243 244 // Still get the result, since there is still one 245 select { 246 case <-c.ShutdownCh: 247 c.Ui.Error( 248 "Two interrupts received. Exiting immediately. Note that data\n" + 249 "loss may have occurred.") 250 return 1 251 case <-doneCh: 252 } 253 case <-doneCh: 254 } 255 256 // Persist the state 257 if state != nil { 258 if err := c.Meta.PersistState(state); err != nil { 259 c.Ui.Error(fmt.Sprintf("Failed to save state: %s", err)) 260 return 1 261 } 262 } 263 264 if applyErr != nil { 265 c.Ui.Error(fmt.Sprintf( 266 "Error applying plan:\n\n"+ 267 "%s\n\n"+ 268 "Terraform does not automatically rollback in the face of errors.\n"+ 269 "Instead, your Terraform state file has been partially updated with\n"+ 270 "any resources that successfully completed. Please address the error\n"+ 271 "above and apply again to incrementally change your infrastructure.", 272 multierror.Flatten(applyErr))) 273 return 1 274 } 275 276 if c.Destroy { 277 c.Ui.Output(c.Colorize().Color(fmt.Sprintf( 278 "[reset][bold][green]\n"+ 279 "Destroy complete! Resources: %d destroyed.", 280 countHook.Removed))) 281 } else { 282 c.Ui.Output(c.Colorize().Color(fmt.Sprintf( 283 "[reset][bold][green]\n"+ 284 "Apply complete! Resources: %d added, %d changed, %d destroyed.", 285 countHook.Added, 286 countHook.Changed, 287 countHook.Removed))) 288 } 289 290 if countHook.Added > 0 || countHook.Changed > 0 { 291 c.Ui.Output(c.Colorize().Color(fmt.Sprintf( 292 "[reset]\n"+ 293 "The state of your infrastructure has been saved to the path\n"+ 294 "below. This state is required to modify and destroy your\n"+ 295 "infrastructure, so keep it safe. To inspect the complete state\n"+ 296 "use the `terraform show` command.\n\n"+ 297 "State path: %s", 298 c.Meta.StateOutPath()))) 299 } 300 301 if !c.Destroy { 302 if outputs := outputsAsString(state, terraform.RootModulePath, ctx.Module().Config().Outputs, true); outputs != "" { 303 c.Ui.Output(c.Colorize().Color(outputs)) 304 } 305 } 306 307 return 0 308 } 309 310 func (c *ApplyCommand) Help() string { 311 if c.Destroy { 312 return c.helpDestroy() 313 } 314 315 return c.helpApply() 316 } 317 318 func (c *ApplyCommand) Synopsis() string { 319 if c.Destroy { 320 return "Destroy Terraform-managed infrastructure" 321 } 322 323 return "Builds or changes infrastructure" 324 } 325 326 func (c *ApplyCommand) helpApply() string { 327 helpText := ` 328 Usage: terraform apply [options] [DIR-OR-PLAN] 329 330 Builds or changes infrastructure according to Terraform configuration 331 files in DIR. 332 333 By default, apply scans the current directory for the configuration 334 and applies the changes appropriately. However, a path to another 335 configuration or an execution plan can be provided. Execution plans can be 336 used to only execute a pre-determined set of actions. 337 338 DIR can also be a SOURCE as given to the "init" command. In this case, 339 apply behaves as though "init" was called followed by "apply". This only 340 works for sources that aren't files, and only if the current working 341 directory is empty of Terraform files. This is a shortcut for getting 342 started. 343 344 Options: 345 346 -backup=path Path to backup the existing state file before 347 modifying. Defaults to the "-state-out" path with 348 ".backup" extension. Set to "-" to disable backup. 349 350 -input=true Ask for input for variables if not directly set. 351 352 -no-color If specified, output won't contain any color. 353 354 -parallelism=n Limit the number of concurrent operations. 355 Defaults to 10. 356 357 -refresh=true Update state prior to checking for differences. This 358 has no effect if a plan file is given to apply. 359 360 -state=path Path to read and save state (unless state-out 361 is specified). Defaults to "terraform.tfstate". 362 363 -state-out=path Path to write state to that is different than 364 "-state". This can be used to preserve the old 365 state. 366 367 -target=resource Resource to target. Operation will be limited to this 368 resource and its dependencies. This flag can be used 369 multiple times. 370 371 -var 'foo=bar' Set a variable in the Terraform configuration. This 372 flag can be set multiple times. 373 374 -var-file=foo Set variables in the Terraform configuration from 375 a file. If "terraform.tfvars" is present, it will be 376 automatically loaded if this flag is not specified. 377 378 379 ` 380 return strings.TrimSpace(helpText) 381 } 382 383 func (c *ApplyCommand) helpDestroy() string { 384 helpText := ` 385 Usage: terraform destroy [options] [DIR] 386 387 Destroy Terraform-managed infrastructure. 388 389 Options: 390 391 -backup=path Path to backup the existing state file before 392 modifying. Defaults to the "-state-out" path with 393 ".backup" extension. Set to "-" to disable backup. 394 395 -force Don't ask for input for destroy confirmation. 396 397 -no-color If specified, output won't contain any color. 398 399 -parallelism=n Limit the number of concurrent operations. 400 Defaults to 10. 401 402 -refresh=true Update state prior to checking for differences. This 403 has no effect if a plan file is given to apply. 404 405 -state=path Path to read and save state (unless state-out 406 is specified). Defaults to "terraform.tfstate". 407 408 -state-out=path Path to write state to that is different than 409 "-state". This can be used to preserve the old 410 state. 411 412 -target=resource Resource to target. Operation will be limited to this 413 resource and its dependencies. This flag can be used 414 multiple times. 415 416 -var 'foo=bar' Set a variable in the Terraform configuration. This 417 flag can be set multiple times. 418 419 -var-file=foo Set variables in the Terraform configuration from 420 a file. If "terraform.tfvars" is present, it will be 421 automatically loaded if this flag is not specified. 422 423 424 ` 425 return strings.TrimSpace(helpText) 426 } 427 428 func outputsAsString(state *terraform.State, modPath []string, schema []*config.Output, includeHeader bool) string { 429 if state == nil { 430 return "" 431 } 432 433 ms := state.ModuleByPath(modPath) 434 if ms == nil { 435 return "" 436 } 437 438 outputs := ms.Outputs 439 outputBuf := new(bytes.Buffer) 440 if len(outputs) > 0 { 441 schemaMap := make(map[string]*config.Output) 442 if schema != nil { 443 for _, s := range schema { 444 schemaMap[s.Name] = s 445 } 446 } 447 448 if includeHeader { 449 outputBuf.WriteString("[reset][bold][green]\nOutputs:\n\n") 450 } 451 452 // Output the outputs in alphabetical order 453 keyLen := 0 454 ks := make([]string, 0, len(outputs)) 455 for key, _ := range outputs { 456 ks = append(ks, key) 457 if len(key) > keyLen { 458 keyLen = len(key) 459 } 460 } 461 sort.Strings(ks) 462 463 for _, k := range ks { 464 schema, ok := schemaMap[k] 465 if ok && schema.Sensitive { 466 outputBuf.WriteString(fmt.Sprintf("%s = <sensitive>\n", k)) 467 continue 468 } 469 470 v := outputs[k] 471 switch typedV := v.Value.(type) { 472 case string: 473 outputBuf.WriteString(fmt.Sprintf("%s = %s\n", k, typedV)) 474 case []interface{}: 475 outputBuf.WriteString(formatListOutput("", k, typedV)) 476 outputBuf.WriteString("\n") 477 case map[string]interface{}: 478 outputBuf.WriteString(formatMapOutput("", k, typedV)) 479 outputBuf.WriteString("\n") 480 } 481 } 482 } 483 484 return strings.TrimSpace(outputBuf.String()) 485 }