Skip to content

Commit 9504b26

Browse files
authored
Merge pull request #32891 from hashicorp/jbardin/sensitive-mod-outputs
Store all sensitive marks for non-root module outputs in state
2 parents bd75dad + defd7f0 commit 9504b26

File tree

2 files changed

+76
-21
lines changed

2 files changed

+76
-21
lines changed

internal/terraform/context_apply2_test.go

Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1887,3 +1887,70 @@ output "null_module_test" {
18871887
_, diags = ctx.Apply(plan, m)
18881888
assertNoErrors(t, diags)
18891889
}
1890+
1891+
func TestContext2Apply_moduleOutputWithSensitiveAttrs(t *testing.T) {
1892+
// Ensure that nested sensitive marks are stored when accessing non-root
1893+
// module outputs, and that they do not cause the entire output value to
1894+
// become sensitive.
1895+
m := testModuleInline(t, map[string]string{
1896+
"main.tf": `
1897+
module "mod" {
1898+
source = "./mod"
1899+
}
1900+
1901+
resource "test_resource" "b" {
1902+
// if the module output were wholly sensitive it would not be valid to use in
1903+
// for_each
1904+
for_each = module.mod.resources
1905+
value = each.value.output
1906+
}
1907+
1908+
output "root_output" {
1909+
// The root output cannot contain any sensitive marks at all.
1910+
// Applying nonsensitive would fail here if the nested sensitive mark were
1911+
// not maintained through the output.
1912+
value = [ for k, v in module.mod.resources : nonsensitive(v.output) ]
1913+
}
1914+
`,
1915+
"./mod/main.tf": `
1916+
resource "test_resource" "a" {
1917+
for_each = {"key": "value"}
1918+
value = each.key
1919+
}
1920+
1921+
output "resources" {
1922+
value = test_resource.a
1923+
}
1924+
`,
1925+
})
1926+
1927+
p := testProvider("test")
1928+
p.GetProviderSchemaResponse = getProviderSchemaResponseFromProviderSchema(&ProviderSchema{
1929+
ResourceTypes: map[string]*configschema.Block{
1930+
"test_resource": {
1931+
Attributes: map[string]*configschema.Attribute{
1932+
"value": {
1933+
Type: cty.String,
1934+
Required: true,
1935+
},
1936+
"output": {
1937+
Type: cty.String,
1938+
Sensitive: true,
1939+
Computed: true,
1940+
},
1941+
},
1942+
},
1943+
},
1944+
})
1945+
ctx := testContext2(t, &ContextOpts{
1946+
Providers: map[addrs.Provider]providers.Factory{
1947+
addrs.NewDefaultProvider("test"): testProviderFuncFixed(p),
1948+
},
1949+
})
1950+
plan, diags := ctx.Plan(m, states.NewState(), &PlanOpts{
1951+
Mode: plans.NormalMode,
1952+
})
1953+
assertNoErrors(t, diags)
1954+
_, diags = ctx.Apply(plan, m)
1955+
assertNoErrors(t, diags)
1956+
}

internal/terraform/node_output.go

Lines changed: 9 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -513,10 +513,6 @@ func (n *NodeDestroyableOutput) DotNode(name string, opts *dag.DotOpts) *dag.Dot
513513
}
514514

515515
func (n *NodeApplyableOutput) setValue(state *states.SyncState, changes *plans.ChangesSync, val cty.Value) {
516-
// If we have an active changeset then we'll first replicate the value in
517-
// there and lookup the prior value in the state. This is used in
518-
// preference to the state where present, since it *is* able to represent
519-
// unknowns, while the state cannot.
520516
if changes != nil && n.Planning {
521517
// if this is a root module, try to get a before value from the state for
522518
// the diff
@@ -538,8 +534,8 @@ func (n *NodeApplyableOutput) setValue(state *states.SyncState, changes *plans.C
538534
}
539535
}
540536

541-
// We will not show the value is either the before or after are marked
542-
// as sensitivity. We can show the value again once sensitivity is
537+
// We will not show the value if either the before or after are marked
538+
// as sensitive. We can show the value again once sensitivity is
543539
// removed from both the config and the state.
544540
sensitiveChange := sensitiveBefore || n.Config.Sensitive
545541

@@ -601,22 +597,14 @@ func (n *NodeApplyableOutput) setValue(state *states.SyncState, changes *plans.C
601597
return
602598
}
603599

604-
// The state itself doesn't represent unknown values, so we null them
605-
// out here and then we'll save the real unknown value in the planned
606-
// changeset, if we have one on this graph walk.
607600
log.Printf("[TRACE] setValue: Saving value for %s in state", n.Addr)
608-
sensitive := n.Config.Sensitive
609-
unmarkedVal, valueMarks := val.UnmarkDeep()
610-
611-
// If the evaluated value contains sensitive marks, the output has no
612-
// choice but to declare itself as "sensitive".
613-
for mark := range valueMarks {
614-
if mark == marks.Sensitive {
615-
sensitive = true
616-
break
617-
}
601+
602+
// non-root outputs need to keep sensitive marks for evaluation, but are
603+
// not serialized.
604+
if n.Addr.Module.IsRoot() {
605+
val, _ = val.UnmarkDeep()
606+
val = cty.UnknownAsNull(val)
618607
}
619608

620-
stateVal := cty.UnknownAsNull(unmarkedVal)
621-
state.SetOutputValue(n.Addr, stateVal, sensitive)
609+
state.SetOutputValue(n.Addr, val, n.Config.Sensitive)
622610
}

0 commit comments

Comments
 (0)