From 299fe25a047119ffb5ee92977af4fd36c37d34f9 Mon Sep 17 00:00:00 2001 From: Martin Atkins Date: Tue, 22 May 2018 18:36:23 -0700 Subject: [PATCH] hcl2shim: Handle unknown values when shimming to/from flatmap Previously unknown values were round-tripping through flatmap and coming out as known strings containing the UnknownVariableValue. (The classic bug that, ironically, was one of the big reasons to write cty!) Now we properly handle unknown values in both directions: going in to flatmap we write UnknownVariableValue at the appropriate key (as the count for sequences or maps) and then coming out of flatmap we turn UnknownVariableValue back into a cty unknown value of the requested type. --- config/hcl2shim/flatmap.go | 38 +++++++++++- config/hcl2shim/flatmap_test.go | 102 ++++++++++++++++++++++++++++++++ 2 files changed, 138 insertions(+), 2 deletions(-) diff --git a/config/hcl2shim/flatmap.go b/config/hcl2shim/flatmap.go index f013bf887..e42e3beb9 100644 --- a/config/hcl2shim/flatmap.go +++ b/config/hcl2shim/flatmap.go @@ -64,6 +64,20 @@ func flatmapValueFromHCL2Primitive(m map[string]string, key string, val cty.Valu } func flatmapValueFromHCL2Map(m map[string]string, prefix string, val cty.Value) { + if !val.IsKnown() { + switch { + case val.Type().IsObjectType(): + // Whole objects can't be unknown in flatmap, so instead we'll + // just write all of the attribute values out as unknown. + for name, aty := range val.Type().AttributeTypes() { + flatmapValueFromHCL2Value(m, prefix+name, cty.UnknownVal(aty)) + } + default: + m[prefix+"%"] = UnknownVariableValue + } + return + } + len := 0 for it := val.ElementIterator(); it.Next(); { ak, av := it.Element() @@ -77,6 +91,11 @@ func flatmapValueFromHCL2Map(m map[string]string, prefix string, val cty.Value) } func flatmapValueFromHCL2Seq(m map[string]string, prefix string, val cty.Value) { + if !val.IsKnown() { + m[prefix+"#"] = UnknownVariableValue + return + } + // For sets this won't actually generate exactly what helper/schema would've // generated, because we don't have access to the set key function it // would've used. However, in practice it doesn't actually matter what the @@ -150,6 +169,9 @@ func hcl2ValueFromFlatmapPrimitive(m map[string]string, key string, ty cty.Type) if !exists { return cty.NullVal(ty), nil } + if rawVal == UnknownVariableValue { + return cty.UnknownVal(ty), nil + } var err error val := cty.StringVal(rawVal) @@ -182,6 +204,10 @@ func hcl2ValueFromFlatmapTuple(m map[string]string, prefix string, etys []cty.Ty if !exists { return cty.NullVal(cty.Tuple(etys)), nil } + if countStr == UnknownVariableValue { + return cty.UnknownVal(cty.Tuple(etys)), nil + } + count, err := strconv.Atoi(countStr) if err != nil { return cty.DynamicVal, fmt.Errorf("invalid count value for %q in state: %s", prefix, err) @@ -209,8 +235,10 @@ func hcl2ValueFromFlatmapMap(m map[string]string, prefix string, ty cty.Type) (c // We actually don't really care about the "count" of a map for our // purposes here, but we do need to check if it _exists_ in order to // recognize the difference between null (not set at all) and empty. - if _, exists := m[prefix+"%"]; !exists { + if strCount, exists := m[prefix+"%"]; !exists { return cty.NullVal(ty), nil + } else if strCount == UnknownVariableValue { + return cty.UnknownVal(ty), nil } for fullKey := range m { @@ -249,6 +277,10 @@ func hcl2ValueFromFlatmapList(m map[string]string, prefix string, ty cty.Type) ( if !exists { return cty.NullVal(ty), nil } + if countStr == UnknownVariableValue { + return cty.UnknownVal(ty), nil + } + count, err := strconv.Atoi(countStr) if err != nil { return cty.DynamicVal, fmt.Errorf("invalid count value for %q in state: %s", prefix, err) @@ -279,8 +311,10 @@ func hcl2ValueFromFlatmapSet(m map[string]string, prefix string, ty cty.Type) (c // We actually don't really care about the "count" of a set for our // purposes here, but we do need to check if it _exists_ in order to // recognize the difference between null (not set at all) and empty. - if _, exists := m[prefix+"#"]; !exists { + if strCount, exists := m[prefix+"#"]; !exists { return cty.NullVal(ty), nil + } else if strCount == UnknownVariableValue { + return cty.UnknownVal(ty), nil } for fullKey := range m { diff --git a/config/hcl2shim/flatmap_test.go b/config/hcl2shim/flatmap_test.go index ee0a5d6f8..7b4d822f4 100644 --- a/config/hcl2shim/flatmap_test.go +++ b/config/hcl2shim/flatmap_test.go @@ -26,6 +26,14 @@ func TestFlatmapValueFromHCL2(t *testing.T) { "foo": "hello", }, }, + { + cty.ObjectVal(map[string]cty.Value{ + "foo": cty.UnknownVal(cty.Bool), + }), + map[string]string{ + "foo": UnknownVariableValue, + }, + }, { cty.ObjectVal(map[string]cty.Value{ "foo": cty.NumberIntVal(12), @@ -64,6 +72,14 @@ func TestFlatmapValueFromHCL2(t *testing.T) { "foo.#": "0", }, }, + { + cty.ObjectVal(map[string]cty.Value{ + "foo": cty.UnknownVal(cty.List(cty.String)), + }), + map[string]string{ + "foo.#": UnknownVariableValue, + }, + }, { cty.ObjectVal(map[string]cty.Value{ "foo": cty.ListVal([]cty.Value{ @@ -101,6 +117,14 @@ func TestFlatmapValueFromHCL2(t *testing.T) { "foo.hello.world": "10", }, }, + { + cty.ObjectVal(map[string]cty.Value{ + "foo": cty.UnknownVal(cty.Map(cty.String)), + }), + map[string]string{ + "foo.%": UnknownVariableValue, + }, + }, { cty.ObjectVal(map[string]cty.Value{ "foo": cty.MapVal(map[string]cty.Value{ @@ -127,6 +151,14 @@ func TestFlatmapValueFromHCL2(t *testing.T) { "foo.1": "world", }, }, + { + cty.ObjectVal(map[string]cty.Value{ + "foo": cty.UnknownVal(cty.Set(cty.Number)), + }), + map[string]string{ + "foo.#": UnknownVariableValue, + }, + }, { cty.ObjectVal(map[string]cty.Value{ "foo": cty.ListVal([]cty.Value{ @@ -179,6 +211,23 @@ func TestFlatmapValueFromHCL2(t *testing.T) { "foo.1.baz.1": "true", }, }, + { + cty.ObjectVal(map[string]cty.Value{ + "foo": cty.ListVal([]cty.Value{ + cty.UnknownVal(cty.Object(map[string]cty.Type{ + "bar": cty.String, + "baz": cty.List(cty.Bool), + "bap": cty.Map(cty.Number), + })), + }), + }), + map[string]string{ + "foo.#": "1", + "foo.0.bar": UnknownVariableValue, + "foo.0.baz.#": UnknownVariableValue, + "foo.0.bap.%": UnknownVariableValue, + }, + }, } for _, test := range tests { @@ -216,16 +265,19 @@ func TestHCL2ValueFromFlatmap(t *testing.T) { "foo": "blah", "bar": "true", "baz": "12.5", + "unk": UnknownVariableValue, }, Type: cty.Object(map[string]cty.Type{ "foo": cty.String, "bar": cty.Bool, "baz": cty.Number, + "unk": cty.Bool, }), Want: cty.ObjectVal(map[string]cty.Value{ "foo": cty.StringVal("blah"), "bar": cty.True, "baz": cty.NumberFloatVal(12.5), + "unk": cty.UnknownVal(cty.Bool), }), }, { @@ -239,6 +291,17 @@ func TestHCL2ValueFromFlatmap(t *testing.T) { "foo": cty.ListValEmpty(cty.String), }), }, + { + Flatmap: map[string]string{ + "foo.#": UnknownVariableValue, + }, + Type: cty.Object(map[string]cty.Type{ + "foo": cty.List(cty.String), + }), + Want: cty.ObjectVal(map[string]cty.Value{ + "foo": cty.UnknownVal(cty.List(cty.String)), + }), + }, { Flatmap: map[string]string{ "foo.#": "1", @@ -288,6 +351,23 @@ func TestHCL2ValueFromFlatmap(t *testing.T) { }), }), }, + { + Flatmap: map[string]string{ + "foo.#": UnknownVariableValue, + }, + Type: cty.Object(map[string]cty.Type{ + "foo": cty.Tuple([]cty.Type{ + cty.String, + cty.Bool, + }), + }), + Want: cty.ObjectVal(map[string]cty.Value{ + "foo": cty.UnknownVal(cty.Tuple([]cty.Type{ + cty.String, + cty.Bool, + })), + }), + }, { Flatmap: map[string]string{ "foo.#": "0", @@ -299,6 +379,17 @@ func TestHCL2ValueFromFlatmap(t *testing.T) { "foo": cty.SetValEmpty(cty.String), }), }, + { + Flatmap: map[string]string{ + "foo.#": UnknownVariableValue, + }, + Type: cty.Object(map[string]cty.Type{ + "foo": cty.Set(cty.String), + }), + Want: cty.ObjectVal(map[string]cty.Value{ + "foo": cty.UnknownVal(cty.Set(cty.String)), + }), + }, { Flatmap: map[string]string{ "foo.#": "1", @@ -357,6 +448,17 @@ func TestHCL2ValueFromFlatmap(t *testing.T) { }), }), }, + { + Flatmap: map[string]string{ + "foo.%": UnknownVariableValue, + }, + Type: cty.Object(map[string]cty.Type{ + "foo": cty.Map(cty.Bool), + }), + Want: cty.ObjectVal(map[string]cty.Value{ + "foo": cty.UnknownVal(cty.Map(cty.Bool)), + }), + }, { Flatmap: map[string]string{ "foo.#": "2",