configschema: Fix ConfigSchema bugs with nested blocks

We were iterating over the wrong value to recursively coerce content for
nested blocks, and also incorrectly constructing the cty.Path used in
errors.
This commit is contained in:
Martin Atkins 2018-07-19 15:15:00 -07:00
parent 0120d53baf
commit d8bf3cc4e0
2 changed files with 173 additions and 29 deletions

View File

@ -108,7 +108,7 @@ func (b *Block) coerceValue(in cty.Value, path cty.Path) (cty.Value, error) {
}
if !coll.CanIterateElements() {
return cty.UnknownVal(b.ImpliedType()), path.NewErrorf("attribute %q must be a list", typeName)
return cty.UnknownVal(b.ImpliedType()), path.NewErrorf("must be a list")
}
l := coll.LengthInt()
if l < blockS.MinItems {
@ -122,15 +122,18 @@ func (b *Block) coerceValue(in cty.Value, path cty.Path) (cty.Value, error) {
continue
}
elems := make([]cty.Value, 0, l)
for it := in.ElementIterator(); it.Next(); {
{
path = append(path, cty.GetAttrStep{Name: typeName})
for it := coll.ElementIterator(); it.Next(); {
var err error
_, val := it.Element()
val, err = blockS.coerceValue(val, append(path, cty.GetAttrStep{Name: typeName}))
idx, val := it.Element()
val, err = blockS.coerceValue(val, append(path, cty.IndexStep{Key: idx}))
if err != nil {
return cty.UnknownVal(b.ImpliedType()), err
}
elems = append(elems, val)
}
}
attrs[typeName] = cty.ListVal(elems)
case blockS.MinItems == 0:
attrs[typeName] = cty.ListValEmpty(blockS.ImpliedType())
@ -153,7 +156,7 @@ func (b *Block) coerceValue(in cty.Value, path cty.Path) (cty.Value, error) {
}
if !coll.CanIterateElements() {
return cty.UnknownVal(b.ImpliedType()), path.NewErrorf("attribute %q must be a set", typeName)
return cty.UnknownVal(b.ImpliedType()), path.NewErrorf("must be a set")
}
l := coll.LengthInt()
if l < blockS.MinItems {
@ -167,15 +170,18 @@ func (b *Block) coerceValue(in cty.Value, path cty.Path) (cty.Value, error) {
continue
}
elems := make([]cty.Value, 0, l)
for it := in.ElementIterator(); it.Next(); {
{
path = append(path, cty.GetAttrStep{Name: typeName})
for it := coll.ElementIterator(); it.Next(); {
var err error
_, val := it.Element()
val, err = blockS.coerceValue(val, append(path, cty.GetAttrStep{Name: typeName}))
idx, val := it.Element()
val, err = blockS.coerceValue(val, append(path, cty.IndexStep{Key: idx}))
if err != nil {
return cty.UnknownVal(b.ImpliedType()), err
}
elems = append(elems, val)
}
}
attrs[typeName] = cty.SetVal(elems)
case blockS.MinItems == 0:
attrs[typeName] = cty.SetValEmpty(blockS.ImpliedType())
@ -198,7 +204,7 @@ func (b *Block) coerceValue(in cty.Value, path cty.Path) (cty.Value, error) {
}
if !coll.CanIterateElements() {
return cty.UnknownVal(b.ImpliedType()), path.NewErrorf("attribute %q must be a map", typeName)
return cty.UnknownVal(b.ImpliedType()), path.NewErrorf("must be a map")
}
l := coll.LengthInt()
if l == 0 {
@ -206,18 +212,21 @@ func (b *Block) coerceValue(in cty.Value, path cty.Path) (cty.Value, error) {
continue
}
elems := make(map[string]cty.Value)
for it := in.ElementIterator(); it.Next(); {
{
path = append(path, cty.GetAttrStep{Name: typeName})
for it := coll.ElementIterator(); it.Next(); {
var err error
key, val := it.Element()
if key.Type() != cty.String || key.IsNull() || !key.IsKnown() {
return cty.UnknownVal(b.ImpliedType()), path.NewErrorf("attribute %q must be a map", typeName)
return cty.UnknownVal(b.ImpliedType()), path.NewErrorf("must be a map")
}
val, err = blockS.coerceValue(val, append(path, cty.GetAttrStep{Name: typeName}))
val, err = blockS.coerceValue(val, append(path, cty.IndexStep{Key: key}))
if err != nil {
return cty.UnknownVal(b.ImpliedType()), err
}
elems[key.AsString()] = val
}
}
attrs[typeName] = cty.MapVal(elems)
default:
attrs[typeName] = cty.MapValEmpty(blockS.ImpliedType())

View File

@ -4,6 +4,8 @@ import (
"testing"
"github.com/zclconf/go-cty/cty"
"github.com/hashicorp/terraform/tfdiags"
)
func TestCoerceValue(t *testing.T) {
@ -66,7 +68,125 @@ func TestCoerceValue(t *testing.T) {
"foo": cty.True,
}),
cty.DynamicVal,
`an object is required`,
`.foo: an object is required`,
},
"list block with one item": {
&Block{
BlockTypes: map[string]*NestedBlock{
"foo": {
Block: Block{},
Nesting: NestingList,
},
},
},
cty.ObjectVal(map[string]cty.Value{
"foo": cty.ListVal([]cty.Value{cty.EmptyObjectVal}),
}),
cty.ObjectVal(map[string]cty.Value{
"foo": cty.ListVal([]cty.Value{cty.EmptyObjectVal}),
}),
``,
},
"set block with one item": {
&Block{
BlockTypes: map[string]*NestedBlock{
"foo": {
Block: Block{},
Nesting: NestingSet,
},
},
},
cty.ObjectVal(map[string]cty.Value{
"foo": cty.ListVal([]cty.Value{cty.EmptyObjectVal}), // can implicitly convert to set
}),
cty.ObjectVal(map[string]cty.Value{
"foo": cty.SetVal([]cty.Value{cty.EmptyObjectVal}),
}),
``,
},
"map block with one item": {
&Block{
BlockTypes: map[string]*NestedBlock{
"foo": {
Block: Block{},
Nesting: NestingMap,
},
},
},
cty.ObjectVal(map[string]cty.Value{
"foo": cty.MapVal(map[string]cty.Value{"foo": cty.EmptyObjectVal}),
}),
cty.ObjectVal(map[string]cty.Value{
"foo": cty.MapVal(map[string]cty.Value{"foo": cty.EmptyObjectVal}),
}),
``,
},
"list block with one item having an attribute": {
&Block{
BlockTypes: map[string]*NestedBlock{
"foo": {
Block: Block{
Attributes: map[string]*Attribute{
"bar": {
Type: cty.String,
Required: true,
},
},
},
Nesting: NestingList,
},
},
},
cty.ObjectVal(map[string]cty.Value{
"foo": cty.ListVal([]cty.Value{cty.ObjectVal(map[string]cty.Value{
"bar": cty.StringVal("hello"),
})}),
}),
cty.ObjectVal(map[string]cty.Value{
"foo": cty.ListVal([]cty.Value{cty.ObjectVal(map[string]cty.Value{
"bar": cty.StringVal("hello"),
})}),
}),
``,
},
"list block with one item having a missing attribute": {
&Block{
BlockTypes: map[string]*NestedBlock{
"foo": {
Block: Block{
Attributes: map[string]*Attribute{
"bar": {
Type: cty.String,
Required: true,
},
},
},
Nesting: NestingList,
},
},
},
cty.ObjectVal(map[string]cty.Value{
"foo": cty.ListVal([]cty.Value{cty.EmptyObjectVal}),
}),
cty.DynamicVal,
`.foo[0]: attribute "bar" is required`,
},
"list block with one item having an extraneous attribute": {
&Block{
BlockTypes: map[string]*NestedBlock{
"foo": {
Block: Block{},
Nesting: NestingList,
},
},
},
cty.ObjectVal(map[string]cty.Value{
"foo": cty.ListVal([]cty.Value{cty.ObjectVal(map[string]cty.Value{
"bar": cty.StringVal("hello"),
})}),
}),
cty.DynamicVal,
`.foo[0]: unexpected attribute "bar"`,
},
"missing optional attribute": {
&Block{
@ -207,6 +327,21 @@ func TestCoerceValue(t *testing.T) {
cty.DynamicVal,
`unexpected attribute "foo"`,
},
"wrong attribute type": {
&Block{
Attributes: map[string]*Attribute{
"foo": {
Type: cty.Number,
Required: true,
},
},
},
cty.ObjectVal(map[string]cty.Value{
"foo": cty.False,
}),
cty.DynamicVal,
`.foo: number required`,
},
}
for name, test := range tests {
@ -218,7 +353,7 @@ func TestCoerceValue(t *testing.T) {
t.Fatalf("coersion succeeded; want error: %q", test.WantErr)
}
} else {
gotErr := gotErrObj.Error()
gotErr := tfdiags.FormatError(gotErrObj)
if gotErr != test.WantErr {
t.Fatalf("wrong error\ngot: %s\nwant: %s", gotErr, test.WantErr)
}