github.com/opentofu/opentofu@v1.7.1/internal/dag/dot.go (about)

     1  // Copyright (c) The OpenTofu Authors
     2  // SPDX-License-Identifier: MPL-2.0
     3  // Copyright (c) 2023 HashiCorp, Inc.
     4  // SPDX-License-Identifier: MPL-2.0
     5  
     6  package dag
     7  
     8  import (
     9  	"bytes"
    10  	"fmt"
    11  	"sort"
    12  	"strings"
    13  )
    14  
    15  // DotOpts are the options for generating a dot formatted Graph.
    16  type DotOpts struct {
    17  	// Allows some nodes to decide to only show themselves when the user has
    18  	// requested the "verbose" graph.
    19  	Verbose bool
    20  
    21  	// Highlight Cycles
    22  	DrawCycles bool
    23  
    24  	// How many levels to expand modules as we draw
    25  	MaxDepth int
    26  
    27  	// use this to keep the cluster_ naming convention from the previous dot writer
    28  	cluster bool
    29  }
    30  
    31  // GraphNodeDotter can be implemented by a node to cause it to be included
    32  // in the dot graph. The Dot method will be called which is expected to
    33  // return a representation of this node.
    34  type GraphNodeDotter interface {
    35  	// Dot is called to return the dot formatting for the node.
    36  	// The first parameter is the title of the node.
    37  	// The second parameter includes user-specified options that affect the dot
    38  	// graph. See GraphDotOpts below for details.
    39  	DotNode(string, *DotOpts) *DotNode
    40  }
    41  
    42  // DotNode provides a structure for Vertices to return in order to specify their
    43  // dot format.
    44  type DotNode struct {
    45  	Name  string
    46  	Attrs map[string]string
    47  }
    48  
    49  // Returns the DOT representation of this Graph.
    50  func (g *marshalGraph) Dot(opts *DotOpts) []byte {
    51  	if opts == nil {
    52  		opts = &DotOpts{
    53  			DrawCycles: true,
    54  			MaxDepth:   -1,
    55  			Verbose:    true,
    56  		}
    57  	}
    58  
    59  	var w indentWriter
    60  	w.WriteString("digraph {\n")
    61  	w.Indent()
    62  
    63  	// some dot defaults
    64  	w.WriteString(`compound = "true"` + "\n")
    65  	w.WriteString(`newrank = "true"` + "\n")
    66  
    67  	// the top level graph is written as the first subgraph
    68  	w.WriteString(`subgraph "root" {` + "\n")
    69  	g.writeBody(opts, &w)
    70  
    71  	// cluster isn't really used other than for naming purposes in some graphs
    72  	opts.cluster = opts.MaxDepth != 0
    73  	maxDepth := opts.MaxDepth
    74  	if maxDepth == 0 {
    75  		maxDepth = -1
    76  	}
    77  
    78  	for _, s := range g.Subgraphs {
    79  		g.writeSubgraph(s, opts, maxDepth, &w)
    80  	}
    81  
    82  	w.Unindent()
    83  	w.WriteString("}\n")
    84  	return w.Bytes()
    85  }
    86  
    87  func (v *marshalVertex) dot(g *marshalGraph, opts *DotOpts) []byte {
    88  	var buf bytes.Buffer
    89  	graphName := g.Name
    90  	if graphName == "" {
    91  		graphName = "root"
    92  	}
    93  
    94  	name := v.Name
    95  	attrs := v.Attrs
    96  	if v.graphNodeDotter != nil {
    97  		node := v.graphNodeDotter.DotNode(name, opts)
    98  		if node == nil {
    99  			return []byte{}
   100  		}
   101  
   102  		newAttrs := make(map[string]string)
   103  		for k, v := range attrs {
   104  			newAttrs[k] = v
   105  		}
   106  		for k, v := range node.Attrs {
   107  			newAttrs[k] = v
   108  		}
   109  
   110  		name = node.Name
   111  		attrs = newAttrs
   112  	}
   113  
   114  	buf.WriteString(fmt.Sprintf(`"[%s] %s"`, graphName, name))
   115  	writeAttrs(&buf, attrs)
   116  	buf.WriteByte('\n')
   117  
   118  	return buf.Bytes()
   119  }
   120  
   121  func (e *marshalEdge) dot(g *marshalGraph) string {
   122  	var buf bytes.Buffer
   123  	graphName := g.Name
   124  	if graphName == "" {
   125  		graphName = "root"
   126  	}
   127  
   128  	sourceName := g.vertexByID(e.Source).Name
   129  	targetName := g.vertexByID(e.Target).Name
   130  	s := fmt.Sprintf(`"[%s] %s" -> "[%s] %s"`, graphName, sourceName, graphName, targetName)
   131  	buf.WriteString(s)
   132  	writeAttrs(&buf, e.Attrs)
   133  
   134  	return buf.String()
   135  }
   136  
   137  func cycleDot(e *marshalEdge, g *marshalGraph) string {
   138  	return e.dot(g) + ` [color = "red", penwidth = "2.0"]`
   139  }
   140  
   141  // Write the subgraph body. The is recursive, and the depth argument is used to
   142  // record the current depth of iteration.
   143  func (g *marshalGraph) writeSubgraph(sg *marshalGraph, opts *DotOpts, depth int, w *indentWriter) {
   144  	if depth == 0 {
   145  		return
   146  	}
   147  	depth--
   148  
   149  	name := sg.Name
   150  	if opts.cluster {
   151  		// we prefix with cluster_ to match the old dot output
   152  		name = "cluster_" + name
   153  		sg.Attrs["label"] = sg.Name
   154  	}
   155  	w.WriteString(fmt.Sprintf("subgraph %q {\n", name))
   156  	sg.writeBody(opts, w)
   157  
   158  	for _, sg := range sg.Subgraphs {
   159  		g.writeSubgraph(sg, opts, depth, w)
   160  	}
   161  }
   162  
   163  func (g *marshalGraph) writeBody(opts *DotOpts, w *indentWriter) {
   164  	w.Indent()
   165  
   166  	for _, as := range attrStrings(g.Attrs) {
   167  		w.WriteString(as + "\n")
   168  	}
   169  
   170  	// list of Vertices that aren't to be included in the dot output
   171  	skip := map[string]bool{}
   172  
   173  	for _, v := range g.Vertices {
   174  		if v.graphNodeDotter == nil {
   175  			skip[v.ID] = true
   176  			continue
   177  		}
   178  
   179  		w.Write(v.dot(g, opts))
   180  	}
   181  
   182  	var dotEdges []string
   183  
   184  	if opts.DrawCycles {
   185  		for _, c := range g.Cycles {
   186  			if len(c) < 2 {
   187  				continue
   188  			}
   189  
   190  			for i, j := 0, 1; i < len(c); i, j = i+1, j+1 {
   191  				if j >= len(c) {
   192  					j = 0
   193  				}
   194  				src := c[i]
   195  				tgt := c[j]
   196  
   197  				if skip[src.ID] || skip[tgt.ID] {
   198  					continue
   199  				}
   200  
   201  				e := &marshalEdge{
   202  					Name:   fmt.Sprintf("%s|%s", src.Name, tgt.Name),
   203  					Source: src.ID,
   204  					Target: tgt.ID,
   205  					Attrs:  make(map[string]string),
   206  				}
   207  
   208  				dotEdges = append(dotEdges, cycleDot(e, g))
   209  				src = tgt
   210  			}
   211  		}
   212  	}
   213  
   214  	for _, e := range g.Edges {
   215  		dotEdges = append(dotEdges, e.dot(g))
   216  	}
   217  
   218  	// srot these again to match the old output
   219  	sort.Strings(dotEdges)
   220  
   221  	for _, e := range dotEdges {
   222  		w.WriteString(e + "\n")
   223  	}
   224  
   225  	w.Unindent()
   226  	w.WriteString("}\n")
   227  }
   228  
   229  func writeAttrs(buf *bytes.Buffer, attrs map[string]string) {
   230  	if len(attrs) > 0 {
   231  		buf.WriteString(" [")
   232  		buf.WriteString(strings.Join(attrStrings(attrs), ", "))
   233  		buf.WriteString("]")
   234  	}
   235  }
   236  
   237  func attrStrings(attrs map[string]string) []string {
   238  	strings := make([]string, 0, len(attrs))
   239  	for k, v := range attrs {
   240  		strings = append(strings, fmt.Sprintf("%s = %q", k, v))
   241  	}
   242  	sort.Strings(strings)
   243  	return strings
   244  }
   245  
   246  // Provide a bytes.Buffer like structure, which will indent when starting a
   247  // newline.
   248  type indentWriter struct {
   249  	bytes.Buffer
   250  	level int
   251  }
   252  
   253  func (w *indentWriter) indent() {
   254  	newline := []byte("\n")
   255  	if !bytes.HasSuffix(w.Bytes(), newline) {
   256  		return
   257  	}
   258  	for i := 0; i < w.level; i++ {
   259  		w.Buffer.WriteString("\t")
   260  	}
   261  }
   262  
   263  // Indent increases indentation by 1
   264  func (w *indentWriter) Indent() { w.level++ }
   265  
   266  // Unindent decreases indentation by 1
   267  func (w *indentWriter) Unindent() { w.level-- }
   268  
   269  // the following methods intercecpt the byte.Buffer writes and insert the
   270  // indentation when starting a new line.
   271  func (w *indentWriter) Write(b []byte) (int, error) {
   272  	w.indent()
   273  	return w.Buffer.Write(b)
   274  }
   275  
   276  func (w *indentWriter) WriteString(s string) (int, error) {
   277  	w.indent()
   278  	return w.Buffer.WriteString(s)
   279  }
   280  func (w *indentWriter) WriteByte(b byte) error {
   281  	w.indent()
   282  	return w.Buffer.WriteByte(b)
   283  }
   284  func (w *indentWriter) WriteRune(r rune) (int, error) {
   285  	w.indent()
   286  	return w.Buffer.WriteRune(r)
   287  }