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  }