package command import ( "bytes" "fmt" "sort" "strings" "github.com/hashicorp/terraform/backend" "github.com/hashicorp/terraform/plans/planfile" "github.com/hashicorp/terraform/repl" "github.com/hashicorp/terraform/states" "github.com/hashicorp/terraform/tfdiags" ) // ApplyCommand is a Command implementation that applies a Terraform // configuration and actually builds or changes infrastructure. type ApplyCommand struct { Meta // If true, then this apply command will become the "destroy" // command. It is just like apply but only processes a destroy. Destroy bool } func (c *ApplyCommand) Run(args []string) int { var refresh, autoApprove bool args = c.Meta.process(args) cmdName := "apply" if c.Destroy { cmdName = "destroy" } cmdFlags := c.Meta.extendedFlagSet(cmdName) cmdFlags.BoolVar(&autoApprove, "auto-approve", false, "skip interactive approval of plan before applying") cmdFlags.BoolVar(&refresh, "refresh", true, "refresh") cmdFlags.IntVar(&c.Meta.parallelism, "parallelism", DefaultParallelism, "parallelism") cmdFlags.StringVar(&c.Meta.statePath, "state", "", "path") cmdFlags.StringVar(&c.Meta.stateOutPath, "state-out", "", "path") cmdFlags.StringVar(&c.Meta.backupPath, "backup", "", "path") cmdFlags.BoolVar(&c.Meta.stateLock, "lock", true, "lock state") cmdFlags.DurationVar(&c.Meta.stateLockTimeout, "lock-timeout", 0, "lock timeout") cmdFlags.Usage = func() { c.Ui.Error(c.Help()) } if err := cmdFlags.Parse(args); err != nil { return 1 } var diags tfdiags.Diagnostics args = cmdFlags.Args() var planPath string if len(args) > 0 { planPath = args[0] args = args[1:] } configPath, err := ModulePath(args) if err != nil { c.Ui.Error(err.Error()) return 1 } // Check for user-supplied plugin path if c.pluginPath, err = c.loadPluginPath(); err != nil { c.Ui.Error(fmt.Sprintf("Error loading plugin path: %s", err)) return 1 } // Try to load plan if path is specified var planFile *planfile.Reader if planPath != "" { planFile, err = c.PlanFile(planPath) if err != nil { c.Ui.Error(err.Error()) return 1 } // If the path doesn't look like a plan, both planFile and err will be // nil. In that case, the user is probably trying to use the positional // argument to specify a configuration path. Point them at -chdir. if planFile == nil { c.Ui.Error(fmt.Sprintf("Failed to load %q as a plan file. Did you mean to use -chdir?", planPath)) return 1 } // If we successfully loaded a plan but this is a destroy operation, // explain that this is not supported. if c.Destroy { c.Ui.Error("Destroy can't be called with a plan file.") return 1 } } if planFile != nil { // Reset the config path for backend loading configPath = "" if !c.variableArgs.Empty() { diags = diags.Append(tfdiags.Sourceless( tfdiags.Error, "Can't set variables when applying a saved plan", "The -var and -var-file options cannot be used when applying a saved plan file, because a saved plan includes the variable values that were set when it was created.", )) c.showDiagnostics(diags) return 1 } } // Set up our count hook that keeps track of resource changes countHook := new(CountHook) c.ExtraHooks = append(c.ExtraHooks, countHook) // Load the backend var be backend.Enhanced var beDiags tfdiags.Diagnostics if planFile == nil { backendConfig, configDiags := c.loadBackendConfig(configPath) diags = diags.Append(configDiags) if configDiags.HasErrors() { c.showDiagnostics(diags) return 1 } be, beDiags = c.Backend(&BackendOpts{ Config: backendConfig, }) } else { plan, err := planFile.ReadPlan() if err != nil { diags = diags.Append(tfdiags.Sourceless( tfdiags.Error, "Failed to read plan from plan file", fmt.Sprintf("Cannot read the plan from the given plan file: %s.", err), )) c.showDiagnostics(diags) return 1 } if plan.Backend.Config == nil { // Should never happen; always indicates a bug in the creation of the plan file diags = diags.Append(tfdiags.Sourceless( tfdiags.Error, "Failed to read plan from plan file", "The given plan file does not have a valid backend configuration. This is a bug in the Terraform command that generated this plan file.", )) c.showDiagnostics(diags) return 1 } be, beDiags = c.BackendForPlan(plan.Backend) } diags = diags.Append(beDiags) if beDiags.HasErrors() { c.showDiagnostics(diags) return 1 } // Applying changes with dev overrides in effect could make it impossible // to switch back to a release version if the schema isn't compatible, // so we'll warn about it. diags = diags.Append(c.providerDevOverrideRuntimeWarnings()) // Before we delegate to the backend, we'll print any warning diagnostics // we've accumulated here, since the backend will start fresh with its own // diagnostics. c.showDiagnostics(diags) diags = nil // Build the operation opReq := c.Operation(be) opReq.AutoApprove = autoApprove opReq.ConfigDir = configPath opReq.Destroy = c.Destroy opReq.PlanFile = planFile opReq.PlanRefresh = refresh opReq.Type = backend.OperationTypeApply opReq.ConfigLoader, err = c.initConfigLoader() if err != nil { c.showDiagnostics(err) return 1 } { var moreDiags tfdiags.Diagnostics opReq.Variables, moreDiags = c.collectVariableValues() diags = diags.Append(moreDiags) if moreDiags.HasErrors() { c.showDiagnostics(diags) return 1 } } op, err := c.RunOperation(be, opReq) if err != nil { c.showDiagnostics(err) return 1 } if op.Result != backend.OperationSuccess { return op.Result.ExitStatus() } // Show the count results from the operation if c.Destroy { c.Ui.Output(c.Colorize().Color(fmt.Sprintf( "[reset][bold][green]\n"+ "Destroy complete! Resources: %d destroyed.", countHook.Removed))) } else { c.Ui.Output(c.Colorize().Color(fmt.Sprintf( "[reset][bold][green]\n"+ "Apply complete! Resources: %d added, %d changed, %d destroyed.", countHook.Added, countHook.Changed, countHook.Removed))) } // only show the state file help message if the state is local. if (countHook.Added > 0 || countHook.Changed > 0) && c.Meta.stateOutPath != "" { c.Ui.Output(c.Colorize().Color(fmt.Sprintf( "[reset]\n"+ "The state of your infrastructure has been saved to the path\n"+ "below. This state is required to modify and destroy your\n"+ "infrastructure, so keep it safe. To inspect the complete state\n"+ "use the `terraform show` command.\n\n"+ "State path: %s", c.Meta.stateOutPath))) } if !c.Destroy { if outputs := outputsAsString(op.State, true); outputs != "" { c.Ui.Output(c.Colorize().Color(outputs)) } } return op.Result.ExitStatus() } func (c *ApplyCommand) Help() string { if c.Destroy { return c.helpDestroy() } return c.helpApply() } func (c *ApplyCommand) Synopsis() string { if c.Destroy { return "Destroy previously-created infrastructure" } return "Create or update infrastructure" } func (c *ApplyCommand) helpApply() string { helpText := ` Usage: terraform apply [options] [PLAN] Creates or updates infrastructure according to Terraform configuration files in the current directory. By default, Terraform will generate a new plan and present it for your approval before taking any action. You can optionally provide a plan file created by a previous call to "terraform plan", in which case Terraform will take the actions described in that plan without any confirmation prompt. Options: -auto-approve Skip interactive approval of plan before applying. -backup=path Path to backup the existing state file before modifying. Defaults to the "-state-out" path with ".backup" extension. Set to "-" to disable backup. -compact-warnings If Terraform produces any warnings that are not accompanied by errors, show them in a more compact form that includes only the summary messages. -lock=true Lock the state file when locking is supported. -lock-timeout=0s Duration to retry a state lock. -input=true Ask for input for variables if not directly set. -no-color If specified, output won't contain any color. -parallelism=n Limit the number of parallel resource operations. Defaults to 10. -refresh=true Update state prior to checking for differences. This has no effect if a plan file is given to apply. -state=path Path to read and save state (unless state-out is specified). Defaults to "terraform.tfstate". -state-out=path Path to write state to that is different than "-state". This can be used to preserve the old state. -target=resource Resource to target. Operation will be limited to this resource and its dependencies. This flag can be used multiple times. -var 'foo=bar' Set a variable in the Terraform configuration. This flag can be set multiple times. -var-file=foo Set variables in the Terraform configuration from a file. If "terraform.tfvars" or any ".auto.tfvars" files are present, they will be automatically loaded. ` return strings.TrimSpace(helpText) } func (c *ApplyCommand) helpDestroy() string { helpText := ` Usage: terraform destroy [options] Destroy Terraform-managed infrastructure. Options: -backup=path Path to backup the existing state file before modifying. Defaults to the "-state-out" path with ".backup" extension. Set to "-" to disable backup. -auto-approve Skip interactive approval before destroying. -lock=true Lock the state file when locking is supported. -lock-timeout=0s Duration to retry a state lock. -no-color If specified, output won't contain any color. -parallelism=n Limit the number of concurrent operations. Defaults to 10. -refresh=true Update state prior to checking for differences. This has no effect if a plan file is given to apply. -state=path Path to read and save state (unless state-out is specified). Defaults to "terraform.tfstate". -state-out=path Path to write state to that is different than "-state". This can be used to preserve the old state. -target=resource Resource to target. Operation will be limited to this resource and its dependencies. This flag can be used multiple times. -var 'foo=bar' Set a variable in the Terraform configuration. This flag can be set multiple times. -var-file=foo Set variables in the Terraform configuration from a file. If "terraform.tfvars" or any ".auto.tfvars" files are present, they will be automatically loaded. ` return strings.TrimSpace(helpText) } func outputsAsString(state *states.State, includeHeader bool) string { if state == nil { return "" } ms := state.RootModule() outputs := ms.OutputValues outputBuf := new(bytes.Buffer) if len(outputs) > 0 { if includeHeader { outputBuf.WriteString("[reset][bold][green]\nOutputs:\n\n") } // Output the outputs in alphabetical order keyLen := 0 ks := make([]string, 0, len(outputs)) for key := range outputs { ks = append(ks, key) if len(key) > keyLen { keyLen = len(key) } } sort.Strings(ks) for _, k := range ks { v := outputs[k] if v.Sensitive { outputBuf.WriteString(fmt.Sprintf("%s = \n", k)) continue } result := repl.FormatValue(v.Value, 0) outputBuf.WriteString(fmt.Sprintf("%s = %s\n", k, result)) } } return strings.TrimSpace(outputBuf.String()) } const outputInterrupt = `Interrupt received. Please wait for Terraform to exit or data loss may occur. Gracefully shutting down...`