mirror of
https://github.com/opentofu/opentofu.git
synced 2025-01-13 09:32:24 -06:00
1cea51a9fa
Insert the walk order into the graph labels. Removed some extra debug output.
290 lines
6.0 KiB
Go
290 lines
6.0 KiB
Go
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
|
|
}
|