From d641d513801662d8d8ea87bb0679cd5de77f9482 Mon Sep 17 00:00:00 2001 From: Mike Ball Date: Sun, 23 Nov 2025 05:53:22 -0500 Subject: [PATCH 1/9] feat(moved): GetAllResourceChanges includes 'moved' This lays a foundation for including 'moved' in summarized plan out put, ultimately to address issue #95 Signed-off-by: Mike Ball --- terraformstate/terraform_state.go | 14 ++++++++++++++ terraformstate/terraform_state_test.go | 4 ++++ writer/table.go | 2 +- 3 files changed, 19 insertions(+), 1 deletion(-) diff --git a/terraformstate/terraform_state.go b/terraformstate/terraform_state.go index 9f67da0..ee57dab 100644 --- a/terraformstate/terraform_state.go +++ b/terraformstate/terraform_state.go @@ -90,6 +90,17 @@ func importedResources(resources ResourceChanges) ResourceChanges { return acc } +func movedResources(resources ResourceChanges) ResourceChanges { + acc := make(ResourceChanges, 0) + for _, r := range resources { + if r.PreviousAddress != "" && r.PreviousAddress != r.Address { + acc = append(acc, r) + + } + } + return acc +} + func FilterNoOpResources(ts *tfjson.Plan) { acc := make(ResourceChanges, 0) for _, r := range ts.ResourceChanges { @@ -107,6 +118,7 @@ func GetAllResourceChanges(plan tfjson.Plan) map[string]ResourceChanges { updatedResources := updatedResources(plan.ResourceChanges) recreatedResources := recreatedResources(plan.ResourceChanges) importedResources := importedResources(plan.ResourceChanges) + movedResources := movedResources(plan.ResourceChanges) sortResources := func(resources ResourceChanges) { sort.Slice(resources, func(i, j int) bool { @@ -119,9 +131,11 @@ func GetAllResourceChanges(plan tfjson.Plan) map[string]ResourceChanges { sortResources(updatedResources) sortResources(recreatedResources) sortResources(importedResources) + sortResources(movedResources) return map[string]ResourceChanges{ "import": importedResources, + "moved": movedResources, "add": addedResources, "delete": deletedResources, "update": updatedResources, diff --git a/terraformstate/terraform_state_test.go b/terraformstate/terraform_state_test.go index 00caaa7..2d4c23d 100644 --- a/terraformstate/terraform_state_test.go +++ b/terraformstate/terraform_state_test.go @@ -42,6 +42,7 @@ func TestGetAllResourceChanges(t *testing.T) { &ResourceChange{Address: "update1", Change: &Change{Actions: Actions{ActionUpdate}}}, &ResourceChange{Address: "import2", Change: &Change{Importing: &Importing{ID: "id1"}}}, &ResourceChange{Address: "import1", Change: &Change{Importing: &Importing{ID: "id2"}}}, + &ResourceChange{Address: "move1", PreviousAddress: "move", Change: &Change{Actions: Actions{}}}, &ResourceChange{Address: "recreate2", Change: &Change{Actions: Actions{ActionDelete, ActionCreate}}}, &ResourceChange{Address: "recreate1", Change: &Change{Actions: Actions{ActionDelete, ActionCreate}}}, } @@ -70,6 +71,9 @@ func TestGetAllResourceChanges(t *testing.T) { &ResourceChange{Address: "import1", Change: &Change{Importing: &Importing{ID: "id2"}}}, &ResourceChange{Address: "import2", Change: &Change{Importing: &Importing{ID: "id1"}}}, }, + "moved": { + &ResourceChange{Address: "move1", PreviousAddress: "move", Change: &Change{Actions: Actions{}}}, + }, } assert.Equal(t, expectedResourceChanges, result) diff --git a/writer/table.go b/writer/table.go index 8179a90..507eeb6 100644 --- a/writer/table.go +++ b/writer/table.go @@ -14,7 +14,7 @@ type TableWriter struct { outputChanges map[string][]string } -var tableOrder = []string{"import", "add", "update", "recreate", "delete"} +var tableOrder = []string{"import", "move", "add", "update", "recreate", "delete"} func (t TableWriter) Write(writer io.Writer) error { tableString := make([][]string, 0, 4) From 41429cf0415fd15bccb1a9d9181c7b94eb85f689 Mon Sep 17 00:00:00 2001 From: Mike Ball Date: Sun, 23 Nov 2025 06:10:00 -0500 Subject: [PATCH 2/9] chore(html): do not overwrite templates dir in tests Previously, https://github.com/dineshba/tf-summarize/pull/78 introduced a change to overwrite the baked-in templates directory with a mock file system during testing. As a result, tests did not exercise the real `writer/templates/*.html` tf-summarize template logic/result. Signed-off-by: Mike Ball --- writer/html.go | 6 ++-- writer/html_test.go | 44 +-------------------------- writer/templates/resourceChanges.html | 5 +-- writer/util.go | 5 --- 4 files changed, 6 insertions(+), 54 deletions(-) diff --git a/writer/html.go b/writer/html.go index c07c206..66ee3b5 100644 --- a/writer/html.go +++ b/writer/html.go @@ -14,13 +14,11 @@ type HTMLWriter struct { OutputChanges map[string][]string } -var cfs = getFS() - // Write outputs the HTML summary to the io.Writer it's passed. func (t HTMLWriter) Write(writer io.Writer) error { templatesDir := "templates" rcTmpl := "resourceChanges.html" - tmpl, err := template.New(rcTmpl).ParseFS(cfs, path.Join(templatesDir, rcTmpl)) + tmpl, err := template.New(rcTmpl).ParseFS(templates, path.Join(templatesDir, rcTmpl)) if err != nil { return err } @@ -35,7 +33,7 @@ func (t HTMLWriter) Write(writer io.Writer) error { } ocTmpl := "outputChanges.html" - outputTmpl, err := template.New(ocTmpl).ParseFS(cfs, path.Join(templatesDir, ocTmpl)) + outputTmpl, err := template.New(ocTmpl).ParseFS(templates, path.Join(templatesDir, ocTmpl)) if err != nil { return err } diff --git a/writer/html_test.go b/writer/html_test.go index 64d704e..7aac5e6 100644 --- a/writer/html_test.go +++ b/writer/html_test.go @@ -3,54 +3,12 @@ package writer import ( "bytes" "testing" - "testing/fstest" "github.com/dineshba/tf-summarize/terraformstate" . "github.com/hashicorp/terraform-json" ) -func TestHTMLWriterWithMockFileSystem(t *testing.T) { - origFS := cfs - cfs = fstest.MapFS{ - "templates/resourceChanges.html": &fstest.MapFile{ - Data: []byte(` - - - - {{ range $change, $resources := .ResourceChanges }}{{ $length := len $resources }}{{ if gt $length 0 }} - - - - {{ end }}{{ end }} -
CHANGERESOURCE
{{ $change }} -
    {{ range $i, $r := $resources }} -
  • {{ $r.Address }}
  • {{ end }} -
-
-`), - }, - "templates/outputChanges.html": &fstest.MapFile{ - Data: []byte(` - - - - {{ range $change, $outputs := .OutputChanges }}{{ $length := len $outputs }}{{ if gt $length 0 }} - - - - {{ end }}{{ end }} -
CHANGEOUTPUT
{{ $change }} -
    {{ range $i, $o := $outputs }} -
  • {{ $o }}
  • {{ end }} -
-
-`), - }, - } - t.Cleanup(func() { - cfs = origFS - }) - +func TestHTMLWriter(t *testing.T) { resourceChanges := map[string]terraformstate.ResourceChanges{ "module.test": { { diff --git a/writer/templates/resourceChanges.html b/writer/templates/resourceChanges.html index 8f4f4ab..fd06ddd 100644 --- a/writer/templates/resourceChanges.html +++ b/writer/templates/resourceChanges.html @@ -6,8 +6,9 @@ {{ $change }} -
    {{ range $i, $r := $resources }} -
  • {{ $r.Address }}
  • {{ end }} +
      {{ range $i, $r := $resources }}{{ if eq $change "moved" }} +
    • {{ $r.Address }} to {{ $r.Address }}
    • {{ else }} +
    • {{ $r.Address }}
    • {{ end }}{{ end }}
    {{ end }}{{ end }} diff --git a/writer/util.go b/writer/util.go index 1f59282..c75e0ff 100644 --- a/writer/util.go +++ b/writer/util.go @@ -2,7 +2,6 @@ package writer import ( "embed" - "io/fs" "regexp" ) @@ -11,10 +10,6 @@ import ( //go:embed templates var templates embed.FS -var getFS = func() fs.FS { - return templates -} - func hasOutputChanges(opChanges map[string][]string) bool { hasChanges := false From 56b8426675bab08d35ba3f7605aa802a270af39b Mon Sep 17 00:00:00 2001 From: Mike Ball Date: Sun, 23 Nov 2025 06:17:24 -0500 Subject: [PATCH 3/9] fix(html): specify change type in CHANGE table column Previously, the HTMLWriter tests' "CHANGE" column value was a resource name, and not the actual type of change seen in the plan. This fixes that. Signed-off-by: Mike Ball --- writer/html_test.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/writer/html_test.go b/writer/html_test.go index 7aac5e6..b1be5ae 100644 --- a/writer/html_test.go +++ b/writer/html_test.go @@ -10,7 +10,7 @@ import ( func TestHTMLWriter(t *testing.T) { resourceChanges := map[string]terraformstate.ResourceChanges{ - "module.test": { + "update": { { Address: "aws_instance.example", Name: "example", @@ -40,7 +40,7 @@ func TestHTMLWriter(t *testing.T) { RESOURCE - module.test + update
    • aws_instance.example
    • From c05c003f208eb80823c375005ca4f52c734141f9 Mon Sep 17 00:00:00 2001 From: Mike Ball Date: Sun, 23 Nov 2025 06:28:23 -0500 Subject: [PATCH 4/9] feat(html): include "moved" resource changes This includes resource address moves in the resource changes in HTML table output. Signed-off-by: Mike Ball --- writer/html_test.go | 20 +++++++++++++++++++- writer/templates/resourceChanges.html | 2 +- 2 files changed, 20 insertions(+), 2 deletions(-) diff --git a/writer/html_test.go b/writer/html_test.go index b1be5ae..d681130 100644 --- a/writer/html_test.go +++ b/writer/html_test.go @@ -21,6 +21,16 @@ func TestHTMLWriter(t *testing.T) { }, }, }, + "moved": { + { + Address: "aws_instance.foo", + PreviousAddress: "aws_instance.bar", + Name: "foo", + Change: &Change{ + Actions: Actions{}, + }, + }, + }, } outputChanges := map[string][]string{ "output_key": {"output_value"}, @@ -39,6 +49,14 @@ func TestHTMLWriter(t *testing.T) { CHANGE RESOURCE + + moved + +
        +
      • aws_instance.bar to aws_instance.foo
      • +
      + + update @@ -64,7 +82,7 @@ func TestHTMLWriter(t *testing.T) { ` if buf.String() != expectedOutput { - t.Errorf("expected %q, got %q", expectedOutput, buf.String()) + t.Errorf("expected %s, got %s", expectedOutput, buf.String()) } } diff --git a/writer/templates/resourceChanges.html b/writer/templates/resourceChanges.html index fd06ddd..283279b 100644 --- a/writer/templates/resourceChanges.html +++ b/writer/templates/resourceChanges.html @@ -7,7 +7,7 @@ {{ $change }}
        {{ range $i, $r := $resources }}{{ if eq $change "moved" }} -
      • {{ $r.Address }} to {{ $r.Address }}
      • {{ else }} +
      • {{ $r.PreviousAddress }} to {{ $r.Address }}
      • {{ else }}
      • {{ $r.Address }}
      • {{ end }}{{ end }}
      From 8bdfb2504875a47ebcfdbe4e41dc352c2a03015d Mon Sep 17 00:00:00 2001 From: Mike Ball Date: Sun, 23 Nov 2025 06:34:46 -0500 Subject: [PATCH 5/9] feat(table): include "moved" resource details in MD table This includes details pertaining resource moves in the markdown table output. --- writer/table.go | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/writer/table.go b/writer/table.go index 507eeb6..74290ae 100644 --- a/writer/table.go +++ b/writer/table.go @@ -22,7 +22,11 @@ func (t TableWriter) Write(writer io.Writer) error { changedResources := t.changes[change] for _, changedResource := range changedResources { if t.mdEnabled { - tableString = append(tableString, []string{change, fmt.Sprintf("`%s`", changedResource.Address)}) + if change == "moved" { + tableString = append(tableString, []string{change, fmt.Sprintf("`%s` to `%s`", changedResource.PreviousAddress, changedResource.Address)}) + } else { + tableString = append(tableString, []string{change, fmt.Sprintf("`%s`", changedResource.Address)}) + } } else { tableString = append(tableString, []string{change, changedResource.Address}) } From 9ec7b0d94a8fc452257129d4466ff72997f157cc Mon Sep 17 00:00:00 2001 From: Mike Ball Date: Sun, 23 Nov 2025 06:53:42 -0500 Subject: [PATCH 6/9] feat(table): include "moved" resources in non-MD table This includes moved resources amongst the changes in the non-markdown table output. Signed-off-by: Mike Ball --- writer/separate_tree_test.go | 7 +++++++ writer/table.go | 8 ++++++-- writer/table_test.go | 11 +++++++---- 3 files changed, 20 insertions(+), 6 deletions(-) diff --git a/writer/separate_tree_test.go b/writer/separate_tree_test.go index dfb30c5..00c9165 100644 --- a/writer/separate_tree_test.go +++ b/writer/separate_tree_test.go @@ -53,6 +53,13 @@ func createMockChanges() map[string]terraformstate.ResourceChanges { Change: &Change{Actions: Actions{ActionDelete}}, }, }, + "moved": { + { + Address: "aws_instance.new", + PreviousAddress: "aws_instance.old", + Change: &Change{Actions: Actions{}}, + }, + }, } } diff --git a/writer/table.go b/writer/table.go index 74290ae..2003f61 100644 --- a/writer/table.go +++ b/writer/table.go @@ -14,7 +14,7 @@ type TableWriter struct { outputChanges map[string][]string } -var tableOrder = []string{"import", "move", "add", "update", "recreate", "delete"} +var tableOrder = []string{"import", "moved", "add", "update", "recreate", "delete"} func (t TableWriter) Write(writer io.Writer) error { tableString := make([][]string, 0, 4) @@ -28,7 +28,11 @@ func (t TableWriter) Write(writer io.Writer) error { tableString = append(tableString, []string{change, fmt.Sprintf("`%s`", changedResource.Address)}) } } else { - tableString = append(tableString, []string{change, changedResource.Address}) + if change == "moved" { + tableString = append(tableString, []string{change, fmt.Sprintf("%s to %s", changedResource.PreviousAddress, changedResource.Address)}) + } else { + tableString = append(tableString, []string{change, changedResource.Address}) + } } } } diff --git a/writer/table_test.go b/writer/table_test.go index 65043d7..2c1e976 100644 --- a/writer/table_test.go +++ b/writer/table_test.go @@ -39,6 +39,8 @@ func TestTableWriter_Write_NoMarkdown(t *testing.T) { expectedOutput := `+--------+--------------------------------------------------+ | CHANGE | RESOURCE | +--------+--------------------------------------------------+ +| moved | aws_instance.old to aws_instance.new | ++--------+--------------------------------------------------+ | add | aws_instance.example1 | +--------+--------------------------------------------------+ | update | aws_instance.example3 | @@ -74,10 +76,11 @@ func TestTableWriter_Write_WithMarkdown(t *testing.T) { err := tw.Write(&output) assert.NoError(t, err) - expectedOutput := `| CHANGE | RESOURCE | -|--------|-------------------------| -| add | ` + "`aws_instance.example1`" + ` | -| delete | ` + "`aws_instance.example2`" + ` | + expectedOutput := `| CHANGE | RESOURCE | +|--------|------------------------------------------| +| moved | ` + "`aws_instance.old` to `aws_instance.new`" + ` | +| add | ` + "`aws_instance.example1`" + ` | +| delete | ` + "`aws_instance.example2`" + ` | | CHANGE | OUTPUT | |--------|----------------------------------------------------------| From 9ff4e1914d7f476f00981f982587e172738a0cb6 Mon Sep 17 00:00:00 2001 From: Mike Ball Date: Wed, 26 Nov 2025 05:34:07 -0500 Subject: [PATCH 7/9] feat(html): ensure 'moved' resources appear at bottom of change table This improves the HTML table to show 'moved' resources at the bottom of the table. This also seeks to establish a better control flow pattern for other writers to emulate. Signed-off-by: Mike Ball --- terraformstate/terraform_state.go | 10 +++++++++- writer/html.go | 5 +++-- writer/html_test.go | 12 +++++++----- writer/templates/resourceChanges.html | 8 ++++++++ writer/writer.go | 2 +- 5 files changed, 28 insertions(+), 9 deletions(-) diff --git a/terraformstate/terraform_state.go b/terraformstate/terraform_state.go index ee57dab..d2acca6 100644 --- a/terraformstate/terraform_state.go +++ b/terraformstate/terraform_state.go @@ -131,7 +131,6 @@ func GetAllResourceChanges(plan tfjson.Plan) map[string]ResourceChanges { sortResources(updatedResources) sortResources(recreatedResources) sortResources(importedResources) - sortResources(movedResources) return map[string]ResourceChanges{ "import": importedResources, @@ -143,6 +142,15 @@ func GetAllResourceChanges(plan tfjson.Plan) map[string]ResourceChanges { } } +// GetAllResourceMoves returns all resources that have moved. +func GetAllResourceMoves(plan tfjson.Plan) map[string]ResourceChanges { + movedResources := movedResources(plan.ResourceChanges) + + return map[string]ResourceChanges{ + "moved": movedResources, + } +} + func GetAllOutputChanges(plan tfjson.Plan) map[string][]string { // create, update, and delete are the only available actions for outputChanges // https://developer.hashicorp.com/terraform/internals/json-format diff --git a/writer/html.go b/writer/html.go index 66ee3b5..cc10fe3 100644 --- a/writer/html.go +++ b/writer/html.go @@ -11,6 +11,7 @@ import ( // HTMLWriter is a Writer that writes HTML. type HTMLWriter struct { ResourceChanges map[string]terraformstate.ResourceChanges + MovedResources map[string]terraformstate.ResourceChanges OutputChanges map[string][]string } @@ -31,7 +32,6 @@ func (t HTMLWriter) Write(writer io.Writer) error { if !hasOutputChanges(t.OutputChanges) { return nil } - ocTmpl := "outputChanges.html" outputTmpl, err := template.New(ocTmpl).ParseFS(templates, path.Join(templatesDir, ocTmpl)) if err != nil { @@ -42,9 +42,10 @@ func (t HTMLWriter) Write(writer io.Writer) error { } // NewHTMLWriter returns a new HTMLWriter with the configuration it's passed. -func NewHTMLWriter(changes map[string]terraformstate.ResourceChanges, outputChanges map[string][]string) Writer { +func NewHTMLWriter(changes map[string]terraformstate.ResourceChanges, movedResources map[string]terraformstate.ResourceChanges, outputChanges map[string][]string) Writer { return HTMLWriter{ ResourceChanges: changes, + MovedResources: movedResources, OutputChanges: outputChanges, } } diff --git a/writer/html_test.go b/writer/html_test.go index d681130..9a7ea50 100644 --- a/writer/html_test.go +++ b/writer/html_test.go @@ -21,6 +21,8 @@ func TestHTMLWriter(t *testing.T) { }, }, }, + } + movedResources := map[string]terraformstate.ResourceChanges{ "moved": { { Address: "aws_instance.foo", @@ -36,7 +38,7 @@ func TestHTMLWriter(t *testing.T) { "output_key": {"output_value"}, } - htmlWriter := NewHTMLWriter(resourceChanges, outputChanges) + htmlWriter := NewHTMLWriter(resourceChanges, movedResources, outputChanges) var buf bytes.Buffer err := htmlWriter.Write(&buf) @@ -50,18 +52,18 @@ func TestHTMLWriter(t *testing.T) { RESOURCE - moved + update
        -
      • aws_instance.bar to aws_instance.foo
      • +
      • aws_instance.example
      - update + moved
        -
      • aws_instance.example
      • +
      • aws_instance.bar to aws_instance.foo
      diff --git a/writer/templates/resourceChanges.html b/writer/templates/resourceChanges.html index 283279b..a05c422 100644 --- a/writer/templates/resourceChanges.html +++ b/writer/templates/resourceChanges.html @@ -11,5 +11,13 @@
    • {{ $r.Address }}
    • {{ end }}{{ end }}
    + {{ end }}{{ end }}{{ range $change, $resources := .MovedResources }}{{ $length := len $resources }}{{ if gt $length 0 }} + + {{ $change }} + +
      {{ range $i, $r := $resources }} +
    • {{ $r.PreviousAddress }} to {{ $r.Address }}
    • {{ end }} +
    + {{ end }}{{ end }} diff --git a/writer/writer.go b/writer/writer.go index d22fa51..e626f90 100644 --- a/writer/writer.go +++ b/writer/writer.go @@ -22,7 +22,7 @@ func CreateWriter(tree, separateTree, drawable, mdEnabled, json, html bool, json return NewJSONWriter(plan.ResourceChanges) } if html { - return NewHTMLWriter(terraformstate.GetAllResourceChanges(plan), terraformstate.GetAllOutputChanges(plan)) + return NewHTMLWriter(terraformstate.GetAllResourceChanges(plan), terraformstate.GetAllResourceMoves(plan), terraformstate.GetAllOutputChanges(plan)) } if jsonSum { return NewJsonSumWriter(terraformstate.GetAllResourceChanges(plan)) From cad28e7d955afc94c59641f5905db61c8ea5797b Mon Sep 17 00:00:00 2001 From: Mike Ball Date: Wed, 26 Nov 2025 05:59:13 -0500 Subject: [PATCH 8/9] feat(table): pass/template moved resources separate from other changes This seeks to enable better control/flow around moved-specific logic. Signed-off-by: Mike Ball --- writer/separate_tree_test.go | 7 ------- writer/table.go | 30 ++++++++++++++++++------------ writer/table_test.go | 33 +++++++++++++++++++++++++++------ writer/writer.go | 2 +- 4 files changed, 46 insertions(+), 26 deletions(-) diff --git a/writer/separate_tree_test.go b/writer/separate_tree_test.go index 00c9165..dfb30c5 100644 --- a/writer/separate_tree_test.go +++ b/writer/separate_tree_test.go @@ -53,13 +53,6 @@ func createMockChanges() map[string]terraformstate.ResourceChanges { Change: &Change{Actions: Actions{ActionDelete}}, }, }, - "moved": { - { - Address: "aws_instance.new", - PreviousAddress: "aws_instance.old", - Change: &Change{Actions: Actions{}}, - }, - }, } } diff --git a/writer/table.go b/writer/table.go index 2003f61..405577f 100644 --- a/writer/table.go +++ b/writer/table.go @@ -11,28 +11,33 @@ import ( type TableWriter struct { mdEnabled bool changes map[string]terraformstate.ResourceChanges + moves map[string]terraformstate.ResourceChanges outputChanges map[string][]string } -var tableOrder = []string{"import", "moved", "add", "update", "recreate", "delete"} +var tableOrder = []string{"import", "add", "update", "recreate", "delete"} func (t TableWriter) Write(writer io.Writer) error { tableString := make([][]string, 0, 4) + for _, change := range tableOrder { changedResources := t.changes[change] + for _, changedResource := range changedResources { if t.mdEnabled { - if change == "moved" { - tableString = append(tableString, []string{change, fmt.Sprintf("`%s` to `%s`", changedResource.PreviousAddress, changedResource.Address)}) - } else { - tableString = append(tableString, []string{change, fmt.Sprintf("`%s`", changedResource.Address)}) - } + tableString = append(tableString, []string{change, fmt.Sprintf("`%s`", changedResource.Address)}) } else { - if change == "moved" { - tableString = append(tableString, []string{change, fmt.Sprintf("%s to %s", changedResource.PreviousAddress, changedResource.Address)}) - } else { - tableString = append(tableString, []string{change, changedResource.Address}) - } + tableString = append(tableString, []string{change, changedResource.Address}) + } + } + } + + for move, movedResources := range t.moves { + for _, movedResource := range movedResources { + if t.mdEnabled { + tableString = append(tableString, []string{move, fmt.Sprintf("`%s` to `%s`", movedResource.PreviousAddress, movedResource.Address)}) + } else { + tableString = append(tableString, []string{move, fmt.Sprintf("%s to %s", movedResource.PreviousAddress, movedResource.Address)}) } } } @@ -88,9 +93,10 @@ func (t TableWriter) Write(writer io.Writer) error { return nil } -func NewTableWriter(changes map[string]terraformstate.ResourceChanges, outputChanges map[string][]string, mdEnabled bool) Writer { +func NewTableWriter(changes map[string]terraformstate.ResourceChanges, moves map[string]terraformstate.ResourceChanges, outputChanges map[string][]string, mdEnabled bool) Writer { return TableWriter{ changes: changes, + moves: moves, mdEnabled: mdEnabled, outputChanges: outputChanges, } diff --git a/writer/table_test.go b/writer/table_test.go index 2c1e976..26e3121 100644 --- a/writer/table_test.go +++ b/writer/table_test.go @@ -24,6 +24,16 @@ func TestTableWriter_Write_NoMarkdown(t *testing.T) { }, } + movedResources := map[string]terraformstate.ResourceChanges{ + "moved": { + { + Address: "aws_instance.new", + PreviousAddress: "aws_instance.old", + Change: &Change{Actions: Actions{}}, + }, + }, + } + outputChanges := map[string][]string{ "update": { "output.example", @@ -31,7 +41,7 @@ func TestTableWriter_Write_NoMarkdown(t *testing.T) { }, } - tw := NewTableWriter(changes, outputChanges, false) + tw := NewTableWriter(changes, movedResources, outputChanges, false) var output bytes.Buffer err := tw.Write(&output) assert.NoError(t, err) @@ -39,8 +49,6 @@ func TestTableWriter_Write_NoMarkdown(t *testing.T) { expectedOutput := `+--------+--------------------------------------------------+ | CHANGE | RESOURCE | +--------+--------------------------------------------------+ -| moved | aws_instance.old to aws_instance.new | -+--------+--------------------------------------------------+ | add | aws_instance.example1 | +--------+--------------------------------------------------+ | update | aws_instance.example3 | @@ -49,6 +57,8 @@ func TestTableWriter_Write_NoMarkdown(t *testing.T) { +--------+--------------------------------------------------+ | delete | aws_instance.example2 | +--------+--------------------------------------------------+ +| moved | aws_instance.old to aws_instance.new | ++--------+--------------------------------------------------+ +--------+--------------------------------------------------------+ | CHANGE | OUTPUT | +--------+--------------------------------------------------------+ @@ -64,6 +74,16 @@ func TestTableWriter_Write_NoMarkdown(t *testing.T) { func TestTableWriter_Write_WithMarkdown(t *testing.T) { changes := createMockChanges() + movedResources := map[string]terraformstate.ResourceChanges{ + "moved": { + { + Address: "aws_instance.new", + PreviousAddress: "aws_instance.old", + Change: &Change{Actions: Actions{}}, + }, + }, + } + outputChanges := map[string][]string{ "update": { "output.example", @@ -71,16 +91,16 @@ func TestTableWriter_Write_WithMarkdown(t *testing.T) { }, } - tw := NewTableWriter(changes, outputChanges, true) + tw := NewTableWriter(changes, movedResources, outputChanges, true) var output bytes.Buffer err := tw.Write(&output) assert.NoError(t, err) expectedOutput := `| CHANGE | RESOURCE | |--------|------------------------------------------| -| moved | ` + "`aws_instance.old` to `aws_instance.new`" + ` | | add | ` + "`aws_instance.example1`" + ` | | delete | ` + "`aws_instance.example2`" + ` | +| moved | ` + "`aws_instance.old` to `aws_instance.new`" + ` | | CHANGE | OUTPUT | |--------|----------------------------------------------------------| @@ -93,9 +113,10 @@ func TestTableWriter_Write_WithMarkdown(t *testing.T) { func TestTableWriter_NoChanges(t *testing.T) { changes := map[string]terraformstate.ResourceChanges{} + movedResources := map[string]terraformstate.ResourceChanges{} outputChanges := map[string][]string{} - tw := NewTableWriter(changes, outputChanges, false) + tw := NewTableWriter(changes, movedResources, outputChanges, false) var output bytes.Buffer err := tw.Write(&output) assert.NoError(t, err) diff --git a/writer/writer.go b/writer/writer.go index e626f90..4417302 100644 --- a/writer/writer.go +++ b/writer/writer.go @@ -28,5 +28,5 @@ func CreateWriter(tree, separateTree, drawable, mdEnabled, json, html bool, json return NewJsonSumWriter(terraformstate.GetAllResourceChanges(plan)) } - return NewTableWriter(terraformstate.GetAllResourceChanges(plan), terraformstate.GetAllOutputChanges(plan), mdEnabled) + return NewTableWriter(terraformstate.GetAllResourceChanges(plan), terraformstate.GetAllResourceMoves(plan), terraformstate.GetAllOutputChanges(plan), mdEnabled) } From 22cb882cf0875f47c4044cb12bcfc52b47810727 Mon Sep 17 00:00:00 2001 From: Mike Ball Date: Wed, 26 Nov 2025 06:02:26 -0500 Subject: [PATCH 9/9] fix(GetAllResourceChanges): disinclude moves from resource changes A move does not inherently require change actions; moves are handled separately from resource changes. Signed-off-by: Mike Ball --- terraformstate/terraform_state.go | 2 -- terraformstate/terraform_state_test.go | 26 +++++++++++++++++++++++++- 2 files changed, 25 insertions(+), 3 deletions(-) diff --git a/terraformstate/terraform_state.go b/terraformstate/terraform_state.go index d2acca6..557bd9d 100644 --- a/terraformstate/terraform_state.go +++ b/terraformstate/terraform_state.go @@ -118,7 +118,6 @@ func GetAllResourceChanges(plan tfjson.Plan) map[string]ResourceChanges { updatedResources := updatedResources(plan.ResourceChanges) recreatedResources := recreatedResources(plan.ResourceChanges) importedResources := importedResources(plan.ResourceChanges) - movedResources := movedResources(plan.ResourceChanges) sortResources := func(resources ResourceChanges) { sort.Slice(resources, func(i, j int) bool { @@ -134,7 +133,6 @@ func GetAllResourceChanges(plan tfjson.Plan) map[string]ResourceChanges { return map[string]ResourceChanges{ "import": importedResources, - "moved": movedResources, "add": addedResources, "delete": deletedResources, "update": updatedResources, diff --git a/terraformstate/terraform_state_test.go b/terraformstate/terraform_state_test.go index 2d4c23d..dbea003 100644 --- a/terraformstate/terraform_state_test.go +++ b/terraformstate/terraform_state_test.go @@ -71,12 +71,36 @@ func TestGetAllResourceChanges(t *testing.T) { &ResourceChange{Address: "import1", Change: &Change{Importing: &Importing{ID: "id2"}}}, &ResourceChange{Address: "import2", Change: &Change{Importing: &Importing{ID: "id1"}}}, }, + } + + assert.Equal(t, expectedResourceChanges, result) +} + +func TestGetAllResourceMoves(t *testing.T) { + resourceChanges := ResourceChanges{ + &ResourceChange{Address: "create2", Change: &Change{Actions: Actions{ActionCreate}}}, + &ResourceChange{Address: "create1", Change: &Change{Actions: Actions{ActionCreate}}}, + &ResourceChange{Address: "delete2", Change: &Change{Actions: Actions{ActionDelete}}}, + &ResourceChange{Address: "delete1", Change: &Change{Actions: Actions{ActionDelete}}}, + &ResourceChange{Address: "update2", Change: &Change{Actions: Actions{ActionUpdate}}}, + &ResourceChange{Address: "update1", Change: &Change{Actions: Actions{ActionUpdate}}}, + &ResourceChange{Address: "import2", Change: &Change{Importing: &Importing{ID: "id1"}}}, + &ResourceChange{Address: "import1", Change: &Change{Importing: &Importing{ID: "id2"}}}, + &ResourceChange{Address: "move1", PreviousAddress: "move", Change: &Change{Actions: Actions{}}}, + &ResourceChange{Address: "recreate2", Change: &Change{Actions: Actions{ActionDelete, ActionCreate}}}, + &ResourceChange{Address: "recreate1", Change: &Change{Actions: Actions{ActionDelete, ActionCreate}}}, + } + plan := tfjson.Plan{ResourceChanges: resourceChanges} + + result := GetAllResourceMoves(plan) + + expectedResourceMoves := map[string]ResourceChanges{ "moved": { &ResourceChange{Address: "move1", PreviousAddress: "move", Change: &Change{Actions: Actions{}}}, }, } - assert.Equal(t, expectedResourceChanges, result) + assert.Equal(t, expectedResourceMoves, result) } func TestGetAllOutputChanges(t *testing.T) {