Skip to content

Commit

Permalink
support -html flag to render output as HTML
Browse files Browse the repository at this point in the history
This addresses issue #69 to support HTML output.

This also...

- updates the README usage docs
- validates that multiple, conflicting format flags are not provided
- fixes a bug in the tests to properly validate expected tf-summarize errors

Signed-off-by: Mike Ball <mikedball@gmail.com>
  • Loading branch information
mdb committed Mar 2, 2024
1 parent 2c17fcc commit 36f9ccf
Show file tree
Hide file tree
Showing 11 changed files with 183 additions and 14 deletions.
8 changes: 7 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -110,10 +110,16 @@ Usage of tf-summarize [args] [tf-plan.json|tfplan]
-draw
[Optional, used only with -tree or -separate-tree] draw trees instead of plain tree
-html
[Optional] print changes in html format
-json
[Optional] print changes in json format
-md
[Optional, used only with table view] output table as markdown
-out string
[Optional] write output to file
-separate-tree
[Optional] print changes in tree format for each add/delete/change/recreate changes
[Optional] print changes in tree format for add/delete/change/recreate changes
-tree
[Optional] print changes in tree format
-v print version
Expand Down
22 changes: 19 additions & 3 deletions main.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ func main() {
printVersion := flag.Bool("v", false, "print version")
tree := flag.Bool("tree", false, "[Optional] print changes in tree format")
json := flag.Bool("json", false, "[Optional] print changes in json format")
html := flag.Bool("html", false, "[Optional] print changes in html format")
separateTree := flag.Bool("separate-tree", false, "[Optional] print changes in tree format for add/delete/change/recreate changes")
drawable := flag.Bool("draw", false, "[Optional, used only with -tree or -separate-tree] draw trees instead of plain tree")
md := flag.Bool("md", false, "[Optional, used only with table view] output table as markdown")
Expand All @@ -35,7 +36,7 @@ func main() {
}

args := flag.Args()
err := validateFlags(*tree, *separateTree, *drawable, *md, args)
err := validateFlags(*tree, *separateTree, *drawable, *md, *json, *html, args)
logIfErrorAndExit("invalid input flags: %s\n", err, flag.Usage)

newReader, err := reader.CreateReader(args)
Expand All @@ -52,7 +53,7 @@ func main() {

terraformstate.FilterNoOpResources(&terraformState)

newWriter := writer.CreateWriter(*tree, *separateTree, *drawable, *md, *json, terraformState)
newWriter := writer.CreateWriter(*tree, *separateTree, *drawable, *md, *json, *html, terraformState)

var outputFile io.Writer = os.Stdout

Expand Down Expand Up @@ -83,7 +84,7 @@ func logIfErrorAndExit(format string, err error, callback func()) {
}
}

func validateFlags(tree, separateTree, drawable bool, md bool, args []string) error {
func validateFlags(tree, separateTree, drawable bool, md bool, json bool, html bool, args []string) error {
if tree && md {
return fmt.Errorf("both -tree and -md should not be provided")
}
Expand All @@ -96,8 +97,23 @@ func validateFlags(tree, separateTree, drawable bool, md bool, args []string) er
if !tree && !separateTree && drawable {
return fmt.Errorf("drawable should be provided with -tree or -seperate-tree")
}
if multipleTrueVals(md, json, html) {
return fmt.Errorf("only one of -md, -json, or -html should be provided")
}
if len(args) > 1 {
return fmt.Errorf("only one argument is allowed which is filename, but got %v", args)
}
return nil
}

func multipleTrueVals(vals ...bool) bool {
v := []bool{}

for _, val := range vals {
if val {
v = append(v, val)
}
}

return len(v) > 1
}
21 changes: 14 additions & 7 deletions main_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -61,13 +61,20 @@ func TestTFSummarize(t *testing.T) {
}, {
command: fmt.Sprintf("cat example/tfplan.json | ./%s -md", testExecutable),
expectedOutput: "basic.txt",
}, {
command: fmt.Sprintf("cat example/tfplan.json | ./%s -html", testExecutable),
expectedOutput: "basic.html",
}, {
command: fmt.Sprintf("cat example/tfplan.json | ./%s -md -html", testExecutable),
expectedError: fmt.Errorf("exit status 1"),
expectedOutput: "multiple_format_flags_error.txt",
}}

for _, test := range tests {
t.Run(fmt.Sprintf("when tf-summarize is passed '%q'", test.command), func(t *testing.T) {
output, err := exec.Command("/bin/sh", "-c", test.command).CombinedOutput()
if err != nil && test.expectedError == nil {
t.Errorf("expected '%s' not to error; got '%v'", test.command, err)
output, cmdErr := exec.Command("/bin/sh", "-c", test.command).CombinedOutput()
if cmdErr != nil && test.expectedError == nil {
t.Errorf("expected '%s' not to error; got '%v'", test.command, cmdErr)
}

b, err := os.ReadFile(fmt.Sprintf("testdata/%s", test.expectedOutput))
Expand All @@ -77,12 +84,12 @@ func TestTFSummarize(t *testing.T) {

expected := string(b)

if test.expectedError != nil && err == nil {
t.Errorf("expected error '%s'; got '%v'", test.expectedError.Error(), err)
if test.expectedError != nil && cmdErr == nil {
t.Errorf("expected error '%s'; got '%v'", test.expectedError.Error(), cmdErr)
}

if test.expectedError != nil && err != nil && test.expectedError.Error() != err.Error() {
t.Errorf("expected error '%s'; got '%v'", test.expectedError.Error(), err.Error())
if test.expectedError != nil && cmdErr != nil && test.expectedError.Error() != cmdErr.Error() {
t.Errorf("expected error '%s'; got '%v'", test.expectedError.Error(), cmdErr.Error())
}

if string(output) != expected {
Expand Down
20 changes: 20 additions & 0 deletions testdata/basic.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
<table>
<tr>
<th>CHANGE</th>
<th>RESOURCE</th>
</tr>
<tr>
<td>add</td>
<td>
<ul>
<li>github_repository.terraform_plan_summary</li>
<li>module.github["demo-repository"].github_branch.development</li>
<li>module.github["demo-repository"].github_branch.main</li>
<li>module.github["demo-repository"].github_repository.repository</li>
<li>module.github["terraform-plan-summary"].github_branch.development</li>
<li>module.github["terraform-plan-summary"].github_branch.main</li>
<li>module.github["terraform-plan-summary"].github_repository.repository</li>
</ul>
</td>
</tr>
</table>
19 changes: 19 additions & 0 deletions testdata/multiple_format_flags_error.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
invalid input flags: only one of -md, -json, or -html should be provided

Usage of ./tf-summarize-test [args] [tf-plan.json|tfplan]

-draw
[Optional, used only with -tree or -separate-tree] draw trees instead of plain tree
-html
[Optional] print changes in html format
-json
[Optional] print changes in json format
-md
[Optional, used only with table view] output table as markdown
-out string
[Optional] write output to file
-separate-tree
[Optional] print changes in tree format for add/delete/change/recreate changes
-tree
[Optional] print changes in tree format
-v print version
50 changes: 50 additions & 0 deletions writer/html.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
package writer

import (
"io"
"path"
"text/template"

"github.com/dineshba/tf-summarize/terraformstate"
)

// HTMLWriter is a Writer that writes HTML.
type HTMLWriter struct {
ResourceChanges map[string]terraformstate.ResourceChanges
OutputChanges map[string][]string
}

// 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(templates, path.Join(templatesDir, rcTmpl))
if err != nil {
return err
}

err = tmpl.Execute(writer, t)
if err != nil {
return err
}

if !hasOutputChanges(t.OutputChanges) {
return nil
}

ocTmpl := "outputChanges.html"
outputTmpl, err := template.New(ocTmpl).ParseFS(templates, path.Join(templatesDir, ocTmpl))
if err != nil {
return err
}

return outputTmpl.Execute(writer, t)
}

// NewHTMLWriter returns a new HTMLWriter with the configuration it's passed.
func NewHTMLWriter(changes map[string]terraformstate.ResourceChanges, outputChanges map[string][]string) Writer {
return HTMLWriter{
ResourceChanges: changes,
OutputChanges: outputChanges,
}
}
2 changes: 1 addition & 1 deletion writer/table.go
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,7 @@ func (t TableWriter) Write(writer io.Writer) error {
table.Render()

// Disable the Output Summary if there are no outputs to display
if len(t.outputChanges["add"]) > 0 || len(t.outputChanges["delete"]) > 0 || len(t.outputChanges["update"]) > 0 {
if hasOutputChanges(t.outputChanges) {
tableString = make([][]string, 0, 4)
for _, change := range tableOrder {
changedOutputs := t.outputChanges[change]
Expand Down
14 changes: 14 additions & 0 deletions writer/templates/outputChanges.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
<table>
<tr>
<th>CHANGE</th>
<th>OUTPUT</th>
</tr>{{ range $change, $resources := .OutputChanges }}{{ $length := len $resources }}{{ if gt $length 0 }}
<tr>
<td>{{ $change }}</td>
<td>
<ul>{{ range $i, $r := $resources }}
<li>{{ $r.Address }}</li>{{ end }}
</ul>
</td>
</tr>{{ end }}{{ end }}
</table>
14 changes: 14 additions & 0 deletions writer/templates/resourceChanges.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
<table>
<tr>
<th>CHANGE</th>
<th>RESOURCE</th>
</tr>{{ range $change, $resources := .ResourceChanges }}{{ $length := len $resources }}{{ if gt $length 0 }}
<tr>
<td>{{ $change }}</td>
<td>
<ul>{{ range $i, $r := $resources }}
<li>{{ $r.Address }}</li>{{ end }}
</ul>
</td>
</tr>{{ end }}{{ end }}
</table>
21 changes: 21 additions & 0 deletions writer/util.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
package writer

import "embed"

// Embed the templates directory in the compiled binary.
//
//go:embed templates
var templates embed.FS

func hasOutputChanges(opChanges map[string][]string) bool {
hasChanges := false

for _, v := range opChanges {
if len(v) > 0 {
hasChanges = true
break
}
}

return hasChanges
}
6 changes: 4 additions & 2 deletions writer/writer.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,8 +11,7 @@ type Writer interface {
Write(writer io.Writer) error
}

func CreateWriter(tree, separateTree, drawable, mdEnabled, json bool, plan tfjson.Plan) Writer {

func CreateWriter(tree, separateTree, drawable, mdEnabled, json, html bool, plan tfjson.Plan) Writer {
if tree {
return NewTreeWriter(plan.ResourceChanges, drawable)
}
Expand All @@ -22,6 +21,9 @@ func CreateWriter(tree, separateTree, drawable, mdEnabled, json bool, plan tfjso
if json {
return NewJSONWriter(plan.ResourceChanges)
}
if html {
return NewHTMLWriter(terraformstate.GetAllResourceChanges(plan), terraformstate.GetAllOutputChanges(plan))
}

return NewTableWriter(terraformstate.GetAllResourceChanges(plan), terraformstate.GetAllOutputChanges(plan), mdEnabled)
}

0 comments on commit 36f9ccf

Please sign in to comment.