| // Copyright IBM Corp. 2014, 2026 | |
| // SPDX-License-Identifier: BUSL-1.1 | |
| package command | |
| import ( | |
| "fmt" | |
| "strings" | |
| "github.com/hashicorp/terraform/internal/backend/backendrun" | |
| "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) | |
| } | |
| diags = diags.Append(c.Validate(args)) | |
| // 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, loadPlanFileDiags := c.LoadPlanFile(args.PlanPath) | |
| diags = diags.Append(loadPlanFileDiags) | |
| if diags.HasErrors() { | |
| 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, args.PolicyPaths) | |
| diags = diags.Append(opDiags) | |
| if diags.HasErrors() { | |
| view.Diagnostics(diags) | |
| return 1 | |
| } | |
| if len(args.PolicyPaths) > 0 { | |
| client, policyDiags, stopClient := c.PolicyClient(c.CommandContext(), args.PolicyPaths, backendPolicyEntitlement(be)) | |
| // if there has been any errors when setting up the policy client, we log them but | |
| // we still proceed with the operation, as a failure to set up the policy client | |
| // should not prevent the apply operation from running | |
| if opReq.View != nil && policyDiags != nil { | |
| opReq.View.PolicyDiagnostics(policyDiags) | |
| } | |
| opReq.PolicyClient = client | |
| defer stopClient() | |
| } | |
| // Collect variable value and add them to the operation request | |
| var varDiags tfdiags.Diagnostics | |
| opReq.Variables, varDiags = args.Vars.CollectValues(func(filename string, src []byte) { | |
| opReq.ConfigLoader.Parser().ForceFileSource(filename, src) | |
| }) | |
| diags = diags.Append(varDiags) | |
| // 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 != backendrun.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.RootOutputValues) | |
| } | |
| } | |
| view.Diagnostics(diags) | |
| if diags.HasErrors() { | |
| return 1 | |
| } | |
| return 0 | |
| } | |
| func (c *ApplyCommand) Validate(args *arguments.Apply) (diags tfdiags.Diagnostics) { | |
| return diags.Append(validatePolicyPaths(args.PolicyPaths, c.AllowExperimentalFeatures)) | |
| } | |
| func (c *ApplyCommand) LoadPlanFile(path string) (*planfile.WrappedPlanFile, tfdiags.Diagnostics) { | |
| var planFile *planfile.WrappedPlanFile | |
| 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.WrappedPlanFile, args *arguments.State, viewType arguments.ViewType) (backendrun.OperationsBackend, 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 backendrun.OperationsBackend | |
| var beDiags tfdiags.Diagnostics | |
| if lp, ok := planFile.Local(); ok { | |
| plan, err := lp.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 == nil && plan.StateStore == 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 has neither a valid backend nor state store configuration. This is a bug in the Terraform command that generated this plan file.", | |
| )) | |
| return nil, diags | |
| } | |
| be, beDiags = c.BackendForLocalPlan(plan) | |
| } else { | |
| // Load the backend | |
| // | |
| // Note: Both new plans and saved cloud plans load their backend from config, | |
| // hence the config parsing in the method below. | |
| be, beDiags = c.backend(".", viewType) | |
| } | |
| diags = diags.Append(beDiags) | |
| if beDiags.HasErrors() { | |
| return nil, diags | |
| } | |
| return be, diags | |
| } | |
| func (c *ApplyCommand) OperationRequest(be backendrun.OperationsBackend, view views.Apply, viewType arguments.ViewType, planFile *planfile.WrappedPlanFile, args *arguments.Operation, autoApprove bool, policyPaths []string) (*backendrun.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. | |
| b, isRemoteBackend := be.(BackendWithRemoteTerraformVersion) | |
| if isRemoteBackend && !b.IsLocalOperations() { | |
| diags = diags.Append(c.providerDevOverrideRuntimeWarningsRemoteExecution()) | |
| } else { | |
| 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 = backendrun.OperationTypeApply | |
| opReq.View = view.Operation() | |
| opReq.StatePersistInterval = c.Meta.StatePersistInterval() | |
| opReq.ActionTargets = args.ActionTargets | |
| opReq.PolicyPaths = policyPaths | |
| // EXPERIMENTAL: maybe enable deferred actions | |
| if c.AllowExperimentalFeatures { | |
| opReq.DeferralAllowed = args.DeferralAllowed | |
| } else if args.DeferralAllowed { | |
| // Belated flag parse error, since we don't know about experiments | |
| // support at actual parse time. | |
| diags = diags.Append(tfdiags.Sourceless( | |
| tfdiags.Error, | |
| "Failed to parse command-line flags", | |
| "The -allow-deferral flag is only valid in experimental builds of Terraform.", | |
| )) | |
| return nil, diags | |
| } | |
| 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) 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. | |
| -replace=resource Terraform will plan to replace this resource instance | |
| instead of doing an update or no-op action. | |
| -state=path Path to read and save state (unless state-out | |
| is specified). Defaults to "terraform.tfstate". | |
| Legacy option for the local backend only. See the local | |
| backend's documentation for more information. | |
| -state-out=path Path to write state to that is different than | |
| "-state". This can be used to preserve the old | |
| state. | |
| Legacy option for the local backend only. See the local | |
| backend's documentation for more information. | |
| -var 'foo=bar' Set a value for one of the input variables in the root | |
| module of the configuration. Use this option more than | |
| once to set more than one variable. | |
| -var-file=filename Load variable values from the given file, in addition | |
| to the default files terraform.tfvars and *.auto.tfvars. | |
| Use this option more than once to include more than one | |
| variables file. | |
| 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) | |
| } |