mirror of
https://github.com/opentofu/opentofu.git
synced 2024-12-28 01:41:48 -06:00
8cf0a8ca9c
In the case of highly-connected graphs, the TransitiveReduction process was far too computationally intensive. Since no operations are applied to the nodes, and the walk order is not even user visible, we don't need to sort them n^2 times.
475 lines
11 KiB
Go
475 lines
11 KiB
Go
package dag
|
|
|
|
import (
|
|
"encoding/json"
|
|
"fmt"
|
|
"io"
|
|
"log"
|
|
"reflect"
|
|
"sort"
|
|
"strconv"
|
|
"sync"
|
|
)
|
|
|
|
const (
|
|
typeOperation = "Operation"
|
|
typeTransform = "Transform"
|
|
typeWalk = "Walk"
|
|
typeDepthFirstWalk = "DepthFirstWalk"
|
|
typeReverseDepthFirstWalk = "ReverseDepthFirstWalk"
|
|
typeTransitiveReduction = "TransitiveReduction"
|
|
typeEdgeInfo = "EdgeInfo"
|
|
typeVertexInfo = "VertexInfo"
|
|
typeVisitInfo = "VisitInfo"
|
|
)
|
|
|
|
// 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 can call it to get attributes.
|
|
graphNodeDotter GraphNodeDotter
|
|
}
|
|
|
|
func newMarshalVertex(v Vertex) *marshalVertex {
|
|
dn, ok := v.(GraphNodeDotter)
|
|
if !ok {
|
|
dn = nil
|
|
}
|
|
|
|
return &marshalVertex{
|
|
ID: marshalVertexID(v),
|
|
Name: VertexName(v),
|
|
Attrs: make(map[string]string),
|
|
graphNodeDotter: dn,
|
|
}
|
|
}
|
|
|
|
// 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
|
|
}
|
|
|
|
// The DebugOperationEnd func type provides a way to call an End function via a
|
|
// method call, allowing for the chaining of methods in a defer statement.
|
|
type DebugOperationEnd func(string)
|
|
|
|
// End calls function e with the info parameter, marking the end of this
|
|
// operation in the logs.
|
|
func (e DebugOperationEnd) End(info string) { e(info) }
|
|
|
|
// encoder provides methods to write debug data to an io.Writer, and is a noop
|
|
// when no writer is present
|
|
type encoder struct {
|
|
sync.Mutex
|
|
w io.Writer
|
|
}
|
|
|
|
// Encode is analogous to json.Encoder.Encode
|
|
func (e *encoder) Encode(i interface{}) {
|
|
if e == nil || e.w == nil {
|
|
return
|
|
}
|
|
e.Lock()
|
|
defer e.Unlock()
|
|
|
|
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) {
|
|
if e == nil {
|
|
return
|
|
}
|
|
e.Encode(marshalTransform{
|
|
Type: typeTransform,
|
|
AddVertex: newMarshalVertex(v),
|
|
})
|
|
}
|
|
|
|
// Remove records the removal of Vertex v.
|
|
func (e *encoder) Remove(v Vertex) {
|
|
if e == nil {
|
|
return
|
|
}
|
|
e.Encode(marshalTransform{
|
|
Type: typeTransform,
|
|
RemoveVertex: newMarshalVertex(v),
|
|
})
|
|
}
|
|
|
|
func (e *encoder) Connect(edge Edge) {
|
|
if e == nil {
|
|
return
|
|
}
|
|
e.Encode(marshalTransform{
|
|
Type: typeTransform,
|
|
AddEdge: newMarshalEdge(edge),
|
|
})
|
|
}
|
|
|
|
func (e *encoder) RemoveEdge(edge Edge) {
|
|
if e == nil {
|
|
return
|
|
}
|
|
e.Encode(marshalTransform{
|
|
Type: typeTransform,
|
|
RemoveEdge: newMarshalEdge(edge),
|
|
})
|
|
}
|
|
|
|
// BeginOperation marks the start of set of graph transformations, and returns
|
|
// an EndDebugOperation func to be called once the opration is complete.
|
|
func (e *encoder) BeginOperation(op string, info string) DebugOperationEnd {
|
|
if e == nil {
|
|
return func(string) {}
|
|
}
|
|
|
|
e.Encode(marshalOperation{
|
|
Type: typeOperation,
|
|
Begin: op,
|
|
Info: info,
|
|
})
|
|
|
|
return func(info string) {
|
|
e.Encode(marshalOperation{
|
|
Type: typeOperation,
|
|
End: op,
|
|
Info: info,
|
|
})
|
|
}
|
|
}
|
|
|
|
// 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"`
|
|
}
|
|
|
|
// 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 != typeTransform {
|
|
continue
|
|
}
|
|
|
|
t := &marshalTransform{}
|
|
err = json.Unmarshal(s.JSON, t)
|
|
if err != nil {
|
|
return g, err
|
|
}
|
|
t.Transform(g)
|
|
}
|
|
return g, nil
|
|
}
|
|
|
|
// marshalVertexInfo allows encoding arbitrary information about the a single
|
|
// Vertex in the logs. These are accumulated for informational display while
|
|
// rebuilding the graph.
|
|
type marshalVertexInfo struct {
|
|
Type string
|
|
Vertex *marshalVertex
|
|
Info string
|
|
}
|
|
|
|
func newVertexInfo(infoType string, v Vertex, info string) *marshalVertexInfo {
|
|
return &marshalVertexInfo{
|
|
Type: infoType,
|
|
Vertex: newMarshalVertex(v),
|
|
Info: info,
|
|
}
|
|
}
|
|
|
|
// marshalEdgeInfo allows encoding arbitrary information about the a single
|
|
// Edge in the logs. These are accumulated for informational display while
|
|
// rebuilding the graph.
|
|
type marshalEdgeInfo struct {
|
|
Type string
|
|
Edge *marshalEdge
|
|
Info string
|
|
}
|
|
|
|
func newEdgeInfo(infoType string, e Edge, info string) *marshalEdgeInfo {
|
|
return &marshalEdgeInfo{
|
|
Type: infoType,
|
|
Edge: newMarshalEdge(e),
|
|
Info: info,
|
|
}
|
|
}
|
|
|
|
// JSON2Dot reads a Graph debug log from and io.Reader, and converts the final
|
|
// graph dot format.
|
|
//
|
|
// TODO: Allow returning the output at a certain point during decode.
|
|
// Encode extra information from the json log into the Dot.
|
|
func JSON2Dot(r io.Reader) ([]byte, error) {
|
|
g, err := decodeGraph(r)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
return g.Dot(nil), nil
|
|
}
|