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/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 c5ffe77d9b..bc9e67c350 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,241 @@ 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 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 +} 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 +} 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) + } + } +}