diff --git a/.changes/v1.13/BUG FIXES-20250910-095424.yaml b/.changes/v1.13/BUG FIXES-20250910-095424.yaml new file mode 100644 index 0000000000..d34da5672b --- /dev/null +++ b/.changes/v1.13/BUG FIXES-20250910-095424.yaml @@ -0,0 +1,5 @@ +kind: BUG FIXES +body: 'apply: hide sensitive inputs when values have changed between plan and apply' +time: 2025-09-10T09:54:24.889605+02:00 +custom: + Issue: "37582" diff --git a/internal/backend/local/backend_apply.go b/internal/backend/local/backend_apply.go index cc734777a3..0dd3704950 100644 --- a/internal/backend/local/backend_apply.go +++ b/internal/backend/local/backend_apply.go @@ -17,6 +17,7 @@ import ( "github.com/hashicorp/terraform/internal/backend/backendrun" "github.com/hashicorp/terraform/internal/command/views" "github.com/hashicorp/terraform/internal/configs" + "github.com/hashicorp/terraform/internal/lang/marks" "github.com/hashicorp/terraform/internal/logging" "github.com/hashicorp/terraform/internal/plans" "github.com/hashicorp/terraform/internal/states" @@ -345,6 +346,14 @@ func (b *Local) opApply( Subject: rng, }) } else { + markedPlannedVar := plannedVar + markedParsedVar := parsedVar.Value + + if decl.Sensitive { + markedPlannedVar = markedPlannedVar.Mark(marks.Sensitive) + markedParsedVar = markedParsedVar.Mark(marks.Sensitive) + } + // The user can't override the planned variables, so we // error when possible to avoid confusion. if parsedVar.Value.Equals(plannedVar).False() { @@ -361,7 +370,7 @@ func (b *Local) opApply( "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), + varName, tfdiags.CompactValueStr(markedPlannedVar), tfdiags.CompactValueStr(markedParsedVar), parsedVar.SourceType.DiagnosticLabel()), Subject: rng, }) @@ -374,7 +383,7 @@ func (b *Local) opApply( "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), + varName, tfdiags.CompactValueStr(markedPlannedVar), tfdiags.CompactValueStr(markedParsedVar), parsedVar.SourceType.DiagnosticLabel()), Subject: rng, }) @@ -386,7 +395,7 @@ func (b *Local) opApply( 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), + varName, tfdiags.CompactValueStr(markedPlannedVar), tfdiags.CompactValueStr(markedParsedVar), parsedVar.SourceType.DiagnosticLabel())) } } diff --git a/internal/command/apply_test.go b/internal/command/apply_test.go index ddad5084c1..0d392be6ff 100644 --- a/internal/command/apply_test.go +++ b/internal/command/apply_test.go @@ -1053,6 +1053,78 @@ func TestApply_planWithEnvVars(t *testing.T) { } } +func TestApply_planWithSensitiveEnvVars(t *testing.T) { + _, snap := testModuleWithSnapshot(t, "apply-sensitive-variable") + 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", "unique") + + args := []string{ + "-state", statePath, + "-no-color", + planPath, + } + code := c.Run(args) + output := done(t) + if code != 0 { + t.Fatal("unexpected failure: ", output.All()) + } + + out := output.Stdout() + + expectedWarn := "Warning: Ignoring variable when applying a saved plan\n" + if !strings.Contains(out, expectedWarn) { + t.Fatalf("expected warning in output, given: %q", out) + } + + if !strings.Contains(out, "(sensitive value)") { + t.Error("should have elided sensitive value") + } + + if strings.Contains(out, "noot") { + t.Error("should have elided sensitive input, but contained value") + } + + if strings.Contains(out, "unique") { + t.Error("should have elided sensitive input, but contained value") + } +} + // 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-sensitive-variable/main.tf b/internal/command/testdata/apply-sensitive-variable/main.tf new file mode 100644 index 0000000000..566ff97928 --- /dev/null +++ b/internal/command/testdata/apply-sensitive-variable/main.tf @@ -0,0 +1,9 @@ +variable "shadow" { + type = string + sensitive = true +} + +output "foo" { + value = var.shadow + sensitive = true +}