From 1d3f863f2b863a7159d6ab37a887ef6b0c922ead Mon Sep 17 00:00:00 2001 From: Alisdair McDiarmid Date: Thu, 14 Dec 2023 16:00:18 -0500 Subject: [PATCH] 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 +}