github.com/terramate-io/tf@v0.0.0-20230830114523-fce866b4dfcd/tools/loggraphdiff/loggraphdiff.go (about)

     1  // Copyright (c) HashiCorp, Inc.
     2  // SPDX-License-Identifier: MPL-2.0
     3  
     4  // loggraphdiff is a tool for interpreting changes to the Terraform graph
     5  // based on the simple graph printing format used in the TF_LOG=trace log
     6  // output from Terraform, which looks like this:
     7  //
     8  //     aws_instance.b (destroy) - *terraform.NodeDestroyResourceInstance
     9  //     aws_instance.b (prepare state) - *terraform.NodeApplyableResource
    10  //       provider.aws - *terraform.NodeApplyableProvider
    11  //     aws_instance.b (prepare state) - *terraform.NodeApplyableResource
    12  //       provider.aws - *terraform.NodeApplyableProvider
    13  //     module.child.aws_instance.a (destroy) - *terraform.NodeDestroyResourceInstance
    14  //       module.child.aws_instance.a (prepare state) - *terraform.NodeApplyableResource
    15  //       module.child.output.a_output - *terraform.NodeApplyableOutput
    16  //       provider.aws - *terraform.NodeApplyableProvider
    17  //     module.child.aws_instance.a (prepare state) - *terraform.NodeApplyableResource
    18  //       provider.aws - *terraform.NodeApplyableProvider
    19  //     module.child.output.a_output - *terraform.NodeApplyableOutput
    20  //       module.child.aws_instance.a (prepare state) - *terraform.NodeApplyableResource
    21  //     provider.aws - *terraform.NodeApplyableProvider
    22  //
    23  // It takes the names of two files containing this style of output and
    24  // produces a single graph description in graphviz format that shows the
    25  // differences between the two graphs: nodes and edges which are only in the
    26  // first graph are shown in red, while those only in the second graph are
    27  // shown in green. This color combination is not useful for those who are
    28  // red/green color blind, so the result can be adjusted by replacing the
    29  // keywords "red" and "green" with a combination that the user is able to
    30  // distinguish.
    31  
    32  package main
    33  
    34  import (
    35  	"bufio"
    36  	"fmt"
    37  	"log"
    38  	"os"
    39  	"sort"
    40  	"strings"
    41  )
    42  
    43  type Graph struct {
    44  	nodes map[string]struct{}
    45  	edges map[[2]string]struct{}
    46  }
    47  
    48  func main() {
    49  	if len(os.Args) != 3 {
    50  		log.Fatal("usage: loggraphdiff <old-graph-file> <new-graph-file>")
    51  	}
    52  
    53  	old, err := readGraph(os.Args[1])
    54  	if err != nil {
    55  		log.Fatalf("failed to read %s: %s", os.Args[1], err)
    56  	}
    57  	new, err := readGraph(os.Args[2])
    58  	if err != nil {
    59  		log.Fatalf("failed to read %s: %s", os.Args[1], err)
    60  	}
    61  
    62  	var nodes []string
    63  	for n := range old.nodes {
    64  		nodes = append(nodes, n)
    65  	}
    66  	for n := range new.nodes {
    67  		if _, exists := old.nodes[n]; !exists {
    68  			nodes = append(nodes, n)
    69  		}
    70  	}
    71  	sort.Strings(nodes)
    72  
    73  	var edges [][2]string
    74  	for e := range old.edges {
    75  		edges = append(edges, e)
    76  	}
    77  	for e := range new.edges {
    78  		if _, exists := old.edges[e]; !exists {
    79  			edges = append(edges, e)
    80  		}
    81  	}
    82  	sort.Slice(edges, func(i, j int) bool {
    83  		if edges[i][0] != edges[j][0] {
    84  			return edges[i][0] < edges[j][0]
    85  		}
    86  		return edges[i][1] < edges[j][1]
    87  	})
    88  
    89  	fmt.Println("digraph G {")
    90  	fmt.Print("  rankdir = \"BT\";\n\n")
    91  	for _, n := range nodes {
    92  		var attrs string
    93  		_, inOld := old.nodes[n]
    94  		_, inNew := new.nodes[n]
    95  		switch {
    96  		case inOld && inNew:
    97  			// no attrs required
    98  		case inOld:
    99  			attrs = " [color=red]"
   100  		case inNew:
   101  			attrs = " [color=green]"
   102  		}
   103  		fmt.Printf("    %q%s;\n", n, attrs)
   104  	}
   105  	fmt.Println("")
   106  	for _, e := range edges {
   107  		var attrs string
   108  		_, inOld := old.edges[e]
   109  		_, inNew := new.edges[e]
   110  		switch {
   111  		case inOld && inNew:
   112  			// no attrs required
   113  		case inOld:
   114  			attrs = " [color=red]"
   115  		case inNew:
   116  			attrs = " [color=green]"
   117  		}
   118  		fmt.Printf("    %q -> %q%s;\n", e[0], e[1], attrs)
   119  	}
   120  	fmt.Println("}")
   121  }
   122  
   123  func readGraph(fn string) (Graph, error) {
   124  	ret := Graph{
   125  		nodes: map[string]struct{}{},
   126  		edges: map[[2]string]struct{}{},
   127  	}
   128  	r, err := os.Open(fn)
   129  	if err != nil {
   130  		return ret, err
   131  	}
   132  
   133  	sc := bufio.NewScanner(r)
   134  	var latestNode string
   135  	for sc.Scan() {
   136  		l := sc.Text()
   137  		dash := strings.Index(l, " - ")
   138  		if dash == -1 {
   139  			// invalid line, so we'll ignore it
   140  			continue
   141  		}
   142  		name := l[:dash]
   143  		if strings.HasPrefix(name, "  ") {
   144  			// It's an edge
   145  			name = name[2:]
   146  			edge := [2]string{latestNode, name}
   147  			ret.edges[edge] = struct{}{}
   148  		} else {
   149  			// It's a node
   150  			latestNode = name
   151  			ret.nodes[name] = struct{}{}
   152  		}
   153  	}
   154  
   155  	return ret, nil
   156  }