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