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)