Skip to content

Commit

Permalink
support -html flag to render output as HTML (#70)
Browse files Browse the repository at this point in the history
* support `-html` flag to render output as HTML

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>

* improve tests to exercise outputs

Previously, the tf-summarize tests did not exercise the tool's ability
to summarize plan outputs. This fixes that!

* ensure 'make help' outputs details on all targets

By adding a code comment in association with all Make targets, `make
help` now documents all targets:

```
$ make help
build                          build the binary
example                        generate example Terraform plan
help                           prints help (only for tasks with comment)
i                              build and install to /usr/local/bin/
install                        build and install to /usr/local/bin/
lint                           lint source code
test                           go test
```

* put HTML resources in <code> within summary

This makes the HTML output a bit more consistent with output elsewhere.

* protecting against stale tests runs

This protects against false positives running the tests.

---------

Signed-off-by: Mike Ball <mikedball@gmail.com>
  • Loading branch information
mdb authored Mar 9, 2024
1 parent 2c17fcc commit c447ded
Show file tree
Hide file tree
Showing 17 changed files with 231 additions and 34 deletions.
36 changes: 18 additions & 18 deletions Makefile
Original file line number Diff line number Diff line change
@@ -1,5 +1,19 @@
TERRAFORM_VERSION:=$(shell cat example/.terraform-version)

define generate-example
docker run \
--interactive \
--tty \
--volume $(shell pwd):/src \
--workdir /src/example \
--entrypoint /bin/sh \
hashicorp/terraform:$(1) \
-c \
"terraform init && \
terraform plan -out tfplan && \
terraform show -json tfplan > tfplan.json"
endef

.PHONY: help
help: ## prints help (only for tasks with comment)
@grep -E '^[a-zA-Z._-]+:.*?## .*$$' $(MAKEFILE_LIST) | sort | awk 'BEGIN {FS = ":.*?## "}; {printf "\033[36m%-30s\033[0m %s\n", $$1, $$2}'
Expand All @@ -13,28 +27,14 @@ build: ## build the binary
install: build ## build and install to /usr/local/bin/
cp $(EXECUTABLE_NAME) /usr/local/bin/$(EXECUTABLE_NAME)

test: lint
go test ./...
test: lint ## go test
go test ./... -count=1

i: install ## build and install to /usr/local/bin/

lint:
lint: ## lint source code
golangci-lint run --timeout 10m -v

define generate-example
docker run \
--interactive \
--tty \
--volume $(shell pwd):/src \
--workdir /src/example \
--entrypoint /bin/sh \
hashicorp/terraform:$(1) \
-c \
"terraform init && \
terraform plan -out tfplan && \
terraform show -json tfplan > tfplan.json"
endef

example:
example: ## generate example Terraform plan
$(call generate-example,$(TERRAFORM_VERSION))
.PHONY: example
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
2 changes: 2 additions & 0 deletions example/.terraform.lock.hcl

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

6 changes: 5 additions & 1 deletion example/github/main.tf
Original file line number Diff line number Diff line change
Expand Up @@ -20,4 +20,8 @@ resource "github_branch" "main" {
resource "github_branch" "development" {
repository = github_repository.repository.name
branch = "development"
}
}

output "repository_name" {
value = github_repository.repository.name
}
4 changes: 4 additions & 0 deletions example/main.tf
Original file line number Diff line number Diff line change
Expand Up @@ -58,3 +58,7 @@ resource "github_repository" "terraform_plan_summary" {
has_wiki = true
vulnerability_alerts = false
}

output "terraform_plan_summary_repository_name" {
value = github_repository.terraform_plan_summary.name
}
2 changes: 1 addition & 1 deletion example/tfplan.json

Large diffs are not rendered by default.

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
34 changes: 34 additions & 0 deletions testdata/basic.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
<table>
<tr>
<th>CHANGE</th>
<th>RESOURCE</th>
</tr>
<tr>
<td>add</td>
<td>
<ul>
<li><code>github_repository.terraform_plan_summary</code></li>
<li><code>module.github["demo-repository"].github_branch.development</code></li>
<li><code>module.github["demo-repository"].github_branch.main</code></li>
<li><code>module.github["demo-repository"].github_repository.repository</code></li>
<li><code>module.github["terraform-plan-summary"].github_branch.development</code></li>
<li><code>module.github["terraform-plan-summary"].github_branch.main</code></li>
<li><code>module.github["terraform-plan-summary"].github_repository.repository</code></li>
</ul>
</td>
</tr>
</table>
<table>
<tr>
<th>CHANGE</th>
<th>OUTPUT</th>
</tr>
<tr>
<td>add</td>
<td>
<ul>
<li><code>terraform_plan_summary_repository_name</code></li>
</ul>
</td>
</tr>
</table>
4 changes: 4 additions & 0 deletions testdata/basic.txt
Original file line number Diff line number Diff line change
Expand Up @@ -7,3 +7,7 @@
| | `module.github["terraform-plan-summary"].github_branch.development` |
| | `module.github["terraform-plan-summary"].github_branch.main` |
| | `module.github["terraform-plan-summary"].github_repository.repository` |

| CHANGE | OUTPUT |
|--------|------------------------------------------|
| add | `terraform_plan_summary_repository_name` |
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, $outputs := .OutputChanges }}{{ $length := len $outputs }}{{ if gt $length 0 }}
<tr>
<td>{{ $change }}</td>
<td>
<ul>{{ range $i, $o := $outputs }}
<li><code>{{ $o }}</code></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><code>{{ $r.Address }}</code></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
}
Loading

0 comments on commit c447ded

Please sign in to comment.