Merge pull request #10030 from hashicorp/jbardin/debug

debug: next steps
This commit is contained in:
James Bardin 2016-11-14 12:15:16 -05:00 committed by GitHub
commit 9c83924a74
26 changed files with 1318 additions and 748 deletions

View File

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

View File

@ -24,6 +24,10 @@ type WalkFunc func(Vertex) error
// walk as an argument
type DepthWalkFunc func(Vertex, int) error
func (g *AcyclicGraph) DirectedGraph() Grapher {
return g
}
// Returns a Set that includes every Vertex yielded by walking down from the
// provided starting Vertex v.
func (g *AcyclicGraph) Ancestors(v Vertex) (*Set, error) {
@ -99,6 +103,8 @@ func (g *AcyclicGraph) TransitiveReduction() {
// v such that the edge (u,v) exists (v is a direct descendant of u).
//
// For each v-prime reachable from v, remove the edge (u, v-prime).
defer g.debug.BeginReduction().End()
for _, u := range g.Vertices() {
uTargets := g.DownEdges(u)
vs := AsVertexList(g.DownEdges(u))
@ -161,6 +167,8 @@ func (g *AcyclicGraph) Cycles() [][]Vertex {
// This will walk nodes in parallel if it can. Because the walk is done
// in parallel, the error returned will be a multierror.
func (g *AcyclicGraph) Walk(cb WalkFunc) error {
defer g.debug.BeginWalk().End()
// Cache the vertices since we use it multiple times
vertices := g.Vertices()
@ -270,6 +278,8 @@ type vertexAtDepth struct {
// the vertices in start. This is not exported now but it would make sense
// to export this publicly at some point.
func (g *AcyclicGraph) DepthFirstWalk(start []Vertex, f DepthWalkFunc) error {
defer g.debug.BeginDepthFirstWalk().End()
seen := make(map[Vertex]struct{})
frontier := make([]*vertexAtDepth, len(start))
for i, v := range start {
@ -312,6 +322,8 @@ func (g *AcyclicGraph) DepthFirstWalk(start []Vertex, f DepthWalkFunc) error {
// reverseDepthFirstWalk does a depth-first walk _up_ the graph starting from
// the vertices in start.
func (g *AcyclicGraph) ReverseDepthFirstWalk(start []Vertex, f DepthWalkFunc) error {
defer g.debug.BeginReverseDepthFirstWalk().End()
seen := make(map[Vertex]struct{})
frontier := make([]*vertexAtDepth, len(start))
for i, v := range start {

261
dag/dot.go Normal file
View File

@ -0,0 +1,261 @@
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
}
// 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
// return a representation of this node.
type GraphNodeDotter interface {
// Dot is called to return the dot formatting for the node.
// The first parameter is the title of the node.
// The second parameter includes user-specified options that affect the dot
// graph. See GraphDotOpts below for details.
DotNode(string, *DotOpts) *DotNode
}
// DotNode provides a structure for Vertices to return in order to specify their
// dot format.
type DotNode struct {
Name string
Attrs map[string]string
}
// 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")
}
// list of Vertices that aren't to be included in the dot output
skip := map[string]bool{}
for _, v := range g.Vertices {
if !v.graphNodeDotter {
skip[v.ID] = true
continue
}
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]
if skip[src.ID] || skip[tgt.ID] {
continue
}
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,9 +2,10 @@ package dag
import (
"bytes"
"encoding/json"
"fmt"
"io"
"sort"
"sync"
)
// Graph is used to represent a dependency graph.
@ -13,7 +14,21 @@ type Graph struct {
edges *Set
downEdges map[interface{}]*Set
upEdges map[interface{}]*Set
once sync.Once
// JSON encoder for recording debug information
debug *encoder
}
// Subgrapher allows a Vertex to be a Graph itself, by returning a Grapher.
type Subgrapher interface {
Subgraph() Grapher
}
// A Grapher is any type that returns a Grapher, mainly used to identify
// dag.Graph and dag.AcyclicGraph. In the case of Graph and AcyclicGraph, they
// return themselves.
type Grapher interface {
DirectedGraph() Grapher
}
// Vertex of the graph.
@ -26,6 +41,10 @@ type NamedVertex interface {
Name() string
}
func (g *Graph) DirectedGraph() Grapher {
return g
}
// Vertices returns the list of all the vertices in the graph.
func (g *Graph) Vertices() []Vertex {
list := g.vertices.List()
@ -87,8 +106,9 @@ func (g *Graph) HasEdge(e Edge) bool {
// Add adds a vertex to the graph. This is safe to call multiple time with
// the same Vertex.
func (g *Graph) Add(v Vertex) Vertex {
g.once.Do(g.init)
g.init()
g.vertices.Add(v)
g.debug.Add(v)
return v
}
@ -97,6 +117,7 @@ func (g *Graph) Add(v Vertex) Vertex {
func (g *Graph) Remove(v Vertex) Vertex {
// Delete the vertex itself
g.vertices.Delete(v)
g.debug.Remove(v)
// Delete the edges to non-existent things
for _, target := range g.DownEdges(v).List() {
@ -118,6 +139,8 @@ func (g *Graph) Replace(original, replacement Vertex) bool {
return false
}
defer g.debug.BeginReplace().End()
// If they're the same, then don't do anything
if original == replacement {
return true
@ -140,7 +163,8 @@ func (g *Graph) Replace(original, replacement Vertex) bool {
// RemoveEdge removes an edge from the graph.
func (g *Graph) RemoveEdge(edge Edge) {
g.once.Do(g.init)
g.init()
g.debug.RemoveEdge(edge)
// Delete the edge from the set
g.edges.Delete(edge)
@ -156,13 +180,13 @@ func (g *Graph) RemoveEdge(edge Edge) {
// DownEdges returns the outward edges from the source Vertex v.
func (g *Graph) DownEdges(v Vertex) *Set {
g.once.Do(g.init)
g.init()
return g.downEdges[hashcode(v)]
}
// UpEdges returns the inward edges to the destination Vertex v.
func (g *Graph) UpEdges(v Vertex) *Set {
g.once.Do(g.init)
g.init()
return g.upEdges[hashcode(v)]
}
@ -171,7 +195,8 @@ func (g *Graph) UpEdges(v Vertex) *Set {
// verified through pointer equality of the vertices, not through the
// value of the edge itself.
func (g *Graph) Connect(edge Edge) {
g.once.Do(g.init)
g.init()
g.debug.Connect(edge)
source := edge.Source()
target := edge.Target()
@ -285,10 +310,51 @@ func (g *Graph) String() string {
}
func (g *Graph) init() {
g.vertices = new(Set)
g.edges = new(Set)
g.downEdges = make(map[interface{}]*Set)
g.upEdges = make(map[interface{}]*Set)
if g.vertices == nil {
g.vertices = new(Set)
}
if g.edges == nil {
g.edges = new(Set)
}
if g.downEdges == nil {
g.downEdges = make(map[interface{}]*Set)
}
if g.upEdges == nil {
g.upEdges = make(map[interface{}]*Set)
}
}
// Dot returns a dot-formatted representation of the Graph.
func (g *Graph) Dot(opts *DotOpts) []byte {
return newMarshalGraph("", g).Dot(opts)
}
// MarshalJSON returns a JSON representation of the entire Graph.
func (g *Graph) MarshalJSON() ([]byte, error) {
dg := newMarshalGraph("root", g)
return json.MarshalIndent(dg, "", " ")
}
// SetDebugWriter sets the io.Writer where the Graph will record debug
// information. After this is set, the graph will immediately encode itself to
// the stream, and continue to record all subsequent operations.
func (g *Graph) SetDebugWriter(w io.Writer) {
g.debug = &encoder{w}
g.debug.Encode(newMarshalGraph("root", g))
}
// DebugVertexInfo encodes arbitrary information about a vertex in the graph
// debug logs.
func (g *Graph) DebugVertexInfo(v Vertex, info string) {
va := newVertexDebugInfo(v, info)
g.debug.Encode(va)
}
// DebugEdgeInfo encodes arbitrary information about an edge in the graph debug
// logs.
func (g *Graph) DebugEdgeInfo(e Edge, info string) {
ea := newEdgeDebugInfo(e, info)
g.debug.Encode(ea)
}
// VertexName returns the name of a vertex.

500
dag/marshal.go Normal file
View File

@ -0,0 +1,500 @@
package dag
import (
"encoding/json"
"fmt"
"io"
"log"
"reflect"
"sort"
"strconv"
)
// the marshal* structs are for serialization of the graph data.
type marshalGraph struct {
// Type is always "Graph", for identification as a top level object in the
// JSON stream.
Type string
// Each marshal structure requires a unique ID so that it can be referenced
// by other structures.
ID string `json:",omitempty"`
// Human readable name for this graph.
Name string `json:",omitempty"`
// Arbitrary attributes that can be added to the output.
Attrs map[string]string `json:",omitempty"`
// List of graph vertices, sorted by ID.
Vertices []*marshalVertex `json:",omitempty"`
// List of edges, sorted by Source ID.
Edges []*marshalEdge `json:",omitempty"`
// Any number of subgraphs. A subgraph itself is considered a vertex, and
// may be referenced by either end of an edge.
Subgraphs []*marshalGraph `json:",omitempty"`
// Any lists of vertices that are included in cycles.
Cycles [][]*marshalVertex `json:",omitempty"`
}
// The add, remove, connect, removeEdge methods mirror the basic Graph
// manipulations to reconstruct a marshalGraph from a debug log.
func (g *marshalGraph) add(v *marshalVertex) {
g.Vertices = append(g.Vertices, v)
sort.Sort(vertices(g.Vertices))
}
func (g *marshalGraph) remove(v *marshalVertex) {
for i, existing := range g.Vertices {
if v.ID == existing.ID {
g.Vertices = append(g.Vertices[:i], g.Vertices[i+1:]...)
return
}
}
}
func (g *marshalGraph) connect(e *marshalEdge) {
g.Edges = append(g.Edges, e)
sort.Sort(edges(g.Edges))
}
func (g *marshalGraph) removeEdge(e *marshalEdge) {
for i, existing := range g.Edges {
if e.Source == existing.Source && e.Target == existing.Target {
g.Edges = append(g.Edges[:i], g.Edges[i+1:]...)
return
}
}
}
func (g *marshalGraph) vertexByID(id string) *marshalVertex {
for _, v := range g.Vertices {
if id == v.ID {
return v
}
}
return nil
}
type marshalVertex struct {
// Unique ID, used to reference this vertex from other structures.
ID string
// Human readable name
Name string `json:",omitempty"`
Attrs map[string]string `json:",omitempty"`
// This is to help transition from the old Dot interfaces. We record if the
// node was a GraphNodeDotter here, so we know if it should be included in the
// dot output
graphNodeDotter bool
}
func newMarshalVertex(v Vertex) *marshalVertex {
return &marshalVertex{
ID: marshalVertexID(v),
Name: VertexName(v),
Attrs: make(map[string]string),
graphNodeDotter: isDotter(v),
}
}
func isDotter(v Vertex) bool {
dn, isDotter := v.(GraphNodeDotter)
dotOpts := &DotOpts{
Verbose: true,
DrawCycles: true,
}
if isDotter && dn.DotNode("fake", dotOpts) == nil {
isDotter = false
}
return isDotter
}
// vertices is a sort.Interface implementation for sorting vertices by ID
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 {
// Human readable name
Name string
// Source and Target Vertices by ID
Source string
Target string
Attrs map[string]string `json:",omitempty"`
}
func newMarshalEdge(e Edge) *marshalEdge {
return &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),
}
}
// edges is a sort.Interface implementation for sorting edges by Source ID
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] }
// build a marshalGraph structure from a *Graph
func newMarshalGraph(name string, g *Graph) *marshalGraph {
mg := &marshalGraph{
Type: "Graph",
Name: name,
Attrs: make(map[string]string),
}
for _, v := range g.Vertices() {
id := marshalVertexID(v)
if sg, ok := marshalSubgrapher(v); ok {
smg := newMarshalGraph(VertexName(v), sg)
smg.ID = id
mg.Subgraphs = append(mg.Subgraphs, smg)
}
mv := newMarshalVertex(v)
mg.Vertices = append(mg.Vertices, mv)
}
sort.Sort(vertices(mg.Vertices))
for _, e := range g.Edges() {
mg.Edges = append(mg.Edges, newMarshalEdge(e))
}
sort.Sort(edges(mg.Edges))
for _, c := range (&AcyclicGraph{*g}).Cycles() {
var cycle []*marshalVertex
for _, v := range c {
mv := newMarshalVertex(v)
cycle = append(cycle, mv)
}
mg.Cycles = append(mg.Cycles, cycle)
}
return mg
}
// 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
}
}
// fallback to a name, which we hope is unique.
return VertexName(v)
// we could try harder by attempting to read the arbitrary value from the
// interface, but we shouldn't get here from terraform right now.
}
// check for a Subgrapher, and return the underlying *Graph.
func marshalSubgrapher(v Vertex) (*Graph, bool) {
sg, ok := v.(Subgrapher)
if !ok {
return nil, false
}
switch g := sg.Subgraph().DirectedGraph().(type) {
case *Graph:
return g, true
case *AcyclicGraph:
return &g.Graph, true
}
return nil, false
}
// ender provides a way to call any End* method expression via an End method
type ender func()
func (e ender) End() { e() }
// encoder provides methods to write debug data to an io.Writer, and is a noop
// when no writer is present
type encoder struct {
w io.Writer
}
// Encode is analogous to json.Encoder.Encode
func (e *encoder) Encode(i interface{}) {
if e == nil || e.w == nil {
return
}
js, err := json.Marshal(i)
if err != nil {
log.Println("[ERROR] dag:", err)
return
}
js = append(js, '\n')
_, err = e.w.Write(js)
if err != nil {
log.Println("[ERROR] dag:", err)
return
}
}
func (e *encoder) Add(v Vertex) {
e.Encode(marshalTransform{
Type: "Transform",
AddVertex: newMarshalVertex(v),
})
}
// Remove records the removal of Vertex v.
func (e *encoder) Remove(v Vertex) {
e.Encode(marshalTransform{
Type: "Transform",
RemoveVertex: newMarshalVertex(v),
})
}
func (e *encoder) Connect(edge Edge) {
e.Encode(marshalTransform{
Type: "Transform",
AddEdge: newMarshalEdge(edge),
})
}
func (e *encoder) RemoveEdge(edge Edge) {
e.Encode(marshalTransform{
Type: "Transform",
RemoveEdge: newMarshalEdge(edge),
})
}
// BeginReplace marks the start of a replace operation, and returns the encoder
// to chain the EndReplace call.
func (e *encoder) BeginReplace() ender {
e.Encode(marshalOperation{
Type: "Operation",
Begin: newString("Replace"),
})
return e.EndReplace
}
func (e *encoder) EndReplace() {
e.Encode(marshalOperation{
Type: "Operation",
End: newString("Replace"),
})
}
// BeginReduction marks the start of a replace operation, and returns the encoder
// to chain the EndReduction call.
func (e *encoder) BeginReduction() ender {
e.Encode(marshalOperation{
Type: "Operation",
Begin: newString("Reduction"),
})
return e.EndReduction
}
func (e *encoder) EndReduction() {
e.Encode(marshalOperation{
Type: "Operation",
End: newString("Reduction"),
})
}
// BeginDepthFirstWalk marks the start of a replace operation, and returns the
// encoder to chain the EndDepthFirstWalk call.
func (e *encoder) BeginDepthFirstWalk() ender {
e.Encode(marshalOperation{
Type: "Operation",
Begin: newString("DepthFirstWalk"),
})
return e.EndDepthFirstWalk
}
func (e *encoder) EndDepthFirstWalk() {
e.Encode(marshalOperation{
Type: "Operation",
End: newString("DepthFirstWalk"),
})
}
// BeginReverseDepthFirstWalk marks the start of a replace operation, and
// returns the encoder to chain the EndReverseDepthFirstWalk call.
func (e *encoder) BeginReverseDepthFirstWalk() ender {
e.Encode(marshalOperation{
Type: "Operation",
Begin: newString("ReverseDepthFirstWalk"),
})
return e.EndReverseDepthFirstWalk
}
func (e *encoder) EndReverseDepthFirstWalk() {
e.Encode(marshalOperation{
Type: "Operation",
End: newString("ReverseDepthFirstWalk"),
})
}
// BeginWalk marks the start of a replace operation, and returns the encoder
// to chain the EndWalk call.
func (e *encoder) BeginWalk() ender {
e.Encode(marshalOperation{
Type: "Operation",
Begin: newString("Walk"),
})
return e.EndWalk
}
func (e *encoder) EndWalk() {
e.Encode(marshalOperation{
Type: "Operation",
End: newString("Walk"),
})
}
// structure for recording graph transformations
type marshalTransform struct {
// Type: "Transform"
Type string
AddEdge *marshalEdge `json:",omitempty"`
RemoveEdge *marshalEdge `json:",omitempty"`
AddVertex *marshalVertex `json:",omitempty"`
RemoveVertex *marshalVertex `json:",omitempty"`
}
func (t marshalTransform) Transform(g *marshalGraph) {
switch {
case t.AddEdge != nil:
g.connect(t.AddEdge)
case t.RemoveEdge != nil:
g.removeEdge(t.RemoveEdge)
case t.AddVertex != nil:
g.add(t.AddVertex)
case t.RemoveVertex != nil:
g.remove(t.RemoveVertex)
}
}
// this structure allows us to decode any object in the json stream for
// inspection, then re-decode it into a proper struct if needed.
type streamDecode struct {
Type string
Map map[string]interface{}
JSON []byte
}
func (s *streamDecode) UnmarshalJSON(d []byte) error {
s.JSON = d
err := json.Unmarshal(d, &s.Map)
if err != nil {
return err
}
if t, ok := s.Map["Type"]; ok {
s.Type, _ = t.(string)
}
return nil
}
// structure for recording the beginning and end of any multi-step
// transformations. These are informational, and not required to reproduce the
// graph state.
type marshalOperation struct {
Type string
Begin *string `json:",omitempty"`
End *string `json:",omitempty"`
Info *string `json:".omitempty"`
}
func newBool(b bool) *bool { return &b }
func newString(s string) *string { return &s }
// decodeGraph decodes a marshalGraph from an encoded graph stream.
func decodeGraph(r io.Reader) (*marshalGraph, error) {
dec := json.NewDecoder(r)
// a stream should always start with a graph
g := &marshalGraph{}
err := dec.Decode(g)
if err != nil {
return nil, err
}
// now replay any operations that occurred on the original graph
for dec.More() {
s := &streamDecode{}
err := dec.Decode(s)
if err != nil {
return g, err
}
// the only Type we're concerned with here is Transform to complete the
// Graph
if s.Type != "Transform" {
continue
}
t := &marshalTransform{}
err = json.Unmarshal(s.JSON, t)
if err != nil {
return g, err
}
t.Transform(g)
}
return g, nil
}
// *DebugInfo structs allow encoding arbitrary information about the graph in
// the logs.
type vertexDebugInfo struct {
Type string
Vertex *marshalVertex
Info string
}
func newVertexDebugInfo(v Vertex, info string) *vertexDebugInfo {
return &vertexDebugInfo{
Type: "VertexDebugInfo",
Vertex: newMarshalVertex(v),
Info: info,
}
}
type edgeDebugInfo struct {
Type string
Edge *marshalEdge
Info string
}
func newEdgeDebugInfo(e Edge, info string) *edgeDebugInfo {
return &edgeDebugInfo{
Type: "EdgeDebugInfo",
Edge: newMarshalEdge(e),
Info: info,
}
}

253
dag/marshal_test.go Normal file
View File

@ -0,0 +1,253 @@
package dag
import (
"bytes"
"encoding/json"
"strings"
"testing"
)
func TestGraphDot_empty(t *testing.T) {
var g Graph
g.Add(1)
g.Add(2)
g.Add(3)
actual := strings.TrimSpace(string(g.Dot(nil)))
expected := strings.TrimSpace(testGraphDotEmptyStr)
if actual != expected {
t.Fatalf("bad: %s", actual)
}
}
func TestGraphDot_basic(t *testing.T) {
var g Graph
g.Add(1)
g.Add(2)
g.Add(3)
g.Connect(BasicEdge(1, 3))
actual := strings.TrimSpace(string(g.Dot(nil)))
expected := strings.TrimSpace(testGraphDotBasicStr)
if actual != expected {
t.Fatalf("bad: %s", actual)
}
}
const testGraphDotBasicStr = `digraph {
compound = "true"
newrank = "true"
subgraph "root" {
"[root] 1" -> "[root] 3"
}
}
`
const testGraphDotEmptyStr = `digraph {
compound = "true"
newrank = "true"
subgraph "root" {
}
}`
func TestGraphJSON_empty(t *testing.T) {
var g Graph
g.Add(1)
g.Add(2)
g.Add(3)
js, err := g.MarshalJSON()
if err != nil {
t.Fatal(err)
}
actual := strings.TrimSpace(string(js))
expected := strings.TrimSpace(testGraphJSONEmptyStr)
if actual != expected {
t.Fatalf("bad: %s", actual)
}
}
func TestGraphJSON_basic(t *testing.T) {
var g Graph
g.Add(1)
g.Add(2)
g.Add(3)
g.Connect(BasicEdge(1, 3))
js, err := g.MarshalJSON()
if err != nil {
t.Fatal(err)
}
actual := strings.TrimSpace(string(js))
expected := strings.TrimSpace(testGraphJSONBasicStr)
if actual != expected {
t.Fatalf("bad: %s", actual)
}
}
// record some graph transformations, and make sure we get the same graph when
// they're replayed
func TestGraphJSON_basicRecord(t *testing.T) {
var g Graph
var buf bytes.Buffer
g.SetDebugWriter(&buf)
g.Add(1)
g.Add(2)
g.Add(3)
g.Connect(BasicEdge(1, 2))
g.Connect(BasicEdge(1, 3))
g.Connect(BasicEdge(2, 3))
(&AcyclicGraph{g}).TransitiveReduction()
recorded := buf.Bytes()
// the Walk doesn't happen in a determined order, so just count operations
// for now to make sure we wrote stuff out.
if len(bytes.Split(recorded, []byte{'\n'})) != 17 {
t.Fatalf("bad: %s", recorded)
}
original, err := g.MarshalJSON()
if err != nil {
t.Fatal(err)
}
// replay the logs, and marshal the graph back out again
m, err := decodeGraph(bytes.NewReader(buf.Bytes()))
if err != nil {
t.Fatal(err)
}
replayed, err := json.MarshalIndent(m, "", " ")
if err != nil {
t.Fatal(err)
}
if !bytes.Equal(original, replayed) {
t.Fatalf("\noriginal: %s\nreplayed: %s", original, replayed)
}
}
// Verify that Vertex and Edge annotations appear in the debug output
func TestGraphJSON_debugInfo(t *testing.T) {
var g Graph
var buf bytes.Buffer
g.SetDebugWriter(&buf)
g.Add(1)
g.Add(2)
g.Add(3)
g.Connect(BasicEdge(1, 2))
g.DebugVertexInfo(2, "2")
g.DebugVertexInfo(3, "3")
g.DebugEdgeInfo(BasicEdge(1, 2), "1|2")
dec := json.NewDecoder(bytes.NewReader(buf.Bytes()))
var found2, found3, foundEdge bool
for dec.More() {
var d streamDecode
err := dec.Decode(&d)
if err != nil {
t.Fatal(err)
}
switch d.Type {
case "VertexDebugInfo":
va := &vertexDebugInfo{}
err := json.Unmarshal(d.JSON, va)
if err != nil {
t.Fatal(err)
}
switch va.Info {
case "2":
if va.Vertex.Name != "2" {
t.Fatalf("wrong vertex annotated 2: %#v", va)
}
found2 = true
case "3":
if va.Vertex.Name != "3" {
t.Fatalf("wrong vertex annotated 3: %#v", va)
}
found3 = true
default:
t.Fatalf("unexpected annotation: %#v", va)
}
case "EdgeDebugInfo":
ea := &edgeDebugInfo{}
err := json.Unmarshal(d.JSON, ea)
if err != nil {
t.Fatal(err)
}
switch ea.Info {
case "1|2":
if ea.Edge.Name != "1|2" {
t.Fatalf("incorrect edge annotation: %#v\n", ea)
}
foundEdge = true
default:
t.Fatalf("unexpected edge Info: %#v", ea)
}
}
}
if !found2 {
t.Fatal("annotation 2 not found")
}
if !found3 {
t.Fatal("annotation 3 not found")
}
if !foundEdge {
t.Fatal("edge annotation not found")
}
}
const testGraphJSONEmptyStr = `{
"Type": "Graph",
"Name": "root",
"Vertices": [
{
"ID": "1",
"Name": "1"
},
{
"ID": "2",
"Name": "2"
},
{
"ID": "3",
"Name": "3"
}
]
}`
const testGraphJSONBasicStr = `{
"Type": "Graph",
"Name": "root",
"Vertices": [
{
"ID": "1",
"Name": "1"
},
{
"ID": "2",
"Name": "2"
},
{
"ID": "3",
"Name": "3"
}
],
"Edges": [
{
"Name": "1|3",
"Source": "1",
"Target": "3"
}
]
}`

View File

@ -1,237 +0,0 @@
// The dot package contains utilities for working with DOT graphs.
package dot
import (
"bytes"
"fmt"
"sort"
"strings"
)
// Graph is a representation of a drawable DOT graph.
type Graph struct {
// Whether this is a "digraph" or just a "graph"
Directed bool
// Used for K/V settings in the DOT
Attrs map[string]string
Nodes []*Node
Edges []*Edge
Subgraphs []*Subgraph
nodesByName map[string]*Node
}
// Subgraph is a Graph that lives inside a Parent graph, and contains some
// additional parameters to control how it is drawn.
type Subgraph struct {
Graph
Name string
Parent *Graph
Cluster bool
}
// An Edge in a DOT graph, as expressed by recording the Name of the Node at
// each end.
type Edge struct {
// Name of source node.
Source string
// Name of dest node.
Dest string
// List of K/V attributes for this edge.
Attrs map[string]string
}
// A Node in a DOT graph.
type Node struct {
Name string
Attrs map[string]string
}
// Creates a properly initialized DOT Graph.
func NewGraph(attrs map[string]string) *Graph {
return &Graph{
Attrs: attrs,
nodesByName: make(map[string]*Node),
}
}
func NewEdge(src, dst string, attrs map[string]string) *Edge {
return &Edge{
Source: src,
Dest: dst,
Attrs: attrs,
}
}
func NewNode(n string, attrs map[string]string) *Node {
return &Node{
Name: n,
Attrs: attrs,
}
}
// Initializes a Subgraph with the provided name, attaches is to this Graph,
// and returns it.
func (g *Graph) AddSubgraph(name string) *Subgraph {
subgraph := &Subgraph{
Graph: *NewGraph(map[string]string{}),
Parent: g,
Name: name,
}
g.Subgraphs = append(g.Subgraphs, subgraph)
return subgraph
}
func (g *Graph) AddAttr(k, v string) {
g.Attrs[k] = v
}
func (g *Graph) AddNode(n *Node) {
g.Nodes = append(g.Nodes, n)
g.nodesByName[n.Name] = n
}
func (g *Graph) AddEdge(e *Edge) {
g.Edges = append(g.Edges, e)
}
// Adds an edge between two Nodes.
//
// Note this does not do any verification of the existence of these nodes,
// which means that any strings you provide that are not existing nodes will
// result in extra auto-defined nodes in your resulting DOT.
func (g *Graph) AddEdgeBetween(src, dst string, attrs map[string]string) error {
g.AddEdge(NewEdge(src, dst, attrs))
return nil
}
// Look up a node by name
func (g *Graph) GetNode(name string) (*Node, error) {
node, ok := g.nodesByName[name]
if !ok {
return nil, fmt.Errorf("Could not find node: %s", name)
}
return node, nil
}
// Returns the DOT representation of this Graph.
func (g *Graph) String() string {
w := newGraphWriter()
g.drawHeader(w)
w.Indent()
g.drawBody(w)
w.Unindent()
g.drawFooter(w)
return w.String()
}
// Returns the DOT representation of this Graph.
func (g *Graph) Bytes() []byte {
w := newGraphWriter()
g.drawHeader(w)
w.Indent()
g.drawBody(w)
w.Unindent()
g.drawFooter(w)
return w.Bytes()
}
func (g *Graph) drawHeader(w *graphWriter) {
if g.Directed {
w.Printf("digraph {\n")
} else {
w.Printf("graph {\n")
}
}
func (g *Graph) drawBody(w *graphWriter) {
for _, as := range attrStrings(g.Attrs) {
w.Printf("%s\n", as)
}
nodeStrings := make([]string, 0, len(g.Nodes))
for _, n := range g.Nodes {
nodeStrings = append(nodeStrings, n.String())
}
sort.Strings(nodeStrings)
for _, ns := range nodeStrings {
w.Printf(ns)
}
edgeStrings := make([]string, 0, len(g.Edges))
for _, e := range g.Edges {
edgeStrings = append(edgeStrings, e.String())
}
sort.Strings(edgeStrings)
for _, es := range edgeStrings {
w.Printf(es)
}
for _, s := range g.Subgraphs {
s.drawHeader(w)
w.Indent()
s.drawBody(w)
w.Unindent()
s.drawFooter(w)
}
}
func (g *Graph) drawFooter(w *graphWriter) {
w.Printf("}\n")
}
// Returns the DOT representation of this Edge.
func (e *Edge) String() string {
var buf bytes.Buffer
buf.WriteString(
fmt.Sprintf(
"%q -> %q", e.Source, e.Dest))
writeAttrs(&buf, e.Attrs)
buf.WriteString("\n")
return buf.String()
}
func (s *Subgraph) drawHeader(w *graphWriter) {
name := s.Name
if s.Cluster {
name = fmt.Sprintf("cluster_%s", name)
}
w.Printf("subgraph %q {\n", name)
}
// Returns the DOT representation of this Node.
func (n *Node) String() string {
var buf bytes.Buffer
buf.WriteString(fmt.Sprintf("%q", n.Name))
writeAttrs(&buf, n.Attrs)
buf.WriteString("\n")
return buf.String()
}
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
}

View File

@ -1,47 +0,0 @@
package dot
import (
"bytes"
"fmt"
)
// graphWriter wraps a bytes.Buffer and tracks indent level levels.
type graphWriter struct {
bytes.Buffer
indent int
indentStr string
}
// Returns an initialized graphWriter at indent level 0.
func newGraphWriter() *graphWriter {
w := &graphWriter{
indent: 0,
}
w.init()
return w
}
// Prints to the buffer at the current indent level.
func (w *graphWriter) Printf(s string, args ...interface{}) {
w.WriteString(w.indentStr + fmt.Sprintf(s, args...))
}
// Increase the indent level.
func (w *graphWriter) Indent() {
w.indent++
w.init()
}
// Decrease the indent level.
func (w *graphWriter) Unindent() {
w.indent--
w.init()
}
func (w *graphWriter) init() {
indentBuf := new(bytes.Buffer)
for i := 0; i < w.indent; i++ {
indentBuf.WriteString("\t")
}
w.indentStr = indentBuf.String()
}

View File

@ -697,11 +697,9 @@ func (c *Context) walk(
log.Printf("[DEBUG] Starting graph walk: %s", operation.String())
dg, _ := NewDebugGraph("walk", graph, nil)
walker := &ContextGraphWalker{
Context: realCtx,
Operation: operation,
DebugGraph: dg,
Context: realCtx,
Operation: operation,
}
// Watch for a stop so we can call the provider Stop() API.
@ -728,20 +726,13 @@ func (c *Context) walk(
// If we have a shadow graph, wait for that to complete.
if shadowCloser != nil {
// create a debug graph for this walk
dg, err := NewDebugGraph("walk-shadow", shadow, nil)
if err != nil {
log.Printf("[ERROR] %v", err)
}
// Build the graph walker for the shadow. We also wrap this in
// a panicwrap so that panics are captured. For the shadow graph,
// we just want panics to be normal errors rather than to crash
// Terraform.
shadowWalker := GraphWalkerPanicwrap(&ContextGraphWalker{
Context: shadowCtx,
Operation: operation,
DebugGraph: dg,
Context: shadowCtx,
Operation: operation,
})
// Kick off the shadow walk. This will block on any operations

View File

@ -166,6 +166,41 @@ func (d *debugInfo) Close() error {
return nil
}
// debug buffer is an io.WriteCloser that will write itself to the debug
// archive when closed.
type debugBuffer struct {
debugInfo *debugInfo
name string
buf bytes.Buffer
}
func (b *debugBuffer) Write(d []byte) (int, error) {
return b.buf.Write(d)
}
func (b *debugBuffer) Close() error {
return b.debugInfo.WriteFile(b.name, b.buf.Bytes())
}
// ioutils only has a noop ReadCloser
type nopWriteCloser struct{}
func (nopWriteCloser) Write([]byte) (int, error) { return 0, nil }
func (nopWriteCloser) Close() error { return nil }
// NewFileWriter returns an io.WriteClose that will be buffered and written to
// the debug archive when closed.
func (d *debugInfo) NewFileWriter(name string) io.WriteCloser {
if d == nil {
return nopWriteCloser{}
}
return &debugBuffer{
debugInfo: d,
name: name,
}
}
type syncer interface {
Sync() error
}
@ -192,15 +227,10 @@ func (d *debugInfo) flush() {
// WriteGraph takes a DebugGraph and writes both the DebugGraph as a dot file
// in the debug archive, and extracts any logs that the DebugGraph collected
// and writes them to a log file in the archive.
func (d *debugInfo) WriteGraph(dg *DebugGraph) error {
if d == nil {
func (d *debugInfo) WriteGraph(name string, g *Graph) error {
if d == nil || g == nil {
return nil
}
if dg == nil {
return nil
}
d.Lock()
defer d.Unlock()
@ -209,12 +239,10 @@ func (d *debugInfo) WriteGraph(dg *DebugGraph) error {
// sync'ed.
defer d.flush()
d.writeFile(dg.Name, dg.LogBytes())
dotPath := fmt.Sprintf("%s/graphs/%d-%s-%s.dot", d.name, d.step, d.phase, dg.Name)
dotPath := fmt.Sprintf("%s/graphs/%d-%s-%s.dot", d.name, d.step, d.phase, name)
d.step++
dotBytes := dg.DotBytes()
dotBytes := g.Dot(nil)
hdr := &tar.Header{
Name: dotPath,
Mode: 0644,

View File

@ -16,7 +16,7 @@ func TestDebugInfo_nil(t *testing.T) {
var d *debugInfo
d.SetPhase("none")
d.WriteGraph(nil)
d.WriteGraph("", nil)
d.WriteFile("none", nil)
d.Close()
}
@ -122,6 +122,7 @@ func TestDebug_plan(t *testing.T) {
files := 0
graphs := 0
json := 0
for {
hdr, err := tr.Next()
if err == io.EOF {
@ -139,6 +140,10 @@ func TestDebug_plan(t *testing.T) {
if strings.HasSuffix(hdr.Name, ".dot") {
graphs++
}
if strings.HasSuffix(hdr.Name, "graph.json") {
json++
}
}
}
@ -146,13 +151,13 @@ func TestDebug_plan(t *testing.T) {
t.Fatal("no files with data found")
}
/*
TODO: once @jbardin finishes the dot refactor, uncomment this. This
won't pass since the new graph doesn't implement the dot nodes.
if graphs == 0 {
t.Fatal("no no-empty graphs found")
}
*/
if graphs == 0 {
t.Fatal("no no-empty graphs found")
}
if json == 0 {
t.Fatal("no json graphs")
}
}
// verify that no hooks panic on nil input

View File

@ -40,9 +40,18 @@ type Graph struct {
// edges.
dependableMap map[string]dag.Vertex
// debugName is a name for reference in the debug output. This is usually
// to indicate what topmost builder was, and if this graph is a shadow or
// not.
debugName string
once sync.Once
}
func (g *Graph) DirectedGraph() dag.Grapher {
return &g.AcyclicGraph
}
// Annotations returns the annotations that are configured for the
// given vertex. The map is guaranteed to be non-nil but may be empty.
//
@ -197,7 +206,6 @@ func (g *Graph) Dependable(n string) dag.Vertex {
// will be walked with full parallelism, so the walker should expect
// to be called in concurrently.
func (g *Graph) Walk(walker GraphWalker) error {
defer dbug.WriteGraph(walker.Debug())
return g.walk(walker)
}
@ -225,6 +233,15 @@ func (g *Graph) walk(walker GraphWalker) error {
panicwrap = nil // just to be sure
}
debugName := "walk-graph.json"
if g.debugName != "" {
debugName = g.debugName + "-" + debugName
}
debugBuf := dbug.NewFileWriter(debugName)
g.SetDebugWriter(debugBuf)
defer debugBuf.Close()
// Walk the graph.
var walkFn dag.WalkFunc
walkFn = func(v dag.Vertex) (rerr error) {
@ -254,10 +271,7 @@ func (g *Graph) walk(walker GraphWalker) error {
}()
walker.EnterVertex(v)
defer func() {
walker.Debug().DebugNode(v)
walker.ExitVertex(v, rerr)
}()
defer walker.ExitVertex(v, rerr)
// vertexCtx is the context that we use when evaluating. This
// is normally the context of our graph but can be overridden
@ -279,7 +293,9 @@ func (g *Graph) walk(walker GraphWalker) error {
// Allow the walker to change our tree if needed. Eval,
// then callback with the output.
log.Printf("[DEBUG] vertex '%s.%s': evaluating", path, dag.VertexName(v))
walker.Debug().Printf("[DEBUG] vertex %T(%s.%s): evaluating\n", v, path, dag.VertexName(v))
g.DebugVertexInfo(v, fmt.Sprintf("evaluating %T(%s)", v, path))
tree = walker.EnterEvalTree(v, tree)
output, err := Eval(tree, vertexCtx)
if rerr = walker.ExitEvalTree(v, output, err); rerr != nil {
@ -293,7 +309,9 @@ func (g *Graph) walk(walker GraphWalker) error {
"[DEBUG] vertex '%s.%s': expanding/walking dynamic subgraph",
path,
dag.VertexName(v))
walker.Debug().Printf("[DEBUG] vertex %T(%s.%s): expanding\n", v, path, dag.VertexName(v))
g.DebugVertexInfo(v, fmt.Sprintf("expanding %T(%s)", v, path))
g, err := ev.DynamicExpand(vertexCtx)
if err != nil {
rerr = err
@ -314,10 +332,9 @@ func (g *Graph) walk(walker GraphWalker) error {
path,
dag.VertexName(v))
walker.Debug().Printf(
"[DEBUG] vertex %T(%s.%s): subgraph\n", v, path, dag.VertexName(v))
g.DebugVertexInfo(v, fmt.Sprintf("subgraph: %T(%s)", v, path))
if rerr = sn.Subgraph().walk(walker); rerr != nil {
if rerr = sn.Subgraph().(*Graph).walk(walker); rerr != nil {
return
}
}

View File

@ -29,6 +29,15 @@ type BasicGraphBuilder struct {
func (b *BasicGraphBuilder) Build(path []string) (*Graph, error) {
g := &Graph{Path: path}
debugName := "build-graph.json"
if b.Name != "" {
debugName = b.Name + "-" + debugName
}
debugBuf := dbug.NewFileWriter(debugName)
g.SetDebugWriter(debugBuf)
defer debugBuf.Close()
for _, step := range b.Steps {
if step == nil {
continue
@ -52,8 +61,8 @@ func (b *BasicGraphBuilder) Build(path []string) (*Graph, error) {
"[TRACE] Graph after step %T:\n\n%s",
step, g.StringWithNodeTypes())
dg, _ := NewDebugGraph(debugName, g, nil)
dbug.WriteGraph(dg)
// TODO: replace entirely with the json logs
dbug.WriteGraph(debugName, g)
if err != nil {
return g, err

View File

@ -7,7 +7,6 @@ import (
"github.com/hashicorp/terraform/config"
"github.com/hashicorp/terraform/config/module"
"github.com/hashicorp/terraform/dag"
"github.com/hashicorp/terraform/dot"
)
// GraphNodeConfigModule represents a module within the configuration graph.
@ -129,11 +128,14 @@ func (n *graphNodeModuleExpanded) DependentOn() []string {
}
// GraphNodeDotter impl.
func (n *graphNodeModuleExpanded) DotNode(name string, opts *GraphDotOpts) *dot.Node {
return dot.NewNode(name, map[string]string{
"label": dag.VertexName(n.Original),
"shape": "component",
})
func (n *graphNodeModuleExpanded) DotNode(name string, opts *dag.DotOpts) *dag.DotNode {
return &dag.DotNode{
Name: name,
Attrs: map[string]string{
"label": dag.VertexName(n.Original),
"shape": "component",
},
}
}
// GraphNodeEvalable impl.
@ -156,7 +158,7 @@ func (n *graphNodeModuleExpanded) EvalTree() EvalNode {
// GraphNodeFlattenable impl.
func (n *graphNodeModuleExpanded) FlattenGraph() *Graph {
graph := n.Subgraph()
graph := n.Subgraph().(*Graph)
input := n.Original.Module.RawConfig
// Go over each vertex and do some modifications to the graph for
@ -189,7 +191,7 @@ func (n *graphNodeModuleExpanded) FlattenGraph() *Graph {
}
// GraphNodeSubgraph impl.
func (n *graphNodeModuleExpanded) Subgraph() *Graph {
func (n *graphNodeModuleExpanded) Subgraph() dag.Grapher {
return n.Graph
}

View File

@ -33,7 +33,7 @@ func TestGraphNodeConfigModuleExpand(t *testing.T) {
t.Fatalf("err: %s", err)
}
actual := strings.TrimSpace(g.Subgraph().String())
actual := strings.TrimSpace(g.Subgraph().(*Graph).String())
expected := strings.TrimSpace(testGraphNodeModuleExpandStr)
if actual != expected {
t.Fatalf("bad:\n\n%s", actual)

View File

@ -5,7 +5,6 @@ import (
"github.com/hashicorp/terraform/config"
"github.com/hashicorp/terraform/dag"
"github.com/hashicorp/terraform/dot"
)
// GraphNodeConfigProvider represents a configured provider within the
@ -59,11 +58,14 @@ func (n *GraphNodeConfigProvider) ProviderConfig() *config.RawConfig {
}
// GraphNodeDotter impl.
func (n *GraphNodeConfigProvider) DotNode(name string, opts *GraphDotOpts) *dot.Node {
return dot.NewNode(name, map[string]string{
"label": n.Name(),
"shape": "diamond",
})
func (n *GraphNodeConfigProvider) DotNode(name string, opts *dag.DotOpts) *dag.DotNode {
return &dag.DotNode{
Name: name,
Attrs: map[string]string{
"label": n.Name(),
"shape": "diamond",
},
}
}
// GraphNodeDotterOrigin impl.

View File

@ -7,7 +7,6 @@ import (
"github.com/hashicorp/terraform/config"
"github.com/hashicorp/terraform/dag"
"github.com/hashicorp/terraform/dot"
)
// GraphNodeCountDependent is implemented by resources for giving only
@ -128,14 +127,17 @@ func (n *GraphNodeConfigResource) Name() string {
}
// GraphNodeDotter impl.
func (n *GraphNodeConfigResource) DotNode(name string, opts *GraphDotOpts) *dot.Node {
func (n *GraphNodeConfigResource) DotNode(name string, opts *dag.DotOpts) *dag.DotNode {
if n.Destroy && !opts.Verbose {
return nil
}
return dot.NewNode(name, map[string]string{
"label": n.Name(),
"shape": "box",
})
return &dag.DotNode{
Name: name,
Attrs: map[string]string{
"label": n.Name(),
"shape": "box",
},
}
}
// GraphNodeFlattenable impl.

View File

@ -1,289 +0,0 @@
package terraform
import (
"bytes"
"fmt"
"sync"
"github.com/davecgh/go-spew/spew"
"github.com/hashicorp/terraform/dag"
"github.com/hashicorp/terraform/dot"
)
// The NodeDebug method outputs debug information to annotate the graphs
// stored in the DebugInfo
type GraphNodeDebugger interface {
NodeDebug() string
}
type GraphNodeDebugOrigin interface {
DotOrigin() bool
}
type DebugGraph struct {
// TODO: can we combine this and dot.Graph into a generalized graph representation?
sync.Mutex
Name string
ord int
buf bytes.Buffer
Dot *dot.Graph
dotOpts *GraphDotOpts
}
// DebugGraph holds a dot representation of the Terraform graph, and can be
// 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
// will be also be written out to the DebugInfo archive.
func NewDebugGraph(name string, g *Graph, opts *GraphDotOpts) (*DebugGraph, error) {
dg := &DebugGraph{
Name: name,
dotOpts: opts,
}
err := dg.build(g)
if err != nil {
dbug.WriteFile(dg.Name, []byte(err.Error()))
return nil, err
}
return dg, nil
}
// Printf to the internal buffer
func (dg *DebugGraph) Printf(f string, args ...interface{}) (int, error) {
if dg == nil {
return 0, nil
}
dg.Lock()
defer dg.Unlock()
return fmt.Fprintf(&dg.buf, f, args...)
}
// Write to the internal buffer
func (dg *DebugGraph) Write(b []byte) (int, error) {
if dg == nil {
return 0, nil
}
dg.Lock()
defer dg.Unlock()
return dg.buf.Write(b)
}
func (dg *DebugGraph) LogBytes() []byte {
if dg == nil {
return nil
}
dg.Lock()
defer dg.Unlock()
return dg.buf.Bytes()
}
func (dg *DebugGraph) DotBytes() []byte {
if dg == nil {
return nil
}
dg.Lock()
defer dg.Unlock()
return dg.Dot.Bytes()
}
func (dg *DebugGraph) DebugNode(v interface{}) {
if dg == nil {
return
}
dg.Lock()
defer dg.Unlock()
// record the ordinal value for each node
ord := dg.ord
dg.ord++
name := graphDotNodeName("root", v)
var node *dot.Node
// TODO: recursive
for _, sg := range dg.Dot.Subgraphs {
node, _ = sg.GetNode(name)
if node != nil {
break
}
}
// record as much of the node data structure as we can
spew.Fdump(&dg.buf, v)
// for now, record the order of visits in the node label
if node != nil {
node.Attrs["label"] = fmt.Sprintf("%s %d", node.Attrs["label"], ord)
}
// if the node provides debug output, insert it into the graph, and log it
if nd, ok := v.(GraphNodeDebugger); ok {
out := nd.NodeDebug()
if node != nil {
node.Attrs["comment"] = out
dg.buf.WriteString(fmt.Sprintf("NodeDebug (%s):'%s'\n", name, out))
}
}
}
// takes a Terraform Graph and build the internal debug graph
func (dg *DebugGraph) build(g *Graph) error {
if dg == nil {
return nil
}
dg.Lock()
defer dg.Unlock()
dg.Dot = dot.NewGraph(map[string]string{
"compound": "true",
"newrank": "true",
})
dg.Dot.Directed = true
if dg.dotOpts == nil {
dg.dotOpts = &GraphDotOpts{
DrawCycles: true,
MaxDepth: -1,
Verbose: true,
}
}
err := dg.buildSubgraph("root", g, 0)
if err != nil {
return err
}
return nil
}
func (dg *DebugGraph) buildSubgraph(modName string, g *Graph, modDepth int) error {
// Respect user-specified module depth
if dg.dotOpts.MaxDepth >= 0 && modDepth > dg.dotOpts.MaxDepth {
return nil
}
// Begin module subgraph
var sg *dot.Subgraph
if modDepth == 0 {
sg = dg.Dot.AddSubgraph(modName)
} else {
sg = dg.Dot.AddSubgraph(modName)
sg.Cluster = true
sg.AddAttr("label", modName)
}
origins, err := graphDotFindOrigins(g)
if err != nil {
return err
}
drawableVertices := make(map[dag.Vertex]struct{})
toDraw := make([]dag.Vertex, 0, len(g.Vertices()))
subgraphVertices := make(map[dag.Vertex]*Graph)
walk := func(v dag.Vertex, depth int) error {
// We only care about nodes that yield non-empty Dot strings.
if dn, ok := v.(GraphNodeDotter); !ok {
return nil
} else if dn.DotNode("fake", dg.dotOpts) == nil {
return nil
}
drawableVertices[v] = struct{}{}
toDraw = append(toDraw, v)
if sn, ok := v.(GraphNodeSubgraph); ok {
subgraphVertices[v] = sn.Subgraph()
}
return nil
}
if err := g.ReverseDepthFirstWalk(origins, walk); err != nil {
return err
}
for _, v := range toDraw {
dn := v.(GraphNodeDotter)
nodeName := graphDotNodeName(modName, v)
sg.AddNode(dn.DotNode(nodeName, dg.dotOpts))
// Draw all the edges from this vertex to other nodes
targets := dag.AsVertexList(g.DownEdges(v))
for _, t := range targets {
target := t.(dag.Vertex)
// Only want edges where both sides are drawable.
if _, ok := drawableVertices[target]; !ok {
continue
}
if err := sg.AddEdgeBetween(
graphDotNodeName(modName, v),
graphDotNodeName(modName, target),
map[string]string{}); err != nil {
return err
}
}
}
// Recurse into any subgraphs
for _, v := range toDraw {
subgraph, ok := subgraphVertices[v]
if !ok {
continue
}
err := dg.buildSubgraph(dag.VertexName(v), subgraph, modDepth+1)
if err != nil {
return err
}
}
if dg.dotOpts.DrawCycles {
colors := []string{"red", "green", "blue"}
for ci, cycle := range g.Cycles() {
for i, c := range cycle {
// Catch the last wrapping edge of the cycle
if i+1 >= len(cycle) {
i = -1
}
edgeAttrs := map[string]string{
"color": colors[ci%len(colors)],
"penwidth": "2.0",
}
if err := sg.AddEdgeBetween(
graphDotNodeName(modName, c),
graphDotNodeName(modName, cycle[i+1]),
edgeAttrs); err != nil {
return err
}
}
}
}
return nil
}
func graphDotNodeName(modName, v dag.Vertex) string {
return fmt.Sprintf("[%s] %s", modName, dag.VertexName(v))
}
func graphDotFindOrigins(g *Graph) ([]dag.Vertex, error) {
var origin []dag.Vertex
for _, v := range g.Vertices() {
if dr, ok := v.(GraphNodeDebugOrigin); ok {
if dr.DotOrigin() {
origin = append(origin, v)
}
}
}
if len(origin) == 0 {
return nil, fmt.Errorf("No DOT origin nodes found.\nGraph: %s", g.String())
}
return origin, nil
}

View File

@ -1,37 +1,9 @@
package terraform
import "github.com/hashicorp/terraform/dot"
// 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
// return a representation of this node.
type GraphNodeDotter interface {
// Dot is called to return the dot formatting for the node.
// The first parameter is the title of the node.
// The second parameter includes user-specified options that affect the dot
// graph. See GraphDotOpts below for details.
DotNode(string, *GraphDotOpts) *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
}
import "github.com/hashicorp/terraform/dag"
// GraphDot returns the dot formatting of a visual representation of
// the given Terraform graph.
func GraphDot(g *Graph, opts *GraphDotOpts) (string, error) {
dg, err := NewDebugGraph("root", g, opts)
if err != nil {
return "", err
}
return dg.Dot.String(), nil
func GraphDot(g *Graph, opts *dag.DotOpts) (string, error) {
return string(g.Dot(opts)), nil
}

View File

@ -4,21 +4,30 @@ import (
"strings"
"testing"
"github.com/hashicorp/terraform/dot"
"github.com/hashicorp/terraform/dag"
)
func TestGraphDot(t *testing.T) {
cases := map[string]struct {
cases := []struct {
Name string
Graph testGraphFunc
Opts GraphDotOpts
Opts dag.DotOpts
Expect string
Error string
}{
"empty": {
{
Name: "empty",
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 {
var g Graph
root := &testDrawableOrigin{"root"}
@ -61,8 +70,10 @@ digraph {
}
`,
},
"cycle": {
Opts: GraphDotOpts{
{
Name: "cycle",
Opts: dag.DotOpts{
DrawCycles: true,
},
Graph: func() *Graph {
@ -108,8 +119,10 @@ digraph {
}
`,
},
"subgraphs, no depth restriction": {
Opts: GraphDotOpts{
{
Name: "subgraphs, no depth restriction",
Opts: dag.DotOpts{
MaxDepth: -1,
},
Graph: func() *Graph {
@ -159,8 +172,10 @@ digraph {
}
`,
},
"subgraphs, with depth restriction": {
Opts: GraphDotOpts{
{
Name: "subgraphs, with depth restriction",
Opts: dag.DotOpts{
MaxDepth: 1,
},
Graph: func() *Graph {
@ -208,25 +223,32 @@ digraph {
},
}
for tn, tc := range cases {
actual, err := GraphDot(tc.Graph(), &tc.Opts)
if err == nil && tc.Error != "" {
t.Fatalf("%s: expected err: %s, got none", tn, tc.Error)
}
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)
}
continue
}
for _, tc := range cases {
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))
expected := strings.TrimSpace(tc.Expect) + "\n"
if actual != expected {
t.Fatalf("%s:\n\nexpected:\n%s\n\ngot:\n%s", tn, expected, actual)
}
if err == nil && tc.Error != "" {
t.Fatalf("%s: expected err: %s, got none", tn, tc.Error)
}
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,8 +262,8 @@ type testDrawable struct {
func (node *testDrawable) Name() string {
return node.VertexName
}
func (node *testDrawable) DotNode(n string, opts *GraphDotOpts) *dot.Node {
return dot.NewNode(n, map[string]string{})
func (node *testDrawable) DotNode(n string, opts *dag.DotOpts) *dag.DotNode {
return &dag.DotNode{Name: n, Attrs: map[string]string{}}
}
func (node *testDrawable) DependableName() []string {
return []string{node.VertexName}
@ -257,8 +279,8 @@ type testDrawableOrigin struct {
func (node *testDrawableOrigin) Name() string {
return node.VertexName
}
func (node *testDrawableOrigin) DotNode(n string, opts *GraphDotOpts) *dot.Node {
return dot.NewNode(n, map[string]string{})
func (node *testDrawableOrigin) DotNode(n string, opts *dag.DotOpts) *dag.DotNode {
return &dag.DotNode{Name: n, Attrs: map[string]string{}}
}
func (node *testDrawableOrigin) DotOrigin() bool {
return true
@ -276,11 +298,11 @@ type testDrawableSubgraph struct {
func (node *testDrawableSubgraph) Name() string {
return node.VertexName
}
func (node *testDrawableSubgraph) Subgraph() *Graph {
func (node *testDrawableSubgraph) Subgraph() dag.Grapher {
return node.SubgraphMock
}
func (node *testDrawableSubgraph) DotNode(n string, opts *GraphDotOpts) *dot.Node {
return dot.NewNode(n, map[string]string{})
func (node *testDrawableSubgraph) DotNode(n string, opts *dag.DotOpts) *dag.DotNode {
return &dag.DotNode{Name: n, Attrs: map[string]string{}}
}
func (node *testDrawableSubgraph) DependentOn() []string {
return node.DependentOnMock

View File

@ -13,7 +13,6 @@ type GraphWalker interface {
ExitVertex(dag.Vertex, error)
EnterEvalTree(dag.Vertex, EvalNode) EvalNode
ExitEvalTree(dag.Vertex, interface{}, error) error
Debug() *DebugGraph
}
// GrpahWalkerPanicwrapper can be optionally implemented to catch panics
@ -59,4 +58,3 @@ func (NullGraphWalker) EnterEvalTree(v dag.Vertex, n EvalNode) EvalNode { return
func (NullGraphWalker) ExitEvalTree(dag.Vertex, interface{}, error) error {
return nil
}
func (NullGraphWalker) Debug() *DebugGraph { return nil }

View File

@ -15,9 +15,8 @@ type ContextGraphWalker struct {
NullGraphWalker
// Configurable values
Context *Context
Operation walkOperation
DebugGraph *DebugGraph
Context *Context
Operation walkOperation
// Outputs, do not set these. Do not read these while the graph
// is being walked.
@ -145,10 +144,6 @@ func (w *ContextGraphWalker) ExitEvalTree(
return nil
}
func (w *ContextGraphWalker) Debug() *DebugGraph {
return w.DebugGraph
}
func (w *ContextGraphWalker) init() {
w.contexts = make(map[string]*BuiltinEvalContext, 5)
w.providerCache = make(map[string]ResourceProvider, 5)

View File

@ -24,7 +24,7 @@ type GraphNodeDynamicExpandable interface {
// GraphNodeSubgraph is an interface a node can implement if it has
// a larger subgraph that should be walked.
type GraphNodeSubgraph interface {
Subgraph() *Graph
Subgraph() dag.Grapher
}
// ExpandTransform is a transformer that does a subgraph expansion
@ -56,7 +56,7 @@ func (n *GraphNodeBasicSubgraph) Name() string {
return n.NameValue
}
func (n *GraphNodeBasicSubgraph) Subgraph() *Graph {
func (n *GraphNodeBasicSubgraph) Subgraph() dag.Grapher {
return n.Graph
}

View File

@ -30,7 +30,7 @@ func TestExpandTransform(t *testing.T) {
t.Fatalf("not subgraph: %#v", out)
}
actual := strings.TrimSpace(sn.Subgraph().String())
actual := strings.TrimSpace(sn.Subgraph().(*Graph).String())
expected := strings.TrimSpace(testExpandTransformStr)
if actual != expected {
t.Fatalf("bad: %s", actual)
@ -66,7 +66,7 @@ type testSubgraph struct {
Graph *Graph
}
func (n *testSubgraph) Subgraph() *Graph {
func (n *testSubgraph) Subgraph() dag.Grapher {
return n.Graph
}

View File

@ -8,7 +8,6 @@ import (
"github.com/hashicorp/go-multierror"
"github.com/hashicorp/terraform/config"
"github.com/hashicorp/terraform/dag"
"github.com/hashicorp/terraform/dot"
)
// GraphNodeProvider is an interface that nodes that can be a provider
@ -355,14 +354,17 @@ func (n *graphNodeCloseProvider) CloseProviderName() string {
}
// GraphNodeDotter impl.
func (n *graphNodeCloseProvider) DotNode(name string, opts *GraphDotOpts) *dot.Node {
func (n *graphNodeCloseProvider) DotNode(name string, opts *dag.DotOpts) *dag.DotNode {
if !opts.Verbose {
return nil
}
return dot.NewNode(name, map[string]string{
"label": n.Name(),
"shape": "diamond",
})
return &dag.DotNode{
Name: name,
Attrs: map[string]string{
"label": n.Name(),
"shape": "diamond",
},
}
}
type graphNodeProvider struct {
@ -393,11 +395,14 @@ func (n *graphNodeProvider) ProviderConfig() *config.RawConfig {
}
// GraphNodeDotter impl.
func (n *graphNodeProvider) DotNode(name string, opts *GraphDotOpts) *dot.Node {
return dot.NewNode(name, map[string]string{
"label": n.Name(),
"shape": "diamond",
})
func (n *graphNodeProvider) DotNode(name string, opts *dag.DotOpts) *dag.DotNode {
return &dag.DotNode{
Name: name,
Attrs: map[string]string{
"label": n.Name(),
"shape": "diamond",
},
}
}
// GraphNodeDotterOrigin impl.

View File

@ -5,7 +5,6 @@ import (
"github.com/hashicorp/terraform/config"
"github.com/hashicorp/terraform/dag"
"github.com/hashicorp/terraform/dot"
)
// DisableProviderTransformer "disables" any providers that are only
@ -102,11 +101,14 @@ func (n *graphNodeDisabledProvider) Name() string {
}
// GraphNodeDotter impl.
func (n *graphNodeDisabledProvider) DotNode(name string, opts *GraphDotOpts) *dot.Node {
return dot.NewNode(name, map[string]string{
"label": n.Name(),
"shape": "diamond",
})
func (n *graphNodeDisabledProvider) DotNode(name string, opts *dag.DotOpts) *dag.DotNode {
return &dag.DotNode{
Name: name,
Attrs: map[string]string{
"label": n.Name(),
"shape": "diamond",
},
}
}
// GraphNodeDotterOrigin impl.