Skip to content

Commit 0af4fe2

Browse files
ext/dynblock: Allow callers to veto for_each values
Callers might have additional rules for what's acceptable in a for_each value for a dynamic block. For example, Terraform wants to forbid using sensitive values here because it would cause the expansion to disclose the length of the given collection. Therefore this provides a hook point for callers to insert additional checks just after the for_each expression has been evaluated and before any of the built-in checks are run. This introduces the "functional options" pattern for ExpandBlock for the first time, as a way to extend the API without breaking compatibility with existing callers. There is currently only this one option.
1 parent 4945193 commit 0af4fe2

File tree

5 files changed

+143
-17
lines changed

5 files changed

+143
-17
lines changed

ext/dynblock/expand_body.go

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,8 @@ type expandBody struct {
1717
forEachCtx *hcl.EvalContext
1818
iteration *iteration // non-nil if we're nested inside another "dynamic" block
1919

20+
checkForEach []func(cty.Value, hcl.Expression, *hcl.EvalContext) hcl.Diagnostics
21+
2022
// These are used with PartialContent to produce a "remaining items"
2123
// body to return. They are nil on all bodies fresh out of the transformer.
2224
//
@@ -66,6 +68,7 @@ func (b *expandBody) PartialContent(schema *hcl.BodySchema) (*hcl.BodyContent, h
6668
original: b.original,
6769
forEachCtx: b.forEachCtx,
6870
iteration: b.iteration,
71+
checkForEach: b.checkForEach,
6972
hiddenAttrs: make(map[string]struct{}),
7073
hiddenBlocks: make(map[string]hcl.BlockHeaderSchema),
7174
}
@@ -236,6 +239,7 @@ func (b *expandBody) expandChild(child hcl.Body, i *iteration) hcl.Body {
236239
chiCtx := i.EvalContext(b.forEachCtx)
237240
ret := Expand(child, chiCtx)
238241
ret.(*expandBody).iteration = i
242+
ret.(*expandBody).checkForEach = b.checkForEach
239243
return ret
240244
}
241245

ext/dynblock/expand_body_test.go

Lines changed: 85 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,9 +7,11 @@ import (
77
"strings"
88
"testing"
99

10+
"github.com/davecgh/go-spew/spew"
1011
"github.com/hashicorp/hcl/v2"
1112
"github.com/hashicorp/hcl/v2/hcldec"
1213
"github.com/hashicorp/hcl/v2/hcltest"
14+
"github.com/zclconf/go-cty-debug/ctydebug"
1315
"github.com/zclconf/go-cty/cty"
1416
)
1517

@@ -336,6 +338,89 @@ func TestExpand(t *testing.T) {
336338

337339
}
338340

341+
func TestExpandWithForEachCheck(t *testing.T) {
342+
forEachExpr := hcltest.MockExprLiteral(cty.MapValEmpty(cty.String).Mark("boop"))
343+
evalCtx := &hcl.EvalContext{}
344+
srcContent := &hcl.BodyContent{
345+
Blocks: hcl.Blocks{
346+
{
347+
Type: "dynamic",
348+
Labels: []string{"foo"},
349+
LabelRanges: []hcl.Range{{}},
350+
Body: hcltest.MockBody(&hcl.BodyContent{
351+
Attributes: hcltest.MockAttrs(map[string]hcl.Expression{
352+
"for_each": forEachExpr,
353+
}),
354+
Blocks: hcl.Blocks{
355+
{
356+
Type: "content",
357+
Body: hcltest.MockBody(&hcl.BodyContent{}),
358+
},
359+
},
360+
}),
361+
},
362+
},
363+
}
364+
srcBody := hcltest.MockBody(srcContent)
365+
366+
hookCalled := false
367+
var gotV cty.Value
368+
var gotEvalCtx *hcl.EvalContext
369+
370+
expBody := Expand(
371+
srcBody, evalCtx,
372+
OptCheckForEach(func(v cty.Value, e hcl.Expression, ec *hcl.EvalContext) hcl.Diagnostics {
373+
hookCalled = true
374+
gotV = v
375+
gotEvalCtx = ec
376+
return hcl.Diagnostics{
377+
&hcl.Diagnostic{
378+
Severity: hcl.DiagError,
379+
Summary: "Bad for_each",
380+
Detail: "I don't like it.",
381+
Expression: e,
382+
EvalContext: ec,
383+
Extra: "diagnostic extra",
384+
},
385+
}
386+
}),
387+
)
388+
389+
_, diags := expBody.Content(&hcl.BodySchema{
390+
Blocks: []hcl.BlockHeaderSchema{
391+
{
392+
Type: "foo",
393+
},
394+
},
395+
})
396+
if !diags.HasErrors() {
397+
t.Fatal("succeeded; want an error")
398+
}
399+
if len(diags) != 1 {
400+
t.Fatalf("wrong number of diagnostics; want only one\n%s", spew.Sdump(diags))
401+
}
402+
if got, want := diags[0].Summary, "Bad for_each"; got != want {
403+
t.Fatalf("wrong error\ngot: %s\nwant: %s\n\n%s", got, want, spew.Sdump(diags[0]))
404+
}
405+
if got, want := diags[0].Extra, "diagnostic extra"; got != want {
406+
// This is important to allow the application which provided the
407+
// hook to pass application-specific extra values through this
408+
// API in case the hook's diagnostics need some sort of special
409+
// treatment.
410+
t.Fatalf("diagnostic didn't preserve 'extra' field\ngot: %s\nwant: %s\n\n%s", got, want, spew.Sdump(diags[0]))
411+
}
412+
413+
if !hookCalled {
414+
t.Fatal("check hook wasn't called")
415+
}
416+
if !gotV.HasMark("boop") {
417+
t.Errorf("wrong value passed to check hook; want the value marked \"boop\"\n%s", ctydebug.ValueString(gotV))
418+
}
419+
if gotEvalCtx != evalCtx {
420+
t.Error("wrong EvalContext passed to check hook; want the one passed to Expand")
421+
}
422+
}
423+
339424
func TestExpandUnknownBodies(t *testing.T) {
340425
srcContent := &hcl.BodyContent{
341426
Blocks: hcl.Blocks{

ext/dynblock/expand_spec.go

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,16 @@ func (b *expandBody) decodeSpec(blockS *hcl.BlockHeaderSchema, rawSpec *hcl.Bloc
4343
eachAttr := specContent.Attributes["for_each"]
4444
eachVal, eachDiags := eachAttr.Expr.Value(b.forEachCtx)
4545
diags = append(diags, eachDiags...)
46+
if diags.HasErrors() {
47+
return nil, diags
48+
}
49+
for _, check := range b.checkForEach {
50+
moreDiags := check(eachVal, eachAttr.Expr, b.forEachCtx)
51+
diags = append(diags, moreDiags...)
52+
if moreDiags.HasErrors() {
53+
return nil, diags
54+
}
55+
}
4656

4757
if !eachVal.CanIterateElements() && eachVal.Type() != cty.DynamicPseudoType {
4858
// We skip this error for DynamicPseudoType because that means we either

ext/dynblock/options.go

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
package dynblock
2+
3+
import (
4+
"github.com/hashicorp/hcl/v2"
5+
"github.com/zclconf/go-cty/cty"
6+
)
7+
8+
type ExpandOption interface {
9+
applyExpandOption(*expandBody)
10+
}
11+
12+
type optCheckForEach struct {
13+
check func(cty.Value, hcl.Expression, *hcl.EvalContext) hcl.Diagnostics
14+
}
15+
16+
func OptCheckForEach(check func(cty.Value, hcl.Expression, *hcl.EvalContext) hcl.Diagnostics) ExpandOption {
17+
return optCheckForEach{check}
18+
}
19+
20+
// applyExpandOption implements ExpandOption.
21+
func (o optCheckForEach) applyExpandOption(body *expandBody) {
22+
body.checkForEach = append(body.checkForEach, o.check)
23+
}

ext/dynblock/public.go

Lines changed: 21 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -27,24 +27,28 @@ import (
2727
// multi-dimensional iteration. However, it is not possible to
2828
// dynamically-generate the "dynamic" blocks themselves except through nesting.
2929
//
30-
// parent {
31-
// dynamic "child" {
32-
// for_each = child_objs
33-
// content {
34-
// dynamic "grandchild" {
35-
// for_each = child.value.children
36-
// labels = [grandchild.key]
37-
// content {
38-
// parent_key = child.key
39-
// value = grandchild.value
40-
// }
41-
// }
42-
// }
43-
// }
44-
// }
45-
func Expand(body hcl.Body, ctx *hcl.EvalContext) hcl.Body {
46-
return &expandBody{
30+
// parent {
31+
// dynamic "child" {
32+
// for_each = child_objs
33+
// content {
34+
// dynamic "grandchild" {
35+
// for_each = child.value.children
36+
// labels = [grandchild.key]
37+
// content {
38+
// parent_key = child.key
39+
// value = grandchild.value
40+
// }
41+
// }
42+
// }
43+
// }
44+
// }
45+
func Expand(body hcl.Body, ctx *hcl.EvalContext, opts ...ExpandOption) hcl.Body {
46+
ret := &expandBody{
4747
original: body,
4848
forEachCtx: ctx,
4949
}
50+
for _, opt := range opts {
51+
opt.applyExpandOption(ret)
52+
}
53+
return ret
5054
}

0 commit comments

Comments
 (0)