Merge pull request #34414 from hashicorp/alisdair/stacks-sensitive-component-outputs

stacks: Support sensitive component outputs
pull/34494/head
Alisdair McDiarmid 2 years ago committed by GitHub
commit 27552162ca
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23

@ -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

@ -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,
}
}
}

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

@ -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.

@ -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")

@ -0,0 +1,8 @@
variable "secret" {
type = string
}
output "result" {
value = sensitive(upper(var.secret))
sensitive = true
}

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

@ -0,0 +1,11 @@
stack "child" {
source = "../sensitive-output"
inputs = {
}
}
output "result" {
type = string
value = stack.child.result
}

@ -0,0 +1,4 @@
output "out" {
value = sensitive("secret")
sensitive = true
}

@ -0,0 +1,10 @@
component "self" {
source = "./"
inputs = {
}
}
output "result" {
type = string
value = component.self.out
}

@ -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,

@ -2726,3 +2726,96 @@ removed {
checkStateString(t, state, `<no 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)
}
}

@ -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")

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

Loading…
Cancel
Save