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