k8s.io/apiserver@v0.31.1/pkg/quota/v1/generic/evaluator.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 generic
    18  
    19  import (
    20  	"fmt"
    21  	"sync/atomic"
    22  
    23  	corev1 "k8s.io/api/core/v1"
    24  	"k8s.io/apimachinery/pkg/api/resource"
    25  	"k8s.io/apimachinery/pkg/labels"
    26  	"k8s.io/apimachinery/pkg/runtime"
    27  	"k8s.io/apimachinery/pkg/runtime/schema"
    28  	"k8s.io/apiserver/pkg/admission"
    29  	quota "k8s.io/apiserver/pkg/quota/v1"
    30  	"k8s.io/client-go/informers"
    31  	"k8s.io/client-go/tools/cache"
    32  )
    33  
    34  // InformerForResourceFunc knows how to provision an informer
    35  type InformerForResourceFunc func(schema.GroupVersionResource) (informers.GenericInformer, error)
    36  
    37  // ListerFuncForResourceFunc knows how to provision a lister from an informer func.
    38  // The lister returns errors until the informer has synced.
    39  func ListerFuncForResourceFunc(f InformerForResourceFunc) quota.ListerForResourceFunc {
    40  	return func(gvr schema.GroupVersionResource) (cache.GenericLister, error) {
    41  		informer, err := f(gvr)
    42  		if err != nil {
    43  			return nil, err
    44  		}
    45  		return &protectedLister{
    46  			hasSynced:   cachedHasSynced(informer.Informer().HasSynced),
    47  			notReadyErr: fmt.Errorf("%v not yet synced", gvr),
    48  			delegate:    informer.Lister(),
    49  		}, nil
    50  	}
    51  }
    52  
    53  // cachedHasSynced returns a function that calls hasSynced() until it returns true once, then returns true
    54  func cachedHasSynced(hasSynced func() bool) func() bool {
    55  	cache := &atomic.Bool{}
    56  	cache.Store(false)
    57  	return func() bool {
    58  		if cache.Load() {
    59  			// short-circuit if already synced
    60  			return true
    61  		}
    62  		if hasSynced() {
    63  			// remember we synced
    64  			cache.Store(true)
    65  			return true
    66  		}
    67  		return false
    68  	}
    69  }
    70  
    71  // protectedLister returns notReadyError if hasSynced returns false, otherwise delegates to delegate
    72  type protectedLister struct {
    73  	hasSynced   func() bool
    74  	notReadyErr error
    75  	delegate    cache.GenericLister
    76  }
    77  
    78  func (p *protectedLister) List(selector labels.Selector) (ret []runtime.Object, err error) {
    79  	if !p.hasSynced() {
    80  		return nil, p.notReadyErr
    81  	}
    82  	return p.delegate.List(selector)
    83  }
    84  func (p *protectedLister) Get(name string) (runtime.Object, error) {
    85  	if !p.hasSynced() {
    86  		return nil, p.notReadyErr
    87  	}
    88  	return p.delegate.Get(name)
    89  }
    90  func (p *protectedLister) ByNamespace(namespace string) cache.GenericNamespaceLister {
    91  	return &protectedNamespaceLister{p.hasSynced, p.notReadyErr, p.delegate.ByNamespace(namespace)}
    92  }
    93  
    94  // protectedNamespaceLister returns notReadyError if hasSynced returns false, otherwise delegates to delegate
    95  type protectedNamespaceLister struct {
    96  	hasSynced   func() bool
    97  	notReadyErr error
    98  	delegate    cache.GenericNamespaceLister
    99  }
   100  
   101  func (p *protectedNamespaceLister) List(selector labels.Selector) (ret []runtime.Object, err error) {
   102  	if !p.hasSynced() {
   103  		return nil, p.notReadyErr
   104  	}
   105  	return p.delegate.List(selector)
   106  }
   107  func (p *protectedNamespaceLister) Get(name string) (runtime.Object, error) {
   108  	if !p.hasSynced() {
   109  		return nil, p.notReadyErr
   110  	}
   111  	return p.delegate.Get(name)
   112  }
   113  
   114  // ListResourceUsingListerFunc returns a listing function based on the shared informer factory for the specified resource.
   115  func ListResourceUsingListerFunc(l quota.ListerForResourceFunc, resource schema.GroupVersionResource) ListFuncByNamespace {
   116  	return func(namespace string) ([]runtime.Object, error) {
   117  		lister, err := l(resource)
   118  		if err != nil {
   119  			return nil, err
   120  		}
   121  		return lister.ByNamespace(namespace).List(labels.Everything())
   122  	}
   123  }
   124  
   125  // ObjectCountQuotaResourceNameFor returns the object count quota name for specified groupResource
   126  func ObjectCountQuotaResourceNameFor(groupResource schema.GroupResource) corev1.ResourceName {
   127  	if len(groupResource.Group) == 0 {
   128  		return corev1.ResourceName("count/" + groupResource.Resource)
   129  	}
   130  	return corev1.ResourceName("count/" + groupResource.Resource + "." + groupResource.Group)
   131  }
   132  
   133  // ListFuncByNamespace knows how to list resources in a namespace
   134  type ListFuncByNamespace func(namespace string) ([]runtime.Object, error)
   135  
   136  // MatchesScopeFunc knows how to evaluate if an object matches a scope
   137  type MatchesScopeFunc func(scope corev1.ScopedResourceSelectorRequirement, object runtime.Object) (bool, error)
   138  
   139  // UsageFunc knows how to measure usage associated with an object
   140  type UsageFunc func(object runtime.Object) (corev1.ResourceList, error)
   141  
   142  // MatchingResourceNamesFunc is a function that returns the list of resources matched
   143  type MatchingResourceNamesFunc func(input []corev1.ResourceName) []corev1.ResourceName
   144  
   145  // MatchesNoScopeFunc returns false on all match checks
   146  func MatchesNoScopeFunc(scope corev1.ScopedResourceSelectorRequirement, object runtime.Object) (bool, error) {
   147  	return false, nil
   148  }
   149  
   150  // Matches returns true if the quota matches the specified item.
   151  func Matches(
   152  	resourceQuota *corev1.ResourceQuota, item runtime.Object,
   153  	matchFunc MatchingResourceNamesFunc, scopeFunc MatchesScopeFunc) (bool, error) {
   154  	if resourceQuota == nil {
   155  		return false, fmt.Errorf("expected non-nil quota")
   156  	}
   157  	// verify the quota matches on at least one resource
   158  	matchResource := len(matchFunc(quota.ResourceNames(resourceQuota.Status.Hard))) > 0
   159  	// by default, no scopes matches all
   160  	matchScope := true
   161  	for _, scope := range getScopeSelectorsFromQuota(resourceQuota) {
   162  		innerMatch, err := scopeFunc(scope, item)
   163  		if err != nil {
   164  			return false, err
   165  		}
   166  		matchScope = matchScope && innerMatch
   167  	}
   168  	return matchResource && matchScope, nil
   169  }
   170  
   171  func getScopeSelectorsFromQuota(quota *corev1.ResourceQuota) []corev1.ScopedResourceSelectorRequirement {
   172  	selectors := []corev1.ScopedResourceSelectorRequirement{}
   173  	for _, scope := range quota.Spec.Scopes {
   174  		selectors = append(selectors, corev1.ScopedResourceSelectorRequirement{
   175  			ScopeName: scope,
   176  			Operator:  corev1.ScopeSelectorOpExists})
   177  	}
   178  	if quota.Spec.ScopeSelector != nil {
   179  		selectors = append(selectors, quota.Spec.ScopeSelector.MatchExpressions...)
   180  	}
   181  	return selectors
   182  }
   183  
   184  // CalculateUsageStats is a utility function that knows how to calculate aggregate usage.
   185  func CalculateUsageStats(options quota.UsageStatsOptions,
   186  	listFunc ListFuncByNamespace,
   187  	scopeFunc MatchesScopeFunc,
   188  	usageFunc UsageFunc) (quota.UsageStats, error) {
   189  	// default each tracked resource to zero
   190  	result := quota.UsageStats{Used: corev1.ResourceList{}}
   191  	for _, resourceName := range options.Resources {
   192  		result.Used[resourceName] = resource.Quantity{Format: resource.DecimalSI}
   193  	}
   194  	items, err := listFunc(options.Namespace)
   195  	if err != nil {
   196  		return result, fmt.Errorf("failed to list content: %v", err)
   197  	}
   198  	for _, item := range items {
   199  		// need to verify that the item matches the set of scopes
   200  		matchesScopes := true
   201  		for _, scope := range options.Scopes {
   202  			innerMatch, err := scopeFunc(corev1.ScopedResourceSelectorRequirement{ScopeName: scope, Operator: corev1.ScopeSelectorOpExists}, item)
   203  			if err != nil {
   204  				return result, nil
   205  			}
   206  			if !innerMatch {
   207  				matchesScopes = false
   208  			}
   209  		}
   210  		if options.ScopeSelector != nil {
   211  			for _, selector := range options.ScopeSelector.MatchExpressions {
   212  				innerMatch, err := scopeFunc(selector, item)
   213  				if err != nil {
   214  					return result, nil
   215  				}
   216  				matchesScopes = matchesScopes && innerMatch
   217  			}
   218  		}
   219  		// only count usage if there was a match
   220  		if matchesScopes {
   221  			usage, err := usageFunc(item)
   222  			if err != nil {
   223  				return result, err
   224  			}
   225  			result.Used = quota.Add(result.Used, usage)
   226  		}
   227  	}
   228  	return result, nil
   229  }
   230  
   231  // objectCountEvaluator provides an implementation for quota.Evaluator
   232  // that associates usage of the specified resource based on the number of items
   233  // returned by the specified listing function.
   234  type objectCountEvaluator struct {
   235  	// GroupResource that this evaluator tracks.
   236  	// It is used to construct a generic object count quota name
   237  	groupResource schema.GroupResource
   238  	// A function that knows how to list resources by namespace.
   239  	// TODO move to dynamic client in future
   240  	listFuncByNamespace ListFuncByNamespace
   241  	// Names associated with this resource in the quota for generic counting.
   242  	resourceNames []corev1.ResourceName
   243  }
   244  
   245  // Constraints returns an error if the configured resource name is not in the required set.
   246  func (o *objectCountEvaluator) Constraints(required []corev1.ResourceName, item runtime.Object) error {
   247  	// no-op for object counting
   248  	return nil
   249  }
   250  
   251  // Handles returns true if the object count evaluator needs to track this attributes.
   252  func (o *objectCountEvaluator) Handles(a admission.Attributes) bool {
   253  	operation := a.GetOperation()
   254  	return operation == admission.Create
   255  }
   256  
   257  // Matches returns true if the evaluator matches the specified quota with the provided input item
   258  func (o *objectCountEvaluator) Matches(resourceQuota *corev1.ResourceQuota, item runtime.Object) (bool, error) {
   259  	return Matches(resourceQuota, item, o.MatchingResources, MatchesNoScopeFunc)
   260  }
   261  
   262  // MatchingResources takes the input specified list of resources and returns the set of resources it matches.
   263  func (o *objectCountEvaluator) MatchingResources(input []corev1.ResourceName) []corev1.ResourceName {
   264  	return quota.Intersection(input, o.resourceNames)
   265  }
   266  
   267  // MatchingScopes takes the input specified list of scopes and input object. Returns the set of scopes resource matches.
   268  func (o *objectCountEvaluator) MatchingScopes(item runtime.Object, scopes []corev1.ScopedResourceSelectorRequirement) ([]corev1.ScopedResourceSelectorRequirement, error) {
   269  	return []corev1.ScopedResourceSelectorRequirement{}, nil
   270  }
   271  
   272  // UncoveredQuotaScopes takes the input matched scopes which are limited by configuration and the matched quota scopes.
   273  // It returns the scopes which are in limited scopes but don't have a corresponding covering quota scope
   274  func (o *objectCountEvaluator) UncoveredQuotaScopes(limitedScopes []corev1.ScopedResourceSelectorRequirement, matchedQuotaScopes []corev1.ScopedResourceSelectorRequirement) ([]corev1.ScopedResourceSelectorRequirement, error) {
   275  	return []corev1.ScopedResourceSelectorRequirement{}, nil
   276  }
   277  
   278  // Usage returns the resource usage for the specified object
   279  func (o *objectCountEvaluator) Usage(object runtime.Object) (corev1.ResourceList, error) {
   280  	quantity := resource.NewQuantity(1, resource.DecimalSI)
   281  	resourceList := corev1.ResourceList{}
   282  	for _, resourceName := range o.resourceNames {
   283  		resourceList[resourceName] = *quantity
   284  	}
   285  	return resourceList, nil
   286  }
   287  
   288  // GroupResource tracked by this evaluator
   289  func (o *objectCountEvaluator) GroupResource() schema.GroupResource {
   290  	return o.groupResource
   291  }
   292  
   293  // UsageStats calculates aggregate usage for the object.
   294  func (o *objectCountEvaluator) UsageStats(options quota.UsageStatsOptions) (quota.UsageStats, error) {
   295  	return CalculateUsageStats(options, o.listFuncByNamespace, MatchesNoScopeFunc, o.Usage)
   296  }
   297  
   298  // Verify implementation of interface at compile time.
   299  var _ quota.Evaluator = &objectCountEvaluator{}
   300  
   301  // NewObjectCountEvaluator returns an evaluator that can perform generic
   302  // object quota counting.  It allows an optional alias for backwards compatibility
   303  // purposes for the legacy object counting names in quota.  Unless its supporting
   304  // backward compatibility, alias should not be used.
   305  func NewObjectCountEvaluator(
   306  	groupResource schema.GroupResource, listFuncByNamespace ListFuncByNamespace,
   307  	alias corev1.ResourceName) quota.Evaluator {
   308  
   309  	resourceNames := []corev1.ResourceName{ObjectCountQuotaResourceNameFor(groupResource)}
   310  	if len(alias) > 0 {
   311  		resourceNames = append(resourceNames, alias)
   312  	}
   313  
   314  	return &objectCountEvaluator{
   315  		groupResource:       groupResource,
   316  		listFuncByNamespace: listFuncByNamespace,
   317  		resourceNames:       resourceNames,
   318  	}
   319  }