From 2a43698fe734148757316473c03ba38b6af41ebe Mon Sep 17 00:00:00 2001 From: Mike Ball Date: Fri, 1 Mar 2024 12:07:05 -0800 Subject: [PATCH] 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 Signed-off-by: Mike Ball --- README.md | 8 +++- main.go | 22 +++++++++-- main_test.go | 3 ++ testdata/basic.html | 20 ++++++++++ writer/html.go | 55 +++++++++++++++++++++++++++ writer/table.go | 2 +- writer/templates/outputChanges.html | 14 +++++++ writer/templates/resourceChanges.html | 14 +++++++ writer/util.go | 14 +++++++ writer/writer.go | 6 ++- 10 files changed, 151 insertions(+), 7 deletions(-) create mode 100644 testdata/basic.html create mode 100644 writer/html.go create mode 100644 writer/templates/outputChanges.html create mode 100644 writer/templates/resourceChanges.html create mode 100644 writer/util.go diff --git a/README.md b/README.md index 9402cfe..b141845 100644 --- a/README.md +++ b/README.md @@ -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 diff --git a/main.go b/main.go index 837c137..f517699 100644 --- a/main.go +++ b/main.go @@ -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") @@ -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) @@ -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 @@ -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") } @@ -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 +} diff --git a/main_test.go b/main_test.go index 80ec18a..1b614f9 100644 --- a/main_test.go +++ b/main_test.go @@ -61,6 +61,9 @@ 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", }} for _, test := range tests { diff --git a/testdata/basic.html b/testdata/basic.html new file mode 100644 index 0000000..f742584 --- /dev/null +++ b/testdata/basic.html @@ -0,0 +1,20 @@ + + + + + + + + + +
CHANGERESOURCE
add +
    +
  • github_repository.terraform_plan_summary
  • +
  • module.github["demo-repository"].github_branch.development
  • +
  • module.github["demo-repository"].github_branch.main
  • +
  • module.github["demo-repository"].github_repository.repository
  • +
  • module.github["terraform-plan-summary"].github_branch.development
  • +
  • module.github["terraform-plan-summary"].github_branch.main
  • +
  • module.github["terraform-plan-summary"].github_repository.repository
  • +
+
diff --git a/writer/html.go b/writer/html.go new file mode 100644 index 0000000..4ce022d --- /dev/null +++ b/writer/html.go @@ -0,0 +1,55 @@ +package writer + +import ( + "embed" + "io" + "path" + "text/template" + + "github.com/dineshba/tf-summarize/terraformstate" +) + +type HTMLWriter struct { + ResourceChanges map[string]terraformstate.ResourceChanges + OutputChanges map[string][]string +} + +// Embed the entire templates directory in the compiled binary. +// +//go:embed templates +var templates embed.FS + +// 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, + } +} diff --git a/writer/table.go b/writer/table.go index 05b0adf..33c2b0d 100644 --- a/writer/table.go +++ b/writer/table.go @@ -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] diff --git a/writer/templates/outputChanges.html b/writer/templates/outputChanges.html new file mode 100644 index 0000000..0ed7df1 --- /dev/null +++ b/writer/templates/outputChanges.html @@ -0,0 +1,14 @@ + + + + + {{ range $change, $resources := .OutputChanges }}{{ $length := len $resources }}{{ if gt $length 0 }} + + + + {{ end }}{{ end }} +
CHANGEOUTPUT
{{ $change }} +
    {{ range $i, $r := $resources }} +
  • {{ $r.Address }}
  • {{ end }} +
+
diff --git a/writer/templates/resourceChanges.html b/writer/templates/resourceChanges.html new file mode 100644 index 0000000..52981a9 --- /dev/null +++ b/writer/templates/resourceChanges.html @@ -0,0 +1,14 @@ + + + + + {{ range $change, $resources := .ResourceChanges }}{{ $length := len $resources }}{{ if gt $length 0 }} + + + + {{ end }}{{ end }} +
CHANGERESOURCE
{{ $change }} +
    {{ range $i, $r := $resources }} +
  • {{ $r.Address }}
  • {{ end }} +
+
diff --git a/writer/util.go b/writer/util.go new file mode 100644 index 0000000..db9b8a5 --- /dev/null +++ b/writer/util.go @@ -0,0 +1,14 @@ +package writer + +func hasOutputChanges(opChanges map[string][]string) bool { + hasChanges := false + + for _, v := range opChanges { + if len(v) > 0 { + hasChanges = true + break + } + } + + return hasChanges +} diff --git a/writer/writer.go b/writer/writer.go index dd9d0a7..383bf13 100644 --- a/writer/writer.go +++ b/writer/writer.go @@ -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) } @@ -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) }