github.com/rstandt/terraform@v0.12.32-0.20230710220336-b1063613405c/tools/loggraphdiff/loggraphdiff.go (about)

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