Skip to content

Commit

Permalink
feat(fix rendering): fix rendering, clean up code, add tui title, and…
Browse files Browse the repository at this point in the history
… move everything into run command
  • Loading branch information
RoseSecurity committed Nov 29, 2024
1 parent 2f59018 commit e2a3b90
Show file tree
Hide file tree
Showing 11 changed files with 183 additions and 123 deletions.
13 changes: 13 additions & 0 deletions .github/workflows/test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -46,3 +46,16 @@ jobs:
make build
build/terramaid -w test/az
cat Terramaid.md
test_multi:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: hashicorp/setup-terraform@v3
- uses: actions/setup-go@v5
with:
go-version-file: "go.mod"
cache: false
- run: |
make build
build/terramaid -w test/multi
cat Terramaid.md
18 changes: 8 additions & 10 deletions cmd/run.go
Original file line number Diff line number Diff line change
Expand Up @@ -62,25 +62,23 @@ func generateDiagrams(opts *options) error {
// Spinner initialization and graph parsing
sp := utils.NewSpinner("Generating Terramaid Diagrams")
sp.Start()

graph, err := internal.ParseTerraform(opts.WorkingDir, opts.TFBinary, opts.TFPlan)
if err != nil {
sp.Stop()
return fmt.Errorf("error parsing Terraform: %w", err)
}

// Convert the graph to a Mermaid diagram
var mermaidDiagram string
switch opts.ChartType {
case "flowchart":
mermaidDiagram, err = internal.ConvertToMermaidFlowchart(graph, opts.Direction, opts.SubgraphName)
if err != nil {
return fmt.Errorf("error converting to Mermaid flowchart: %w", err)
}
default:
return fmt.Errorf("unsupported chart type: %s", opts.ChartType)
// Generate the Mermaid diagram
mermaidDiagram, err := internal.GenerateMermaidFlowchart(graph, opts.Direction, opts.SubgraphName)
if err != nil {
sp.Stop()
return fmt.Errorf("error generating Mermaid diagram: %w", err)
}

// Write the Mermaid diagram to the specified output file
if err := os.WriteFile(opts.Output, []byte(mermaidDiagram), 0o644); err != nil {
sp.Stop()
return fmt.Errorf("error writing to file: %w", err)
}

Expand Down
11 changes: 5 additions & 6 deletions docs/terramaid_run.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,15 +9,14 @@ terramaid run [flags]
### Options

```
-c, --chart-type string Specify the type of Mermaid chart to generate (env: TERRAMAID_CHART_TYPE)
-r, --direction string Specify the direction of the diagram (env: TERRAMAID_DIRECTION)
-c, --chart-type string Specify the type of Mermaid chart to generate (env: TERRAMAID_CHART_TYPE) (default "flowchart")
-r, --direction string Specify the direction of the diagram (env: TERRAMAID_DIRECTION) (default "TD")
-h, --help help for run
-o, --output string Output file for Mermaid diagram (env: TERRAMAID_OUTPUT)
-s, --subgraph-name string Specify the subgraph name of the diagram (env: TERRAMAID_SUBGRAPH_NAME)
-o, --output string Output file for Mermaid diagram (env: TERRAMAID_OUTPUT) (default "Terramaid.md")
-s, --subgraph-name string Specify the subgraph name of the diagram (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)
-p, --tf-plan string Path to Terraform plan file (env: TERRAMAID_TF_PLAN)
-w, --working-dir string Working directory for Terraform (env: TERRAMAID_WORKING_DIR)
-w, --working-dir string Working directory for Terraform (env: TERRAMAID_WORKING_DIR) (default ".")
```

### SEE ALSO
Expand Down
6 changes: 5 additions & 1 deletion go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -11,9 +11,9 @@ require (
github.com/hashicorp/terraform-exec v0.21.0
github.com/jwalton/go-supportscolor v1.2.0
github.com/mattn/go-colorable v0.1.13
github.com/nao1215/markdown v0.6.0
github.com/spf13/cobra v1.8.1
golang.org/x/mod v0.17.0
golang.org/x/text v0.19.0
)

require (
Expand All @@ -24,13 +24,17 @@ require (
github.com/hashicorp/go-version v1.7.0 // indirect
github.com/hashicorp/terraform-json v0.22.1 // indirect
github.com/inconshreveable/mousetrap v1.1.0 // indirect
github.com/karrick/godirwalk v1.17.0 // indirect
github.com/kr/pretty v0.3.1 // indirect
github.com/mattn/go-isatty v0.0.20 // indirect
github.com/mattn/go-runewidth v0.0.9 // indirect
github.com/olekukonko/tablewriter v0.0.5 // indirect
github.com/pkg/errors v0.9.1 // indirect
github.com/russross/blackfriday/v2 v2.1.0 // indirect
github.com/spf13/pflag v1.0.5 // indirect
github.com/zclconf/go-cty v1.14.4 // indirect
golang.org/x/sys v0.18.0 // indirect
golang.org/x/term v0.1.0 // indirect
golang.org/x/text v0.19.0 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
)
8 changes: 8 additions & 0 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,8 @@ github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99 h1:BQSFePA1RWJOl
github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99/go.mod h1:1lJo3i6rXxKeerYnT8Nvf0QmHCRC1n8sfWVwXF2Frvo=
github.com/jwalton/go-supportscolor v1.2.0 h1:g6Ha4u7Vm3LIsQ5wmeBpS4gazu0UP1DRDE8y6bre4H8=
github.com/jwalton/go-supportscolor v1.2.0/go.mod h1:hFVUAZV2cWg+WFFC4v8pT2X/S2qUUBYMioBD9AINXGs=
github.com/karrick/godirwalk v1.17.0 h1:b4kY7nqDdioR/6qnbHQyDvmA17u5G1cZ6J+CZXwSWoI=
github.com/karrick/godirwalk v1.17.0/go.mod h1:j4mkqPuvaLI8mp1DroR3P6ad7cyYd4c1qeJ3RV7ULlk=
github.com/kevinburke/ssh_config v1.2.0 h1:x584FjTGwHzMwvHx18PXxbBVzfnxogHaAReU4gf13a4=
github.com/kevinburke/ssh_config v1.2.0/go.mod h1:CT57kijsi8u/K/BOFA39wgDQJ9CxiF4nAY/ojJ6r6mM=
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
Expand All @@ -68,6 +70,12 @@ github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovk
github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM=
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
github.com/mattn/go-runewidth v0.0.9 h1:Lm995f3rfxdpd6TSmuVCHVb/QhupuXlYr8sCI/QdE+0=
github.com/mattn/go-runewidth v0.0.9/go.mod h1:H031xJmbD/WCDINGzjvQ9THkh0rPKHF+m2gUSrubnMI=
github.com/nao1215/markdown v0.6.0 h1:kqhrC47K434YA1jMTUwJwSV/hla8ifN3NzehMEffI/E=
github.com/nao1215/markdown v0.6.0/go.mod h1:ObBhnNduWwPN+bu4dtv4JoLRt57ONla7l//03iHIVhY=
github.com/olekukonko/tablewriter v0.0.5 h1:P2Ga83D34wi1o9J6Wh1mRuqd4mF/x/lgBS7N7AbDhec=
github.com/olekukonko/tablewriter v0.0.5/go.mod h1:hPp6KlRPjbx+hW8ykQs1w3UBbZlj6HuIJcUGPhkA7kY=
github.com/pjbgf/sha1cd v0.3.0 h1:4D5XXmUUBUl/xQ6IjCkEAbqXskkq/4O7LmGn0AqMDs4=
github.com/pjbgf/sha1cd v0.3.0/go.mod h1:nZ1rrWOcGJ5uZgEEVL1VUM9iRQiZvWdbZjkKyFzPPsI=
github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA=
Expand Down
170 changes: 64 additions & 106 deletions internal/flowchart.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,141 +6,99 @@ import (
"strings"

"github.com/awalterschulze/gographviz"
"golang.org/x/text/cases"
"golang.org/x/text/language"
)

type Node struct {
ID string
Label string
Count int
Provider string
}

type Edge struct {
From string
To string
}

type Graph struct {
Nodes []Node
Edges []Edge
NodeMap map[string]int
}

var labelCleaner = regexp.MustCompile(`\s*\(expand\)|\s*\(close\)|\[root\]\s*|"|[\\/]`)
var labelCleaner = regexp.MustCompile(`\s*\(expand\)|\s*\(close\)|\[root\]\s*|"`)

// CleanLabel removes unnecessary parts from the label
func CleanLabel(label string) string {
return labelCleaner.ReplaceAllString(label, "")
}

// CleanID removes unnecessary parts from the ID
func CleanID(id string) string {
return labelCleaner.ReplaceAllString(id, "")
}

// ExtractProvider extracts the provider for separate subgraph
func ExtractProvider(label string) string {
parts := strings.Split(label, "_")
if len(parts) > 0 {
// Remove quotes from the provider name
provider := strings.ReplaceAll(parts[0], "\"", "")
// Replace backslashes and forward slashes with underscores for Mermaid compatibility
provider = strings.ReplaceAll(provider, "\\", "_")
provider = strings.ReplaceAll(provider, "/", "_")
return provider
id = labelCleaner.ReplaceAllString(id, "")
if strings.HasPrefix(id, "provider[") {
id = strings.ReplaceAll(id, "provider[", "provider_")
id = strings.ReplaceAll(id, "]", "")
id = strings.ReplaceAll(id, "/", "_")
id = strings.ReplaceAll(id, ".", "_")
return id
}
return ""
id = strings.ReplaceAll(id, ".", "_")
id = strings.ReplaceAll(id, "/", "_")
return id
}

// TransformGraph transforms the parsed graph into cleaned nodes and edges
func TransformGraph(graph *gographviz.Graph) Graph {
nodes := []Node{}
edges := []Edge{}
nodeMap := make(map[string]int)

for _, node := range graph.Nodes.Nodes {
cleanedID := CleanID(node.Name)
cleanedLabel := CleanLabel(node.Attrs["label"])
provider := ExtractProvider(cleanedLabel)
if cleanedLabel != "" {
nodeMap[cleanedLabel]++
nodes = append(nodes, Node{ID: cleanedID, Label: cleanedLabel, Count: nodeMap[cleanedLabel], Provider: provider})
}
}

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 != "" {
edges = append(edges, Edge{From: CleanID(edge.Src), To: CleanID(edge.Dst)})
}
func CleanLabel(label string) string {
label = labelCleaner.ReplaceAllString(label, "")
if strings.HasPrefix(label, "provider[") {
label = strings.ReplaceAll(label, "[", ": ")
label = strings.ReplaceAll(label, "]", "")
}

return Graph{Nodes: nodes, Edges: edges, NodeMap: nodeMap}
label = strings.ReplaceAll(label, "\\", "")
return label
}

// ConvertToMermaidFlowchart 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

caser := cases.Title(language.English)
validDirections := map[string]bool{
"TB": true, "TD": true, "BT": true, "RL": true, "LR": true,
}
// GenerateMermaidFlowchart generates a Mermaid diagram from a gographviz graph
func GenerateMermaidFlowchart(graph *gographviz.Graph, direction string, subgraphName string) (string, error) {
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)
return "", fmt.Errorf("invalid direction %s: valid options are TB, TD, BT, RL, LR", direction)
}

sb.WriteString("```mermaid\n")
sb.WriteString("flowchart " + direction + "\n")
var sb strings.Builder
sb.WriteString(fmt.Sprintf("```mermaid\nflowchart %s\n", direction))

if subgraphName != "" {
sb.WriteString(fmt.Sprintf("\tsubgraph %s\n", subgraphName))
sb.WriteString(fmt.Sprintf(" subgraph %s\n", subgraphName))
}

providerSubgraphs := make(map[string]bool)
for _, n := range graph.Nodes.Nodes {
provider := ExtractProvider(n.Attrs["label"])
if provider != "" && !providerSubgraphs[provider] {
sb.WriteString(fmt.Sprintf("\t\tsubgraph %s\n", caser.String(provider)))
providerSubgraphs[provider] = true
addedNodes := make(map[string]string)

addedProviders := make(map[string]bool)

for _, node := range graph.Nodes.Nodes {
nodeID := CleanID(node.Name)
nodeLabel := CleanLabel(node.Attrs["label"])

if nodeLabel == "" {
continue
}
}

nodeMap := make(map[string]int)
for _, n := range graph.Nodes.Nodes {
label := CleanLabel(n.Attrs["label"])
nodeName := CleanID(n.Name)
if label != "" && nodeName != "" {
nodeMap[label]++
count := nodeMap[label]
if count > 1 {
sb.WriteString(fmt.Sprintf("\t\t\t%s[\"%s\\nCount: %d\"]\n", nodeName, label, count))
} else {
sb.WriteString(fmt.Sprintf("\t\t\t%s[\"%s\"]\n", nodeName, label))
if strings.HasPrefix(nodeLabel, "provider:") {
if addedProviders[nodeID] {
continue
}
addedProviders[nodeID] = true
}

if _, exists := addedNodes[nodeID]; !exists {
sb.WriteString(fmt.Sprintf(" %s[\"%s\"]\n", nodeID, nodeLabel))
addedNodes[nodeID] = nodeLabel
}
}

for range providerSubgraphs {
sb.WriteString("\t\tend\n")
if subgraphName != "" {
sb.WriteString(" end\n")
}

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 != "" {
sb.WriteString(fmt.Sprintf("\t\t%s --> %s\n", srcName, dstName))
fromID := CleanID(edge.Src)
toID := CleanID(edge.Dst)

if _, exists := addedNodes[fromID]; !exists {
fromLabel := CleanLabel(graph.Nodes.Lookup[edge.Src].Attrs["label"])
if fromLabel != "" {
sb.WriteString(fmt.Sprintf(" %s[\"%s\"]\n", fromID, fromLabel))
addedNodes[fromID] = fromLabel
}
}
}

if subgraphName != "" {
sb.WriteString("\tend\n")
if _, exists := addedNodes[toID]; !exists {
toLabel := CleanLabel(graph.Nodes.Lookup[edge.Dst].Attrs["label"])
if toLabel != "" {
sb.WriteString(fmt.Sprintf(" %s[\"%s\"]\n", toID, toLabel))
addedNodes[toID] = toLabel
}
}

sb.WriteString(fmt.Sprintf(" %s --> %s\n", fromID, toID))
}

sb.WriteString("```\n")
Expand Down
Binary file removed test/aws/terramaid
Binary file not shown.
45 changes: 45 additions & 0 deletions test/multi/main.tf
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
data "cloudsmith_organization" "my_organization" {
slug = var.cloudsmith_org
}

resource "cloudsmith_repository" "my_repository" {
description = "A certifiably-awesome private package repository"
name = "My Repository"
namespace = data.cloudsmith_organization.my_organization.slug_perm
slug = "test-repo"
}

data "aws_ami" "ubuntu" {
most_recent = true

filter {
name = "name"
values = ["ubuntu/images/hvm-ssd/ubuntu-jammy-22.04-amd64-server-*"]
}

filter {
name = "virtualization-type"
values = ["hvm"]
}

owners = ["099720109477"] # Canonical
}

resource "aws_instance" "web" {
ami = data.aws_ami.ubuntu.id
instance_type = "t3.micro"

tags = {
Name = "HelloWorld"
}
}

resource "google_healthcare_dataset" "dataset" {
location = "us-central1"
name = "my-dataset"
}

resource "google_healthcare_consent_store" "my-consent" {
dataset = google_healthcare_dataset.dataset.id
name = var.consent_store_name
}
4 changes: 4 additions & 0 deletions test/multi/outputs.tf
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
output "ubuntu_ami" {
value = data.aws_ami.ubuntu.id
description = "Ubuntu AWS AMI ID"
}
12 changes: 12 additions & 0 deletions test/multi/variables.tf
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
variable "cloudsmith_org" {
type = string
description = "Cloudsmith test org"
default = "test"
}

variable "consent_store_name" {
type = string
description = "Consent store name for GCP"
default = "my-consent-store"
}

Loading

0 comments on commit e2a3b90

Please sign in to comment.