3dfebdfc45
Previously our behavior for an unknown for_each was to produce a single block whose content was the result of evaluating content with the iterator set to cty.DynamicVal. That produced a reasonable idea of the content, but the number of blocks in the result was still not accurate, and that can present a problem for applications that use unknown values to predict the overall shape of a not-yet-complete structure. We can't return an unknown block via the HCL API, but to make that situation easier to recognize by callers we'll now go a little further and force _all_ of the leaf attributes in such a block to be unknown values, even if they are constants in the configuration. This allows a calling application that is making predictions to use a single object whose leaves are all unknown as a heuristic to recognize what is effectively an unknown set of blocks. This is still not a perfect heuristic, but is the best we can do here within the HCL API assumptions. A fundamental assumption of the HCL API is that it's possible to walk the block structure without evaluating any expressions and the dynamic block extension is intentionally subverting that assumption, so some oddities are to be expected. Calling applications that need a fully reliable sense of the final structure should not use the dynamic block extension.
384 lines
11 KiB
Go
384 lines
11 KiB
Go
package dynblock
|
|
|
|
import (
|
|
"testing"
|
|
|
|
"github.com/hashicorp/hcl2/hcl"
|
|
"github.com/hashicorp/hcl2/hcldec"
|
|
"github.com/hashicorp/hcl2/hcltest"
|
|
"github.com/zclconf/go-cty/cty"
|
|
)
|
|
|
|
func TestExpand(t *testing.T) {
|
|
srcBody := hcltest.MockBody(&hcl.BodyContent{
|
|
Blocks: hcl.Blocks{
|
|
{
|
|
Type: "a",
|
|
Labels: []string{"static0"},
|
|
LabelRanges: []hcl.Range{hcl.Range{}},
|
|
Body: hcltest.MockBody(&hcl.BodyContent{
|
|
Attributes: hcltest.MockAttrs(map[string]hcl.Expression{
|
|
"val": hcltest.MockExprLiteral(cty.StringVal("static a 0")),
|
|
}),
|
|
}),
|
|
},
|
|
{
|
|
Type: "b",
|
|
Body: hcltest.MockBody(&hcl.BodyContent{
|
|
Blocks: hcl.Blocks{
|
|
{
|
|
Type: "c",
|
|
Body: hcltest.MockBody(&hcl.BodyContent{
|
|
Attributes: hcltest.MockAttrs(map[string]hcl.Expression{
|
|
"val0": hcltest.MockExprLiteral(cty.StringVal("static c 0")),
|
|
}),
|
|
}),
|
|
},
|
|
{
|
|
Type: "dynamic",
|
|
Labels: []string{"c"},
|
|
LabelRanges: []hcl.Range{hcl.Range{}},
|
|
Body: hcltest.MockBody(&hcl.BodyContent{
|
|
Attributes: hcltest.MockAttrs(map[string]hcl.Expression{
|
|
"for_each": hcltest.MockExprLiteral(cty.ListVal([]cty.Value{
|
|
cty.StringVal("dynamic c 0"),
|
|
cty.StringVal("dynamic c 1"),
|
|
})),
|
|
"iterator": hcltest.MockExprVariable("dyn_c"),
|
|
}),
|
|
Blocks: hcl.Blocks{
|
|
{
|
|
Type: "content",
|
|
Body: hcltest.MockBody(&hcl.BodyContent{
|
|
Attributes: hcltest.MockAttrs(map[string]hcl.Expression{
|
|
"val0": hcltest.MockExprTraversalSrc("dyn_c.value"),
|
|
}),
|
|
}),
|
|
},
|
|
},
|
|
}),
|
|
},
|
|
},
|
|
}),
|
|
},
|
|
{
|
|
Type: "dynamic",
|
|
Labels: []string{"a"},
|
|
LabelRanges: []hcl.Range{hcl.Range{}},
|
|
Body: hcltest.MockBody(&hcl.BodyContent{
|
|
Attributes: hcltest.MockAttrs(map[string]hcl.Expression{
|
|
"for_each": hcltest.MockExprLiteral(cty.ListVal([]cty.Value{
|
|
cty.StringVal("dynamic a 0"),
|
|
cty.StringVal("dynamic a 1"),
|
|
cty.StringVal("dynamic a 2"),
|
|
})),
|
|
"labels": hcltest.MockExprList([]hcl.Expression{
|
|
hcltest.MockExprTraversalSrc("a.key"),
|
|
}),
|
|
}),
|
|
Blocks: hcl.Blocks{
|
|
{
|
|
Type: "content",
|
|
Body: hcltest.MockBody(&hcl.BodyContent{
|
|
Attributes: hcltest.MockAttrs(map[string]hcl.Expression{
|
|
"val": hcltest.MockExprTraversalSrc("a.value"),
|
|
}),
|
|
}),
|
|
},
|
|
},
|
|
}),
|
|
},
|
|
{
|
|
Type: "dynamic",
|
|
Labels: []string{"b"},
|
|
LabelRanges: []hcl.Range{hcl.Range{}},
|
|
Body: hcltest.MockBody(&hcl.BodyContent{
|
|
Attributes: hcltest.MockAttrs(map[string]hcl.Expression{
|
|
"for_each": hcltest.MockExprLiteral(cty.ListVal([]cty.Value{
|
|
cty.StringVal("dynamic b 0"),
|
|
cty.StringVal("dynamic b 1"),
|
|
})),
|
|
"iterator": hcltest.MockExprVariable("dyn_b"),
|
|
}),
|
|
Blocks: hcl.Blocks{
|
|
{
|
|
Type: "content",
|
|
Body: hcltest.MockBody(&hcl.BodyContent{
|
|
Blocks: hcl.Blocks{
|
|
{
|
|
Type: "c",
|
|
Body: hcltest.MockBody(&hcl.BodyContent{
|
|
Attributes: hcltest.MockAttrs(map[string]hcl.Expression{
|
|
"val0": hcltest.MockExprLiteral(cty.StringVal("static c 1")),
|
|
"val1": hcltest.MockExprTraversalSrc("dyn_b.value"),
|
|
}),
|
|
}),
|
|
},
|
|
{
|
|
Type: "dynamic",
|
|
Labels: []string{"c"},
|
|
LabelRanges: []hcl.Range{hcl.Range{}},
|
|
Body: hcltest.MockBody(&hcl.BodyContent{
|
|
Attributes: hcltest.MockAttrs(map[string]hcl.Expression{
|
|
"for_each": hcltest.MockExprLiteral(cty.ListVal([]cty.Value{
|
|
cty.StringVal("dynamic c 2"),
|
|
cty.StringVal("dynamic c 3"),
|
|
})),
|
|
}),
|
|
Blocks: hcl.Blocks{
|
|
{
|
|
Type: "content",
|
|
Body: hcltest.MockBody(&hcl.BodyContent{
|
|
Attributes: hcltest.MockAttrs(map[string]hcl.Expression{
|
|
"val0": hcltest.MockExprTraversalSrc("c.value"),
|
|
"val1": hcltest.MockExprTraversalSrc("dyn_b.value"),
|
|
}),
|
|
}),
|
|
},
|
|
},
|
|
}),
|
|
},
|
|
},
|
|
}),
|
|
},
|
|
},
|
|
}),
|
|
},
|
|
{
|
|
Type: "dynamic",
|
|
Labels: []string{"b"},
|
|
LabelRanges: []hcl.Range{hcl.Range{}},
|
|
Body: hcltest.MockBody(&hcl.BodyContent{
|
|
Attributes: hcltest.MockAttrs(map[string]hcl.Expression{
|
|
"for_each": hcltest.MockExprLiteral(cty.MapVal(map[string]cty.Value{
|
|
"foo": cty.ListVal([]cty.Value{
|
|
cty.StringVal("dynamic c nested 0"),
|
|
cty.StringVal("dynamic c nested 1"),
|
|
}),
|
|
})),
|
|
"iterator": hcltest.MockExprVariable("dyn_b"),
|
|
}),
|
|
Blocks: hcl.Blocks{
|
|
{
|
|
Type: "content",
|
|
Body: hcltest.MockBody(&hcl.BodyContent{
|
|
Blocks: hcl.Blocks{
|
|
{
|
|
Type: "dynamic",
|
|
Labels: []string{"c"},
|
|
LabelRanges: []hcl.Range{hcl.Range{}},
|
|
Body: hcltest.MockBody(&hcl.BodyContent{
|
|
Attributes: hcltest.MockAttrs(map[string]hcl.Expression{
|
|
"for_each": hcltest.MockExprTraversalSrc("dyn_b.value"),
|
|
}),
|
|
Blocks: hcl.Blocks{
|
|
{
|
|
Type: "content",
|
|
Body: hcltest.MockBody(&hcl.BodyContent{
|
|
Attributes: hcltest.MockAttrs(map[string]hcl.Expression{
|
|
"val0": hcltest.MockExprTraversalSrc("c.value"),
|
|
"val1": hcltest.MockExprTraversalSrc("dyn_b.key"),
|
|
}),
|
|
}),
|
|
},
|
|
},
|
|
}),
|
|
},
|
|
},
|
|
}),
|
|
},
|
|
},
|
|
}),
|
|
},
|
|
{
|
|
Type: "dynamic",
|
|
Labels: []string{"b"},
|
|
LabelRanges: []hcl.Range{hcl.Range{}},
|
|
Body: hcltest.MockBody(&hcl.BodyContent{
|
|
Attributes: hcltest.MockAttrs(map[string]hcl.Expression{
|
|
"for_each": hcltest.MockExprLiteral(cty.UnknownVal(cty.Map(cty.String))),
|
|
"iterator": hcltest.MockExprVariable("dyn_b"),
|
|
}),
|
|
Blocks: hcl.Blocks{
|
|
{
|
|
Type: "content",
|
|
Body: hcltest.MockBody(&hcl.BodyContent{
|
|
Blocks: hcl.Blocks{
|
|
{
|
|
Type: "dynamic",
|
|
Labels: []string{"c"},
|
|
LabelRanges: []hcl.Range{hcl.Range{}},
|
|
Body: hcltest.MockBody(&hcl.BodyContent{
|
|
Attributes: hcltest.MockAttrs(map[string]hcl.Expression{
|
|
"for_each": hcltest.MockExprTraversalSrc("dyn_b.value"),
|
|
}),
|
|
Blocks: hcl.Blocks{
|
|
{
|
|
Type: "content",
|
|
Body: hcltest.MockBody(&hcl.BodyContent{
|
|
Attributes: hcltest.MockAttrs(map[string]hcl.Expression{
|
|
"val0": hcltest.MockExprTraversalSrc("c.value"),
|
|
"val1": hcltest.MockExprTraversalSrc("dyn_b.key"),
|
|
}),
|
|
}),
|
|
},
|
|
},
|
|
}),
|
|
},
|
|
},
|
|
}),
|
|
},
|
|
},
|
|
}),
|
|
},
|
|
{
|
|
Type: "a",
|
|
Labels: []string{"static1"},
|
|
LabelRanges: []hcl.Range{hcl.Range{}},
|
|
Body: hcltest.MockBody(&hcl.BodyContent{
|
|
Attributes: hcltest.MockAttrs(map[string]hcl.Expression{
|
|
"val": hcltest.MockExprLiteral(cty.StringVal("static a 1")),
|
|
}),
|
|
}),
|
|
},
|
|
},
|
|
})
|
|
|
|
dynBody := Expand(srcBody, nil)
|
|
var remain hcl.Body
|
|
|
|
t.Run("PartialDecode", func(t *testing.T) {
|
|
decSpec := &hcldec.BlockMapSpec{
|
|
TypeName: "a",
|
|
LabelNames: []string{"key"},
|
|
Nested: &hcldec.AttrSpec{
|
|
Name: "val",
|
|
Type: cty.String,
|
|
Required: true,
|
|
},
|
|
}
|
|
|
|
var got cty.Value
|
|
var diags hcl.Diagnostics
|
|
got, remain, diags = hcldec.PartialDecode(dynBody, decSpec, nil)
|
|
if len(diags) != 0 {
|
|
t.Errorf("unexpected diagnostics")
|
|
for _, diag := range diags {
|
|
t.Logf("- %s", diag)
|
|
}
|
|
return
|
|
}
|
|
|
|
want := cty.MapVal(map[string]cty.Value{
|
|
"static0": cty.StringVal("static a 0"),
|
|
"static1": cty.StringVal("static a 1"),
|
|
"0": cty.StringVal("dynamic a 0"),
|
|
"1": cty.StringVal("dynamic a 1"),
|
|
"2": cty.StringVal("dynamic a 2"),
|
|
})
|
|
|
|
if !got.RawEquals(want) {
|
|
t.Errorf("wrong result\ngot: %#v\nwant: %#v", got, want)
|
|
}
|
|
})
|
|
|
|
t.Run("Decode", func(t *testing.T) {
|
|
decSpec := &hcldec.BlockListSpec{
|
|
TypeName: "b",
|
|
Nested: &hcldec.BlockListSpec{
|
|
TypeName: "c",
|
|
Nested: &hcldec.ObjectSpec{
|
|
"val0": &hcldec.AttrSpec{
|
|
Name: "val0",
|
|
Type: cty.String,
|
|
},
|
|
"val1": &hcldec.AttrSpec{
|
|
Name: "val1",
|
|
Type: cty.String,
|
|
},
|
|
},
|
|
},
|
|
}
|
|
|
|
var got cty.Value
|
|
var diags hcl.Diagnostics
|
|
got, diags = hcldec.Decode(remain, decSpec, nil)
|
|
if len(diags) != 0 {
|
|
t.Errorf("unexpected diagnostics")
|
|
for _, diag := range diags {
|
|
t.Logf("- %s", diag)
|
|
}
|
|
return
|
|
}
|
|
|
|
want := cty.ListVal([]cty.Value{
|
|
cty.ListVal([]cty.Value{
|
|
cty.ObjectVal(map[string]cty.Value{
|
|
"val0": cty.StringVal("static c 0"),
|
|
"val1": cty.NullVal(cty.String),
|
|
}),
|
|
cty.ObjectVal(map[string]cty.Value{
|
|
"val0": cty.StringVal("dynamic c 0"),
|
|
"val1": cty.NullVal(cty.String),
|
|
}),
|
|
cty.ObjectVal(map[string]cty.Value{
|
|
"val0": cty.StringVal("dynamic c 1"),
|
|
"val1": cty.NullVal(cty.String),
|
|
}),
|
|
}),
|
|
cty.ListVal([]cty.Value{
|
|
cty.ObjectVal(map[string]cty.Value{
|
|
"val0": cty.StringVal("static c 1"),
|
|
"val1": cty.StringVal("dynamic b 0"),
|
|
}),
|
|
cty.ObjectVal(map[string]cty.Value{
|
|
"val0": cty.StringVal("dynamic c 2"),
|
|
"val1": cty.StringVal("dynamic b 0"),
|
|
}),
|
|
cty.ObjectVal(map[string]cty.Value{
|
|
"val0": cty.StringVal("dynamic c 3"),
|
|
"val1": cty.StringVal("dynamic b 0"),
|
|
}),
|
|
}),
|
|
cty.ListVal([]cty.Value{
|
|
cty.ObjectVal(map[string]cty.Value{
|
|
"val0": cty.StringVal("static c 1"),
|
|
"val1": cty.StringVal("dynamic b 1"),
|
|
}),
|
|
cty.ObjectVal(map[string]cty.Value{
|
|
"val0": cty.StringVal("dynamic c 2"),
|
|
"val1": cty.StringVal("dynamic b 1"),
|
|
}),
|
|
cty.ObjectVal(map[string]cty.Value{
|
|
"val0": cty.StringVal("dynamic c 3"),
|
|
"val1": cty.StringVal("dynamic b 1"),
|
|
}),
|
|
}),
|
|
cty.ListVal([]cty.Value{
|
|
cty.ObjectVal(map[string]cty.Value{
|
|
"val0": cty.StringVal("dynamic c nested 0"),
|
|
"val1": cty.StringVal("foo"),
|
|
}),
|
|
cty.ObjectVal(map[string]cty.Value{
|
|
"val0": cty.StringVal("dynamic c nested 1"),
|
|
"val1": cty.StringVal("foo"),
|
|
}),
|
|
}),
|
|
cty.ListVal([]cty.Value{
|
|
// This one comes from a dynamic block with an unknown for_each
|
|
// value, so we produce a single block object with all of the
|
|
// leaf attribute values set to unknown values.
|
|
cty.ObjectVal(map[string]cty.Value{
|
|
"val0": cty.UnknownVal(cty.String),
|
|
"val1": cty.UnknownVal(cty.String),
|
|
}),
|
|
}),
|
|
})
|
|
|
|
if !got.RawEquals(want) {
|
|
t.Errorf("wrong result\ngot: %#v\nwant: %#v", got, want)
|
|
}
|
|
})
|
|
|
|
}
|