prevent an empty string from being lost

The helper/schema diff process loses empty strings, causing them to show
up as unset (null) during apply. Besides failing to show as set by
GetOk, the absence of the value also triggers the schema to insert a
default value again during apply.

It would also be be preferable if the defaults weren't re-evaluated
again during ApplyResourceChange, but that would require a more invasive
patch to the field readers, and ensuring the empty string is stored in
the plan should block the default.
This commit is contained in:
James Bardin 2019-06-19 17:14:14 -04:00
parent 1bba574fe9
commit 4dcda04d96
3 changed files with 52 additions and 2 deletions

View File

@ -174,3 +174,22 @@ resource "test_resource_defaults" "foo" {
}, },
}) })
} }
func TestDefaults_emptyString(t *testing.T) {
config := `
resource "test_resource_defaults" "test" {
default_string = ""
}
`
resource.UnitTest(t, resource.TestCase{
Providers: testAccProviders,
Steps: []resource.TestStep{
{
Config: config,
Check: resource.ComposeTestCheckFunc(
resource.TestCheckResourceAttr("test_resource_defaults.test", "default_string", ""),
),
},
},
})
}

View File

@ -1201,6 +1201,8 @@ func normalizeNullValues(dst, src cty.Value, apply bool) cty.Value {
} }
} }
// check the invariants that we need below, to ensure we are working with
// non-null and known values.
if src.IsNull() || !src.IsKnown() || !dst.IsKnown() { if src.IsNull() || !src.IsKnown() || !dst.IsKnown() {
return dst return dst
} }
@ -1319,8 +1321,12 @@ func normalizeNullValues(dst, src cty.Value, apply bool) cty.Value {
return cty.ListVal(dsts) return cty.ListVal(dsts)
} }
case ty.IsPrimitiveType(): case ty == cty.String:
if dst.IsNull() && src.IsWhollyKnown() && apply { // The legacy SDK should not be able to remove a value during plan or
// apply, however we are only going to overwrite this if the source was
// an empty string, since that is what is often equated with unset and
// lost in the diff process.
if dst.IsNull() && src.AsString() == "" {
return src return src
} }
} }

View File

@ -984,6 +984,18 @@ func TestNormalizeNullValues(t *testing.T) {
}, },
{ {
// Plan primitives are kept // Plan primitives are kept
Src: cty.ObjectVal(map[string]cty.Value{
"string": cty.NumberIntVal(0),
}),
Dst: cty.ObjectVal(map[string]cty.Value{
"string": cty.NullVal(cty.Number),
}),
Expect: cty.ObjectVal(map[string]cty.Value{
"string": cty.NullVal(cty.Number),
}),
},
{
// Neither plan nor apply should remove empty strings
Src: cty.ObjectVal(map[string]cty.Value{ Src: cty.ObjectVal(map[string]cty.Value{
"string": cty.StringVal(""), "string": cty.StringVal(""),
}), }),
@ -991,8 +1003,21 @@ func TestNormalizeNullValues(t *testing.T) {
"string": cty.NullVal(cty.String), "string": cty.NullVal(cty.String),
}), }),
Expect: cty.ObjectVal(map[string]cty.Value{ Expect: cty.ObjectVal(map[string]cty.Value{
"string": cty.StringVal(""),
}),
},
{
// Neither plan nor apply should remove empty strings
Src: cty.ObjectVal(map[string]cty.Value{
"string": cty.StringVal(""),
}),
Dst: cty.ObjectVal(map[string]cty.Value{
"string": cty.NullVal(cty.String), "string": cty.NullVal(cty.String),
}), }),
Expect: cty.ObjectVal(map[string]cty.Value{
"string": cty.StringVal(""),
}),
Apply: true,
}, },
{ {
// The null map is retained, because the src was unknown // The null map is retained, because the src was unknown