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 }