this repo has no description
0
fork

Configure Feed

Select the types of activity you want to include in your feed.

refactor(controller): remove external graph library

+194 -187
+137 -82
controller/activities/graph.go
··· 3 3 import ( 4 4 "context" 5 5 "fmt" 6 - "strconv" 7 6 "strings" 8 - 9 - "github.com/awalterschulze/gographviz" 10 7 ) 11 8 12 - // Node represents a node in the graph. 13 - type Node struct { 14 - Name string 15 - // Attributes can be added here if needed 9 + // Graph represents a simple directed graph with efficient operations. 10 + type Graph struct { 11 + Nodes map[string]bool `json:"nodes"` 12 + Edges map[string][]string `json:"edges"` // source -> []destinations 13 + } 14 + 15 + // NewGraph creates a new empty graph. 16 + func NewGraph() *Graph { 17 + return &Graph{ 18 + Nodes: make(map[string]bool), 19 + Edges: make(map[string][]string), 20 + } 21 + } 22 + 23 + // AddNode adds a node to the graph. 24 + func (g *Graph) AddNode(name string) { 25 + g.Nodes[name] = true 26 + } 27 + 28 + // AddEdge adds a directed edge from src to dest. 29 + func (g *Graph) AddEdge(src, dest string) { 30 + g.AddNode(src) 31 + g.AddNode(dest) 32 + g.Edges[src] = append(g.Edges[src], dest) 16 33 } 17 34 18 - // Edge represents a directed edge in the graph. 19 - type Edge struct { 20 - Src string 21 - Dest string 22 - // Attributes can be added here if needed 35 + // GetNodes returns all node names. 36 + func (g *Graph) GetNodes() []string { 37 + nodes := make([]string, 0, len(g.Nodes)) 38 + for name := range g.Nodes { 39 + nodes = append(nodes, name) 40 + } 41 + return nodes 23 42 } 24 43 25 - // Graph represents a serializable graph. 26 - type Graph struct { 27 - Nodes []*Node 28 - Edges []*Edge 44 + // NodeCount returns the number of nodes in the graph. 45 + func (g *Graph) NodeCount() int { 46 + return len(g.Nodes) 47 + } 48 + 49 + // EdgeCount returns the number of edges in the graph. 50 + func (g *Graph) EdgeCount() int { 51 + count := 0 52 + for _, dests := range g.Edges { 53 + count += len(dests) 54 + } 55 + return count 29 56 } 30 57 31 58 // PruneGraph takes a graph and a list of changed nodes, and returns a new graph ··· 33 60 func PruneGraph(ctx context.Context, graph *Graph, changed []string) (*Graph, error) { 34 61 // Build reverse dependency map: target -> dependents 35 62 dependents := make(map[string][]string) 36 - for _, edge := range graph.Edges { 37 - dependents[edge.Dest] = append(dependents[edge.Dest], edge.Src) 63 + for src, dests := range graph.Edges { 64 + for _, dest := range dests { 65 + dependents[dest] = append(dependents[dest], src) 66 + } 38 67 } 39 68 40 69 // Collect all nodes to keep (changed + all that depend on them) ··· 49 78 visit(dep) 50 79 } 51 80 } 81 + 82 + // Only visit nodes that actually exist in the graph 52 83 for _, nodeName := range changed { 53 - // Make sure we have the node in the graph before visiting 54 - var found bool 55 - for _, n := range graph.Nodes { 56 - if n.Name == nodeName { 57 - found = true 58 - break 59 - } 60 - } 61 - if found { 84 + if graph.Nodes[nodeName] { 62 85 visit(nodeName) 63 86 } 64 87 } 65 88 66 - // Reconstruct pruned graph 67 - prunedGraph := &Graph{Nodes: []*Node{}, Edges: []*Edge{}} 68 - for _, node := range graph.Nodes { 69 - if keep[node.Name] { 70 - prunedGraph.Nodes = append(prunedGraph.Nodes, node) 71 - } 89 + // Create pruned graph 90 + prunedGraph := NewGraph() 91 + for node := range keep { 92 + prunedGraph.AddNode(node) 72 93 } 73 - for _, edge := range graph.Edges { 74 - if keep[edge.Src] && keep[edge.Dest] { 75 - prunedGraph.Edges = append(prunedGraph.Edges, edge) 94 + for src, dests := range graph.Edges { 95 + if keep[src] { 96 + for _, dest := range dests { 97 + if keep[dest] { 98 + prunedGraph.AddEdge(src, dest) 99 + } 100 + } 76 101 } 77 102 } 78 103 79 104 return prunedGraph, nil 80 105 } 81 106 82 - // NewGraphFromDot creates a Graph from a DOT string. 107 + // NewGraphFromDot creates a Graph from a DOT string using a simple parser. 83 108 func NewGraphFromDot(dot string) (*Graph, error) { 84 - ast, err := gographviz.ParseString(dot) 85 - if err != nil { 86 - return nil, fmt.Errorf("failed to parse DOT string: %w", err) 87 - } 109 + graph := NewGraph() 88 110 89 - g := gographviz.NewGraph() 90 - if err := gographviz.Analyse(ast, g); err != nil { 91 - return nil, fmt.Errorf("failed to analyse graph: %w", err) 92 - } 111 + lines := strings.Split(dot, "\n") 112 + for _, line := range lines { 113 + line = strings.TrimSpace(line) 114 + if line == "" || strings.HasPrefix(line, "//") || line == "digraph {" || line == "}" { 115 + continue 116 + } 93 117 94 - graph := &Graph{Nodes: []*Node{}, Edges: []*Edge{}} 95 - nodeSet := make(map[string]bool) 118 + // Remove trailing semicolon if present 119 + line = strings.TrimSuffix(line, ";") 120 + line = strings.TrimSpace(line) 96 121 97 - addNode := func(name string) { 98 - if !nodeSet[name] { 99 - graph.Nodes = append(graph.Nodes, &Node{Name: name}) 100 - nodeSet[name] = true 122 + // Parse edges: "A" -> "B" 123 + if strings.Contains(line, "->") { 124 + parts := strings.Split(line, "->") 125 + if len(parts) == 2 { 126 + src := extractQuotedString(strings.TrimSpace(parts[0])) 127 + dest := extractQuotedString(strings.TrimSpace(parts[1])) 128 + if src != "" && dest != "" { 129 + graph.AddEdge(src, dest) 130 + } 131 + } 132 + } else { 133 + // Parse standalone nodes: "C" 134 + nodeName := extractQuotedString(line) 135 + if nodeName != "" { 136 + graph.AddNode(nodeName) 137 + } 101 138 } 102 139 } 103 140 104 - unquote := func(s string) string { 105 - res, err := strconv.Unquote(s) 106 - if err != nil { 107 - return s 108 - } 109 - return res 110 - } 141 + return graph, nil 142 + } 111 143 112 - for _, edge := range g.Edges.Edges { 113 - src := unquote(edge.Src) 114 - dest := unquote(edge.Dst) 115 - addNode(src) 116 - addNode(dest) 117 - graph.Edges = append(graph.Edges, &Edge{Src: src, Dest: dest}) 144 + // extractQuotedString extracts the content between quotes from a string like "hello" 145 + func extractQuotedString(s string) string { 146 + s = strings.TrimSpace(s) 147 + if len(s) >= 2 && s[0] == '"' && s[len(s)-1] == '"' { 148 + return s[1 : len(s)-1] 118 149 } 119 - 120 - for _, node := range g.Nodes.Nodes { 121 - name := unquote(node.Name) 122 - addNode(name) 123 - } 124 - return graph, nil 150 + return "" 125 151 } 126 152 127 153 // ToDot converts a Graph to a DOT string. 128 154 func (g *Graph) ToDot() string { 129 155 var b strings.Builder 130 156 b.WriteString("digraph {\n") 131 - for _, edge := range g.Edges { 132 - b.WriteString(fmt.Sprintf(" %q -> %q;\n", edge.Src, edge.Dest)) 157 + 158 + // Write edges first (they implicitly declare nodes) 159 + for src, dests := range g.Edges { 160 + for _, dest := range dests { 161 + b.WriteString(fmt.Sprintf(" %q -> %q;\n", src, dest)) 162 + } 133 163 } 134 - for _, node := range g.Nodes { 135 - b.WriteString(fmt.Sprintf(" %q;\n", node.Name)) 164 + 165 + // Write standalone nodes (nodes without edges) 166 + for node := range g.Nodes { 167 + hasEdge := false 168 + // Check if node appears in any edge 169 + if _, exists := g.Edges[node]; exists { 170 + hasEdge = true 171 + } 172 + if !hasEdge { 173 + for _, dests := range g.Edges { 174 + for _, dest := range dests { 175 + if dest == node { 176 + hasEdge = true 177 + break 178 + } 179 + } 180 + if hasEdge { 181 + break 182 + } 183 + } 184 + } 185 + if !hasEdge { 186 + b.WriteString(fmt.Sprintf(" %q;\n", node)) 187 + } 136 188 } 189 + 137 190 b.WriteString("}") 138 191 return b.String() 139 192 } ··· 148 201 inDegree := make(map[string]int) 149 202 150 203 // Initialize all nodes with in-degree 0 151 - for _, node := range g.Nodes { 152 - inDegree[node.Name] = 0 153 - adjList[node.Name] = []string{} 204 + for node := range g.Nodes { 205 + inDegree[node] = 0 206 + adjList[node] = []string{} 154 207 } 155 208 156 209 // Build the graph and calculate in-degrees 157 210 // Edge from Src to Dest means Src depends on Dest 158 211 // So Dest should run before Src 159 - for _, edge := range g.Edges { 160 - adjList[edge.Dest] = append(adjList[edge.Dest], edge.Src) 161 - inDegree[edge.Src]++ 212 + for src, dests := range g.Edges { 213 + for _, dest := range dests { 214 + adjList[dest] = append(adjList[dest], src) 215 + inDegree[src]++ 216 + } 162 217 } 163 218 164 219 var levels [][]string 165 220 remaining := make(map[string]bool) 166 - for _, node := range g.Nodes { 167 - remaining[node.Name] = true 221 + for node := range g.Nodes { 222 + remaining[node] = true 168 223 } 169 224 170 225 // Process nodes level by level
+54 -100
controller/activities/graph_test.go
··· 27 27 name string 28 28 changed []string 29 29 expectedNodes []string 30 - expectedEdges []Edge 30 + expectedEdges map[string][]string 31 31 }{ 32 32 { 33 33 name: "Prune to C and its dependencies", 34 34 changed: []string{"C"}, 35 35 expectedNodes: []string{"A", "B", "C", "D"}, 36 - expectedEdges: []Edge{ 37 - {Src: "A", Dest: "B"}, 38 - {Src: "B", Dest: "C"}, 39 - {Src: "D", Dest: "B"}, 36 + expectedEdges: map[string][]string{ 37 + "A": {"B"}, 38 + "B": {"C"}, 39 + "D": {"B"}, 40 40 }, 41 41 }, 42 42 { 43 43 name: "Prune to F and its dependencies", 44 44 changed: []string{"F"}, 45 45 expectedNodes: []string{"E", "F"}, 46 - expectedEdges: []Edge{ 47 - {Src: "E", Dest: "F"}, 46 + expectedEdges: map[string][]string{ 47 + "E": {"F"}, 48 48 }, 49 49 }, 50 50 { 51 51 name: "Prune to B and its dependencies", 52 52 changed: []string{"B"}, 53 53 expectedNodes: []string{"A", "B", "D"}, 54 - expectedEdges: []Edge{ 55 - {Src: "A", Dest: "B"}, 56 - {Src: "D", Dest: "B"}, 54 + expectedEdges: map[string][]string{ 55 + "A": {"B"}, 56 + "D": {"B"}, 57 57 }, 58 58 }, 59 59 { 60 60 name: "No nodes changed", 61 61 changed: []string{}, 62 62 expectedNodes: []string{}, 63 - expectedEdges: []Edge{}, 63 + expectedEdges: map[string][]string{}, 64 64 }, 65 65 { 66 66 name: "Changed node not in graph", 67 67 changed: []string{"Z"}, 68 68 expectedNodes: []string{}, 69 - expectedEdges: []Edge{}, 69 + expectedEdges: map[string][]string{}, 70 70 }, 71 71 { 72 72 name: "Multiple changed nodes", 73 73 changed: []string{"C", "F"}, 74 74 expectedNodes: []string{"A", "B", "C", "D", "E", "F"}, 75 - expectedEdges: []Edge{ 76 - {Src: "A", Dest: "B"}, 77 - {Src: "B", Dest: "C"}, 78 - {Src: "D", Dest: "B"}, 79 - {Src: "E", Dest: "F"}, 75 + expectedEdges: map[string][]string{ 76 + "A": {"B"}, 77 + "B": {"C"}, 78 + "D": {"B"}, 79 + "E": {"F"}, 80 80 }, 81 81 }, 82 82 } ··· 88 88 t.Fatalf("PruneGraph failed: %v", err) 89 89 } 90 90 91 - prunedNodes := make([]string, 0, len(prunedGraph.Nodes)) 92 - for _, n := range prunedGraph.Nodes { 93 - prunedNodes = append(prunedNodes, n.Name) 94 - } 91 + prunedNodes := prunedGraph.GetNodes() 95 92 sort.Strings(prunedNodes) 96 93 sort.Strings(tc.expectedNodes) 97 94 ··· 99 96 t.Errorf("Expected nodes %v, but got %v", tc.expectedNodes, prunedNodes) 100 97 } 101 98 102 - prunedEdges := make([]Edge, len(prunedGraph.Edges)) 103 - for i, e := range prunedGraph.Edges { 104 - prunedEdges[i] = *e 105 - } 106 - 107 - // Sort edges for consistent comparison 108 - sort.Slice(prunedEdges, func(i, j int) bool { 109 - if prunedEdges[i].Src != prunedEdges[j].Src { 110 - return prunedEdges[i].Src < prunedEdges[j].Src 111 - } 112 - return prunedEdges[i].Dest < prunedEdges[j].Dest 113 - }) 114 - sort.Slice(tc.expectedEdges, func(i, j int) bool { 115 - if tc.expectedEdges[i].Src != tc.expectedEdges[j].Src { 116 - return tc.expectedEdges[i].Src < tc.expectedEdges[j].Src 117 - } 118 - return tc.expectedEdges[i].Dest < tc.expectedEdges[j].Dest 119 - }) 120 - 121 - if !reflect.DeepEqual(prunedEdges, tc.expectedEdges) { 122 - t.Errorf("Expected edges %v, but got %v", tc.expectedEdges, prunedEdges) 99 + // Compare edges 100 + if !reflect.DeepEqual(prunedGraph.Edges, tc.expectedEdges) { 101 + t.Errorf("Expected edges %v, but got %v", tc.expectedEdges, prunedGraph.Edges) 123 102 } 124 103 }) 125 104 } ··· 271 250 "xshare/azuresql" -> "core"; 272 251 "xshare/azuresqlusers" ; 273 252 "xshare/azuresqlusers" -> "xshare/azuresql"; 274 - }` 275 - 253 + } 254 + ` 276 255 graph, err := NewGraphFromDot(realWorldDot) 277 256 if err != nil { 278 257 t.Fatalf("Failed to create graph from real-world DOT: %v", err) ··· 284 263 t.Fatalf("PruneGraph failed: %v", err) 285 264 } 286 265 266 + // Expected result: 287 267 // digraph { 288 268 // "bootstrap-va" -> "cluster-va"; 289 269 // "dems-cluster-identity" -> "cluster-va"; 290 270 // "pes/keyvault" -> "dems-cluster-identity"; 291 271 // } 292 272 expectedNodes := []string{"bootstrap-va", "cluster-va", "dems-cluster-identity", "pes/keyvault"} 293 - expectedEdges := []Edge{ 294 - {Src: "bootstrap-va", Dest: "cluster-va"}, 295 - {Src: "dems-cluster-identity", Dest: "cluster-va"}, 296 - {Src: "pes/keyvault", Dest: "dems-cluster-identity"}, 273 + expectedEdges := map[string][]string{ 274 + "bootstrap-va": {"cluster-va"}, 275 + "dems-cluster-identity": {"cluster-va"}, 276 + "pes/keyvault": {"dems-cluster-identity"}, 297 277 } 298 278 299 - prunedNodes := make([]string, 0, len(prunedGraph.Nodes)) 300 - for _, n := range prunedGraph.Nodes { 301 - prunedNodes = append(prunedNodes, n.Name) 302 - } 279 + prunedNodes := prunedGraph.GetNodes() 303 280 sort.Strings(prunedNodes) 304 281 sort.Strings(expectedNodes) 305 282 ··· 307 284 t.Errorf("Expected nodes %v, but got %v", expectedNodes, prunedNodes) 308 285 } 309 286 310 - prunedEdges := make([]Edge, len(prunedGraph.Edges)) 311 - for i, e := range prunedGraph.Edges { 312 - prunedEdges[i] = *e 313 - } 314 - 315 - // Sort edges for consistent comparison 316 - sort.Slice(prunedEdges, func(i, j int) bool { 317 - if prunedEdges[i].Src != prunedEdges[j].Src { 318 - return prunedEdges[i].Src < prunedEdges[j].Src 319 - } 320 - return prunedEdges[i].Dest < prunedEdges[j].Dest 321 - }) 322 - sort.Slice(expectedEdges, func(i, j int) bool { 323 - if expectedEdges[i].Src != expectedEdges[j].Src { 324 - return expectedEdges[i].Src < expectedEdges[j].Src 325 - } 326 - return expectedEdges[i].Dest < expectedEdges[j].Dest 327 - }) 328 - 329 - if !reflect.DeepEqual(prunedEdges, expectedEdges) { 330 - t.Errorf("Expected edges %v, but got %v", expectedEdges, prunedEdges) 287 + if !reflect.DeepEqual(prunedGraph.Edges, expectedEdges) { 288 + t.Errorf("Expected edges %v, but got %v", expectedEdges, prunedGraph.Edges) 331 289 } 332 290 } 333 291 ··· 335 293 testCases := []struct { 336 294 name string 337 295 nodes []string 338 - edges []Edge 296 + edges map[string][]string 339 297 expectedLevels [][]string 340 298 }{ 341 299 { 342 300 name: "Simple linear dependency", 343 301 nodes: []string{"A", "B", "C"}, 344 - edges: []Edge{ 345 - {Src: "B", Dest: "A"}, 346 - {Src: "C", Dest: "B"}, 302 + edges: map[string][]string{ 303 + "B": {"A"}, 304 + "C": {"B"}, 347 305 }, 348 306 expectedLevels: [][]string{ 349 307 {"A"}, ··· 354 312 { 355 313 name: "Parallel dependencies", 356 314 nodes: []string{"A", "B", "C", "D"}, 357 - edges: []Edge{ 358 - {Src: "C", Dest: "A"}, 359 - {Src: "C", Dest: "B"}, 360 - {Src: "D", Dest: "C"}, 315 + edges: map[string][]string{ 316 + "C": {"A", "B"}, 317 + "D": {"C"}, 361 318 }, 362 319 expectedLevels: [][]string{ 363 320 {"A", "B"}, ··· 368 325 { 369 326 name: "Complex dependency graph", 370 327 nodes: []string{"A", "B", "C", "D", "E", "F"}, 371 - edges: []Edge{ 372 - {Src: "C", Dest: "A"}, 373 - {Src: "C", Dest: "B"}, 374 - {Src: "D", Dest: "C"}, 375 - {Src: "E", Dest: "C"}, 376 - {Src: "F", Dest: "D"}, 377 - {Src: "F", Dest: "E"}, 328 + edges: map[string][]string{ 329 + "C": {"A", "B"}, 330 + "D": {"C"}, 331 + "E": {"C"}, 332 + "F": {"D", "E"}, 378 333 }, 379 334 expectedLevels: [][]string{ 380 335 {"A", "B"}, ··· 386 341 { 387 342 name: "No dependencies", 388 343 nodes: []string{"A", "B", "C"}, 389 - edges: []Edge{}, 344 + edges: map[string][]string{}, 390 345 expectedLevels: [][]string{{"A", "B", "C"}}, 391 346 }, 392 347 { 393 348 name: "Single node", 394 349 nodes: []string{"A"}, 395 - edges: []Edge{}, 350 + edges: map[string][]string{}, 396 351 expectedLevels: [][]string{{"A"}}, 397 352 }, 398 353 { 399 354 name: "Real world example: bootstrap depends on cluster", 400 355 nodes: []string{"bootstrap", "cluster"}, 401 - edges: []Edge{ 402 - {Src: "bootstrap", Dest: "cluster"}, 356 + edges: map[string][]string{ 357 + "bootstrap": {"cluster"}, 403 358 }, 404 359 expectedLevels: [][]string{ 405 360 {"cluster"}, ··· 411 366 for _, tc := range testCases { 412 367 t.Run(tc.name, func(t *testing.T) { 413 368 // Create graph 414 - graph := &Graph{ 415 - Nodes: make([]*Node, len(tc.nodes)), 416 - Edges: make([]*Edge, len(tc.edges)), 417 - } 369 + graph := NewGraph() 418 370 419 - for i, nodeName := range tc.nodes { 420 - graph.Nodes[i] = &Node{Name: nodeName} 371 + for _, nodeName := range tc.nodes { 372 + graph.AddNode(nodeName) 421 373 } 422 374 423 - for i, edge := range tc.edges { 424 - graph.Edges[i] = &Edge{Src: edge.Src, Dest: edge.Dest} 375 + for src, dests := range tc.edges { 376 + for _, dest := range dests { 377 + graph.AddEdge(src, dest) 378 + } 425 379 } 426 380 427 381 // Get topological sort
-1
controller/go.mod
··· 3 3 go 1.24.3 4 4 5 5 require ( 6 - github.com/awalterschulze/gographviz v2.0.3+incompatible 7 6 github.com/go-git/go-git/v5 v5.16.0 8 7 go.temporal.io/sdk v1.34.0 9 8 )
-2
controller/go.sum
··· 11 11 github.com/anmitsu/go-shlex v0.0.0-20200514113438-38f4b401e2be/go.mod h1:ySMOLuWl6zY27l47sB3qLNK6tF2fkHG55UZxx8oIVo4= 12 12 github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5 h1:0CwZNZbxp69SHPdPJAN/hZIm0C4OItdklCFmMRWYpio= 13 13 github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5/go.mod h1:wHh0iHkYZB8zMSxRWpUBQtwG5a7fFgvEO+odwuTv2gs= 14 - github.com/awalterschulze/gographviz v2.0.3+incompatible h1:9sVEXJBJLwGX7EQVhLm2elIKCm7P2YHFC8v6096G09E= 15 - github.com/awalterschulze/gographviz v2.0.3+incompatible/go.mod h1:GEV5wmg4YquNw7v1kkyoX9etIk8yVmXj+AkDHuuETHs= 16 14 github.com/benbjohnson/clock v1.1.0/go.mod h1:J11/hYXuz8f4ySSvYwY0FKfm+ezbsZBKZxNJlLklBHA= 17 15 github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU= 18 16 github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw=
+3 -2
controller/workflows/infra.go
··· 5 5 "time" 6 6 7 7 "cloudlab/controller/activities" 8 + 8 9 "go.temporal.io/sdk/workflow" 9 10 ) 10 11 ··· 58 59 return nil, err 59 60 } 60 61 61 - logger.Info("Infra workflow completed graph pruning.", "nodes", len(prunedGraph.Nodes), "edges", len(prunedGraph.Edges)) 62 + logger.Info("Infra workflow completed graph pruning.", "nodes", prunedGraph.NodeCount(), "edges", prunedGraph.EdgeCount()) 62 63 63 64 // Get dependency levels for parallel execution 64 65 dependencyLevels := prunedGraph.TopologicalSort() ··· 93 94 logger.Info("Completed terragrunt apply for dependency level", "level", levelIndex, "modules", level) 94 95 } 95 96 96 - logger.Info("Infra workflow completed successfully.", "totalLevels", len(dependencyLevels), "appliedModules", len(prunedGraph.Nodes)) 97 + logger.Info("Infra workflow completed successfully.", "totalLevels", len(dependencyLevels), "appliedModules", prunedGraph.NodeCount()) 97 98 98 99 return prunedGraph, nil 99 100 }