github.com/kiali/kiali@v1.84.0/graph/telemetry/istio/appender/appender.go (about)

     1  package appender
     2  
     3  import (
     4  	"context"
     5  	"fmt"
     6  	"strconv"
     7  
     8  	"github.com/kiali/kiali/business"
     9  	"github.com/kiali/kiali/config"
    10  	"github.com/kiali/kiali/graph"
    11  	"github.com/kiali/kiali/models"
    12  )
    13  
    14  const (
    15  	defaultAggregate      = "request_operation"
    16  	defaultQuantile       = 0.95
    17  	defaultThroughputType = "response"
    18  	defaultWaypoints      = true
    19  )
    20  
    21  // ParseAppenders determines which appenders should run for this graphing request
    22  func ParseAppenders(o graph.TelemetryOptions) (appenders []graph.Appender, finalizers []graph.Appender) {
    23  	requestedAppenders := map[string]bool{}
    24  	requestedFinalizers := map[string]bool{}
    25  
    26  	if !o.Appenders.All {
    27  		for _, appenderName := range o.Appenders.AppenderNames {
    28  			switch appenderName {
    29  
    30  			// namespace appenders
    31  			case AggregateNodeAppenderName:
    32  				requestedAppenders[AggregateNodeAppenderName] = true
    33  			case DeadNodeAppenderName:
    34  				requestedAppenders[DeadNodeAppenderName] = true
    35  			case IdleNodeAppenderName:
    36  				requestedAppenders[IdleNodeAppenderName] = true
    37  			case IstioAppenderName:
    38  				requestedAppenders[IstioAppenderName] = true
    39  			case MeshCheckAppenderName, SidecarsCheckAppenderName:
    40  				requestedAppenders[MeshCheckAppenderName] = true
    41  			case ResponseTimeAppenderName:
    42  				requestedAppenders[ResponseTimeAppenderName] = true
    43  			case SecurityPolicyAppenderName:
    44  				requestedAppenders[SecurityPolicyAppenderName] = true
    45  			case ServiceEntryAppenderName:
    46  				requestedAppenders[ServiceEntryAppenderName] = true
    47  			case ThroughputAppenderName:
    48  				requestedAppenders[ThroughputAppenderName] = true
    49  			case WorkloadEntryAppenderName:
    50  				requestedAppenders[WorkloadEntryAppenderName] = true
    51  
    52  			// finalizer appenders
    53  			case AmbientAppenderName:
    54  				requestedAppenders[AmbientAppenderName] = true
    55  			case HealthAppenderName:
    56  				// currently, because health is still calculated in the client, if requesting health
    57  				// we also need to run the healthConfig appender.  Eventually, asking for health will supply
    58  				// the result of a server-side health calculation.
    59  				requestedAppenders[HealthAppenderName] = true
    60  				requestedFinalizers[HealthAppenderName] = true
    61  			case LabelerAppenderName:
    62  				requestedFinalizers[LabelerAppenderName] = true
    63  			case OutsiderAppenderName, TrafficGeneratorAppenderName:
    64  				// skip - these are always run, ignore if specified
    65  			case "":
    66  				// skip
    67  			default:
    68  				graph.BadRequest(fmt.Sprintf("Invalid appender [%s]", appenderName))
    69  			}
    70  		}
    71  	}
    72  
    73  	// The appender order is important
    74  	// To pre-process service nodes run service_entry appender first
    75  	// To reduce processing, filter dead nodes next
    76  	// To reduce processing, next run appenders that don't apply to idle (aka unused) services
    77  	// - lazily inject aggregate nodes so other decorations can influence the new nodes/edges, if necessary
    78  	// Add orphan (idle) services
    79  	// Run remaining appenders
    80  	if _, ok := requestedAppenders[ServiceEntryAppenderName]; ok || o.Appenders.All {
    81  		a := ServiceEntryAppender{
    82  			AccessibleNamespaces: o.AccessibleNamespaces,
    83  			GraphType:            o.GraphType,
    84  		}
    85  		appenders = append(appenders, a)
    86  	}
    87  	if _, ok := requestedAppenders[DeadNodeAppenderName]; ok || o.Appenders.All {
    88  		a := DeadNodeAppender{}
    89  		appenders = append(appenders, a)
    90  	}
    91  	if _, ok := requestedAppenders[WorkloadEntryAppenderName]; ok || o.Appenders.All {
    92  		a := WorkloadEntryAppender{
    93  			GraphType: o.GraphType,
    94  		}
    95  		appenders = append(appenders, a)
    96  	}
    97  	if _, ok := requestedAppenders[ResponseTimeAppenderName]; ok || o.Appenders.All {
    98  		quantile := defaultQuantile
    99  		responseTimeString := o.Params.Get("responseTime")
   100  		if responseTimeString != "" {
   101  			switch responseTimeString {
   102  			case "avg":
   103  				quantile = 0.0
   104  			case "50":
   105  				quantile = 0.5
   106  			case "95":
   107  				quantile = 0.95
   108  			case "99":
   109  				quantile = 0.99
   110  			default:
   111  				graph.BadRequest(fmt.Sprintf(`Invalid responseTime, must be one of: avg | 50 | 95 | 99: [%s]`, responseTimeString))
   112  			}
   113  		}
   114  		a := ResponseTimeAppender{
   115  			Quantile:           quantile,
   116  			GraphType:          o.GraphType,
   117  			InjectServiceNodes: o.InjectServiceNodes,
   118  			Namespaces:         o.Namespaces,
   119  			QueryTime:          o.QueryTime,
   120  			Rates:              o.Rates,
   121  		}
   122  		appenders = append(appenders, a)
   123  	}
   124  	if _, ok := requestedAppenders[SecurityPolicyAppenderName]; ok || o.Appenders.All {
   125  		a := SecurityPolicyAppender{
   126  			GraphType:          o.GraphType,
   127  			InjectServiceNodes: o.InjectServiceNodes,
   128  			Namespaces:         o.Namespaces,
   129  			QueryTime:          o.QueryTime,
   130  			Rates:              o.Rates,
   131  		}
   132  		appenders = append(appenders, a)
   133  	}
   134  	if _, ok := requestedAppenders[ThroughputAppenderName]; ok || o.Appenders.All {
   135  		throughputType := o.Params.Get("throughputType")
   136  		if throughputType != "" {
   137  			if throughputType != "request" && throughputType != "response" {
   138  				graph.BadRequest(fmt.Sprintf("Invalid throughputType, expecting one of (request, response). [%s]", throughputType))
   139  			}
   140  		} else {
   141  			throughputType = defaultThroughputType
   142  		}
   143  		a := ThroughputAppender{
   144  			GraphType:          o.GraphType,
   145  			InjectServiceNodes: o.InjectServiceNodes,
   146  			Namespaces:         o.Namespaces,
   147  			QueryTime:          o.QueryTime,
   148  			Rates:              o.Rates,
   149  			ThroughputType:     throughputType,
   150  		}
   151  		appenders = append(appenders, a)
   152  	}
   153  	if _, ok := requestedAppenders[AggregateNodeAppenderName]; ok || o.Appenders.All {
   154  		aggregate := o.NodeOptions.Aggregate
   155  		if aggregate == "" {
   156  			if aggregate = o.Params.Get("aggregate"); aggregate == "" {
   157  				aggregate = defaultAggregate
   158  			}
   159  		}
   160  		a := AggregateNodeAppender{
   161  			Aggregate:          aggregate,
   162  			AggregateValue:     o.NodeOptions.AggregateValue,
   163  			GraphType:          o.GraphType,
   164  			InjectServiceNodes: o.InjectServiceNodes,
   165  			Namespaces:         o.Namespaces,
   166  			QueryTime:          o.QueryTime,
   167  			Rates:              o.Rates,
   168  			Service:            o.NodeOptions.Service,
   169  		}
   170  		appenders = append(appenders, a)
   171  	}
   172  	if _, ok := requestedAppenders[IdleNodeAppenderName]; ok || o.Appenders.All {
   173  		hasNodeOptions := o.App != "" || o.Workload != "" || o.Service != ""
   174  		a := IdleNodeAppender{
   175  			GraphType:          o.GraphType,
   176  			InjectServiceNodes: o.InjectServiceNodes,
   177  			IsNodeGraph:        hasNodeOptions,
   178  		}
   179  		appenders = append(appenders, a)
   180  	}
   181  	if _, ok := requestedAppenders[IstioAppenderName]; ok || o.Appenders.All {
   182  		a := IstioAppender{
   183  			AccessibleNamespaces: o.AccessibleNamespaces,
   184  		}
   185  		appenders = append(appenders, a)
   186  	}
   187  	if _, ok := requestedAppenders[MeshCheckAppenderName]; ok || o.Appenders.All {
   188  		a := MeshCheckAppender{
   189  			AccessibleNamespaces: o.AccessibleNamespaces,
   190  		}
   191  		appenders = append(appenders, a)
   192  	}
   193  
   194  	// The finalizer order is important
   195  	// always run the outsider finalizer
   196  	finalizers = append(finalizers, &OutsiderAppender{
   197  		AccessibleNamespaces: o.AccessibleNamespaces,
   198  		Namespaces:           o.Namespaces,
   199  	})
   200  
   201  	if _, ok := requestedAppenders[AmbientAppenderName]; ok || o.Appenders.All {
   202  		waypoints := defaultWaypoints
   203  		waypointsString := o.Params.Get("waypoints")
   204  		if waypointsString != "" {
   205  			var waypointsErr error
   206  			waypoints, waypointsErr = strconv.ParseBool(waypointsString)
   207  			if waypointsErr != nil {
   208  				graph.BadRequest(fmt.Sprintf("Invalid waypoints param [%s]", waypointsString))
   209  			}
   210  		}
   211  		a := AmbientAppender{
   212  			Waypoints: waypoints,
   213  		}
   214  		appenders = append(appenders, a)
   215  	}
   216  
   217  	// if health finalizer is to be run, do it after the outsider finalizer
   218  	if _, ok := requestedFinalizers[HealthAppenderName]; ok {
   219  		finalizers = append(finalizers, &HealthAppender{
   220  			Namespaces:        o.Namespaces,
   221  			QueryTime:         o.QueryTime,
   222  			RequestedDuration: o.Duration,
   223  		})
   224  	}
   225  
   226  	// if labeler finalizer is to be run, do it after the outsider finalizer
   227  	if _, ok := requestedFinalizers[LabelerAppenderName]; ok {
   228  		finalizers = append(finalizers, &LabelerAppender{})
   229  	}
   230  
   231  	// always run the traffic generator finalizer
   232  	finalizers = append(finalizers, &TrafficGeneratorAppender{})
   233  
   234  	return appenders, finalizers
   235  }
   236  
   237  const (
   238  	appsMapKey           = "appsMapKey"           // global vendor info map[cluster:namespace]appsMap
   239  	serviceEntryHostsKey = "serviceEntryHostsKey" // global vendor info service entries for all accessible namespaces
   240  	serviceListKey       = "serviceListKey"       // global vendor info map[cluster:namespace]serviceDefinitionList
   241  	workloadListKey      = "workloadListKey"      // global vendor info map[cluster:namespace]workloadListKey
   242  )
   243  
   244  type serviceEntry struct {
   245  	cluster   string
   246  	exportTo  []string
   247  	hosts     []string
   248  	location  string
   249  	name      string // serviceEntry name
   250  	namespace string // namespace in which the service entry is defined
   251  }
   252  
   253  type serviceEntryHosts map[string][]*serviceEntry
   254  
   255  type appsMap map[string]*models.AppListItem
   256  
   257  func newServiceEntryHosts() serviceEntryHosts {
   258  	return make(map[string][]*serviceEntry)
   259  }
   260  
   261  func (seh serviceEntryHosts) addHost(host string, se *serviceEntry) {
   262  	seArr := []*serviceEntry{se}
   263  	if serviceEntriesForHost, ok := seh[host]; ok {
   264  		// if the same host is defined in multiple service entries, prefer the most
   265  		// specific namespace match when checking for a match...
   266  		if se.exportTo == nil || se.exportTo[0] == "*" {
   267  			seh[host] = append(serviceEntriesForHost, seArr...)
   268  		} else {
   269  			seh[host] = append(seArr, serviceEntriesForHost...)
   270  		}
   271  	} else {
   272  		seh[host] = seArr
   273  	}
   274  	se.hosts = append(se.hosts, host)
   275  }
   276  
   277  // getServiceLists returns a map[clusterName]*models.ServiceList for all clusters with traffic in the namespace, or if trafficMap is nil
   278  // then all clusters on which the namespace is valid.
   279  func getServiceLists(trafficMap graph.TrafficMap, namespace string, gi *graph.AppenderGlobalInfo) map[string]*models.ServiceList {
   280  	clusters := getTrafficClusters(trafficMap, namespace, gi)
   281  	serviceLists := map[string]*models.ServiceList{}
   282  
   283  	for _, cluster := range clusters {
   284  		serviceLists[cluster] = getServiceList(cluster, namespace, gi)
   285  	}
   286  
   287  	return serviceLists
   288  }
   289  
   290  func getServiceList(cluster, namespace string, gi *graph.AppenderGlobalInfo) *models.ServiceList {
   291  	var serviceListMap map[string]*models.ServiceList
   292  	if existingServiceMap, ok := gi.Vendor[serviceListKey]; ok {
   293  		serviceListMap = existingServiceMap.(map[string]*models.ServiceList)
   294  	} else {
   295  		serviceListMap = make(map[string]*models.ServiceList)
   296  		gi.Vendor[serviceListKey] = serviceListMap
   297  	}
   298  
   299  	key := graph.GetClusterSensitiveKey(cluster, namespace)
   300  	if serviceList, ok := serviceListMap[key]; ok {
   301  		return serviceList
   302  	}
   303  
   304  	criteria := business.ServiceCriteria{
   305  		Cluster:                cluster,
   306  		Namespace:              namespace,
   307  		IncludeHealth:          false,
   308  		IncludeOnlyDefinitions: true,
   309  	}
   310  	serviceList, err := gi.Business.Svc.GetServiceList(context.TODO(), criteria)
   311  	graph.CheckError(err)
   312  	serviceListMap[key] = serviceList
   313  
   314  	return serviceList
   315  }
   316  
   317  func getServiceDefinition(cluster, namespace, serviceName string, gi *graph.AppenderGlobalInfo) (*models.ServiceOverview, bool) {
   318  	if serviceName == "" || serviceName == graph.Unknown {
   319  		return nil, false
   320  	}
   321  	for _, srv := range getServiceList(cluster, namespace, gi).Services {
   322  		if srv.Name == serviceName {
   323  			return &srv, true
   324  		}
   325  	}
   326  	return nil, false
   327  }
   328  
   329  // getServiceEntryHosts returns ServiceEntryHost information cached for a specific cluster and namespace. If not
   330  // previously cached a new, empty cache entry is created and returned.
   331  func getServiceEntryHosts(cluster, namespace string, gi *graph.AppenderGlobalInfo) (serviceEntryHosts, bool) {
   332  	key := fmt.Sprintf("%s:%s:%s", serviceEntryHostsKey, cluster, namespace)
   333  	if seHosts, ok := gi.Vendor[key]; ok {
   334  		return seHosts.(serviceEntryHosts), true
   335  	}
   336  
   337  	seHosts := newServiceEntryHosts()
   338  	gi.Vendor[key] = seHosts
   339  
   340  	return seHosts, false
   341  }
   342  
   343  // getWorkloadLists returns a map[clusterName]*models.WorkloadList for all clusters with traffic in the namespace, or if trafficMap is nil
   344  // then all clusters on which the namespace is valid.
   345  func getWorkloadLists(trafficMap graph.TrafficMap, namespace string, gi *graph.AppenderGlobalInfo) map[string]*models.WorkloadList {
   346  	clusters := getTrafficClusters(trafficMap, namespace, gi)
   347  	workloadLists := map[string]*models.WorkloadList{}
   348  
   349  	for _, cluster := range clusters {
   350  		workloadLists[cluster] = getWorkloadList(cluster, namespace, gi)
   351  	}
   352  
   353  	return workloadLists
   354  }
   355  
   356  func getWorkloadList(cluster, namespace string, gi *graph.AppenderGlobalInfo) *models.WorkloadList {
   357  	var workloadListMap map[string]*models.WorkloadList
   358  	if existingWorkloadListMap, ok := gi.Vendor[workloadListKey]; ok {
   359  		workloadListMap = existingWorkloadListMap.(map[string]*models.WorkloadList)
   360  	} else {
   361  		workloadListMap = make(map[string]*models.WorkloadList)
   362  		gi.Vendor[workloadListKey] = workloadListMap
   363  	}
   364  
   365  	key := graph.GetClusterSensitiveKey(cluster, namespace)
   366  	if workloadList, ok := workloadListMap[key]; ok {
   367  		return workloadList
   368  	}
   369  
   370  	criteria := business.WorkloadCriteria{Cluster: cluster, Namespace: namespace, IncludeIstioResources: false, IncludeHealth: false}
   371  	workloadList, err := gi.Business.Workload.GetWorkloadList(context.TODO(), criteria)
   372  	graph.CheckError(err)
   373  	workloadListMap[key] = &workloadList
   374  
   375  	return &workloadList
   376  }
   377  
   378  func getWorkload(cluster, namespace, workloadName string, gi *graph.AppenderGlobalInfo) (*models.WorkloadListItem, bool) {
   379  	if workloadName == "" || workloadName == graph.Unknown {
   380  		return nil, false
   381  	}
   382  
   383  	workloadList := getWorkloadList(cluster, namespace, gi)
   384  
   385  	for _, workload := range workloadList.Workloads {
   386  		if workload.Name == workloadName {
   387  			return &workload, true
   388  		}
   389  	}
   390  	return nil, false
   391  }
   392  
   393  func getAppWorkloads(cluster, namespace, app, version string, gi *graph.AppenderGlobalInfo) []models.WorkloadListItem {
   394  	cfg := config.Get()
   395  	appLabel := cfg.IstioLabels.AppLabelName
   396  	versionLabel := cfg.IstioLabels.VersionLabelName
   397  
   398  	result := []models.WorkloadListItem{}
   399  	versionOk := graph.IsOKVersion(version)
   400  	for _, workload := range getWorkloadList(cluster, namespace, gi).Workloads {
   401  		if appVal, ok := workload.Labels[appLabel]; ok && app == appVal {
   402  			if !versionOk {
   403  				result = append(result, workload)
   404  			} else if versionVal, ok := workload.Labels[versionLabel]; ok && version == versionVal {
   405  				result = append(result, workload)
   406  			}
   407  		}
   408  	}
   409  	return result
   410  }
   411  
   412  func getApp(namespace, appName string, gi *graph.AppenderGlobalInfo) (*models.AppListItem, bool) {
   413  	if appName == "" || appName == graph.Unknown {
   414  		return nil, false
   415  	}
   416  
   417  	var allAppsMap map[string]appsMap
   418  	if existingAllAppsMap, ok := gi.Vendor[appsMapKey]; ok {
   419  		allAppsMap = existingAllAppsMap.(map[string]appsMap)
   420  	} else {
   421  		allAppsMap = make(map[string]appsMap)
   422  		gi.Vendor[appsMapKey] = allAppsMap
   423  	}
   424  
   425  	var namespaceApps appsMap
   426  	if existingNamespaceApps, ok := allAppsMap[namespace]; ok {
   427  		if app, ok := existingNamespaceApps[appName]; ok {
   428  			return app, true
   429  		} else {
   430  			namespaceApps = existingNamespaceApps
   431  		}
   432  	} else {
   433  		namespaceApps = appsMap{}
   434  		allAppsMap[namespace] = namespaceApps
   435  	}
   436  
   437  	if appList, err := gi.Business.App.GetAppList(context.TODO(), business.AppCriteria{Namespace: namespace, IncludeIstioResources: false, IncludeHealth: false}); err == nil {
   438  		for _, app := range appList.Apps {
   439  			if app.Name == appName {
   440  				namespaceApps[appName] = &app
   441  				return &app, true
   442  			}
   443  		}
   444  	}
   445  
   446  	return nil, false
   447  }
   448  
   449  // getTrafficClusters returns an array of clusters for which the TrafficMap has accessible nodes for the given
   450  // namespace, or of trafficMap is nil then clusters for which the namespace exists.
   451  func getTrafficClusters(trafficMap graph.TrafficMap, namespace string, gi *graph.AppenderGlobalInfo) []string {
   452  	if trafficMap == nil {
   453  		namespaceClusters, err := gi.Business.Namespace.GetNamespaceClusters(context.TODO(), namespace)
   454  		graph.CheckError(err)
   455  
   456  		clusters := make([]string, len(namespaceClusters))
   457  		for i, nc := range namespaceClusters {
   458  			clusters[i] = nc.Cluster
   459  		}
   460  		return clusters
   461  	}
   462  
   463  	clusterMap := map[string]bool{}
   464  
   465  	for _, n := range trafficMap {
   466  		if b, ok := n.Metadata[graph.IsInaccessible]; ok && b.(bool) {
   467  			continue
   468  		}
   469  		if n.Namespace == namespace {
   470  			clusterMap[n.Cluster] = true
   471  		}
   472  	}
   473  
   474  	// TODO change to maps.Keys(clusterMap) with Go 1.21
   475  	clusters := make([]string, len(clusterMap))
   476  	i := 0
   477  	for k := range clusterMap {
   478  		clusters[i] = k
   479  		i++
   480  	}
   481  	return clusters
   482  }