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 }