github.com/kiali/kiali@v1.84.0/business/apps.go (about) 1 package business 2 3 import ( 4 "context" 5 "fmt" 6 "sort" 7 "strings" 8 "sync" 9 "time" 10 11 "k8s.io/apimachinery/pkg/labels" 12 13 "github.com/kiali/kiali/config" 14 "github.com/kiali/kiali/kubernetes" 15 "github.com/kiali/kiali/log" 16 "github.com/kiali/kiali/models" 17 "github.com/kiali/kiali/observability" 18 "github.com/kiali/kiali/prometheus" 19 ) 20 21 // AppService deals with fetching Workloads group by "app" label, which will be identified as an "application" 22 type AppService struct { 23 prom prometheus.ClientInterface 24 userClients map[string]kubernetes.ClientInterface 25 businessLayer *Layer 26 } 27 28 type AppCriteria struct { 29 Namespace string 30 Cluster string 31 AppName string 32 IncludeIstioResources bool 33 IncludeHealth bool 34 RateInterval string 35 QueryTime time.Time 36 } 37 38 func joinMap(m1 map[string][]string, m2 map[string]string) { 39 for k, v2 := range m2 { 40 dup := false 41 for _, v1 := range m1[k] { 42 if v1 == v2 { 43 dup = true 44 break 45 } 46 } 47 if !dup { 48 m1[k] = append(m1[k], v2) 49 } 50 } 51 } 52 53 func buildFinalLabels(m map[string][]string) map[string]string { 54 consolidated := make(map[string]string, len(m)) 55 for k, list := range m { 56 sort.Strings(list) 57 consolidated[k] = strings.Join(list, ",") 58 } 59 return consolidated 60 } 61 62 // GetClusterAppList is the API handler to fetch the list of applications in a given namespace and cluster 63 func (in *AppService) GetClusterAppList(ctx context.Context, criteria AppCriteria) (models.ClusterApps, error) { 64 var end observability.EndFunc 65 ctx, end = observability.StartSpan(ctx, "GetClusterAppList", 66 observability.Attribute("package", "business"), 67 observability.Attribute("namespace", criteria.Namespace), 68 observability.Attribute("cluster", criteria.Cluster), 69 observability.Attribute("includeHealth", criteria.IncludeHealth), 70 observability.Attribute("includeIstioResources", criteria.IncludeIstioResources), 71 observability.Attribute("rateInterval", criteria.RateInterval), 72 observability.Attribute("queryTime", criteria.QueryTime), 73 ) 74 defer end() 75 76 appList := &models.ClusterApps{ 77 Apps: []models.AppListItem{}, 78 } 79 80 namespace := criteria.Namespace 81 cluster := criteria.Cluster 82 83 if _, ok := in.userClients[cluster]; !ok { 84 return *appList, fmt.Errorf("Cluster [%s] is not found or is not accessible for Kiali", cluster) 85 } 86 87 if _, err := in.businessLayer.Namespace.GetClusterNamespace(ctx, namespace, cluster); err != nil { 88 return *appList, err 89 } 90 91 allApps, err := in.businessLayer.App.fetchNamespaceApps(ctx, namespace, cluster, "") 92 if err != nil { 93 log.Errorf("Error fetching Applications for cluster %s per namespace %s: %s", cluster, namespace, err) 94 return *appList, err 95 } 96 97 icCriteria := IstioConfigCriteria{ 98 IncludeAuthorizationPolicies: true, 99 IncludeDestinationRules: true, 100 IncludeEnvoyFilters: true, 101 IncludeGateways: true, 102 IncludePeerAuthentications: true, 103 IncludeRequestAuthentications: true, 104 IncludeSidecars: true, 105 IncludeVirtualServices: true, 106 } 107 istioConfigList := &models.IstioConfigList{} 108 109 if criteria.IncludeIstioResources { 110 istioConfigList, err = in.businessLayer.IstioConfig.GetIstioConfigListForNamespace(ctx, cluster, namespace, icCriteria) 111 if err != nil { 112 log.Errorf("Error fetching Istio Config for Cluster %s per namespace %s: %s", cluster, namespace, err) 113 return *appList, err 114 } 115 } 116 117 for keyApp, valueApp := range allApps { 118 appItem := &models.AppListItem{ 119 Name: keyApp, 120 IstioSidecar: true, 121 Health: models.EmptyAppHealth(), 122 } 123 applabels := make(map[string][]string) 124 svcReferences := make([]*models.IstioValidationKey, 0) 125 for _, srv := range valueApp.Services { 126 joinMap(applabels, srv.Labels) 127 if criteria.IncludeIstioResources { 128 vsFiltered := kubernetes.FilterVirtualServicesByService(istioConfigList.VirtualServices, srv.Namespace, srv.Name) 129 for _, v := range vsFiltered { 130 ref := models.BuildKey(v.Kind, v.Name, v.Namespace) 131 svcReferences = append(svcReferences, &ref) 132 } 133 drFiltered := kubernetes.FilterDestinationRulesByService(istioConfigList.DestinationRules, srv.Namespace, srv.Name) 134 for _, d := range drFiltered { 135 ref := models.BuildKey(d.Kind, d.Name, d.Namespace) 136 svcReferences = append(svcReferences, &ref) 137 } 138 gwFiltered := kubernetes.FilterGatewaysByVirtualServices(istioConfigList.Gateways, istioConfigList.VirtualServices) 139 for _, g := range gwFiltered { 140 ref := models.BuildKey(g.Kind, g.Name, g.Namespace) 141 svcReferences = append(svcReferences, &ref) 142 } 143 144 } 145 146 } 147 148 wkdReferences := make([]*models.IstioValidationKey, 0) 149 for _, wrk := range valueApp.Workloads { 150 joinMap(applabels, wrk.Labels) 151 if criteria.IncludeIstioResources { 152 wSelector := labels.Set(wrk.Labels).AsSelector().String() 153 wkdReferences = append(wkdReferences, FilterWorkloadReferences(wSelector, *istioConfigList)...) 154 } 155 } 156 appItem.Labels = buildFinalLabels(applabels) 157 appItem.IstioReferences = FilterUniqueIstioReferences(append(svcReferences, wkdReferences...)) 158 159 for _, w := range valueApp.Workloads { 160 if appItem.IstioSidecar = w.IstioSidecar; !appItem.IstioSidecar { 161 break 162 } 163 } 164 for _, w := range valueApp.Workloads { 165 if appItem.IstioAmbient = w.HasIstioAmbient(); !appItem.IstioAmbient { 166 break 167 } 168 } 169 if criteria.IncludeHealth { 170 appItem.Health, err = in.businessLayer.Health.GetAppHealth(ctx, criteria.Namespace, valueApp.cluster, appItem.Name, criteria.RateInterval, criteria.QueryTime, valueApp) 171 if err != nil { 172 log.Errorf("Error fetching Health in namespace %s for app %s: %s", criteria.Namespace, appItem.Name, err) 173 } 174 } 175 appItem.Cluster = cluster 176 appItem.Namespace = namespace 177 appList.Apps = append(appList.Apps, *appItem) 178 } 179 180 return *appList, nil 181 } 182 183 // GetAppList is the API handler to fetch the list of applications in a given namespace 184 func (in *AppService) GetAppList(ctx context.Context, criteria AppCriteria) (models.AppList, error) { 185 var end observability.EndFunc 186 ctx, end = observability.StartSpan(ctx, "GetAppList", 187 observability.Attribute("package", "business"), 188 observability.Attribute("namespace", criteria.Namespace), 189 observability.Attribute("cluster", criteria.Cluster), 190 observability.Attribute("includeHealth", criteria.IncludeHealth), 191 observability.Attribute("includeIstioResources", criteria.IncludeIstioResources), 192 observability.Attribute("rateInterval", criteria.RateInterval), 193 observability.Attribute("queryTime", criteria.QueryTime), 194 ) 195 defer end() 196 197 appList := &models.AppList{ 198 Namespace: models.Namespace{Name: criteria.Namespace}, 199 Cluster: criteria.Cluster, 200 Apps: []models.AppListItem{}, 201 } 202 203 var err error 204 var allApps []namespaceApps 205 206 wg := &sync.WaitGroup{} 207 type result struct { 208 cluster string 209 nsApps namespaceApps 210 err error 211 } 212 resultsCh := make(chan result) 213 214 // TODO: Use a context to define a timeout. The context should be passed to the k8s client 215 go func() { 216 for cluster := range in.userClients { 217 wg.Add(1) 218 go func(c string) { 219 defer wg.Done() 220 nsApps, error2 := in.fetchNamespaceApps(ctx, criteria.Namespace, c, "") 221 if error2 != nil { 222 resultsCh <- result{cluster: c, nsApps: nil, err: error2} 223 } else { 224 resultsCh <- result{cluster: c, nsApps: nsApps, err: nil} 225 } 226 }(cluster) 227 } 228 wg.Wait() 229 close(resultsCh) 230 }() 231 232 // Combine namespace data 233 conf := config.Get() 234 for resultCh := range resultsCh { 235 if resultCh.err != nil { 236 // Return failure if we are in single cluster 237 if resultCh.cluster == conf.KubernetesConfig.ClusterName && len(in.userClients) == 1 { 238 log.Errorf("Error fetching Applications for local cluster %s: %s", resultCh.cluster, resultCh.err) 239 return models.AppList{}, resultCh.err 240 } else { 241 log.Infof("Error fetching Applications for cluster %s: %s", resultCh.cluster, resultCh.err) 242 } 243 } 244 allApps = append(allApps, resultCh.nsApps) 245 } 246 247 icCriteria := IstioConfigCriteria{ 248 IncludeAuthorizationPolicies: true, 249 IncludeDestinationRules: true, 250 IncludeEnvoyFilters: true, 251 IncludeGateways: true, 252 IncludePeerAuthentications: true, 253 IncludeRequestAuthentications: true, 254 IncludeSidecars: true, 255 IncludeVirtualServices: true, 256 } 257 var istioConfigMap models.IstioConfigMap 258 259 // TODO: MC 260 if criteria.IncludeIstioResources { 261 istioConfigMap, err = in.businessLayer.IstioConfig.GetIstioConfigMap(ctx, criteria.Namespace, icCriteria) 262 if err != nil { 263 log.Errorf("Error fetching Istio Config per namespace %s: %s", criteria.Namespace, err) 264 return models.AppList{}, err 265 } 266 } 267 268 for _, clusterApps := range allApps { 269 for keyApp, valueApp := range clusterApps { 270 appItem := &models.AppListItem{ 271 Name: keyApp, 272 IstioSidecar: true, 273 Health: models.EmptyAppHealth(), 274 } 275 istioConfigList := models.IstioConfigList{} 276 if _, ok := istioConfigMap[valueApp.cluster]; ok { 277 istioConfigList = istioConfigMap[valueApp.cluster] 278 } 279 applabels := make(map[string][]string) 280 svcReferences := make([]*models.IstioValidationKey, 0) 281 for _, srv := range valueApp.Services { 282 joinMap(applabels, srv.Labels) 283 if criteria.IncludeIstioResources { 284 vsFiltered := kubernetes.FilterVirtualServicesByService(istioConfigList.VirtualServices, srv.Namespace, srv.Name) 285 for _, v := range vsFiltered { 286 ref := models.BuildKey(v.Kind, v.Name, v.Namespace) 287 svcReferences = append(svcReferences, &ref) 288 } 289 drFiltered := kubernetes.FilterDestinationRulesByService(istioConfigList.DestinationRules, srv.Namespace, srv.Name) 290 for _, d := range drFiltered { 291 ref := models.BuildKey(d.Kind, d.Name, d.Namespace) 292 svcReferences = append(svcReferences, &ref) 293 } 294 gwFiltered := kubernetes.FilterGatewaysByVirtualServices(istioConfigList.Gateways, istioConfigList.VirtualServices) 295 for _, g := range gwFiltered { 296 ref := models.BuildKey(g.Kind, g.Name, g.Namespace) 297 svcReferences = append(svcReferences, &ref) 298 } 299 300 } 301 302 } 303 304 wkdReferences := make([]*models.IstioValidationKey, 0) 305 for _, wrk := range valueApp.Workloads { 306 joinMap(applabels, wrk.Labels) 307 if criteria.IncludeIstioResources { 308 wSelector := labels.Set(wrk.Labels).AsSelector().String() 309 wkdReferences = append(wkdReferences, FilterWorkloadReferences(wSelector, istioConfigList)...) 310 } 311 } 312 appItem.Labels = buildFinalLabels(applabels) 313 appItem.IstioReferences = FilterUniqueIstioReferences(append(svcReferences, wkdReferences...)) 314 315 for _, w := range valueApp.Workloads { 316 if appItem.IstioSidecar = w.IstioSidecar; !appItem.IstioSidecar { 317 break 318 } 319 } 320 for _, w := range valueApp.Workloads { 321 if appItem.IstioAmbient = w.HasIstioAmbient(); !appItem.IstioAmbient { 322 break 323 } 324 } 325 if criteria.IncludeHealth { 326 appItem.Health, err = in.businessLayer.Health.GetAppHealth(ctx, criteria.Namespace, valueApp.cluster, appItem.Name, criteria.RateInterval, criteria.QueryTime, valueApp) 327 if err != nil { 328 log.Errorf("Error fetching Health in namespace %s for app %s: %s", criteria.Namespace, appItem.Name, err) 329 } 330 } 331 appItem.Cluster = valueApp.cluster 332 appItem.Namespace = criteria.Namespace 333 appList.Apps = append(appList.Apps, *appItem) 334 } 335 } 336 337 return *appList, nil 338 } 339 340 // GetApp is the API handler to fetch the details for a given namespace and app name 341 func (in *AppService) GetAppDetails(ctx context.Context, criteria AppCriteria) (models.App, error) { 342 var end observability.EndFunc 343 ctx, end = observability.StartSpan(ctx, "GetApp", 344 observability.Attribute("package", "business"), 345 observability.Attribute("namespace", criteria.Namespace), 346 observability.Attribute("cluster", criteria.Cluster), 347 observability.Attribute("appName", criteria.AppName), 348 observability.Attribute("rateInterval", criteria.RateInterval), 349 observability.Attribute("queryTime", criteria.QueryTime), 350 ) 351 defer end() 352 353 appInstance := &models.App{Namespace: models.Namespace{Name: criteria.Namespace}, Name: criteria.AppName, Health: models.EmptyAppHealth(), Cluster: criteria.Cluster} 354 ns, err := in.businessLayer.Namespace.GetClusterNamespace(ctx, criteria.Namespace, criteria.Cluster) 355 if err != nil { 356 return *appInstance, err 357 } 358 appInstance.Namespace = *ns 359 360 namespaceApps, err := in.fetchNamespaceApps(ctx, criteria.Namespace, criteria.Cluster, criteria.AppName) 361 if err != nil { 362 return *appInstance, err 363 } 364 365 var appDetails *appDetails 366 var ok bool 367 // Send a NewNotFound if the app is not found in the deployment list, instead to send an empty result 368 if appDetails, ok = namespaceApps[criteria.AppName]; !ok { 369 return *appInstance, kubernetes.NewNotFound(criteria.AppName, "Kiali", "App") 370 } 371 372 (*appInstance).Workloads = make([]models.WorkloadItem, len(appDetails.Workloads)) 373 for i, wkd := range appDetails.Workloads { 374 (*appInstance).Workloads[i] = models.WorkloadItem{WorkloadName: wkd.Name, IstioSidecar: wkd.IstioSidecar, Labels: wkd.Labels, IstioAmbient: wkd.IstioAmbient, ServiceAccountNames: wkd.Pods.ServiceAccounts()} 375 } 376 377 (*appInstance).ServiceNames = make([]string, len(appDetails.Services)) 378 for i, svc := range appDetails.Services { 379 (*appInstance).ServiceNames[i] = svc.Name 380 } 381 382 pods := models.Pods{} 383 for _, workload := range appDetails.Workloads { 384 pods = append(pods, workload.Pods...) 385 } 386 (*appInstance).Runtimes = NewDashboardsService(ns, nil).GetCustomDashboardRefs(criteria.Namespace, criteria.AppName, "", pods) 387 if criteria.IncludeHealth { 388 (*appInstance).Health, err = in.businessLayer.Health.GetAppHealth(ctx, criteria.Namespace, criteria.Cluster, criteria.AppName, criteria.RateInterval, criteria.QueryTime, appDetails) 389 if err != nil { 390 log.Errorf("Error fetching Health in namespace %s for app %s: %s", criteria.Namespace, criteria.AppName, err) 391 } 392 } 393 394 (*appInstance).Cluster = appDetails.cluster 395 396 return *appInstance, nil 397 } 398 399 // AppDetails holds Services and Workloads having the same "app" label 400 type appDetails struct { 401 app string 402 cluster string 403 Services []models.ServiceOverview 404 Workloads models.Workloads 405 } 406 407 // NamespaceApps is a map of app_name and cluster x AppDetails 408 type namespaceApps = map[string]*appDetails 409 410 func castAppDetails(allEntities namespaceApps, ss *models.ServiceList, w *models.Workload, cluster string) { 411 appLabel := config.Get().IstioLabels.AppLabelName 412 413 if app, ok := w.Labels[appLabel]; ok { 414 if appEntities, ok := allEntities[app]; ok { 415 appEntities.Workloads = append(appEntities.Workloads, w) 416 } else { 417 allEntities[app] = &appDetails{ 418 app: app, 419 cluster: cluster, 420 Workloads: models.Workloads{w}, 421 } 422 } 423 if ss != nil { 424 for _, service := range ss.Services { 425 if appEntities, ok := allEntities[app]; ok { 426 found := false 427 for _, s := range appEntities.Services { 428 if s.Name == service.Name && s.Namespace == service.Namespace { 429 found = true 430 } 431 } 432 if !found { 433 appEntities.Services = append(appEntities.Services, service) 434 } 435 } 436 } 437 } 438 } 439 } 440 441 // Helper method to fetch all applications for a given namespace. 442 // Optionally if appName parameter is provided, it filters apps for that name. 443 // Return an error on any problem. 444 func (in *AppService) fetchNamespaceApps(ctx context.Context, namespace string, cluster string, appName string) (namespaceApps, error) { 445 var ss *models.ServiceList 446 var ws models.Workloads 447 cfg := config.Get() 448 449 appNameSelector := "" 450 if appName != "" { 451 selector := labels.Set(map[string]string{cfg.IstioLabels.AppLabelName: appName}) 452 appNameSelector = selector.String() 453 } 454 455 // Check if user has access to the namespace (RBAC) in cache scenarios and/or 456 // if namespace is accessible from Kiali (Deployment.AccessibleNamespaces) 457 if _, err := in.businessLayer.Namespace.GetClusterNamespace(ctx, namespace, cluster); err != nil { 458 return nil, err 459 } 460 461 var err error 462 ws, err = in.businessLayer.Workload.fetchWorkloadsFromCluster(ctx, cluster, namespace, appNameSelector) 463 if err != nil { 464 return nil, err 465 } 466 allEntities := make(namespaceApps) 467 for _, w := range ws { 468 // Check if namespace is cached 469 serviceCriteria := ServiceCriteria{ 470 Cluster: cluster, 471 Namespace: namespace, 472 IncludeHealth: false, 473 IncludeIstioResources: false, 474 IncludeOnlyDefinitions: true, 475 ServiceSelector: labels.Set(w.Labels).String(), 476 } 477 ss, err = in.businessLayer.Svc.GetServiceList(ctx, serviceCriteria) 478 if err != nil { 479 return nil, err 480 } 481 castAppDetails(allEntities, ss, w, cluster) 482 } 483 484 return allEntities, nil 485 }