| // Copyright (c) HashiCorp, Inc. |
| // SPDX-License-Identifier: MPL-2.0 |
| |
| package command |
| |
| import ( |
| "fmt" |
| "strings" |
| |
| "github.com/hashicorp/terraform/internal/backend" |
| "github.com/hashicorp/terraform/internal/command/arguments" |
| "github.com/hashicorp/terraform/internal/command/views" |
| "github.com/hashicorp/terraform/internal/plans/planfile" |
| "github.com/hashicorp/terraform/internal/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(rawArgs []string) int { |
| var diags tfdiags.Diagnostics |
| |
| // Parse and apply global view arguments |
| common, rawArgs := arguments.ParseView(rawArgs) |
| c.View.Configure(common) |
| |
| // Propagate -no-color for legacy use of Ui. The remote backend and |
| // cloud package use this; it should be removed when/if they are |
| // migrated to views. |
| c.Meta.color = !common.NoColor |
| c.Meta.Color = c.Meta.color |
| |
| // Parse and validate flags |
| var args *arguments.Apply |
| switch { |
| case c.Destroy: |
| args, diags = arguments.ParseApplyDestroy(rawArgs) |
| default: |
| args, diags = arguments.ParseApply(rawArgs) |
| } |
| |
| // Instantiate the view, even if there are flag errors, so that we render |
| // diagnostics according to the desired view |
| view := views.NewApply(args.ViewType, c.Destroy, c.View) |
| |
| if diags.HasErrors() { |
| view.Diagnostics(diags) |
| view.HelpPrompt() |
| return 1 |
| } |
| |
| // Check for user-supplied plugin path |
| var err error |
| if c.pluginPath, err = c.loadPluginPath(); err != nil { |
| diags = diags.Append(err) |
| view.Diagnostics(diags) |
| return 1 |
| } |
| |
| // Attempt to load the plan file, if specified |
| planFile, diags := c.LoadPlanFile(args.PlanPath) |
| if diags.HasErrors() { |
| view.Diagnostics(diags) |
| return 1 |
| } |
| |
| // Check for invalid combination of plan file and variable overrides |
| if planFile != nil && !args.Vars.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.", |
| )) |
| view.Diagnostics(diags) |
| return 1 |
| } |
| |
| // FIXME: the -input flag value is needed to initialize the backend and the |
| // operation, but there is no clear path to pass this value down, so we |
| // continue to mutate the Meta object state for now. |
| c.Meta.input = args.InputEnabled |
| |
| // FIXME: the -parallelism flag is used to control the concurrency of |
| // Terraform operations. At the moment, this value is used both to |
| // initialize the backend via the ContextOpts field inside CLIOpts, and to |
| // set a largely unused field on the Operation request. Again, there is no |
| // clear path to pass this value down, so we continue to mutate the Meta |
| // object state for now. |
| c.Meta.parallelism = args.Operation.Parallelism |
| |
| // Prepare the backend, passing the plan file if present, and the |
| // backend-specific arguments |
| be, beDiags := c.PrepareBackend(planFile, args.State, args.ViewType) |
| diags = diags.Append(beDiags) |
| if diags.HasErrors() { |
| view.Diagnostics(diags) |
| return 1 |
| } |
| |
| // Build the operation request |
| opReq, opDiags := c.OperationRequest(be, view, args.ViewType, planFile, args.Operation, args.AutoApprove) |
| diags = diags.Append(opDiags) |
| |
| // Collect variable value and add them to the operation request |
| diags = diags.Append(c.GatherVariables(opReq, args.Vars)) |
| |
| // 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. |
| view.Diagnostics(diags) |
| if diags.HasErrors() { |
| return 1 |
| } |
| diags = nil |
| |
| // Run the operation |
| op, err := c.RunOperation(be, opReq) |
| if err != nil { |
| diags = diags.Append(err) |
| view.Diagnostics(diags) |
| return 1 |
| } |
| |
| if op.Result != backend.OperationSuccess { |
| return op.Result.ExitStatus() |
| } |
| |
| // Render the resource count and outputs, unless those counts are being |
| // rendered already in a remote Terraform process. |
| if rb, isRemoteBackend := be.(BackendWithRemoteTerraformVersion); !isRemoteBackend || rb.IsLocalOperations() { |
| view.ResourceCount(args.State.StateOutPath) |
| if !c.Destroy && op.State != nil { |
| view.Outputs(op.State.RootModule().OutputValues) |
| } |
| } |
| |
| view.Diagnostics(diags) |
| |
| if diags.HasErrors() { |
| return 1 |
| } |
| |
| return 0 |
| } |
| |
| func (c *ApplyCommand) LoadPlanFile(path string) (*planfile.Reader, tfdiags.Diagnostics) { |
| var planFile *planfile.Reader |
| var diags tfdiags.Diagnostics |
| |
| // Try to load plan if path is specified |
| if path != "" { |
| var err error |
| planFile, err = c.PlanFile(path) |
| if err != nil { |
| diags = diags.Append(tfdiags.Sourceless( |
| tfdiags.Error, |
| fmt.Sprintf("Failed to load %q as a plan file", path), |
| fmt.Sprintf("Error: %s", err), |
| )) |
| return nil, diags |
| } |
| |
| // 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 { |
| diags = diags.Append(tfdiags.Sourceless( |
| tfdiags.Error, |
| fmt.Sprintf("Failed to load %q as a plan file", path), |
| "The specified path is a directory, not a plan file. You can use the global -chdir flag to use this directory as the configuration root.", |
| )) |
| return nil, diags |
| } |
| |
| // If we successfully loaded a plan but this is a destroy operation, |
| // explain that this is not supported. |
| if c.Destroy { |
| diags = diags.Append(tfdiags.Sourceless( |
| tfdiags.Error, |
| "Destroy can't be called with a plan file", |
| fmt.Sprintf("If this plan was created using plan -destroy, apply it using:\n terraform apply %q", path), |
| )) |
| return nil, diags |
| } |
| } |
| |
| return planFile, diags |
| } |
| |
| func (c *ApplyCommand) PrepareBackend(planFile *planfile.Reader, args *arguments.State, viewType arguments.ViewType) (backend.Enhanced, tfdiags.Diagnostics) { |
| var diags tfdiags.Diagnostics |
| |
| // FIXME: we need to apply the state arguments to the meta object here |
| // because they are later used when initializing the backend. Carving a |
| // path to pass these arguments to the functions that need them is |
| // difficult but would make their use easier to understand. |
| c.Meta.applyStateArguments(args) |
| |
| // Load the backend |
| var be backend.Enhanced |
| var beDiags tfdiags.Diagnostics |
| if planFile == nil { |
| backendConfig, configDiags := c.loadBackendConfig(".") |
| diags = diags.Append(configDiags) |
| if configDiags.HasErrors() { |
| return nil, diags |
| } |
| |
| be, beDiags = c.Backend(&BackendOpts{ |
| Config: backendConfig, |
| ViewType: viewType, |
| }) |
| } 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), |
| )) |
| return nil, diags |
| } |
| 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.", |
| )) |
| return nil, diags |
| } |
| be, beDiags = c.BackendForPlan(plan.Backend) |
| } |
| |
| diags = diags.Append(beDiags) |
| if beDiags.HasErrors() { |
| return nil, diags |
| } |
| return be, diags |
| } |
| |
| func (c *ApplyCommand) OperationRequest( |
| be backend.Enhanced, |
| view views.Apply, |
| viewType arguments.ViewType, |
| planFile *planfile.Reader, |
| args *arguments.Operation, |
| autoApprove bool, |
| ) (*backend.Operation, tfdiags.Diagnostics) { |
| var diags tfdiags.Diagnostics |
| |
| // 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()) |
| |
| // Build the operation |
| opReq := c.Operation(be, viewType) |
| opReq.AutoApprove = autoApprove |
| opReq.ConfigDir = "." |
| opReq.PlanMode = args.PlanMode |
| opReq.Hooks = view.Hooks() |
| opReq.PlanFile = planFile |
| opReq.PlanRefresh = args.Refresh |
| opReq.Targets = args.Targets |
| opReq.ForceReplace = args.ForceReplace |
| opReq.Type = backend.OperationTypeApply |
| opReq.View = view.Operation() |
| |
| var err error |
| opReq.ConfigLoader, err = c.initConfigLoader() |
| if err != nil { |
| diags = diags.Append(fmt.Errorf("Failed to initialize config loader: %s", err)) |
| return nil, diags |
| } |
| |
| return opReq, diags |
| } |
| |
| func (c *ApplyCommand) GatherVariables(opReq *backend.Operation, args *arguments.Vars) tfdiags.Diagnostics { |
| var diags tfdiags.Diagnostics |
| |
| // FIXME the arguments package currently trivially gathers variable related |
| // arguments in a heterogenous slice, in order to minimize the number of |
| // code paths gathering variables during the transition to this structure. |
| // Once all commands that gather variables have been converted to this |
| // structure, we could move the variable gathering code to the arguments |
| // package directly, removing this shim layer. |
| |
| varArgs := args.All() |
| items := make([]rawFlag, len(varArgs)) |
| for i := range varArgs { |
| items[i].Name = varArgs[i].Name |
| items[i].Value = varArgs[i].Value |
| } |
| c.Meta.variableArgs = rawFlags{items: &items} |
| opReq.Variables, diags = c.collectVariableValues() |
| |
| return diags |
| } |
| |
| 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 [global options] 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. |
| |
| -destroy Destroy Terraform-managed infrastructure. |
| The command "terraform destroy" is a convenience alias |
| for this option. |
| |
| -lock=false Don't hold a state lock during the operation. This is |
| dangerous if others might concurrently run commands |
| against the same workspace. |
| |
| -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. |
| |
| -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. |
| |
| If you don't provide a saved plan file then this command will also accept |
| all of the plan-customization options accepted by the terraform plan command. |
| For more information on those options, run: |
| terraform plan -help |
| ` |
| return strings.TrimSpace(helpText) |
| } |
| |
| func (c *ApplyCommand) helpDestroy() string { |
| helpText := ` |
| Usage: terraform [global options] destroy [options] |
| |
| Destroy Terraform-managed infrastructure. |
| |
| This command is a convenience alias for: |
| terraform apply -destroy |
| |
| This command also accepts many of the plan-customization options accepted by |
| the terraform plan command. For more information on those options, run: |
| terraform plan -help |
| ` |
| return strings.TrimSpace(helpText) |
| } |