github.com/kiali/kiali@v1.84.0/graph/config/cytoscape/cytoscape.go (about) 1 // Package cytoscape provides conversion from our graph to the CystoscapeJS 2 // configuration json model. 3 // 4 // The following links are useful for understanding CytoscapeJS and it's configuration: 5 // 6 // Main page: http://js.cytoscape.org/ 7 // JSON config: http://js.cytoscape.org/#notation/elements-json 8 // Demos: http://js.cytoscape.org/#demos 9 // 10 // Algorithm: Process the graph structure adding nodes and edges, decorating each 11 // with information provided. An optional second pass generates compound 12 // nodes for requested boxing. 13 // 14 // The package provides the Cytoscape implementation of graph/ConfigVendor. 15 16 package cytoscape 17 18 import ( 19 "crypto/md5" 20 "fmt" 21 "sort" 22 "strings" 23 24 "github.com/kiali/kiali/graph" 25 ) 26 27 // ResponseFlags is a map of maps. Each response code is broken down by responseFlags:percentageOfTraffic, e.g.: 28 // "200" : { 29 // "-" : "80.0", 30 // "DC" : "10.0", 31 // "FI,FD" : "10.0" 32 // }, ... 33 34 type ResponseFlags map[string]string 35 36 // ResponseHosts is a map of maps. Each response host is broken down by responseFlags:percentageOfTraffic, e.g.: 37 // 38 // "200" : { 39 // "www.google.com" : "80.0", 40 // "www.yahoo.com" : "20.0" 41 // }, ... 42 type ResponseHosts map[string]string 43 44 // ResponseDetail holds information broken down by response code. 45 type ResponseDetail struct { 46 Flags ResponseFlags `json:"flags,omitempty"` 47 Hosts ResponseHosts `json:"hosts,omitempty"` 48 } 49 50 // Responses maps responseCodes to detailed information for that code 51 type Responses map[string]*ResponseDetail 52 53 // ProtocolTraffic supplies all of the traffic information for a single protocol 54 type ProtocolTraffic struct { 55 Protocol string `json:"protocol,omitempty"` // protocol 56 Rates map[string]string `json:"rates,omitempty"` // map[rate]value 57 Responses Responses `json:"responses,omitempty"` // see comment above 58 } 59 60 // GWInfo contains the resolved gateway configuration if the node represents an Istio gateway 61 type GWInfo struct { 62 // IngressInfo contains the resolved gateway configuration if the node represents an Istio ingress gateway 63 IngressInfo GWInfoIngress `json:"ingressInfo,omitempty"` 64 // EgressInfo contains the resolved gateway configuration if the node represents an Istio egress gateway 65 EgressInfo GWInfoIngress `json:"egressInfo,omitempty"` 66 // GatewayAPIInfo contains the resolved gateway configuration if the node represents a Gateway API gateway 67 GatewayAPIInfo GWInfoIngress `json:"gatewayAPIInfo,omitempty"` 68 } 69 70 // GWInfoIngress contains the resolved gateway configuration if the node represents an Istio ingress gateway 71 type GWInfoIngress struct { 72 // Hostnames is the list of hosts being served by the associated Istio gateways. 73 Hostnames []string `json:"hostnames,omitempty"` 74 } 75 76 // VSInfo contains the resolved VS configuration if the node has a VS attached. 77 type VSInfo struct { 78 // Hostnames is the list of hostnames configured in the associated VSs 79 Hostnames []string `json:"hostnames,omitempty"` 80 } 81 82 // HealthConfig maps annotations information for health 83 type HealthConfig map[string]string 84 85 type NodeData struct { 86 // Cytoscape Fields 87 ID string `json:"id"` // unique internal node ID (n0, n1...) 88 Parent string `json:"parent,omitempty"` // Compound Node parent ID 89 90 // App Fields (not required by Cytoscape) 91 NodeType string `json:"nodeType"` 92 Cluster string `json:"cluster"` 93 Namespace string `json:"namespace"` 94 Workload string `json:"workload,omitempty"` 95 App string `json:"app,omitempty"` 96 Version string `json:"version,omitempty"` 97 Service string `json:"service,omitempty"` // requested service for NodeTypeService 98 Aggregate string `json:"aggregate,omitempty"` // set like "<aggregate>=<aggregateVal>" 99 DestServices []graph.ServiceName `json:"destServices,omitempty"` // requested services for [dest] node 100 Labels map[string]string `json:"labels,omitempty"` // k8s labels associated with the node 101 Traffic []ProtocolTraffic `json:"traffic,omitempty"` // traffic rates for all detected protocols 102 HealthData interface{} `json:"healthData"` // data to calculate health status from configurations 103 HealthDataApp interface{} `json:"-"` // for local use to generate appBox health 104 HasCB bool `json:"hasCB,omitempty"` // true (has circuit breaker) | false 105 HasFaultInjection bool `json:"hasFaultInjection,omitempty"` // true (vs has fault injection) | false 106 HasHealthConfig HealthConfig `json:"hasHealthConfig,omitempty"` // set to the health config override 107 HasMirroring bool `json:"hasMirroring,omitempty"` // true (has mirroring) | false 108 HasRequestRouting bool `json:"hasRequestRouting,omitempty"` // true (vs has request routing) | false 109 HasRequestTimeout bool `json:"hasRequestTimeout,omitempty"` // true (vs has request timeout) | false 110 HasTCPTrafficShifting bool `json:"hasTCPTrafficShifting,omitempty"` // true (vs has tcp traffic shifting) | false 111 HasTrafficShifting bool `json:"hasTrafficShifting,omitempty"` // true (vs has traffic shifting) | false 112 HasVS *VSInfo `json:"hasVS,omitempty"` // it can be empty if there is a VS without hostnames 113 HasWorkloadEntry []graph.WEInfo `json:"hasWorkloadEntry,omitempty"` // static workload entry information | empty if there are no workload entries 114 IsBox string `json:"isBox,omitempty"` // set for NodeTypeBox, current values: [ 'app', 'cluster', 'namespace' ] 115 IsDead bool `json:"isDead,omitempty"` // true (has no pods) | false 116 IsGateway *GWInfo `json:"isGateway,omitempty"` // Istio ingress/egress gateway information 117 IsIdle bool `json:"isIdle,omitempty"` // true | false 118 IsInaccessible bool `json:"isInaccessible,omitempty"` // true if the node exists in an inaccessible namespace 119 IsK8sGatewayAPI bool `json:"isK8sGatewayAPI,omitempty"` // true (object is auto-generated from K8s API Gateway) | false 120 IsOutOfMesh bool `json:"isOutOfMesh,omitempty"` // true (has missing sidecar) | false 121 IsOutside bool `json:"isOutside,omitempty"` // true | false 122 IsRoot bool `json:"isRoot,omitempty"` // true | false 123 IsServiceEntry *graph.SEInfo `json:"isServiceEntry,omitempty"` // set static service entry information 124 IsWaypoint bool `json:"isWaypoint,omitempty"` // true | false 125 } 126 127 type EdgeData struct { 128 // Cytoscape Fields 129 ID string `json:"id"` // unique internal edge ID (e0, e1...) 130 Source string `json:"source"` // parent node ID 131 Target string `json:"target"` // child node ID 132 133 // App Fields (not required by Cytoscape) 134 DestPrincipal string `json:"destPrincipal,omitempty"` // principal used for the edge destination 135 IsMTLS string `json:"isMTLS,omitempty"` // set to the percentage of traffic using a mutual TLS connection 136 ResponseTime string `json:"responseTime,omitempty"` // in millis 137 SourcePrincipal string `json:"sourcePrincipal,omitempty"` // principal used for the edge source 138 Throughput string `json:"throughput,omitempty"` // in bytes/sec (request or response, depends on client request) 139 Traffic ProtocolTraffic `json:"traffic,omitempty"` // traffic rates for the edge protocol 140 } 141 142 type NodeWrapper struct { 143 Data *NodeData `json:"data"` 144 } 145 146 type EdgeWrapper struct { 147 Data *EdgeData `json:"data"` 148 } 149 150 type Elements struct { 151 Nodes []*NodeWrapper `json:"nodes"` 152 Edges []*EdgeWrapper `json:"edges"` 153 } 154 155 type Config struct { 156 Timestamp int64 `json:"timestamp"` 157 Duration int64 `json:"duration"` 158 GraphType string `json:"graphType"` 159 Elements Elements `json:"elements"` 160 } 161 162 func nodeHash(id string) string { 163 return fmt.Sprintf("%x", md5.Sum([]byte(id))) 164 } 165 166 func edgeHash(from, to, protocol string) string { 167 return fmt.Sprintf("%x", md5.Sum([]byte(fmt.Sprintf("%s.%s.%s", from, to, protocol)))) 168 } 169 170 // NewConfig is required by the graph/ConfigVendor interface 171 func NewConfig(trafficMap graph.TrafficMap, o graph.ConfigOptions) (result Config) { 172 nodes := []*NodeWrapper{} 173 edges := []*EdgeWrapper{} 174 175 buildConfig(trafficMap, &nodes, &edges, o) 176 177 // Add compound nodes as needed, inner boxes first 178 if strings.Contains(o.BoxBy, graph.BoxByApp) || o.GraphType == graph.GraphTypeApp || o.GraphType == graph.GraphTypeVersionedApp { 179 boxByApp(&nodes) 180 } 181 if strings.Contains(o.BoxBy, graph.BoxByNamespace) { 182 boxByNamespace(&nodes) 183 } 184 if strings.Contains(o.BoxBy, graph.BoxByCluster) { 185 boxByCluster(&nodes) 186 } 187 188 // sort nodes and edges for better json presentation (and predictable testing) 189 // kiali-1258 parent nodes must come before the child references 190 sort.Slice(nodes, func(i, j int) bool { 191 switch { 192 case nodes[i].Data.IsBox != nodes[j].Data.IsBox: 193 rank := func(boxBy string) int { 194 switch boxBy { 195 case graph.BoxByCluster: 196 return 0 197 case graph.BoxByNamespace: 198 return 1 199 case graph.BoxByApp: 200 return 2 201 default: 202 return 3 203 } 204 } 205 return rank(nodes[i].Data.IsBox) < rank(nodes[j].Data.IsBox) 206 case nodes[i].Data.Cluster != nodes[j].Data.Cluster: 207 return nodes[i].Data.Cluster < nodes[j].Data.Cluster 208 case nodes[i].Data.Namespace != nodes[j].Data.Namespace: 209 return nodes[i].Data.Namespace < nodes[j].Data.Namespace 210 case nodes[i].Data.App != nodes[j].Data.App: 211 return nodes[i].Data.App < nodes[j].Data.App 212 case nodes[i].Data.Version != nodes[j].Data.Version: 213 return nodes[i].Data.Version < nodes[j].Data.Version 214 case nodes[i].Data.Service != nodes[j].Data.Service: 215 return nodes[i].Data.Service < nodes[j].Data.Service 216 default: 217 return nodes[i].Data.Workload < nodes[j].Data.Workload 218 } 219 }) 220 sort.Slice(edges, func(i, j int) bool { 221 switch { 222 case edges[i].Data.Source != edges[j].Data.Source: 223 return edges[i].Data.Source < edges[j].Data.Source 224 case edges[i].Data.Target != edges[j].Data.Target: 225 return edges[i].Data.Target < edges[j].Data.Target 226 default: 227 // source and target are the same, it must differ on protocol 228 return edges[i].Data.Traffic.Protocol < edges[j].Data.Traffic.Protocol 229 } 230 }) 231 232 elements := Elements{nodes, edges} 233 result = Config{ 234 Duration: int64(o.Duration.Seconds()), 235 Timestamp: o.QueryTime, 236 GraphType: o.GraphType, 237 Elements: elements, 238 } 239 return result 240 } 241 242 func buildConfig(trafficMap graph.TrafficMap, nodes *[]*NodeWrapper, edges *[]*EdgeWrapper, o graph.ConfigOptions) { 243 for id, n := range trafficMap { 244 nodeID := nodeHash(id) 245 246 nd := &NodeData{ 247 ID: nodeID, 248 NodeType: n.NodeType, 249 Cluster: n.Cluster, 250 Namespace: n.Namespace, 251 Workload: n.Workload, 252 App: n.App, 253 Version: n.Version, 254 Service: n.Service, 255 } 256 257 addNodeTelemetry(n, nd) 258 259 if val, ok := n.Metadata[graph.HealthData]; ok { 260 nd.HealthData = val 261 } 262 if val, ok := n.Metadata[graph.HealthDataApp]; ok { 263 nd.HealthDataApp = val 264 } 265 266 // set k8s labels, if any 267 if val, ok := n.Metadata[graph.Labels]; ok { 268 nd.Labels = val.(graph.LabelsMetadata) 269 } 270 271 // set annotations, if available 272 if val, ok := n.Metadata[graph.HasHealthConfig]; ok { 273 nd.HasHealthConfig = val.(map[string]string) 274 } 275 276 // node may have deployment but no pods running) 277 if val, ok := n.Metadata[graph.IsDead]; ok { 278 nd.IsDead = val.(bool) 279 } 280 281 // node may be idle 282 if val, ok := n.Metadata[graph.IsIdle]; ok { 283 nd.IsIdle = val.(bool) 284 } 285 286 // node may be a root 287 if val, ok := n.Metadata[graph.IsRoot]; ok { 288 nd.IsRoot = val.(bool) 289 } 290 291 // node is not accessible to the current user 292 if val, ok := n.Metadata[graph.IsInaccessible]; ok { 293 nd.IsInaccessible = val.(bool) 294 } 295 296 // node may represent an Istio Ingress Gateway 297 if ingGateways, ok := n.Metadata[graph.IsIngressGateway]; ok { 298 var configuredHostnames []string 299 for _, hosts := range ingGateways.(graph.GatewaysMetadata) { 300 configuredHostnames = append(configuredHostnames, hosts...) 301 } 302 303 nd.IsGateway = &GWInfo{ 304 IngressInfo: GWInfoIngress{Hostnames: configuredHostnames}, 305 } 306 } else if egrGateways, ok := n.Metadata[graph.IsEgressGateway]; ok { 307 // node may represent an Istio Egress Gateway 308 var configuredHostnames []string 309 for _, hosts := range egrGateways.(graph.GatewaysMetadata) { 310 configuredHostnames = append(configuredHostnames, hosts...) 311 } 312 313 nd.IsGateway = &GWInfo{ 314 EgressInfo: GWInfoIngress{Hostnames: configuredHostnames}, 315 } 316 } else if apiGateways, ok := n.Metadata[graph.IsGatewayAPI]; ok { 317 // node may represent a Gateway API 318 var configuredHostnames []string 319 for _, hosts := range apiGateways.(graph.GatewaysMetadata) { 320 configuredHostnames = append(configuredHostnames, hosts...) 321 } 322 323 nd.IsGateway = &GWInfo{ 324 GatewayAPIInfo: GWInfoIngress{Hostnames: configuredHostnames}, 325 } 326 327 nd.IsK8sGatewayAPI = true 328 } 329 330 // node may have a circuit breaker 331 if val, ok := n.Metadata[graph.HasCB]; ok { 332 nd.HasCB = val.(bool) 333 } 334 335 // node may have a virtual service 336 if virtualServices, ok := n.Metadata[graph.HasVS]; ok { 337 338 var configuredHostnames []string 339 for _, hosts := range virtualServices.(graph.VirtualServicesMetadata) { 340 configuredHostnames = append(configuredHostnames, hosts...) 341 } 342 343 nd.HasVS = &VSInfo{Hostnames: configuredHostnames} 344 } 345 346 // set mesh checks, if available 347 if val, ok := n.Metadata[graph.IsOutOfMesh]; ok { 348 nd.IsOutOfMesh = val.(bool) 349 } 350 351 // check if node is on another namespace 352 if val, ok := n.Metadata[graph.IsOutside]; ok { 353 nd.IsOutside = val.(bool) 354 } 355 356 // check if node is a waypoint proxy 357 if val, ok := n.Metadata[graph.IsWaypoint]; ok { 358 nd.IsWaypoint = val.(bool) 359 } 360 361 if val, ok := n.Metadata[graph.HasMirroring]; ok { 362 nd.HasMirroring = val.(bool) 363 } 364 365 if val, ok := n.Metadata[graph.HasRequestRouting]; ok { 366 nd.HasRequestRouting = val.(bool) 367 } 368 369 if val, ok := n.Metadata[graph.HasFaultInjection]; ok { 370 nd.HasFaultInjection = val.(bool) 371 } 372 373 if val, ok := n.Metadata[graph.HasTrafficShifting]; ok { 374 nd.HasTrafficShifting = val.(bool) 375 } 376 377 if val, ok := n.Metadata[graph.HasTCPTrafficShifting]; ok { 378 nd.HasTCPTrafficShifting = val.(bool) 379 } 380 381 if val, ok := n.Metadata[graph.HasRequestTimeout]; ok { 382 nd.HasRequestTimeout = val.(bool) 383 } 384 385 if val, ok := n.Metadata[graph.IsK8sGatewayAPI]; ok { 386 nd.IsK8sGatewayAPI = val.(bool) 387 } 388 389 // node may have destination service info 390 if val, ok := n.Metadata[graph.DestServices]; ok { 391 nd.DestServices = []graph.ServiceName{} 392 for _, val := range val.(graph.DestServicesMetadata) { 393 nd.DestServices = append(nd.DestServices, val) 394 } 395 } 396 397 // node may have service entry static info 398 if val, ok := n.Metadata[graph.IsServiceEntry]; ok { 399 nd.IsServiceEntry = val.(*graph.SEInfo) 400 } 401 402 // node may have a workload entry associated with it 403 if val, ok := n.Metadata[graph.HasWorkloadEntry]; ok { 404 nd.HasWorkloadEntry = []graph.WEInfo{} 405 if weInfo, ok := val.([]graph.WEInfo); ok { 406 nd.HasWorkloadEntry = append(nd.HasWorkloadEntry, weInfo...) 407 } 408 } 409 410 // node may be an aggregate 411 if n.NodeType == graph.NodeTypeAggregate { 412 nd.Aggregate = fmt.Sprintf("%s=%s", n.Metadata[graph.Aggregate].(string), n.Metadata[graph.AggregateValue].(string)) 413 } 414 415 nw := NodeWrapper{ 416 Data: nd, 417 } 418 419 *nodes = append(*nodes, &nw) 420 421 for _, e := range n.Edges { 422 sourceIDHash := nodeHash(n.ID) 423 destIDHash := nodeHash(e.Dest.ID) 424 protocol := "" 425 if e.Metadata[graph.ProtocolKey] != nil { 426 protocol = e.Metadata[graph.ProtocolKey].(string) 427 } 428 edgeID := edgeHash(sourceIDHash, destIDHash, protocol) 429 ed := EdgeData{ 430 ID: edgeID, 431 Source: sourceIDHash, 432 Target: destIDHash, 433 Traffic: ProtocolTraffic{ 434 Protocol: protocol, 435 }, 436 } 437 if e.Metadata[graph.DestPrincipal] != nil { 438 ed.DestPrincipal = e.Metadata[graph.DestPrincipal].(string) 439 } 440 if e.Metadata[graph.SourcePrincipal] != nil { 441 ed.SourcePrincipal = e.Metadata[graph.SourcePrincipal].(string) 442 } 443 addEdgeTelemetry(e, &ed) 444 445 ew := EdgeWrapper{ 446 Data: &ed, 447 } 448 *edges = append(*edges, &ew) 449 } 450 } 451 } 452 453 func addNodeTelemetry(n *graph.Node, nd *NodeData) { 454 for _, p := range graph.Protocols { 455 protocolTraffic := ProtocolTraffic{Protocol: p.Name} 456 for _, r := range p.NodeRates { 457 if rateVal := getRate(n.Metadata, r.Name); rateVal > 0.0 { 458 if protocolTraffic.Rates == nil { 459 protocolTraffic.Rates = make(map[string]string) 460 } 461 protocolTraffic.Rates[string(r.Name)] = rateToString(r.Precision, rateVal) 462 } 463 } 464 if protocolTraffic.Rates != nil { 465 if nd.Traffic == nil { 466 nd.Traffic = []ProtocolTraffic{} 467 } 468 nd.Traffic = append(nd.Traffic, protocolTraffic) 469 } 470 } 471 } 472 473 func addEdgeTelemetry(e *graph.Edge, ed *EdgeData) { 474 if val, ok := e.Metadata[graph.IsMTLS]; ok { 475 ed.IsMTLS = fmt.Sprintf("%.0f", val.(float64)) 476 } 477 if val, ok := e.Metadata[graph.ResponseTime]; ok { 478 responseTime := val.(float64) 479 ed.ResponseTime = fmt.Sprintf("%.0f", responseTime) 480 } 481 if val, ok := e.Metadata[graph.Throughput]; ok { 482 throughput := val.(float64) 483 ed.Throughput = fmt.Sprintf("%.0f", throughput) 484 } 485 486 // an edge represents traffic for at most one protocol 487 for _, p := range graph.Protocols { 488 protocolTraffic := ProtocolTraffic{Protocol: p.Name} 489 total := 0.0 490 err := 0.0 491 var percentErr, percentReq graph.Rate 492 for _, r := range p.EdgeRates { 493 rateVal := getRate(e.Metadata, r.Name) 494 switch { 495 case r.IsTotal: 496 // there is one field holding the total traffic 497 total = rateVal 498 case r.IsErr: 499 // error rates can be reported for several error status codes, so sum up all 500 // of the error traffic to be used in the percentErr calculation below. 501 err += rateVal 502 case r.IsPercentErr: 503 // hold onto the percentErr field so we know how to report it below 504 percentErr = r 505 case r.IsPercentReq: 506 // hold onto the percentReq field so we know how to report it below 507 percentReq = r 508 } 509 if rateVal > 0.0 { 510 if protocolTraffic.Rates == nil { 511 protocolTraffic.Rates = make(map[string]string) 512 } 513 protocolTraffic.Rates[string(r.Name)] = rateToString(r.Precision, rateVal) 514 } 515 } 516 if protocolTraffic.Rates != nil { 517 if total > 0 { 518 if percentErr.Name != "" { 519 rateVal := err / total * 100 520 if rateVal > 0.0 { 521 protocolTraffic.Rates[string(percentErr.Name)] = fmt.Sprintf("%.*f", percentErr.Precision, rateVal) 522 } 523 } 524 if percentReq.Name != "" { 525 rateVal := 0.0 526 for _, r := range p.NodeRates { 527 if !r.IsOut { 528 continue 529 } 530 rateVal = total / getRate(e.Source.Metadata, r.Name) * 100.0 531 break 532 } 533 if rateVal > 0.0 { 534 protocolTraffic.Rates[string(percentReq.Name)] = fmt.Sprintf("%.*f", percentReq.Precision, rateVal) 535 } 536 } 537 mdResponses := e.Metadata[p.EdgeResponses].(graph.Responses) 538 for code, detail := range mdResponses { 539 responseFlags := make(ResponseFlags) 540 responseHosts := make(ResponseHosts) 541 for flags, value := range detail.Flags { 542 responseFlags[flags] = fmt.Sprintf("%.*f", 1, value/total*100.0) 543 } 544 for host, value := range detail.Hosts { 545 responseHosts[host] = fmt.Sprintf("%.*f", 1, value/total*100.0) 546 } 547 responseDetail := &ResponseDetail{Flags: responseFlags, Hosts: responseHosts} 548 if protocolTraffic.Responses == nil { 549 protocolTraffic.Responses = Responses{code: responseDetail} 550 } else { 551 protocolTraffic.Responses[code] = responseDetail 552 } 553 } 554 ed.Traffic = protocolTraffic 555 } 556 break 557 } 558 } 559 } 560 561 func getRate(md graph.Metadata, k graph.MetadataKey) float64 { 562 if rate, ok := md[k]; ok { 563 return rate.(float64) 564 } 565 return 0.0 566 } 567 568 // boxByApp adds compound nodes to box nodes for the same app 569 func boxByApp(nodes *[]*NodeWrapper) { 570 box := make(map[string][]*NodeData) 571 572 for _, nw := range *nodes { 573 if nw.Data.App != "unknown" && nw.Data.App != "" { 574 k := fmt.Sprintf("box_%s_%s_%s", nw.Data.Cluster, nw.Data.Namespace, nw.Data.App) 575 box[k] = append(box[k], nw.Data) 576 } 577 } 578 579 generateBoxCompoundNodes(box, nodes, graph.BoxByApp) 580 } 581 582 // boxByNamespace adds compound nodes to box nodes in the same namespace 583 func boxByNamespace(nodes *[]*NodeWrapper) { 584 box := make(map[string][]*NodeData) 585 586 for _, nw := range *nodes { 587 // never box unknown 588 if nw.Data.Parent == "" && nw.Data.Namespace != graph.Unknown { 589 k := fmt.Sprintf("box_%s_%s", nw.Data.Cluster, nw.Data.Namespace) 590 box[k] = append(box[k], nw.Data) 591 } 592 } 593 if len(box) > 1 { 594 generateBoxCompoundNodes(box, nodes, graph.BoxByNamespace) 595 } 596 } 597 598 // boxByCluster adds compound nodes to box nodes in the same cluster 599 func boxByCluster(nodes *[]*NodeWrapper) { 600 box := make(map[string][]*NodeData) 601 602 for _, nw := range *nodes { 603 // never box unknown 604 if nw.Data.Parent == "" && nw.Data.Cluster != graph.Unknown { 605 k := fmt.Sprintf("box_%s", nw.Data.Cluster) 606 box[k] = append(box[k], nw.Data) 607 } 608 } 609 if len(box) > 1 { 610 generateBoxCompoundNodes(box, nodes, graph.BoxByCluster) 611 } 612 } 613 614 func generateBoxCompoundNodes(box map[string][]*NodeData, nodes *[]*NodeWrapper, boxBy string) { 615 for k, members := range box { 616 if len(members) > 1 { 617 // create the compound (parent) node for the member nodes 618 nodeID := nodeHash(k) 619 namespace := "" 620 app := "" 621 switch boxBy { 622 case graph.BoxByNamespace: 623 namespace = members[0].Namespace 624 case graph.BoxByApp: 625 namespace = members[0].Namespace 626 app = members[0].App 627 } 628 nd := NodeData{ 629 ID: nodeID, 630 NodeType: graph.NodeTypeBox, 631 Cluster: members[0].Cluster, 632 Namespace: namespace, 633 App: app, 634 Version: "", 635 IsBox: boxBy, 636 } 637 638 nw := NodeWrapper{ 639 Data: &nd, 640 } 641 642 // assign each member node to the compound parent 643 nd.IsOutOfMesh = false // TODO: this is probably unecessarily noisy 644 nd.IsInaccessible = false 645 nd.IsOutside = false 646 647 for _, n := range members { 648 n.Parent = nodeID 649 650 // For logical boxing (app), copy some member attributes to to the box node 651 if boxBy == graph.BoxByApp { 652 // make sure to use app health for the app box 653 if nd.HealthData == nil && n.NodeType == graph.NodeTypeApp { 654 if graph.IsOK(n.Workload) { 655 // for versionedApp node, use the app health (n.HealthData has workload health) 656 nd.HealthData = n.HealthDataApp 657 } else { 658 // for app node just ue the node's health 659 nd.HealthData = n.HealthData 660 } 661 } 662 nd.IsOutOfMesh = nd.IsOutOfMesh || n.IsOutOfMesh 663 nd.IsInaccessible = nd.IsInaccessible || n.IsInaccessible 664 nd.IsOutside = nd.IsOutside || n.IsOutside 665 } 666 } 667 668 // add the compound node to the list of nodes 669 *nodes = append(*nodes, &nw) 670 } 671 } 672 } 673 674 func rateToString(minPrecision int, rateVal float64) string { 675 precision := minPrecision 676 if requiredPrecision := calcPrecision(rateVal, 5); requiredPrecision > minPrecision { 677 precision = requiredPrecision 678 } 679 680 return fmt.Sprintf("%.*f", precision, rateVal) 681 } 682 683 // calcPrecision returns the precision necessary to see at least one significant digit (up to max) 684 func calcPrecision(val float64, max int) int { 685 if val <= 0 { 686 return 0 687 } 688 689 precision := 0 690 for precision < max { 691 if val >= 1 { 692 break 693 } 694 val *= 10 695 precision++ 696 } 697 return precision 698 }