Merge pull request #10030 from hashicorp/jbardin/debug
debug: next steps
This commit is contained in:
commit
9c83924a74
|
@ -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,
|
||||
|
|
12
dag/dag.go
12
dag/dag.go
|
@ -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 {
|
||||
|
|
|
@ -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)
|
||||
}
|
88
dag/graph.go
88
dag/graph.go
|
@ -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.
|
||||
|
|
|
@ -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,
|
||||
}
|
||||
}
|
|
@ -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"
|
||||
}
|
||||
]
|
||||
}`
|
237
dot/graph.go
237
dot/graph.go
|
@ -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
|
||||
}
|
|
@ -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()
|
||||
}
|
|
@ -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
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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.
|
||||
|
|
|
@ -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.
|
||||
|
|
|
@ -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
|
||||
}
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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 }
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
||||
|
|
|
@ -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.
|
||||
|
|
|
@ -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.
|
||||
|
|
Loading…
Reference in New Issue