diff --git a/internal/configs/configschema/coerce_value.go b/internal/configs/configschema/coerce_value.go index 4e3263df4387..d1ccc3013ef3 100644 --- a/internal/configs/configschema/coerce_value.go +++ b/internal/configs/configschema/coerce_value.go @@ -73,6 +73,14 @@ func (b *Block) coerceValue(in cty.Value, path cty.Path) (cty.Value, error) { return cty.UnknownVal(impliedType), path.NewErrorf("attribute %q has none of required, optional, or computed set", name) } + // check for NestingGroup which cannot be null + if attrS.NestedType != nil && attrS.NestedType.Nesting == NestingGroup && val.IsNull() { + // we can cheat here and use EmptyValue to get a "zero" value + // object, and expect the conversion to turn out the correct final + // object type + val = cty.EmptyObjectVal + } + val, err := convert.Convert(val, attrConvType) if err != nil { return cty.UnknownVal(impliedType), append(path, cty.GetAttrStep{Name: name}).NewError(err) diff --git a/internal/configs/configschema/coerce_value_test.go b/internal/configs/configschema/coerce_value_test.go index 3313bdca9a01..c71828fa816b 100644 --- a/internal/configs/configschema/coerce_value_test.go +++ b/internal/configs/configschema/coerce_value_test.go @@ -616,6 +616,54 @@ func TestCoerceValue(t *testing.T) { }), ``, }, + "nested type nulls": { + // handle NestedTypes with null + &Block{ + Attributes: map[string]*Attribute{ + "foo": { + NestedType: &Object{ + Nesting: NestingSingle, + Attributes: map[string]*Attribute{ + "bar": {Type: cty.DynamicPseudoType, Optional: true}, + "baz": {Type: cty.DynamicPseudoType, Optional: true}, + }, + }, + Optional: true, + }, + "fob": { + NestedType: &Object{ + Nesting: NestingGroup, + Attributes: map[string]*Attribute{ + "bar": {Type: cty.DynamicPseudoType, Optional: true}, + "baz": {Type: cty.DynamicPseudoType, Computed: true}, + }, + }, + Optional: true, + }, + }, + }, + cty.ObjectVal(map[string]cty.Value{ + "foo": cty.ObjectVal(map[string]cty.Value{ + "bar": cty.StringVal("test"), + "baz": cty.NullVal(cty.Number), + }), + "fob": cty.ObjectVal(map[string]cty.Value{ + "bar": cty.NullVal(cty.String), + "baz": cty.NullVal(cty.Number), + }), + }), + cty.ObjectVal(map[string]cty.Value{ + "foo": cty.ObjectVal(map[string]cty.Value{ + "bar": cty.StringVal("test"), + "baz": cty.NullVal(cty.Number), + }), + "fob": cty.ObjectVal(map[string]cty.Value{ + "bar": cty.NullVal(cty.String), + "baz": cty.NullVal(cty.Number), + }), + }), + ``, + }, } for name, test := range tests { @@ -633,7 +681,6 @@ func TestCoerceValue(t *testing.T) { } return } - if !gotValue.RawEquals(test.WantValue) { t.Errorf("wrong result\ninput: %#v\ngot: %#v\nwant: %#v", test.Input, gotValue, test.WantValue) } diff --git a/internal/configs/configschema/implied_type.go b/internal/configs/configschema/implied_type.go index c94c6591ff1a..ced1943d6776 100644 --- a/internal/configs/configschema/implied_type.go +++ b/internal/configs/configschema/implied_type.go @@ -138,8 +138,9 @@ func (o *Object) specType() cty.Type { } else { ret = cty.Object(attrTys) } + switch o.Nesting { - case NestingSingle: + case NestingSingle, NestingGroup: return ret case NestingList: return cty.List(ret) diff --git a/internal/configs/configschema/implied_type_test.go b/internal/configs/configschema/implied_type_test.go index 7dbb611d388d..08887536f887 100644 --- a/internal/configs/configschema/implied_type_test.go +++ b/internal/configs/configschema/implied_type_test.go @@ -316,6 +316,25 @@ func TestObjectImpliedType(t *testing.T) { }, ), }, + "nesting-group-attributes": { + &Object{ + Nesting: NestingGroup, + Attributes: map[string]*Attribute{ + "optional": {Type: cty.String, Optional: true}, + "required": {Type: cty.Number, Required: true}, + "computed": {Type: cty.List(cty.Bool), Computed: true}, + "optional_computed": {Type: cty.Map(cty.Bool), Optional: true, Computed: true}, + }, + }, + cty.Object( + map[string]cty.Type{ + "optional": cty.String, + "required": cty.Number, + "computed": cty.List(cty.Bool), + "optional_computed": cty.Map(cty.Bool), + }, + ), + }, "nested attributes": { &Object{ Nesting: NestingSingle,