From d17ba647a8945c534fda7d3239edb464cee132bb Mon Sep 17 00:00:00 2001 From: James Bardin Date: Thu, 7 Feb 2019 20:24:36 -0500 Subject: [PATCH] add SetUnknowns SetUnknown walks through a resource and changes any unset (null) values that are going computed in the schema to Unknown. --- helper/plugin/unknown.go | 89 +++++++++ helper/plugin/unknown_test.go | 353 ++++++++++++++++++++++++++++++++++ 2 files changed, 442 insertions(+) create mode 100644 helper/plugin/unknown.go create mode 100644 helper/plugin/unknown_test.go diff --git a/helper/plugin/unknown.go b/helper/plugin/unknown.go new file mode 100644 index 000000000..cd4c2a6fe --- /dev/null +++ b/helper/plugin/unknown.go @@ -0,0 +1,89 @@ +package plugin + +import ( + "fmt" + + "github.com/hashicorp/terraform/configs/configschema" + "github.com/zclconf/go-cty/cty" +) + +// SetUnknowns takes a cty.Value, and compares it to the schema setting any null +// leaf values which are computed as unknown. +func SetUnknowns(val cty.Value, schema *configschema.Block) cty.Value { + if val.IsNull() || !val.IsKnown() { + return val + } + + valMap := val.AsValueMap() + newVals := make(map[string]cty.Value) + + for name, attr := range schema.Attributes { + v := valMap[name] + + if attr.Computed && v.IsNull() { + newVals[name] = cty.UnknownVal(attr.Type) + continue + } + + newVals[name] = v + } + + for name, blockS := range schema.BlockTypes { + blockVal := valMap[name] + if blockVal.IsNull() || !blockVal.IsKnown() { + newVals[name] = blockVal + continue + } + + blockType := blockS.Block.ImpliedType() + + switch blockS.Nesting { + case configschema.NestingSingle: + newVals[name] = SetUnknowns(blockVal, &blockS.Block) + case configschema.NestingSet, configschema.NestingList: + listVals := blockVal.AsValueSlice() + newListVals := make([]cty.Value, 0, len(listVals)) + + for _, v := range listVals { + newListVals = append(newListVals, SetUnknowns(v, &blockS.Block)) + } + + switch blockS.Nesting { + case configschema.NestingSet: + switch len(newListVals) { + case 0: + newVals[name] = cty.SetValEmpty(blockType) + default: + newVals[name] = cty.SetVal(newListVals) + } + case configschema.NestingList: + switch len(newListVals) { + case 0: + newVals[name] = cty.ListValEmpty(blockType) + default: + newVals[name] = cty.ListVal(newListVals) + } + } + + case configschema.NestingMap: + mapVals := blockVal.AsValueMap() + newMapVals := make(map[string]cty.Value) + + for k, v := range mapVals { + newMapVals[k] = SetUnknowns(v, &blockS.Block) + } + + switch len(newMapVals) { + case 0: + newVals[name] = cty.MapValEmpty(blockType) + default: + newVals[name] = cty.MapVal(newMapVals) + } + + default: + panic(fmt.Sprintf("failed to set unknown values for nested block %q", name)) + } + } + + return cty.ObjectVal(newVals) +} diff --git a/helper/plugin/unknown_test.go b/helper/plugin/unknown_test.go new file mode 100644 index 000000000..df4ba42c4 --- /dev/null +++ b/helper/plugin/unknown_test.go @@ -0,0 +1,353 @@ +package plugin + +import ( + "testing" + + "github.com/hashicorp/terraform/configs/configschema" + "github.com/zclconf/go-cty/cty" +) + +func TestSetUnknowns(t *testing.T) { + for n, tc := range map[string]struct { + Schema *configschema.Block + Val cty.Value + Expected cty.Value + }{ + "empty": { + &configschema.Block{}, + cty.EmptyObjectVal, + cty.EmptyObjectVal, + }, + "no prior": { + &configschema.Block{ + Attributes: map[string]*configschema.Attribute{ + "foo": { + Type: cty.String, + Optional: true, + }, + "bar": { + Type: cty.String, + Computed: true, + }, + }, + BlockTypes: map[string]*configschema.NestedBlock{ + "baz": { + Nesting: configschema.NestingSingle, + Block: configschema.Block{ + Attributes: map[string]*configschema.Attribute{ + "boz": { + Type: cty.String, + Optional: true, + Computed: true, + }, + "biz": { + Type: cty.String, + Optional: true, + Computed: true, + }, + }, + }, + }, + }, + }, + cty.ObjectVal(map[string]cty.Value{}), + cty.ObjectVal(map[string]cty.Value{ + "bar": cty.UnknownVal(cty.String), + }), + }, + "no prior with set": { + // the set value should remain null + &configschema.Block{ + 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.ObjectVal(map[string]cty.Value{}), + cty.ObjectVal(map[string]cty.Value{}), + }, + "prior attributes": { + &configschema.Block{ + Attributes: map[string]*configschema.Attribute{ + "foo": { + Type: cty.String, + Optional: true, + }, + "bar": { + Type: cty.String, + Computed: true, + }, + "baz": { + Type: cty.String, + Optional: true, + Computed: true, + }, + "boz": { + Type: cty.String, + Optional: true, + Computed: true, + }, + }, + }, + cty.ObjectVal(map[string]cty.Value{ + "foo": cty.StringVal("bonjour"), + "bar": cty.StringVal("petit dejeuner"), + "baz": cty.StringVal("grande dejeuner"), + }), + cty.ObjectVal(map[string]cty.Value{ + "foo": cty.StringVal("bonjour"), + "bar": cty.StringVal("petit dejeuner"), + "baz": cty.StringVal("grande dejeuner"), + "boz": cty.UnknownVal(cty.String), + }), + }, + "prior nested single": { + &configschema.Block{ + BlockTypes: map[string]*configschema.NestedBlock{ + "foo": { + Nesting: configschema.NestingSingle, + Block: configschema.Block{ + Attributes: map[string]*configschema.Attribute{ + "bar": { + Type: cty.String, + Optional: true, + Computed: true, + }, + "baz": { + Type: cty.String, + Optional: true, + Computed: true, + }, + }, + }, + }, + }, + }, + cty.ObjectVal(map[string]cty.Value{ + "foo": cty.ObjectVal(map[string]cty.Value{ + "bar": cty.StringVal("beep"), + }), + }), + cty.ObjectVal(map[string]cty.Value{ + "foo": cty.ObjectVal(map[string]cty.Value{ + "bar": cty.StringVal("beep"), + "baz": cty.UnknownVal(cty.String), + }), + }), + }, + "prior nested list": { + &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.String, + Optional: true, + Computed: true, + }, + }, + }, + }, + }, + }, + cty.ObjectVal(map[string]cty.Value{ + "foo": cty.ListVal([]cty.Value{ + cty.ObjectVal(map[string]cty.Value{ + "bar": cty.StringVal("bap"), + }), + cty.ObjectVal(map[string]cty.Value{ + "bar": cty.StringVal("blep"), + }), + }), + }), + cty.ObjectVal(map[string]cty.Value{ + "foo": cty.ListVal([]cty.Value{ + cty.ObjectVal(map[string]cty.Value{ + "bar": cty.StringVal("bap"), + "baz": cty.UnknownVal(cty.String), + }), + cty.ObjectVal(map[string]cty.Value{ + "bar": cty.StringVal("blep"), + "baz": cty.UnknownVal(cty.String), + }), + }), + }), + }, + "prior nested map": { + &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.String, + Optional: true, + Computed: true, + }, + }, + }, + }, + }, + }, + cty.ObjectVal(map[string]cty.Value{ + "foo": cty.MapVal(map[string]cty.Value{ + "a": cty.ObjectVal(map[string]cty.Value{ + "bar": cty.NullVal(cty.String), + "baz": cty.StringVal("boop"), + }), + "b": cty.ObjectVal(map[string]cty.Value{ + "bar": cty.StringVal("blep"), + "baz": cty.NullVal(cty.String), + }), + }), + }), + cty.ObjectVal(map[string]cty.Value{ + "foo": cty.MapVal(map[string]cty.Value{ + "a": cty.ObjectVal(map[string]cty.Value{ + "bar": cty.UnknownVal(cty.String), + "baz": cty.StringVal("boop"), + }), + "b": cty.ObjectVal(map[string]cty.Value{ + "bar": cty.StringVal("blep"), + "baz": cty.UnknownVal(cty.String), + }), + }), + }), + }, + "prior nested set": { + &configschema.Block{ + BlockTypes: map[string]*configschema.NestedBlock{ + "foo": { + Nesting: configschema.NestingSet, + Block: configschema.Block{ + Attributes: map[string]*configschema.Attribute{ + "bar": { + Type: cty.String, + Optional: true, + }, + "baz": { + Type: cty.String, + Optional: true, + Computed: true, + }, + }, + }, + }, + }, + }, + cty.ObjectVal(map[string]cty.Value{ + "foo": cty.SetVal([]cty.Value{ + cty.ObjectVal(map[string]cty.Value{ + "bar": cty.StringVal("blep"), + "baz": cty.NullVal(cty.String), + }), + cty.ObjectVal(map[string]cty.Value{ + "bar": cty.StringVal("boop"), + "baz": cty.NullVal(cty.String), + }), + }), + }), + cty.ObjectVal(map[string]cty.Value{ + "foo": cty.SetVal([]cty.Value{ + cty.ObjectVal(map[string]cty.Value{ + "bar": cty.StringVal("blep"), + "baz": cty.UnknownVal(cty.String), + }), + cty.ObjectVal(map[string]cty.Value{ + "bar": cty.StringVal("boop"), + "baz": cty.UnknownVal(cty.String), + }), + }), + }), + }, + "sets differing only by unknown": { + &configschema.Block{ + BlockTypes: map[string]*configschema.NestedBlock{ + "foo": { + Nesting: configschema.NestingSet, + Block: configschema.Block{ + Attributes: map[string]*configschema.Attribute{ + "bar": { + Type: cty.String, + Optional: true, + }, + "baz": { + Type: cty.String, + Optional: true, + Computed: true, + }, + }, + }, + }, + }, + }, + cty.ObjectVal(map[string]cty.Value{ + "foo": cty.SetVal([]cty.Value{ + cty.ObjectVal(map[string]cty.Value{ + "bar": cty.StringVal("boop"), + "baz": cty.NullVal(cty.String), + }), + cty.ObjectVal(map[string]cty.Value{ + "bar": cty.StringVal("boop"), + "baz": cty.UnknownVal(cty.String), + }), + }), + }), + cty.ObjectVal(map[string]cty.Value{ + "foo": cty.SetVal([]cty.Value{ + cty.ObjectVal(map[string]cty.Value{ + "bar": cty.StringVal("boop"), + "baz": cty.UnknownVal(cty.String), + }), + cty.ObjectVal(map[string]cty.Value{ + "bar": cty.StringVal("boop"), + "baz": cty.UnknownVal(cty.String), + }), + }), + }), + }, + } { + 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) + } + }) + } +}