From 82588af89249982102c77512aa01fdb6c9e5b62f Mon Sep 17 00:00:00 2001 From: James Bardin Date: Fri, 8 Feb 2019 14:46:29 -0500 Subject: [PATCH] switch blocks based on value type, and check attrs Check attributes on null objects, and fill in unknowns. If we're evaluating the object, it either means we are at the top level, or a NestingSingle block was present, and in either case we need to treat the attributes as null rather than the entire object. Switch on the block types rather than Nesting, so we don't need add any logic to change between List/Tuple or Map/Object when DynamicPseudoType is involved. --- helper/plugin/unknown.go | 78 +++++++++++---- helper/plugin/unknown_test.go | 172 +++++++++++++++++++++++++++++----- 2 files changed, 211 insertions(+), 39 deletions(-) diff --git a/helper/plugin/unknown.go b/helper/plugin/unknown.go index cd4c2a6fe4..48e24e5d62 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 df4ba42c4d..4214b18497 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) } }) }