k8s.io/apiserver@v0.31.1/pkg/admission/plugin/resourcequota/resource_access.go (about) 1 /* 2 Copyright 2016 The Kubernetes Authors. 3 4 Licensed under the Apache License, Version 2.0 (the "License"); 5 you may not use this file except in compliance with the License. 6 You may obtain a copy of the License at 7 8 http://www.apache.org/licenses/LICENSE-2.0 9 10 Unless required by applicable law or agreed to in writing, software 11 distributed under the License is distributed on an "AS IS" BASIS, 12 WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 See the License for the specific language governing permissions and 14 limitations under the License. 15 */ 16 17 package resourcequota 18 19 import ( 20 "context" 21 "fmt" 22 "time" 23 24 "golang.org/x/sync/singleflight" 25 corev1 "k8s.io/api/core/v1" 26 metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 27 "k8s.io/apimachinery/pkg/labels" 28 "k8s.io/apiserver/pkg/storage" 29 "k8s.io/client-go/kubernetes" 30 corev1listers "k8s.io/client-go/listers/core/v1" 31 "k8s.io/utils/lru" 32 ) 33 34 // QuotaAccessor abstracts the get/set logic from the rest of the Evaluator. This could be a test stub, a straight passthrough, 35 // or most commonly a series of deconflicting caches. 36 type QuotaAccessor interface { 37 // UpdateQuotaStatus is called to persist final status. This method should write to persistent storage. 38 // An error indicates that write didn't complete successfully. 39 UpdateQuotaStatus(newQuota *corev1.ResourceQuota) error 40 41 // GetQuotas gets all possible quotas for a given namespace 42 GetQuotas(namespace string) ([]corev1.ResourceQuota, error) 43 } 44 45 type quotaAccessor struct { 46 client kubernetes.Interface 47 48 // lister can list/get quota objects from a shared informer's cache 49 lister corev1listers.ResourceQuotaLister 50 51 // liveLookups holds the last few live lookups we've done to help ammortize cost on repeated lookup failures. 52 // This lets us handle the case of latent caches, by looking up actual results for a namespace on cache miss/no results. 53 // We track the lookup result here so that for repeated requests, we don't look it up very often. 54 liveLookupCache *lru.Cache 55 group singleflight.Group 56 liveTTL time.Duration 57 // updatedQuotas holds a cache of quotas that we've updated. This is used to pull the "really latest" during back to 58 // back quota evaluations that touch the same quota doc. This only works because we can compare etcd resourceVersions 59 // for the same resource as integers. Before this change: 22 updates with 12 conflicts. after this change: 15 updates with 0 conflicts 60 updatedQuotas *lru.Cache 61 } 62 63 // newQuotaAccessor creates an object that conforms to the QuotaAccessor interface to be used to retrieve quota objects. 64 func newQuotaAccessor() (*quotaAccessor, error) { 65 liveLookupCache := lru.New(100) 66 updatedCache := lru.New(100) 67 68 // client and lister will be set when SetInternalKubeClientSet and SetInternalKubeInformerFactory are invoked 69 return "aAccessor{ 70 liveLookupCache: liveLookupCache, 71 liveTTL: time.Duration(30 * time.Second), 72 updatedQuotas: updatedCache, 73 }, nil 74 } 75 76 func (e *quotaAccessor) UpdateQuotaStatus(newQuota *corev1.ResourceQuota) error { 77 updatedQuota, err := e.client.CoreV1().ResourceQuotas(newQuota.Namespace).UpdateStatus(context.TODO(), newQuota, metav1.UpdateOptions{}) 78 if err != nil { 79 return err 80 } 81 82 key := newQuota.Namespace + "/" + newQuota.Name 83 e.updatedQuotas.Add(key, updatedQuota) 84 return nil 85 } 86 87 var etcdVersioner = storage.APIObjectVersioner{} 88 89 // checkCache compares the passed quota against the value in the look-aside cache and returns the newer 90 // if the cache is out of date, it deletes the stale entry. This only works because of etcd resourceVersions 91 // being monotonically increasing integers 92 func (e *quotaAccessor) checkCache(quota *corev1.ResourceQuota) *corev1.ResourceQuota { 93 key := quota.Namespace + "/" + quota.Name 94 uncastCachedQuota, ok := e.updatedQuotas.Get(key) 95 if !ok { 96 return quota 97 } 98 cachedQuota := uncastCachedQuota.(*corev1.ResourceQuota) 99 100 if etcdVersioner.CompareResourceVersion(quota, cachedQuota) >= 0 { 101 e.updatedQuotas.Remove(key) 102 return quota 103 } 104 return cachedQuota 105 } 106 107 func (e *quotaAccessor) GetQuotas(namespace string) ([]corev1.ResourceQuota, error) { 108 // determine if there are any quotas in this namespace 109 // if there are no quotas, we don't need to do anything 110 items, err := e.lister.ResourceQuotas(namespace).List(labels.Everything()) 111 if err != nil { 112 return nil, fmt.Errorf("error resolving quota: %v", err) 113 } 114 115 // if there are no items held in our indexer, check our live-lookup LRU, if that misses, do the live lookup to prime it. 116 if len(items) == 0 { 117 lruItemObj, ok := e.liveLookupCache.Get(namespace) 118 if !ok || lruItemObj.(liveLookupEntry).expiry.Before(time.Now()) { 119 // use singleflight.Group to avoid flooding the apiserver with repeated 120 // requests. See #22422 for details. 121 lruItemObj, err, _ = e.group.Do(namespace, func() (interface{}, error) { 122 liveList, err := e.client.CoreV1().ResourceQuotas(namespace).List(context.TODO(), metav1.ListOptions{}) 123 if err != nil { 124 return nil, err 125 } 126 newEntry := liveLookupEntry{expiry: time.Now().Add(e.liveTTL)} 127 for i := range liveList.Items { 128 newEntry.items = append(newEntry.items, &liveList.Items[i]) 129 } 130 e.liveLookupCache.Add(namespace, newEntry) 131 return newEntry, nil 132 }) 133 if err != nil { 134 return nil, err 135 } 136 } 137 lruEntry := lruItemObj.(liveLookupEntry) 138 for i := range lruEntry.items { 139 items = append(items, lruEntry.items[i]) 140 } 141 } 142 143 resourceQuotas := []corev1.ResourceQuota{} 144 for i := range items { 145 quota := items[i] 146 quota = e.checkCache(quota) 147 // always make a copy. We're going to muck around with this and we should never mutate the originals 148 resourceQuotas = append(resourceQuotas, *quota) 149 } 150 151 return resourceQuotas, nil 152 }