Provider a marshaler for dag.Graph

The dot format generation was done with a mix of code from the terraform
package and the dot package. Unify the dot generation code, and it into
the dag package.

Use an intermediate structure to allow a dag.Graph to marshal itself
directly. This structure will be ablt to marshal directly to JSON, or be
translated to dot format. This was we can record more information about
the graph in the debug logs, and provide a way to translate those logged
structures to dot, which is convenient for viewing the graphs.
This commit is contained in:
James Bardin 2016-11-09 09:58:52 -05:00
parent bda84e03f7
commit 28d406c040
12 changed files with 473 additions and 58 deletions

View File

@ -6,6 +6,7 @@ import (
"os" "os"
"strings" "strings"
"github.com/hashicorp/terraform/dag"
"github.com/hashicorp/terraform/terraform" "github.com/hashicorp/terraform/terraform"
) )
@ -67,7 +68,7 @@ func (c *GraphCommand) Run(args []string) int {
return 1 return 1
} }
graphStr, err := terraform.GraphDot(g, &terraform.GraphDotOpts{ graphStr, err := terraform.GraphDot(g, &dag.DotOpts{
DrawCycles: drawCycles, DrawCycles: drawCycles,
MaxDepth: moduleDepth, MaxDepth: moduleDepth,
Verbose: verbose, Verbose: verbose,

230
dag/dot.go Normal file
View File

@ -0,0 +1,230 @@
package dag
import (
"bytes"
"fmt"
"sort"
"strings"
)
// DotOpts are the options for generating a dot formatted Graph.
type DotOpts struct {
// Allows some nodes to decide to only show themselves when the user has
// requested the "verbose" graph.
Verbose bool
// Highlight Cycles
DrawCycles bool
// How many levels to expand modules as we draw
MaxDepth int
// use this to keep the cluster_ naming convention from the previous dot writer
cluster bool
}
// Returns the DOT representation of this Graph.
func (g *marshalGraph) Dot(opts *DotOpts) []byte {
if opts == nil {
opts = &DotOpts{
DrawCycles: true,
MaxDepth: -1,
Verbose: true,
}
}
var w indentWriter
w.WriteString("digraph {\n")
w.Indent()
// some dot defaults
w.WriteString(`compound = "true"` + "\n")
w.WriteString(`newrank = "true"` + "\n")
// the top level graph is written as the first subgraph
w.WriteString(`subgraph "root" {` + "\n")
g.writeBody(opts, &w)
// cluster isn't really used other than for naming purposes in some graphs
opts.cluster = opts.MaxDepth != 0
maxDepth := opts.MaxDepth
if maxDepth == 0 {
maxDepth = -1
}
for _, s := range g.Subgraphs {
g.writeSubgraph(s, opts, maxDepth, &w)
}
w.Unindent()
w.WriteString("}\n")
return w.Bytes()
}
func (v *marshalVertex) dot(g *marshalGraph) []byte {
var buf bytes.Buffer
graphName := g.Name
if graphName == "" {
graphName = "root"
}
buf.WriteString(fmt.Sprintf(`"[%s] %s"`, graphName, v.Name))
writeAttrs(&buf, v.Attrs)
buf.WriteByte('\n')
return buf.Bytes()
}
func (e *marshalEdge) dot(g *marshalGraph) string {
var buf bytes.Buffer
graphName := g.Name
if graphName == "" {
graphName = "root"
}
sourceName := g.vertexByID(e.Source).Name
targetName := g.vertexByID(e.Target).Name
s := fmt.Sprintf(`"[%s] %s" -> "[%s] %s"`, graphName, sourceName, graphName, targetName)
buf.WriteString(s)
writeAttrs(&buf, e.Attrs)
return buf.String()
}
func cycleDot(e *marshalEdge, g *marshalGraph) string {
return e.dot(g) + ` [color = "red", penwidth = "2.0"]`
}
// Write the subgraph body. The is recursive, and the depth argument is used to
// record the current depth of iteration.
func (g *marshalGraph) writeSubgraph(sg *marshalGraph, opts *DotOpts, depth int, w *indentWriter) {
if depth == 0 {
return
}
depth--
name := sg.Name
if opts.cluster {
// we prefix with cluster_ to match the old dot output
name = "cluster_" + name
sg.Attrs["label"] = sg.Name
}
w.WriteString(fmt.Sprintf("subgraph %q {\n", name))
sg.writeBody(opts, w)
for _, sg := range sg.Subgraphs {
g.writeSubgraph(sg, opts, depth, w)
}
}
func (g *marshalGraph) writeBody(opts *DotOpts, w *indentWriter) {
w.Indent()
for _, as := range attrStrings(g.Attrs) {
w.WriteString(as + "\n")
}
for _, v := range g.Vertices {
w.Write(v.dot(g))
}
var dotEdges []string
if opts.DrawCycles {
for _, c := range g.Cycles {
if len(c) < 2 {
continue
}
for i, j := 0, 1; i < len(c); i, j = i+1, j+1 {
if j >= len(c) {
j = 0
}
src := c[i]
tgt := c[j]
e := &marshalEdge{
Name: fmt.Sprintf("%s|%s", src.Name, tgt.Name),
Source: src.ID,
Target: tgt.ID,
Attrs: make(map[string]string),
}
dotEdges = append(dotEdges, cycleDot(e, g))
src = tgt
}
}
}
for _, e := range g.Edges {
dotEdges = append(dotEdges, e.dot(g))
}
// srot these again to match the old output
sort.Strings(dotEdges)
for _, e := range dotEdges {
w.WriteString(e + "\n")
}
w.Unindent()
w.WriteString("}\n")
}
func writeAttrs(buf *bytes.Buffer, attrs map[string]string) {
if len(attrs) > 0 {
buf.WriteString(" [")
buf.WriteString(strings.Join(attrStrings(attrs), ", "))
buf.WriteString("]")
}
}
func attrStrings(attrs map[string]string) []string {
strings := make([]string, 0, len(attrs))
for k, v := range attrs {
strings = append(strings, fmt.Sprintf("%s = %q", k, v))
}
sort.Strings(strings)
return strings
}
// Provide a bytes.Buffer like structure, which will indent when starting a
// newline.
type indentWriter struct {
bytes.Buffer
level int
}
func (w *indentWriter) indent() {
newline := []byte("\n")
if !bytes.HasSuffix(w.Bytes(), newline) {
return
}
for i := 0; i < w.level; i++ {
w.Buffer.WriteString("\t")
}
}
// Indent increases indentation by 1
func (w *indentWriter) Indent() { w.level++ }
// Unindent decreases indentation by 1
func (w *indentWriter) Unindent() { w.level-- }
// the following methods intercecpt the byte.Buffer writes and insert the
// indentation when starting a new line.
func (w *indentWriter) Write(b []byte) (int, error) {
w.indent()
return w.Buffer.Write(b)
}
func (w *indentWriter) WriteString(s string) (int, error) {
w.indent()
return w.Buffer.WriteString(s)
}
func (w *indentWriter) WriteByte(b byte) error {
w.indent()
return w.Buffer.WriteByte(b)
}
func (w *indentWriter) WriteRune(r rune) (int, error) {
w.indent()
return w.Buffer.WriteRune(r)
}

View File

@ -2,6 +2,7 @@ package dag
import ( import (
"bytes" "bytes"
"encoding/json"
"fmt" "fmt"
"sort" "sort"
"sync" "sync"
@ -284,6 +285,20 @@ func (g *Graph) String() string {
return buf.String() return buf.String()
} }
func (g *Graph) Marshal() ([]byte, error) {
dg := newMarshalGraph("", g)
return json.MarshalIndent(dg, "", " ")
}
func (g *Graph) Dot(opts *DotOpts) []byte {
return newMarshalGraph("", g).Dot(opts)
}
func (g *Graph) MarshalJSON() ([]byte, error) {
dg := newMarshalGraph("", g)
return json.MarshalIndent(dg, "", " ")
}
func (g *Graph) init() { func (g *Graph) init() {
g.vertices = new(Set) g.vertices = new(Set)
g.edges = new(Set) g.edges = new(Set)

150
dag/marshal.go Normal file
View File

@ -0,0 +1,150 @@
package dag
import (
"fmt"
"reflect"
"sort"
"strconv"
)
// the marshal* structs are for serialization of the graph data.
type marshalGraph struct {
ID string `json:",omitempty"`
Name string `json:",omitempty"`
Attrs map[string]string `json:",omitempty"`
Vertices []*marshalVertex `json:",omitempty"`
Edges []*marshalEdge `json:",omitempty"`
Subgraphs []*marshalGraph `json:",omitempty"`
Cycles [][]*marshalVertex `json:",omitempty"`
}
func (g *marshalGraph) vertexByID(id string) *marshalVertex {
for _, v := range g.Vertices {
if id == v.ID {
return v
}
}
return nil
}
type marshalVertex struct {
ID string
Name string `json:",omitempty"`
Attrs map[string]string `json:",omitempty"`
}
type vertices []*marshalVertex
func (v vertices) Less(i, j int) bool { return v[i].Name < v[j].Name }
func (v vertices) Len() int { return len(v) }
func (v vertices) Swap(i, j int) { v[i], v[j] = v[j], v[i] }
type marshalEdge struct {
Name string
Source string
Target string
Attrs map[string]string `json:",omitempty"`
}
type edges []*marshalEdge
func (e edges) Less(i, j int) bool { return e[i].Name < e[j].Name }
func (e edges) Len() int { return len(e) }
func (e edges) Swap(i, j int) { e[i], e[j] = e[j], e[i] }
func newMarshalGraph(name string, g *Graph) *marshalGraph {
dg := &marshalGraph{
Name: name,
Attrs: make(map[string]string),
}
for _, v := range g.Vertices() {
id := marshalVertexID(v)
if sg, ok := marshalSubgraph(v); ok {
sdg := newMarshalGraph(VertexName(v), sg)
sdg.ID = id
dg.Subgraphs = append(dg.Subgraphs, sdg)
}
dv := &marshalVertex{
ID: id,
Name: VertexName(v),
Attrs: make(map[string]string),
}
dg.Vertices = append(dg.Vertices, dv)
}
sort.Sort(vertices(dg.Vertices))
for _, e := range g.Edges() {
de := &marshalEdge{
Name: fmt.Sprintf("%s|%s", VertexName(e.Source()), VertexName(e.Target())),
Source: marshalVertexID(e.Source()),
Target: marshalVertexID(e.Target()),
Attrs: make(map[string]string),
}
dg.Edges = append(dg.Edges, de)
}
sort.Sort(edges(dg.Edges))
for _, c := range (&AcyclicGraph{*g}).Cycles() {
var cycle []*marshalVertex
for _, v := range c {
dv := &marshalVertex{
ID: marshalVertexID(v),
Name: VertexName(v),
Attrs: make(map[string]string),
}
cycle = append(cycle, dv)
}
dg.Cycles = append(dg.Cycles, cycle)
}
return dg
}
// Attempt to return a unique ID for any vertex.
func marshalVertexID(v Vertex) string {
val := reflect.ValueOf(v)
switch val.Kind() {
case reflect.Chan, reflect.Func, reflect.Map, reflect.Ptr, reflect.Slice, reflect.UnsafePointer:
return strconv.Itoa(int(val.Pointer()))
case reflect.Interface:
return strconv.Itoa(int(val.InterfaceData()[1]))
}
if v, ok := v.(Hashable); ok {
h := v.Hashcode()
if h, ok := h.(string); ok {
return h
}
}
// we could try harder by attempting to read the arbitrary value from the
// interface, but we shouldn't get here from terraform right now.
panic("unhashable value in graph")
}
func debugSubgraph(v Vertex) (*Graph, bool) {
val := reflect.ValueOf(v)
m, ok := val.Type().MethodByName("Subgraph")
if !ok {
return nil, false
}
if m.Type.NumOut() != 1 {
return nil, false
}
// can't check for the subgraph type, because we can't import terraform, so
// we assume this is the correct method.
// TODO: create a dag interface type that we can satisfy
sg := val.MethodByName("Subgraph").Call(nil)[0]
ag := sg.Elem().FieldByName("AcyclicGraph").Interface().(AcyclicGraph)
return &ag.Graph, true
}

View File

@ -129,7 +129,7 @@ func (n *graphNodeModuleExpanded) DependentOn() []string {
} }
// GraphNodeDotter impl. // GraphNodeDotter impl.
func (n *graphNodeModuleExpanded) DotNode(name string, opts *GraphDotOpts) *dot.Node { func (n *graphNodeModuleExpanded) DotNode(name string, opts *dag.DotOpts) *dot.Node {
return dot.NewNode(name, map[string]string{ return dot.NewNode(name, map[string]string{
"label": dag.VertexName(n.Original), "label": dag.VertexName(n.Original),
"shape": "component", "shape": "component",

View File

@ -59,7 +59,7 @@ func (n *GraphNodeConfigProvider) ProviderConfig() *config.RawConfig {
} }
// GraphNodeDotter impl. // GraphNodeDotter impl.
func (n *GraphNodeConfigProvider) DotNode(name string, opts *GraphDotOpts) *dot.Node { func (n *GraphNodeConfigProvider) DotNode(name string, opts *dag.DotOpts) *dot.Node {
return dot.NewNode(name, map[string]string{ return dot.NewNode(name, map[string]string{
"label": n.Name(), "label": n.Name(),
"shape": "diamond", "shape": "diamond",

View File

@ -128,7 +128,7 @@ func (n *GraphNodeConfigResource) Name() string {
} }
// GraphNodeDotter impl. // GraphNodeDotter impl.
func (n *GraphNodeConfigResource) DotNode(name string, opts *GraphDotOpts) *dot.Node { func (n *GraphNodeConfigResource) DotNode(name string, opts *dag.DotOpts) *dot.Node {
if n.Destroy && !opts.Verbose { if n.Destroy && !opts.Verbose {
return nil return nil
} }

View File

@ -28,14 +28,14 @@ type DebugGraph struct {
buf bytes.Buffer buf bytes.Buffer
Dot *dot.Graph Dot *dot.Graph
dotOpts *GraphDotOpts dotOpts *dag.DotOpts
} }
// DebugGraph holds a dot representation of the Terraform graph, and can be // DebugGraph holds a dot representation of the Terraform graph, and can be
// written out to the DebugInfo log with DebugInfo.WriteGraph. A DebugGraph can // written out to the DebugInfo log with DebugInfo.WriteGraph. A DebugGraph can
// log data to it's internal buffer via the Printf and Write methods, which // log data to it's internal buffer via the Printf and Write methods, which
// will be also be written out to the DebugInfo archive. // will be also be written out to the DebugInfo archive.
func NewDebugGraph(name string, g *Graph, opts *GraphDotOpts) (*DebugGraph, error) { func NewDebugGraph(name string, g *Graph, opts *dag.DotOpts) (*DebugGraph, error) {
dg := &DebugGraph{ dg := &DebugGraph{
Name: name, Name: name,
dotOpts: opts, dotOpts: opts,
@ -142,7 +142,7 @@ func (dg *DebugGraph) build(g *Graph) error {
dg.Dot.Directed = true dg.Dot.Directed = true
if dg.dotOpts == nil { if dg.dotOpts == nil {
dg.dotOpts = &GraphDotOpts{ dg.dotOpts = &dag.DotOpts{
DrawCycles: true, DrawCycles: true,
MaxDepth: -1, MaxDepth: -1,
Verbose: true, Verbose: true,
@ -182,6 +182,12 @@ func (dg *DebugGraph) buildSubgraph(modName string, g *Graph, modDepth int) erro
toDraw := make([]dag.Vertex, 0, len(g.Vertices())) toDraw := make([]dag.Vertex, 0, len(g.Vertices()))
subgraphVertices := make(map[dag.Vertex]*Graph) subgraphVertices := make(map[dag.Vertex]*Graph)
for _, v := range g.Vertices() {
if sn, ok := v.(GraphNodeSubgraph); ok {
subgraphVertices[v] = sn.Subgraph()
}
}
walk := func(v dag.Vertex, depth int) error { walk := func(v dag.Vertex, depth int) error {
// We only care about nodes that yield non-empty Dot strings. // We only care about nodes that yield non-empty Dot strings.
if dn, ok := v.(GraphNodeDotter); !ok { if dn, ok := v.(GraphNodeDotter); !ok {

View File

@ -1,6 +1,9 @@
package terraform package terraform
import "github.com/hashicorp/terraform/dot" import (
"github.com/hashicorp/terraform/dag"
"github.com/hashicorp/terraform/dot"
)
// GraphNodeDotter can be implemented by a node to cause it to be included // GraphNodeDotter can be implemented by a node to cause it to be included
// in the dot graph. The Dot method will be called which is expected to // in the dot graph. The Dot method will be called which is expected to
@ -10,25 +13,12 @@ type GraphNodeDotter interface {
// The first parameter is the title of the node. // The first parameter is the title of the node.
// The second parameter includes user-specified options that affect the dot // The second parameter includes user-specified options that affect the dot
// graph. See GraphDotOpts below for details. // graph. See GraphDotOpts below for details.
DotNode(string, *GraphDotOpts) *dot.Node DotNode(string, *dag.DotOpts) *dot.Node
}
// GraphDotOpts are the options for generating a dot formatted Graph.
type GraphDotOpts struct {
// Allows some nodes to decide to only show themselves when the user has
// requested the "verbose" graph.
Verbose bool
// Highlight Cycles
DrawCycles bool
// How many levels to expand modules as we draw
MaxDepth int
} }
// GraphDot returns the dot formatting of a visual representation of // GraphDot returns the dot formatting of a visual representation of
// the given Terraform graph. // the given Terraform graph.
func GraphDot(g *Graph, opts *GraphDotOpts) (string, error) { func GraphDot(g *Graph, opts *dag.DotOpts) (string, error) {
dg, err := NewDebugGraph("root", g, opts) dg, err := NewDebugGraph("root", g, opts)
if err != nil { if err != nil {
return "", err return "", err

View File

@ -4,21 +4,31 @@ import (
"strings" "strings"
"testing" "testing"
"github.com/hashicorp/terraform/dag"
"github.com/hashicorp/terraform/dot" "github.com/hashicorp/terraform/dot"
) )
func TestGraphDot(t *testing.T) { func TestGraphDot(t *testing.T) {
cases := map[string]struct { cases := []struct {
Name string
Graph testGraphFunc Graph testGraphFunc
Opts GraphDotOpts Opts dag.DotOpts
Expect string Expect string
Error string Error string
}{ }{
"empty": { {
Name: "empty",
Graph: func() *Graph { return &Graph{} }, Graph: func() *Graph { return &Graph{} },
Error: "No DOT origin nodes found", Expect: `
digraph {
compound = "true"
newrank = "true"
subgraph "root" {
}
}`,
}, },
"three-level": { {
Name: "three-level",
Graph: func() *Graph { Graph: func() *Graph {
var g Graph var g Graph
root := &testDrawableOrigin{"root"} root := &testDrawableOrigin{"root"}
@ -61,8 +71,10 @@ digraph {
} }
`, `,
}, },
"cycle": {
Opts: GraphDotOpts{ {
Name: "cycle",
Opts: dag.DotOpts{
DrawCycles: true, DrawCycles: true,
}, },
Graph: func() *Graph { Graph: func() *Graph {
@ -108,8 +120,10 @@ digraph {
} }
`, `,
}, },
"subgraphs, no depth restriction": {
Opts: GraphDotOpts{ {
Name: "subgraphs, no depth restriction",
Opts: dag.DotOpts{
MaxDepth: -1, MaxDepth: -1,
}, },
Graph: func() *Graph { Graph: func() *Graph {
@ -159,8 +173,10 @@ digraph {
} }
`, `,
}, },
"subgraphs, with depth restriction": {
Opts: GraphDotOpts{ {
Name: "subgraphs, with depth restriction",
Opts: dag.DotOpts{
MaxDepth: 1, MaxDepth: 1,
}, },
Graph: func() *Graph { Graph: func() *Graph {
@ -208,8 +224,14 @@ digraph {
}, },
} }
for tn, tc := range cases { for _, tc := range cases {
actual, err := GraphDot(tc.Graph(), &tc.Opts) tn := tc.Name
t.Run(tn, func(t *testing.T) {
g := tc.Graph()
var err error
//actual, err := GraphDot(g, &tc.Opts)
actual := string(g.Dot(&tc.Opts))
if err == nil && tc.Error != "" { if err == nil && tc.Error != "" {
t.Fatalf("%s: expected err: %s, got none", tn, tc.Error) t.Fatalf("%s: expected err: %s, got none", tn, tc.Error)
} }
@ -220,13 +242,14 @@ digraph {
if !strings.Contains(err.Error(), tc.Error) { if !strings.Contains(err.Error(), tc.Error) {
t.Fatalf("%s: expected err: %s\nto contain: %s", tn, err, tc.Error) t.Fatalf("%s: expected err: %s\nto contain: %s", tn, err, tc.Error)
} }
continue return
} }
expected := strings.TrimSpace(tc.Expect) + "\n" expected := strings.TrimSpace(tc.Expect) + "\n"
if actual != expected { if actual != expected {
t.Fatalf("%s:\n\nexpected:\n%s\n\ngot:\n%s", tn, expected, actual) t.Fatalf("%s:\n\nexpected:\n%s\n\ngot:\n%s", tn, expected, actual)
} }
})
} }
} }
@ -240,7 +263,7 @@ type testDrawable struct {
func (node *testDrawable) Name() string { func (node *testDrawable) Name() string {
return node.VertexName return node.VertexName
} }
func (node *testDrawable) DotNode(n string, opts *GraphDotOpts) *dot.Node { func (node *testDrawable) DotNode(n string, opts *dag.DotOpts) *dot.Node {
return dot.NewNode(n, map[string]string{}) return dot.NewNode(n, map[string]string{})
} }
func (node *testDrawable) DependableName() []string { func (node *testDrawable) DependableName() []string {
@ -257,7 +280,7 @@ type testDrawableOrigin struct {
func (node *testDrawableOrigin) Name() string { func (node *testDrawableOrigin) Name() string {
return node.VertexName return node.VertexName
} }
func (node *testDrawableOrigin) DotNode(n string, opts *GraphDotOpts) *dot.Node { func (node *testDrawableOrigin) DotNode(n string, opts *dag.DotOpts) *dot.Node {
return dot.NewNode(n, map[string]string{}) return dot.NewNode(n, map[string]string{})
} }
func (node *testDrawableOrigin) DotOrigin() bool { func (node *testDrawableOrigin) DotOrigin() bool {
@ -279,7 +302,7 @@ func (node *testDrawableSubgraph) Name() string {
func (node *testDrawableSubgraph) Subgraph() *Graph { func (node *testDrawableSubgraph) Subgraph() *Graph {
return node.SubgraphMock return node.SubgraphMock
} }
func (node *testDrawableSubgraph) DotNode(n string, opts *GraphDotOpts) *dot.Node { func (node *testDrawableSubgraph) DotNode(n string, opts *dag.DotOpts) *dot.Node {
return dot.NewNode(n, map[string]string{}) return dot.NewNode(n, map[string]string{})
} }
func (node *testDrawableSubgraph) DependentOn() []string { func (node *testDrawableSubgraph) DependentOn() []string {

View File

@ -355,7 +355,7 @@ func (n *graphNodeCloseProvider) CloseProviderName() string {
} }
// GraphNodeDotter impl. // GraphNodeDotter impl.
func (n *graphNodeCloseProvider) DotNode(name string, opts *GraphDotOpts) *dot.Node { func (n *graphNodeCloseProvider) DotNode(name string, opts *dag.DotOpts) *dot.Node {
if !opts.Verbose { if !opts.Verbose {
return nil return nil
} }
@ -393,7 +393,7 @@ func (n *graphNodeProvider) ProviderConfig() *config.RawConfig {
} }
// GraphNodeDotter impl. // GraphNodeDotter impl.
func (n *graphNodeProvider) DotNode(name string, opts *GraphDotOpts) *dot.Node { func (n *graphNodeProvider) DotNode(name string, opts *dag.DotOpts) *dot.Node {
return dot.NewNode(name, map[string]string{ return dot.NewNode(name, map[string]string{
"label": n.Name(), "label": n.Name(),
"shape": "diamond", "shape": "diamond",

View File

@ -102,7 +102,7 @@ func (n *graphNodeDisabledProvider) Name() string {
} }
// GraphNodeDotter impl. // GraphNodeDotter impl.
func (n *graphNodeDisabledProvider) DotNode(name string, opts *GraphDotOpts) *dot.Node { func (n *graphNodeDisabledProvider) DotNode(name string, opts *dag.DotOpts) *dot.Node {
return dot.NewNode(name, map[string]string{ return dot.NewNode(name, map[string]string{
"label": n.Name(), "label": n.Name(),
"shape": "diamond", "shape": "diamond",