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