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  }