github.com/terramate-io/tf@v0.0.0-20230830114523-fce866b4dfcd/tools/loggraphdiff/loggraphdiff.go (about) 1 // Copyright (c) HashiCorp, Inc. 2 // SPDX-License-Identifier: MPL-2.0 3 4 // loggraphdiff is a tool for interpreting changes to the Terraform graph 5 // based on the simple graph printing format used in the TF_LOG=trace log 6 // output from Terraform, which looks like this: 7 // 8 // aws_instance.b (destroy) - *terraform.NodeDestroyResourceInstance 9 // aws_instance.b (prepare state) - *terraform.NodeApplyableResource 10 // provider.aws - *terraform.NodeApplyableProvider 11 // aws_instance.b (prepare state) - *terraform.NodeApplyableResource 12 // provider.aws - *terraform.NodeApplyableProvider 13 // module.child.aws_instance.a (destroy) - *terraform.NodeDestroyResourceInstance 14 // module.child.aws_instance.a (prepare state) - *terraform.NodeApplyableResource 15 // module.child.output.a_output - *terraform.NodeApplyableOutput 16 // provider.aws - *terraform.NodeApplyableProvider 17 // module.child.aws_instance.a (prepare state) - *terraform.NodeApplyableResource 18 // provider.aws - *terraform.NodeApplyableProvider 19 // module.child.output.a_output - *terraform.NodeApplyableOutput 20 // module.child.aws_instance.a (prepare state) - *terraform.NodeApplyableResource 21 // provider.aws - *terraform.NodeApplyableProvider 22 // 23 // It takes the names of two files containing this style of output and 24 // produces a single graph description in graphviz format that shows the 25 // differences between the two graphs: nodes and edges which are only in the 26 // first graph are shown in red, while those only in the second graph are 27 // shown in green. This color combination is not useful for those who are 28 // red/green color blind, so the result can be adjusted by replacing the 29 // keywords "red" and "green" with a combination that the user is able to 30 // distinguish. 31 32 package main 33 34 import ( 35 "bufio" 36 "fmt" 37 "log" 38 "os" 39 "sort" 40 "strings" 41 ) 42 43 type Graph struct { 44 nodes map[string]struct{} 45 edges map[[2]string]struct{} 46 } 47 48 func main() { 49 if len(os.Args) != 3 { 50 log.Fatal("usage: loggraphdiff <old-graph-file> <new-graph-file>") 51 } 52 53 old, err := readGraph(os.Args[1]) 54 if err != nil { 55 log.Fatalf("failed to read %s: %s", os.Args[1], err) 56 } 57 new, err := readGraph(os.Args[2]) 58 if err != nil { 59 log.Fatalf("failed to read %s: %s", os.Args[1], err) 60 } 61 62 var nodes []string 63 for n := range old.nodes { 64 nodes = append(nodes, n) 65 } 66 for n := range new.nodes { 67 if _, exists := old.nodes[n]; !exists { 68 nodes = append(nodes, n) 69 } 70 } 71 sort.Strings(nodes) 72 73 var edges [][2]string 74 for e := range old.edges { 75 edges = append(edges, e) 76 } 77 for e := range new.edges { 78 if _, exists := old.edges[e]; !exists { 79 edges = append(edges, e) 80 } 81 } 82 sort.Slice(edges, func(i, j int) bool { 83 if edges[i][0] != edges[j][0] { 84 return edges[i][0] < edges[j][0] 85 } 86 return edges[i][1] < edges[j][1] 87 }) 88 89 fmt.Println("digraph G {") 90 fmt.Print(" rankdir = \"BT\";\n\n") 91 for _, n := range nodes { 92 var attrs string 93 _, inOld := old.nodes[n] 94 _, inNew := new.nodes[n] 95 switch { 96 case inOld && inNew: 97 // no attrs required 98 case inOld: 99 attrs = " [color=red]" 100 case inNew: 101 attrs = " [color=green]" 102 } 103 fmt.Printf(" %q%s;\n", n, attrs) 104 } 105 fmt.Println("") 106 for _, e := range edges { 107 var attrs string 108 _, inOld := old.edges[e] 109 _, inNew := new.edges[e] 110 switch { 111 case inOld && inNew: 112 // no attrs required 113 case inOld: 114 attrs = " [color=red]" 115 case inNew: 116 attrs = " [color=green]" 117 } 118 fmt.Printf(" %q -> %q%s;\n", e[0], e[1], attrs) 119 } 120 fmt.Println("}") 121 } 122 123 func readGraph(fn string) (Graph, error) { 124 ret := Graph{ 125 nodes: map[string]struct{}{}, 126 edges: map[[2]string]struct{}{}, 127 } 128 r, err := os.Open(fn) 129 if err != nil { 130 return ret, err 131 } 132 133 sc := bufio.NewScanner(r) 134 var latestNode string 135 for sc.Scan() { 136 l := sc.Text() 137 dash := strings.Index(l, " - ") 138 if dash == -1 { 139 // invalid line, so we'll ignore it 140 continue 141 } 142 name := l[:dash] 143 if strings.HasPrefix(name, " ") { 144 // It's an edge 145 name = name[2:] 146 edge := [2]string{latestNode, name} 147 ret.edges[edge] = struct{}{} 148 } else { 149 // It's a node 150 latestNode = name 151 ret.nodes[name] = struct{}{} 152 } 153 } 154 155 return ret, nil 156 }