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 +}