// Copyright (c) The OpenTofu Authors // SPDX-License-Identifier: MPL-2.0 // Copyright (c) 2023 HashiCorp, Inc. // SPDX-License-Identifier: MPL-2.0 // loggraphdiff is a tool for interpreting changes to the Terraform graph // based on the simple graph printing format used in the TF_LOG=trace log // output from Terraform, which looks like this: // // aws_instance.b (destroy) - *terraform.NodeDestroyResourceInstance // aws_instance.b (prepare state) - *terraform.NodeApplyableResource // provider.aws - *terraform.NodeApplyableProvider // aws_instance.b (prepare state) - *terraform.NodeApplyableResource // provider.aws - *terraform.NodeApplyableProvider // module.child.aws_instance.a (destroy) - *terraform.NodeDestroyResourceInstance // module.child.aws_instance.a (prepare state) - *terraform.NodeApplyableResource // module.child.output.a_output - *terraform.NodeApplyableOutput // provider.aws - *terraform.NodeApplyableProvider // module.child.aws_instance.a (prepare state) - *terraform.NodeApplyableResource // provider.aws - *terraform.NodeApplyableProvider // module.child.output.a_output - *terraform.NodeApplyableOutput // module.child.aws_instance.a (prepare state) - *terraform.NodeApplyableResource // provider.aws - *terraform.NodeApplyableProvider // // It takes the names of two files containing this style of output and // produces a single graph description in graphviz format that shows the // differences between the two graphs: nodes and edges which are only in the // first graph are shown in red, while those only in the second graph are // shown in green. This color combination is not useful for those who are // red/green color blind, so the result can be adjusted by replacing the // keywords "red" and "green" with a combination that the user is able to // distinguish. package main import ( "bufio" "fmt" "log" "os" "sort" "strings" ) type Graph struct { nodes map[string]struct{} edges map[[2]string]struct{} } func main() { if len(os.Args) != 3 { log.Fatal("usage: loggraphdiff ") } old, err := readGraph(os.Args[1]) if err != nil { log.Fatalf("failed to read %s: %s", os.Args[1], err) } new, err := readGraph(os.Args[2]) if err != nil { log.Fatalf("failed to read %s: %s", os.Args[1], err) } var nodes []string for n := range old.nodes { nodes = append(nodes, n) } for n := range new.nodes { if _, exists := old.nodes[n]; !exists { nodes = append(nodes, n) } } sort.Strings(nodes) var edges [][2]string for e := range old.edges { edges = append(edges, e) } for e := range new.edges { if _, exists := old.edges[e]; !exists { edges = append(edges, e) } } sort.Slice(edges, func(i, j int) bool { if edges[i][0] != edges[j][0] { return edges[i][0] < edges[j][0] } return edges[i][1] < edges[j][1] }) fmt.Println("digraph G {") fmt.Print(" rankdir = \"BT\";\n\n") for _, n := range nodes { var attrs string _, inOld := old.nodes[n] _, inNew := new.nodes[n] switch { case inOld && inNew: // no attrs required case inOld: attrs = " [color=red]" case inNew: attrs = " [color=green]" } fmt.Printf(" %q%s;\n", n, attrs) } fmt.Println("") for _, e := range edges { var attrs string _, inOld := old.edges[e] _, inNew := new.edges[e] switch { case inOld && inNew: // no attrs required case inOld: attrs = " [color=red]" case inNew: attrs = " [color=green]" } fmt.Printf(" %q -> %q%s;\n", e[0], e[1], attrs) } fmt.Println("}") } func readGraph(fn string) (Graph, error) { ret := Graph{ nodes: map[string]struct{}{}, edges: map[[2]string]struct{}{}, } r, err := os.Open(fn) if err != nil { return ret, err } sc := bufio.NewScanner(r) var latestNode string for sc.Scan() { l := sc.Text() dash := strings.Index(l, " - ") if dash == -1 { // invalid line, so we'll ignore it continue } name := l[:dash] if strings.HasPrefix(name, " ") { // It's an edge name = name[2:] edge := [2]string{latestNode, name} ret.edges[edge] = struct{}{} } else { // It's a node latestNode = name ret.nodes[name] = struct{}{} } } return ret, nil }