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