github.com/kiali/kiali@v1.84.0/graph/telemetry/istio/appender/istio_details.go (about) 1 package appender 2 3 import ( 4 "context" 5 "strings" 6 7 networking_v1beta1 "istio.io/client-go/pkg/apis/networking/v1beta1" 8 "k8s.io/apimachinery/pkg/labels" 9 k8s_networking_v1 "sigs.k8s.io/gateway-api/apis/v1" 10 11 "github.com/kiali/kiali/business" 12 "github.com/kiali/kiali/config" 13 "github.com/kiali/kiali/graph" 14 "github.com/kiali/kiali/kubernetes" 15 "github.com/kiali/kiali/log" 16 "github.com/kiali/kiali/models" 17 ) 18 19 const IstioAppenderName = "istio" 20 21 // IstioAppender is responsible for badging nodes with special Istio significance: 22 // - CircuitBreaker: n.Metadata[HasCB] = true 23 // - Ingress Gateways: n.Metadata[IsIngressGateway] = Map of GatewayName => hosts 24 // - VirtualService: n.Metadata[HasVS] = Map of VirtualServiceName => hosts 25 // Name: istio 26 type IstioAppender struct { 27 AccessibleNamespaces graph.AccessibleNamespaces 28 } 29 30 // Name implements Appender 31 func (a IstioAppender) Name() string { 32 return IstioAppenderName 33 } 34 35 // IsFinalizer implements Appender 36 func (a IstioAppender) IsFinalizer() bool { 37 return false 38 } 39 40 // AppendGraph implements Appender 41 func (a IstioAppender) AppendGraph(trafficMap graph.TrafficMap, globalInfo *graph.AppenderGlobalInfo, namespaceInfo *graph.AppenderNamespaceInfo) { 42 if len(trafficMap) == 0 { 43 return 44 } 45 46 serviceLists := getServiceLists(trafficMap, namespaceInfo.Namespace, globalInfo) 47 48 addBadging(trafficMap, globalInfo, namespaceInfo) 49 addLabels(trafficMap, globalInfo, serviceLists) 50 a.decorateGateways(trafficMap, globalInfo, namespaceInfo) 51 } 52 53 func addBadging(trafficMap graph.TrafficMap, globalInfo *graph.AppenderGlobalInfo, namespaceInfo *graph.AppenderNamespaceInfo) { 54 clusters := getTrafficClusters(trafficMap, namespaceInfo.Namespace, globalInfo) 55 destinationRuleLists := map[string]models.IstioConfigList{} 56 virtualServiceLists := map[string]models.IstioConfigList{} 57 58 for _, cluster := range clusters { 59 // Currently no other appenders use DestinationRules or VirtualServices, so they are not cached in AppenderNamespaceInfo 60 destinationRuleList, err := globalInfo.Business.IstioConfig.GetIstioConfigListForNamespace(context.TODO(), cluster, namespaceInfo.Namespace, business.IstioConfigCriteria{ 61 IncludeDestinationRules: true, 62 }) 63 graph.CheckError(err) 64 destinationRuleLists[cluster] = *destinationRuleList 65 66 virtualServiceList, err := globalInfo.Business.IstioConfig.GetIstioConfigList(context.TODO(), cluster, business.IstioConfigCriteria{ 67 IncludeVirtualServices: true, 68 }) 69 graph.CheckError(err) 70 virtualServiceLists[cluster] = *virtualServiceList 71 } 72 73 applyCircuitBreakers(trafficMap, namespaceInfo.Namespace, destinationRuleLists) 74 applyVirtualServices(trafficMap, namespaceInfo.Namespace, virtualServiceLists) 75 } 76 77 func applyCircuitBreakers(trafficMap graph.TrafficMap, namespace string, destinationRuleLists map[string]models.IstioConfigList) { 78 NODES: 79 for _, n := range trafficMap { 80 // Skip the check if this node is outside the requested namespace, we limit badging to the requested namespaces 81 if n.Namespace != namespace { 82 continue 83 } 84 85 // Note, Because DestinationRules are applied to services we limit CB badges to service nodes and app nodes. 86 // Whether we should add to workload nodes is debatable, we could add it later if needed. 87 versionOk := graph.IsOK(n.Version) 88 switch { 89 case n.NodeType == graph.NodeTypeService: 90 for _, destinationRule := range destinationRuleLists[n.Cluster].DestinationRules { 91 if models.HasDRCircuitBreaker(destinationRule, namespace, n.Service, "") { 92 n.Metadata[graph.HasCB] = true 93 continue NODES 94 } 95 } 96 case !versionOk && (n.NodeType == graph.NodeTypeApp): 97 if destServices, ok := n.Metadata[graph.DestServices]; ok { 98 for _, ds := range destServices.(graph.DestServicesMetadata) { 99 for _, destinationRule := range destinationRuleLists[n.Cluster].DestinationRules { 100 if models.HasDRCircuitBreaker(destinationRule, ds.Namespace, ds.Name, "") { 101 n.Metadata[graph.HasCB] = true 102 continue NODES 103 } 104 } 105 } 106 } 107 case versionOk: 108 if destServices, ok := n.Metadata[graph.DestServices]; ok { 109 for _, ds := range destServices.(graph.DestServicesMetadata) { 110 for _, destinationRule := range destinationRuleLists[n.Cluster].DestinationRules { 111 if models.HasDRCircuitBreaker(destinationRule, ds.Namespace, ds.Name, n.Version) { 112 n.Metadata[graph.HasCB] = true 113 continue NODES 114 } 115 } 116 } 117 } 118 default: 119 continue 120 } 121 } 122 } 123 124 func applyVirtualServices(trafficMap graph.TrafficMap, namespace string, virtualServiceLists map[string]models.IstioConfigList) { 125 NODES: 126 for _, n := range trafficMap { 127 if n.NodeType != graph.NodeTypeService { 128 continue 129 } 130 for _, virtualService := range virtualServiceLists[n.Cluster].VirtualServices { 131 if models.IsVSValidHost(virtualService, n.Namespace, n.Service) { 132 var vsMetadata graph.VirtualServicesMetadata 133 var vsOk bool 134 if vsMetadata, vsOk = n.Metadata[graph.HasVS].(graph.VirtualServicesMetadata); !vsOk { 135 vsMetadata = make(graph.VirtualServicesMetadata) 136 n.Metadata[graph.HasVS] = vsMetadata 137 } 138 139 if len(virtualService.Spec.Hosts) != 0 { 140 vsMetadata[virtualService.Name] = virtualService.Spec.Hosts 141 } 142 143 if models.HasVSRequestRouting(virtualService) { 144 n.Metadata[graph.HasRequestRouting] = true 145 } 146 147 if models.HasVSRequestTimeout(virtualService) { 148 n.Metadata[graph.HasRequestTimeout] = true 149 } 150 151 if models.HasVSFaultInjection(virtualService) { 152 n.Metadata[graph.HasFaultInjection] = true 153 } 154 155 if models.HasVSTrafficShifting(virtualService) { 156 n.Metadata[graph.HasTrafficShifting] = true 157 } 158 159 if models.HasVSTCPTrafficShifting(virtualService) { 160 n.Metadata[graph.HasTCPTrafficShifting] = true 161 } 162 163 if models.HasVSMirroring(virtualService) { 164 n.Metadata[graph.HasMirroring] = true 165 } 166 167 if kubernetes.IsAutogenerated(virtualService.Name) { 168 n.Metadata[graph.IsK8sGatewayAPI] = true 169 } 170 171 continue NODES 172 } 173 } 174 } 175 } 176 177 // addLabels is a chance to add any missing label info to nodes when the telemetry does not provide enough information. 178 // For example, service injection has this problem. 179 func addLabels(trafficMap graph.TrafficMap, globalInfo *graph.AppenderGlobalInfo, serviceLists map[string]*models.ServiceList) { 180 // build map for quick lookup 181 svcMap := map[graph.ClusterSensitiveKey]models.ServiceOverview{} 182 for cluster, serviceList := range serviceLists { 183 for _, sd := range serviceList.Services { 184 svcMap[graph.GetClusterSensitiveKey(cluster, sd.Name)] = sd 185 } 186 } 187 188 appLabelName := config.Get().IstioLabels.AppLabelName 189 for _, n := range trafficMap { 190 if serviceList, ok := serviceLists[n.Cluster]; ok { 191 // make sure service nodes have the defined app label so it can be used for app grouping in the UI. 192 if n.NodeType == graph.NodeTypeService && n.Namespace == serviceList.Namespace && n.App == "" { 193 // For service nodes that are a service entries, use the `hosts` property of the SE to find 194 // a matching Kubernetes Svc for adding missing labels 195 if _, ok := n.Metadata[graph.IsServiceEntry]; ok { 196 seInfo := n.Metadata[graph.IsServiceEntry].(*graph.SEInfo) 197 for _, host := range seInfo.Hosts { 198 var hostToTest string 199 200 hostSplitted := strings.Split(host, ".") 201 if len(hostSplitted) == 3 && hostSplitted[2] == config.IstioMultiClusterHostSuffix { 202 hostToTest = host 203 } else { 204 hostToTest = hostSplitted[0] 205 } 206 207 if svc, found := svcMap[graph.GetClusterSensitiveKey(n.Cluster, hostToTest)]; found { 208 if app, ok := svc.Labels[appLabelName]; ok { 209 n.App = app 210 } 211 continue 212 } 213 } 214 continue 215 } 216 // A service node that is an Istio egress cluster will not have a service definition 217 if _, ok := n.Metadata[graph.IsEgressCluster]; ok { 218 continue 219 } 220 221 if svc, found := svcMap[graph.GetClusterSensitiveKey(n.Cluster, n.Service)]; !found { 222 log.Debugf("Service not found, may not apply app label correctly for [%s:%s]", n.Namespace, n.Service) 223 continue 224 } else if app, ok := svc.Labels[appLabelName]; ok { 225 n.App = app 226 } 227 } 228 } 229 } 230 } 231 232 func decorateMatchingGateways(cluster string, gwCrd *networking_v1beta1.Gateway, gatewayNodeMapping map[*models.WorkloadListItem][]*graph.Node, nodeMetadataKey graph.MetadataKey) { 233 gwSelector := labels.Set(gwCrd.Spec.Selector).AsSelector() 234 for gw, nodes := range gatewayNodeMapping { 235 if gw.Cluster != cluster { 236 continue 237 } 238 239 if gwSelector.Matches(labels.Set(gw.Labels)) { 240 // If we are here, the GatewayCrd selects the Gateway workload. 241 // So, all node graphs associated with the GW workload should be listening 242 // requests for the hostnames listed in the GatewayCRD. 243 244 // Let's extract the hostnames and add them to the node metadata. 245 for _, node := range nodes { 246 gwServers := gwCrd.Spec.Servers 247 var hostnames []string 248 249 for _, gwServer := range gwServers { 250 gwHosts := gwServer.Hosts 251 hostnames = append(hostnames, gwHosts...) 252 } 253 254 // Metadata format: { gatewayName => array of hostnames } 255 node.Metadata[nodeMetadataKey].(graph.GatewaysMetadata)[gwCrd.Name] = hostnames 256 } 257 } 258 } 259 } 260 261 func decorateMatchingAPIGateways(cluster string, gwCrd *k8s_networking_v1.Gateway, gatewayNodeMapping map[*models.WorkloadListItem][]*graph.Node, nodeMetadataKey graph.MetadataKey) { 262 gwSelector := labels.Set(gwCrd.Labels).AsSelector() 263 for gw, nodes := range gatewayNodeMapping { 264 if gw.Cluster != cluster { 265 continue 266 } 267 268 if gwSelector.Matches(labels.Set(gw.Labels)) { 269 // If we are here, the GatewayCrd selects the GatewayAPI workload. 270 // So, all node graphs associated with the GW API workload should be listening 271 // requests for the hostnames listed in the GatewayAPI CRD. 272 273 // Let's extract the hostnames and add them to the node metadata. 274 for _, node := range nodes { 275 gwListeners := gwCrd.Spec.Listeners 276 var hostnames []string 277 278 for _, gwListener := range gwListeners { 279 if gwListener.Hostname != nil { 280 hostnames = append(hostnames, string(*gwListener.Hostname)) 281 } 282 } 283 284 // Metadata format: { gatewayName => array of hostnames } 285 node.Metadata[nodeMetadataKey].(graph.GatewaysMetadata)[gwCrd.Name] = hostnames 286 } 287 } 288 } 289 } 290 291 func resolveGatewayNodeMapping(gatewayWorkloads map[string][]models.WorkloadListItem, nodeMetadataKey graph.MetadataKey, trafficMap graph.TrafficMap) map[*models.WorkloadListItem][]*graph.Node { 292 istioAppLabelName := config.Get().IstioLabels.AppLabelName 293 294 gatewayNodeMapping := make(map[*models.WorkloadListItem][]*graph.Node) 295 for key, gwWorkloadsList := range gatewayWorkloads { 296 split := strings.Split(key, ":") 297 gwCluster := split[0] 298 gwNs := split[1] 299 for _, gw := range gwWorkloadsList { 300 for _, node := range trafficMap { 301 if _, ok := node.Metadata[nodeMetadataKey]; !ok { 302 if (node.NodeType == graph.NodeTypeApp || node.NodeType == graph.NodeTypeWorkload) && node.App == gw.Labels[istioAppLabelName] && node.Cluster == gwCluster && node.Namespace == gwNs { 303 node.Metadata[nodeMetadataKey] = graph.GatewaysMetadata{} 304 gatewayNodeMapping[&gw] = append(gatewayNodeMapping[&gw], node) 305 } 306 } 307 } 308 } 309 } 310 311 return gatewayNodeMapping 312 } 313 314 func (a IstioAppender) decorateGateways(trafficMap graph.TrafficMap, globalInfo *graph.AppenderGlobalInfo, namespaceInfo *graph.AppenderNamespaceInfo) { 315 // Get ingress-gateways deployments in the namespace. Then, find if the graph is showing any of them. If so, flag the GW nodes. 316 ingressWorkloads := a.getIngressGatewayWorkloads(globalInfo) 317 ingressNodeMapping := resolveGatewayNodeMapping(ingressWorkloads, graph.IsIngressGateway, trafficMap) 318 319 // Get egress-gateways deployments in the namespace. (Same logic as in the previous chunk of code) 320 egressWorkloads := a.getEgressGatewayWorkloads(globalInfo) 321 egressNodeMapping := resolveGatewayNodeMapping(egressWorkloads, graph.IsEgressGateway, trafficMap) 322 323 // Get Gateway API workloads (ingress) 324 gatewayAPIWorkloads := a.getGatewayAPIWorkloads(globalInfo) 325 gatewayAPINodeMapping := resolveGatewayNodeMapping(gatewayAPIWorkloads, graph.IsGatewayAPI, trafficMap) 326 327 // If there is any ingress or egress gateway node in the processing namespace, find Gateway CRDs and 328 // match them against gateways in the graph. 329 if len(ingressNodeMapping) != 0 || len(egressNodeMapping) != 0 { 330 gatewaysCrds := a.getIstioGatewayResources(globalInfo) 331 332 for accessibleNamespaceKey, gwCrds := range gatewaysCrds { 333 cluster := strings.Split(accessibleNamespaceKey, ":")[0] 334 for _, gwCrd := range gwCrds { 335 decorateMatchingGateways(cluster, gwCrd, ingressNodeMapping, graph.IsIngressGateway) 336 decorateMatchingGateways(cluster, gwCrd, egressNodeMapping, graph.IsEgressGateway) 337 } 338 } 339 } 340 // If there is any GatewayAPI node in the processing namespace, find GatewayAPI CRDs and 341 // match them against gateways in the graph. 342 if len(gatewayAPINodeMapping) != 0 { 343 gatewaysCrds := a.getGatewayAPIResources(globalInfo) 344 345 for accessibleNamespaceKey, gwCrds := range gatewaysCrds { 346 cluster := strings.Split(accessibleNamespaceKey, ":")[0] 347 for _, gwCrd := range gwCrds { 348 decorateMatchingAPIGateways(cluster, gwCrd, gatewayAPINodeMapping, graph.IsGatewayAPI) 349 } 350 } 351 } 352 } 353 354 func (a IstioAppender) getEgressGatewayWorkloads(globalInfo *graph.AppenderGlobalInfo) map[string][]models.WorkloadListItem { 355 return a.getIstioComponentWorkloads("EgressGateways", globalInfo) 356 } 357 358 func (a IstioAppender) getIngressGatewayWorkloads(globalInfo *graph.AppenderGlobalInfo) map[string][]models.WorkloadListItem { 359 return a.getIstioComponentWorkloads("IngressGateways", globalInfo) 360 } 361 362 func (a IstioAppender) getIstioComponentWorkloads(component string, globalInfo *graph.AppenderGlobalInfo) map[string][]models.WorkloadListItem { 363 componentWorkloads := make(map[string][]models.WorkloadListItem) 364 for key, an := range a.AccessibleNamespaces { 365 criteria := business.WorkloadCriteria{Cluster: an.Cluster, Namespace: an.Name, IncludeIstioResources: false, IncludeHealth: false} 366 wList, err := globalInfo.Business.Workload.GetWorkloadList(context.TODO(), criteria) 367 graph.CheckError(err) 368 369 // Find Istio component deployments 370 for _, workload := range wList.Workloads { 371 if workload.Type == "Deployment" { 372 if labelValue, ok := workload.Labels["operator.istio.io/component"]; ok && labelValue == component { 373 componentWorkloads[key] = append(componentWorkloads[key], workload) 374 } 375 } 376 } 377 } 378 379 return componentWorkloads 380 } 381 382 func (a IstioAppender) getGatewayAPIWorkloads(globalInfo *graph.AppenderGlobalInfo) map[string][]models.WorkloadListItem { 383 managedWorkloads := make(map[string][]models.WorkloadListItem) 384 for key, an := range a.AccessibleNamespaces { 385 criteria := business.WorkloadCriteria{Cluster: an.Cluster, Namespace: an.Name, IncludeIstioResources: false, IncludeHealth: false} 386 wList, err := globalInfo.Business.Workload.GetWorkloadList(context.TODO(), criteria) 387 graph.CheckError(err) 388 389 // Find Istio managed Gateway API deployments 390 for _, workload := range wList.Workloads { 391 if workload.Type == "Deployment" { 392 if _, ok := workload.Labels["istio.io/gateway-name"]; ok { 393 managedWorkloads[key] = append(managedWorkloads[key], workload) 394 } 395 } 396 } 397 } 398 399 return managedWorkloads 400 } 401 402 func (a IstioAppender) getIstioGatewayResources(globalInfo *graph.AppenderGlobalInfo) map[string][]*networking_v1beta1.Gateway { 403 retVal := map[string][]*networking_v1beta1.Gateway{} 404 for key, an := range a.AccessibleNamespaces { 405 istioCfg, err := globalInfo.Business.IstioConfig.GetIstioConfigListForNamespace(context.TODO(), an.Cluster, an.Name, business.IstioConfigCriteria{ 406 IncludeGateways: true, 407 }) 408 graph.CheckError(err) 409 410 retVal[key] = append(retVal[key], istioCfg.Gateways...) 411 } 412 413 return retVal 414 } 415 416 func (a IstioAppender) getGatewayAPIResources(globalInfo *graph.AppenderGlobalInfo) map[string][]*k8s_networking_v1.Gateway { 417 retVal := map[string][]*k8s_networking_v1.Gateway{} 418 for key, an := range a.AccessibleNamespaces { 419 istioCfg, err := globalInfo.Business.IstioConfig.GetIstioConfigListForNamespace(context.TODO(), an.Cluster, an.Name, business.IstioConfigCriteria{ 420 IncludeK8sGateways: true, 421 }) 422 graph.CheckError(err) 423 424 retVal[key] = append(retVal[key], istioCfg.K8sGateways...) 425 } 426 427 return retVal 428 }