diff --git a/.changes/backported/BUG FIXES-20250205-36435.yaml b/.changes/backported/BUG FIXES-20250205-36435.yaml new file mode 100644 index 000000000000..a3db320dbfde --- /dev/null +++ b/.changes/backported/BUG FIXES-20250205-36435.yaml @@ -0,0 +1,5 @@ +kind: BUG FIXES +body: Attempting to override a variable during `apply` via `TF_VAR_` environment variable will now yield warning instead of misleading error. +time: 2025-02-05T12:53:26.000+00:00 +custom: + Issue: "36435" diff --git a/internal/backend/local/backend_apply.go b/internal/backend/local/backend_apply.go index cb8bd0268180..413f92afc693 100644 --- a/internal/backend/local/backend_apply.go +++ b/internal/backend/local/backend_apply.go @@ -340,16 +340,49 @@ func (b *Local) opApply( }) } else { // The user can't override the planned variables, so we - // error when possible to avoid confusion. If the parsed - // variables comes from an auto-file however, it's not input - // directly by the user so we have to ignore it. - if parsedVar.Value.Equals(plannedVar).False() && parsedVar.SourceType != terraform.ValueFromAutoFile { - diags = diags.Append(&hcl.Diagnostic{ - Severity: hcl.DiagError, - Summary: "Can't change variable when applying a saved plan", - Detail: fmt.Sprintf("The variable %s cannot be set using the -var and -var-file options when applying a saved plan file, because a saved plan includes the variable values that were set when it was created. The saved plan specifies %s as the value whereas during apply the value %s was %s. To declare an ephemeral variable which is not saved in the plan file, use ephemeral = true.", varName, tfdiags.CompactValueStr(parsedVar.Value), tfdiags.CompactValueStr(plannedVar), parsedVar.SourceType.DiagnosticLabel()), - Subject: rng, - }) + // error when possible to avoid confusion. + if parsedVar.Value.Equals(plannedVar).False() { + switch parsedVar.SourceType { + case terraform.ValueFromAutoFile: + // If the parsed variables comes from an auto-file, + // it's not input directly by the user so we have to ignore it. + continue + case terraform.ValueFromEnvVar: + diags = diags.Append(&hcl.Diagnostic{ + Severity: hcl.DiagWarning, + Summary: "Ignoring variable when applying a saved plan", + Detail: fmt.Sprintf("The variable %s cannot be overriden when applying a saved plan file, "+ + "because a saved plan includes the variable values that were set when it was created. "+ + "The saved plan specifies %s as the value whereas during apply the value %s was %s. "+ + "To declare an ephemeral variable which is not saved in the plan file, use ephemeral = true.", + varName, tfdiags.CompactValueStr(plannedVar), tfdiags.CompactValueStr(parsedVar.Value), + parsedVar.SourceType.DiagnosticLabel()), + Subject: rng, + }) + case terraform.ValueFromCLIArg, terraform.ValueFromNamedFile: + diags = diags.Append(&hcl.Diagnostic{ + Severity: hcl.DiagError, + Summary: "Can't change variable when applying a saved plan", + Detail: fmt.Sprintf("The variable %s cannot be set using the -var and -var-file options when "+ + "applying a saved plan file, because a saved plan includes the variable values that were "+ + "set when it was created. The saved plan specifies %s as the value whereas during apply "+ + "the value %s was %s. To declare an ephemeral variable which is not saved in the plan "+ + "file, use ephemeral = true.", + varName, tfdiags.CompactValueStr(plannedVar), tfdiags.CompactValueStr(parsedVar.Value), + parsedVar.SourceType.DiagnosticLabel()), + Subject: rng, + }) + default: + // Other SourceTypes should never reach this point because + // - ValueFromConfig - supplied plan already contains the original configuration + // - ValueFromInput - we disable prompt when plan file is supplied + // - ValueFromCaller - only used in tests + panic(fmt.Sprintf("Attempted to change variable %s when applying a saved plan. "+ + "The saved plan specifies %s as the value whereas during apply the value %s was %s. "+ + "This is a bug in Terraform, please report it.", + varName, tfdiags.CompactValueStr(plannedVar), tfdiags.CompactValueStr(parsedVar.Value), + parsedVar.SourceType.DiagnosticLabel())) + } } } } diff --git a/internal/command/apply_test.go b/internal/command/apply_test.go index 2880c2e2dc99..19cdf4052c7c 100644 --- a/internal/command/apply_test.go +++ b/internal/command/apply_test.go @@ -994,6 +994,64 @@ func TestApply_planUndeclaredVars(t *testing.T) { } } +func TestApply_planWithEnvVars(t *testing.T) { + _, snap := testModuleWithSnapshot(t, "apply-output-only") + plan := testPlan(t) + + addr, diags := addrs.ParseAbsOutputValueStr("output.shadow") + if diags.HasErrors() { + t.Fatal(diags.Err()) + } + + shadowVal := mustNewDynamicValue("noot", cty.DynamicPseudoType) + plan.VariableValues = map[string]plans.DynamicValue{ + "shadow": shadowVal, + } + plan.Changes.Outputs = append(plan.Changes.Outputs, &plans.OutputChangeSrc{ + Addr: addr, + ChangeSrc: plans.ChangeSrc{ + Action: plans.Create, + After: shadowVal, + }, + }) + planPath := testPlanFileMatchState( + t, + snap, + states.NewState(), + plan, + statemgr.SnapshotMeta{}, + ) + + statePath := testTempFile(t) + + p := applyFixtureProvider() + view, done := testView(t) + c := &ApplyCommand{ + Meta: Meta{ + testingOverrides: metaOverridesForProvider(p), + View: view, + }, + } + + t.Setenv("TF_VAR_shadow", "env") + + args := []string{ + "-state", statePath, + "-no-color", + planPath, + } + code := c.Run(args) + output := done(t) + if code != 0 { + t.Fatal("unexpected failure: ", output.All()) + } + + expectedWarn := "Warning: Ignoring variable when applying a saved plan\n" + if !strings.Contains(output.Stdout(), expectedWarn) { + t.Fatalf("expected warning in output, given: %q", output.Stdout()) + } +} + // A saved plan includes a list of "apply-time variables", i.e. ephemeral // input variables that were set during the plan, and must therefore be set // during apply. No other variables may be set during apply. diff --git a/internal/command/testdata/apply-output-only/main.tf b/internal/command/testdata/apply-output-only/main.tf new file mode 100644 index 000000000000..af93614d23c0 --- /dev/null +++ b/internal/command/testdata/apply-output-only/main.tf @@ -0,0 +1,7 @@ +variable "shadow" { + type = string +} + +output "foo" { + value = var.shadow +}