Skip to content

Commit

Permalink
helper/schema: Normalize bools to "true"/"false" in diffs
Browse files Browse the repository at this point in the history
For a long time now, the diff logic has relied on the behavior of
`mapstructure.WeakDecode` to determine how various primitives are
converted into strings.  The `schema.DiffString` function is used for
all primitive field types: TypeBool, TypeInt, TypeFloat, and TypeString.

The `mapstructure` library's string representation of booleans is "0"
and "1", which differs from `strconv.FormatBool`'s "false" and "true"
(which is used in writing out boolean fields to the state).

Because of this difference, diffs have long had the potential for
cosmetically odd but semantically neutral output like:

    "true" => "1"
    "false" => "0"

So long as `mapstructure.Decode` or `strconv.ParseBool` are used to
interpret these strings, there's no functional problem.

We had our first clear functional problem with hashicorp#6005 and friends, where
users noticed diffs like the above showing up unexpectedly and causing
troubles when `ignore_changes` was in play.

This particular bug occurs down in Terraform core's EvalIgnoreChanges.
There, the diff is modified to account for ignored attributes, and
special logic attempts to handle properly the situation where the
ignored attribute was going to trigger a resource replacement. That
logic relies on the string representations of the Old and New fields in
the diff to be the same so that it filters properly.

So therefore, we now get a bug when a diff includes `Old: "0", New:
"false"` since the strings do not match, and `ignore_changes` is not
properly handled.

Here, we introduce `TypeBool`-specific normalizing into `finalizeDiff`.
I spiked out a full `diffBool` function, but figuring out which pieces
of `diffString` to duplicate there got hairy. This seemed like a simpler
and more direct solution.

Fixes hashicorp#6005 (and potentially others!)
  • Loading branch information
phinze authored and cristicalin committed May 24, 2016
1 parent 8023a0c commit 1f0ecff
Show file tree
Hide file tree
Showing 4 changed files with 110 additions and 1 deletion.
4 changes: 4 additions & 0 deletions builtin/providers/test/resource.go
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,10 @@ func testResource() *schema.Resource {
Type: schema.TypeString,
Optional: true,
},
"optional_bool": &schema.Schema{
Type: schema.TypeBool,
Optional: true,
},
"optional_force_new": &schema.Schema{
Type: schema.TypeString,
Optional: true,
Expand Down
41 changes: 41 additions & 0 deletions builtin/providers/test/resource_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -124,6 +124,47 @@ resource "test_resource" "foo" {
})
}

// Covers specific scenario in #6005, handled by normalizing boolean strings in
// helper/schema
func TestResource_ignoreChangesForceNewBoolean(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 = "yep"
optional_force_new = "one"
optional_bool = true
lifecycle {
ignore_changes = ["optional_force_new"]
}
}
`),
Check: func(s *terraform.State) error {
return nil
},
},
resource.TestStep{
Config: strings.TrimSpace(`
resource "test_resource" "foo" {
required = "yep"
optional_force_new = "two"
optional_bool = true
lifecycle {
ignore_changes = ["optional_force_new"]
}
}
`),
Check: func(s *terraform.State) error {
return nil
},
},
},
})
}

func TestResource_ignoreChangesMap(t *testing.T) {
resource.UnitTest(t, resource.TestCase{
Providers: testAccProviders,
Expand Down
14 changes: 14 additions & 0 deletions helper/schema/schema.go
Original file line number Diff line number Diff line change
Expand Up @@ -243,6 +243,20 @@ func (s *Schema) finalizeDiff(
return d
}

if s.Type == TypeBool {
normalizeBoolString := func(s string) string {
switch s {
case "0":
return "false"
case "1":
return "true"
}
return s
}
d.Old = normalizeBoolString(d.Old)
d.New = normalizeBoolString(d.New)
}

if d.NewRemoved {
return d
}
Expand Down
52 changes: 51 additions & 1 deletion helper/schema/schema_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -491,7 +491,7 @@ func TestSchemaMap_Diff(t *testing.T) {
Attributes: map[string]*terraform.ResourceAttrDiff{
"port": &terraform.ResourceAttrDiff{
Old: "",
New: "0",
New: "false",
RequiresNew: true,
},
},
Expand Down Expand Up @@ -2344,6 +2344,56 @@ func TestSchemaMap_Diff(t *testing.T) {

Err: false,
},

"Bools can be set with 0/1 in config, still get true/false": {
Schema: map[string]*Schema{
"one": &Schema{
Type: TypeBool,
Optional: true,
},
"two": &Schema{
Type: TypeBool,
Optional: true,
},
"three": &Schema{
Type: TypeBool,
Optional: true,
},
},

State: &terraform.InstanceState{
Attributes: map[string]string{
"one": "false",
"two": "true",
"three": "true",
},
},

Config: map[string]interface{}{
"one": "1",
"two": "0",
},

Diff: &terraform.InstanceDiff{
Attributes: map[string]*terraform.ResourceAttrDiff{
"one": &terraform.ResourceAttrDiff{
Old: "false",
New: "true",
},
"two": &terraform.ResourceAttrDiff{
Old: "true",
New: "false",
},
"three": &terraform.ResourceAttrDiff{
Old: "true",
New: "false",
NewRemoved: true,
},
},
},

Err: false,
},
}

for tn, tc := range cases {
Expand Down

0 comments on commit 1f0ecff

Please sign in to comment.