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

     1  package business
     2  
     3  import (
     4  	"context"
     5  	"fmt"
     6  	"sync"
     7  	"time"
     8  
     9  	"k8s.io/apimachinery/pkg/labels"
    10  
    11  	"github.com/kiali/kiali/config"
    12  	"github.com/kiali/kiali/kubernetes"
    13  	"github.com/kiali/kiali/log"
    14  	"github.com/kiali/kiali/models"
    15  	"github.com/kiali/kiali/observability"
    16  	"github.com/kiali/kiali/util/httputil"
    17  )
    18  
    19  func NewIstioStatusService(userClients map[string]kubernetes.ClientInterface, businessLayer *Layer, cpm ControlPlaneMonitor) IstioStatusService {
    20  	return IstioStatusService{
    21  		userClients:         userClients,
    22  		businessLayer:       businessLayer,
    23  		controlPlaneMonitor: cpm,
    24  	}
    25  }
    26  
    27  // SvcService deals with fetching istio/kubernetes services related content and convert to kiali model
    28  type IstioStatusService struct {
    29  	userClients         map[string]kubernetes.ClientInterface
    30  	businessLayer       *Layer
    31  	controlPlaneMonitor ControlPlaneMonitor
    32  }
    33  
    34  func (iss *IstioStatusService) GetStatus(ctx context.Context, cluster string) (kubernetes.IstioComponentStatus, error) {
    35  	var end observability.EndFunc
    36  	ctx, end = observability.StartSpan(ctx, "GetStatus",
    37  		observability.Attribute("package", "business"),
    38  	)
    39  	defer end()
    40  
    41  	if !config.Get().ExternalServices.Istio.ComponentStatuses.Enabled || !config.Get().ExternalServices.Istio.IstioAPIEnabled {
    42  		return kubernetes.IstioComponentStatus{}, nil
    43  	}
    44  
    45  	ics, err := iss.getIstioComponentStatus(ctx, cluster)
    46  	if err != nil {
    47  		return nil, err
    48  	}
    49  
    50  	return ics.Merge(iss.getAddonComponentStatus()), nil
    51  }
    52  
    53  func (iss *IstioStatusService) getIstioComponentStatus(ctx context.Context, cluster string) (kubernetes.IstioComponentStatus, error) {
    54  	// Fetching workloads from component namespaces
    55  	workloads, err := iss.getComponentNamespacesWorkloads(ctx, cluster)
    56  	if err != nil {
    57  		return kubernetes.IstioComponentStatus{}, err
    58  	}
    59  
    60  	deploymentStatus, err := iss.getStatusOf(workloads)
    61  	if err != nil {
    62  		return kubernetes.IstioComponentStatus{}, err
    63  	}
    64  
    65  	k8s, ok := iss.userClients[cluster]
    66  	if !ok {
    67  		return kubernetes.IstioComponentStatus{}, fmt.Errorf("Cluster %s doesn't exist ", cluster)
    68  	}
    69  
    70  	istiodStatus, err := iss.controlPlaneMonitor.CanConnectToIstiod(k8s)
    71  	if err != nil {
    72  		return kubernetes.IstioComponentStatus{}, err
    73  	}
    74  
    75  	return deploymentStatus.Merge(istiodStatus), nil
    76  }
    77  
    78  func (iss *IstioStatusService) getComponentNamespacesWorkloads(ctx context.Context, cluster string) ([]*models.Workload, error) {
    79  	var wg sync.WaitGroup
    80  
    81  	nss := map[string]bool{}
    82  	wls := make([]*models.Workload, 0)
    83  
    84  	comNs := getComponentNamespaces()
    85  
    86  	wlChan := make(chan []*models.Workload, len(comNs))
    87  	errChan := make(chan error, len(comNs))
    88  
    89  	for _, n := range comNs {
    90  		if !nss[n] {
    91  			wg.Add(1)
    92  			nss[n] = true
    93  
    94  			go func(ctx context.Context, n string, wliChan chan []*models.Workload, errChan chan error) {
    95  				defer wg.Done()
    96  				var wls models.Workloads
    97  				var err error
    98  				wls, err = iss.businessLayer.Workload.fetchWorkloadsFromCluster(ctx, cluster, n, "")
    99  				wliChan <- wls
   100  				errChan <- err
   101  			}(ctx, n, wlChan, errChan)
   102  		}
   103  	}
   104  
   105  	wg.Wait()
   106  
   107  	close(wlChan)
   108  	close(errChan)
   109  	for err := range errChan {
   110  		if err != nil {
   111  			return nil, err
   112  		}
   113  	}
   114  
   115  	for wl := range wlChan {
   116  		if wl != nil {
   117  			wls = append(wls, wl...)
   118  		}
   119  	}
   120  
   121  	return wls, nil
   122  }
   123  
   124  func getComponentNamespaces() []string {
   125  	nss := make([]string, 0)
   126  
   127  	// By default, add the istio control plane namespace
   128  	nss = append(nss, config.Get().IstioNamespace)
   129  
   130  	// Adding Istio Components namespaces
   131  	externalServices := config.Get().ExternalServices
   132  	for _, cmp := range externalServices.Istio.ComponentStatuses.Components {
   133  		if cmp.Namespace != "" {
   134  			nss = append(nss, cmp.Namespace)
   135  		}
   136  	}
   137  
   138  	return nss
   139  }
   140  
   141  func istioCoreComponents() map[string]config.ComponentStatus {
   142  	components := map[string]config.ComponentStatus{}
   143  	cs := config.Get().ExternalServices.Istio.ComponentStatuses
   144  	for _, c := range cs.Components {
   145  		components[c.AppLabel] = c
   146  	}
   147  	return components
   148  }
   149  
   150  func (iss *IstioStatusService) getStatusOf(workloads []*models.Workload) (kubernetes.IstioComponentStatus, error) {
   151  	statusComponents := istioCoreComponents()
   152  	isc := kubernetes.IstioComponentStatus{}
   153  	cf := map[string]bool{}
   154  	mcf := map[string]int{}
   155  
   156  	// Map workloads there by app name
   157  	for _, workload := range workloads {
   158  		appLabel := labels.Set(workload.Labels).Get("app")
   159  		if appLabel == "" {
   160  			continue
   161  		}
   162  
   163  		stat, found := statusComponents[appLabel]
   164  		if !found {
   165  			continue
   166  		}
   167  
   168  		if stat.IsMultiCluster {
   169  			mcf[appLabel]++
   170  		} else {
   171  			// Component found
   172  			cf[appLabel] = true
   173  			// @TODO when components exists on remote clusters only but config not marked multicluster
   174  		}
   175  
   176  		status := GetWorkloadStatus(*workload)
   177  		// Add status
   178  		isc = append(isc, kubernetes.ComponentStatus{
   179  			Name:   workload.Name,
   180  			Status: status,
   181  			IsCore: stat.IsCore,
   182  		})
   183  
   184  	}
   185  
   186  	// Add missing deployments
   187  	componentNotFound := 0
   188  	for comp, stat := range statusComponents {
   189  		if _, found := cf[comp]; !found {
   190  			if number, mfound := mcf[comp]; !mfound || number < len(iss.userClients) { // multicluster components should exist on all clusters
   191  				componentNotFound += 1
   192  				isc = append(isc, kubernetes.ComponentStatus{
   193  					Name:   comp,
   194  					Status: kubernetes.ComponentNotFound,
   195  					IsCore: stat.IsCore,
   196  				})
   197  			}
   198  		}
   199  	}
   200  
   201  	// When all the deployments are missing,
   202  	// Warn users that their kiali config might be wrong
   203  	if componentNotFound == len(statusComponents) {
   204  		return isc, fmt.Errorf(
   205  			"Kiali is unable to find any Istio deployment in namespace %s. Are you sure the Istio namespace is configured correctly in Kiali?",
   206  			config.Get().IstioNamespace)
   207  	}
   208  
   209  	return isc, nil
   210  }
   211  
   212  func GetWorkloadStatus(wl models.Workload) string {
   213  	status := kubernetes.ComponentUnhealthy
   214  
   215  	if wl.DesiredReplicas == 0 {
   216  		status = kubernetes.ComponentNotReady
   217  	} else if wl.DesiredReplicas == wl.AvailableReplicas && wl.DesiredReplicas == wl.CurrentReplicas {
   218  		status = kubernetes.ComponentHealthy
   219  	}
   220  	return status
   221  }
   222  
   223  func (iss *IstioStatusService) getAddonComponentStatus() kubernetes.IstioComponentStatus {
   224  	var wg sync.WaitGroup
   225  	wg.Add(4)
   226  
   227  	staChan := make(chan kubernetes.IstioComponentStatus, 4)
   228  	extServices := config.Get().ExternalServices
   229  
   230  	// https://github.com/kiali/kiali/issues/6966 - use the well-known Prom healthy endpoint
   231  	if extServices.Prometheus.HealthCheckUrl == "" {
   232  		extServices.Prometheus.HealthCheckUrl = extServices.Prometheus.URL + "/-/healthy"
   233  	}
   234  
   235  	ics := kubernetes.IstioComponentStatus{}
   236  
   237  	go getAddonStatus("prometheus", true, extServices.Prometheus.IsCore, &extServices.Prometheus.Auth, extServices.Prometheus.URL, extServices.Prometheus.HealthCheckUrl, staChan, &wg)
   238  	go getAddonStatus("grafana", extServices.Grafana.Enabled, extServices.Grafana.IsCore, &extServices.Grafana.Auth, extServices.Grafana.InClusterURL, extServices.Grafana.HealthCheckUrl, staChan, &wg)
   239  	go iss.getTracingStatus("tracing", extServices.Tracing.Enabled, extServices.Tracing.IsCore, staChan, &wg)
   240  
   241  	// Custom dashboards may use the main Prometheus config
   242  	customProm := extServices.CustomDashboards.Prometheus
   243  	if customProm.URL == "" {
   244  		customProm = extServices.Prometheus
   245  	}
   246  	go getAddonStatus("custom dashboards", extServices.CustomDashboards.Enabled, extServices.CustomDashboards.IsCore, &customProm.Auth, customProm.URL, customProm.HealthCheckUrl, staChan, &wg)
   247  
   248  	wg.Wait()
   249  
   250  	close(staChan)
   251  	for stat := range staChan {
   252  		ics.Merge(stat)
   253  	}
   254  
   255  	return ics
   256  }
   257  
   258  func getAddonStatus(name string, enabled bool, isCore bool, auth *config.Auth, url string, healthCheckUrl string, staChan chan<- kubernetes.IstioComponentStatus, wg *sync.WaitGroup) {
   259  	defer wg.Done()
   260  
   261  	// When the addOn is disabled, don't perform any check
   262  	if !enabled {
   263  		return
   264  	}
   265  
   266  	// Take the alternative health check url if present
   267  	if healthCheckUrl != "" {
   268  		url = healthCheckUrl
   269  	}
   270  
   271  	if auth.UseKialiToken {
   272  		token, _, err := kubernetes.GetKialiTokenForHomeCluster()
   273  		if err != nil {
   274  			log.Errorf("Could not read the Kiali Service Account token: %v", err)
   275  		}
   276  		auth.Token = token
   277  	}
   278  
   279  	status := kubernetes.ComponentHealthy
   280  	// Call the addOn service endpoint to find out whether is reachable or not
   281  	_, statusCode, _, err := httputil.HttpGet(url, auth, 10*time.Second, nil, nil)
   282  	if err != nil || statusCode > 399 {
   283  		log.Tracef("addon health check failed: name=[%v], url=[%v], code=[%v]", name, url, statusCode)
   284  		status = kubernetes.ComponentUnreachable
   285  	}
   286  
   287  	staChan <- kubernetes.IstioComponentStatus{
   288  		kubernetes.ComponentStatus{
   289  			Name:   name,
   290  			Status: status,
   291  			IsCore: isCore,
   292  		},
   293  	}
   294  }
   295  
   296  func (iss *IstioStatusService) getTracingStatus(name string, enabled bool, isCore bool, staChan chan<- kubernetes.IstioComponentStatus, wg *sync.WaitGroup) {
   297  	defer wg.Done()
   298  
   299  	if !enabled {
   300  		return
   301  	}
   302  
   303  	status := kubernetes.ComponentHealthy
   304  
   305  	accessible, err := iss.businessLayer.Tracing.GetStatus()
   306  	if !accessible {
   307  		log.Errorf("Error fetching availability of the tracing service: %v", err)
   308  		status = kubernetes.ComponentUnreachable
   309  	}
   310  
   311  	staChan <- kubernetes.IstioComponentStatus{
   312  		kubernetes.ComponentStatus{
   313  			Name:   name,
   314  			Status: status,
   315  			IsCore: isCore,
   316  		},
   317  	}
   318  }