Skip to content

Commit b94a1f3

Browse files
committed
Allow marked values in dynamic block for_each
Follow up of hashicorp/hcl#679 Previously, for_each in dynamic blocks did not allow marked values such as sensitive. However, hashicorp/hcl#679 now supports this by propagating the marks to expanded children. The reason behind this is to add a new mark called "ephemeral", so we'll pull the changes to support Terraform 1.10. Note that tfhcl's dynamic block support has incomplete mark propagation since marked values resolve to unknown values. This is because in the past the marked values could not be sent over the wire protocol, and may be fixed in the near future.
1 parent a6dc3eb commit b94a1f3

File tree

4 files changed

+151
-17
lines changed

4 files changed

+151
-17
lines changed

terraform/tfhcl/expand_body.go

Lines changed: 36 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,6 @@
1+
// Copyright (c) HashiCorp, Inc.
2+
// SPDX-License-Identifier: MPL-2.0
3+
14
package tfhcl
25

36
import (
@@ -15,6 +18,7 @@ type expandBody struct {
1518
ctx *hcl.EvalContext
1619
dynamicIteration *dynamicIteration // non-nil if we're nested inside a "dynamic" block
1720
metaArgIteration *metaArgIteration // non-nil if we're nested inside a block with meta-arguments
21+
valueMarks cty.ValueMarks
1822

1923
// These are used with PartialContent to produce a "remaining items"
2024
// body to return. They are nil on all bodies fresh out of the transformer.
@@ -126,7 +130,7 @@ func (b *expandBody) extendSchema(schema *hcl.BodySchema) *hcl.BodySchema {
126130
func (b *expandBody) prepareAttributes(rawAttrs hcl.Attributes) (hcl.Attributes, hcl.Diagnostics) {
127131
var diags hcl.Diagnostics
128132

129-
if len(b.hiddenAttrs) == 0 && b.dynamicIteration == nil && b.metaArgIteration == nil {
133+
if len(b.hiddenAttrs) == 0 && b.dynamicIteration == nil && b.metaArgIteration == nil && len(b.valueMarks) == 0 {
130134
// Easy path: just pass through the attrs from the original body verbatim
131135
return rawAttrs, diags
132136
}
@@ -143,9 +147,10 @@ func (b *expandBody) prepareAttributes(rawAttrs hcl.Attributes) (hcl.Attributes,
143147
if b.dynamicIteration != nil || b.metaArgIteration != nil {
144148
attr := *rawAttr // shallow copy so we can mutate it
145149
expr := exprWrap{
146-
Expression: attr.Expr,
147-
di: b.dynamicIteration,
148-
mi: b.metaArgIteration,
150+
Expression: attr.Expr,
151+
di: b.dynamicIteration,
152+
mi: b.metaArgIteration,
153+
resultMarks: b.valueMarks,
149154
}
150155
// Unlike hcl/ext/dynblock, wrapped expressions are evaluated immediately.
151156
// The result is bound to the expression and can be accessed without
@@ -161,8 +166,18 @@ func (b *expandBody) prepareAttributes(rawAttrs hcl.Attributes) (hcl.Attributes,
161166
}
162167
attrs[name] = &attr
163168
} else {
164-
// If we have no active iteration then no wrapping is required.
165-
attrs[name] = rawAttr
169+
// If we have no active iteration then no wrapping is required
170+
// unless we have marks to apply.
171+
if len(b.valueMarks) != 0 {
172+
attr := *rawAttr // shallow copy so we can mutate it
173+
attr.Expr = exprWrap{
174+
Expression: attr.Expr,
175+
resultMarks: b.valueMarks,
176+
}
177+
attrs[name] = &attr
178+
} else {
179+
attrs[name] = rawAttr
180+
}
166181
}
167182
}
168183
return attrs, diags
@@ -228,14 +243,16 @@ func (b *expandBody) expandDynamicBlock(schema *hcl.BodySchema, rawBlock *hcl.Bl
228243
return hcl.Blocks{}, diags
229244
}
230245

231-
if !spec.forEachVal.IsKnown() {
246+
// For dynamic blocks only, it allows marked values
247+
forEachVal, marks := spec.forEachVal.Unmark()
248+
if !forEachVal.IsKnown() {
232249
// If for_each is unknown, no blocks are returned
233250
return hcl.Blocks{}, diags
234251
}
235252

236253
var blocks hcl.Blocks
237254

238-
for it := spec.forEachVal.ElementIterator(); it.Next(); {
255+
for it := forEachVal.ElementIterator(); it.Next(); {
239256
key, value := it.Element()
240257
i := b.dynamicIteration.MakeChild(spec.iteratorName, key, value)
241258

@@ -244,7 +261,7 @@ func (b *expandBody) expandDynamicBlock(schema *hcl.BodySchema, rawBlock *hcl.Bl
244261
if block != nil {
245262
// Attach our new iteration context so that attributes
246263
// and other nested blocks can refer to our iterator.
247-
block.Body = b.expandChild(block.Body, i, b.metaArgIteration)
264+
block.Body = b.expandChild(block.Body, i, b.metaArgIteration, marks)
248265
blocks = append(blocks, block)
249266
}
250267
}
@@ -278,7 +295,7 @@ func (b *expandBody) expandMetaArgBlock(schema *hcl.BodySchema, rawBlock *hcl.Bl
278295
i := MakeCountIteration(cty.NumberIntVal(int64(idx)))
279296

280297
expandedBlock := *rawBlock // shallow copy
281-
expandedBlock.Body = b.expandChild(rawBlock.Body, b.dynamicIteration, i)
298+
expandedBlock.Body = b.expandChild(rawBlock.Body, b.dynamicIteration, i, nil)
282299
blocks = append(blocks, &expandedBlock)
283300
}
284301

@@ -299,7 +316,7 @@ func (b *expandBody) expandMetaArgBlock(schema *hcl.BodySchema, rawBlock *hcl.Bl
299316
i := MakeForEachIteration(it.Element())
300317

301318
expandedBlock := *rawBlock // shallow copy
302-
expandedBlock.Body = b.expandChild(rawBlock.Body, b.dynamicIteration, i)
319+
expandedBlock.Body = b.expandChild(rawBlock.Body, b.dynamicIteration, i, nil)
303320
blocks = append(blocks, &expandedBlock)
304321
}
305322

@@ -317,15 +334,16 @@ func (b *expandBody) expandStaticBlock(rawBlock *hcl.Block) *hcl.Block {
317334
// case it contains expressions that refer to our inherited
318335
// iterators, or nested "dynamic" blocks.
319336
expandedBlock := *rawBlock
320-
expandedBlock.Body = b.expandChild(rawBlock.Body, b.dynamicIteration, b.metaArgIteration)
337+
expandedBlock.Body = b.expandChild(rawBlock.Body, b.dynamicIteration, b.metaArgIteration, nil)
321338
return &expandedBlock
322339
}
323340

324-
func (b *expandBody) expandChild(child hcl.Body, i *dynamicIteration, mi *metaArgIteration) hcl.Body {
341+
func (b *expandBody) expandChild(child hcl.Body, i *dynamicIteration, mi *metaArgIteration, valueMarks cty.ValueMarks) hcl.Body {
325342
chiCtx := i.EvalContext(mi.EvalContext(b.ctx))
326343
ret := Expand(child, chiCtx)
327344
ret.(*expandBody).dynamicIteration = i
328345
ret.(*expandBody).metaArgIteration = mi
346+
ret.(*expandBody).valueMarks = valueMarks
329347
return ret
330348
}
331349

@@ -339,3 +357,8 @@ func (b *expandBody) JustAttributes() (hcl.Attributes, hcl.Diagnostics) {
339357
func (b *expandBody) MissingItemRange() hcl.Range {
340358
return b.original.MissingItemRange()
341359
}
360+
361+
// hcldec.MarkedBody impl
362+
func (b *expandBody) BodyValueMarks() cty.ValueMarks {
363+
return b.valueMarks
364+
}

terraform/tfhcl/expand_body_test.go

Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,16 @@
1+
// Copyright (c) HashiCorp, Inc.
2+
// SPDX-License-Identifier: MPL-2.0
3+
14
package tfhcl
25

36
import (
47
"testing"
58

9+
"github.com/google/go-cmp/cmp"
610
"github.com/hashicorp/hcl/v2"
711
"github.com/hashicorp/hcl/v2/hcldec"
812
"github.com/hashicorp/hcl/v2/hcltest"
13+
"github.com/zclconf/go-cty-debug/ctydebug"
914
"github.com/zclconf/go-cty/cty"
1015
)
1116

@@ -331,3 +336,71 @@ func TestExpand(t *testing.T) {
331336
})
332337

333338
}
339+
340+
func TestExpandMarkedForEach(t *testing.T) {
341+
srcBody := hcltest.MockBody(&hcl.BodyContent{
342+
Blocks: hcl.Blocks{
343+
{
344+
Type: "dynamic",
345+
Labels: []string{"b"},
346+
LabelRanges: []hcl.Range{{}},
347+
Body: hcltest.MockBody(&hcl.BodyContent{
348+
Attributes: hcltest.MockAttrs(map[string]hcl.Expression{
349+
"for_each": hcltest.MockExprLiteral(cty.TupleVal([]cty.Value{
350+
cty.StringVal("hey"),
351+
}).Mark("boop")),
352+
"iterator": hcltest.MockExprTraversalSrc("dyn_b"),
353+
}),
354+
Blocks: hcl.Blocks{
355+
{
356+
Type: "content",
357+
Body: hcltest.MockBody(&hcl.BodyContent{
358+
Attributes: hcltest.MockAttrs(map[string]hcl.Expression{
359+
"val0": hcltest.MockExprLiteral(cty.StringVal("static c 1")),
360+
"val1": hcltest.MockExprTraversalSrc("dyn_b.value"),
361+
}),
362+
}),
363+
},
364+
},
365+
}),
366+
},
367+
},
368+
})
369+
370+
// Emulate eval context because iterators are indistinguishable from any resource and effectively resolve to unknown.
371+
ctx := &hcl.EvalContext{
372+
Variables: map[string]cty.Value{"dyn_b": cty.DynamicVal},
373+
}
374+
375+
dynBody := Expand(srcBody, ctx)
376+
377+
t.Run("Decode", func(t *testing.T) {
378+
decSpec := &hcldec.BlockListSpec{
379+
TypeName: "b",
380+
Nested: &hcldec.ObjectSpec{
381+
"val0": &hcldec.AttrSpec{
382+
Name: "val0",
383+
Type: cty.String,
384+
},
385+
"val1": &hcldec.AttrSpec{
386+
Name: "val1",
387+
Type: cty.String,
388+
},
389+
},
390+
}
391+
392+
want := cty.ListVal([]cty.Value{
393+
cty.ObjectVal(map[string]cty.Value{
394+
"val0": cty.StringVal("static c 1"),
395+
"val1": cty.UnknownVal(cty.String),
396+
}).Mark("boop"),
397+
})
398+
got, diags := hcldec.Decode(dynBody, decSpec, ctx)
399+
if diags.HasErrors() {
400+
t.Fatalf("unexpected errors\n%s", diags.Error())
401+
}
402+
if diff := cmp.Diff(want, got, ctydebug.CmpOptions); diff != "" {
403+
t.Errorf("wrong result\n%s", diff)
404+
}
405+
})
406+
}

terraform/tfhcl/expand_spec.go

Lines changed: 21 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,6 @@
1+
// Copyright (c) HashiCorp, Inc.
2+
// SPDX-License-Identifier: MPL-2.0
3+
14
package tfhcl
25

36
import (
@@ -42,7 +45,9 @@ func (b *expandBody) decodeDynamicSpec(blockS *hcl.BlockHeaderSchema, rawSpec *h
4245
eachVal, eachDiags := eachAttr.Expr.Value(b.ctx)
4346
diags = append(diags, eachDiags...)
4447

45-
if !eachVal.CanIterateElements() && eachVal.Type() != cty.DynamicPseudoType {
48+
// For dynamic blocks only, it allows marked values
49+
unmarkedEachVal, _ := eachVal.Unmark()
50+
if !unmarkedEachVal.CanIterateElements() && unmarkedEachVal.Type() != cty.DynamicPseudoType {
4651
// We skip this error for DynamicPseudoType because that means we either
4752
// have a null (which is checked immediately below) or an unknown
4853
// (which is handled in the expandBody Content methods).
@@ -56,7 +61,7 @@ func (b *expandBody) decodeDynamicSpec(blockS *hcl.BlockHeaderSchema, rawSpec *h
5661
})
5762
return nil, diags
5863
}
59-
if eachVal.IsNull() {
64+
if unmarkedEachVal.IsNull() {
6065
diags = append(diags, &hcl.Diagnostic{
6166
Severity: hcl.DiagError,
6267
Summary: "Invalid dynamic for_each value",
@@ -188,13 +193,26 @@ func (s *expandDynamicSpec) newBlock(i *dynamicIteration, ctx *hcl.EvalContext)
188193
return nil, diags
189194
}
190195
if !labelVal.IsKnown() {
196+
// Unlike hcl/ext/dynblock, if the label is unknown
197+
// it will not return an error and will not append a new block.
191198
return nil, diags
192199
}
193200
if labelVal.IsMarked() {
201+
// This situation is tricky because HCL just works generically
202+
// with marks and so doesn't have any good language to talk about
203+
// the meaning of specific mark types, but yet we cannot allow
204+
// marked values here because the HCL API guarantees that a block's
205+
// labels are always known static constant Go strings.
206+
// Therefore this is a low-quality error message but at least
207+
// better than panicking below when we call labelVal.AsString.
208+
// If this becomes a problem then we could potentially add a new
209+
// option for the public function [Expand] to allow calling
210+
// applications to specify custom label validation functions that
211+
// could then supersede this generic message.
194212
diags = append(diags, &hcl.Diagnostic{
195213
Severity: hcl.DiagError,
196214
Summary: "Invalid dynamic block label",
197-
Detail: "Cannot use a marked value as a dynamic block label.",
215+
Detail: "This value has dynamic marks that make it unsuitable for use as a block label.",
198216
Subject: labelExpr.Range().Ptr(),
199217
Expression: labelExpr,
200218
EvalContext: lCtx,

terraform/tfhcl/expr_wrap.go

Lines changed: 21 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,6 @@
1+
// Copyright (c) HashiCorp, Inc.
2+
// SPDX-License-Identifier: MPL-2.0
3+
14
package tfhcl
25

36
import (
@@ -9,6 +12,13 @@ type exprWrap struct {
912
hcl.Expression
1013
di *dynamicIteration
1114
mi *metaArgIteration
15+
16+
// resultMarks is a set of marks that must be applied to whatever
17+
// value results from this expression. We do this whenever a
18+
// dynamic block's for_each expression produced a marked result,
19+
// since in that case any nested expressions inside are treated
20+
// as being derived from that for_each expression.
21+
resultMarks cty.ValueMarks
1222
}
1323

1424
func (e exprWrap) Variables() []hcl.Traversal {
@@ -35,12 +45,22 @@ func (e exprWrap) Variables() []hcl.Traversal {
3545
}
3646

3747
func (e exprWrap) Value(ctx *hcl.EvalContext) (cty.Value, hcl.Diagnostics) {
48+
if e.di == nil && e.mi == nil {
49+
// If we don't have an active iteration then we can just use the
50+
// given EvalContext directly.
51+
return e.prepareValue(e.Expression.Value(ctx))
52+
}
53+
3854
extCtx := e.di.EvalContext(e.mi.EvalContext(ctx))
39-
return e.Expression.Value(extCtx)
55+
return e.prepareValue(e.Expression.Value(extCtx))
4056
}
4157

4258
// UnwrapExpression returns the expression being wrapped by this instance.
4359
// This allows the original expression to be recovered by hcl.UnwrapExpression.
4460
func (e exprWrap) UnwrapExpression() hcl.Expression {
4561
return e.Expression
4662
}
63+
64+
func (e exprWrap) prepareValue(val cty.Value, diags hcl.Diagnostics) (cty.Value, hcl.Diagnostics) {
65+
return val.WithMarks(e.resultMarks), diags
66+
}

0 commit comments

Comments
 (0)