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