github.com/kiali/kiali@v1.84.0/kubernetes/cache/cache.go (about)

     1  package cache
     2  
     3  import (
     4  	"context"
     5  	"fmt"
     6  	"strings"
     7  	"sync"
     8  	"time"
     9  
    10  	"golang.org/x/exp/maps"
    11  
    12  	v1 "k8s.io/api/core/v1"
    13  	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
    14  
    15  	"github.com/kiali/kiali/config"
    16  	"github.com/kiali/kiali/kubernetes"
    17  	"github.com/kiali/kiali/log"
    18  	"github.com/kiali/kiali/models"
    19  	"github.com/kiali/kiali/store"
    20  	"github.com/kiali/kiali/util"
    21  )
    22  
    23  const ambientCheckExpirationTime = 10 * time.Minute
    24  
    25  // KialiCache stores both kube objects and non-kube related data such as pods' proxy status.
    26  // It is exclusively used by the business layer where it's expected to be a singleton.
    27  // This business layer cache needs access to all the kiali service account has access
    28  // to so it uses the kiali service account token instead of a user token. Access to
    29  // the objects returned by the cache should be filtered/restricted to the user's
    30  // token access but the cache returns objects without any filtering or restrictions.
    31  type KialiCache interface {
    32  
    33  	// GetClusters returns the list of clusters that the cache knows about.
    34  	// This gets set by the mesh service.
    35  	GetClusters() []kubernetes.Cluster
    36  	GetKubeCaches() map[string]KubeCache
    37  	GetKubeCache(cluster string) (KubeCache, error)
    38  
    39  	// GetNamespace returns a namespace from the in memory cache if it exists.
    40  	GetNamespace(cluster string, token string, name string) (models.Namespace, bool)
    41  
    42  	// GetNamespaces returns all namespaces for the cluster/token from the in memory cache.
    43  	GetNamespaces(cluster string, token string) ([]models.Namespace, bool)
    44  
    45  	// Returns a list of ztunnel pods from the ztunnel daemonset
    46  	GetZtunnelPods(cluster string) []v1.Pod
    47  
    48  	// IsAmbientEnabled checks if the istio Ambient profile was enabled
    49  	// by checking if the ztunnel daemonset exists on the cluster.
    50  	IsAmbientEnabled(cluster string) bool
    51  
    52  	// RefreshTokenNamespaces clears the in memory cache of namespaces.
    53  	RefreshTokenNamespaces(cluster string)
    54  
    55  	RegistryStatusCache
    56  	ProxyStatusCache
    57  
    58  	// SetClusters sets the list of clusters that the cache knows about.
    59  	SetClusters([]kubernetes.Cluster)
    60  
    61  	// SetNamespaces sets the in memory cache of namespaces.
    62  	// We cache all namespaces for cluster + token.
    63  	SetNamespaces(token string, namespaces []models.Namespace)
    64  
    65  	// SetNamespace caches a specific namespace by cluster + token.
    66  	SetNamespace(token string, namespace models.Namespace)
    67  
    68  	// Stop stops the cache and all its kube caches.
    69  	Stop()
    70  }
    71  
    72  type kialiCacheImpl struct {
    73  	ambientChecksPerCluster store.Store[string, bool]
    74  	cleanup                 func()
    75  	conf                    config.Config
    76  
    77  	clientFactory kubernetes.ClientFactory
    78  	// Maps a cluster name to a KubeCache
    79  	kubeCache map[string]KubeCache
    80  
    81  	// Store the namespaces per token + cluster as a map[string]namespace where string is the namespace name
    82  	// so you can easily deref the namespace in GetNamespace and SetNamespace. The downside to this is that
    83  	// we need an additional lock for the namespace map that gets returned from the store to ensure it is threadsafe.
    84  	namespaceStore store.Store[namespacesKey, map[string]models.Namespace]
    85  
    86  	// Only necessary because we want to cache the namespaces per cluster and token as a map
    87  	// and maps are not thread safe. We need an additional lock on top of the Store to ensure
    88  	// that the map returned from the store is threadsafe.
    89  	namespacesLock sync.RWMutex
    90  
    91  	refreshDuration time.Duration
    92  	// ProxyStatusStore stores the proxy status and should be key'd off cluster + namespace + pod.
    93  	proxyStatusStore store.Store[string, *kubernetes.ProxyStatus]
    94  	// RegistryStatusStore stores the registry status and should be key'd off of the cluster name.
    95  	registryStatusStore store.Store[string, *kubernetes.RegistryStatus]
    96  
    97  	// Info about the kube clusters that the cache knows about.
    98  	clusters    []kubernetes.Cluster
    99  	clusterLock sync.RWMutex
   100  }
   101  
   102  func NewKialiCache(clientFactory kubernetes.ClientFactory, cfg config.Config) (KialiCache, error) {
   103  	ctx, cancel := context.WithCancel(context.Background())
   104  	namespaceKeyTTL := time.Duration(cfg.KubernetesConfig.CacheTokenNamespaceDuration) * time.Second
   105  	kialiCacheImpl := kialiCacheImpl{
   106  		ambientChecksPerCluster: store.NewExpirationStore(ctx, store.New[string, bool](), util.AsPtr(ambientCheckExpirationTime), nil),
   107  		cleanup:                 cancel,
   108  		clientFactory:           clientFactory,
   109  		conf:                    cfg,
   110  		kubeCache:               make(map[string]KubeCache),
   111  		namespaceStore:          store.NewExpirationStore(ctx, store.New[namespacesKey, map[string]models.Namespace](), &namespaceKeyTTL, nil),
   112  		refreshDuration:         time.Duration(cfg.KubernetesConfig.CacheDuration) * time.Second,
   113  		proxyStatusStore:        store.New[string, *kubernetes.ProxyStatus](),
   114  		registryStatusStore:     store.New[string, *kubernetes.RegistryStatus](),
   115  	}
   116  
   117  	for cluster, client := range clientFactory.GetSAClients() {
   118  		cache, err := NewKubeCache(client, cfg)
   119  		if err != nil {
   120  			log.Errorf("[Kiali Cache] Error creating kube cache for cluster: [%s]. Err: %v", cluster, err)
   121  			return nil, err
   122  		}
   123  		log.Infof("[Kiali Cache] Kube cache is active for cluster: [%s]", cluster)
   124  
   125  		kialiCacheImpl.kubeCache[cluster] = cache
   126  	}
   127  
   128  	// TODO: Treat all clusters the same way.
   129  	// Ensure home client got set.
   130  	if _, found := kialiCacheImpl.kubeCache[cfg.KubernetesConfig.ClusterName]; !found {
   131  		return nil, fmt.Errorf("home cluster not configured in kiali cache")
   132  	}
   133  
   134  	return &kialiCacheImpl, nil
   135  }
   136  
   137  // GetKubeCaches returns a kube cache for every configured Kiali Service Account client keyed by cluster name.
   138  func (c *kialiCacheImpl) GetKubeCaches() map[string]KubeCache {
   139  	return c.kubeCache
   140  }
   141  
   142  func (c *kialiCacheImpl) GetKubeCache(cluster string) (KubeCache, error) {
   143  	cache, found := c.kubeCache[cluster]
   144  	if !found {
   145  		// This should not happen but it probably means the user clients have clusters that the cache doesn't know about.
   146  		return nil, fmt.Errorf("cache for cluster [%s] not found", cluster)
   147  	}
   148  	return cache, nil
   149  }
   150  
   151  // Stops all caches across all clusters.
   152  func (c *kialiCacheImpl) Stop() {
   153  	log.Infof("Stopping Kiali Cache")
   154  
   155  	wg := sync.WaitGroup{}
   156  	for _, kc := range c.kubeCache {
   157  		wg.Add(1)
   158  		go func(c KubeCache) {
   159  			defer wg.Done()
   160  			c.Stop()
   161  		}(kc)
   162  	}
   163  	wg.Wait()
   164  }
   165  
   166  func (c *kialiCacheImpl) GetClusters() []kubernetes.Cluster {
   167  	defer c.clusterLock.RUnlock()
   168  	c.clusterLock.RLock()
   169  	return c.clusters
   170  }
   171  
   172  func (c *kialiCacheImpl) SetClusters(clusters []kubernetes.Cluster) {
   173  	defer c.clusterLock.Unlock()
   174  	c.clusterLock.Lock()
   175  	c.clusters = clusters
   176  }
   177  
   178  // IsAmbientEnabled checks if the istio Ambient profile was enabled
   179  // by checking if the ztunnel daemonset exists on the cluster.
   180  func (in *kialiCacheImpl) IsAmbientEnabled(cluster string) bool {
   181  	check, found := in.ambientChecksPerCluster.Get(cluster)
   182  	if !found {
   183  		kubeCache, err := in.GetKubeCache(cluster)
   184  		if err != nil {
   185  			log.Debugf("Unable to get kube cache when checking for ambient profile: %s", err)
   186  			return false
   187  		}
   188  
   189  		selector := map[string]string{
   190  			"app": "ztunnel",
   191  		}
   192  		daemonsets, err := kubeCache.GetDaemonSetsWithSelector(metav1.NamespaceAll, selector)
   193  		if err != nil {
   194  			// Don't set the check so we will check again the next time since this error may be transient.
   195  			log.Debugf("Error checking for ztunnel in Kiali accessible namespaces in cluster '%s': %s", cluster, err.Error())
   196  			return false
   197  		}
   198  
   199  		if len(daemonsets) == 0 {
   200  			log.Debugf("No ztunnel daemonsets found in Kiali accessible namespaces in cluster '%s'", cluster)
   201  			in.ambientChecksPerCluster.Set(cluster, false)
   202  			return false
   203  		}
   204  
   205  		in.ambientChecksPerCluster.Set(cluster, true)
   206  		return true
   207  	}
   208  
   209  	return check
   210  }
   211  
   212  // GetZtunnelPods returns the pods list from ztunnel daemonset
   213  func (in *kialiCacheImpl) GetZtunnelPods(cluster string) []v1.Pod {
   214  
   215  	ztunnelPods := []v1.Pod{}
   216  	kubeCache, err := in.GetKubeCache(cluster)
   217  	if err != nil {
   218  		log.Debugf("Unable to get kube cache when checking for ambient profile: %s", err)
   219  		return ztunnelPods
   220  
   221  	}
   222  	selector := map[string]string{
   223  		"app": "ztunnel",
   224  	}
   225  	daemonsets, err := kubeCache.GetDaemonSetsWithSelector(metav1.NamespaceAll, selector)
   226  	if err != nil {
   227  		// Don't set the check so we will check again the next time since this error may be transient.
   228  		log.Debugf("Error checking for ztunnel in Kiali accessible namespaces in cluster '%s': %s", cluster, err.Error())
   229  		return ztunnelPods
   230  	}
   231  
   232  	if len(daemonsets) == 0 {
   233  		log.Debugf("No ztunnel daemonsets found in Kiali accessible namespaces in cluster '%s'", cluster)
   234  		return ztunnelPods
   235  	}
   236  
   237  	dsPods, err := kubeCache.GetPods(daemonsets[0].Namespace, "")
   238  	if err != nil {
   239  		log.Errorf("Unable to get ztunnel pods: %s", err)
   240  		return ztunnelPods
   241  
   242  	}
   243  
   244  	for _, pod := range dsPods {
   245  		if strings.Contains(pod.Name, "ztunnel") {
   246  			ztunnelPods = append(ztunnelPods, pod)
   247  		}
   248  	}
   249  
   250  	return ztunnelPods
   251  }
   252  
   253  type namespacesKey struct {
   254  	cluster string
   255  	token   string
   256  }
   257  
   258  func (c *kialiCacheImpl) GetNamespace(cluster string, token string, namespace string) (models.Namespace, bool) {
   259  	c.namespacesLock.RLock()
   260  	defer c.namespacesLock.RUnlock()
   261  
   262  	key := namespacesKey{cluster: cluster, token: token}
   263  	namespaces, found := c.namespaceStore.Get(key)
   264  	if !found {
   265  		return models.Namespace{}, false
   266  	}
   267  
   268  	ns, found := namespaces[namespace]
   269  	return ns, found
   270  }
   271  
   272  func (c *kialiCacheImpl) GetNamespaces(cluster string, token string) ([]models.Namespace, bool) {
   273  	c.namespacesLock.RLock()
   274  	defer c.namespacesLock.RUnlock()
   275  
   276  	key := namespacesKey{cluster: cluster, token: token}
   277  	namespaces, found := c.namespaceStore.Get(key)
   278  
   279  	return maps.Values(namespaces), found
   280  }
   281  
   282  func (c *kialiCacheImpl) RefreshTokenNamespaces(cluster string) {
   283  	c.namespacesLock.Lock()
   284  	defer c.namespacesLock.Unlock()
   285  
   286  	for _, key := range c.namespaceStore.Keys() {
   287  		if key.cluster == cluster {
   288  			c.namespaceStore.Remove(key)
   289  		}
   290  	}
   291  }
   292  
   293  func (c *kialiCacheImpl) SetNamespaces(token string, namespaces []models.Namespace) {
   294  	c.namespacesLock.Lock()
   295  	defer c.namespacesLock.Unlock()
   296  
   297  	namespacesByCluster := make(map[string][]models.Namespace)
   298  	for _, namespace := range namespaces {
   299  		namespacesByCluster[namespace.Cluster] = append(namespacesByCluster[namespace.Cluster], namespace)
   300  	}
   301  
   302  	for cluster, clusterNamespaces := range namespacesByCluster {
   303  		key := namespacesKey{cluster: cluster, token: token}
   304  		ns := make(map[string]models.Namespace)
   305  		for _, namespace := range clusterNamespaces {
   306  			ns[namespace.Name] = namespace
   307  		}
   308  		c.namespaceStore.Set(key, ns)
   309  	}
   310  }
   311  
   312  func (c *kialiCacheImpl) SetNamespace(token string, namespace models.Namespace) {
   313  	c.namespacesLock.Lock()
   314  	defer c.namespacesLock.Unlock()
   315  
   316  	key := namespacesKey{cluster: namespace.Cluster, token: token}
   317  	ns, found := c.namespaceStore.Get(key)
   318  	if !found {
   319  		ns = make(map[string]models.Namespace)
   320  	}
   321  
   322  	ns[namespace.Name] = namespace
   323  	c.namespaceStore.Set(key, ns)
   324  }
   325  
   326  // Interface guard for kiali cache impl
   327  var _ KialiCache = (*kialiCacheImpl)(nil)