From 0b981fa64184c940097bddf32b85fd12916de27e Mon Sep 17 00:00:00 2001 From: Radek Simko Date: Tue, 11 Dec 2018 11:55:59 +0000 Subject: [PATCH] command/format: Fix rendering of force-new updates --- command/format/diff.go | 2 +- command/format/diff_test.go | 452 +++++++++++++++++++++++++++++++++++- 2 files changed, 452 insertions(+), 2 deletions(-) diff --git a/command/format/diff.go b/command/format/diff.go index 37127e2d0..5b8847005 100644 --- a/command/format/diff.go +++ b/command/format/diff.go @@ -844,7 +844,7 @@ func (p *blockBodyDiffPrinter) writeActionSymbol(action plans.Action) { } func (p *blockBodyDiffPrinter) pathForcesNewResource(path cty.Path) bool { - if p.action.IsReplace() { + if !p.action.IsReplace() { // "requiredReplace" only applies when the instance is being replaced return false } diff --git a/command/format/diff_test.go b/command/format/diff_test.go index 2dc002a8c..0e17fcdd4 100644 --- a/command/format/diff_test.go +++ b/command/format/diff_test.go @@ -97,7 +97,7 @@ func TestResourceChange_primitiveTypes(t *testing.T) { }), ExpectedOutput: ` # test_instance.example must be replaced -/+ resource "test_instance" "example" { - ~ ami = "ami-BEFORE" -> "ami-AFTER" + ~ ami = "ami-BEFORE" -> "ami-AFTER" # forces replacement id = "i-02ae66f368e8518a9" } `, @@ -161,6 +161,39 @@ new line } `, }, + "force-new update of multi-line string field": { + Action: plans.DeleteThenCreate, + Mode: addrs.ManagedResourceMode, + Before: cty.ObjectVal(map[string]cty.Value{ + "id": cty.StringVal("i-02ae66f368e8518a9"), + "more_lines": cty.StringVal(`original +`), + }), + After: cty.ObjectVal(map[string]cty.Value{ + "id": cty.UnknownVal(cty.String), + "more_lines": cty.StringVal(`original +new line +`), + }), + Schema: &configschema.Block{ + Attributes: map[string]*configschema.Attribute{ + "id": {Type: cty.String, Optional: true, Computed: true}, + "more_lines": {Type: cty.String, Optional: true}, + }, + }, + RequiredReplace: cty.NewPathSet(cty.Path{ + cty.GetAttrStep{Name: "more_lines"}, + }), + ExpectedOutput: ` # test_instance.example must be replaced +-/+ resource "test_instance" "example" { + ~ id = "i-02ae66f368e8518a9" -> (known after apply) + ~ more_lines = <<~EOT # forces replacement + original + + new line + EOT + } +`, + }, // Sensitive @@ -262,6 +295,40 @@ func TestResourceChange_JSON(t *testing.T) { } ) } +`, + }, + "force-new update": { + Action: plans.DeleteThenCreate, + Mode: addrs.ManagedResourceMode, + Before: cty.ObjectVal(map[string]cty.Value{ + "id": cty.StringVal("i-02ae66f368e8518a9"), + "json_field": cty.StringVal(`{"aaa": "value"}`), + }), + After: cty.ObjectVal(map[string]cty.Value{ + "id": cty.UnknownVal(cty.String), + "json_field": cty.StringVal(`{"aaa": "value", "bbb": "new_value"}`), + }), + Schema: &configschema.Block{ + Attributes: map[string]*configschema.Attribute{ + "id": {Type: cty.String, Optional: true, Computed: true}, + "json_field": {Type: cty.String, Optional: true}, + }, + }, + RequiredReplace: cty.NewPathSet(cty.Path{ + cty.GetAttrStep{Name: "json_field"}, + }), + ExpectedOutput: ` # test_instance.example must be replaced +-/+ resource "test_instance" "example" { + ~ id = "i-02ae66f368e8518a9" -> (known after apply) + ~ json_field = jsonencode( + ~ { + - aaa = "value" + } -> { + + aaa = "value" + + bbb = "new_value" + } # forces replacement + ) + } `, }, "in-place update (whitespace change)": { @@ -293,6 +360,39 @@ func TestResourceChange_JSON(t *testing.T) { } ) } +`, + }, + "force-new update (whitespace change)": { + Action: plans.DeleteThenCreate, + Mode: addrs.ManagedResourceMode, + Before: cty.ObjectVal(map[string]cty.Value{ + "id": cty.StringVal("i-02ae66f368e8518a9"), + "json_field": cty.StringVal(`{"aaa": "value", "bbb": "another"}`), + }), + After: cty.ObjectVal(map[string]cty.Value{ + "id": cty.UnknownVal(cty.String), + "json_field": cty.StringVal(`{"aaa":"value", + "bbb":"another"}`), + }), + Schema: &configschema.Block{ + Attributes: map[string]*configschema.Attribute{ + "id": {Type: cty.String, Optional: true, Computed: true}, + "json_field": {Type: cty.String, Optional: true}, + }, + }, + RequiredReplace: cty.NewPathSet(cty.Path{ + cty.GetAttrStep{Name: "json_field"}, + }), + ExpectedOutput: ` # test_instance.example must be replaced +-/+ resource "test_instance" "example" { + ~ id = "i-02ae66f368e8518a9" -> (known after apply) + ~ json_field = jsonencode( # whitespace changes force replacement + { + aaa = "value" + bbb = "another" + } + ) + } `, }, } @@ -372,6 +472,48 @@ func TestResourceChange_primitiveList(t *testing.T) { "cccc", ] } +`, + }, + "force-new update - insertion": { + Action: plans.DeleteThenCreate, + Mode: addrs.ManagedResourceMode, + Before: cty.ObjectVal(map[string]cty.Value{ + "id": cty.StringVal("i-02ae66f368e8518a9"), + "ami": cty.StringVal("ami-STATIC"), + "list_field": cty.ListVal([]cty.Value{ + cty.StringVal("aaaa"), + cty.StringVal("cccc"), + }), + }), + After: cty.ObjectVal(map[string]cty.Value{ + "id": cty.UnknownVal(cty.String), + "ami": cty.StringVal("ami-STATIC"), + "list_field": cty.ListVal([]cty.Value{ + cty.StringVal("aaaa"), + cty.StringVal("bbbb"), + cty.StringVal("cccc"), + }), + }), + Schema: &configschema.Block{ + Attributes: map[string]*configschema.Attribute{ + "id": {Type: cty.String, Optional: true, Computed: true}, + "ami": {Type: cty.String, Optional: true}, + "list_field": {Type: cty.List(cty.String), Optional: true}, + }, + }, + RequiredReplace: cty.NewPathSet(cty.Path{ + cty.GetAttrStep{Name: "list_field"}, + }), + ExpectedOutput: ` # test_instance.example must be replaced +-/+ resource "test_instance" "example" { + ami = "ami-STATIC" + ~ id = "i-02ae66f368e8518a9" -> (known after apply) + ~ list_field = [ # forces replacement + "aaaa", + + "bbbb", + "cccc", + ] + } `, }, "in-place update - deletion": { @@ -490,6 +632,48 @@ func TestResourceChange_primitiveSet(t *testing.T) { "cccc", ] } +`, + }, + "force-new update - insertion": { + Action: plans.DeleteThenCreate, + Mode: addrs.ManagedResourceMode, + Before: cty.ObjectVal(map[string]cty.Value{ + "id": cty.StringVal("i-02ae66f368e8518a9"), + "ami": cty.StringVal("ami-STATIC"), + "set_field": cty.SetVal([]cty.Value{ + cty.StringVal("aaaa"), + cty.StringVal("cccc"), + }), + }), + After: cty.ObjectVal(map[string]cty.Value{ + "id": cty.UnknownVal(cty.String), + "ami": cty.StringVal("ami-STATIC"), + "set_field": cty.SetVal([]cty.Value{ + cty.StringVal("aaaa"), + cty.StringVal("bbbb"), + cty.StringVal("cccc"), + }), + }), + Schema: &configschema.Block{ + Attributes: map[string]*configschema.Attribute{ + "id": {Type: cty.String, Optional: true, Computed: true}, + "ami": {Type: cty.String, Optional: true}, + "set_field": {Type: cty.Set(cty.String), Optional: true}, + }, + }, + RequiredReplace: cty.NewPathSet(cty.Path{ + cty.GetAttrStep{Name: "set_field"}, + }), + ExpectedOutput: ` # test_instance.example must be replaced +-/+ resource "test_instance" "example" { + ami = "ami-STATIC" + ~ id = "i-02ae66f368e8518a9" -> (known after apply) + ~ set_field = [ # forces replacement + "aaaa", + + "bbbb", + "cccc", + ] + } `, }, "in-place update - deletion": { @@ -608,6 +792,48 @@ func TestResourceChange_map(t *testing.T) { "c" = "cccc" } } +`, + }, + "force-new update - insertion": { + Action: plans.DeleteThenCreate, + Mode: addrs.ManagedResourceMode, + Before: cty.ObjectVal(map[string]cty.Value{ + "id": cty.StringVal("i-02ae66f368e8518a9"), + "ami": cty.StringVal("ami-STATIC"), + "map_field": cty.MapVal(map[string]cty.Value{ + "a": cty.StringVal("aaaa"), + "c": cty.StringVal("cccc"), + }), + }), + After: cty.ObjectVal(map[string]cty.Value{ + "id": cty.UnknownVal(cty.String), + "ami": cty.StringVal("ami-STATIC"), + "map_field": cty.MapVal(map[string]cty.Value{ + "a": cty.StringVal("aaaa"), + "b": cty.StringVal("bbbb"), + "c": cty.StringVal("cccc"), + }), + }), + Schema: &configschema.Block{ + Attributes: map[string]*configschema.Attribute{ + "id": {Type: cty.String, Optional: true, Computed: true}, + "ami": {Type: cty.String, Optional: true}, + "map_field": {Type: cty.Map(cty.String), Optional: true}, + }, + }, + RequiredReplace: cty.NewPathSet(cty.Path{ + cty.GetAttrStep{Name: "map_field"}, + }), + ExpectedOutput: ` # test_instance.example must be replaced +-/+ resource "test_instance" "example" { + ami = "ami-STATIC" + ~ id = "i-02ae66f368e8518a9" -> (known after apply) + ~ map_field = { # forces replacement + "a" = "aaaa" + + "b" = "bbbb" + "c" = "cccc" + } + } `, }, "in-place update - deletion": { @@ -816,6 +1042,118 @@ func TestResourceChange_nestedList(t *testing.T) { volume_type = "gp2" } } +`, + }, + "force-new update (inside block)": { + Action: plans.DeleteThenCreate, + Mode: addrs.ManagedResourceMode, + Before: cty.ObjectVal(map[string]cty.Value{ + "id": cty.StringVal("i-02ae66f368e8518a9"), + "ami": cty.StringVal("ami-BEFORE"), + "root_block_device": cty.ListVal([]cty.Value{ + cty.ObjectVal(map[string]cty.Value{ + "volume_type": cty.StringVal("gp2"), + }), + }), + }), + After: cty.ObjectVal(map[string]cty.Value{ + "id": cty.StringVal("i-02ae66f368e8518a9"), + "ami": cty.StringVal("ami-AFTER"), + "root_block_device": cty.ListVal([]cty.Value{ + cty.ObjectVal(map[string]cty.Value{ + "volume_type": cty.StringVal("different"), + }), + }), + }), + RequiredReplace: cty.NewPathSet(cty.Path{ + cty.GetAttrStep{Name: "root_block_device"}, + cty.IndexStep{Key: cty.NumberIntVal(0)}, + cty.GetAttrStep{Name: "volume_type"}, + }), + Schema: &configschema.Block{ + Attributes: map[string]*configschema.Attribute{ + "id": {Type: cty.String, Optional: true, Computed: true}, + "ami": {Type: cty.String, Optional: true}, + }, + BlockTypes: map[string]*configschema.NestedBlock{ + "root_block_device": { + Block: configschema.Block{ + Attributes: map[string]*configschema.Attribute{ + "volume_type": { + Type: cty.String, + Optional: true, + Computed: true, + }, + }, + }, + Nesting: 2, + }, + }, + }, + ExpectedOutput: ` # test_instance.example must be replaced +-/+ resource "test_instance" "example" { + ~ ami = "ami-BEFORE" -> "ami-AFTER" + id = "i-02ae66f368e8518a9" + + ~ root_block_device { + ~ volume_type = "gp2" -> "different" # forces replacement + } + } +`, + }, + "force-new update (whole block)": { + Action: plans.DeleteThenCreate, + Mode: addrs.ManagedResourceMode, + Before: cty.ObjectVal(map[string]cty.Value{ + "id": cty.StringVal("i-02ae66f368e8518a9"), + "ami": cty.StringVal("ami-BEFORE"), + "root_block_device": cty.ListVal([]cty.Value{ + cty.ObjectVal(map[string]cty.Value{ + "volume_type": cty.StringVal("gp2"), + }), + }), + }), + After: cty.ObjectVal(map[string]cty.Value{ + "id": cty.StringVal("i-02ae66f368e8518a9"), + "ami": cty.StringVal("ami-AFTER"), + "root_block_device": cty.ListVal([]cty.Value{ + cty.ObjectVal(map[string]cty.Value{ + "volume_type": cty.StringVal("different"), + }), + }), + }), + RequiredReplace: cty.NewPathSet(cty.Path{ + cty.GetAttrStep{Name: "root_block_device"}, + }), + Schema: &configschema.Block{ + Attributes: map[string]*configschema.Attribute{ + "id": {Type: cty.String, Optional: true, Computed: true}, + "ami": {Type: cty.String, Optional: true}, + }, + BlockTypes: map[string]*configschema.NestedBlock{ + "root_block_device": { + Block: configschema.Block{ + Attributes: map[string]*configschema.Attribute{ + "volume_type": { + Type: cty.String, + Optional: true, + Computed: true, + }, + }, + }, + Nesting: 2, + }, + }, + }, + ExpectedOutput: ` # test_instance.example must be replaced +-/+ resource "test_instance" "example" { + ~ ami = "ami-BEFORE" -> "ami-AFTER" + id = "i-02ae66f368e8518a9" + + ~ root_block_device { # forces replacement + ~ volume_type = "gp2" -> "different" + } + } `, }, "in-place update - deletion": { @@ -988,6 +1326,118 @@ func TestResourceChange_nestedSet(t *testing.T) { volume_type = "gp2" } } +`, + }, + "force-new update (inside block)": { + Action: plans.DeleteThenCreate, + Mode: addrs.ManagedResourceMode, + Before: cty.ObjectVal(map[string]cty.Value{ + "id": cty.StringVal("i-02ae66f368e8518a9"), + "ami": cty.StringVal("ami-BEFORE"), + "root_block_device": cty.SetVal([]cty.Value{ + cty.ObjectVal(map[string]cty.Value{ + "volume_type": cty.StringVal("gp2"), + }), + }), + }), + After: cty.ObjectVal(map[string]cty.Value{ + "id": cty.StringVal("i-02ae66f368e8518a9"), + "ami": cty.StringVal("ami-AFTER"), + "root_block_device": cty.SetVal([]cty.Value{ + cty.ObjectVal(map[string]cty.Value{ + "volume_type": cty.StringVal("different"), + }), + }), + }), + RequiredReplace: cty.NewPathSet(cty.Path{ + cty.GetAttrStep{Name: "root_block_device"}, + cty.IndexStep{Key: cty.NumberIntVal(0)}, + cty.GetAttrStep{Name: "volume_type"}, + }), + Schema: &configschema.Block{ + Attributes: map[string]*configschema.Attribute{ + "id": {Type: cty.String, Optional: true, Computed: true}, + "ami": {Type: cty.String, Optional: true}, + }, + BlockTypes: map[string]*configschema.NestedBlock{ + "root_block_device": { + Block: configschema.Block{ + Attributes: map[string]*configschema.Attribute{ + "volume_type": { + Type: cty.String, + Optional: true, + Computed: true, + }, + }, + }, + Nesting: 2, + }, + }, + }, + ExpectedOutput: ` # test_instance.example must be replaced +-/+ resource "test_instance" "example" { + ~ ami = "ami-BEFORE" -> "ami-AFTER" + id = "i-02ae66f368e8518a9" + + ~ root_block_device { + ~ volume_type = "gp2" -> "different" # forces replacement + } + } +`, + }, + "force-new update (whole block)": { + Action: plans.DeleteThenCreate, + Mode: addrs.ManagedResourceMode, + Before: cty.ObjectVal(map[string]cty.Value{ + "id": cty.StringVal("i-02ae66f368e8518a9"), + "ami": cty.StringVal("ami-BEFORE"), + "root_block_device": cty.SetVal([]cty.Value{ + cty.ObjectVal(map[string]cty.Value{ + "volume_type": cty.StringVal("gp2"), + }), + }), + }), + After: cty.ObjectVal(map[string]cty.Value{ + "id": cty.StringVal("i-02ae66f368e8518a9"), + "ami": cty.StringVal("ami-AFTER"), + "root_block_device": cty.SetVal([]cty.Value{ + cty.ObjectVal(map[string]cty.Value{ + "volume_type": cty.StringVal("different"), + }), + }), + }), + RequiredReplace: cty.NewPathSet(cty.Path{ + cty.GetAttrStep{Name: "root_block_device"}, + }), + Schema: &configschema.Block{ + Attributes: map[string]*configschema.Attribute{ + "id": {Type: cty.String, Optional: true, Computed: true}, + "ami": {Type: cty.String, Optional: true}, + }, + BlockTypes: map[string]*configschema.NestedBlock{ + "root_block_device": { + Block: configschema.Block{ + Attributes: map[string]*configschema.Attribute{ + "volume_type": { + Type: cty.String, + Optional: true, + Computed: true, + }, + }, + }, + Nesting: 2, + }, + }, + }, + ExpectedOutput: ` # test_instance.example must be replaced +-/+ resource "test_instance" "example" { + ~ ami = "ami-BEFORE" -> "ami-AFTER" + id = "i-02ae66f368e8518a9" + + ~ root_block_device { # forces replacement + ~ volume_type = "gp2" -> "different" + } + } `, }, "in-place update - deletion": {