blob: 8b25eea7cf713d3a6291d664ed65f14d1117569d [file] [log] [blame]
// 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-graph-file> <new-graph-file>")
}
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
}