Merge pull request #19943 from hashicorp/jbardin/empty-containers

Better handling for empty containers and zero values in provider shims
This commit is contained in:
James Bardin 2019-01-08 16:54:12 -05:00 committed by GitHub
commit f7913bb168
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
11 changed files with 585 additions and 39 deletions

View File

@ -28,6 +28,19 @@ func testResourceNestedSet() *schema.Resource {
Optional: true,
ForceNew: true,
},
"type_list": {
Type: schema.TypeList,
Optional: true,
Elem: &schema.Resource{
Schema: map[string]*schema.Schema{
"value": {
Type: schema.TypeString,
ForceNew: true,
Optional: true,
},
},
},
},
"single": {
Type: schema.TypeSet,
Optional: true,
@ -98,7 +111,6 @@ func testResourceNestedSet() *schema.Resource {
Type: schema.TypeString,
Required: true,
},
"list": {
Type: schema.TypeList,
Optional: true,
@ -106,6 +118,18 @@ func testResourceNestedSet() *schema.Resource {
Type: schema.TypeString,
},
},
"list_block": {
Type: schema.TypeList,
Optional: true,
Elem: &schema.Resource{
Schema: map[string]*schema.Schema{
"unused": {
Type: schema.TypeString,
Optional: true,
},
},
},
},
},
},
},

View File

@ -3,6 +3,7 @@ package test
import (
"errors"
"fmt"
"regexp"
"strings"
"testing"
@ -56,6 +57,118 @@ resource "test_resource_nested_set" "foo" {
})
}
// the empty type_list must be passed to the provider with 1 nil element
func TestResourceNestedSet_emptyBlock(t *testing.T) {
checkFunc := func(s *terraform.State) error {
root := s.ModuleByPath(addrs.RootModuleInstance)
res := root.Resources["test_resource_nested_set.foo"]
for k, v := range res.Primary.Attributes {
if strings.HasPrefix(k, "type_list") && v != "1" {
return fmt.Errorf("unexpected set value: %s:%s", k, v)
}
}
return nil
}
resource.UnitTest(t, resource.TestCase{
Providers: testAccProviders,
CheckDestroy: testAccCheckResourceDestroy,
Steps: []resource.TestStep{
resource.TestStep{
Config: strings.TrimSpace(`
resource "test_resource_nested_set" "foo" {
type_list {
}
}
`),
Check: checkFunc,
},
},
})
}
func TestResourceNestedSet_emptyNestedListBlock(t *testing.T) {
checkFunc := func(s *terraform.State) error {
root := s.ModuleByPath(addrs.RootModuleInstance)
res := root.Resources["test_resource_nested_set.foo"]
found := false
for k, v := range res.Primary.Attributes {
if !regexp.MustCompile(`^with_list\.\d+\.list_block\.`).MatchString(k) {
continue
}
found = true
if strings.HasSuffix(k, ".#") {
if v != "1" {
return fmt.Errorf("expected block with no objects: got %s:%s", k, v)
}
continue
}
// there should be no other attribute values for an empty block
return fmt.Errorf("unexpected attribute: %s:%s", k, v)
}
if !found {
return fmt.Errorf("with_list.X.list_block not found")
}
return nil
}
resource.UnitTest(t, resource.TestCase{
Providers: testAccProviders,
CheckDestroy: testAccCheckResourceDestroy,
Steps: []resource.TestStep{
resource.TestStep{
Config: strings.TrimSpace(`
resource "test_resource_nested_set" "foo" {
with_list {
required = "ok"
list_block {
}
}
}
`),
Check: checkFunc,
},
},
})
}
func TestResourceNestedSet_emptyNestedList(t *testing.T) {
checkFunc := func(s *terraform.State) error {
root := s.ModuleByPath(addrs.RootModuleInstance)
res := root.Resources["test_resource_nested_set.foo"]
found := false
for k, v := range res.Primary.Attributes {
if regexp.MustCompile(`^with_list\.\d+\.list\.#$`).MatchString(k) {
found = true
if v != "0" {
return fmt.Errorf("expected empty list: %s, got %s", k, v)
}
break
}
}
if !found {
return fmt.Errorf("with_list.X.nested_list not found")
}
return nil
}
resource.UnitTest(t, resource.TestCase{
Providers: testAccProviders,
CheckDestroy: testAccCheckResourceDestroy,
Steps: []resource.TestStep{
resource.TestStep{
Config: strings.TrimSpace(`
resource "test_resource_nested_set" "foo" {
with_list {
required = "ok"
list = []
}
}
`),
Check: checkFunc,
},
},
})
}
func TestResourceNestedSet_addRemove(t *testing.T) {
var id string
checkFunc := func(s *terraform.State) error {
@ -339,3 +452,50 @@ resource "test_resource_nested_set" "foo" {
},
})
}
// This is the same as forceNewEmptyString, but we start with the empty value,
// instead of changing it.
func TestResourceNestedSet_nestedSetEmptyString(t *testing.T) {
checkFunc := func(s *terraform.State) error {
return nil
}
resource.UnitTest(t, resource.TestCase{
Providers: testAccProviders,
CheckDestroy: testAccCheckResourceDestroy,
Steps: []resource.TestStep{
resource.TestStep{
Config: strings.TrimSpace(`
resource "test_resource_nested_set" "foo" {
multi {
set {
required = ""
}
}
}
`),
Check: checkFunc,
},
},
})
}
func TestResourceNestedSet_emptySet(t *testing.T) {
checkFunc := func(s *terraform.State) error {
return nil
}
resource.UnitTest(t, resource.TestCase{
Providers: testAccProviders,
CheckDestroy: testAccCheckResourceDestroy,
Steps: []resource.TestStep{
resource.TestStep{
Config: strings.TrimSpace(`
resource "test_resource_nested_set" "foo" {
multi {
}
}
`),
Check: checkFunc,
},
},
})
}

View File

@ -142,6 +142,7 @@ resource "test_resource_nested" "foo" {
"nested.0.nested_again.0.string": "a",
"nested.1.string": "",
"nested.1.optional": "false",
"nested.1.nested_again.#": "0",
}
delete(got, "id") // it's random, so not useful for testing

View File

@ -521,3 +521,23 @@ resource "test_resource" "two" {
},
})
}
func TestResource_emptyMapValue(t *testing.T) {
resource.UnitTest(t, resource.TestCase{
Providers: testAccProviders,
CheckDestroy: testAccCheckResourceDestroy,
Steps: []resource.TestStep{
resource.TestStep{
Config: strings.TrimSpace(`
resource "test_resource" "foo" {
required = "ok"
required_map = {
a = "a"
b = ""
}
}
`),
},
},
})
}

View File

@ -347,10 +347,8 @@ func hcl2ValueFromFlatmapSet(m map[string]string, prefix string, ty cty.Type) (c
return cty.UnknownVal(ty), nil
}
// 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 strCount, exists := m[prefix+"#"]; !exists {
strCount, exists := m[prefix+"#"]
if !exists {
return cty.NullVal(ty), nil
} else if strCount == UnknownVariableValue {
return cty.UnknownVal(ty), nil
@ -394,7 +392,31 @@ func hcl2ValueFromFlatmapSet(m map[string]string, prefix string, ty cty.Type) (c
vals = append(vals, val)
}
if len(vals) == 0 {
if len(vals) == 0 && strCount == "1" {
// An empty set wouldn't be represented in the flatmap, so this must be
// a single empty object since the count is actually 1.
// Add an appropriately typed null value to the set.
var val cty.Value
switch {
case ety.IsMapType():
val = cty.MapValEmpty(ety)
case ety.IsListType():
val = cty.ListValEmpty(ety)
case ety.IsSetType():
val = cty.SetValEmpty(ety)
case ety.IsObjectType():
// TODO: cty.ObjectValEmpty
objectMap := map[string]cty.Value{}
for attr, ty := range ety.AttributeTypes() {
objectMap[attr] = cty.NullVal(ty)
}
val = cty.ObjectVal(objectMap)
default:
val = cty.NullVal(ety)
}
vals = append(vals, val)
} else if len(vals) == 0 {
return cty.SetValEmpty(ety), nil
}

View File

@ -679,10 +679,52 @@ func TestHCL2ValueFromFlatmap(t *testing.T) {
}),
}),
},
{
Flatmap: map[string]string{
"foo.#": "1",
},
Type: cty.Object(map[string]cty.Type{
"foo": cty.Set(cty.Object(map[string]cty.Type{
"bar": cty.String,
})),
}),
Want: cty.ObjectVal(map[string]cty.Value{
"foo": cty.SetVal([]cty.Value{
cty.ObjectVal(map[string]cty.Value{
"bar": cty.NullVal(cty.String),
}),
}),
}),
},
{
Flatmap: map[string]string{
"multi.#": "1",
"multi.2.set.#": "1",
"multi.2.set.3.required": "val",
},
Type: cty.Object(map[string]cty.Type{
"multi": cty.Set(cty.Object(map[string]cty.Type{
"set": cty.Set(cty.Object(map[string]cty.Type{
"required": cty.String,
})),
})),
}),
Want: cty.ObjectVal(map[string]cty.Value{
"multi": cty.SetVal([]cty.Value{
cty.ObjectVal(map[string]cty.Value{
"set": cty.SetVal([]cty.Value{
cty.ObjectVal(map[string]cty.Value{
"required": cty.StringVal("val"),
}),
}),
}),
}),
}),
},
}
for _, test := range tests {
t.Run(fmt.Sprintf("%#v as %#v", test.Flatmap, test.Type), func(t *testing.T) {
for i, test := range tests {
t.Run(fmt.Sprintf("%d %#v as %#v", i, test.Flatmap, test.Type), func(t *testing.T) {
got, err := HCL2ValueFromFlatmap(test.Flatmap, test.Type)
if test.WantErr != "" {

View File

@ -80,11 +80,6 @@ func ConfigValueFromHCL2Block(v cty.Value, schema *configschema.Block) map[strin
case configschema.NestingList, configschema.NestingSet:
l := bv.LengthInt()
if l == 0 {
// skip empty collections to better mimic how HCL1 would behave
continue
}
elems := make([]interface{}, 0, l)
for it := bv.ElementIterator(); it.Next(); {
_, ev := it.Element()
@ -97,11 +92,6 @@ func ConfigValueFromHCL2Block(v cty.Value, schema *configschema.Block) map[strin
ret[name] = elems
case configschema.NestingMap:
if bv.LengthInt() == 0 {
// skip empty collections to better mimic how HCL1 would behave
continue
}
elems := make(map[string]interface{})
for it := bv.ElementIterator(); it.Next(); {
ek, ev := it.Element()

View File

@ -151,7 +151,7 @@ func TestConfigValueFromHCL2Block(t *testing.T) {
},
{
cty.ObjectVal(map[string]cty.Value{
"address": cty.ListValEmpty(cty.EmptyObject), // should be omitted altogether in result
"address": cty.ListValEmpty(cty.EmptyObject),
}),
&configschema.Block{
BlockTypes: map[string]*configschema.NestedBlock{
@ -161,7 +161,9 @@ func TestConfigValueFromHCL2Block(t *testing.T) {
},
},
},
map[string]interface{}{},
map[string]interface{}{
"address": []interface{}{},
},
},
{
cty.ObjectVal(map[string]cty.Value{
@ -193,7 +195,9 @@ func TestConfigValueFromHCL2Block(t *testing.T) {
},
},
},
map[string]interface{}{},
map[string]interface{}{
"address": []interface{}{},
},
},
{
cty.ObjectVal(map[string]cty.Value{
@ -225,7 +229,9 @@ func TestConfigValueFromHCL2Block(t *testing.T) {
},
},
},
map[string]interface{}{},
map[string]interface{}{
"address": map[string]interface{}{},
},
},
{
cty.NullVal(cty.EmptyObject),
@ -234,8 +240,8 @@ func TestConfigValueFromHCL2Block(t *testing.T) {
},
}
for _, test := range tests {
t.Run(fmt.Sprintf("%#v", test.Input), func(t *testing.T) {
for i, test := range tests {
t.Run(fmt.Sprintf("%d-%#v", i, test.Input), func(t *testing.T) {
got := ConfigValueFromHCL2Block(test.Input, test.Schema)
if !reflect.DeepEqual(got, test.Want) {
t.Errorf("wrong result\ninput: %#v\ngot: %#v\nwant: %#v", test.Input, got, test.Want)

View File

@ -438,8 +438,6 @@ func (s *GRPCProviderServer) ReadResource(_ context.Context, req *proto.ReadReso
// helper/schema should always copy the ID over, but do it again just to be safe
newInstanceState.Attributes["id"] = newInstanceState.ID
newInstanceState.Attributes = normalizeFlatmapContainers(instanceState.Attributes, newInstanceState.Attributes)
newStateVal, err := hcl2shim.HCL2ValueFromFlatmap(newInstanceState.Attributes, block.ImpliedType())
if err != nil {
resp.Diagnostics = convert.AppendProtoDiag(resp.Diagnostics, err)
@ -488,7 +486,6 @@ func (s *GRPCProviderServer) PlanResourceChange(_ context.Context, req *proto.Pl
resp.Diagnostics = convert.AppendProtoDiag(resp.Diagnostics, err)
return resp, nil
}
priorPrivate := make(map[string]interface{})
if len(req.PriorPrivate) > 0 {
if err := json.Unmarshal(req.PriorPrivate, &priorPrivate); err != nil {
@ -536,7 +533,7 @@ func (s *GRPCProviderServer) PlanResourceChange(_ context.Context, req *proto.Pl
// now we need to apply the diff to the prior state, so get the planned state
plannedAttrs, err := diff.Apply(priorState.Attributes, block)
plannedAttrs = normalizeFlatmapContainers(priorState.Attributes, plannedAttrs)
plannedAttrs = normalizeFlatmapContainers(priorState.Attributes, plannedAttrs, false)
plannedStateVal, err := hcl2shim.HCL2ValueFromFlatmap(plannedAttrs, block.ImpliedType())
if err != nil {
@ -544,6 +541,8 @@ func (s *GRPCProviderServer) PlanResourceChange(_ context.Context, req *proto.Pl
return resp, nil
}
plannedStateVal = copyMissingValues(plannedStateVal, proposedNewStateVal)
plannedStateVal, err = block.CoerceValue(plannedStateVal)
if err != nil {
resp.Diagnostics = convert.AppendProtoDiag(resp.Diagnostics, err)
@ -683,6 +682,13 @@ func (s *GRPCProviderServer) ApplyResourceChange(_ context.Context, req *proto.A
}
}
// strip out non-diffs
for k, v := range diff.Attributes {
if v.New == v.Old && !v.NewComputed && !v.NewRemoved {
delete(diff.Attributes, k)
}
}
// add NewExtra Fields that may have been stored in the private data
if newExtra := private[newExtraKey]; newExtra != nil {
for k, v := range newExtra.(map[string]interface{}) {
@ -718,7 +724,7 @@ func (s *GRPCProviderServer) ApplyResourceChange(_ context.Context, req *proto.A
// here we use the planned state to check for unknown/zero containers values
// when normalizing the flatmap.
plannedState := hcl2shim.FlatmapValueFromHCL2(plannedStateVal)
newInstanceState.Attributes = normalizeFlatmapContainers(plannedState, newInstanceState.Attributes)
newInstanceState.Attributes = normalizeFlatmapContainers(plannedState, newInstanceState.Attributes, true)
}
newStateVal := cty.NullVal(block.ImpliedType())
@ -733,6 +739,8 @@ func (s *GRPCProviderServer) ApplyResourceChange(_ context.Context, req *proto.A
}
}
newStateVal = copyMissingValues(newStateVal, plannedStateVal)
newStateVal = copyTimeoutValues(newStateVal, plannedStateVal)
newStateMP, err := msgpack.Marshal(newStateVal, block.ImpliedType())
@ -898,24 +906,62 @@ func pathToAttributePath(path cty.Path) *proto.AttributePath {
// set of flatmapped attributes. The prior value is used to determine if there
// could be zero-length flatmap containers which we need to preserve. This
// allows a provider to set an empty computed container in the state without
// creating perpetual diff.
func normalizeFlatmapContainers(prior map[string]string, attrs map[string]string) map[string]string {
keyRx := regexp.MustCompile(`.\.[%#]$`)
// creating perpetual diff. This can differ slightly between plan and apply, so
// the apply flag is passed when called from ApplyResourceChange.
func normalizeFlatmapContainers(prior map[string]string, attrs map[string]string, apply bool) map[string]string {
isCount := regexp.MustCompile(`.\.[%#]$`).MatchString
// while we can't determine if the value was actually computed here, we will
// While we can't determine if the value was actually computed here, we will
// trust that our shims stored and retrieved a zero-value container
// correctly.
zeros := map[string]bool{}
// Empty blocks have a count of 1 with no other attributes. Just record all
// "1"s here to override 0-length blocks when setting the count below.
ones := map[string]bool{}
for k, v := range prior {
if keyRx.MatchString(k) && (v == "0" || v == hcl2shim.UnknownVariableValue) {
if isCount(k) && (v == "0" || v == hcl2shim.UnknownVariableValue) {
zeros[k] = true
}
// fixup any 1->0 conversions that happened during Apply
if apply && isCount(k) && v == "1" && attrs[k] == "0" {
attrs[k] = "1"
}
}
for k, v := range attrs {
// store any "1" values, since if the length was 1 and there are no
// items, it was probably an empty set block. Hopefully checking for a 1
// value with no items is sufficient, without cross-referencing the
// schema.
if isCount(k) && v == "1" {
ones[k] = true
}
}
// The "ones" were stored to look for sets with an empty value, so we need
// to verify that we only store ones with no attrs.
expectedEmptySets := map[string]bool{}
for one := range ones {
prefix := one[:len(one)-1]
found := 0
for k := range attrs {
// since this can be recursive, we check that the attrs isn't also a #.
if strings.HasPrefix(k, prefix) && !isCount(k) {
found++
}
}
if found == 0 {
expectedEmptySets[one] = true
}
}
// find container keys
var keys []string
for k, v := range attrs {
if !keyRx.MatchString(k) {
if !isCount(k) {
continue
}
@ -967,6 +1013,9 @@ func normalizeFlatmapContainers(prior map[string]string, attrs map[string]string
// must have set the computed value to an empty container, and we
// need to leave it in the flatmap.
attrs[k] = "0"
case len(indexes) == 0 && ones[k]:
// We need to retain any empty blocks that had a 1 count with no attributes.
attrs[k] = "1"
case len(indexes) > 0:
attrs[k] = strconv.Itoa(len(indexes))
default:
@ -974,6 +1023,12 @@ func normalizeFlatmapContainers(prior map[string]string, attrs map[string]string
}
}
for k := range expectedEmptySets {
if _, ok := attrs[k]; !ok {
attrs[k] = "1"
}
}
return attrs
}
@ -1043,3 +1098,86 @@ func stripSchema(s *schema.Schema) *schema.Schema {
return newSchema
}
// Zero values and empty containers may be lost during apply. Copy zero values
// and empty containers from src to dst when they are missing in dst.
// This takes a little more liberty with set types, since we can't correlate
// modified set values. In the case of sets, if the src set was wholly known we
// assume the value was correctly applied and copy that entirely to the new
// value.
func copyMissingValues(dst, src cty.Value) cty.Value {
ty := dst.Type()
// In this case the provider set an empty string which was lost in
// conversion. Since src is unknown, there must have been a corresponding
// value set.
if ty == cty.String && dst.IsNull() && !src.IsKnown() {
return cty.StringVal("")
}
if src.IsNull() || !src.IsKnown() || !dst.IsKnown() {
return dst
}
switch {
case ty.IsMapType(), ty.IsObjectType():
var dstMap map[string]cty.Value
if dst.IsNull() {
dstMap = map[string]cty.Value{}
} else {
dstMap = dst.AsValueMap()
}
ei := src.ElementIterator()
for ei.Next() {
k, v := ei.Element()
key := k.AsString()
dstVal := dstMap[key]
if dstVal == cty.NilVal {
dstVal = cty.NullVal(ty.ElementType())
}
dstMap[key] = copyMissingValues(dstVal, v)
}
// you can't call MapVal/ObjectVal with empty maps, but nothing was
// copied in anyway. If the dst is nil, and the src is known, assume the
// src is correct.
if len(dstMap) == 0 {
if dst.IsNull() && src.IsWhollyKnown() {
return src
}
return dst
}
if ty.IsMapType() {
return cty.MapVal(dstMap)
}
return cty.ObjectVal(dstMap)
case ty.IsSetType():
// If the original was wholly known, then we expect that is what the
// provider applied. The apply process loses too much information to
// reliably re-create the set.
if src.IsWhollyKnown() {
return src
}
case ty.IsListType(), ty.IsTupleType():
// If the dst is nil, and the src is known, then we lost an empty value
// so take the original. This doesn't attempt to descend into the list
// values, since missing empty values may prevent us from correlating
// the correct src and dst indexes.
if dst.IsNull() && src.IsWhollyKnown() {
return src
}
case ty.IsPrimitiveType():
if dst.IsNull() && src.IsWhollyKnown() {
return src
}
}
return dst
}

View File

@ -673,7 +673,7 @@ func TestNormalizeFlatmapContainers(t *testing.T) {
}{
{
attrs: map[string]string{"id": "1", "multi.2.set.#": "1", "multi.1.set.#": "0", "single.#": "0"},
expect: map[string]string{"id": "1"},
expect: map[string]string{"id": "1", "multi.2.set.#": "1"},
},
{
attrs: map[string]string{"id": "1", "multi.2.set.#": "2", "multi.2.set.1.foo": "bar", "multi.1.set.#": "0", "single.#": "0"},
@ -681,11 +681,15 @@ func TestNormalizeFlatmapContainers(t *testing.T) {
},
{
attrs: map[string]string{"id": "78629a0f5f3f164f", "multi.#": "1"},
expect: map[string]string{"id": "78629a0f5f3f164f", "multi.#": "1"},
},
{
attrs: map[string]string{"id": "78629a0f5f3f164f", "multi.#": "0"},
expect: map[string]string{"id": "78629a0f5f3f164f"},
},
{
attrs: map[string]string{"multi.529860700.set.#": "1", "multi.#": "1", "id": "78629a0f5f3f164f"},
expect: map[string]string{"id": "78629a0f5f3f164f"},
attrs: map[string]string{"multi.529860700.set.#": "0", "multi.#": "1", "id": "78629a0f5f3f164f"},
expect: map[string]string{"id": "78629a0f5f3f164f", "multi.#": "1"},
},
{
attrs: map[string]string{"set.2.required": "bar", "set.2.list.#": "1", "set.2.list.0": "x", "set.1.list.#": "0", "set.#": "2"},
@ -707,10 +711,143 @@ func TestNormalizeFlatmapContainers(t *testing.T) {
},
} {
t.Run(strconv.Itoa(i), func(t *testing.T) {
got := normalizeFlatmapContainers(tc.prior, tc.attrs)
got := normalizeFlatmapContainers(tc.prior, tc.attrs, false)
if !reflect.DeepEqual(tc.expect, got) {
t.Fatalf("expected:\n%#v\ngot:\n%#v\n", tc.expect, got)
}
})
}
}
func TestCopyMissingValues(t *testing.T) {
for i, tc := range []struct {
Src, Dst, Expect cty.Value
}{
{
// The known set value is copied over the null set value
Src: cty.ObjectVal(map[string]cty.Value{
"set": cty.SetVal([]cty.Value{
cty.ObjectVal(map[string]cty.Value{
"foo": cty.NullVal(cty.String),
}),
}),
}),
Dst: cty.ObjectVal(map[string]cty.Value{
"set": cty.NullVal(cty.Set(cty.Object(map[string]cty.Type{
"foo": cty.String,
}))),
}),
Expect: cty.ObjectVal(map[string]cty.Value{
"set": cty.SetVal([]cty.Value{
cty.ObjectVal(map[string]cty.Value{
"foo": cty.NullVal(cty.String),
}),
}),
}),
},
{
// The empty map is copied over the null map
Src: cty.ObjectVal(map[string]cty.Value{
"map": cty.MapValEmpty(cty.String),
}),
Dst: cty.ObjectVal(map[string]cty.Value{
"map": cty.NullVal(cty.Map(cty.String)),
}),
Expect: cty.ObjectVal(map[string]cty.Value{
"map": cty.MapValEmpty(cty.String),
}),
},
{
// A zerp value primitive is copied over a null primitive
Src: cty.ObjectVal(map[string]cty.Value{
"string": cty.StringVal(""),
}),
Dst: cty.ObjectVal(map[string]cty.Value{
"string": cty.NullVal(cty.String),
}),
Expect: cty.ObjectVal(map[string]cty.Value{
"string": cty.StringVal(""),
}),
},
{
// The null map is retained, because the src was unknown
Src: cty.ObjectVal(map[string]cty.Value{
"map": cty.UnknownVal(cty.Map(cty.String)),
}),
Dst: cty.ObjectVal(map[string]cty.Value{
"map": cty.NullVal(cty.Map(cty.String)),
}),
Expect: cty.ObjectVal(map[string]cty.Value{
"map": cty.NullVal(cty.Map(cty.String)),
}),
},
{
// the nul set is retained, because the src set contains an unknown value
Src: cty.ObjectVal(map[string]cty.Value{
"set": cty.SetVal([]cty.Value{
cty.ObjectVal(map[string]cty.Value{
"foo": cty.UnknownVal(cty.String),
}),
}),
}),
Dst: cty.ObjectVal(map[string]cty.Value{
"set": cty.NullVal(cty.Set(cty.Object(map[string]cty.Type{
"foo": cty.String,
}))),
}),
Expect: cty.ObjectVal(map[string]cty.Value{
"set": cty.NullVal(cty.Set(cty.Object(map[string]cty.Type{
"foo": cty.String,
}))),
}),
},
{
// Retain the zero value within the map
Src: cty.ObjectVal(map[string]cty.Value{
"map": cty.MapVal(map[string]cty.Value{
"a": cty.StringVal("a"),
"b": cty.StringVal(""),
}),
}),
Dst: cty.ObjectVal(map[string]cty.Value{
"map": cty.MapVal(map[string]cty.Value{
"a": cty.StringVal("a"),
}),
}),
Expect: cty.ObjectVal(map[string]cty.Value{
"map": cty.MapVal(map[string]cty.Value{
"a": cty.StringVal("a"),
"b": cty.StringVal(""),
}),
}),
},
{
// Recover the lost unknown key, assuming it was set to an empty
// string and lost.
Src: cty.ObjectVal(map[string]cty.Value{
"map": cty.MapVal(map[string]cty.Value{
"a": cty.StringVal("a"),
"b": cty.UnknownVal(cty.String),
}),
}),
Dst: cty.ObjectVal(map[string]cty.Value{
"map": cty.MapVal(map[string]cty.Value{
"a": cty.StringVal("a"),
}),
}),
Expect: cty.ObjectVal(map[string]cty.Value{
"map": cty.MapVal(map[string]cty.Value{
"a": cty.StringVal("a"),
"b": cty.StringVal(""),
}),
}),
},
} {
t.Run(fmt.Sprintf("%d", i), func(t *testing.T) {
got := copyMissingValues(tc.Dst, tc.Src)
if !got.RawEquals(tc.Expect) {
t.Fatalf("\nexpected: %#v\ngot: %#v\n", tc.Expect, got)
}
})
}
}

View File

@ -420,11 +420,17 @@ func (p *GRPCProvider) ApplyResourceChange(r providers.ApplyResourceChangeReques
resp.Diagnostics = resp.Diagnostics.Append(err)
return resp
}
configMP, err := msgpack.Marshal(r.Config, resSchema.Block.ImpliedType())
if err != nil {
resp.Diagnostics = resp.Diagnostics.Append(err)
return resp
}
protoReq := &proto.ApplyResourceChange_Request{
TypeName: r.TypeName,
PriorState: &proto.DynamicValue{Msgpack: priorMP},
PlannedState: &proto.DynamicValue{Msgpack: plannedMP},
Config: &proto.DynamicValue{Msgpack: configMP},
PlannedPrivate: r.PlannedPrivate,
}