forked from hashicorp/terraform
-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathloggraphdiff.go
More file actions
153 lines (142 loc) · 4.01 KB
/
loggraphdiff.go
File metadata and controls
153 lines (142 loc) · 4.01 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
// 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
}