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