github.com/subuk/terraform@v0.6.14-0.20160317140351-de1567c2e732/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] 280 281 Builds or changes infrastructure according to Terraform configuration 282 files in DIR. 283 284 DIR can also be a SOURCE as given to the "init" command. In this case, 285 apply behaves as though "init" was called followed by "apply". This only 286 works for sources that aren't files, and only if the current working 287 directory is empty of Terraform files. This is a shortcut for getting 288 started. 289 290 Options: 291 292 -backup=path Path to backup the existing state file before 293 modifying. Defaults to the "-state-out" path with 294 ".backup" extension. Set to "-" to disable backup. 295 296 -input=true Ask for input for variables if not directly set. 297 298 -no-color If specified, output won't contain any color. 299 300 -parallelism=n Limit the number of concurrent operations. 301 Defaults to 10. 302 303 -refresh=true Update state prior to checking for differences. This 304 has no effect if a plan file is given to apply. 305 306 -state=path Path to read and save state (unless state-out 307 is specified). Defaults to "terraform.tfstate". 308 309 -state-out=path Path to write state to that is different than 310 "-state". This can be used to preserve the old 311 state. 312 313 -target=resource Resource to target. Operation will be limited to this 314 resource and its dependencies. This flag can be used 315 multiple times. 316 317 -var 'foo=bar' Set a variable in the Terraform configuration. This 318 flag can be set multiple times. 319 320 -var-file=foo Set variables in the Terraform configuration from 321 a file. If "terraform.tfvars" is present, it will be 322 automatically loaded if this flag is not specified. 323 324 325 ` 326 return strings.TrimSpace(helpText) 327 } 328 329 func (c *ApplyCommand) helpDestroy() string { 330 helpText := ` 331 Usage: terraform destroy [options] [DIR] 332 333 Destroy Terraform-managed infrastructure. 334 335 Options: 336 337 -backup=path Path to backup the existing state file before 338 modifying. Defaults to the "-state-out" path with 339 ".backup" extension. Set to "-" to disable backup. 340 341 -force Don't ask for input for destroy confirmation. 342 343 -no-color If specified, output won't contain any color. 344 345 -parallelism=n Limit the number of concurrent operations. 346 Defaults to 10. 347 348 -refresh=true Update state prior to checking for differences. This 349 has no effect if a plan file is given to apply. 350 351 -state=path Path to read and save state (unless state-out 352 is specified). Defaults to "terraform.tfstate". 353 354 -state-out=path Path to write state to that is different than 355 "-state". This can be used to preserve the old 356 state. 357 358 -target=resource Resource to target. Operation will be limited to this 359 resource and its dependencies. This flag can be used 360 multiple times. 361 362 -var 'foo=bar' Set a variable in the Terraform configuration. This 363 flag can be set multiple times. 364 365 -var-file=foo Set variables in the Terraform configuration from 366 a file. If "terraform.tfvars" is present, it will be 367 automatically loaded if this flag is not specified. 368 369 370 ` 371 return strings.TrimSpace(helpText) 372 } 373 374 func outputsAsString(state *terraform.State) string { 375 if state == nil { 376 return "" 377 } 378 379 outputs := state.RootModule().Outputs 380 outputBuf := new(bytes.Buffer) 381 if len(outputs) > 0 { 382 outputBuf.WriteString("[reset][bold][green]\nOutputs:\n\n") 383 384 // Output the outputs in alphabetical order 385 keyLen := 0 386 keys := make([]string, 0, len(outputs)) 387 for key, _ := range outputs { 388 keys = append(keys, key) 389 if len(key) > keyLen { 390 keyLen = len(key) 391 } 392 } 393 sort.Strings(keys) 394 395 for _, k := range keys { 396 v := outputs[k] 397 398 outputBuf.WriteString(fmt.Sprintf( 399 " %s%s = %s\n", 400 k, 401 strings.Repeat(" ", keyLen-len(k)), 402 v)) 403 } 404 } 405 406 return strings.TrimSpace(outputBuf.String()) 407 }