From e4b319401e121d89ca074eda838d7a9aed86c0ef Mon Sep 17 00:00:00 2001 From: Alisdair McDiarmid Date: Wed, 13 Dec 2023 19:38:22 -0500 Subject: [PATCH 1/3] stacksruntime: Add sensitive outputs tests --- internal/stacks/stackruntime/plan_test.go | 147 ++++++++++++++++++ .../sensitive-output-nested.tfstack.hcl | 11 ++ .../test/sensitive-output/sensitive-output.tf | 4 + .../sensitive-output.tfstack.hcl | 10 ++ 4 files changed, 172 insertions(+) create mode 100644 internal/stacks/stackruntime/testdata/mainbundle/test/sensitive-output-nested/sensitive-output-nested.tfstack.hcl create mode 100644 internal/stacks/stackruntime/testdata/mainbundle/test/sensitive-output/sensitive-output.tf create mode 100644 internal/stacks/stackruntime/testdata/mainbundle/test/sensitive-output/sensitive-output.tfstack.hcl diff --git a/internal/stacks/stackruntime/plan_test.go b/internal/stacks/stackruntime/plan_test.go index c5ffe77d9b..96dc6ab418 100644 --- a/internal/stacks/stackruntime/plan_test.go +++ b/internal/stacks/stackruntime/plan_test.go @@ -15,6 +15,7 @@ import ( terraformProvider "github.com/hashicorp/terraform/internal/builtin/providers/terraform" "github.com/hashicorp/terraform/internal/collections" "github.com/hashicorp/terraform/internal/configs/configschema" + "github.com/hashicorp/terraform/internal/lang/marks" "github.com/hashicorp/terraform/internal/plans" "github.com/hashicorp/terraform/internal/providers" "github.com/hashicorp/terraform/internal/stacks/stackaddrs" @@ -236,6 +237,152 @@ func TestPlanVariableOutputRoundtripNested(t *testing.T) { } } +var cmpCollectionsSet = cmp.Comparer(func(x, y collections.Set[stackaddrs.AbsComponent]) bool { + if x.Len() != y.Len() { + return false + } + + for _, v := range x.Elems() { + if !y.Has(v) { + return false + } + } + + return true +}) + +func TestPlanSensitiveOutput(t *testing.T) { + ctx := context.Background() + cfg := loadMainBundleConfigForTest(t, "sensitive-output") + + fakePlanTimestamp, err := time.Parse(time.RFC3339, "1991-08-25T20:57:08Z") + if err != nil { + t.Fatal(err) + } + + changesCh := make(chan stackplan.PlannedChange, 8) + diagsCh := make(chan tfdiags.Diagnostic, 2) + req := PlanRequest{ + Config: cfg, + ForcePlanTimestamp: &fakePlanTimestamp, + } + resp := PlanResponse{ + PlannedChanges: changesCh, + Diagnostics: diagsCh, + } + + go Plan(ctx, &req, &resp) + gotChanges, diags := collectPlanOutput(changesCh, diagsCh) + + if len(diags) != 0 { + t.Errorf("unexpected diagnostics\n%s", diags.ErrWithWarnings().Error()) + } + + wantChanges := []stackplan.PlannedChange{ + &stackplan.PlannedChangeApplyable{ + Applyable: true, + }, + &stackplan.PlannedChangeComponentInstance{ + Addr: stackaddrs.Absolute( + stackaddrs.RootStackInstance, + stackaddrs.ComponentInstance{ + Component: stackaddrs.Component{Name: "self"}, + }, + ), + Action: plans.Create, + PlannedInputValues: make(map[string]plans.DynamicValue), + PlannedOutputValues: map[string]cty.Value{ + "out": cty.StringVal("secret").Mark(marks.Sensitive), + }, + PlanTimestamp: fakePlanTimestamp, + }, + &stackplan.PlannedChangeHeader{ + TerraformVersion: version.SemVer, + }, + &stackplan.PlannedChangeOutputValue{ + Addr: stackaddrs.OutputValue{Name: "result"}, + Action: plans.Create, + OldValue: plans.DynamicValue{0xc0}, // MessagePack nil + NewValue: mustPlanDynamicValue(cty.StringVal("secret")), + NewValueMarks: []cty.PathValueMarks{{Marks: cty.NewValueMarks(marks.Sensitive)}}, + }, + } + sort.SliceStable(gotChanges, func(i, j int) bool { + // An arbitrary sort just to make the result stable for comparison. + return fmt.Sprintf("%T", gotChanges[i]) < fmt.Sprintf("%T", gotChanges[j]) + }) + + if diff := cmp.Diff(wantChanges, gotChanges, ctydebug.CmpOptions, cmpCollectionsSet); diff != "" { + t.Errorf("wrong changes\n%s", diff) + } +} + +func TestPlanSensitiveOutputNested(t *testing.T) { + ctx := context.Background() + cfg := loadMainBundleConfigForTest(t, "sensitive-output-nested") + + fakePlanTimestamp, err := time.Parse(time.RFC3339, "1991-08-25T20:57:08Z") + if err != nil { + t.Fatal(err) + } + + changesCh := make(chan stackplan.PlannedChange, 8) + diagsCh := make(chan tfdiags.Diagnostic, 2) + req := PlanRequest{ + Config: cfg, + ForcePlanTimestamp: &fakePlanTimestamp, + } + resp := PlanResponse{ + PlannedChanges: changesCh, + Diagnostics: diagsCh, + } + + go Plan(ctx, &req, &resp) + gotChanges, diags := collectPlanOutput(changesCh, diagsCh) + + if len(diags) != 0 { + t.Errorf("unexpected diagnostics\n%s", diags.ErrWithWarnings().Error()) + } + + wantChanges := []stackplan.PlannedChange{ + &stackplan.PlannedChangeApplyable{ + Applyable: true, + }, + &stackplan.PlannedChangeComponentInstance{ + Addr: stackaddrs.Absolute( + stackaddrs.RootStackInstance.Child("child", addrs.NoKey), + stackaddrs.ComponentInstance{ + Component: stackaddrs.Component{Name: "self"}, + }, + ), + Action: plans.Create, + PlannedInputValues: make(map[string]plans.DynamicValue), + PlannedOutputValues: map[string]cty.Value{ + "out": cty.StringVal("secret").Mark(marks.Sensitive), + }, + PlanTimestamp: fakePlanTimestamp, + }, + &stackplan.PlannedChangeHeader{ + TerraformVersion: version.SemVer, + }, + &stackplan.PlannedChangeOutputValue{ + Addr: stackaddrs.OutputValue{Name: "result"}, + Action: plans.Create, + OldValue: plans.DynamicValue{0xc0}, // MessagePack nil + NewValue: mustPlanDynamicValue(cty.StringVal("secret")), + NewValueMarks: []cty.PathValueMarks{{Marks: cty.NewValueMarks(marks.Sensitive)}}, + }, + } + sort.SliceStable(gotChanges, func(i, j int) bool { + // An arbitrary sort just to make the result stable for comparison. + return fmt.Sprintf("%T", gotChanges[i]) < fmt.Sprintf("%T", gotChanges[j]) + }) + + if diff := cmp.Diff(wantChanges, gotChanges, ctydebug.CmpOptions, cmpCollectionsSet); diff != "" { + t.Errorf("wrong changes\n%s", diff) + } +} + func TestPlanWithProviderConfig(t *testing.T) { ctx := context.Background() cfg := loadMainBundleConfigForTest(t, "with-provider-config") diff --git a/internal/stacks/stackruntime/testdata/mainbundle/test/sensitive-output-nested/sensitive-output-nested.tfstack.hcl b/internal/stacks/stackruntime/testdata/mainbundle/test/sensitive-output-nested/sensitive-output-nested.tfstack.hcl new file mode 100644 index 0000000000..58aa9630bb --- /dev/null +++ b/internal/stacks/stackruntime/testdata/mainbundle/test/sensitive-output-nested/sensitive-output-nested.tfstack.hcl @@ -0,0 +1,11 @@ +stack "child" { + source = "../sensitive-output" + + inputs = { + } +} + +output "result" { + type = string + value = stack.child.result +} diff --git a/internal/stacks/stackruntime/testdata/mainbundle/test/sensitive-output/sensitive-output.tf b/internal/stacks/stackruntime/testdata/mainbundle/test/sensitive-output/sensitive-output.tf new file mode 100644 index 0000000000..1031c01a58 --- /dev/null +++ b/internal/stacks/stackruntime/testdata/mainbundle/test/sensitive-output/sensitive-output.tf @@ -0,0 +1,4 @@ +output "out" { + value = sensitive("secret") + sensitive = true +} diff --git a/internal/stacks/stackruntime/testdata/mainbundle/test/sensitive-output/sensitive-output.tfstack.hcl b/internal/stacks/stackruntime/testdata/mainbundle/test/sensitive-output/sensitive-output.tfstack.hcl new file mode 100644 index 0000000000..1df80f4d0c --- /dev/null +++ b/internal/stacks/stackruntime/testdata/mainbundle/test/sensitive-output/sensitive-output.tfstack.hcl @@ -0,0 +1,10 @@ +component "self" { + source = "./" + inputs = { + } +} + +output "result" { + type = string + value = component.self.out +} From fc756571130af35fc33e4f5651c9db487df6d861 Mon Sep 17 00:00:00 2001 From: Alisdair McDiarmid Date: Thu, 14 Dec 2023 15:58:01 -0500 Subject: [PATCH 2/3] terraform: Support sensitive input variable values The stacks runtime interacts directly with the modules runtime's planning operations, rather than through the usual CLI paths. As a result, root module input variable values can be marked as sensitive upon entry. This commit adds support for such marked values to the modules runtime and the `plans.Plan` type. This is sufficient for stacks, which does not use the planfile serialization, but we may in future choose to serialize these decoded marks also. --- internal/plans/plan.go | 1 + internal/terraform/context_apply.go | 3 + internal/terraform/context_apply2_test.go | 93 +++++++++++++++++++++++ internal/terraform/context_plan.go | 16 +++- internal/terraform/context_plan2_test.go | 80 +++++++++++++++++++ 5 files changed, 192 insertions(+), 1 deletion(-) diff --git a/internal/plans/plan.go b/internal/plans/plan.go index 1d1873344f..c38f4e099f 100644 --- a/internal/plans/plan.go +++ b/internal/plans/plan.go @@ -42,6 +42,7 @@ type Plan struct { UIMode Mode VariableValues map[string]DynamicValue + VariableMarks map[string][]cty.PathValueMarks Changes *Changes DriftedResources []*ResourceInstanceChangeSrc TargetAddrs []addrs.Targetable diff --git a/internal/terraform/context_apply.go b/internal/terraform/context_apply.go index 2f5f35ba04..5310b4458c 100644 --- a/internal/terraform/context_apply.go +++ b/internal/terraform/context_apply.go @@ -202,6 +202,9 @@ func (c *Context) applyGraph(plan *plans.Plan, config *configs.Config, opts *App )) continue } + if pvm, ok := plan.VariableMarks[name]; ok { + val = val.MarkWithPaths(pvm) + } variables[name] = &InputValue{ Value: val, diff --git a/internal/terraform/context_apply2_test.go b/internal/terraform/context_apply2_test.go index d40a79bfb2..cd08cfd3a6 100644 --- a/internal/terraform/context_apply2_test.go +++ b/internal/terraform/context_apply2_test.go @@ -2726,3 +2726,96 @@ removed { checkStateString(t, state, ``) } + +func TestContext2Apply_sensitiveInputVariableValue(t *testing.T) { + m := testModuleInline(t, map[string]string{ + "main.tf": ` +variable "a" { + type = string + # this variable is not marked sensitive +} + +resource "test_resource" "a" { + value = var.a +} +`, + }) + + p := testProvider("test") + p.GetProviderSchemaResponse = getProviderSchemaResponseFromProviderSchema(&ProviderSchema{ + ResourceTypes: map[string]*configschema.Block{ + "test_resource": { + Attributes: map[string]*configschema.Attribute{ + "value": { + Type: cty.String, + Required: true, + }, + }, + }, + }, + }) + + ctx := testContext2(t, &ContextOpts{ + Providers: map[addrs.Provider]providers.Factory{ + addrs.NewDefaultProvider("test"): testProviderFuncFixed(p), + }, + }) + + // Build state with sensitive value in resource object + state := states.NewState() + root := state.EnsureModule(addrs.RootModuleInstance) + root.SetResourceInstanceCurrent( + mustResourceInstanceAddr("test_resource.a").Resource, + &states.ResourceInstanceObjectSrc{ + Status: states.ObjectReady, + AttrsJSON: []byte(`{"value":"secret"}]}`), + AttrSensitivePaths: []cty.PathValueMarks{ + { + Path: cty.GetAttrPath("value"), + Marks: cty.NewValueMarks(marks.Sensitive), + }, + }, + }, + mustProviderConfig(`provider["registry.terraform.io/hashicorp/test"]`), + ) + + // Create a sensitive-marked value for the input variable. This is not + // possible through the normal CLI path, but is possible when the plan is + // created and modified by the stacks runtime. + secret := cty.StringVal("updated").Mark(marks.Sensitive) + plan, diags := ctx.Plan(m, state, &PlanOpts{ + Mode: plans.NormalMode, + SetVariables: InputValues{ + "a": &InputValue{ + Value: secret, + SourceType: ValueFromUnknown, + }, + }, + }) + assertNoErrors(t, diags) + + state, diags = ctx.Apply(plan, m, nil) + if diags.HasErrors() { + t.Fatalf("diags: %s", diags.Err()) + } + + // check that the provider was not asked to destroy the resource + if !p.ApplyResourceChangeCalled { + t.Fatalf("Expected ApplyResourceChange to be called, but it was not called") + } + + instance := state.ResourceInstance(mustResourceInstanceAddr("test_resource.a")) + expected := "{\"value\":\"updated\"}" + if diff := cmp.Diff(string(instance.Current.AttrsJSON), expected); len(diff) > 0 { + t.Errorf("expected:\n%s\nactual:\n%s\ndiff:\n%s", expected, string(instance.Current.AttrsJSON), diff) + } + expectedMarkses := []cty.PathValueMarks{ + { + Path: cty.GetAttrPath("value"), + Marks: cty.NewValueMarks(marks.Sensitive), + }, + } + if diff := cmp.Diff(instance.Current.AttrSensitivePaths, expectedMarkses); len(diff) > 0 { + t.Errorf("unexpected sensitive paths\ndiff:\n%s", diff) + } +} diff --git a/internal/terraform/context_plan.go b/internal/terraform/context_plan.go index f6836c364a..e31eeb95d6 100644 --- a/internal/terraform/context_plan.go +++ b/internal/terraform/context_plan.go @@ -253,15 +253,25 @@ The -target option is not for routine use, and is provided only for exceptional // convert the variables into the format expected for the plan varVals := make(map[string]plans.DynamicValue, len(opts.SetVariables)) + varMarks := make(map[string][]cty.PathValueMarks, len(opts.SetVariables)) for k, iv := range opts.SetVariables { if iv.Value == cty.NilVal { continue // We only record values that the caller actually set } + // Root variable values arriving from the traditional CLI path are + // unmarked, as they are directly decoded from .tfvars, CLI arguments, + // or the environment. However, variable values arriving from other + // plans (via the coordination efforts of the stacks runtime) may have + // gathered marks during evaluation. We must separate the value from + // its marks here to maintain compatibility with plans.DynamicValue, + // which cannot represent marks. + value, pvm := iv.Value.UnmarkDeepWithPaths() + // We use cty.DynamicPseudoType here so that we'll save both the // value _and_ its dynamic type in the plan, so we can recover // exactly the same value later. - dv, err := plans.NewDynamicValue(iv.Value, cty.DynamicPseudoType) + dv, err := plans.NewDynamicValue(value, cty.DynamicPseudoType) if err != nil { diags = diags.Append(tfdiags.Sourceless( tfdiags.Error, @@ -271,12 +281,16 @@ The -target option is not for routine use, and is provided only for exceptional continue } varVals[k] = dv + varMarks[k] = pvm } // insert the run-specific data from the context into the plan; variables, // targets and provider SHAs. if plan != nil { plan.VariableValues = varVals + if len(varMarks) > 0 { + plan.VariableMarks = varMarks + } plan.TargetAddrs = opts.Targets } else if !diags.HasErrors() { panic("nil plan but no errors") diff --git a/internal/terraform/context_plan2_test.go b/internal/terraform/context_plan2_test.go index 2979991e71..99427d3619 100644 --- a/internal/terraform/context_plan2_test.go +++ b/internal/terraform/context_plan2_test.go @@ -4897,3 +4897,83 @@ resource "test_object" "a" {} t.Fatalf("wrong error:\ngot: %s\nwant: message containing %q", got, want) } } + +func TestContext2Plan_sensitiveInputVariableValue(t *testing.T) { + m := testModuleInline(t, map[string]string{ + "main.tf": ` +variable "boop" { + type = string + # this variable is not marked sensitive +} + +resource "test_resource" "a" { + value = var.boop +} + +`, + }) + + p := testProvider("test") + p.GetProviderSchemaResponse = getProviderSchemaResponseFromProviderSchema(&ProviderSchema{ + ResourceTypes: map[string]*configschema.Block{ + "test_resource": { + Attributes: map[string]*configschema.Attribute{ + "value": { + Type: cty.String, + Required: true, + }, + }, + }, + }, + }) + + ctx := testContext2(t, &ContextOpts{ + Providers: map[addrs.Provider]providers.Factory{ + addrs.NewDefaultProvider("test"): testProviderFuncFixed(p), + }, + }) + + // Build state with sensitive value in resource object + state := states.NewState() + root := state.EnsureModule(addrs.RootModuleInstance) + root.SetResourceInstanceCurrent( + mustResourceInstanceAddr("test_resource.a").Resource, + &states.ResourceInstanceObjectSrc{ + Status: states.ObjectReady, + AttrsJSON: []byte(`{"value":"secret"}]}`), + AttrSensitivePaths: []cty.PathValueMarks{ + { + Path: cty.GetAttrPath("value"), + Marks: cty.NewValueMarks(marks.Sensitive), + }, + }, + }, + mustProviderConfig(`provider["registry.terraform.io/hashicorp/test"]`), + ) + + // Create a sensitive-marked value for the input variable. This is not + // possible through the normal CLI path, but is possible when the plan is + // created and modified by the stacks runtime. + secret := cty.StringVal("secret").Mark(marks.Sensitive) + plan, diags := ctx.Plan(m, state, &PlanOpts{ + Mode: plans.NormalMode, + SetVariables: InputValues{ + "boop": &InputValue{ + Value: secret, + SourceType: ValueFromUnknown, + }, + }, + }) + assertNoErrors(t, diags) + for _, res := range plan.Changes.Resources { + switch res.Addr.String() { + case "test_resource.a": + spew.Dump(res) + if res.Action != plans.NoOp { + t.Errorf("unexpected %s change for %s", res.Action, res.Addr) + } + default: + t.Errorf("unexpected %s change for %s", res.Action, res.Addr) + } + } +} From 1d3f863f2b863a7159d6ab37a887ef6b0c922ead Mon Sep 17 00:00:00 2001 From: Alisdair McDiarmid Date: Thu, 14 Dec 2023 16:00:18 -0500 Subject: [PATCH 3/3] stackruntime: Support sensitive component inputs Components can emit sensitive values as outputs, which can be consumed as inputs to other components. This commit ensures that such values are correctly processed in order to pass their sensitivity to the modules runtime. --- internal/stacks/stackplan/planned_change.go | 23 ++--- internal/stacks/stackruntime/helper_test.go | 8 ++ .../internal/stackeval/component_instance.go | 14 +-- internal/stacks/stackruntime/plan_test.go | 89 +++++++++++++++++++ .../sensitive-output-as-input.tf | 8 ++ .../sensitive-output-as-input.tfstack.hcl | 19 ++++ 6 files changed, 146 insertions(+), 15 deletions(-) create mode 100644 internal/stacks/stackruntime/testdata/mainbundle/test/sensitive-output-as-input/sensitive-output-as-input.tf create mode 100644 internal/stacks/stackruntime/testdata/mainbundle/test/sensitive-output-as-input/sensitive-output-as-input.tfstack.hcl diff --git a/internal/stacks/stackplan/planned_change.go b/internal/stacks/stackplan/planned_change.go index e0de7e934e..34d66788b1 100644 --- a/internal/stacks/stackplan/planned_change.go +++ b/internal/stacks/stackplan/planned_change.go @@ -111,6 +111,8 @@ type PlannedChangeComponentInstance struct { // with what's captured here. PlannedInputValues map[string]plans.DynamicValue + PlannedInputValueMarks map[string][]cty.PathValueMarks + PlannedOutputValues map[string]cty.Value // PlanTimestamp is the timestamp that would be returned from the @@ -128,20 +130,21 @@ func (pc *PlannedChangeComponentInstance) PlannedChangeProto() (*terraform1.Plan if n := len(pc.PlannedInputValues); n != 0 { plannedInputValues = make(map[string]*tfstackdata1.DynamicValue, n) for k, v := range pc.PlannedInputValues { + var sensitivePaths []*planproto.Path + if pvm, ok := pc.PlannedInputValueMarks[k]; ok { + for _, p := range pvm { + path, err := planproto.NewPath(p.Path) + if err != nil { + return nil, err + } + sensitivePaths = append(sensitivePaths, path) + } + } plannedInputValues[k] = &tfstackdata1.DynamicValue{ Value: &planproto.DynamicValue{ Msgpack: v, }, - // FIXME: We're currently losing track of sensitivity here -- - // or, more accurately, in the caller that's populating - // pc.PlannedInputValues -- but that's not _super_ important - // because we don't directly use these values during the - // apply phase anyway, and instead recalculate the input - // values based on updated data from other components having - // already been applied. These values are here only to give - // us something to compare against as a safety check to catch - // if a bug somewhere causes the values to be inconsistent - // between plan and apply. + SensitivePaths: sensitivePaths, } } } diff --git a/internal/stacks/stackruntime/helper_test.go b/internal/stacks/stackruntime/helper_test.go index 881ad618a8..1f5b3a4d2e 100644 --- a/internal/stacks/stackruntime/helper_test.go +++ b/internal/stacks/stackruntime/helper_test.go @@ -110,3 +110,11 @@ func mustPlanDynamicValue(v cty.Value) plans.DynamicValue { } return ret } + +func mustPlanDynamicValueDynamicType(v cty.Value) plans.DynamicValue { + ret, err := plans.NewDynamicValue(v, cty.DynamicPseudoType) + if err != nil { + panic(err) + } + return ret +} diff --git a/internal/stacks/stackruntime/internal/stackeval/component_instance.go b/internal/stacks/stackruntime/internal/stackeval/component_instance.go index 4dd5b7df6b..1926f32757 100644 --- a/internal/stacks/stackruntime/internal/stackeval/component_instance.go +++ b/internal/stacks/stackruntime/internal/stackeval/component_instance.go @@ -703,8 +703,10 @@ func (c *ComponentInstance) ApplyModuleTreePlan(ctx context.Context, plan *plans // and let the plan file serializer worry about encoding, but we'll // defer that API change for now to avoid disrupting other codepaths. modifiedPlan.VariableValues = make(map[string]plans.DynamicValue, len(inputValues)) + modifiedPlan.VariableMarks = make(map[string][]cty.PathValueMarks, len(inputValues)) for name, iv := range inputValues { - dv, err := plans.NewDynamicValue(iv.Value, cty.DynamicPseudoType) + val, pvm := iv.Value.UnmarkDeepWithPaths() + dv, err := plans.NewDynamicValue(val, cty.DynamicPseudoType) if err != nil { diags = diags.Append(tfdiags.Sourceless( tfdiags.Error, @@ -717,6 +719,7 @@ func (c *ComponentInstance) ApplyModuleTreePlan(ctx context.Context, plan *plans continue } modifiedPlan.VariableValues[name] = dv + modifiedPlan.VariableMarks[name] = pvm } if diags.HasErrors() { return nil, diags @@ -1015,10 +1018,11 @@ func (c *ComponentInstance) PlanChanges(ctx context.Context) ([]stackplan.Planne changes = append(changes, &stackplan.PlannedChangeComponentInstance{ Addr: c.Addr(), - Action: action, - RequiredComponents: c.RequiredComponents(ctx), - PlannedInputValues: corePlan.VariableValues, - PlannedOutputValues: outputVals, + Action: action, + RequiredComponents: c.RequiredComponents(ctx), + PlannedInputValues: corePlan.VariableValues, + PlannedInputValueMarks: corePlan.VariableMarks, + PlannedOutputValues: outputVals, // We must remember the plan timestamp so that the plantimestamp // function can return a consistent result during a later apply phase. diff --git a/internal/stacks/stackruntime/plan_test.go b/internal/stacks/stackruntime/plan_test.go index 96dc6ab418..bc9e67c350 100644 --- a/internal/stacks/stackruntime/plan_test.go +++ b/internal/stacks/stackruntime/plan_test.go @@ -383,6 +383,95 @@ func TestPlanSensitiveOutputNested(t *testing.T) { } } +func TestPlanSensitiveOutputAsInput(t *testing.T) { + ctx := context.Background() + cfg := loadMainBundleConfigForTest(t, "sensitive-output-as-input") + + fakePlanTimestamp, err := time.Parse(time.RFC3339, "1991-08-25T20:57:08Z") + if err != nil { + t.Fatal(err) + } + + changesCh := make(chan stackplan.PlannedChange, 8) + diagsCh := make(chan tfdiags.Diagnostic, 2) + req := PlanRequest{ + Config: cfg, + ForcePlanTimestamp: &fakePlanTimestamp, + } + resp := PlanResponse{ + PlannedChanges: changesCh, + Diagnostics: diagsCh, + } + + go Plan(ctx, &req, &resp) + gotChanges, diags := collectPlanOutput(changesCh, diagsCh) + + if len(diags) != 0 { + t.Errorf("unexpected diagnostics\n%s", diags.ErrWithWarnings().Error()) + } + + wantChanges := []stackplan.PlannedChange{ + &stackplan.PlannedChangeApplyable{ + Applyable: true, + }, + &stackplan.PlannedChangeComponentInstance{ + Addr: stackaddrs.Absolute( + stackaddrs.RootStackInstance.Child("sensitive", addrs.NoKey), + stackaddrs.ComponentInstance{ + Component: stackaddrs.Component{Name: "self"}, + }, + ), + Action: plans.Create, + PlannedInputValues: make(map[string]plans.DynamicValue), + PlannedOutputValues: map[string]cty.Value{ + "out": cty.StringVal("secret").Mark(marks.Sensitive), + }, + PlanTimestamp: fakePlanTimestamp, + }, + &stackplan.PlannedChangeComponentInstance{ + Addr: stackaddrs.Absolute( + stackaddrs.RootStackInstance, + stackaddrs.ComponentInstance{ + Component: stackaddrs.Component{Name: "self"}, + }, + ), + Action: plans.Create, + PlannedInputValues: map[string]plans.DynamicValue{ + "secret": mustPlanDynamicValueDynamicType(cty.StringVal("secret")), + }, + PlannedInputValueMarks: map[string][]cty.PathValueMarks{ + "secret": { + { + Marks: cty.NewValueMarks(marks.Sensitive), + }, + }, + }, + PlannedOutputValues: map[string]cty.Value{ + "result": cty.StringVal("SECRET").Mark(marks.Sensitive), + }, + PlanTimestamp: fakePlanTimestamp, + }, + &stackplan.PlannedChangeHeader{ + TerraformVersion: version.SemVer, + }, + &stackplan.PlannedChangeOutputValue{ + Addr: stackaddrs.OutputValue{Name: "result"}, + Action: plans.Create, + OldValue: plans.DynamicValue{0xc0}, // MessagePack nil + NewValue: mustPlanDynamicValue(cty.StringVal("SECRET")), + NewValueMarks: []cty.PathValueMarks{{Marks: cty.NewValueMarks(marks.Sensitive)}}, + }, + } + sort.SliceStable(gotChanges, func(i, j int) bool { + // An arbitrary sort just to make the result stable for comparison. + return fmt.Sprintf("%T", gotChanges[i]) < fmt.Sprintf("%T", gotChanges[j]) + }) + + if diff := cmp.Diff(wantChanges, gotChanges, ctydebug.CmpOptions, cmpCollectionsSet); diff != "" { + t.Errorf("wrong changes\n%s", diff) + } +} + func TestPlanWithProviderConfig(t *testing.T) { ctx := context.Background() cfg := loadMainBundleConfigForTest(t, "with-provider-config") diff --git a/internal/stacks/stackruntime/testdata/mainbundle/test/sensitive-output-as-input/sensitive-output-as-input.tf b/internal/stacks/stackruntime/testdata/mainbundle/test/sensitive-output-as-input/sensitive-output-as-input.tf new file mode 100644 index 0000000000..797d7db5c6 --- /dev/null +++ b/internal/stacks/stackruntime/testdata/mainbundle/test/sensitive-output-as-input/sensitive-output-as-input.tf @@ -0,0 +1,8 @@ +variable "secret" { + type = string +} + +output "result" { + value = sensitive(upper(var.secret)) + sensitive = true +} diff --git a/internal/stacks/stackruntime/testdata/mainbundle/test/sensitive-output-as-input/sensitive-output-as-input.tfstack.hcl b/internal/stacks/stackruntime/testdata/mainbundle/test/sensitive-output-as-input/sensitive-output-as-input.tfstack.hcl new file mode 100644 index 0000000000..7d9d932a5b --- /dev/null +++ b/internal/stacks/stackruntime/testdata/mainbundle/test/sensitive-output-as-input/sensitive-output-as-input.tfstack.hcl @@ -0,0 +1,19 @@ +stack "sensitive" { + source = "../sensitive-output" + + inputs = { + } +} + +component "self" { + source = "./" + + inputs = { + secret = stack.sensitive.result + } +} + +output "result" { + type = string + value = component.self.result +}