diff --git a/.changes/v1.13/BUG FIXES-20250604-144021.yaml b/.changes/v1.13/BUG FIXES-20250604-144021.yaml new file mode 100644 index 000000000000..21bd5249b7ac --- /dev/null +++ b/.changes/v1.13/BUG FIXES-20250604-144021.yaml @@ -0,0 +1,5 @@ +kind: BUG FIXES +body: Nested module outputs could lose sensitivity, even when marked as such in the configuration +time: 2025-06-04T14:40:21.12567-04:00 +custom: + Issue: "37212" diff --git a/internal/terraform/context_plan2_test.go b/internal/terraform/context_plan2_test.go index fb1451879781..6df225d5a855 100644 --- a/internal/terraform/context_plan2_test.go +++ b/internal/terraform/context_plan2_test.go @@ -6847,3 +6847,68 @@ data "test_data_source" "foo" { } } + +func TestContext2Plan_sensitiveOutput(t *testing.T) { + m := testModuleInline(t, map[string]string{ + "main.tf": ` +module "child" { + source = "./child" +} + +output "is_secret" { + // not only must the plan store the output as sensitive, it must also be + // evaluated as such + value = issensitive(module.child.secret) +} +`, + "./child/main.tf": ` +output "secret" { + sensitive = true + value = "test" +} +`, + }) + + ctx := testContext2(t, &ContextOpts{}) + + plan, diags := ctx.Plan(m, states.NewState(), DefaultPlanOpts) + tfdiags.AssertNoErrors(t, diags) + + expectedChanges := &plans.Changes{ + Outputs: []*plans.OutputChange{ + { + Addr: mustAbsOutputValue("module.child.output.secret"), + Change: plans.Change{ + Action: plans.Create, + BeforeIdentity: cty.NullVal(cty.DynamicPseudoType), + AfterIdentity: cty.NullVal(cty.DynamicPseudoType), + Before: cty.NullVal(cty.DynamicPseudoType), + After: cty.StringVal("test"), + }, + Sensitive: true, + }, + { + Addr: mustAbsOutputValue("output.is_secret"), + Change: plans.Change{ + Action: plans.Create, + BeforeIdentity: cty.NullVal(cty.DynamicPseudoType), + AfterIdentity: cty.NullVal(cty.DynamicPseudoType), + Before: cty.NullVal(cty.DynamicPseudoType), + After: cty.True, + }, + }, + }, + } + changes, err := plan.Changes.Decode(nil) + if err != nil { + t.Fatal(err) + } + + sort.SliceStable(changes.Outputs, func(i, j int) bool { + return changes.Outputs[i].Addr.String() < changes.Outputs[j].Addr.String() + }) + + if diff := cmp.Diff(expectedChanges, changes, ctydebug.CmpOptions); diff != "" { + t.Fatalf("unexpected changes: %s", diff) + } +} diff --git a/internal/terraform/evaluate.go b/internal/terraform/evaluate.go index e3151ea654d1..573b937bf0e6 100644 --- a/internal/terraform/evaluate.go +++ b/internal/terraform/evaluate.go @@ -430,7 +430,7 @@ func (d *evaluationStateData) GetModule(addr addrs.ModuleCall, rng tfdiags.Sourc namedVals := d.Evaluator.NamedValues moduleInstAddr := absAddr.Instance(instKey) attrs := make(map[string]cty.Value, len(outputConfigs)) - for name := range outputConfigs { + for name, cfg := range outputConfigs { outputAddr := moduleInstAddr.OutputValue(name) // Although we do typically expect the graph dependencies to @@ -446,6 +446,9 @@ func (d *evaluationStateData) GetModule(addr addrs.ModuleCall, rng tfdiags.Sourc continue } outputVal := namedVals.GetOutputValue(outputAddr) + if cfg.Sensitive { + outputVal = outputVal.Mark(marks.Sensitive) + } attrs[name] = outputVal }