github.com/RyanSiu1995/gcb-visualizer@v1.0.2-0.20211121083050-f618fa372726/internal/utils/cloudbuild.go (about)

     1  package util
     2  
     3  import (
     4  	"encoding/json"
     5  	"fmt"
     6  	"io/ioutil"
     7  	"os"
     8  	"path/filepath"
     9  	"strings"
    10  
    11  	yamlUtil "github.com/ghodss/yaml"
    12  	graphviz "github.com/goccy/go-graphviz"
    13  	"github.com/goccy/go-graphviz/cgraph"
    14  	log "github.com/sirupsen/logrus"
    15  	"github.com/skratchdot/open-golang/open"
    16  	cloudbuild "google.golang.org/api/cloudbuild/v1"
    17  )
    18  
    19  var formatMapping = map[string]graphviz.Format{
    20  	".dot":  "dot",
    21  	".png":  graphviz.PNG,
    22  	".jpg":  graphviz.JPG,
    23  	".jpeg": graphviz.JPG,
    24  }
    25  
    26  func init() {
    27  	if _, exist := os.LookupEnv("DEBUG"); exist {
    28  		log.SetLevel(log.DebugLevel)
    29  	}
    30  }
    31  
    32  // ParseYaml takes a string of file path and returns the cloud build object
    33  func ParseYaml(filePath string) (*cloudbuild.Build, error) {
    34  	var jsonFileInByte []byte
    35  	var err error
    36  	if strings.ToLower(filepath.Ext(filePath)) != ".json" {
    37  		yamlFileInByte, err := ioutil.ReadFile(filePath)
    38  		if err != nil {
    39  			fmt.Println(err.Error())
    40  			return nil, err
    41  		}
    42  		jsonFileInByte, err = yamlUtil.YAMLToJSON(yamlFileInByte)
    43  		if err != nil {
    44  			fmt.Println(err.Error())
    45  			return nil, err
    46  		}
    47  	} else {
    48  		jsonFileInByte, err = ioutil.ReadFile(filePath)
    49  		if err != nil {
    50  			fmt.Println(err.Error())
    51  			return nil, err
    52  		}
    53  	}
    54  
    55  	var build cloudbuild.Build
    56  	if err = json.Unmarshal(jsonFileInByte, &build); err != nil {
    57  		fmt.Println(err.Error())
    58  	}
    59  
    60  	return &build, nil
    61  }
    62  
    63  // BuildStepsToDAG takes the list of build steps and build a directed acyclic graph
    64  func BuildStepsToDAG(steps []*cloudbuild.BuildStep) *cgraph.Graph {
    65  	g := graphviz.New()
    66  	graph, err := g.Graph()
    67  	if err != nil {
    68  		log.Fatal(err.Error())
    69  	}
    70  
    71  	var nodeList []*cgraph.Node
    72  	for idx, step := range steps {
    73  		name := step.Id
    74  		if name == "" {
    75  			name = fmt.Sprintf("Step %d", idx)
    76  		}
    77  		node, err := graph.CreateNode(name)
    78  		if err != nil {
    79  			log.Fatal(err.Error())
    80  		}
    81  		nodeList = append(nodeList, node)
    82  	}
    83  	mapping := buildIDToIdxMap(steps)
    84  	for idx := range steps {
    85  		handleWaitFor(steps, idx, mapping, graph, nodeList)
    86  	}
    87  	return graph
    88  }
    89  
    90  // Visualize is a high level API to show the graph
    91  func Visualize(graph *cgraph.Graph) error {
    92  	dir, err := ioutil.TempDir("", "gcb-temp")
    93  	if err != nil {
    94  		return err
    95  	}
    96  	filename := filepath.Join(dir, "temp.png")
    97  
    98  	g := graphviz.New()
    99  
   100  	if err := g.RenderFilename(graph, graphviz.PNG, filename); err != nil {
   101  		return err
   102  	}
   103  
   104  	err = open.Run(filename)
   105  	if err != nil {
   106  		return err
   107  	}
   108  	return nil
   109  }
   110  
   111  // SaveGraph is a high level API to save the graph
   112  func SaveGraph(graph *cgraph.Graph, filename string) error {
   113  	g := graphviz.New()
   114  
   115  	ext := filepath.Ext(filename)
   116  	if err := g.RenderFilename(graph, formatMapping[strings.ToLower(ext)], filename); err != nil {
   117  		return err
   118  	}
   119  
   120  	return nil
   121  }
   122  
   123  func handleWaitFor(steps []*cloudbuild.BuildStep, idx int, mapping map[string]int, graph *cgraph.Graph, nodes []*cgraph.Node) {
   124  	waitFor := steps[idx].WaitFor
   125  	if len(waitFor) == 1 {
   126  		if !Contains(waitFor, "-") {
   127  			log.Debugf("Node %d is handled with normal waitFor case with length 1...", idx)
   128  			from := mapping[waitFor[0]]
   129  			log.Debugf("Node %d has waitFor %s. Adding the edge from %d to %d...", idx, waitFor[0], from, idx)
   130  			graph.CreateEdge("", nodes[from], nodes[idx])
   131  		} else {
   132  			// If the "-" in the start, it will be nothing to do
   133  			if idx != 0 {
   134  				log.Debugf("Node %d are handled with \"-\" case...", idx)
   135  				// Search for next node without waitFor
   136  				for i := idx; i < len(steps); i++ {
   137  					if len(steps[i].WaitFor) == 0 {
   138  						log.Debugf("The next node with waitFor for immediately started node %d is node %d. Adding the edge from %d to %d...", idx, i, idx, i)
   139  						graph.CreateEdge("", nodes[idx], nodes[i])
   140  						break
   141  					}
   142  				}
   143  			}
   144  		}
   145  	} else if len(waitFor) != 0 {
   146  		log.Debugf("Node %d is handled with normal waitFor case...", idx)
   147  		// Handle all normal cases
   148  		for _, item := range waitFor {
   149  			from := mapping[item]
   150  			log.Debugf("Node %d has waitFor %s. Adding the edge from %d to %d...", idx, item, from, idx)
   151  			graph.CreateEdge("", nodes[from], nodes[idx])
   152  		}
   153  	} else {
   154  		log.Debugf("Node %d is handled with no waitFor case...", idx)
   155  		// Handle all cases without waitFor
   156  		for i := idx - 1; i >= 0; i-- {
   157  			if len(steps[i].WaitFor) != 0 {
   158  				if isLastNode(steps, mapping, idx, i) {
   159  					log.Debugf("Found the last node with waitFor before node %d is node %d. Adding the edge from %d to %d...", idx, i, i, idx)
   160  					graph.CreateEdge("", nodes[i], nodes[idx])
   161  				}
   162  			} else {
   163  				log.Debugf("Last node without waitFor for node %d is node %d. Adding the edge from %d to %d...", idx, i, i, idx)
   164  				graph.CreateEdge("", nodes[i], nodes[idx])
   165  				// If we encounter the last node without WaitFor, all the rest of cases should be routed to this node instead
   166  				break
   167  			}
   168  		}
   169  	}
   170  }
   171  
   172  func isLastNode(steps []*cloudbuild.BuildStep, mapping map[string]int, threshold int, idx int) bool {
   173  	id := steps[idx].Id
   174  	for i := idx; i < threshold; i++ {
   175  		if Contains(steps[i].WaitFor, id) {
   176  			return false
   177  		}
   178  	}
   179  	return true
   180  }
   181  
   182  func buildIDToIdxMap(steps []*cloudbuild.BuildStep) map[string]int {
   183  	var mapping = make(map[string]int)
   184  	for idx, step := range steps {
   185  		if step.Id != "" {
   186  			mapping[step.Id] = idx
   187  		}
   188  	}
   189  	return mapping
   190  }