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..d7c50c9 100644 --- a/main_test.go +++ b/main_test.go @@ -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)) @@ -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 { 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/testdata/multiple_format_flags_error.txt b/testdata/multiple_format_flags_error.txt new file mode 100644 index 0000000..4556cf1 --- /dev/null +++ b/testdata/multiple_format_flags_error.txt @@ -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 diff --git a/writer/html.go b/writer/html.go new file mode 100644 index 0000000..66ee3b5 --- /dev/null +++ b/writer/html.go @@ -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, + } +} 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..fa08542 --- /dev/null +++ b/writer/util.go @@ -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 +} 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) }