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 }