1/*
2Copyright 2016 The Kubernetes Authors.
3
4Licensed under the Apache License, Version 2.0 (the "License");
5you may not use this file except in compliance with the License.
6You may obtain a copy of the License at
7
8    http://www.apache.org/licenses/LICENSE-2.0
9
10Unless required by applicable law or agreed to in writing, software
11distributed under the License is distributed on an "AS IS" BASIS,
12WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13See the License for the specific language governing permissions and
14limitations under the License.
15*/
16
17package generic
18
19import (
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
35type 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.
39func 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
54func cachedHasSynced(hasSynced func() bool) func() bool {
55	cache := &atomic.Value{}
56	cache.Store(false)
57	return func() bool {
58		if cache.Load().(bool) {
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
72type protectedLister struct {
73	hasSynced   func() bool
74	notReadyErr error
75	delegate    cache.GenericLister
76}
77
78func (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}
84func (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}
90func (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
95type protectedNamespaceLister struct {
96	hasSynced   func() bool
97	notReadyErr error
98	delegate    cache.GenericNamespaceLister
99}
100
101func (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}
107func (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.
115func 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
126func 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
134type ListFuncByNamespace func(namespace string) ([]runtime.Object, error)
135
136// MatchesScopeFunc knows how to evaluate if an object matches a scope
137type MatchesScopeFunc func(scope corev1.ScopedResourceSelectorRequirement, object runtime.Object) (bool, error)
138
139// UsageFunc knows how to measure usage associated with an object
140type UsageFunc func(object runtime.Object) (corev1.ResourceList, error)
141
142// MatchingResourceNamesFunc is a function that returns the list of resources matched
143type MatchingResourceNamesFunc func(input []corev1.ResourceName) []corev1.ResourceName
144
145// MatchesNoScopeFunc returns false on all match checks
146func 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.
151func 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
171func 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.
185func 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}, 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.
234type 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.
246func (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.
252func (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
258func (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.
263func (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.
268func (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
274func (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
279func (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
289func (o *objectCountEvaluator) GroupResource() schema.GroupResource {
290	return o.groupResource
291}
292
293// UsageStats calculates aggregate usage for the object.
294func (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.
299var _ 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.
305func 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}
320