mirror of
https://github.com/opentofu/opentofu.git
synced 2024-12-23 07:33:32 -06:00
9b7bec31b4
Signed-off-by: Nathan Baulch <nathan.baulch@gmail.com>
288 lines
6.2 KiB
Go
288 lines
6.2 KiB
Go
// Copyright (c) The OpenTofu Authors
|
|
// SPDX-License-Identifier: MPL-2.0
|
|
// Copyright (c) 2023 HashiCorp, Inc.
|
|
// SPDX-License-Identifier: MPL-2.0
|
|
|
|
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, opts *DotOpts) []byte {
|
|
var buf bytes.Buffer
|
|
graphName := g.Name
|
|
if graphName == "" {
|
|
graphName = "root"
|
|
}
|
|
|
|
name := v.Name
|
|
attrs := v.Attrs
|
|
if v.graphNodeDotter != nil {
|
|
node := v.graphNodeDotter.DotNode(name, opts)
|
|
if node == nil {
|
|
return []byte{}
|
|
}
|
|
|
|
newAttrs := make(map[string]string)
|
|
for k, v := range attrs {
|
|
newAttrs[k] = v
|
|
}
|
|
for k, v := range node.Attrs {
|
|
newAttrs[k] = v
|
|
}
|
|
|
|
name = node.Name
|
|
attrs = newAttrs
|
|
}
|
|
|
|
buf.WriteString(fmt.Sprintf(`"[%s] %s"`, graphName, name))
|
|
writeAttrs(&buf, 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 == nil {
|
|
skip[v.ID] = true
|
|
continue
|
|
}
|
|
|
|
w.Write(v.dot(g, opts))
|
|
}
|
|
|
|
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))
|
|
}
|
|
|
|
// sort 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 intercept 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)
|
|
}
|