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 &quotaAccessor{
    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  }