| // 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/tfdiags" | |
| ) | |
| // PlanCommand is a Command implementation that compares a Terraform | |
| // configuration to an actual infrastructure and shows the differences. | |
| type PlanCommand struct { | |
| Meta | |
| } | |
| func (c *PlanCommand) Run(rawArgs []string) int { | |
| // 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 | |
| args, diags := arguments.ParsePlan(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.NewPlan(args.ViewType, 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 | |
| } | |
| // 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 with the backend-specific arguments | |
| be, beDiags := c.PrepareBackend(args.State, args.ViewType) | |
| b, isRemoteBackend := be.(BackendWithRemoteTerraformVersion) | |
| if isRemoteBackend && !b.IsLocalOperations() { | |
| diags = diags.Append(c.providerDevOverrideRuntimeWarningsRemoteExecution()) | |
| } else { | |
| diags = diags.Append(c.providerDevOverrideRuntimeWarnings()) | |
| } | |
| diags = diags.Append(beDiags) | |
| if diags.HasErrors() { | |
| view.Diagnostics(diags) | |
| return 1 | |
| } | |
| // Build the operation request | |
| opReq, opDiags := c.OperationRequest(be, view, args.ViewType, args.Operation, args.OutPath, args.GenerateConfigPath, 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 plan 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 | |
| // Perform 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() | |
| } | |
| if args.DetailedExitCode && !op.PlanEmpty { | |
| return 2 | |
| } | |
| return op.Result.ExitStatus() | |
| } | |
| func (c *PlanCommand) Validate(args *arguments.Plan) (diags tfdiags.Diagnostics) { | |
| return diags.Append(validatePolicyPaths(args.PolicyPaths, c.AllowExperimentalFeatures)) | |
| } | |
| func (c *PlanCommand) PrepareBackend(args *arguments.State, viewType arguments.ViewType) (backendrun.OperationsBackend, 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 | |
| be, diags := c.backend(".", viewType) | |
| if diags.HasErrors() { | |
| return nil, diags | |
| } | |
| return be, diags | |
| } | |
| func (c *PlanCommand) OperationRequest(be backendrun.OperationsBackend, view views.Plan, viewType arguments.ViewType, args *arguments.Operation, planOutPath string, generateConfigOut string, policyPaths []string) (*backendrun.Operation, tfdiags.Diagnostics) { | |
| var diags tfdiags.Diagnostics | |
| // Build the operation | |
| opReq := c.Operation(be, viewType) | |
| opReq.ConfigDir = "." | |
| opReq.PlanMode = args.PlanMode | |
| opReq.Hooks = view.Hooks() | |
| opReq.PlanRefresh = args.Refresh | |
| opReq.PlanOutPath = planOutPath | |
| opReq.GenerateConfigOut = generateConfigOut | |
| opReq.Targets = args.Targets | |
| opReq.ForceReplace = args.ForceReplace | |
| opReq.Type = backendrun.OperationTypePlan | |
| opReq.View = view.Operation() | |
| opReq.ActionTargets = args.ActionTargets | |
| // 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 *PlanCommand) Help() string { | |
| helpText := ` | |
| Usage: terraform [global options] plan [options] | |
| Generates a speculative execution plan, showing what actions Terraform | |
| would take to apply the current configuration. This command will not | |
| actually perform the planned actions. | |
| You can optionally save the plan to a file, which you can then pass to | |
| the "apply" command to perform exactly the actions described in the plan. | |
| Plan Customization Options: | |
| The following options customize how Terraform will produce its plan. You | |
| can also use these options when you run "terraform apply" without passing | |
| it a saved plan, in order to plan and apply in a single command. | |
| -destroy Select the "destroy" planning mode, which creates a plan | |
| to destroy all objects currently managed by this | |
| Terraform configuration instead of the usual behavior. | |
| -refresh-only Select the "refresh only" planning mode, which checks | |
| whether remote objects still match the outcome of the | |
| most recent Terraform apply but does not propose any | |
| actions to undo any changes made outside of Terraform. | |
| -refresh=false Skip checking for external changes to remote objects | |
| while creating the plan. This can potentially make | |
| planning faster, but at the expense of possibly planning | |
| against a stale record of the remote system state. | |
| -replace=resource Force replacement of a particular resource instance using | |
| its resource address. If the plan would've normally | |
| produced an update or no-op action for this instance, | |
| Terraform will plan to replace it instead. You can use | |
| this option multiple times to replace more than one object. | |
| -target=resource Limit the planning operation to only the given module, | |
| resource, or resource instance and all of its | |
| dependencies. You can use this option multiple times to | |
| include more than one object. This is for exceptional | |
| use only. | |
| -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. | |
| Other Options: | |
| -compact-warnings If Terraform produces any warnings that are not | |
| accompanied by errors, shows them in a more compact | |
| form that includes only the summary messages. | |
| -detailed-exitcode Return detailed exit codes when the command exits. | |
| This will change the meaning of exit codes to: | |
| 0 - Succeeded, diff is empty (no changes) | |
| 1 - Errored | |
| 2 - Succeeded, there is a diff | |
| -generate-config-out=path (Experimental) If import blocks are present in | |
| configuration, instructs Terraform to generate HCL | |
| for any imported resources not already present. The | |
| configuration is written to a new file at PATH, | |
| which must not already exist. Terraform may still | |
| attempt to write configuration if the plan errors. | |
| -input=true Ask for input for variables if not directly set. | |
| -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. | |
| -no-color If specified, output won't contain any color. | |
| -out=path Write a plan file to the given path. This can be | |
| used as input to the "apply" command. | |
| -parallelism=n Limit the number of concurrent operations. Defaults | |
| to 10. | |
| -state=statefile A legacy option used for the local backend only. | |
| See the local backend's documentation for more | |
| information. | |
| ` | |
| return strings.TrimSpace(helpText) | |
| } | |
| func (c *PlanCommand) Synopsis() string { | |
| return "Show changes required by the current configuration" | |
| } |