diff --git a/helper/plugin/unknown.go b/helper/plugin/unknown.go index cd4c2a6fe..48e24e5d6 100644 --- a/helper/plugin/unknown.go +++ b/helper/plugin/unknown.go @@ -8,12 +8,35 @@ import ( ) // SetUnknowns takes a cty.Value, and compares it to the schema setting any null -// leaf values which are computed as unknown. +// values which are computed to unknown. func SetUnknowns(val cty.Value, schema *configschema.Block) cty.Value { - if val.IsNull() || !val.IsKnown() { + if !val.IsKnown() { return val } + // If the object was null, we still need to handle the top level attributes + // which might be computed, but we don't need to expand the blocks. + if val.IsNull() { + objMap := map[string]cty.Value{} + allNull := true + for name, attr := range schema.Attributes { + switch { + case attr.Computed: + objMap[name] = cty.UnknownVal(attr.Type) + allNull = false + default: + objMap[name] = cty.NullVal(attr.Type) + } + } + + // If this object has no unknown attributes, then we can leave it null. + if allNull { + return val + } + + return cty.ObjectVal(objMap) + } + valMap := val.AsValueMap() newVals := make(map[string]cty.Value) @@ -35,12 +58,18 @@ func SetUnknowns(val cty.Value, schema *configschema.Block) cty.Value { continue } - blockType := blockS.Block.ImpliedType() + blockValType := blockVal.Type() + blockElementType := blockS.Block.ImpliedType() - switch blockS.Nesting { - case configschema.NestingSingle: + // This switches on the value type here, so we can correctly switch + // between Tuples/Lists and Maps/Objects. + switch { + case blockS.Nesting == configschema.NestingSingle: + // NestingSingle is the only exception here, where we treat the + // block directly as an object newVals[name] = SetUnknowns(blockVal, &blockS.Block) - case configschema.NestingSet, configschema.NestingList: + + case blockValType.IsSetType(), blockValType.IsListType(), blockValType.IsTupleType(): listVals := blockVal.AsValueSlice() newListVals := make([]cty.Value, 0, len(listVals)) @@ -48,24 +77,26 @@ func SetUnknowns(val cty.Value, schema *configschema.Block) cty.Value { newListVals = append(newListVals, SetUnknowns(v, &blockS.Block)) } - switch blockS.Nesting { - case configschema.NestingSet: + switch { + case blockValType.IsSetType(): switch len(newListVals) { case 0: - newVals[name] = cty.SetValEmpty(blockType) + newVals[name] = cty.SetValEmpty(blockElementType) default: newVals[name] = cty.SetVal(newListVals) } - case configschema.NestingList: + case blockValType.IsListType(): switch len(newListVals) { case 0: - newVals[name] = cty.ListValEmpty(blockType) + newVals[name] = cty.ListValEmpty(blockElementType) default: newVals[name] = cty.ListVal(newListVals) } + case blockValType.IsTupleType(): + newVals[name] = cty.TupleVal(newListVals) } - case configschema.NestingMap: + case blockValType.IsMapType(), blockValType.IsObjectType(): mapVals := blockVal.AsValueMap() newMapVals := make(map[string]cty.Value) @@ -73,15 +104,26 @@ func SetUnknowns(val cty.Value, schema *configschema.Block) cty.Value { newMapVals[k] = SetUnknowns(v, &blockS.Block) } - switch len(newMapVals) { - case 0: - newVals[name] = cty.MapValEmpty(blockType) - default: - newVals[name] = cty.MapVal(newMapVals) + switch { + case blockValType.IsMapType(): + switch len(newMapVals) { + case 0: + newVals[name] = cty.MapValEmpty(blockElementType) + default: + newVals[name] = cty.MapVal(newMapVals) + } + case blockValType.IsObjectType(): + if len(newMapVals) == 0 { + // We need to populate empty values to make a valid object. + for attr, ty := range blockElementType.AttributeTypes() { + newMapVals[attr] = cty.NullVal(ty) + } + } + newVals[name] = cty.ObjectVal(newMapVals) } default: - panic(fmt.Sprintf("failed to set unknown values for nested block %q", name)) + panic(fmt.Sprintf("failed to set unknown values for nested block %q:%#v", name, blockValType)) } } diff --git a/helper/plugin/unknown_test.go b/helper/plugin/unknown_test.go index df4ba42c4..4214b1849 100644 --- a/helper/plugin/unknown_test.go +++ b/helper/plugin/unknown_test.go @@ -50,14 +50,27 @@ func TestSetUnknowns(t *testing.T) { }, }, }, - cty.ObjectVal(map[string]cty.Value{}), + cty.NullVal(cty.Object(map[string]cty.Type{ + "foo": cty.String, + "bar": cty.String, + "baz": cty.Object(map[string]cty.Type{ + "boz": cty.String, + "biz": cty.String, + }), + })), cty.ObjectVal(map[string]cty.Value{ + "foo": cty.NullVal(cty.String), "bar": cty.UnknownVal(cty.String), }), }, - "no prior with set": { - // the set value should remain null + "null stays null": { + // if the object has no computed attributes, it should stay null &configschema.Block{ + Attributes: map[string]*configschema.Attribute{ + "foo": &configschema.Attribute{ + Type: cty.String, + }, + }, BlockTypes: map[string]*configschema.NestedBlock{ "baz": { Nesting: configschema.NestingSet, @@ -73,8 +86,52 @@ func TestSetUnknowns(t *testing.T) { }, }, }, - cty.ObjectVal(map[string]cty.Value{}), - cty.ObjectVal(map[string]cty.Value{}), + cty.NullVal(cty.Object(map[string]cty.Type{ + "foo": cty.String, + "baz": cty.Set(cty.Object(map[string]cty.Type{ + "boz": cty.String, + })), + })), + cty.NullVal(cty.Object(map[string]cty.Type{ + "foo": cty.String, + "baz": cty.Set(cty.Object(map[string]cty.Type{ + "boz": cty.String, + })), + })), + }, + "no prior with set": { + // the set value should remain null + &configschema.Block{ + Attributes: map[string]*configschema.Attribute{ + "foo": &configschema.Attribute{ + Type: cty.String, + Computed: true, + }, + }, + BlockTypes: map[string]*configschema.NestedBlock{ + "baz": { + Nesting: configschema.NestingSet, + Block: configschema.Block{ + Attributes: map[string]*configschema.Attribute{ + "boz": { + Type: cty.String, + Optional: true, + Computed: true, + }, + }, + }, + }, + }, + }, + cty.NullVal(cty.Object(map[string]cty.Type{ + "foo": cty.String, + "baz": cty.Set(cty.Object(map[string]cty.Type{ + "boz": cty.String, + })), + })), + cty.ObjectVal(map[string]cty.Value{ + "foo": cty.UnknownVal(cty.String), + }), }, "prior attributes": { &configschema.Block{ @@ -329,24 +386,97 @@ func TestSetUnknowns(t *testing.T) { }), }), }, + "prior nested list with dynamic": { + &configschema.Block{ + BlockTypes: map[string]*configschema.NestedBlock{ + "foo": { + Nesting: configschema.NestingList, + Block: configschema.Block{ + Attributes: map[string]*configschema.Attribute{ + "bar": { + Type: cty.String, + Optional: true, + Computed: true, + }, + "baz": { + Type: cty.DynamicPseudoType, + Optional: true, + Computed: true, + }, + }, + }, + }, + }, + }, + cty.ObjectVal(map[string]cty.Value{ + "foo": cty.TupleVal([]cty.Value{ + cty.ObjectVal(map[string]cty.Value{ + "bar": cty.NullVal(cty.String), + "baz": cty.NumberIntVal(8), + }), + }), + }), + cty.ObjectVal(map[string]cty.Value{ + "foo": cty.TupleVal([]cty.Value{ + cty.ObjectVal(map[string]cty.Value{ + "bar": cty.UnknownVal(cty.String), + "baz": cty.NumberIntVal(8), + }), + }), + }), + }, + "prior nested map with dynamic": { + &configschema.Block{ + BlockTypes: map[string]*configschema.NestedBlock{ + "foo": { + Nesting: configschema.NestingMap, + Block: configschema.Block{ + Attributes: map[string]*configschema.Attribute{ + "bar": { + Type: cty.String, + Optional: true, + Computed: true, + }, + "baz": { + Type: cty.DynamicPseudoType, + Optional: true, + Computed: true, + }, + }, + }, + }, + }, + }, + cty.ObjectVal(map[string]cty.Value{ + "foo": cty.ObjectVal(map[string]cty.Value{ + "a": cty.ObjectVal(map[string]cty.Value{ + "bar": cty.StringVal("beep"), + "baz": cty.NullVal(cty.DynamicPseudoType), + }), + "b": cty.ObjectVal(map[string]cty.Value{ + "bar": cty.StringVal("boop"), + "baz": cty.NumberIntVal(8), + }), + }), + }), + cty.ObjectVal(map[string]cty.Value{ + "foo": cty.ObjectVal(map[string]cty.Value{ + "a": cty.ObjectVal(map[string]cty.Value{ + "bar": cty.StringVal("beep"), + "baz": cty.UnknownVal(cty.DynamicPseudoType), + }), + "b": cty.ObjectVal(map[string]cty.Value{ + "bar": cty.StringVal("boop"), + "baz": cty.NumberIntVal(8), + }), + }), + }), + }, } { t.Run(n, func(t *testing.T) { - // coerce the values because SetUnknowns expects the values to be - // complete, and so we can take shortcuts writing them in the - // test. - v, err := tc.Schema.CoerceValue(tc.Val) - if err != nil { - t.Fatal(err) - } - - expected, err := tc.Schema.CoerceValue(tc.Expected) - if err != nil { - t.Fatal(err) - } - - got := SetUnknowns(v, tc.Schema) - if !got.RawEquals(expected) { - t.Fatalf("\nexpected: %#v\ngot: %#v\n", expected, got) + got := SetUnknowns(tc.Val, tc.Schema) + if !got.RawEquals(tc.Expected) { + t.Fatalf("\nexpected: %#v\ngot: %#v\n", tc.Expected, got) } }) }