package k8score import ( "slices" apiruntime "k8s.io/apimachinery/pkg/runtime" "k8s.io/client-go/tools/cache" ) // kindStore is one informer's store for a kind and that informer's own sync. type kindStore struct { indexer cache.Indexer synced func() bool } func (rc *ResourceCache) addKindStore(key, namespace string, inf cache.SharedIndexInformer) { if rc.kindStores[key] == nil { rc.kindStores[key] = map[string]kindStore{} } rc.kindStores[key][namespace] = kindStore{indexer: inf.GetIndexer(), synced: inf.HasSynced} } // storeFor returns the store holding namespace's objects: that namespace's own // informer, or the one informer serving the kind's whole scope. func (rc *ResourceCache) storeFor(key, namespace string) (kindStore, bool) { stores := rc.kindStores[key] if st, ok := stores[namespace]; ok { return st, true } st, ok := stores[""] return st, ok } // pastDeadline reports a tracked kind whose deferred sync deadline fired. // Background kinds (Events) have no deadline, and a kind mid-Phase-1 is not in // the tracking map, so neither is ever past it. func (rc *ResourceCache) pastDeadline(key string) bool { if !rc.deferredFailed.Load() || rc.backgroundKeys[key] { return false } rc.deferredMu.RLock() _, tracked := rc.deferredSynced[key] rc.deferredMu.RUnlock() return tracked } func (rc *ResourceCache) storeReadiness(key string, st kindStore) KindReadiness { if st.synced() { return KindReady } if rc.pastDeadline(key) { return KindFailed } return KindPending } // KindReadAssessment contains full membership facts for one cache handle. // Requested nil means all namespaces; an empty non-nil set means no target. // Consumers must project namespaces to what the caller may see. type KindReadAssessment struct { // Readiness is the whole kind's state; it is Ready only once every // informer for the kind has synced. Readiness KindReadiness Requested []string // AllNamespaces: one informer covers every namespace, so each namespace's // state is Readiness. AllNamespaces bool // Read, Loading, Failed and Uncovered partition the target: Requested, or // for an all-namespaces request on a namespace-scoped collector, the // namespaces it collects. A namespace is Read when the store holding it is // complete now; Failed when its load missed the sync deadline. Uncovered // is known from configuration before any sync. An all-namespaces request // on an all-namespaces informer has no partition: Readiness is its state // and Read is nil. So is a kind Radar doesn't watch (Readiness // Unavailable), which has nothing to assess. Otherwise Read is non-nil // even when empty, because readers take a nil namespace list to mean // every namespace. Read, Loading, Failed, Uncovered []string // Complete: every requested namespace is readable now; an empty target is // complete. An all-namespaces request (Requested nil) is complete only on // one informer over every namespace that has loaded: a namespace-scoped // cache never collects every namespace. Complete bool } // AssessKindRead assesses an already authorized namespace target without I/O. // It describes the scope this cache can serve now, not a read that happened. func (rc *ResourceCache) AssessKindRead(resource string, requested []string) KindReadAssessment { a := KindReadAssessment{Readiness: rc.KindReadinessFor(resource), Requested: slices.Clone(requested)} if a.Readiness == KindUnavailable { return a } covered := rc.KindNamespaces(resource) if covered == nil { a.AllNamespaces = true if requested == nil { a.Complete = a.Readiness == KindReady return a } a.Read = []string{} a.assign(a.Readiness, slices.Clone(requested)...) a.Complete = len(a.Loading) == 0 && len(a.Failed) == 0 return a } a.Read = []string{} target := requested if target == nil { target = covered } isCovered := namespaceSet(covered) for _, namespace := range target { if !isCovered[namespace] { a.Uncovered = append(a.Uncovered, namespace) continue } state := a.Readiness if st, ok := rc.storeFor(resource, namespace); ok { state = rc.storeReadiness(resource, st) } a.assign(state, namespace) } a.Complete = requested != nil && len(a.Loading) == 0 && len(a.Failed) == 0 && len(a.Uncovered) == 0 return a } // TargetReadiness is the state of the requested namespaces alone: Pending // while any of them loads, Failed when one missed its load deadline and none // is loading, Ready otherwise. Other namespaces of the kind don't count, so a // target of loaded namespaces is Ready while another namespace still loads. // Uncovered namespaces don't count either; Complete covers them. Without a // partition (one informer for every namespace and no target), it is Readiness. func (a KindReadAssessment) TargetReadiness() KindReadiness { if a.Read == nil { return a.Readiness } switch { case len(a.Loading) > 0: return KindPending case len(a.Failed) > 0: return KindFailed } return KindReady } // namespaceSet indexes namespaces for membership checks: a collector's list // and a caller's target can each hold thousands. func namespaceSet(namespaces []string) map[string]bool { set := make(map[string]bool, len(namespaces)) for _, ns := range namespaces { set[ns] = true } return set } func (a *KindReadAssessment) assign(state KindReadiness, namespaces ...string) { switch state { case KindReady: a.Read = append(a.Read, namespaces...) case KindFailed: a.Failed = append(a.Failed, namespaces...) default: a.Loading = append(a.Loading, namespaces...) } } // readableIndexers returns one read per store a request needs. With nil // namespaces each read is a whole store, for the kind's whole scope; otherwise // each read selects one namespace's objects from the store holding them. It // fails unless every store involved is complete now. func (rc *ResourceCache) readableIndexers(resource string, namespaces []string) ([]func() []any, bool) { if rc == nil || !rc.isEnabled(resource) || rc.kindStores[resource] == nil { return nil, false } stores := rc.kindStores[resource] var reads []func() []any if namespaces == nil { for _, st := range stores { if !st.synced() { return nil, false } reads = append(reads, st.indexer.List) } return reads, true } covered := rc.KindNamespaces(resource) isCovered := namespaceSet(covered) for _, namespace := range namespaces { if covered != nil && !isCovered[namespace] { return nil, false } st, ok := rc.storeFor(resource, namespace) if !ok || !st.synced() { return nil, false } reads = append(reads, func() []any { items, _ := st.indexer.ByIndex(cache.NamespaceIndex, namespace) return items }) } return reads, true } // ReadableObjects lists resource in namespaces straight from the informer // stores holding them. ok is false unless each namespace's store is complete // now, so a namespace whose own informer has synced can be read while another // namespace of the kind is still loading. It never consults the kind's // whole-kind readiness, so it is for readers that assessed the scope first. func (rc *ResourceCache) ReadableObjects(resource string, namespaces []string) ([]apiruntime.Object, bool) { reads, ok := rc.readableIndexers(resource, namespaces) if !ok { return nil, false } var out []apiruntime.Object for _, read := range reads { for _, item := range read() { if obj, isObj := item.(apiruntime.Object); isObj { out = append(out, obj) } } } return out, true } // ReadableCount counts resource in namespaces under ReadableObjects' rules // without copying the objects. func (rc *ResourceCache) ReadableCount(resource string, namespaces []string) (int, bool) { reads, ok := rc.readableIndexers(resource, namespaces) if !ok { return 0, false } n := 0 for _, read := range reads { n += len(read()) } return n, true } // CachedObject looks up a collected object even before its informer syncs. // A hit proves existence; a miss does not prove absence. The returned object // is a copy. Nil or disabled caches return no hit. func (rc *ResourceCache) CachedObject(resource, namespace, name string) (apiruntime.Object, bool) { if rc == nil || !rc.isEnabled(resource) { return nil, false } if covered := rc.KindNamespaces(resource); covered != nil && !slices.Contains(covered, namespace) { return nil, false } st, ok := rc.storeFor(resource, namespace) if !ok { return nil, false } key := name if namespace != "" { key = namespace + "/" + name } item, found, err := st.indexer.GetByKey(key) if err != nil || !found { return nil, false } obj, ok := item.(apiruntime.Object) if !ok { return nil, false } return obj.DeepCopyObject(), true } // ReadableObject gets one object under ReadableObjects' rules. found reports // whether it exists; ok is false when its namespace is not readable now, in // which case a miss proves nothing. namespace is "" for a cluster-scoped kind. func (rc *ResourceCache) ReadableObject(resource, namespace, name string) (obj apiruntime.Object, found, ok bool) { if rc == nil || !rc.isEnabled(resource) || rc.kindStores[resource] == nil { return nil, false, false } if covered := rc.KindNamespaces(resource); covered != nil && !slices.Contains(covered, namespace) { return nil, false, false } st, exists := rc.storeFor(resource, namespace) if !exists || !st.synced() { return nil, false, false } key := name if namespace != "" { key = namespace + "/" + name } item, found, err := st.indexer.GetByKey(key) if err != nil || !found { return nil, false, err == nil } obj, isObj := item.(apiruntime.Object) return obj, isObj, isObj }