fix(apply): Warn users when apply is passed plan options -target, -destroy and -refresh-only if they disagree with the saved plan in use.
Project: hashicorp/terraform | Language: Go | Concepts: error-handling, cli-design, backwards-compatibility, testing-strategy
Difficulty: mid
Link: https://github.com/hashicorp/terraform/pull/38698
The problem
When running terraform apply with a saved plan file, users could still pass planning-related flags like -target, -destroy, or -refresh-only. These flags have no effect when a plan file is provided, but Terraform would silently ignore them, potentially confusing users who expected them to be honored.
Why it's non-trivial
A naive fix would be to simply error out or block these flags entirely. However, this would break existing workflows (such as CI/CD pipelines) that pass these flags by default, even when they're not needed. The solution needs to warn users about ignored flags without breaking backwards compatibility.
The approach
The PR adds logic to detect when planning-related flags are passed to apply with a plan file, and issues warnings if the flags' intent doesn't match the plan file's contents.
Warn on mismatched -target flags:
+ if len(op.Targets) != 0 {
+ // Do target flags all match targets in the plan?
+ for _, target := range op.Targets {
+ found := false
+ for _, planTarget := range plan.TargetAddrs {
+ if target.TargetContains(planTarget) {
+ found = true
+ break
+ }
+ }
+ if !found {
+ diags = diags.Append(tfdiags.Sourceless(
+ tfdiags.Warning,
+ "Can't change resource targeting when applying a saved plan",
+ fmt.Sprintf("The target address %q was supplied using a -target flag but does not match a target in the saved plan file. This flag will be ignored and won't influence the apply operation.", target),
+ ))
+ }
+ }
+ }
This code checks if any -target flags were passed that do not match the targets in the plan file. If so, it appends a warning diagnostic, informing the user that the flag will be ignored.
Warn on mismatched -destroy and -refresh-only flags:
+ if op.PlanMode != plan.UIMode {
+ if op.PlanMode == plans.DestroyMode {
+ diags = diags.Append(tfdiags.Sourceless(
+ tfdiags.Warning,
+ "Can't change plan mode when applying a saved plan",
+ "The -destroy flag was supplied to the apply command, but the plan file was not created with the -destroy flag. This flag will be ignored and won't influence the apply operation.",
+ ))
+ }
+ if op.PlanMode == plans.RefreshOnlyMode {
+ diags = diags.Append(tfdiags.Sourceless(
+ tfdiags.Warning,
+ "Can't change plan mode when applying a saved plan",
+ "The -refresh-only flag was supplied to the apply command, but the plan file was not created with the -refresh-only flag. This flag will be ignored and won't influence the apply operation.",
+ ))
+ }
+ }
This block checks if -destroy or -refresh-only flags are set during apply, but the plan file was not created with those modes. If so, a warning is issued.
Test coverage for the new warnings:
+func TestApply_changedPlanOptions_applyTime(t *testing.T) {
+ t.Run("change to -target", func(t *testing.T) {
+ ...
+ args := []string{
+ "-no-color",
+ "-target", "test_instance.foo",
+ "-state-out", statePath,
+ planPath,
+ }
+ code := c.Run(args)
+ output := done(t)
+ if !strings.Contains(output.Stdout(), `Warning: Can't change resource targeting when applying a saved plan`) {
+ t.Fatalf("missing warning text:\n%s", output.All())
+ }
+ })
+ t.Run("change to -destroy", func(t *testing.T) {
+ ...
+ args := []string{
+ "-no-color",
+ "-destroy",
+ "-state-out", statePath,
+ planPath,
+ }
+ code := c.Run(args)
+ output := done(t)
+ if !strings.Contains(output.Stdout(), `Warning: Can't change plan mode when applying a saved plan`) {
+ t.Fatalf("missing warning:\n%s", output.All())
+ }
+ })
+ t.Run("change to -refresh-only", func(t *testing.T) {
+ ...
+ args := []string{
+ "-no-color",
+ "-refresh-only",
+ "-state-out", statePath,
+ planPath,
+ }
+ code := c.Run(args)
+ output := done(t)
+ if !strings.Contains(output.Stdout(), `Warning: Can't change plan mode when applying a saved plan`) {
+ t.Fatalf("missing warning text:\n%s", output.All())
+ }
+ })
+}
These tests verify that the appropriate warnings are shown when -target, -destroy, or -refresh-only are passed to apply with a plan file.
From the review thread
mildwonkey: this needs to be categorized and documented as a breaking change, please - workflows that worked before will not, with this validation in place
SarahFrench: Thanks, I've addressed this by changing to raising only warnings. I'll update the text to reflect this.
Takeaways
- When changing CLI behavior, prefer warnings over errors to avoid breaking existing user workflows, especially in widely used automation.
- Always surface ignored or overridden user input explicitly; silent failures or ignored flags can lead to confusion and hard-to-diagnose bugs.
- Comprehensive tests should cover not just functional success, but also user-facing warnings and messages for edge cases.