From fae5afb381de27ed3c935e91c320e25f0c960290 Mon Sep 17 00:00:00 2001 From: Tom Morelly Date: Thu, 1 Aug 2024 10:24:52 +1000 Subject: [PATCH 01/10] chore(make): add help and fmt target --- Makefile | 20 ++++++++++++++------ 1 file changed, 14 insertions(+), 6 deletions(-) diff --git a/Makefile b/Makefile index e5811d6..0d5bd21 100644 --- a/Makefile +++ b/Makefile @@ -2,22 +2,30 @@ BINARY_NAME=terramaid VERSION=v1 GO=go +default: help + +help: ## list makefile targets + @grep -E '^[a-zA-Z_-]+:.*?## .*$$' $(MAKEFILE_LIST) | sort | awk 'BEGIN {FS = ":.*?## "}; {printf "\033[36m%-30s\033[0m %s\n", $$1, $$2}' + all: build -build: +fmt: ## format go files + gofumpt -w . + +build: ## build terramaid $(GO) build -ldflags="-s -w" -o build/$(BINARY_NAME) main.go -install: +install: ## install deps $(GO) install ./...@latest -clean: +clean: ## clean up build artifacts $(GO) clean rm ./build/$(BINARY_NAME) -run: build +run: build ## run ./build/$(BINARY_NAME) -docs: build +docs: build ## docs gen ./build/$(BINARY_NAME) docs -.PHONY: all build install clean run \ No newline at end of file +.PHONY: all build install clean run fmt help \ No newline at end of file From ad96a4b8ecc6a248b04627bea624662549731b9e Mon Sep 17 00:00:00 2001 From: Tom Morelly Date: Thu, 1 Aug 2024 10:25:17 +1000 Subject: [PATCH 02/10] feat(envs): add configuration via env vars --- cmd/docs.go | 31 +++++++------ cmd/root.go | 122 ++++++++++++++++++++++++++++--------------------- cmd/version.go | 40 ++++++++-------- go.mod | 1 + go.sum | 2 + main.go | 15 ++++-- 6 files changed, 119 insertions(+), 92 deletions(-) diff --git a/cmd/docs.go b/cmd/docs.go index 6d3c1fe..832dff5 100644 --- a/cmd/docs.go +++ b/cmd/docs.go @@ -1,24 +1,25 @@ package cmd import ( - "log" - "github.com/spf13/cobra" "github.com/spf13/cobra/doc" ) -var docsCmd = &cobra.Command{ - Use: "docs", - Short: "Generate documentation for the CLI", - SilenceUsage: true, - Run: func(cmd *cobra.Command, args []string) { - err := doc.GenMarkdownTree(RootCmd, "./docs") - if err != nil { - log.Fatal(err) - } - }, -} +func docsCmd() *cobra.Command { + cmd := &cobra.Command{ + Use: "docs", + Short: "Generate documentation for the CLI", + SilenceUsage: true, + Hidden: true, + RunE: func(cmd *cobra.Command, args []string) error { + err := doc.GenMarkdownTree(cmd.Root(), "./docs") + if err != nil { + return err + } + + return nil + }, + } -func init() { - RootCmd.AddCommand(docsCmd) + return cmd } diff --git a/cmd/root.go b/cmd/root.go index 909cdf0..f02be1b 100644 --- a/cmd/root.go +++ b/cmd/root.go @@ -2,70 +2,88 @@ package cmd import ( "fmt" + "log" "os" "os/exec" "github.com/RoseSecurity/terramaid/internal" + "github.com/caarlos0/env/v11" "github.com/spf13/cobra" ) -var ( - workingDir string - tfDir string - tfPlan string - tfBinary string - output string - direction string - subgraphName string -) +var Version = "0.0.1" +type opts struct { + WorkingDir string `env:"WORKING_DIR" envDefault:"."` + TFDir string `env:"TF_DIR" envDefault:"."` + TFPlan string `env:"TF_PLAN"` + TFBinary string `env:"TF_BINARY"` + Output string `env:"OUTPUT" envDefault:"Terramaid.md"` + Direction string `env:"DIRECTION" envDefault:"TD"` + SubgraphName string `env:"SUBGRAPH_NAME" envDefault:"Terraform"` +} + +func TerramaidCmd() *cobra.Command { + opts := &opts{} + + // Parse Envs + if err := env.ParseWithOptions(opts, env.Options{Prefix: "TERRAMAID_"}); err != nil { + log.Fatalf("error parsing envs: %w", err) + } + + cmd := &cobra.Command{ + Use: "terramaid", + Short: "A utility for generating Mermaid diagrams from Terraform", + SilenceUsage: true, + SilenceErrors: true, + RunE: func(cmd *cobra.Command, args []string) error { + if opts.TFBinary == "" { + tfBinary, err := exec.LookPath("terraform") + if err != nil { + return fmt.Errorf("error finding Terraform binary: %w", err) + } + + opts.TFBinary = tfBinary + } + + graph, err := internal.ParseTerraform(opts.WorkingDir, opts.TFBinary, opts.TFPlan) + if err != nil { + return fmt.Errorf("error parsing Terraform: %w", err) + } -var RootCmd = &cobra.Command{ - Use: "terramaid", - Short: "A utility for generating Mermaid diagrams from Terraform", - SilenceUsage: true, - Run: func(cmd *cobra.Command, args []string) { - var err error - if tfBinary == "" { - tfBinary, err = exec.LookPath("terraform") + // Convert the graph to a Mermaid diagram + mermaidDiagram, err := internal.ConvertToMermaid(graph, opts.Direction, opts.SubgraphName) if err != nil { - fmt.Printf("Error finding Terraform binary: %v\n", err) - os.Exit(1) + return fmt.Errorf("error converting to Mermaid: %w\n", err) + } + + // Write the Mermaid diagram to the specified output file + if err := os.WriteFile(opts.Output, []byte(mermaidDiagram), 0o644); err != nil { + return fmt.Errorf("Error writing to file: %w\n", err) } - } - - graph, err := internal.ParseTerraform(workingDir, tfBinary, tfPlan) - if err != nil { - fmt.Printf("Error parsing Terraform: %v\n", err) - os.Exit(1) - } - - // Convert the graph to a Mermaid diagram - mermaidDiagram, err := internal.ConvertToMermaid(graph, direction, subgraphName) - if err != nil { - fmt.Printf("Error converting to Mermaid: %v\n", err) - os.Exit(1) - } - - // Write the Mermaid diagram to the specified output file - err = os.WriteFile(output, []byte(mermaidDiagram), 0644) - if err != nil { - fmt.Printf("Error writing to file: %v\n", err) - os.Exit(1) - } - fmt.Printf("Mermaid diagram successfully written to %s\n", output) - }, + + fmt.Printf("Mermaid diagram successfully written to %s\n", opts.Output) + + return nil + }, + } + + cmd.Flags().StringVarP(&opts.Output, "output", "o", opts.Output, "Output file for Mermaid diagram (env: TERRAMAID_OUTPUT)") + cmd.Flags().StringVarP(&opts.Direction, "direction", "r", opts.Direction, "Specify the direction of the flowchart (env: TERRAMAID_DIRECTION)") + cmd.Flags().StringVarP(&opts.SubgraphName, "subgraph-name", "s", opts.SubgraphName, "Specify the subgraph name of the flowchart (env: TERRAMAID_SUBGRAPH_NAME)") + cmd.Flags().StringVarP(&opts.TFDir, "tf-dir", "d", opts.TFDir, "Path to Terraform directory (env: TERRAMAID_TF_DIR)") + cmd.Flags().StringVarP(&opts.TFPlan, "tf-plan", "p", opts.TFPlan, "Path to Terraform plan file (env: TERRAMAID_TF_PLAN)") + cmd.Flags().StringVarP(&opts.TFBinary, "tf-binary", "b", opts.TFBinary, "Path to Terraform binary (env: TERRAMAID_TF_BINARY)") + cmd.Flags().StringVarP(&opts.WorkingDir, "working-dir", "w", opts.WorkingDir, "Working directory for Terraform (env: TERRAMAID_WORKING_DIR)") + + cmd.AddCommand(docsCmd(), versionCmd()) + + return cmd } func Execute() error { - return RootCmd.Execute() -} + if err := TerramaidCmd().Execute(); err != nil { + return err + } -func init() { - RootCmd.Flags().StringVarP(&output, "output", "o", "Terramaid.md", "Output file for Mermaid diagram") - RootCmd.Flags().StringVarP(&direction, "direction", "r", "TD", "Specify the direction of the flowchart") - RootCmd.Flags().StringVarP(&subgraphName, "subgraphName", "s", "Terraform", "Specify the subgraph name of the flowchart") - RootCmd.Flags().StringVarP(&tfDir, "tfDir", "d", ".", "Path to Terraform directory") - RootCmd.Flags().StringVarP(&tfPlan, "tfPlan", "p", "", "Path to Terraform plan file") - RootCmd.Flags().StringVarP(&tfBinary, "tfBinary", "b", "", "Path to Terraform binary") - RootCmd.Flags().StringVarP(&workingDir, "workingDir", "w", ".", "Working directory for Terraform") + return nil } diff --git a/cmd/version.go b/cmd/version.go index 635443c..2dcf71e 100644 --- a/cmd/version.go +++ b/cmd/version.go @@ -11,28 +11,30 @@ import ( "github.com/spf13/cobra" ) -var Version = "1.6.3" - type Release struct { TagName string `json:"tag_name"` } -var versionCmd = &cobra.Command{ - Use: "version", - Short: "Print the CLI version", - Long: `This command prints the CLI version`, - Example: "terramaid version", - Run: func(cmd *cobra.Command, args []string) { - fmt.Println("terramaid: " + Version) - latestReleaseTag, err := latestRelease() - if err == nil && latestReleaseTag != "" { - latestRelease := strings.TrimPrefix(latestReleaseTag, "v") - currentRelease := strings.TrimPrefix(Version, "v") - if latestRelease != currentRelease { - updateTerramaid(latestRelease) +func versionCmd() *cobra.Command { + cmd := &cobra.Command{ + Use: "version", + Short: "Print the CLI version", + Long: `This command prints the CLI version`, + Example: "terramaid version", + Run: func(cmd *cobra.Command, args []string) { + fmt.Println("terramaid: " + Version) + latestReleaseTag, err := latestRelease() + if err == nil && latestReleaseTag != "" { + latestRelease := strings.TrimPrefix(latestReleaseTag, "v") + currentRelease := strings.TrimPrefix(Version, "v") + if latestRelease != currentRelease { + updateTerramaid(latestRelease) + } } - } - }, + }, + } + + return cmd } func latestRelease() (string, error) { @@ -60,7 +62,3 @@ func updateTerramaid(latestVersion string) { c1.Println(fmt.Sprintf("\nYour version of Terramaid is out of date. The latest version is %s\n\n", latestVersion)) } - -func init() { - RootCmd.AddCommand(versionCmd) -} diff --git a/go.mod b/go.mod index 2c6fdc7..9a53396 100644 --- a/go.mod +++ b/go.mod @@ -4,6 +4,7 @@ go 1.22.2 require ( github.com/awalterschulze/gographviz v2.0.3+incompatible + github.com/caarlos0/env/v11 v11.1.0 github.com/fatih/color v1.17.0 github.com/hashicorp/terraform-exec v0.21.0 github.com/spf13/cobra v1.8.1 diff --git a/go.sum b/go.sum index 438dffe..d71ef24 100644 --- a/go.sum +++ b/go.sum @@ -8,6 +8,8 @@ github.com/apparentlymart/go-textseg/v15 v15.0.0 h1:uYvfpb3DyLSCGWnctWKGj857c6ew github.com/apparentlymart/go-textseg/v15 v15.0.0/go.mod h1:K8XmNZdhEBkdlyDdvbmmsvpAG721bKi0joRfFdHIWJ4= github.com/awalterschulze/gographviz v2.0.3+incompatible h1:9sVEXJBJLwGX7EQVhLm2elIKCm7P2YHFC8v6096G09E= github.com/awalterschulze/gographviz v2.0.3+incompatible/go.mod h1:GEV5wmg4YquNw7v1kkyoX9etIk8yVmXj+AkDHuuETHs= +github.com/caarlos0/env/v11 v11.1.0 h1:a5qZqieE9ZfzdvbbdhTalRrHT5vu/4V1/ad1Ka6frhI= +github.com/caarlos0/env/v11 v11.1.0/go.mod h1:LwgkYk1kDvfGpHthrWWLof3Ny7PezzFwS4QrsJdHTMo= github.com/cloudflare/circl v1.3.7 h1:qlCDlTPz2n9fu58M0Nh1J/JzcFpfgkFHHX3O35r5vcU= github.com/cloudflare/circl v1.3.7/go.mod h1:sRTcRWXGLrKw6yIGJ+l7amYJFfAXbZG0kBSc8r4zxgA= github.com/cpuguy83/go-md2man/v2 v2.0.4 h1:wfIWP927BUkWJb2NmU/kNDYIBTh/ziUX91+lVfRxZq4= diff --git a/main.go b/main.go index fe044c1..2366fb6 100644 --- a/main.go +++ b/main.go @@ -1,13 +1,20 @@ package main import ( + "fmt" + "os" + "github.com/RoseSecurity/terramaid/cmd" - u "github.com/RoseSecurity/terramaid/pkg/utils" ) +var version string + func main() { - err := cmd.Execute() - if err != nil { - u.LogErrorAndExit(err) + cmd.Version = version + + if err := cmd.Execute(); err != nil { + fmt.Fprintf(os.Stderr, "Error: %s\n", err) + + os.Exit(1) } } From 663520134fb7db37311c18ccd1aed57183609d63 Mon Sep 17 00:00:00 2001 From: Tom Morelly Date: Thu, 1 Aug 2024 10:25:42 +1000 Subject: [PATCH 03/10] feat(goreleaser): inject terramaid version during build --- .goreleaser.yaml | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/.goreleaser.yaml b/.goreleaser.yaml index 7a4bcda..e1a77aa 100644 --- a/.goreleaser.yaml +++ b/.goreleaser.yaml @@ -9,8 +9,7 @@ before: builds: - env: - CGO_ENABLED=0 - ldflags: - - -s -w + ldflags: -s -w -X main.version={{ .Version }} goos: - linux - windows From db7f45d005dee02911c05ef1abe1fceb9f6030ff Mon Sep 17 00:00:00 2001 From: Tom Morelly Date: Thu, 1 Aug 2024 10:25:59 +1000 Subject: [PATCH 04/10] chore(docs): add terramaid usage in README.md --- README.md | 30 ++++++++++++++++++++++++++++++ 1 file changed, 30 insertions(+) diff --git a/README.md b/README.md index f5a4f24..0460b77 100644 --- a/README.md +++ b/README.md @@ -56,6 +56,36 @@ cd terramaid make build ``` +### Usage +`terramaid` can be configured using CLI args and Env vars. While CLI args have precedence over the latter. + + +The following configuration options are available: +```bash +> terramaid -h +A utility for generating Mermaid diagrams from Terraform + +Usage: + terramaid [flags] + terramaid [command] + +Available Commands: + completion Generate the autocompletion script for the specified shell + help Help about any command + version Print the CLI version + +Flags: + -r, --direction string Specify the direction of the flowchart (env: TERRAMAID_DIRECTION) (default "TD") + -h, --help help for terramaid + -o, --output string Output file for Mermaid diagram (env: TERRAMAID_OUTPUT) (default "Terramaid.md") + -s, --subgraph-name string Specify the subgraph name of the flowchart (env: TERRAMAID_SUBGRAPH_NAME) (default "Terraform") + -b, --tf-binary string Path to Terraform binary (env: TERRAMAID_TF_BINARY) + -d, --tf-dir string Path to Terraform directory (env: TERRAMAID_TF_DIR) (default ".") + -p, --tf-plan string Path to Terraform plan file (env: TERRAMAID_TF_PLAN) + -w, --working-wir string Working directory for Terraform (env: TERRAMAID_WORKING_DIR) (default ".") + +Use "terramaid [command] --help" for more information about a command. +``` ### Docker Image Run the following command to utilize the Terramaid Docker image: From c7e5a91d97da18b645857765995d754959ac8518 Mon Sep 17 00:00:00 2001 From: Tom Morelly Date: Fri, 2 Aug 2024 13:55:12 +1000 Subject: [PATCH 05/10] feat(validate): add some validation checks --- cmd/root.go | 31 ++++++++++++++++++++++++------- internal/parse.go | 29 ++++++++++++++++++++--------- main.go | 2 +- pkg/utils/utils.go | 13 +++++++++++++ 4 files changed, 58 insertions(+), 17 deletions(-) create mode 100644 pkg/utils/utils.go diff --git a/cmd/root.go b/cmd/root.go index f02be1b..4bdc258 100644 --- a/cmd/root.go +++ b/cmd/root.go @@ -7,11 +7,13 @@ import ( "os/exec" "github.com/RoseSecurity/terramaid/internal" + "github.com/RoseSecurity/terramaid/pkg/utils" "github.com/caarlos0/env/v11" "github.com/spf13/cobra" ) var Version = "0.0.1" + type opts struct { WorkingDir string `env:"WORKING_DIR" envDefault:"."` TFDir string `env:"TF_DIR" envDefault:"."` @@ -27,15 +29,27 @@ func TerramaidCmd() *cobra.Command { // Parse Envs if err := env.ParseWithOptions(opts, env.Options{Prefix: "TERRAMAID_"}); err != nil { - log.Fatalf("error parsing envs: %w", err) + log.Fatalf("error parsing envs: %s", err.Error()) } cmd := &cobra.Command{ - Use: "terramaid", - Short: "A utility for generating Mermaid diagrams from Terraform", - SilenceUsage: true, + Use: "terramaid", + Short: "A utility for generating Mermaid diagrams from Terraform", + SilenceUsage: true, SilenceErrors: true, - RunE: func(cmd *cobra.Command, args []string) error { + PreRunE: func(cmd *cobra.Command, args []string) error { + if opts.TFDir != "" && !utils.DirExists(opts.TFDir) { + return fmt.Errorf("TF Dir \"%s\" does not exist", opts.TFDir) + } + + if opts.WorkingDir != "" && !utils.DirExists(opts.WorkingDir) { + return fmt.Errorf("working Dir \"%s\" does not exist", opts.WorkingDir) + } + + if opts.TFPlan != "" && !utils.DirExists(opts.TFPlan) { + return fmt.Errorf("TF planfile \"%s\" does not exist", opts.TFPlan) + } + if opts.TFBinary == "" { tfBinary, err := exec.LookPath("terraform") if err != nil { @@ -45,6 +59,9 @@ func TerramaidCmd() *cobra.Command { opts.TFBinary = tfBinary } + return nil + }, + RunE: func(cmd *cobra.Command, args []string) error { graph, err := internal.ParseTerraform(opts.WorkingDir, opts.TFBinary, opts.TFPlan) if err != nil { return fmt.Errorf("error parsing Terraform: %w", err) @@ -53,12 +70,12 @@ func TerramaidCmd() *cobra.Command { // Convert the graph to a Mermaid diagram mermaidDiagram, err := internal.ConvertToMermaid(graph, opts.Direction, opts.SubgraphName) if err != nil { - return fmt.Errorf("error converting to Mermaid: %w\n", err) + return fmt.Errorf("error converting to Mermaid: %w", err) } // Write the Mermaid diagram to the specified output file if err := os.WriteFile(opts.Output, []byte(mermaidDiagram), 0o644); err != nil { - return fmt.Errorf("Error writing to file: %w\n", err) + return fmt.Errorf("error writing to file: %w", err) } fmt.Printf("Mermaid diagram successfully written to %s\n", opts.Output) diff --git a/internal/parse.go b/internal/parse.go index d3db3bb..f87cde2 100644 --- a/internal/parse.go +++ b/internal/parse.go @@ -2,11 +2,20 @@ package internal import ( "context" + "fmt" "github.com/awalterschulze/gographviz" "github.com/hashicorp/terraform-exec/tfexec" ) +const emptyGraph = `digraph G { + rankdir = "RL"; + node [shape = rect, fontname = "sans-serif"]; + /* This configuration does not contain any resources. */ + /* For a more detailed graph, try: terraform graph -type=plan */ +} +` + // ParseTerraform parses the Terraform plan and returns the generated graph func ParseTerraform(workingDir, tfPath, planFile string) (*gographviz.Graph, error) { ctx := context.Background() @@ -15,31 +24,33 @@ func ParseTerraform(workingDir, tfPath, planFile string) (*gographviz.Graph, err return nil, err } - err = tf.Init(ctx, tfexec.Upgrade(true)) - if err != nil { + if err := tf.Init(ctx, tfexec.Upgrade(true)); err != nil { return nil, err } - var output string - // Graph Terraform resources + opts := &tfexec.GraphPlanOption{} + if planFile != "" { - output, err = tf.Graph(ctx, tfexec.GraphPlan(planFile)) - } else { - output, err = tf.Graph(ctx) + opts = tfexec.GraphPlan(planFile) } + output, err := tf.Graph(ctx, opts) if err != nil { return nil, err } + if output == emptyGraph { + return nil, fmt.Errorf("no output from terraform graph") + } + // Parse the DOT output - dot := string(output) - graphAst, err := gographviz.ParseString(dot) + graphAst, err := gographviz.ParseString(string(output)) if err != nil { return nil, err } graph := gographviz.NewGraph() + if err := gographviz.Analyse(graphAst, graph); err != nil { return nil, err } diff --git a/main.go b/main.go index 2366fb6..abc6a7c 100644 --- a/main.go +++ b/main.go @@ -10,7 +10,7 @@ import ( var version string func main() { - cmd.Version = version + cmd.Version = version if err := cmd.Execute(); err != nil { fmt.Fprintf(os.Stderr, "Error: %s\n", err) diff --git a/pkg/utils/utils.go b/pkg/utils/utils.go new file mode 100644 index 0000000..24f1857 --- /dev/null +++ b/pkg/utils/utils.go @@ -0,0 +1,13 @@ +package utils + +import ( + "os" +) + +func DirExists(d string) bool { + if _, err := os.Stat(d); os.IsNotExist(err) { + return false + } + + return true +} From e5dc6cf7f3da9be31e0ef1d93117e77721437c51 Mon Sep 17 00:00:00 2001 From: Tom Morelly Date: Fri, 2 Aug 2024 13:55:25 +1000 Subject: [PATCH 06/10] feat(ci): add e2e test --- .github/workflows/e2e.yml | 22 ++++++++++++++++++++++ 1 file changed, 22 insertions(+) create mode 100644 .github/workflows/e2e.yml diff --git a/.github/workflows/e2e.yml b/.github/workflows/e2e.yml new file mode 100644 index 0000000..355541c --- /dev/null +++ b/.github/workflows/e2e.yml @@ -0,0 +1,22 @@ +name: E2E + +on: + pull_request: + push: + branches: + - main + +jobs: + E2E: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: hashicorp/setup-terraform@v3 + - uses: actions/setup-go@v5 + with: + go-version: '1.22.2' + cache: false + - run: | + make build + terramaid -w test/ + cat Terramaid.md From fba7fd47fbc5ea053886bff84d1e7d4d336d2ab7 Mon Sep 17 00:00:00 2001 From: RoseSecurity Date: Sat, 3 Aug 2024 16:34:09 -0500 Subject: [PATCH 07/10] Update docs, add additional dependendies, prestage for chartType feature --- .github/workflows/{e2e.yml => test.yml} | 4 +- Brewfile | 3 +- Makefile | 16 +++---- README.md | 9 +++- cmd/root.go | 61 ++++++++++++++---------- docs/terramaid_completion.md | 2 +- docs/terramaid_completion_bash.md | 2 +- docs/terramaid_completion_fish.md | 2 +- docs/terramaid_completion_powershell.md | 2 +- docs/terramaid_completion_zsh.md | 2 +- docs/terramaid_version.md | 2 +- main.go | 8 +--- pkg/utils/utils.go | 31 +++++++++++- test/tfplan | Bin 4739 -> 0 bytes 14 files changed, 93 insertions(+), 51 deletions(-) rename .github/workflows/{e2e.yml => test.yml} (95%) delete mode 100644 test/tfplan diff --git a/.github/workflows/e2e.yml b/.github/workflows/test.yml similarity index 95% rename from .github/workflows/e2e.yml rename to .github/workflows/test.yml index 355541c..f18de41 100644 --- a/.github/workflows/e2e.yml +++ b/.github/workflows/test.yml @@ -1,4 +1,4 @@ -name: E2E +name: Test on: pull_request: @@ -7,7 +7,7 @@ on: - main jobs: - E2E: + test: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 diff --git a/Brewfile b/Brewfile index f8415df..6e603c3 100644 --- a/Brewfile +++ b/Brewfile @@ -1 +1,2 @@ -brew "go" \ No newline at end of file +brew "go" +brew "gofumpt" diff --git a/Makefile b/Makefile index 0d5bd21..ce15041 100644 --- a/Makefile +++ b/Makefile @@ -4,28 +4,28 @@ GO=go default: help -help: ## list makefile targets +help: ## List Makefile targets @grep -E '^[a-zA-Z_-]+:.*?## .*$$' $(MAKEFILE_LIST) | sort | awk 'BEGIN {FS = ":.*?## "}; {printf "\033[36m%-30s\033[0m %s\n", $$1, $$2}' all: build -fmt: ## format go files +fmt: ## Format Go files gofumpt -w . -build: ## build terramaid +build: ## Build Terramaid $(GO) build -ldflags="-s -w" -o build/$(BINARY_NAME) main.go -install: ## install deps +install: ## Install dependencies $(GO) install ./...@latest -clean: ## clean up build artifacts +clean: ## Clean up build artifacts $(GO) clean rm ./build/$(BINARY_NAME) -run: build ## run +run: build ## Run Terramaid ./build/$(BINARY_NAME) -docs: build ## docs gen +docs: build ## Generate documentation ./build/$(BINARY_NAME) docs -.PHONY: all build install clean run fmt help \ No newline at end of file +.PHONY: all build install clean run fmt help diff --git a/README.md b/README.md index 0460b77..0409c73 100644 --- a/README.md +++ b/README.md @@ -57,11 +57,15 @@ make build ``` ### Usage -`terramaid` can be configured using CLI args and Env vars. While CLI args have precedence over the latter. +`terramaid` can be configured using CLI parameters and environment variables. + +> [!NOTE] +> CLI parameters take precedence over environment variables. The following configuration options are available: -```bash + +```sh > terramaid -h A utility for generating Mermaid diagrams from Terraform @@ -86,6 +90,7 @@ Flags: Use "terramaid [command] --help" for more information about a command. ``` + ### Docker Image Run the following command to utilize the Terramaid Docker image: diff --git a/cmd/root.go b/cmd/root.go index 4bdc258..848ec05 100644 --- a/cmd/root.go +++ b/cmd/root.go @@ -14,7 +14,7 @@ import ( var Version = "0.0.1" -type opts struct { +type options struct { WorkingDir string `env:"WORKING_DIR" envDefault:"."` TFDir string `env:"TF_DIR" envDefault:"."` TFPlan string `env:"TF_PLAN"` @@ -22,13 +22,14 @@ type opts struct { Output string `env:"OUTPUT" envDefault:"Terramaid.md"` Direction string `env:"DIRECTION" envDefault:"TD"` SubgraphName string `env:"SUBGRAPH_NAME" envDefault:"Terraform"` + chartType string `env:"CHART_TYPE" envDefault:"flowchart"` } func TerramaidCmd() *cobra.Command { - opts := &opts{} + options := &options{} // Parse Envs - if err := env.ParseWithOptions(opts, env.Options{Prefix: "TERRAMAID_"}); err != nil { + if err := env.ParseWithOptions(options, env.Options{Prefix: "TERRAMAID_"}); err != nil { log.Fatalf("error parsing envs: %s", err.Error()) } @@ -38,59 +39,71 @@ func TerramaidCmd() *cobra.Command { SilenceUsage: true, SilenceErrors: true, PreRunE: func(cmd *cobra.Command, args []string) error { - if opts.TFDir != "" && !utils.DirExists(opts.TFDir) { - return fmt.Errorf("TF Dir \"%s\" does not exist", opts.TFDir) + if options.TFDir != "" && !utils.DirExists(options.TFDir) { + return fmt.Errorf("Terraform directory \"%s\" does not exist", options.TFDir) } - if opts.WorkingDir != "" && !utils.DirExists(opts.WorkingDir) { - return fmt.Errorf("working Dir \"%s\" does not exist", opts.WorkingDir) + if options.TFDir != "" && !utils.TerraformFilesExist(options.TFDir) { + return fmt.Errorf("Terraform files do not exist in directory \"%s\"", options.TFDir) } - if opts.TFPlan != "" && !utils.DirExists(opts.TFPlan) { - return fmt.Errorf("TF planfile \"%s\" does not exist", opts.TFPlan) + if options.WorkingDir != "" && !utils.DirExists(options.WorkingDir) { + return fmt.Errorf("Working directory \"%s\" does not exist", options.WorkingDir) } - if opts.TFBinary == "" { + if options.TFPlan != "" && !utils.DirExists(options.TFPlan) { + return fmt.Errorf("Terraform planfile \"%s\" does not exist", options.TFPlan) + } + + if options.TFBinary == "" { tfBinary, err := exec.LookPath("terraform") if err != nil { return fmt.Errorf("error finding Terraform binary: %w", err) } - opts.TFBinary = tfBinary + options.TFBinary = tfBinary } return nil }, RunE: func(cmd *cobra.Command, args []string) error { - graph, err := internal.ParseTerraform(opts.WorkingDir, opts.TFBinary, opts.TFPlan) + graph, err := internal.ParseTerraform(options.WorkingDir, options.TFBinary, options.TFPlan) if err != nil { return fmt.Errorf("error parsing Terraform: %w", err) } // Convert the graph to a Mermaid diagram - mermaidDiagram, err := internal.ConvertToMermaid(graph, opts.Direction, opts.SubgraphName) - if err != nil { - return fmt.Errorf("error converting to Mermaid: %w", err) + switch chartType { + case "flowchart": + mermaidDiagram, err = internal.ConvertToMermaidFlowchart(graph, options.Direction, options.SubgraphName) + if err != nil { + return fmt.Errorf("error converting to Mermaid flowchart: %w", err) + os.Exit(1) + } + default: + fmt.Printf("Unsupported chart type: %s\n", chartType) + os.Exit(1) } // Write the Mermaid diagram to the specified output file - if err := os.WriteFile(opts.Output, []byte(mermaidDiagram), 0o644); err != nil { + if err := os.WriteFile(options.Output, []byte(mermaidDiagram), 0o644); err != nil { return fmt.Errorf("error writing to file: %w", err) } - fmt.Printf("Mermaid diagram successfully written to %s\n", opts.Output) + fmt.Printf("Mermaid diagram successfully written to %s\n", options.Output) return nil }, } - cmd.Flags().StringVarP(&opts.Output, "output", "o", opts.Output, "Output file for Mermaid diagram (env: TERRAMAID_OUTPUT)") - cmd.Flags().StringVarP(&opts.Direction, "direction", "r", opts.Direction, "Specify the direction of the flowchart (env: TERRAMAID_DIRECTION)") - cmd.Flags().StringVarP(&opts.SubgraphName, "subgraph-name", "s", opts.SubgraphName, "Specify the subgraph name of the flowchart (env: TERRAMAID_SUBGRAPH_NAME)") - cmd.Flags().StringVarP(&opts.TFDir, "tf-dir", "d", opts.TFDir, "Path to Terraform directory (env: TERRAMAID_TF_DIR)") - cmd.Flags().StringVarP(&opts.TFPlan, "tf-plan", "p", opts.TFPlan, "Path to Terraform plan file (env: TERRAMAID_TF_PLAN)") - cmd.Flags().StringVarP(&opts.TFBinary, "tf-binary", "b", opts.TFBinary, "Path to Terraform binary (env: TERRAMAID_TF_BINARY)") - cmd.Flags().StringVarP(&opts.WorkingDir, "working-dir", "w", opts.WorkingDir, "Working directory for Terraform (env: TERRAMAID_WORKING_DIR)") + cmd.Flags().StringVarP(&options.Output, "output", "o", options.Output, "Output file for Mermaid diagram (env: TERRAMAID_OUTPUT)") + cmd.Flags().StringVarP(&options.Direction, "direction", "r", options.Direction, "Specify the direction of the diagram (env: TERRAMAID_DIRECTION)") + cmd.Flags().StringVarP(&options.SubgraphName, "subgraph-name", "s", options.SubgraphName, "Specify the subgraph name of the diagram (env: TERRAMAID_SUBGRAPH_NAME)") + cmd.Flags().StringVarP(&options.chartType, "chart-type", "c", options.chartType, "Specify the type of Mermaid chart to generate (env: TERRAMAID_CHART_TYPE)") + cmd.Flags().StringVarP(&options.TFDir, "tf-dir", "d", options.TFDir, "Path to Terraform directory (env: TERRAMAID_TF_DIR)") + cmd.Flags().StringVarP(&options.TFPlan, "tf-plan", "p", options.TFPlan, "Path to Terraform plan file (env: TERRAMAID_TF_PLAN)") + cmd.Flags().StringVarP(&options.TFBinary, "tf-binary", "b", options.TFBinary, "Path to Terraform binary (env: TERRAMAID_TF_BINARY)") + cmd.Flags().StringVarP(&options.WorkingDir, "working-dir", "w", options.WorkingDir, "Working directory for Terraform (env: TERRAMAID_WORKING_DIR)") cmd.AddCommand(docsCmd(), versionCmd()) diff --git a/docs/terramaid_completion.md b/docs/terramaid_completion.md index f4b024c..69a66b8 100644 --- a/docs/terramaid_completion.md +++ b/docs/terramaid_completion.md @@ -22,4 +22,4 @@ See each sub-command's help for details on how to use the generated script. * [terramaid completion powershell](terramaid_completion_powershell.md) - Generate the autocompletion script for powershell * [terramaid completion zsh](terramaid_completion_zsh.md) - Generate the autocompletion script for zsh -###### Auto generated by spf13/cobra on 4-Jul-2024 +###### Auto generated by spf13/cobra on 3-Aug-2024 diff --git a/docs/terramaid_completion_bash.md b/docs/terramaid_completion_bash.md index 161af1b..1c50862 100644 --- a/docs/terramaid_completion_bash.md +++ b/docs/terramaid_completion_bash.md @@ -41,4 +41,4 @@ terramaid completion bash * [terramaid completion](terramaid_completion.md) - Generate the autocompletion script for the specified shell -###### Auto generated by spf13/cobra on 4-Jul-2024 +###### Auto generated by spf13/cobra on 3-Aug-2024 diff --git a/docs/terramaid_completion_fish.md b/docs/terramaid_completion_fish.md index b52faed..9aa518b 100644 --- a/docs/terramaid_completion_fish.md +++ b/docs/terramaid_completion_fish.md @@ -32,4 +32,4 @@ terramaid completion fish [flags] * [terramaid completion](terramaid_completion.md) - Generate the autocompletion script for the specified shell -###### Auto generated by spf13/cobra on 4-Jul-2024 +###### Auto generated by spf13/cobra on 3-Aug-2024 diff --git a/docs/terramaid_completion_powershell.md b/docs/terramaid_completion_powershell.md index b059e83..352574e 100644 --- a/docs/terramaid_completion_powershell.md +++ b/docs/terramaid_completion_powershell.md @@ -29,4 +29,4 @@ terramaid completion powershell [flags] * [terramaid completion](terramaid_completion.md) - Generate the autocompletion script for the specified shell -###### Auto generated by spf13/cobra on 4-Jul-2024 +###### Auto generated by spf13/cobra on 3-Aug-2024 diff --git a/docs/terramaid_completion_zsh.md b/docs/terramaid_completion_zsh.md index 204bc61..d2f328c 100644 --- a/docs/terramaid_completion_zsh.md +++ b/docs/terramaid_completion_zsh.md @@ -43,4 +43,4 @@ terramaid completion zsh [flags] * [terramaid completion](terramaid_completion.md) - Generate the autocompletion script for the specified shell -###### Auto generated by spf13/cobra on 4-Jul-2024 +###### Auto generated by spf13/cobra on 3-Aug-2024 diff --git a/docs/terramaid_version.md b/docs/terramaid_version.md index e791687..716a48a 100644 --- a/docs/terramaid_version.md +++ b/docs/terramaid_version.md @@ -26,4 +26,4 @@ terramaid version * [terramaid](terramaid.md) - A utility for generating Mermaid diagrams from Terraform -###### Auto generated by spf13/cobra on 4-Jul-2024 +###### Auto generated by spf13/cobra on 3-Aug-2024 diff --git a/main.go b/main.go index abc6a7c..2cbd10e 100644 --- a/main.go +++ b/main.go @@ -1,10 +1,8 @@ package main import ( - "fmt" - "os" - "github.com/RoseSecurity/terramaid/cmd" + u "github.com/RoseSecurity/terramaid/pkg/utils" ) var version string @@ -13,8 +11,6 @@ func main() { cmd.Version = version if err := cmd.Execute(); err != nil { - fmt.Fprintf(os.Stderr, "Error: %s\n", err) - - os.Exit(1) + u.LogErrorAndExit(err) } } diff --git a/pkg/utils/utils.go b/pkg/utils/utils.go index 24f1857..a442a89 100644 --- a/pkg/utils/utils.go +++ b/pkg/utils/utils.go @@ -2,12 +2,39 @@ package utils import ( "os" + "path/filepath" ) -func DirExists(d string) bool { - if _, err := os.Stat(d); os.IsNotExist(err) { +// Check if a directory exists +func DirExists(dir string) bool { + if _, err := os.Stat(dir); os.IsNotExist(err) { return false } return true } + +// Check if Terraform files exist in a directory +func TerraformFilesExist(dir string) bool { + validExtensions := []string{".tf", ".tf.json", ".tftest.hcl", ".tftest.json", "terraform.tfvars", "terraform.tfvars.json"} + + found := false + err := filepath.Walk(dir, func(path string, info os.FileInfo, err error) error { + if err != nil { + return err + } + for _, ext := range validExtensions { + if filepath.Ext(path) == ext || filepath.Base(path) == ext { + found = true + return filepath.SkipDir + } + } + return nil + }) + + if err != nil { + return false + } + + return found +} diff --git a/test/tfplan b/test/tfplan deleted file mode 100644 index d21a1c662a09d4554cf2af835fb7818ce9536c37..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 4739 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 Date: Sat, 3 Aug 2024 17:11:43 -0500 Subject: [PATCH 08/10] Add Flowchart functionality --- README.md | 24 ++++---- cmd/root.go | 11 ++-- internal/flowchart.go | 139 ++++++++++++++++++++++++++++++++++++++++++ internal/graph.go | 50 --------------- 4 files changed, 157 insertions(+), 67 deletions(-) create mode 100644 internal/flowchart.go delete mode 100644 internal/graph.go diff --git a/README.md b/README.md index 0409c73..849a957 100644 --- a/README.md +++ b/README.md @@ -21,17 +21,19 @@ Terramaid transforms your Terraform resources and plans into visually appealing ### Output ```mermaid -flowchart TD; - subgraph Terraform - aws_db_instance.dev_example_db_instance["aws_db_instance.dev_example_db_instance"] - aws_instance.dev_example_instance["aws_instance.dev_example_instance"] - aws_s3_bucket.dev_logs_bucket["aws_s3_bucket.dev_logs_bucket"] - aws_s3_bucket.dev_test_bucket["aws_s3_bucket.dev_test_bucket"] - aws_s3_bucket_policy.dev_logs_bucket_policy["aws_s3_bucket_policy.dev_logs_bucket_policy"] - aws_s3_bucket_policy.dev_test_bucket_policy["aws_s3_bucket_policy.dev_test_bucket_policy"] - aws_s3_bucket_policy.dev_logs_bucket_policy --> aws_s3_bucket.dev_logs_bucket - aws_s3_bucket_policy.dev_test_bucket_policy --> aws_s3_bucket.dev_test_bucket - end +flowchart TD + subgraph Hashicorp + subgraph Terraform + aws_db_instance.example_db["aws_db_instance.example_db"] + aws_instance.example_instance["aws_instance.example_instance"] + aws_s3_bucket.logs["aws_s3_bucket.logs"] + aws_s3_bucket.test["aws_s3_bucket.test"] + aws_s3_bucket_policy.logs_policy["aws_s3_bucket_policy.logs_policy"] + aws_s3_bucket_policy.test_policy["aws_s3_bucket_policy.test_policy"] + aws_s3_bucket_policy.logs_policy --> aws_s3_bucket.logs + aws_s3_bucket_policy.test_policy --> aws_s3_bucket.test + end + end ``` ## Installation diff --git a/cmd/root.go b/cmd/root.go index 848ec05..8ca8bfc 100644 --- a/cmd/root.go +++ b/cmd/root.go @@ -22,7 +22,7 @@ type options struct { Output string `env:"OUTPUT" envDefault:"Terramaid.md"` Direction string `env:"DIRECTION" envDefault:"TD"` SubgraphName string `env:"SUBGRAPH_NAME" envDefault:"Terraform"` - chartType string `env:"CHART_TYPE" envDefault:"flowchart"` + ChartType string `env:"CHART_TYPE" envDefault:"flowchart"` } func TerramaidCmd() *cobra.Command { @@ -73,16 +73,15 @@ func TerramaidCmd() *cobra.Command { } // Convert the graph to a Mermaid diagram - switch chartType { + var mermaidDiagram string + switch options.ChartType { case "flowchart": mermaidDiagram, err = internal.ConvertToMermaidFlowchart(graph, options.Direction, options.SubgraphName) if err != nil { return fmt.Errorf("error converting to Mermaid flowchart: %w", err) - os.Exit(1) } default: - fmt.Printf("Unsupported chart type: %s\n", chartType) - os.Exit(1) + return fmt.Errorf("unsupported chart type: %s", options.ChartType) } // Write the Mermaid diagram to the specified output file @@ -99,7 +98,7 @@ func TerramaidCmd() *cobra.Command { cmd.Flags().StringVarP(&options.Output, "output", "o", options.Output, "Output file for Mermaid diagram (env: TERRAMAID_OUTPUT)") cmd.Flags().StringVarP(&options.Direction, "direction", "r", options.Direction, "Specify the direction of the diagram (env: TERRAMAID_DIRECTION)") cmd.Flags().StringVarP(&options.SubgraphName, "subgraph-name", "s", options.SubgraphName, "Specify the subgraph name of the diagram (env: TERRAMAID_SUBGRAPH_NAME)") - cmd.Flags().StringVarP(&options.chartType, "chart-type", "c", options.chartType, "Specify the type of Mermaid chart to generate (env: TERRAMAID_CHART_TYPE)") + cmd.Flags().StringVarP(&options.ChartType, "chart-type", "c", options.ChartType, "Specify the type of Mermaid chart to generate (env: TERRAMAID_CHART_TYPE)") cmd.Flags().StringVarP(&options.TFDir, "tf-dir", "d", options.TFDir, "Path to Terraform directory (env: TERRAMAID_TF_DIR)") cmd.Flags().StringVarP(&options.TFPlan, "tf-plan", "p", options.TFPlan, "Path to Terraform plan file (env: TERRAMAID_TF_PLAN)") cmd.Flags().StringVarP(&options.TFBinary, "tf-binary", "b", options.TFBinary, "Path to Terraform binary (env: TERRAMAID_TF_BINARY)") diff --git a/internal/flowchart.go b/internal/flowchart.go new file mode 100644 index 0000000..d0ceee3 --- /dev/null +++ b/internal/flowchart.go @@ -0,0 +1,139 @@ +package internal + +import ( + "fmt" + "regexp" + "strings" + + "github.com/awalterschulze/gographviz" + "golang.org/x/text/cases" + "golang.org/x/text/language" + +) + +type Node struct { + ID string + Label string +} + +type Edge struct { + From string + To string +} + +type Graph struct { + Nodes []Node + Edges []Edge +} + +// Removes unnecessary parts from the label +func CleanLabel(label string) string { + re := regexp.MustCompile(`\s*\(expand\)|\s*\(close\)|\[root\]\s*|"`) + return re.ReplaceAllString(label, "") +} + +// Removes unnecessary parts from the ID +func CleanID(id string) string { + re := regexp.MustCompile(`\s*\(expand\)|\s*\(close\)|\[root\]\s*|"`) + return re.ReplaceAllString(id, "") +} + +// Extracts the provider for separate subgraph +func ExtractProvider(label string) string { + if strings.Contains(label, "provider") { + parts := strings.Split(label, "/") + if len(parts) > 2 { + return parts[len(parts)-2] + } + } + return "" +} + +// Transforms the parsed graph into cleaned nodes and edges +func TransformGraph(graph *gographviz.Graph) Graph { + nodes := []Node{} + edges := []Edge{} + + for _, node := range graph.Nodes.Nodes { + cleanedID := CleanID(node.Name) + cleanedLabel := CleanLabel(node.Attrs["label"]) + if cleanedLabel != "" && !strings.Contains(cleanedLabel, "provider") { + nodes = append(nodes, Node{ID: cleanedID, Label: cleanedLabel}) + } + } + + for _, edge := range graph.Edges.Edges { + fromLabel := CleanLabel(graph.Nodes.Lookup[edge.Src].Attrs["label"]) + toLabel := CleanLabel(graph.Nodes.Lookup[edge.Dst].Attrs["label"]) + if fromLabel != "" && toLabel != "" && !strings.Contains(fromLabel, "provider") && !strings.Contains(toLabel, "provider") { + edges = append(edges, Edge{From: CleanID(edge.Src), To: CleanID(edge.Dst)}) + } + } + + return Graph{Nodes: nodes, Edges: edges} +} + +// Converts a gographviz graph to a Mermaid.js compatible string. +// It accepts a graph, direction, and an optional subgraph name. +func ConvertToMermaidFlowchart(graph *gographviz.Graph, direction string, subgraphName string) (string, error) { + var sb strings.Builder + + // Capitalize the provider name + caser := cases.Title(language.English) + // Validate the direction of the flowchart. Valid options are: TB, TD, BT, RL, LR + validDirections := map[string]bool{ + "TB": true, "TD": true, "BT": true, "RL": true, "LR": true, + } + if !validDirections[direction] { + return "", fmt.Errorf("invalid direction %s: valid options are: TB, TD, BT, RL, LR", direction) + } + // Start Mermaid graph definition + sb.WriteString("```mermaid\n") + sb.WriteString("flowchart " + direction + "\n") + + // Add subgraph for providers + providerSubgraphs := make(map[string]bool) + for _, n := range graph.Nodes.Nodes { + label := n.Attrs["label"] + provider := ExtractProvider(label) + if provider != "" && !providerSubgraphs[provider] { + sb.WriteString(fmt.Sprintf("\tsubgraph %s\n", caser.String(provider))) + providerSubgraphs[provider] = true + } + } + + if subgraphName != "" { + sb.WriteString(fmt.Sprintf("\tsubgraph %s\n", subgraphName)) + } + + // Iterate over nodes to add them to the Mermaid graph + for _, n := range graph.Nodes.Nodes { + label := CleanLabel(n.Attrs["label"]) + nodeName := CleanID(n.Name) + if label != "" && nodeName != "" && !strings.Contains(label, "provider") { + sb.WriteString(fmt.Sprintf("\t\t%s[\"%s\"]\n", nodeName, label)) + } + } + + // Iterate over edges to add them to the Mermaid graph + for _, edge := range graph.Edges.Edges { + srcLabel := CleanLabel(graph.Nodes.Lookup[edge.Src].Attrs["label"]) + dstLabel := CleanLabel(graph.Nodes.Lookup[edge.Dst].Attrs["label"]) + srcName := CleanID(edge.Src) + dstName := CleanID(edge.Dst) + if srcLabel != "" && dstLabel != "" && !strings.Contains(srcLabel, "provider") && !strings.Contains(dstLabel, "provider") { + sb.WriteString(fmt.Sprintf("\t\t%s --> %s\n", srcName, dstName)) + } + } + + // Close all open subgraphs + for range providerSubgraphs { + sb.WriteString("\tend\n") + } + if subgraphName != "" { + sb.WriteString("\tend\n") + } + + sb.WriteString("```\n") + return sb.String(), nil +} diff --git a/internal/graph.go b/internal/graph.go deleted file mode 100644 index 3b08a63..0000000 --- a/internal/graph.go +++ /dev/null @@ -1,50 +0,0 @@ -package internal - -import ( - "fmt" - "strings" - - "github.com/awalterschulze/gographviz" -) - -// ConvertToMermaid converts a gographviz graph to a Mermaid.js compatible string. -// It accepts a graph, direction, and an optional subgraph name. -func ConvertToMermaid(graph *gographviz.Graph, direction string, subgraphName string) (string, error) { - var sb strings.Builder - - // Validate the direction of the flowchart. Valid options are: TB, TD, BT, RL, LR - validDirections := map[string]bool{ - "TB": true, "TD": true, "BT": true, "RL": true, "LR": true, - } - if !validDirections[direction] { - return "", fmt.Errorf("invalid direction %s: valid options are: TB, TD, BT, RL, LR", direction) - } - - // Start Mermaid graph definition - sb.WriteString("```mermaid\n") - sb.WriteString("flowchart " + direction + ";\n") - if subgraphName != "" { - sb.WriteString(fmt.Sprintf("\tsubgraph %s\n", subgraphName)) - } - - // Iterate over nodes to add them to the Mermaid graph - for _, node := range graph.Nodes.Nodes { - label := strings.Trim(node.Attrs["label"], "\"") - nodeName := strings.Trim(node.Name, "\"") - sb.WriteString(fmt.Sprintf(" %s[\"%s\"]\n", nodeName, label)) - } - - // Iterate over edges to add them to the Mermaid graph - for _, edge := range graph.Edges.Edges { - srcName := strings.Trim(edge.Src, "\"") - dstName := strings.Trim(edge.Dst, "\"") - sb.WriteString(fmt.Sprintf(" %s --> %s\n", srcName, dstName)) - } - - if subgraphName != "" { - sb.WriteString("\tend\n") - } - sb.WriteString("```\n") - - return sb.String(), nil -} From b21d809efce31116cff3f100e60ec2d9d7869720 Mon Sep 17 00:00:00 2001 From: RoseSecurity Date: Sat, 3 Aug 2024 17:13:37 -0500 Subject: [PATCH 09/10] Syntax Update --- internal/flowchart.go | 15 +++++++-------- pkg/utils/utils.go | 1 - 2 files changed, 7 insertions(+), 9 deletions(-) diff --git a/internal/flowchart.go b/internal/flowchart.go index d0ceee3..a0f5375 100644 --- a/internal/flowchart.go +++ b/internal/flowchart.go @@ -7,8 +7,7 @@ import ( "github.com/awalterschulze/gographviz" "golang.org/x/text/cases" - "golang.org/x/text/language" - + "golang.org/x/text/language" ) type Node struct { @@ -90,7 +89,7 @@ func ConvertToMermaidFlowchart(graph *gographviz.Graph, direction string, subgra // Start Mermaid graph definition sb.WriteString("```mermaid\n") sb.WriteString("flowchart " + direction + "\n") - + // Add subgraph for providers providerSubgraphs := make(map[string]bool) for _, n := range graph.Nodes.Nodes { @@ -101,11 +100,11 @@ func ConvertToMermaidFlowchart(graph *gographviz.Graph, direction string, subgra providerSubgraphs[provider] = true } } - + if subgraphName != "" { sb.WriteString(fmt.Sprintf("\tsubgraph %s\n", subgraphName)) } - + // Iterate over nodes to add them to the Mermaid graph for _, n := range graph.Nodes.Nodes { label := CleanLabel(n.Attrs["label"]) @@ -114,7 +113,7 @@ func ConvertToMermaidFlowchart(graph *gographviz.Graph, direction string, subgra sb.WriteString(fmt.Sprintf("\t\t%s[\"%s\"]\n", nodeName, label)) } } - + // Iterate over edges to add them to the Mermaid graph for _, edge := range graph.Edges.Edges { srcLabel := CleanLabel(graph.Nodes.Lookup[edge.Src].Attrs["label"]) @@ -125,7 +124,7 @@ func ConvertToMermaidFlowchart(graph *gographviz.Graph, direction string, subgra sb.WriteString(fmt.Sprintf("\t\t%s --> %s\n", srcName, dstName)) } } - + // Close all open subgraphs for range providerSubgraphs { sb.WriteString("\tend\n") @@ -133,7 +132,7 @@ func ConvertToMermaidFlowchart(graph *gographviz.Graph, direction string, subgra if subgraphName != "" { sb.WriteString("\tend\n") } - + sb.WriteString("```\n") return sb.String(), nil } diff --git a/pkg/utils/utils.go b/pkg/utils/utils.go index a442a89..1b3bae5 100644 --- a/pkg/utils/utils.go +++ b/pkg/utils/utils.go @@ -31,7 +31,6 @@ func TerraformFilesExist(dir string) bool { } return nil }) - if err != nil { return false } From 6dda75501b782228001cd924bd4103b28cdeac9c Mon Sep 17 00:00:00 2001 From: RoseSecurity Date: Sat, 3 Aug 2024 17:15:37 -0500 Subject: [PATCH 10/10] Fix Tests --- .github/workflows/test.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index f18de41..1853468 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -18,5 +18,5 @@ jobs: cache: false - run: | make build - terramaid -w test/ + build/terramaid -w test/ cat Terramaid.md