github.com/kiali/kiali@v1.84.0/business/services.go (about)

     1  package business
     2  
     3  import (
     4  	"context"
     5  	"fmt"
     6  	"sync"
     7  	"time"
     8  
     9  	networking_v1beta1 "istio.io/client-go/pkg/apis/networking/v1beta1"
    10  	apps_v1 "k8s.io/api/apps/v1"
    11  	core_v1 "k8s.io/api/core/v1"
    12  	"k8s.io/apimachinery/pkg/api/errors"
    13  	"k8s.io/apimachinery/pkg/labels"
    14  
    15  	"github.com/kiali/kiali/business/checkers"
    16  	"github.com/kiali/kiali/config"
    17  	"github.com/kiali/kiali/kubernetes"
    18  	"github.com/kiali/kiali/kubernetes/cache"
    19  	"github.com/kiali/kiali/log"
    20  	"github.com/kiali/kiali/models"
    21  	"github.com/kiali/kiali/observability"
    22  	"github.com/kiali/kiali/prometheus"
    23  )
    24  
    25  // SvcService deals with fetching istio/kubernetes services related content and convert to kiali model
    26  type SvcService struct {
    27  	config        config.Config
    28  	kialiCache    cache.KialiCache
    29  	businessLayer *Layer
    30  	prom          prometheus.ClientInterface
    31  	userClients   map[string]kubernetes.ClientInterface
    32  }
    33  
    34  type ServiceCriteria struct {
    35  	Cluster                string
    36  	Namespace              string
    37  	IncludeHealth          bool
    38  	IncludeIstioResources  bool
    39  	IncludeOnlyDefinitions bool
    40  	ServiceSelector        string
    41  	RateInterval           string
    42  	QueryTime              time.Time
    43  }
    44  
    45  // GetServiceList returns a list of all services for a given criteria
    46  func (in *SvcService) GetServiceList(ctx context.Context, criteria ServiceCriteria) (*models.ServiceList, error) {
    47  	var end observability.EndFunc
    48  	conf := config.Get()
    49  
    50  	ctx, end = observability.StartSpan(ctx, "GetServiceList",
    51  		observability.Attribute("package", "business"),
    52  		observability.Attribute("cluster", criteria.Cluster),
    53  		observability.Attribute("namespace", criteria.Namespace),
    54  		observability.Attribute("includeHealth", criteria.IncludeHealth),
    55  		observability.Attribute("includeIstioResources", criteria.IncludeIstioResources),
    56  		observability.Attribute("includeOnlyDefinitions", criteria.IncludeOnlyDefinitions),
    57  		observability.Attribute("rateInterval", criteria.RateInterval),
    58  		observability.Attribute("queryTime", criteria.QueryTime),
    59  	)
    60  	defer end()
    61  
    62  	serviceList := models.ServiceList{
    63  		Services:    []models.ServiceOverview{},
    64  		Validations: models.IstioValidations{},
    65  	}
    66  	// Check if user has access to the namespace (RBAC) in cache scenarios and/or
    67  	// if namespace is accessible from Kiali (Deployment.AccessibleNamespaces)
    68  	for cluster := range in.userClients {
    69  		if criteria.Cluster != "" && cluster != criteria.Cluster {
    70  			continue
    71  		}
    72  
    73  		if _, err := in.businessLayer.Namespace.GetClusterNamespace(ctx, criteria.Namespace, cluster); err != nil {
    74  			// We want to throw an error if we're single vs. multi cluster to be backward compatible
    75  			// TODO: Probably need this in a few other places as well. It'd be nice to have a
    76  			// centralized check for this in the config instead of this hacky one.
    77  			if len(in.userClients) == 1 {
    78  				return nil, err
    79  			}
    80  
    81  			if errors.IsNotFound(err) || errors.IsForbidden(err) {
    82  				// If a cluster is not found or not accessible, then we skip it
    83  				log.Debugf("Error while accessing to cluster [%s]: %s", cluster, err.Error())
    84  				continue
    85  			}
    86  
    87  			// On any other error, abort and return the error.
    88  			return nil, err
    89  		}
    90  
    91  		singleClusterSVCList, err := in.getServiceListForCluster(ctx, criteria, cluster)
    92  		if err != nil {
    93  			if cluster == conf.KubernetesConfig.ClusterName {
    94  				return nil, err
    95  			}
    96  
    97  			log.Errorf("Unable to get services list from cluster: %s. Err: %s. Skipping", cluster, err)
    98  			continue
    99  		}
   100  
   101  		serviceList.Services = append(serviceList.Services, singleClusterSVCList.Services...)
   102  		serviceList.Namespace = singleClusterSVCList.Namespace
   103  		serviceList.Validations = serviceList.Validations.MergeValidations(singleClusterSVCList.Validations)
   104  	}
   105  
   106  	return &serviceList, nil
   107  }
   108  
   109  func (in *SvcService) getServiceListForCluster(ctx context.Context, criteria ServiceCriteria, cluster string) (*models.ServiceList, error) {
   110  	var (
   111  		svcs            []core_v1.Service
   112  		rSvcs           []*kubernetes.RegistryService
   113  		pods            []core_v1.Pod
   114  		deployments     []apps_v1.Deployment
   115  		istioConfigList models.IstioConfigList
   116  		err             error
   117  		kubeCache       cache.KubeCache
   118  	)
   119  
   120  	kubeCache, err = in.kialiCache.GetKubeCache(cluster)
   121  	if err != nil {
   122  		return nil, err
   123  	}
   124  
   125  	var selectorLabels map[string]string
   126  	if criteria.ServiceSelector != "" {
   127  		if selector, err := labels.ConvertSelectorToLabelsMap(criteria.ServiceSelector); err == nil {
   128  			selectorLabels = selector
   129  		} else {
   130  			log.Warningf("Services not filtered. Selector %s not valid", criteria.ServiceSelector)
   131  		}
   132  	}
   133  
   134  	svcs, err = kubeCache.GetServicesBySelectorLabels(criteria.Namespace, selectorLabels)
   135  	if err != nil {
   136  		log.Errorf("Error fetching Services per namespace %s: %s", criteria.Namespace, err)
   137  		return nil, err
   138  	}
   139  
   140  	if in.config.ExternalServices.Istio.IstioAPIEnabled && cluster == in.config.KubernetesConfig.ClusterName {
   141  		registryCriteria := RegistryCriteria{
   142  			Namespace:       criteria.Namespace,
   143  			ServiceSelector: criteria.ServiceSelector,
   144  			Cluster:         cluster,
   145  		}
   146  		rSvcs = in.businessLayer.RegistryStatus.GetRegistryServices(registryCriteria)
   147  	}
   148  
   149  	if !criteria.IncludeOnlyDefinitions {
   150  		pods, err = kubeCache.GetPods(criteria.Namespace, "")
   151  		if err != nil {
   152  			log.Errorf("Error fetching Pods per namespace %s: %s", criteria.Namespace, err)
   153  			return nil, err
   154  		}
   155  	}
   156  
   157  	if !criteria.IncludeOnlyDefinitions {
   158  		deployments, err = kubeCache.GetDeployments(criteria.Namespace)
   159  		if err != nil {
   160  			log.Errorf("Error fetching Deployments per namespace %s: %s", criteria.Namespace, err)
   161  			return nil, err
   162  		}
   163  	}
   164  
   165  	// Cross-namespace query of all Istio Resources to find references
   166  	// References MAY have visibility for a user but not access if they are not allowed to access to the namespace
   167  	if criteria.IncludeIstioResources {
   168  		istioCriteria := IstioConfigCriteria{
   169  			IncludeDestinationRules:   true,
   170  			IncludeGateways:           true,
   171  			IncludeK8sGateways:        true,
   172  			IncludeK8sHTTPRoutes:      true,
   173  			IncludeK8sReferenceGrants: true,
   174  			IncludeServiceEntries:     true,
   175  			IncludeVirtualServices:    true,
   176  		}
   177  		istioConfigs, err := in.businessLayer.IstioConfig.GetIstioConfigList(ctx, cluster, istioCriteria)
   178  		if err != nil {
   179  			log.Errorf("Error fetching IstioConfigList per cluster %s per namespace %s: %s", cluster, criteria.Namespace, err)
   180  			return nil, err
   181  		}
   182  		istioConfigList = *istioConfigs
   183  	}
   184  
   185  	// Convert to Kiali model
   186  	services := in.buildServiceList(cluster, criteria.Namespace, svcs, rSvcs, pods, deployments, istioConfigList, criteria)
   187  
   188  	// Check if we need to add health
   189  
   190  	if criteria.IncludeHealth {
   191  		for i, sv := range services.Services {
   192  			// TODO: Fix health for multi-cluster
   193  			services.Services[i].Health, err = in.businessLayer.Health.GetServiceHealth(ctx, criteria.Namespace, sv.Cluster, sv.Name, criteria.RateInterval, criteria.QueryTime, sv.ParseToService())
   194  			if err != nil {
   195  				log.Errorf("Error fetching health per service %s: %s", sv.Name, err)
   196  			}
   197  		}
   198  	}
   199  
   200  	return services, nil
   201  }
   202  
   203  func getVSKialiScenario(vs []*networking_v1beta1.VirtualService) string {
   204  	scenario := ""
   205  	for _, v := range vs {
   206  		if scenario, ok := v.Labels["kiali_wizard"]; ok {
   207  			return scenario
   208  		}
   209  	}
   210  	return scenario
   211  }
   212  
   213  func getDRKialiScenario(dr []*networking_v1beta1.DestinationRule) string {
   214  	scenario := ""
   215  	for _, d := range dr {
   216  		if scenario, ok := d.Labels["kiali_wizard"]; ok {
   217  			return scenario
   218  		}
   219  	}
   220  	return scenario
   221  }
   222  
   223  func (in *SvcService) buildServiceList(cluster string, namespace string, svcs []core_v1.Service, rSvcs []*kubernetes.RegistryService, pods []core_v1.Pod, deployments []apps_v1.Deployment, istioConfigList models.IstioConfigList, criteria ServiceCriteria) *models.ServiceList {
   224  	services := []models.ServiceOverview{}
   225  	validations := models.IstioValidations{}
   226  	if !criteria.IncludeOnlyDefinitions {
   227  		validations = in.getServiceValidations(svcs, deployments, pods)
   228  	}
   229  
   230  	kubernetesServices := in.buildKubernetesServices(svcs, pods, istioConfigList, criteria.IncludeOnlyDefinitions)
   231  	services = append(services, kubernetesServices...)
   232  	// Add cluster to each kube service
   233  	for i := range services {
   234  		services[i].Cluster = cluster
   235  	}
   236  
   237  	// Add Istio Registry Services that are not present in the Kubernetes list
   238  	// TODO: Registry services are not associated to a cluster. They can have multiple clusters under
   239  	// "clusterVIPs". We need to decide how to handle this.
   240  	rSvcs = kubernetes.FilterRegistryServicesByServices(rSvcs, svcs)
   241  	registryServices := in.buildRegistryServices(rSvcs, istioConfigList)
   242  	services = append(services, registryServices...)
   243  	return &models.ServiceList{Namespace: namespace, Services: services, Validations: validations}
   244  }
   245  
   246  func (in *SvcService) buildKubernetesServices(svcs []core_v1.Service, pods []core_v1.Pod, istioConfigList models.IstioConfigList, onlyDefinitions bool) []models.ServiceOverview {
   247  	services := make([]models.ServiceOverview, len(svcs))
   248  	conf := in.config
   249  
   250  	// Convert each k8sClients service into our model
   251  	for i, item := range svcs {
   252  		var kialiWizard string
   253  		hasSidecar := true
   254  		hasAmbient := false
   255  		svcReferences := make([]*models.IstioValidationKey, 0)
   256  
   257  		if !onlyDefinitions {
   258  			sPods := kubernetes.FilterPodsByService(&item, pods)
   259  			/** Check if Service has istioSidecar deployed */
   260  			mPods := models.Pods{}
   261  			mPods.Parse(sPods)
   262  			hasSidecar = mPods.HasAnyIstioSidecar()
   263  			hasAmbient = mPods.HasAnyAmbient()
   264  			svcVirtualServices := kubernetes.FilterAutogeneratedVirtualServices(kubernetes.FilterVirtualServicesByService(istioConfigList.VirtualServices, item.Namespace, item.Name))
   265  			svcDestinationRules := kubernetes.FilterDestinationRulesByService(istioConfigList.DestinationRules, item.Namespace, item.Name)
   266  			svcGateways := kubernetes.FilterGatewaysByVirtualServices(istioConfigList.Gateways, svcVirtualServices)
   267  			svcK8sHTTPRoutes := kubernetes.FilterK8sHTTPRoutesByService(istioConfigList.K8sHTTPRoutes, istioConfigList.K8sReferenceGrants, item.Namespace, item.Name)
   268  			svcK8sGateways := kubernetes.FilterK8sGatewaysByHTTPRoutes(istioConfigList.K8sGateways, svcK8sHTTPRoutes)
   269  
   270  			for _, vs := range svcVirtualServices {
   271  				ref := models.BuildKey(vs.Kind, vs.Name, vs.Namespace)
   272  				svcReferences = append(svcReferences, &ref)
   273  			}
   274  			for _, dr := range svcDestinationRules {
   275  				ref := models.BuildKey(dr.Kind, dr.Name, dr.Namespace)
   276  				svcReferences = append(svcReferences, &ref)
   277  			}
   278  			for _, gw := range svcGateways {
   279  				ref := models.BuildKey(gw.Kind, gw.Name, gw.Namespace)
   280  				svcReferences = append(svcReferences, &ref)
   281  			}
   282  			for _, gw := range svcK8sGateways {
   283  				// Should be K8s type to generate correct link
   284  				ref := models.BuildKey(kubernetes.K8sGatewayType, gw.Name, gw.Namespace)
   285  				svcReferences = append(svcReferences, &ref)
   286  			}
   287  			for _, route := range svcK8sHTTPRoutes {
   288  				// Should be K8s type to generate correct link
   289  				ref := models.BuildKey(kubernetes.K8sHTTPRouteType, route.Name, route.Namespace)
   290  				svcReferences = append(svcReferences, &ref)
   291  			}
   292  			svcReferences = FilterUniqueIstioReferences(svcReferences)
   293  			kialiWizard = getVSKialiScenario(svcVirtualServices)
   294  			if kialiWizard == "" {
   295  				kialiWizard = getDRKialiScenario(svcDestinationRules)
   296  			}
   297  		}
   298  
   299  		/** Check if Service has the label app required by Istio */
   300  		_, appLabel := item.Spec.Selector[conf.IstioLabels.AppLabelName]
   301  		/** Check if Service has additional item icon */
   302  		services[i] = models.ServiceOverview{
   303  			Name:                   item.Name,
   304  			Namespace:              item.Namespace,
   305  			IstioSidecar:           hasSidecar,
   306  			IstioAmbient:           hasAmbient,
   307  			AppLabel:               appLabel,
   308  			AdditionalDetailSample: models.GetFirstAdditionalIcon(&conf, item.ObjectMeta.Annotations),
   309  			Health:                 models.EmptyServiceHealth(),
   310  			HealthAnnotations:      models.GetHealthAnnotation(item.Annotations, models.GetHealthConfigAnnotation()),
   311  			Labels:                 item.Labels,
   312  			Selector:               item.Spec.Selector,
   313  			IstioReferences:        svcReferences,
   314  			KialiWizard:            kialiWizard,
   315  			ServiceRegistry:        "Kubernetes",
   316  		}
   317  	}
   318  	return services
   319  }
   320  
   321  func filterIstioServiceByClusterId(clusterId string, item *kubernetes.RegistryService) bool {
   322  	if clusterId == "Kubernetes" {
   323  		return true
   324  	}
   325  	// External and Federation services are always local to the control plane
   326  	if item.Attributes.ServiceRegistry != "Kubernetes" {
   327  		return true
   328  	}
   329  	if _, ok := item.ClusterVIPs12.Addresses[clusterId]; ok {
   330  		return true
   331  	}
   332  	if _, ok := item.ClusterVIPs11[clusterId]; ok {
   333  		return true
   334  	}
   335  	return false
   336  }
   337  
   338  func (in *SvcService) buildRegistryServices(rSvcs []*kubernetes.RegistryService, istioConfigList models.IstioConfigList) []models.ServiceOverview {
   339  	services := []models.ServiceOverview{}
   340  	conf := in.config
   341  
   342  	// The istiod registry doesn't have a explicit flag when a service is deployed in a different control plane.
   343  	// The only way to identify it is to check that the service has an address in the current cluster.
   344  	// To avoid side effects, Kiali will process only services that belongs to the current cluster.
   345  	// This should be revisited on more multi-cluster deployments scenarios.
   346  	//
   347  	//	{
   348  	//		"hostname": "test-svc.evil.svc.cluster.local",
   349  	//		"clusterVIPs": {
   350  	//			"Addresses": {
   351  	//				"istio-west": [
   352  	//					"0.0.0.0"
   353  	//				]
   354  	//			}
   355  	//	}
   356  	// By default Istio uses "Kubernetes" as clusterId for single control planes scenarios.
   357  	// This clusterId is propagated into the Istio Registry and we need it to filter services in multi-cluster scenarios.
   358  	// I.e.:
   359  	//    "clusterVIPs": {
   360  	//      "Addresses": {
   361  	//        "Kubernetes": [
   362  	//          "10.217.4.189"
   363  	//        ]
   364  	//      }
   365  	//    }
   366  	clusterId := conf.KubernetesConfig.ClusterName
   367  	for _, item := range rSvcs {
   368  		if !filterIstioServiceByClusterId(clusterId, item) {
   369  			continue
   370  		}
   371  		_, appLabel := item.Attributes.LabelSelectors[conf.IstioLabels.AppLabelName]
   372  		// ServiceEntry/External and Federation will be marked as hasSidecar == true as they will have telemetry
   373  		hasSidecar := true
   374  		if item.Attributes.ServiceRegistry != "External" && item.Attributes.ServiceRegistry != "Federation" {
   375  			hasSidecar = false
   376  		}
   377  		// TODO wildcards may force additional checks on hostnames ?
   378  		svcServiceEntries := kubernetes.FilterServiceEntriesByHostname(istioConfigList.ServiceEntries, item.Hostname)
   379  		svcDestinationRules := kubernetes.FilterDestinationRulesByHostname(istioConfigList.DestinationRules, item.Hostname)
   380  		svcVirtualServices := kubernetes.FilterVirtualServicesByHostname(istioConfigList.VirtualServices, item.Hostname)
   381  		svcGateways := kubernetes.FilterGatewaysByVirtualServices(istioConfigList.Gateways, svcVirtualServices)
   382  		svcReferences := make([]*models.IstioValidationKey, 0)
   383  		for _, se := range svcServiceEntries {
   384  			ref := models.BuildKey(se.Kind, se.Name, se.Namespace)
   385  			svcReferences = append(svcReferences, &ref)
   386  		}
   387  		for _, vs := range svcVirtualServices {
   388  			ref := models.BuildKey(vs.Kind, vs.Name, vs.Namespace)
   389  			svcReferences = append(svcReferences, &ref)
   390  		}
   391  		for _, dr := range svcDestinationRules {
   392  			ref := models.BuildKey(dr.Kind, dr.Name, dr.Namespace)
   393  			svcReferences = append(svcReferences, &ref)
   394  		}
   395  		for _, gw := range svcGateways {
   396  			ref := models.BuildKey(gw.Kind, gw.Name, gw.Namespace)
   397  			svcReferences = append(svcReferences, &ref)
   398  		}
   399  		svcReferences = FilterUniqueIstioReferences(svcReferences)
   400  		// External Istio registries may have references to ServiceEntry and/or Federation
   401  		service := models.ServiceOverview{
   402  			Name:              item.Attributes.Name,
   403  			Namespace:         item.Attributes.Namespace,
   404  			IstioSidecar:      hasSidecar,
   405  			AppLabel:          appLabel,
   406  			Health:            models.EmptyServiceHealth(),
   407  			HealthAnnotations: map[string]string{},
   408  			Labels:            item.Attributes.Labels,
   409  			Selector:          item.Attributes.LabelSelectors,
   410  			IstioReferences:   svcReferences,
   411  			ServiceRegistry:   item.Attributes.ServiceRegistry,
   412  		}
   413  		services = append(services, service)
   414  	}
   415  	return services
   416  }
   417  
   418  // GetService returns a single service and associated data using the interval and queryTime
   419  func (in *SvcService) GetServiceDetails(ctx context.Context, cluster, namespace, service, interval string, queryTime time.Time) (*models.ServiceDetails, error) {
   420  	var end observability.EndFunc
   421  	ctx, end = observability.StartSpan(ctx, "GetServiceDetails",
   422  		observability.Attribute("package", "business"),
   423  		observability.Attribute("cluster", cluster),
   424  		observability.Attribute("namespace", namespace),
   425  		observability.Attribute("service", service),
   426  		observability.Attribute("interval", interval),
   427  		observability.Attribute("queryTime", queryTime),
   428  	)
   429  	defer end()
   430  
   431  	// Check if user has access to the namespace (RBAC) in cache scenarios and/or
   432  	// if namespace is accessible from Kiali (Deployment.AccessibleNamespaces)
   433  	if _, err := in.businessLayer.Namespace.GetClusterNamespace(ctx, namespace, cluster); err != nil {
   434  		return nil, err
   435  	}
   436  
   437  	svc, err := in.GetService(ctx, cluster, namespace, service)
   438  	if err != nil {
   439  		return nil, err
   440  	}
   441  
   442  	kubeCache, err := in.kialiCache.GetKubeCache(cluster)
   443  	if err != nil {
   444  		return nil, err
   445  	}
   446  
   447  	var eps *core_v1.Endpoints
   448  	var pods []core_v1.Pod
   449  	var hth models.ServiceHealth
   450  	var istioConfigList *models.IstioConfigList
   451  	var ws models.Workloads
   452  	var rSvcs []*kubernetes.RegistryService
   453  	var nsmtls models.MTLSStatus
   454  
   455  	wg := sync.WaitGroup{}
   456  	// Max possible number of errors. It's ok if the buffer size exceeds the number of goroutines
   457  	// in cases where istio api is disabled.
   458  	errChan := make(chan error, 8)
   459  
   460  	labelsSelector := labels.Set(svc.Selectors).String()
   461  	// If service doesn't have any selector, we can't know which are the pods and workloads applying.
   462  	if labelsSelector != "" {
   463  		wg.Add(1)
   464  		go func() {
   465  			defer wg.Done()
   466  			var err2 error
   467  			pods, err2 = kubeCache.GetPods(namespace, labelsSelector)
   468  			if err2 != nil {
   469  				errChan <- err2
   470  			}
   471  		}()
   472  
   473  		wg.Add(1)
   474  		go func(ctx context.Context) {
   475  			defer wg.Done()
   476  			var err2 error
   477  			ws, err2 = in.businessLayer.Workload.fetchWorkloadsFromCluster(ctx, cluster, namespace, labelsSelector)
   478  			if err2 != nil {
   479  				log.Errorf("Error fetching Workloads per namespace %s and service %s: %s", namespace, service, err2)
   480  				errChan <- err2
   481  			}
   482  		}(ctx)
   483  
   484  		if in.config.ExternalServices.Istio.IstioAPIEnabled {
   485  			registryCriteria := RegistryCriteria{
   486  				Namespace: namespace,
   487  				Cluster:   cluster,
   488  			}
   489  			rSvcs = in.businessLayer.RegistryStatus.GetRegistryServices(registryCriteria)
   490  		}
   491  	}
   492  
   493  	wg.Add(1)
   494  	go func(ctx context.Context) {
   495  		defer wg.Done()
   496  		var err2 error
   497  		eps, err2 = kubeCache.GetEndpoints(namespace, service)
   498  		if err2 != nil && !errors.IsNotFound(err2) {
   499  			log.Errorf("Error fetching Endpoints namespace %s and service %s: %s", namespace, service, err2)
   500  			errChan <- err2
   501  		}
   502  	}(ctx)
   503  
   504  	wg.Add(1)
   505  	go func(ctx context.Context) {
   506  		defer wg.Done()
   507  		var err2 error
   508  		// TODO: Fix health for multi-cluster
   509  		hth, err2 = in.businessLayer.Health.GetServiceHealth(ctx, namespace, cluster, service, interval, queryTime, &svc)
   510  		if err2 != nil {
   511  			errChan <- err2
   512  		}
   513  	}(ctx)
   514  
   515  	wg.Add(1)
   516  	go func(ctx context.Context) {
   517  		defer wg.Done()
   518  		var err2 error
   519  		nsmtls, err2 = in.businessLayer.TLS.NamespaceWidemTLSStatus(ctx, namespace, cluster)
   520  		if err2 != nil {
   521  			errChan <- err2
   522  		}
   523  	}(ctx)
   524  
   525  	wg.Add(1)
   526  	go func(ctx context.Context) {
   527  		defer wg.Done()
   528  		var err2 error
   529  		criteria := IstioConfigCriteria{
   530  			IncludeDestinationRules: true,
   531  			// TODO the frontend is merging the Gateways per ServiceDetails but it would be a clean design to locate it here
   532  			IncludeGateways:           true,
   533  			IncludeK8sGateways:        true,
   534  			IncludeK8sHTTPRoutes:      true,
   535  			IncludeK8sReferenceGrants: true,
   536  			IncludeServiceEntries:     true,
   537  			IncludeVirtualServices:    true,
   538  		}
   539  		istioConfigList, err2 = in.businessLayer.IstioConfig.GetIstioConfigListForNamespace(ctx, cluster, namespace, criteria)
   540  		if err2 != nil {
   541  			log.Errorf("Error fetching IstioConfigList per namespace %s: %s", namespace, err2)
   542  			errChan <- err2
   543  		}
   544  	}(ctx)
   545  
   546  	var vsCreate, vsUpdate, vsDelete bool
   547  	wg.Add(1)
   548  	go func() {
   549  		defer wg.Done()
   550  		/*
   551  			We can safely assume that permissions for VirtualServices will be similar as DestinationRules.
   552  
   553  			Synced with:
   554  			https://github.com/kiali/kiali-operator/blob/master/roles/default/kiali-deploy/templates/kubernetes/role.yaml#L62
   555  		*/
   556  		userClient, found := in.userClients[cluster]
   557  		if !found {
   558  			errChan <- fmt.Errorf("client not found for cluster: %s", cluster)
   559  			return
   560  		}
   561  		vsCreate, vsUpdate, vsDelete = getPermissions(context.TODO(), userClient, cluster, namespace, kubernetes.VirtualServices)
   562  	}()
   563  
   564  	wg.Wait()
   565  	if len(errChan) != 0 {
   566  		err = <-errChan
   567  		return nil, err
   568  	}
   569  
   570  	wo := models.WorkloadOverviews{}
   571  	for _, w := range ws {
   572  		wi := &models.WorkloadListItem{}
   573  		wi.ParseWorkload(w)
   574  		wo = append(wo, wi)
   575  	}
   576  
   577  	serviceOverviews := make([]*models.ServiceOverview, 0)
   578  	// Convert filtered k8sClients services into ServiceOverview, only several attributes are needed
   579  	for _, item := range rSvcs {
   580  		// app label selector of services should match, loading all versions
   581  		if selector, err3 := labels.ConvertSelectorToLabelsMap(labelsSelector); err3 == nil {
   582  			if appSelector, ok := item.Attributes.LabelSelectors["app"]; ok && selector.Has("app") && appSelector == selector.Get("app") {
   583  				if _, ok1 := item.Attributes.LabelSelectors["version"]; ok1 {
   584  					ports := map[string]int{}
   585  					for _, port := range item.Ports {
   586  						ports[port.Name] = port.Port
   587  					}
   588  					serviceOverviews = append(serviceOverviews, &models.ServiceOverview{
   589  						Name:  item.Attributes.Name,
   590  						Ports: ports,
   591  					})
   592  				}
   593  			}
   594  		}
   595  	}
   596  	// loading the single service if no versions
   597  	if len(serviceOverviews) == 0 {
   598  		ports := map[string]int{}
   599  		for _, port := range svc.Ports {
   600  			ports[port.Name] = int(port.Port)
   601  		}
   602  		serviceOverviews = append(serviceOverviews, &models.ServiceOverview{
   603  			Name:  svc.Name,
   604  			Ports: ports,
   605  		})
   606  	}
   607  
   608  	s := models.ServiceDetails{Workloads: wo, Health: hth, NamespaceMTLS: nsmtls, SubServices: serviceOverviews}
   609  	s.Service = svc
   610  	s.SetPods(kubernetes.FilterPodsByEndpoints(eps, pods))
   611  	// ServiceDetail will consider if the Service is a External/Federation entry
   612  	if s.Service.Type == "External" || s.Service.Type == "Federation" {
   613  		s.IstioSidecar = true
   614  	} else {
   615  		s.SetIstioSidecar(wo)
   616  	}
   617  	s.SetEndpoints(eps)
   618  	s.IstioPermissions = models.ResourcePermissions{
   619  		Create: vsCreate,
   620  		Update: vsUpdate,
   621  		Delete: vsDelete,
   622  	}
   623  	s.VirtualServices = kubernetes.FilterAutogeneratedVirtualServices(kubernetes.FilterVirtualServicesByService(istioConfigList.VirtualServices, namespace, service))
   624  	s.DestinationRules = kubernetes.FilterDestinationRulesByService(istioConfigList.DestinationRules, namespace, service)
   625  	s.K8sHTTPRoutes = kubernetes.FilterK8sHTTPRoutesByService(istioConfigList.K8sHTTPRoutes, istioConfigList.K8sReferenceGrants, namespace, service)
   626  	if s.Service.Type == "External" || s.Service.Type == "Federation" {
   627  		// On ServiceEntries cases the Service name is the hostname
   628  		s.ServiceEntries = kubernetes.FilterServiceEntriesByHostname(istioConfigList.ServiceEntries, s.Service.Name)
   629  	}
   630  
   631  	return &s, nil
   632  }
   633  
   634  func (in *SvcService) UpdateService(ctx context.Context, cluster, namespace, service string, interval string, queryTime time.Time, jsonPatch string, patchType string) (*models.ServiceDetails, error) {
   635  	var end observability.EndFunc
   636  	ctx, end = observability.StartSpan(ctx, "UpdateService",
   637  		observability.Attribute("package", "business"),
   638  		observability.Attribute("cluster", cluster),
   639  		observability.Attribute("namespace", namespace),
   640  		observability.Attribute("service", service),
   641  		observability.Attribute("interval", interval),
   642  		observability.Attribute("queryTime", queryTime),
   643  		observability.Attribute("jsonPatch", jsonPatch),
   644  		observability.Attribute("patchType", patchType),
   645  	)
   646  	defer end()
   647  
   648  	// Identify controller and apply patch to workload
   649  	// Check if user has access to the namespace (RBAC) in cache scenarios and/or
   650  	// if namespace is accessible from Kiali (Deployment.AccessibleNamespaces)
   651  	if _, err := in.businessLayer.Namespace.GetClusterNamespace(context.TODO(), namespace, cluster); err != nil {
   652  		return nil, err
   653  	}
   654  
   655  	userClient, found := in.userClients[cluster]
   656  	if !found {
   657  		return nil, fmt.Errorf("cluster: %s not found", cluster)
   658  	}
   659  
   660  	if err := userClient.UpdateService(namespace, service, jsonPatch, patchType); err != nil {
   661  		return nil, err
   662  	}
   663  
   664  	// Stop and restart cache here after a Create/Update/Delete operation to force a refresh
   665  	// so that the next request that reads from the cache will be sure to have the write operation.
   666  	kubeCache, err := in.kialiCache.GetKubeCache(cluster)
   667  	if err != nil {
   668  		return nil, err
   669  	}
   670  	kubeCache.Refresh(namespace)
   671  
   672  	// After the update we fetch the whole workload
   673  	return in.GetServiceDetails(ctx, cluster, namespace, service, interval, queryTime)
   674  }
   675  
   676  func (in *SvcService) GetService(ctx context.Context, cluster, namespace, service string) (models.Service, error) {
   677  	var end observability.EndFunc
   678  	ctx, end = observability.StartSpan(ctx, "GetService",
   679  		observability.Attribute("package", "business"),
   680  		observability.Attribute("cluster", cluster),
   681  		observability.Attribute("namespace", namespace),
   682  		observability.Attribute("service", service),
   683  	)
   684  	defer end()
   685  
   686  	// Check if user has access to the namespace (RBAC) in cache scenarios and/or
   687  	// if namespace is accessible from Kiali (Deployment.AccessibleNamespaces)
   688  	if _, err := in.businessLayer.Namespace.GetClusterNamespace(ctx, namespace, cluster); err != nil {
   689  		return models.Service{}, err
   690  	}
   691  
   692  	cache, err := in.kialiCache.GetKubeCache(cluster)
   693  	if err != nil {
   694  		return models.Service{}, err
   695  	}
   696  
   697  	svc := models.Service{}
   698  	// First try to get the service from kube.
   699  	// If it doesn't exist, try to get it from the Istio Registry.
   700  	kSvc, err := cache.GetService(namespace, service)
   701  	if err != nil {
   702  		// Check if this service is in the Istio Registry
   703  		criteria := RegistryCriteria{
   704  			Namespace: namespace,
   705  			Cluster:   cluster,
   706  		}
   707  		rSvcs := in.businessLayer.RegistryStatus.GetRegistryServices(criteria)
   708  		for _, rSvc := range rSvcs {
   709  			if rSvc.Attributes.Name == service {
   710  				svc.ParseRegistryService(cluster, rSvc)
   711  				break
   712  			}
   713  		}
   714  		// Service not found in Kubernetes and Istio
   715  		if svc.Name == "" {
   716  			return svc, kubernetes.NewNotFound(service, "Kiali", "Service")
   717  		}
   718  	} else {
   719  		svc.Parse(cluster, kSvc)
   720  	}
   721  
   722  	return svc, nil
   723  }
   724  
   725  func (in *SvcService) getServiceValidations(services []core_v1.Service, deployments []apps_v1.Deployment, pods []core_v1.Pod) models.IstioValidations {
   726  	validations := checkers.ServiceChecker{
   727  		Services:    services,
   728  		Deployments: deployments,
   729  		Pods:        pods,
   730  	}.Check()
   731  
   732  	return validations
   733  }
   734  
   735  // GetServiceAppName returns the "Application" name (app label) that relates to a service
   736  // This label is taken from the service selector, which means it is assumed that pods are selected using that label
   737  func (in *SvcService) GetServiceAppName(ctx context.Context, cluster, namespace, service string) (string, error) {
   738  	var end observability.EndFunc
   739  	ctx, end = observability.StartSpan(ctx, "GetServiceAppName",
   740  		observability.Attribute("package", "business"),
   741  		observability.Attribute("cluster", cluster),
   742  		observability.Attribute("namespace", namespace),
   743  		observability.Attribute("service", service),
   744  	)
   745  	defer end()
   746  
   747  	// Check if user has access to the namespace (RBAC) in cache scenarios and/or
   748  	// if namespace is accessible from Kiali (Deployment.AccessibleNamespaces)
   749  	if _, err := in.businessLayer.Namespace.GetClusterNamespace(ctx, namespace, cluster); err != nil {
   750  		return "", err
   751  	}
   752  
   753  	svc, err := in.GetService(ctx, cluster, namespace, service)
   754  	if err != nil {
   755  		return "", fmt.Errorf("Service [cluster: %s] [namespace: %s] [name: %s] doesn't exist.", cluster, namespace, service)
   756  	}
   757  
   758  	appLabelName := in.config.IstioLabels.AppLabelName
   759  	app := svc.Selectors[appLabelName]
   760  	return app, nil
   761  }