mirror of https://github.com/hashicorp/terraform
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.pull/10030/head
parent
bda84e03f7
commit
28d406c040
@ -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)
|
||||
}
|
||||
@ -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
|
||||
}
|
||||
Loading…
Reference in new issue