diff --git a/terraformstate/terraform_state.go b/terraformstate/terraform_state.go index 9f67da0..557bd9d 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 { @@ -129,6 +140,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/terraformstate/terraform_state_test.go b/terraformstate/terraform_state_test.go index 00caaa7..dbea003 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}}}, } @@ -75,6 +76,33 @@ func TestGetAllResourceChanges(t *testing.T) { 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, expectedResourceMoves, result) +} + func TestGetAllOutputChanges(t *testing.T) { outputChanges := map[string]*Change{ diff --git a/writer/html.go b/writer/html.go index c07c206..cc10fe3 100644 --- a/writer/html.go +++ b/writer/html.go @@ -11,16 +11,15 @@ 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 } -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 } @@ -33,9 +32,8 @@ func (t HTMLWriter) Write(writer io.Writer) error { if !hasOutputChanges(t.OutputChanges) { return nil } - 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 } @@ -44,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 64d704e..9a7ea50 100644 --- a/writer/html_test.go +++ b/writer/html_test.go @@ -3,56 +3,14 @@ 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": { + "update": { { Address: "aws_instance.example", Name: "example", @@ -64,11 +22,23 @@ func TestHTMLWriterWithMockFileSystem(t *testing.T) { }, }, } + movedResources := map[string]terraformstate.ResourceChanges{ + "moved": { + { + Address: "aws_instance.foo", + PreviousAddress: "aws_instance.bar", + Name: "foo", + Change: &Change{ + Actions: Actions{}, + }, + }, + }, + } outputChanges := map[string][]string{ "output_key": {"output_value"}, } - htmlWriter := NewHTMLWriter(resourceChanges, outputChanges) + htmlWriter := NewHTMLWriter(resourceChanges, movedResources, outputChanges) var buf bytes.Buffer err := htmlWriter.Write(&buf) @@ -82,13 +52,21 @@ func TestHTMLWriterWithMockFileSystem(t *testing.T) { RESOURCE - module.test + update + + moved + + + + @@ -106,7 +84,7 @@ func TestHTMLWriterWithMockFileSystem(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/table.go b/writer/table.go index 8179a90..405577f 100644 --- a/writer/table.go +++ b/writer/table.go @@ -11,6 +11,7 @@ import ( type TableWriter struct { mdEnabled bool changes map[string]terraformstate.ResourceChanges + moves map[string]terraformstate.ResourceChanges outputChanges map[string][]string } @@ -18,8 +19,10 @@ 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 { tableString = append(tableString, []string{change, fmt.Sprintf("`%s`", changedResource.Address)}) @@ -29,6 +32,16 @@ func (t TableWriter) Write(writer io.Writer) error { } } + 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)}) + } + } + } + table := tablewriter.NewWriter(writer) table.SetHeader([]string{"Change", "Resource"}) table.SetAutoMergeCells(true) @@ -80,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 65043d7..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) @@ -47,6 +57,8 @@ func TestTableWriter_Write_NoMarkdown(t *testing.T) { +--------+--------------------------------------------------+ | delete | aws_instance.example2 | +--------+--------------------------------------------------+ +| moved | aws_instance.old to aws_instance.new | ++--------+--------------------------------------------------+ +--------+--------------------------------------------------------+ | CHANGE | OUTPUT | +--------+--------------------------------------------------------+ @@ -62,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", @@ -69,15 +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 | -|--------|-------------------------| -| add | ` + "`aws_instance.example1`" + ` | -| delete | ` + "`aws_instance.example2`" + ` | + expectedOutput := `| CHANGE | RESOURCE | +|--------|------------------------------------------| +| add | ` + "`aws_instance.example1`" + ` | +| delete | ` + "`aws_instance.example2`" + ` | +| moved | ` + "`aws_instance.old` to `aws_instance.new`" + ` | | CHANGE | OUTPUT | |--------|----------------------------------------------------------| @@ -90,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/templates/resourceChanges.html b/writer/templates/resourceChanges.html index 8f4f4ab..a05c422 100644 --- a/writer/templates/resourceChanges.html +++ b/writer/templates/resourceChanges.html @@ -3,11 +3,20 @@ CHANGE RESOURCE {{ range $change, $resources := .ResourceChanges }}{{ $length := len $resources }}{{ if gt $length 0 }} + + {{ $change }} + + + + {{ end }}{{ end }}{{ range $change, $resources := .MovedResources }}{{ $length := len $resources }}{{ if gt $length 0 }} {{ $change }} {{ 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 diff --git a/writer/writer.go b/writer/writer.go index d22fa51..4417302 100644 --- a/writer/writer.go +++ b/writer/writer.go @@ -22,11 +22,11 @@ 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)) } - return NewTableWriter(terraformstate.GetAllResourceChanges(plan), terraformstate.GetAllOutputChanges(plan), mdEnabled) + return NewTableWriter(terraformstate.GetAllResourceChanges(plan), terraformstate.GetAllResourceMoves(plan), terraformstate.GetAllOutputChanges(plan), mdEnabled) }