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:
parent
bda84e03f7
commit
28d406c040
|
@ -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,
|
||||||
|
|
|
@ -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)
|
||||||
|
}
|
15
dag/graph.go
15
dag/graph.go
|
@ -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)
|
||||||
|
|
|
@ -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
|
||||||
|
}
|
|
@ -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",
|
||||||
|
|
|
@ -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",
|
||||||
|
|
|
@ -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
|
||||||
}
|
}
|
||||||
|
|
|
@ -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 {
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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,25 +224,32 @@ digraph {
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
for tn, tc := range cases {
|
for _, tc := range cases {
|
||||||
actual, err := GraphDot(tc.Graph(), &tc.Opts)
|
tn := tc.Name
|
||||||
if err == nil && tc.Error != "" {
|
t.Run(tn, func(t *testing.T) {
|
||||||
t.Fatalf("%s: expected err: %s, got none", tn, tc.Error)
|
g := tc.Graph()
|
||||||
}
|
var err error
|
||||||
if err != nil && tc.Error == "" {
|
//actual, err := GraphDot(g, &tc.Opts)
|
||||||
t.Fatalf("%s: unexpected err: %s", tn, err)
|
actual := string(g.Dot(&tc.Opts))
|
||||||
}
|
|
||||||
if err != nil && tc.Error != "" {
|
|
||||||
if !strings.Contains(err.Error(), tc.Error) {
|
|
||||||
t.Fatalf("%s: expected err: %s\nto contain: %s", tn, err, tc.Error)
|
|
||||||
}
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
expected := strings.TrimSpace(tc.Expect) + "\n"
|
if err == nil && tc.Error != "" {
|
||||||
if actual != expected {
|
t.Fatalf("%s: expected err: %s, got none", tn, tc.Error)
|
||||||
t.Fatalf("%s:\n\nexpected:\n%s\n\ngot:\n%s", tn, expected, actual)
|
}
|
||||||
}
|
if err != nil && tc.Error == "" {
|
||||||
|
t.Fatalf("%s: unexpected err: %s", tn, err)
|
||||||
|
}
|
||||||
|
if err != nil && tc.Error != "" {
|
||||||
|
if !strings.Contains(err.Error(), tc.Error) {
|
||||||
|
t.Fatalf("%s: expected err: %s\nto contain: %s", tn, err, tc.Error)
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
expected := strings.TrimSpace(tc.Expect) + "\n"
|
||||||
|
if actual != expected {
|
||||||
|
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 {
|
||||||
|
|
|
@ -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",
|
||||||
|
|
|
@ -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",
|
||||||
|
|
Loading…
Reference in New Issue