154 lines
4.0 KiB
Go
154 lines
4.0 KiB
Go
// 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.Fatal("failed to read %s: %s", os.Args[1], err)
|
|
}
|
|
new, err := readGraph(os.Args[2])
|
|
if err != nil {
|
|
log.Fatal("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.Println(" rankdir = \"BT\";\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
|
|
}
|