Commit b72c006e authored by Kubernetes Submit Queue's avatar Kubernetes Submit Queue Committed by GitHub

Merge pull request #34554 from derekwaynecarr/quota-storage-class

Automatic merge from submit-queue (batch tested with PRs 37270, 38309, 37568, 34554) Ability to quota storage by storage class Adds the ability to quota storage by storage class. 1. `<storage-class>.storageclass.storage.k8s.io/persistentvolumeclaims` - quota the number of claims with a specific storage class 2. `<storage-class>.storageclass.storage.k8s.io/requests.storage` - quota the cumulative request for storage in a particular storage class. For example: ``` $ cat quota.yaml apiVersion: v1 kind: ResourceQuota metadata: name: storage-quota spec: hard: requests.storage: 100Gi persistentvolumeclaims: 100 gold.storageclass.storage.k8s.io/requests.storage: 50Gi gold.storageclass.storage.k8s.io/persistentvolumeclaims: 5 silver.storageclass.storage.k8s.io/requests.storage: 75Gi silver.storageclass.storage.k8s.io/persistentvolumeclaims: 10 bronze.storageclass.storage.k8s.io.kubernetes.io/requests.storage: 100Gi bronze.storageclass.storage.k8s.io/persistentvolumeclaims: 15 $ kubectl create -f quota.yaml $ cat pvc-bronze.yaml kind: PersistentVolumeClaim apiVersion: v1 metadata: generateName: pvc-bronze- annotations: volume.beta.kubernetes.io/storage-class: "bronze" spec: accessModes: - ReadWriteOnce resources: requests: storage: 8Gi $ kubectl create -f pvc-bronze.yaml $ kubectl get quota storage-quota -o yaml apiVersion: v1 kind: ResourceQuota ... status: hard: bronze.storageclass.storage.k8s.io/persistentvolumeclaims: "15" bronze.storageclass.storage.k8s.io/requests.storage: 100Gi gold.storageclass.storage.k8s.io/persistentvolumeclaims: "5" gold.storageclass.storage.k8s.io/requests.storage: 50Gi persistentvolumeclaims: "100" requests.storage: 100Gi silver.storageclass.storage.k8s.io/persistentvolumeclaims: "10" silver.storageclass.storage.k8s.io/requests.storage: 75Gi used: bronze.storageclass.storage.k8s.io/persistentvolumeclaims: "1" bronze.storageclass.storage.k8s.io/requests.storage: 8Gi gold.storageclass.storage.k8s.io/persistentvolumeclaims: "0" gold.storageclass.storage.k8s.io/requests.storage: "0" persistentvolumeclaims: "1" requests.storage: 8Gi silver.storageclass.storage.k8s.io/persistentvolumeclaims: "0" silver.storageclass.storage.k8s.io/requests.storage: "0" ```
parents ac05e713 459a7a05
...@@ -197,6 +197,7 @@ pkg/kubelet/volumemanager/populator ...@@ -197,6 +197,7 @@ pkg/kubelet/volumemanager/populator
pkg/kubelet/volumemanager/reconciler pkg/kubelet/volumemanager/reconciler
pkg/proxy/config pkg/proxy/config
pkg/proxy/healthcheck pkg/proxy/healthcheck
pkg/quota
pkg/quota/install pkg/quota/install
pkg/registry pkg/registry
pkg/registry/authorization/util pkg/registry/authorization/util
......
...@@ -182,9 +182,8 @@ func (rq *ResourceQuotaController) addQuota(obj interface{}) { ...@@ -182,9 +182,8 @@ func (rq *ResourceQuotaController) addQuota(obj interface{}) {
for constraint := range resourceQuota.Status.Hard { for constraint := range resourceQuota.Status.Hard {
if _, usageFound := resourceQuota.Status.Used[constraint]; !usageFound { if _, usageFound := resourceQuota.Status.Used[constraint]; !usageFound {
matchedResources := []api.ResourceName{api.ResourceName(constraint)} matchedResources := []api.ResourceName{api.ResourceName(constraint)}
for _, evaluator := range rq.registry.Evaluators() { for _, evaluator := range rq.registry.Evaluators() {
if intersection := quota.Intersection(evaluator.MatchesResources(), matchedResources); len(intersection) != 0 { if intersection := evaluator.MatchingResources(matchedResources); len(intersection) > 0 {
rq.missingUsageQueue.Add(key) rq.missingUsageQueue.Add(key)
return return
} }
...@@ -348,7 +347,6 @@ func (rq *ResourceQuotaController) replenishQuota(groupKind schema.GroupKind, na ...@@ -348,7 +347,6 @@ func (rq *ResourceQuotaController) replenishQuota(groupKind schema.GroupKind, na
} }
// only queue those quotas that are tracking a resource associated with this kind. // only queue those quotas that are tracking a resource associated with this kind.
matchedResources := evaluator.MatchesResources()
for i := range resourceQuotas { for i := range resourceQuotas {
resourceQuota := resourceQuotas[i].(*v1.ResourceQuota) resourceQuota := resourceQuotas[i].(*v1.ResourceQuota)
internalResourceQuota := &api.ResourceQuota{} internalResourceQuota := &api.ResourceQuota{}
...@@ -357,7 +355,7 @@ func (rq *ResourceQuotaController) replenishQuota(groupKind schema.GroupKind, na ...@@ -357,7 +355,7 @@ func (rq *ResourceQuotaController) replenishQuota(groupKind schema.GroupKind, na
continue continue
} }
resourceQuotaResources := quota.ResourceNames(internalResourceQuota.Status.Hard) resourceQuotaResources := quota.ResourceNames(internalResourceQuota.Status.Hard)
if len(quota.Intersection(matchedResources, resourceQuotaResources)) > 0 { if intersection := evaluator.MatchingResources(resourceQuotaResources); len(intersection) > 0 {
// TODO: make this support targeted replenishment to a specific kind, right now it does a full recalc on that quota. // TODO: make this support targeted replenishment to a specific kind, right now it does a full recalc on that quota.
rq.enqueueResourceQuota(resourceQuota) rq.enqueueResourceQuota(resourceQuota)
} }
......
...@@ -533,12 +533,18 @@ func memory(stats statsFunc) cmpFunc { ...@@ -533,12 +533,18 @@ func memory(stats statsFunc) cmpFunc {
// adjust p1, p2 usage relative to the request (if any) // adjust p1, p2 usage relative to the request (if any)
p1Memory := p1Usage[v1.ResourceMemory] p1Memory := p1Usage[v1.ResourceMemory]
p1Spec := core.PodUsageFunc(p1) p1Spec, err := core.PodUsageFunc(p1)
if err != nil {
return -1
}
p1Request := p1Spec[api.ResourceRequestsMemory] p1Request := p1Spec[api.ResourceRequestsMemory]
p1Memory.Sub(p1Request) p1Memory.Sub(p1Request)
p2Memory := p2Usage[v1.ResourceMemory] p2Memory := p2Usage[v1.ResourceMemory]
p2Spec := core.PodUsageFunc(p2) p2Spec, err := core.PodUsageFunc(p2)
if err != nil {
return 1
}
p2Request := p2Spec[api.ResourceRequestsMemory] p2Request := p2Spec[api.ResourceRequestsMemory]
p2Memory.Sub(p2Request) p2Memory.Sub(p2Request)
......
...@@ -30,7 +30,7 @@ go_library( ...@@ -30,7 +30,7 @@ go_library(
"//pkg/api/resource:go_default_library", "//pkg/api/resource:go_default_library",
"//pkg/api/v1:go_default_library", "//pkg/api/v1:go_default_library",
"//pkg/api/validation:go_default_library", "//pkg/api/validation:go_default_library",
"//pkg/apis/meta/v1:go_default_library", "//pkg/apis/storage/util:go_default_library",
"//pkg/client/clientset_generated/release_1_5:go_default_library", "//pkg/client/clientset_generated/release_1_5:go_default_library",
"//pkg/controller/informers:go_default_library", "//pkg/controller/informers:go_default_library",
"//pkg/kubelet/qos:go_default_library", "//pkg/kubelet/qos:go_default_library",
...@@ -56,6 +56,7 @@ go_test( ...@@ -56,6 +56,7 @@ go_test(
"//pkg/api:go_default_library", "//pkg/api:go_default_library",
"//pkg/api/resource:go_default_library", "//pkg/api/resource:go_default_library",
"//pkg/apis/meta/v1:go_default_library", "//pkg/apis/meta/v1:go_default_library",
"//pkg/apis/storage/util:go_default_library",
"//pkg/client/clientset_generated/release_1_5/fake:go_default_library", "//pkg/client/clientset_generated/release_1_5/fake:go_default_library",
"//pkg/quota:go_default_library", "//pkg/quota:go_default_library",
], ],
......
...@@ -17,7 +17,6 @@ limitations under the License. ...@@ -17,7 +17,6 @@ limitations under the License.
package core package core
import ( import (
"k8s.io/kubernetes/pkg/admission"
"k8s.io/kubernetes/pkg/api" "k8s.io/kubernetes/pkg/api"
"k8s.io/kubernetes/pkg/api/v1" "k8s.io/kubernetes/pkg/api/v1"
clientset "k8s.io/kubernetes/pkg/client/clientset_generated/release_1_5" clientset "k8s.io/kubernetes/pkg/client/clientset_generated/release_1_5"
...@@ -28,17 +27,10 @@ import ( ...@@ -28,17 +27,10 @@ import (
// NewConfigMapEvaluator returns an evaluator that can evaluate configMaps // NewConfigMapEvaluator returns an evaluator that can evaluate configMaps
func NewConfigMapEvaluator(kubeClient clientset.Interface) quota.Evaluator { func NewConfigMapEvaluator(kubeClient clientset.Interface) quota.Evaluator {
allResources := []api.ResourceName{api.ResourceConfigMaps} return &generic.ObjectCountEvaluator{
return &generic.GenericEvaluator{ AllowCreateOnUpdate: false,
Name: "Evaluator.ConfigMap", InternalGroupKind: api.Kind("ConfigMap"),
InternalGroupKind: api.Kind("ConfigMap"), ResourceName: api.ResourceConfigMaps,
InternalOperationResources: map[admission.Operation][]api.ResourceName{
admission.Create: allResources,
},
MatchedResourceNames: allResources,
MatchesScopeFunc: generic.MatchesNoScopeFunc,
ConstraintsFunc: generic.ObjectCountConstraintsFunc(api.ResourceConfigMaps),
UsageFunc: generic.ObjectCountUsageFunc(api.ResourceConfigMaps),
ListFuncByNamespace: func(namespace string, options v1.ListOptions) ([]runtime.Object, error) { ListFuncByNamespace: func(namespace string, options v1.ListOptions) ([]runtime.Object, error) {
itemList, err := kubeClient.Core().ConfigMaps(namespace).List(options) itemList, err := kubeClient.Core().ConfigMaps(namespace).List(options)
if err != nil { if err != nil {
......
...@@ -24,6 +24,7 @@ import ( ...@@ -24,6 +24,7 @@ import (
"k8s.io/kubernetes/pkg/api" "k8s.io/kubernetes/pkg/api"
"k8s.io/kubernetes/pkg/api/resource" "k8s.io/kubernetes/pkg/api/resource"
"k8s.io/kubernetes/pkg/api/v1" "k8s.io/kubernetes/pkg/api/v1"
"k8s.io/kubernetes/pkg/apis/storage/util"
clientset "k8s.io/kubernetes/pkg/client/clientset_generated/release_1_5" clientset "k8s.io/kubernetes/pkg/client/clientset_generated/release_1_5"
"k8s.io/kubernetes/pkg/controller/informers" "k8s.io/kubernetes/pkg/controller/informers"
"k8s.io/kubernetes/pkg/quota" "k8s.io/kubernetes/pkg/quota"
...@@ -33,6 +34,31 @@ import ( ...@@ -33,6 +34,31 @@ import (
"k8s.io/kubernetes/pkg/util/sets" "k8s.io/kubernetes/pkg/util/sets"
) )
// pvcResources are the set of static resources managed by quota associated with pvcs.
// for each resouce in this list, it may be refined dynamically based on storage class.
var pvcResources = []api.ResourceName{
api.ResourcePersistentVolumeClaims,
api.ResourceRequestsStorage,
}
// storageClassSuffix is the suffix to the qualified portion of storage class resource name.
// For example, if you want to quota storage by storage class, you would have a declaration
// that follows <storage-class>.storageclass.storage.k8s.io/<resource>.
// For example:
// * gold.storageclass.storage.k8s.io/: 500Gi
// * bronze.storageclass.storage.k8s.io/requests.storage: 500Gi
const storageClassSuffix string = ".storageclass.storage.k8s.io/"
// ResourceByStorageClass returns a quota resource name by storage class.
func ResourceByStorageClass(storageClass string, resourceName api.ResourceName) api.ResourceName {
return api.ResourceName(string(storageClass + storageClassSuffix + string(resourceName)))
}
// V1ResourceByStorageClass returns a quota resource name by storage class.
func V1ResourceByStorageClass(storageClass string, resourceName v1.ResourceName) v1.ResourceName {
return v1.ResourceName(string(storageClass + storageClassSuffix + string(resourceName)))
}
// listPersistentVolumeClaimsByNamespaceFuncUsingClient returns a pvc listing function based on the provided client. // listPersistentVolumeClaimsByNamespaceFuncUsingClient returns a pvc listing function based on the provided client.
func listPersistentVolumeClaimsByNamespaceFuncUsingClient(kubeClient clientset.Interface) generic.ListFuncByNamespace { func listPersistentVolumeClaimsByNamespaceFuncUsingClient(kubeClient clientset.Interface) generic.ListFuncByNamespace {
// TODO: ideally, we could pass dynamic client pool down into this code, and have one way of doing this. // TODO: ideally, we could pass dynamic client pool down into this code, and have one way of doing this.
...@@ -54,59 +80,49 @@ func listPersistentVolumeClaimsByNamespaceFuncUsingClient(kubeClient clientset.I ...@@ -54,59 +80,49 @@ func listPersistentVolumeClaimsByNamespaceFuncUsingClient(kubeClient clientset.I
// NewPersistentVolumeClaimEvaluator returns an evaluator that can evaluate persistent volume claims // NewPersistentVolumeClaimEvaluator returns an evaluator that can evaluate persistent volume claims
// if the specified shared informer factory is not nil, evaluator may use it to support listing functions. // if the specified shared informer factory is not nil, evaluator may use it to support listing functions.
func NewPersistentVolumeClaimEvaluator(kubeClient clientset.Interface, f informers.SharedInformerFactory) quota.Evaluator { func NewPersistentVolumeClaimEvaluator(kubeClient clientset.Interface, f informers.SharedInformerFactory) quota.Evaluator {
allResources := []api.ResourceName{api.ResourcePersistentVolumeClaims, api.ResourceRequestsStorage}
listFuncByNamespace := listPersistentVolumeClaimsByNamespaceFuncUsingClient(kubeClient) listFuncByNamespace := listPersistentVolumeClaimsByNamespaceFuncUsingClient(kubeClient)
if f != nil { if f != nil {
listFuncByNamespace = generic.ListResourceUsingInformerFunc(f, schema.GroupResource{Resource: "persistentvolumeclaims"}) listFuncByNamespace = generic.ListResourceUsingInformerFunc(f, schema.GroupResource{Resource: "persistentvolumeclaims"})
} }
return &pvcEvaluator{
return &generic.GenericEvaluator{ listFuncByNamespace: listFuncByNamespace,
Name: "Evaluator.PersistentVolumeClaim",
InternalGroupKind: api.Kind("PersistentVolumeClaim"),
InternalOperationResources: map[admission.Operation][]api.ResourceName{
admission.Create: allResources,
},
MatchedResourceNames: allResources,
MatchesScopeFunc: generic.MatchesNoScopeFunc,
ConstraintsFunc: PersistentVolumeClaimConstraintsFunc,
UsageFunc: PersistentVolumeClaimUsageFunc,
ListFuncByNamespace: listFuncByNamespace,
} }
} }
// PersistentVolumeClaimUsageFunc knows how to measure usage associated with persistent volume claims // pvcEvaluator knows how to evaluate quota usage for persistent volume claims
func PersistentVolumeClaimUsageFunc(object runtime.Object) api.ResourceList { type pvcEvaluator struct {
result := api.ResourceList{} // listFuncByNamespace knows how to list pvc claims
var found bool listFuncByNamespace generic.ListFuncByNamespace
var request resource.Quantity }
switch t := object.(type) { // Constraints verifies that all required resources are present on the item.
case *v1.PersistentVolumeClaim: func (p *pvcEvaluator) Constraints(required []api.ResourceName, item runtime.Object) error {
request, found = t.Spec.Resources.Requests[v1.ResourceStorage] pvc, ok := item.(*api.PersistentVolumeClaim)
case *api.PersistentVolumeClaim: if !ok {
request, found = t.Spec.Resources.Requests[api.ResourceStorage] return fmt.Errorf("unexpected input object %v", item)
default:
panic(fmt.Sprintf("expect *api.PersistenVolumeClaim or *v1.PersistentVolumeClaim, got %v", t))
} }
result[api.ResourcePersistentVolumeClaims] = resource.MustParse("1") // these are the items that we will be handling based on the objects actual storage-class
if found { pvcRequiredSet := append([]api.ResourceName{}, pvcResources...)
result[api.ResourceRequestsStorage] = request if storageClassRef := util.GetClaimStorageClass(pvc); len(storageClassRef) > 0 {
pvcRequiredSet = append(pvcRequiredSet, ResourceByStorageClass(storageClassRef, api.ResourcePersistentVolumeClaims))
pvcRequiredSet = append(pvcRequiredSet, ResourceByStorageClass(storageClassRef, api.ResourceRequestsStorage))
} }
return result
}
// PersistentVolumeClaimConstraintsFunc verifies that all required resources are present on the claim // in effect, this will remove things from the required set that are not tied to this pvcs storage class
// In addition, it validates that the resources are valid (i.e. requests < limits) // for example, if a quota has bronze and gold storage class items defined, we should not error a bronze pvc for not being gold.
func PersistentVolumeClaimConstraintsFunc(required []api.ResourceName, object runtime.Object) error { // but we should error a bronze pvc if it doesn't make a storage request size...
pvc, ok := object.(*api.PersistentVolumeClaim) requiredResources := quota.Intersection(required, pvcRequiredSet)
if !ok { requiredSet := quota.ToSet(requiredResources)
return fmt.Errorf("unexpected input object %v", object)
// usage for this pvc will only include global pvc items + this storage class specific items
pvcUsage, err := p.Usage(item)
if err != nil {
return err
} }
requiredSet := quota.ToSet(required) // determine what required resources were not tracked by usage.
missingSet := sets.NewString() missingSet := sets.NewString()
pvcUsage := PersistentVolumeClaimUsageFunc(pvc)
pvcSet := quota.ToSet(quota.ResourceNames(pvcUsage)) pvcSet := quota.ToSet(quota.ResourceNames(pvcUsage))
if diff := requiredSet.Difference(pvcSet); len(diff) > 0 { if diff := requiredSet.Difference(pvcSet); len(diff) > 0 {
missingSet.Insert(diff.List()...) missingSet.Insert(diff.List()...)
...@@ -116,3 +132,89 @@ func PersistentVolumeClaimConstraintsFunc(required []api.ResourceName, object ru ...@@ -116,3 +132,89 @@ func PersistentVolumeClaimConstraintsFunc(required []api.ResourceName, object ru
} }
return fmt.Errorf("must specify %s", strings.Join(missingSet.List(), ",")) return fmt.Errorf("must specify %s", strings.Join(missingSet.List(), ","))
} }
// GroupKind that this evaluator tracks
func (p *pvcEvaluator) GroupKind() schema.GroupKind {
return api.Kind("PersistentVolumeClaim")
}
// Handles returns true if the evalutor should handle the specified operation.
func (p *pvcEvaluator) Handles(operation admission.Operation) bool {
return admission.Create == operation
}
// Matches returns true if the evaluator matches the specified quota with the provided input item
func (p *pvcEvaluator) Matches(resourceQuota *api.ResourceQuota, item runtime.Object) (bool, error) {
return generic.Matches(resourceQuota, item, p.MatchingResources, generic.MatchesNoScopeFunc)
}
// MatchingResources takes the input specified list of resources and returns the set of resources it matches.
func (p *pvcEvaluator) MatchingResources(items []api.ResourceName) []api.ResourceName {
result := []api.ResourceName{}
for _, item := range items {
if quota.Contains(pvcResources, item) {
result = append(result, item)
continue
}
// match pvc resources scoped by storage class (<storage-class-name>.storage-class.kubernetes.io/<resource>)
for _, resource := range pvcResources {
byStorageClass := storageClassSuffix + string(resource)
if strings.HasSuffix(string(item), byStorageClass) {
result = append(result, item)
break
}
}
}
return result
}
// Usage knows how to measure usage associated with item.
func (p *pvcEvaluator) Usage(item runtime.Object) (api.ResourceList, error) {
result := api.ResourceList{}
pvc, err := toInternalPersistentVolumeClaimOrError(item)
if err != nil {
return result, err
}
storageClassRef := util.GetClaimStorageClass(pvc)
// charge for claim
result[api.ResourcePersistentVolumeClaims] = resource.MustParse("1")
if len(storageClassRef) > 0 {
storageClassClaim := api.ResourceName(storageClassRef + storageClassSuffix + string(api.ResourcePersistentVolumeClaims))
result[storageClassClaim] = resource.MustParse("1")
}
// charge for storage
if request, found := pvc.Spec.Resources.Requests[api.ResourceStorage]; found {
result[api.ResourceRequestsStorage] = request
// charge usage to the storage class (if present)
if len(storageClassRef) > 0 {
storageClassStorage := api.ResourceName(storageClassRef + storageClassSuffix + string(api.ResourceRequestsStorage))
result[storageClassStorage] = request
}
}
return result, nil
}
// UsageStats calculates aggregate usage for the object.
func (p *pvcEvaluator) UsageStats(options quota.UsageStatsOptions) (quota.UsageStats, error) {
return generic.CalculateUsageStats(options, p.listFuncByNamespace, generic.MatchesNoScopeFunc, p.Usage)
}
// ensure we implement required interface
var _ quota.Evaluator = &pvcEvaluator{}
func toInternalPersistentVolumeClaimOrError(obj runtime.Object) (*api.PersistentVolumeClaim, error) {
pvc := &api.PersistentVolumeClaim{}
switch t := obj.(type) {
case *v1.PersistentVolumeClaim:
if err := v1.Convert_v1_PersistentVolumeClaim_To_api_PersistentVolumeClaim(t, pvc, nil); err != nil {
return nil, err
}
case *api.PersistentVolumeClaim:
pvc = t
default:
return nil, fmt.Errorf("expect *api.PersistentVolumeClaim or *v1.PersistentVolumeClaim, got %v", t)
}
return pvc, nil
}
...@@ -22,6 +22,7 @@ import ( ...@@ -22,6 +22,7 @@ import (
"k8s.io/kubernetes/pkg/api" "k8s.io/kubernetes/pkg/api"
"k8s.io/kubernetes/pkg/api/resource" "k8s.io/kubernetes/pkg/api/resource"
metav1 "k8s.io/kubernetes/pkg/apis/meta/v1" metav1 "k8s.io/kubernetes/pkg/apis/meta/v1"
"k8s.io/kubernetes/pkg/apis/storage/util"
"k8s.io/kubernetes/pkg/client/clientset_generated/release_1_5/fake" "k8s.io/kubernetes/pkg/client/clientset_generated/release_1_5/fake"
"k8s.io/kubernetes/pkg/quota" "k8s.io/kubernetes/pkg/quota"
) )
...@@ -53,6 +54,52 @@ func TestPersistentVolumeClaimsConstraintsFunc(t *testing.T) { ...@@ -53,6 +54,52 @@ func TestPersistentVolumeClaimsConstraintsFunc(t *testing.T) {
}, },
}, },
}) })
validClaimGoldStorageClass := testVolumeClaim("foo", "ns", api.PersistentVolumeClaimSpec{
Selector: &metav1.LabelSelector{
MatchExpressions: []metav1.LabelSelectorRequirement{
{
Key: "key2",
Operator: "Exists",
},
},
},
AccessModes: []api.PersistentVolumeAccessMode{
api.ReadWriteOnce,
api.ReadOnlyMany,
},
Resources: api.ResourceRequirements{
Requests: api.ResourceList{
api.ResourceName(api.ResourceStorage): resource.MustParse("10Gi"),
},
},
})
validClaimGoldStorageClass.Annotations = map[string]string{
util.StorageClassAnnotation: "gold",
}
validClaimBronzeStorageClass := testVolumeClaim("foo", "ns", api.PersistentVolumeClaimSpec{
Selector: &metav1.LabelSelector{
MatchExpressions: []metav1.LabelSelectorRequirement{
{
Key: "key2",
Operator: "Exists",
},
},
},
AccessModes: []api.PersistentVolumeAccessMode{
api.ReadWriteOnce,
api.ReadOnlyMany,
},
Resources: api.ResourceRequirements{
Requests: api.ResourceList{
api.ResourceName(api.ResourceStorage): resource.MustParse("10Gi"),
},
},
})
validClaimBronzeStorageClass.Annotations = map[string]string{
util.StorageClassAnnotation: "bronze",
}
missingStorage := testVolumeClaim("foo", "ns", api.PersistentVolumeClaimSpec{ missingStorage := testVolumeClaim("foo", "ns", api.PersistentVolumeClaimSpec{
Selector: &metav1.LabelSelector{ Selector: &metav1.LabelSelector{
MatchExpressions: []metav1.LabelSelectorRequirement{ MatchExpressions: []metav1.LabelSelectorRequirement{
...@@ -71,6 +118,27 @@ func TestPersistentVolumeClaimsConstraintsFunc(t *testing.T) { ...@@ -71,6 +118,27 @@ func TestPersistentVolumeClaimsConstraintsFunc(t *testing.T) {
}, },
}) })
missingGoldStorage := testVolumeClaim("foo", "ns", api.PersistentVolumeClaimSpec{
Selector: &metav1.LabelSelector{
MatchExpressions: []metav1.LabelSelectorRequirement{
{
Key: "key2",
Operator: "Exists",
},
},
},
AccessModes: []api.PersistentVolumeAccessMode{
api.ReadWriteOnce,
api.ReadOnlyMany,
},
Resources: api.ResourceRequirements{
Requests: api.ResourceList{},
},
})
missingGoldStorage.Annotations = map[string]string{
util.StorageClassAnnotation: "gold",
}
testCases := map[string]struct { testCases := map[string]struct {
pvc *api.PersistentVolumeClaim pvc *api.PersistentVolumeClaim
required []api.ResourceName required []api.ResourceName
...@@ -81,6 +149,11 @@ func TestPersistentVolumeClaimsConstraintsFunc(t *testing.T) { ...@@ -81,6 +149,11 @@ func TestPersistentVolumeClaimsConstraintsFunc(t *testing.T) {
required: []api.ResourceName{api.ResourceRequestsStorage}, required: []api.ResourceName{api.ResourceRequestsStorage},
err: `must specify requests.storage`, err: `must specify requests.storage`,
}, },
"missing gold storage": {
pvc: missingGoldStorage,
required: []api.ResourceName{ResourceByStorageClass("gold", api.ResourceRequestsStorage)},
err: `must specify gold.storageclass.storage.k8s.io/requests.storage`,
},
"valid-claim-quota-storage": { "valid-claim-quota-storage": {
pvc: validClaim, pvc: validClaim,
required: []api.ResourceName{api.ResourceRequestsStorage}, required: []api.ResourceName{api.ResourceRequestsStorage},
...@@ -93,9 +166,30 @@ func TestPersistentVolumeClaimsConstraintsFunc(t *testing.T) { ...@@ -93,9 +166,30 @@ func TestPersistentVolumeClaimsConstraintsFunc(t *testing.T) {
pvc: validClaim, pvc: validClaim,
required: []api.ResourceName{api.ResourceRequestsStorage, api.ResourcePersistentVolumeClaims}, required: []api.ResourceName{api.ResourceRequestsStorage, api.ResourcePersistentVolumeClaims},
}, },
"valid-claim-gold-quota-gold": {
pvc: validClaimGoldStorageClass,
required: []api.ResourceName{
api.ResourceRequestsStorage,
api.ResourcePersistentVolumeClaims,
ResourceByStorageClass("gold", api.ResourceRequestsStorage),
ResourceByStorageClass("gold", api.ResourcePersistentVolumeClaims),
},
},
"valid-claim-bronze-with-quota-gold": {
pvc: validClaimBronzeStorageClass,
required: []api.ResourceName{
api.ResourceRequestsStorage,
api.ResourcePersistentVolumeClaims,
ResourceByStorageClass("gold", api.ResourceRequestsStorage),
ResourceByStorageClass("gold", api.ResourcePersistentVolumeClaims),
},
},
} }
kubeClient := fake.NewSimpleClientset()
evaluator := NewPersistentVolumeClaimEvaluator(kubeClient, nil)
for testName, test := range testCases { for testName, test := range testCases {
err := PersistentVolumeClaimConstraintsFunc(test.required, test.pvc) err := evaluator.Constraints(test.required, test.pvc)
switch { switch {
case err != nil && len(test.err) == 0, case err != nil && len(test.err) == 0,
err == nil && len(test.err) != 0, err == nil && len(test.err) != 0,
...@@ -125,6 +219,29 @@ func TestPersistentVolumeClaimEvaluatorUsage(t *testing.T) { ...@@ -125,6 +219,29 @@ func TestPersistentVolumeClaimEvaluatorUsage(t *testing.T) {
}, },
}, },
}) })
validClaimByStorageClass := testVolumeClaim("foo", "ns", api.PersistentVolumeClaimSpec{
Selector: &metav1.LabelSelector{
MatchExpressions: []metav1.LabelSelectorRequirement{
{
Key: "key2",
Operator: "Exists",
},
},
},
AccessModes: []api.PersistentVolumeAccessMode{
api.ReadWriteOnce,
api.ReadOnlyMany,
},
Resources: api.ResourceRequirements{
Requests: api.ResourceList{
api.ResourceName(api.ResourceStorage): resource.MustParse("10Gi"),
},
},
})
storageClassName := "gold"
validClaimByStorageClass.Annotations = map[string]string{
util.StorageClassAnnotation: storageClassName,
}
kubeClient := fake.NewSimpleClientset() kubeClient := fake.NewSimpleClientset()
evaluator := NewPersistentVolumeClaimEvaluator(kubeClient, nil) evaluator := NewPersistentVolumeClaimEvaluator(kubeClient, nil)
...@@ -139,9 +256,21 @@ func TestPersistentVolumeClaimEvaluatorUsage(t *testing.T) { ...@@ -139,9 +256,21 @@ func TestPersistentVolumeClaimEvaluatorUsage(t *testing.T) {
api.ResourcePersistentVolumeClaims: resource.MustParse("1"), api.ResourcePersistentVolumeClaims: resource.MustParse("1"),
}, },
}, },
"pvc-usage-by-class": {
pvc: validClaimByStorageClass,
usage: api.ResourceList{
api.ResourceRequestsStorage: resource.MustParse("10Gi"),
api.ResourcePersistentVolumeClaims: resource.MustParse("1"),
ResourceByStorageClass(storageClassName, api.ResourceRequestsStorage): resource.MustParse("10Gi"),
ResourceByStorageClass(storageClassName, api.ResourcePersistentVolumeClaims): resource.MustParse("1"),
},
},
} }
for testName, testCase := range testCases { for testName, testCase := range testCases {
actual := evaluator.Usage(testCase.pvc) actual, err := evaluator.Usage(testCase.pvc)
if err != nil {
t.Errorf("%s unexpected error: %v", testName, err)
}
if !quota.Equals(testCase.usage, actual) { if !quota.Equals(testCase.usage, actual) {
t.Errorf("%s expected: %v, actual: %v", testName, testCase.usage, actual) t.Errorf("%s expected: %v, actual: %v", testName, testCase.usage, actual)
} }
......
...@@ -25,7 +25,6 @@ import ( ...@@ -25,7 +25,6 @@ import (
"k8s.io/kubernetes/pkg/api/resource" "k8s.io/kubernetes/pkg/api/resource"
"k8s.io/kubernetes/pkg/api/v1" "k8s.io/kubernetes/pkg/api/v1"
"k8s.io/kubernetes/pkg/api/validation" "k8s.io/kubernetes/pkg/api/validation"
metav1 "k8s.io/kubernetes/pkg/apis/meta/v1"
clientset "k8s.io/kubernetes/pkg/client/clientset_generated/release_1_5" clientset "k8s.io/kubernetes/pkg/client/clientset_generated/release_1_5"
"k8s.io/kubernetes/pkg/controller/informers" "k8s.io/kubernetes/pkg/controller/informers"
"k8s.io/kubernetes/pkg/kubelet/qos" "k8s.io/kubernetes/pkg/kubelet/qos"
...@@ -37,6 +36,17 @@ import ( ...@@ -37,6 +36,17 @@ import (
"k8s.io/kubernetes/pkg/util/validation/field" "k8s.io/kubernetes/pkg/util/validation/field"
) )
// podResources are the set of resources managed by quota associated with pods.
var podResources = []api.ResourceName{
api.ResourceCPU,
api.ResourceMemory,
api.ResourceRequestsCPU,
api.ResourceRequestsMemory,
api.ResourceLimitsCPU,
api.ResourceLimitsMemory,
api.ResourcePods,
}
// listPodsByNamespaceFuncUsingClient returns a pod listing function based on the provided client. // listPodsByNamespaceFuncUsingClient returns a pod listing function based on the provided client.
func listPodsByNamespaceFuncUsingClient(kubeClient clientset.Interface) generic.ListFuncByNamespace { func listPodsByNamespaceFuncUsingClient(kubeClient clientset.Interface) generic.ListFuncByNamespace {
// TODO: ideally, we could pass dynamic client pool down into this code, and have one way of doing this. // TODO: ideally, we could pass dynamic client pool down into this code, and have one way of doing this.
...@@ -58,44 +68,27 @@ func listPodsByNamespaceFuncUsingClient(kubeClient clientset.Interface) generic. ...@@ -58,44 +68,27 @@ func listPodsByNamespaceFuncUsingClient(kubeClient clientset.Interface) generic.
// NewPodEvaluator returns an evaluator that can evaluate pods // NewPodEvaluator returns an evaluator that can evaluate pods
// if the specified shared informer factory is not nil, evaluator may use it to support listing functions. // if the specified shared informer factory is not nil, evaluator may use it to support listing functions.
func NewPodEvaluator(kubeClient clientset.Interface, f informers.SharedInformerFactory) quota.Evaluator { func NewPodEvaluator(kubeClient clientset.Interface, f informers.SharedInformerFactory) quota.Evaluator {
computeResources := []api.ResourceName{
api.ResourceCPU,
api.ResourceMemory,
api.ResourceRequestsCPU,
api.ResourceRequestsMemory,
api.ResourceLimitsCPU,
api.ResourceLimitsMemory,
}
allResources := append(computeResources, api.ResourcePods)
listFuncByNamespace := listPodsByNamespaceFuncUsingClient(kubeClient) listFuncByNamespace := listPodsByNamespaceFuncUsingClient(kubeClient)
if f != nil { if f != nil {
listFuncByNamespace = generic.ListResourceUsingInformerFunc(f, schema.GroupResource{Resource: "pods"}) listFuncByNamespace = generic.ListResourceUsingInformerFunc(f, schema.GroupResource{Resource: "pods"})
} }
return &generic.GenericEvaluator{ return &podEvaluator{
Name: "Evaluator.Pod", listFuncByNamespace: listFuncByNamespace,
InternalGroupKind: api.Kind("Pod"), }
InternalOperationResources: map[admission.Operation][]api.ResourceName{ }
admission.Create: allResources,
// TODO: the quota system can only charge for deltas on compute resources when pods support updates. // podEvaluator knows how to measure usage of pods.
// admission.Update: computeResources, type podEvaluator struct {
}, // knows how to list pods
GetFuncByNamespace: func(namespace, name string) (runtime.Object, error) { listFuncByNamespace generic.ListFuncByNamespace
return kubeClient.Core().Pods(namespace).Get(name, metav1.GetOptions{}) }
},
ConstraintsFunc: PodConstraintsFunc, // Constraints verifies that all required resources are present on the pod
MatchedResourceNames: allResources,
MatchesScopeFunc: PodMatchesScopeFunc,
UsageFunc: PodUsageFunc,
ListFuncByNamespace: listFuncByNamespace,
}
}
// PodConstraintsFunc verifies that all required resources are present on the pod
// In addition, it validates that the resources are valid (i.e. requests < limits) // In addition, it validates that the resources are valid (i.e. requests < limits)
func PodConstraintsFunc(required []api.ResourceName, object runtime.Object) error { func (p *podEvaluator) Constraints(required []api.ResourceName, item runtime.Object) error {
pod, ok := object.(*api.Pod) pod, ok := item.(*api.Pod)
if !ok { if !ok {
return fmt.Errorf("Unexpected input object %v", object) return fmt.Errorf("Unexpected input object %v", item)
} }
// Pod level resources are often set during admission control // Pod level resources are often set during admission control
...@@ -114,7 +107,7 @@ func PodConstraintsFunc(required []api.ResourceName, object runtime.Object) erro ...@@ -114,7 +107,7 @@ func PodConstraintsFunc(required []api.ResourceName, object runtime.Object) erro
return allErrs.ToAggregate() return allErrs.ToAggregate()
} }
// TODO: fix this when we have pod level cgroups // TODO: fix this when we have pod level resource requirements
// since we do not yet pod level requests/limits, we need to ensure each // since we do not yet pod level requests/limits, we need to ensure each
// container makes an explict request or limit for a quota tracked resource // container makes an explict request or limit for a quota tracked resource
requiredSet := quota.ToSet(required) requiredSet := quota.ToSet(required)
...@@ -131,6 +124,40 @@ func PodConstraintsFunc(required []api.ResourceName, object runtime.Object) erro ...@@ -131,6 +124,40 @@ func PodConstraintsFunc(required []api.ResourceName, object runtime.Object) erro
return fmt.Errorf("must specify %s", strings.Join(missingSet.List(), ",")) return fmt.Errorf("must specify %s", strings.Join(missingSet.List(), ","))
} }
// GroupKind that this evaluator tracks
func (p *podEvaluator) GroupKind() schema.GroupKind {
return api.Kind("Pod")
}
// Handles returns true of the evalutor should handle the specified operation.
func (p *podEvaluator) Handles(operation admission.Operation) bool {
// TODO: update this if/when pods support resizing resource requirements.
return admission.Create == operation
}
// Matches returns true if the evaluator matches the specified quota with the provided input item
func (p *podEvaluator) Matches(resourceQuota *api.ResourceQuota, item runtime.Object) (bool, error) {
return generic.Matches(resourceQuota, item, p.MatchingResources, podMatchesScopeFunc)
}
// MatchingResources takes the input specified list of resources and returns the set of resources it matches.
func (p *podEvaluator) MatchingResources(input []api.ResourceName) []api.ResourceName {
return quota.Intersection(input, podResources)
}
// Usage knows how to measure usage associated with pods
func (p *podEvaluator) Usage(item runtime.Object) (api.ResourceList, error) {
return PodUsageFunc(item)
}
// UsageStats calculates aggregate usage for the object.
func (p *podEvaluator) UsageStats(options quota.UsageStatsOptions) (quota.UsageStats, error) {
return generic.CalculateUsageStats(options, p.listFuncByNamespace, podMatchesScopeFunc, p.Usage)
}
// verifies we implement the required interface.
var _ quota.Evaluator = &podEvaluator{}
// enforcePodContainerConstraints checks for required resources that are not set on this container and // enforcePodContainerConstraints checks for required resources that are not set on this container and
// adds them to missingSet. // adds them to missingSet.
func enforcePodContainerConstraints(container *api.Container, requiredSet, missingSet sets.String) { func enforcePodContainerConstraints(container *api.Container, requiredSet, missingSet sets.String) {
...@@ -165,27 +192,49 @@ func podUsageHelper(requests api.ResourceList, limits api.ResourceList) api.Reso ...@@ -165,27 +192,49 @@ func podUsageHelper(requests api.ResourceList, limits api.ResourceList) api.Reso
return result return result
} }
func toInternalPodOrDie(obj runtime.Object) *api.Pod { func toInternalPodOrError(obj runtime.Object) (*api.Pod, error) {
pod := &api.Pod{} pod := &api.Pod{}
switch t := obj.(type) { switch t := obj.(type) {
case *v1.Pod: case *v1.Pod:
if err := v1.Convert_v1_Pod_To_api_Pod(t, pod, nil); err != nil { if err := v1.Convert_v1_Pod_To_api_Pod(t, pod, nil); err != nil {
panic(err) return nil, err
} }
case *api.Pod: case *api.Pod:
pod = t pod = t
default: default:
panic(fmt.Sprintf("expect *api.Pod or *v1.Pod, got %v", t)) return nil, fmt.Errorf("expect *api.Pod or *v1.Pod, got %v", t)
} }
return pod return pod, nil
}
// podMatchesScopeFunc is a function that knows how to evaluate if a pod matches a scope
func podMatchesScopeFunc(scope api.ResourceQuotaScope, object runtime.Object) (bool, error) {
pod, err := toInternalPodOrError(object)
if err != nil {
return false, err
}
switch scope {
case api.ResourceQuotaScopeTerminating:
return isTerminating(pod), nil
case api.ResourceQuotaScopeNotTerminating:
return !isTerminating(pod), nil
case api.ResourceQuotaScopeBestEffort:
return isBestEffort(pod), nil
case api.ResourceQuotaScopeNotBestEffort:
return !isBestEffort(pod), nil
}
return false, nil
} }
// PodUsageFunc knows how to measure usage associated with pods // PodUsageFunc knows how to measure usage associated with pods
func PodUsageFunc(obj runtime.Object) api.ResourceList { func PodUsageFunc(obj runtime.Object) (api.ResourceList, error) {
pod := toInternalPodOrDie(obj) pod, err := toInternalPodOrError(obj)
if err != nil {
return api.ResourceList{}, err
}
// by convention, we do not quota pods that have reached an end-of-life state // by convention, we do not quota pods that have reached an end-of-life state
if !QuotaPod(pod) { if !QuotaPod(pod) {
return api.ResourceList{} return api.ResourceList{}, nil
} }
requests := api.ResourceList{} requests := api.ResourceList{}
limits := api.ResourceList{} limits := api.ResourceList{}
...@@ -203,23 +252,7 @@ func PodUsageFunc(obj runtime.Object) api.ResourceList { ...@@ -203,23 +252,7 @@ func PodUsageFunc(obj runtime.Object) api.ResourceList {
limits = quota.Max(limits, pod.Spec.InitContainers[i].Resources.Limits) limits = quota.Max(limits, pod.Spec.InitContainers[i].Resources.Limits)
} }
return podUsageHelper(requests, limits) return podUsageHelper(requests, limits), nil
}
// PodMatchesScopeFunc is a function that knows how to evaluate if a pod matches a scope
func PodMatchesScopeFunc(scope api.ResourceQuotaScope, object runtime.Object) bool {
pod := toInternalPodOrDie(object)
switch scope {
case api.ResourceQuotaScopeTerminating:
return isTerminating(pod)
case api.ResourceQuotaScopeNotTerminating:
return !isTerminating(pod)
case api.ResourceQuotaScopeBestEffort:
return isBestEffort(pod)
case api.ResourceQuotaScopeNotBestEffort:
return !isBestEffort(pod)
}
return false
} }
func isBestEffort(pod *api.Pod) bool { func isBestEffort(pod *api.Pod) bool {
...@@ -234,7 +267,6 @@ func isTerminating(pod *api.Pod) bool { ...@@ -234,7 +267,6 @@ func isTerminating(pod *api.Pod) bool {
} }
// QuotaPod returns true if the pod is eligible to track against a quota // QuotaPod returns true if the pod is eligible to track against a quota
// if it's not in a terminal state according to its phase.
func QuotaPod(pod *api.Pod) bool { func QuotaPod(pod *api.Pod) bool {
return !(api.PodFailed == pod.Status.Phase || api.PodSucceeded == pod.Status.Phase) return !(api.PodFailed == pod.Status.Phase || api.PodSucceeded == pod.Status.Phase)
} }
......
...@@ -86,8 +86,10 @@ func TestPodConstraintsFunc(t *testing.T) { ...@@ -86,8 +86,10 @@ func TestPodConstraintsFunc(t *testing.T) {
err: `must specify memory`, err: `must specify memory`,
}, },
} }
kubeClient := fake.NewSimpleClientset()
evaluator := NewPodEvaluator(kubeClient, nil)
for testName, test := range testCases { for testName, test := range testCases {
err := PodConstraintsFunc(test.required, test.pod) err := evaluator.Constraints(test.required, test.pod)
switch { switch {
case err != nil && len(test.err) == 0, case err != nil && len(test.err) == 0,
err == nil && len(test.err) != 0, err == nil && len(test.err) != 0,
...@@ -245,7 +247,10 @@ func TestPodEvaluatorUsage(t *testing.T) { ...@@ -245,7 +247,10 @@ func TestPodEvaluatorUsage(t *testing.T) {
}, },
} }
for testName, testCase := range testCases { for testName, testCase := range testCases {
actual := evaluator.Usage(testCase.pod) actual, err := evaluator.Usage(testCase.pod)
if err != nil {
t.Errorf("%s unexpected error: %v", testName, err)
}
if !quota.Equals(testCase.usage, actual) { if !quota.Equals(testCase.usage, actual) {
t.Errorf("%s expected: %v, actual: %v", testName, testCase.usage, actual) t.Errorf("%s expected: %v, actual: %v", testName, testCase.usage, actual)
} }
......
...@@ -17,7 +17,6 @@ limitations under the License. ...@@ -17,7 +17,6 @@ limitations under the License.
package core package core
import ( import (
"k8s.io/kubernetes/pkg/admission"
"k8s.io/kubernetes/pkg/api" "k8s.io/kubernetes/pkg/api"
"k8s.io/kubernetes/pkg/api/v1" "k8s.io/kubernetes/pkg/api/v1"
clientset "k8s.io/kubernetes/pkg/client/clientset_generated/release_1_5" clientset "k8s.io/kubernetes/pkg/client/clientset_generated/release_1_5"
...@@ -28,17 +27,10 @@ import ( ...@@ -28,17 +27,10 @@ import (
// NewReplicationControllerEvaluator returns an evaluator that can evaluate replication controllers // NewReplicationControllerEvaluator returns an evaluator that can evaluate replication controllers
func NewReplicationControllerEvaluator(kubeClient clientset.Interface) quota.Evaluator { func NewReplicationControllerEvaluator(kubeClient clientset.Interface) quota.Evaluator {
allResources := []api.ResourceName{api.ResourceReplicationControllers} return &generic.ObjectCountEvaluator{
return &generic.GenericEvaluator{ AllowCreateOnUpdate: false,
Name: "Evaluator.ReplicationController", InternalGroupKind: api.Kind("ReplicationController"),
InternalGroupKind: api.Kind("ReplicationController"), ResourceName: api.ResourceReplicationControllers,
InternalOperationResources: map[admission.Operation][]api.ResourceName{
admission.Create: allResources,
},
MatchedResourceNames: allResources,
MatchesScopeFunc: generic.MatchesNoScopeFunc,
ConstraintsFunc: generic.ObjectCountConstraintsFunc(api.ResourceReplicationControllers),
UsageFunc: generic.ObjectCountUsageFunc(api.ResourceReplicationControllers),
ListFuncByNamespace: func(namespace string, options v1.ListOptions) ([]runtime.Object, error) { ListFuncByNamespace: func(namespace string, options v1.ListOptions) ([]runtime.Object, error) {
itemList, err := kubeClient.Core().ReplicationControllers(namespace).List(options) itemList, err := kubeClient.Core().ReplicationControllers(namespace).List(options)
if err != nil { if err != nil {
......
...@@ -17,7 +17,6 @@ limitations under the License. ...@@ -17,7 +17,6 @@ limitations under the License.
package core package core
import ( import (
"k8s.io/kubernetes/pkg/admission"
"k8s.io/kubernetes/pkg/api" "k8s.io/kubernetes/pkg/api"
"k8s.io/kubernetes/pkg/api/v1" "k8s.io/kubernetes/pkg/api/v1"
clientset "k8s.io/kubernetes/pkg/client/clientset_generated/release_1_5" clientset "k8s.io/kubernetes/pkg/client/clientset_generated/release_1_5"
...@@ -28,17 +27,10 @@ import ( ...@@ -28,17 +27,10 @@ import (
// NewResourceQuotaEvaluator returns an evaluator that can evaluate resource quotas // NewResourceQuotaEvaluator returns an evaluator that can evaluate resource quotas
func NewResourceQuotaEvaluator(kubeClient clientset.Interface) quota.Evaluator { func NewResourceQuotaEvaluator(kubeClient clientset.Interface) quota.Evaluator {
allResources := []api.ResourceName{api.ResourceQuotas} return &generic.ObjectCountEvaluator{
return &generic.GenericEvaluator{ AllowCreateOnUpdate: false,
Name: "Evaluator.ResourceQuota", InternalGroupKind: api.Kind("ResourceQuota"),
InternalGroupKind: api.Kind("ResourceQuota"), ResourceName: api.ResourceQuotas,
InternalOperationResources: map[admission.Operation][]api.ResourceName{
admission.Create: allResources,
},
MatchedResourceNames: allResources,
MatchesScopeFunc: generic.MatchesNoScopeFunc,
ConstraintsFunc: generic.ObjectCountConstraintsFunc(api.ResourceQuotas),
UsageFunc: generic.ObjectCountUsageFunc(api.ResourceQuotas),
ListFuncByNamespace: func(namespace string, options v1.ListOptions) ([]runtime.Object, error) { ListFuncByNamespace: func(namespace string, options v1.ListOptions) ([]runtime.Object, error) {
itemList, err := kubeClient.Core().ResourceQuotas(namespace).List(options) itemList, err := kubeClient.Core().ResourceQuotas(namespace).List(options)
if err != nil { if err != nil {
......
...@@ -17,7 +17,6 @@ limitations under the License. ...@@ -17,7 +17,6 @@ limitations under the License.
package core package core
import ( import (
"k8s.io/kubernetes/pkg/admission"
"k8s.io/kubernetes/pkg/api" "k8s.io/kubernetes/pkg/api"
"k8s.io/kubernetes/pkg/api/v1" "k8s.io/kubernetes/pkg/api/v1"
clientset "k8s.io/kubernetes/pkg/client/clientset_generated/release_1_5" clientset "k8s.io/kubernetes/pkg/client/clientset_generated/release_1_5"
...@@ -28,17 +27,10 @@ import ( ...@@ -28,17 +27,10 @@ import (
// NewSecretEvaluator returns an evaluator that can evaluate secrets // NewSecretEvaluator returns an evaluator that can evaluate secrets
func NewSecretEvaluator(kubeClient clientset.Interface) quota.Evaluator { func NewSecretEvaluator(kubeClient clientset.Interface) quota.Evaluator {
allResources := []api.ResourceName{api.ResourceSecrets} return &generic.ObjectCountEvaluator{
return &generic.GenericEvaluator{ AllowCreateOnUpdate: false,
Name: "Evaluator.Secret", InternalGroupKind: api.Kind("Secret"),
InternalGroupKind: api.Kind("Secret"), ResourceName: api.ResourceSecrets,
InternalOperationResources: map[admission.Operation][]api.ResourceName{
admission.Create: allResources,
},
MatchedResourceNames: allResources,
MatchesScopeFunc: generic.MatchesNoScopeFunc,
ConstraintsFunc: generic.ObjectCountConstraintsFunc(api.ResourceSecrets),
UsageFunc: generic.ObjectCountUsageFunc(api.ResourceSecrets),
ListFuncByNamespace: func(namespace string, options v1.ListOptions) ([]runtime.Object, error) { ListFuncByNamespace: func(namespace string, options v1.ListOptions) ([]runtime.Object, error) {
itemList, err := kubeClient.Core().Secrets(namespace).List(options) itemList, err := kubeClient.Core().Secrets(namespace).List(options)
if err != nil { if err != nil {
......
...@@ -28,28 +28,21 @@ import ( ...@@ -28,28 +28,21 @@ import (
"k8s.io/kubernetes/pkg/quota" "k8s.io/kubernetes/pkg/quota"
"k8s.io/kubernetes/pkg/quota/generic" "k8s.io/kubernetes/pkg/quota/generic"
"k8s.io/kubernetes/pkg/runtime" "k8s.io/kubernetes/pkg/runtime"
"k8s.io/kubernetes/pkg/runtime/schema"
"k8s.io/kubernetes/pkg/util/sets" "k8s.io/kubernetes/pkg/util/sets"
) )
// serviceResources are the set of resources managed by quota associated with services.
var serviceResources = []api.ResourceName{
api.ResourceServices,
api.ResourceServicesNodePorts,
api.ResourceServicesLoadBalancers,
}
// NewServiceEvaluator returns an evaluator that can evaluate service quotas // NewServiceEvaluator returns an evaluator that can evaluate service quotas
func NewServiceEvaluator(kubeClient clientset.Interface) quota.Evaluator { func NewServiceEvaluator(kubeClient clientset.Interface) quota.Evaluator {
allResources := []api.ResourceName{ return &serviceEvaluator{
api.ResourceServices, listFuncByNamespace: func(namespace string, options v1.ListOptions) ([]runtime.Object, error) {
api.ResourceServicesNodePorts,
api.ResourceServicesLoadBalancers,
}
return &generic.GenericEvaluator{
Name: "Evaluator.Service",
InternalGroupKind: api.Kind("Service"),
InternalOperationResources: map[admission.Operation][]api.ResourceName{
admission.Create: allResources,
admission.Update: allResources,
},
MatchedResourceNames: allResources,
MatchesScopeFunc: generic.MatchesNoScopeFunc,
ConstraintsFunc: ServiceConstraintsFunc,
UsageFunc: ServiceUsageFunc,
ListFuncByNamespace: func(namespace string, options v1.ListOptions) ([]runtime.Object, error) {
itemList, err := kubeClient.Core().Services(namespace).List(options) itemList, err := kubeClient.Core().Services(namespace).List(options)
if err != nil { if err != nil {
return nil, err return nil, err
...@@ -63,39 +56,104 @@ func NewServiceEvaluator(kubeClient clientset.Interface) quota.Evaluator { ...@@ -63,39 +56,104 @@ func NewServiceEvaluator(kubeClient clientset.Interface) quota.Evaluator {
} }
} }
// ServiceUsageFunc knows how to measure usage associated with services // serviceEvaluator knows how to measure usage for services.
func ServiceUsageFunc(object runtime.Object) api.ResourceList { type serviceEvaluator struct {
result := api.ResourceList{} // knows how to list items by namespace
var serviceType api.ServiceType listFuncByNamespace generic.ListFuncByNamespace
var ports int }
// Constraints verifies that all required resources are present on the item
func (p *serviceEvaluator) Constraints(required []api.ResourceName, item runtime.Object) error {
service, ok := item.(*api.Service)
if !ok {
return fmt.Errorf("unexpected input object %v", item)
}
requiredSet := quota.ToSet(required)
missingSet := sets.NewString()
serviceUsage, err := p.Usage(service)
if err != nil {
return err
}
serviceSet := quota.ToSet(quota.ResourceNames(serviceUsage))
if diff := requiredSet.Difference(serviceSet); len(diff) > 0 {
missingSet.Insert(diff.List()...)
}
if len(missingSet) == 0 {
return nil
}
return fmt.Errorf("must specify %s", strings.Join(missingSet.List(), ","))
}
// GroupKind that this evaluator tracks
func (p *serviceEvaluator) GroupKind() schema.GroupKind {
return api.Kind("Service")
}
// Handles returns true of the evalutor should handle the specified operation.
func (p *serviceEvaluator) Handles(operation admission.Operation) bool {
// We handle create and update because a service type can change.
return admission.Create == operation || admission.Update == operation
}
switch t := object.(type) { // Matches returns true if the evaluator matches the specified quota with the provided input item
func (p *serviceEvaluator) Matches(resourceQuota *api.ResourceQuota, item runtime.Object) (bool, error) {
return generic.Matches(resourceQuota, item, p.MatchingResources, generic.MatchesNoScopeFunc)
}
// MatchingResources takes the input specified list of resources and returns the set of resources it matches.
func (p *serviceEvaluator) MatchingResources(input []api.ResourceName) []api.ResourceName {
return quota.Intersection(input, serviceResources)
}
// convert the input object to an internal service object or error.
func toInternalServiceOrError(obj runtime.Object) (*api.Service, error) {
svc := &api.Service{}
switch t := obj.(type) {
case *v1.Service: case *v1.Service:
serviceType = api.ServiceType(t.Spec.Type) if err := v1.Convert_v1_Service_To_api_Service(t, svc, nil); err != nil {
ports = len(t.Spec.Ports) return nil, err
}
case *api.Service: case *api.Service:
serviceType = t.Spec.Type svc = t
ports = len(t.Spec.Ports)
default: default:
panic(fmt.Sprintf("expect *api.Service or *v1.Service, got %v", t)) return nil, fmt.Errorf("expect *api.Service or *v1.Service, got %v", t)
} }
return svc, nil
}
// Usage knows how to measure usage associated with pods
func (p *serviceEvaluator) Usage(item runtime.Object) (api.ResourceList, error) {
result := api.ResourceList{}
svc, err := toInternalServiceOrError(item)
if err != nil {
return result, err
}
ports := len(svc.Spec.Ports)
// default service usage // default service usage
result[api.ResourceServices] = resource.MustParse("1") result[api.ResourceServices] = *(resource.NewQuantity(1, resource.DecimalSI))
result[api.ResourceServicesLoadBalancers] = resource.MustParse("0") result[api.ResourceServicesLoadBalancers] = resource.Quantity{Format: resource.DecimalSI}
result[api.ResourceServicesNodePorts] = resource.MustParse("0") result[api.ResourceServicesNodePorts] = resource.Quantity{Format: resource.DecimalSI}
switch serviceType { switch svc.Spec.Type {
case api.ServiceTypeNodePort: case api.ServiceTypeNodePort:
// node port services need to count node ports // node port services need to count node ports
value := resource.NewQuantity(int64(ports), resource.DecimalSI) value := resource.NewQuantity(int64(ports), resource.DecimalSI)
result[api.ResourceServicesNodePorts] = *value result[api.ResourceServicesNodePorts] = *value
case api.ServiceTypeLoadBalancer: case api.ServiceTypeLoadBalancer:
// load balancer services need to count load balancers // load balancer services need to count load balancers
result[api.ResourceServicesLoadBalancers] = resource.MustParse("1") result[api.ResourceServicesLoadBalancers] = *(resource.NewQuantity(1, resource.DecimalSI))
} }
return result return result, nil
} }
// UsageStats calculates aggregate usage for the object.
func (p *serviceEvaluator) UsageStats(options quota.UsageStatsOptions) (quota.UsageStats, error) {
return generic.CalculateUsageStats(options, p.listFuncByNamespace, generic.MatchesNoScopeFunc, p.Usage)
}
var _ quota.Evaluator = &serviceEvaluator{}
// QuotaServiceType returns true if the service type is eligible to track against a quota // QuotaServiceType returns true if the service type is eligible to track against a quota
func QuotaServiceType(service *v1.Service) bool { func QuotaServiceType(service *v1.Service) bool {
switch service.Spec.Type { switch service.Spec.Type {
...@@ -115,24 +173,3 @@ func GetQuotaServiceType(service *v1.Service) v1.ServiceType { ...@@ -115,24 +173,3 @@ func GetQuotaServiceType(service *v1.Service) v1.ServiceType {
} }
return v1.ServiceType("") return v1.ServiceType("")
} }
// ServiceConstraintsFunc verifies that all required resources are captured in service usage.
func ServiceConstraintsFunc(required []api.ResourceName, object runtime.Object) error {
service, ok := object.(*api.Service)
if !ok {
return fmt.Errorf("unexpected input object %v", object)
}
requiredSet := quota.ToSet(required)
missingSet := sets.NewString()
serviceUsage := ServiceUsageFunc(service)
serviceSet := quota.ToSet(quota.ResourceNames(serviceUsage))
if diff := requiredSet.Difference(serviceSet); len(diff) > 0 {
missingSet.Insert(diff.List()...)
}
if len(missingSet) == 0 {
return nil
}
return fmt.Errorf("must specify %s", strings.Join(missingSet.List(), ","))
}
...@@ -28,12 +28,21 @@ import ( ...@@ -28,12 +28,21 @@ import (
func TestServiceEvaluatorMatchesResources(t *testing.T) { func TestServiceEvaluatorMatchesResources(t *testing.T) {
kubeClient := fake.NewSimpleClientset() kubeClient := fake.NewSimpleClientset()
evaluator := NewServiceEvaluator(kubeClient) evaluator := NewServiceEvaluator(kubeClient)
// we give a lot of resources
input := []api.ResourceName{
api.ResourceConfigMaps,
api.ResourceCPU,
api.ResourceServices,
api.ResourceServicesNodePorts,
api.ResourceServicesLoadBalancers,
}
// but we only match these...
expected := quota.ToSet([]api.ResourceName{ expected := quota.ToSet([]api.ResourceName{
api.ResourceServices, api.ResourceServices,
api.ResourceServicesNodePorts, api.ResourceServicesNodePorts,
api.ResourceServicesLoadBalancers, api.ResourceServicesLoadBalancers,
}) })
actual := quota.ToSet(evaluator.MatchesResources()) actual := quota.ToSet(evaluator.MatchingResources(input))
if !expected.Equal(actual) { if !expected.Equal(actual) {
t.Errorf("expected: %v, actual: %v", expected, actual) t.Errorf("expected: %v, actual: %v", expected, actual)
} }
...@@ -109,7 +118,10 @@ func TestServiceEvaluatorUsage(t *testing.T) { ...@@ -109,7 +118,10 @@ func TestServiceEvaluatorUsage(t *testing.T) {
}, },
} }
for testName, testCase := range testCases { for testName, testCase := range testCases {
actual := evaluator.Usage(testCase.service) actual, err := evaluator.Usage(testCase.service)
if err != nil {
t.Errorf("%s unexpected error: %v", testName, err)
}
if !quota.Equals(testCase.usage, actual) { if !quota.Equals(testCase.usage, actual) {
t.Errorf("%s expected: %v, actual: %v", testName, testCase.usage, actual) t.Errorf("%s expected: %v, actual: %v", testName, testCase.usage, actual)
} }
...@@ -168,8 +180,11 @@ func TestServiceConstraintsFunc(t *testing.T) { ...@@ -168,8 +180,11 @@ func TestServiceConstraintsFunc(t *testing.T) {
required: []api.ResourceName{api.ResourceServicesNodePorts}, required: []api.ResourceName{api.ResourceServicesNodePorts},
}, },
} }
kubeClient := fake.NewSimpleClientset()
evaluator := NewServiceEvaluator(kubeClient)
for testName, test := range testCases { for testName, test := range testCases {
err := ServiceConstraintsFunc(test.required, test.service) err := evaluator.Constraints(test.required, test.service)
switch { switch {
case err != nil && len(test.err) == 0, case err != nil && len(test.err) == 0,
err == nil && len(test.err) != 0, err == nil && len(test.err) != 0,
......
...@@ -29,6 +29,8 @@ type UsageStatsOptions struct { ...@@ -29,6 +29,8 @@ type UsageStatsOptions struct {
Namespace string Namespace string
// Scopes that must match counted objects // Scopes that must match counted objects
Scopes []api.ResourceQuotaScope Scopes []api.ResourceQuotaScope
// Resources are the set of resources to include in the measurement
Resources []api.ResourceName
} }
// UsageStats is result of measuring observed resource use in the system // UsageStats is result of measuring observed resource use in the system
...@@ -41,20 +43,17 @@ type UsageStats struct { ...@@ -41,20 +43,17 @@ type UsageStats struct {
type Evaluator interface { type Evaluator interface {
// Constraints ensures that each required resource is present on item // Constraints ensures that each required resource is present on item
Constraints(required []api.ResourceName, item runtime.Object) error Constraints(required []api.ResourceName, item runtime.Object) error
// Get returns the object with specified namespace and name
Get(namespace, name string) (runtime.Object, error)
// GroupKind returns the groupKind that this object knows how to evaluate // GroupKind returns the groupKind that this object knows how to evaluate
GroupKind() schema.GroupKind GroupKind() schema.GroupKind
// MatchesResources is the list of resources that this evaluator matches // Handles determines if quota could be impacted by the specified operation.
MatchesResources() []api.ResourceName // If true, admission control must perform quota processing for the operation, otherwise it is safe to ignore quota.
Handles(operation admission.Operation) bool
// Matches returns true if the specified quota matches the input item // Matches returns true if the specified quota matches the input item
Matches(resourceQuota *api.ResourceQuota, item runtime.Object) bool Matches(resourceQuota *api.ResourceQuota, item runtime.Object) (bool, error)
// OperationResources returns the set of resources that could be updated for the // MatchingResources takes the input specified list of resources and returns the set of resources evaluator matches.
// specified operation for this kind. If empty, admission control will ignore MatchingResources(input []api.ResourceName) []api.ResourceName
// quota processing for the operation.
OperationResources(operation admission.Operation) []api.ResourceName
// Usage returns the resource usage for the specified object // Usage returns the resource usage for the specified object
Usage(object runtime.Object) api.ResourceList Usage(item runtime.Object) (api.ResourceList, error)
// UsageStats calculates latest observed usage stats for all objects // UsageStats calculates latest observed usage stats for all objects
UsageStats(options UsageStatsOptions) (UsageStats, error) UsageStats(options UsageStatsOptions) (UsageStats, error)
} }
...@@ -69,6 +68,7 @@ type Registry interface { ...@@ -69,6 +68,7 @@ type Registry interface {
// is the "winner" // is the "winner"
type UnionRegistry []Registry type UnionRegistry []Registry
// Evaluators returns a mapping of evaluators by group kind.
func (r UnionRegistry) Evaluators() map[schema.GroupKind]Evaluator { func (r UnionRegistry) Evaluators() map[schema.GroupKind]Evaluator {
ret := map[schema.GroupKind]Evaluator{} ret := map[schema.GroupKind]Evaluator{}
......
...@@ -223,18 +223,21 @@ func CalculateUsage(namespaceName string, scopes []api.ResourceQuotaScope, hardL ...@@ -223,18 +223,21 @@ func CalculateUsage(namespaceName string, scopes []api.ResourceQuotaScope, hardL
potentialResources := []api.ResourceName{} potentialResources := []api.ResourceName{}
evaluators := registry.Evaluators() evaluators := registry.Evaluators()
for _, evaluator := range evaluators { for _, evaluator := range evaluators {
potentialResources = append(potentialResources, evaluator.MatchesResources()...) potentialResources = append(potentialResources, evaluator.MatchingResources(hardResources)...)
} }
// NOTE: the intersection just removes duplicates since the evaluator match intersects wtih hard
matchedResources := Intersection(hardResources, potentialResources) matchedResources := Intersection(hardResources, potentialResources)
// sum the observed usage from each evaluator // sum the observed usage from each evaluator
newUsage := api.ResourceList{} newUsage := api.ResourceList{}
usageStatsOptions := UsageStatsOptions{Namespace: namespaceName, Scopes: scopes}
for _, evaluator := range evaluators { for _, evaluator := range evaluators {
// only trigger the evaluator if it matches a resource in the quota, otherwise, skip calculating anything // only trigger the evaluator if it matches a resource in the quota, otherwise, skip calculating anything
if intersection := Intersection(evaluator.MatchesResources(), matchedResources); len(intersection) == 0 { intersection := evaluator.MatchingResources(matchedResources)
if len(intersection) == 0 {
continue continue
} }
usageStatsOptions := UsageStatsOptions{Namespace: namespaceName, Scopes: scopes, Resources: intersection}
stats, err := evaluator.UsageStats(usageStatsOptions) stats, err := evaluator.UsageStats(usageStatsOptions)
if err != nil { if err != nil {
return nil, err return nil, err
......
...@@ -218,6 +218,7 @@ func TestIsQualifiedName(t *testing.T) { ...@@ -218,6 +218,7 @@ func TestIsQualifiedName(t *testing.T) {
"1.2.3.4/5678", "1.2.3.4/5678",
"Uppercase_Is_OK_123", "Uppercase_Is_OK_123",
"example.com/Uppercase_Is_OK_123", "example.com/Uppercase_Is_OK_123",
"requests.storage-foo",
strings.Repeat("a", 63), strings.Repeat("a", 63),
strings.Repeat("a", 253) + "/" + strings.Repeat("b", 63), strings.Repeat("a", 253) + "/" + strings.Repeat("b", 63),
} }
......
...@@ -54,10 +54,8 @@ go_test( ...@@ -54,10 +54,8 @@ go_test(
"//pkg/client/clientset_generated/internalclientset/fake:go_default_library", "//pkg/client/clientset_generated/internalclientset/fake:go_default_library",
"//pkg/client/testing/core:go_default_library", "//pkg/client/testing/core:go_default_library",
"//pkg/quota:go_default_library", "//pkg/quota:go_default_library",
"//pkg/quota/evaluator/core:go_default_library",
"//pkg/quota/generic:go_default_library", "//pkg/quota/generic:go_default_library",
"//pkg/quota/install:go_default_library", "//pkg/quota/install:go_default_library",
"//pkg/runtime:go_default_library",
"//pkg/runtime/schema:go_default_library", "//pkg/runtime/schema:go_default_library",
"//pkg/util/sets:go_default_library", "//pkg/util/sets:go_default_library",
"//vendor:github.com/hashicorp/golang-lru", "//vendor:github.com/hashicorp/golang-lru",
......
...@@ -31,10 +31,8 @@ import ( ...@@ -31,10 +31,8 @@ import (
"k8s.io/kubernetes/pkg/client/clientset_generated/internalclientset/fake" "k8s.io/kubernetes/pkg/client/clientset_generated/internalclientset/fake"
testcore "k8s.io/kubernetes/pkg/client/testing/core" testcore "k8s.io/kubernetes/pkg/client/testing/core"
"k8s.io/kubernetes/pkg/quota" "k8s.io/kubernetes/pkg/quota"
"k8s.io/kubernetes/pkg/quota/evaluator/core"
"k8s.io/kubernetes/pkg/quota/generic" "k8s.io/kubernetes/pkg/quota/generic"
"k8s.io/kubernetes/pkg/quota/install" "k8s.io/kubernetes/pkg/quota/install"
"k8s.io/kubernetes/pkg/runtime"
"k8s.io/kubernetes/pkg/runtime/schema" "k8s.io/kubernetes/pkg/runtime/schema"
"k8s.io/kubernetes/pkg/util/sets" "k8s.io/kubernetes/pkg/util/sets"
) )
...@@ -896,44 +894,22 @@ func TestAdmissionSetsMissingNamespace(t *testing.T) { ...@@ -896,44 +894,22 @@ func TestAdmissionSetsMissingNamespace(t *testing.T) {
ObjectMeta: api.ObjectMeta{Name: "quota", Namespace: namespace, ResourceVersion: "124"}, ObjectMeta: api.ObjectMeta{Name: "quota", Namespace: namespace, ResourceVersion: "124"},
Status: api.ResourceQuotaStatus{ Status: api.ResourceQuotaStatus{
Hard: api.ResourceList{ Hard: api.ResourceList{
api.ResourceCPU: resource.MustParse("3"), api.ResourcePods: resource.MustParse("3"),
}, },
Used: api.ResourceList{ Used: api.ResourceList{
api.ResourceCPU: resource.MustParse("1"), api.ResourcePods: resource.MustParse("1"),
}, },
}, },
} }
kubeClient := fake.NewSimpleClientset(resourceQuota) kubeClient := fake.NewSimpleClientset(resourceQuota)
indexer := cache.NewIndexer(cache.MetaNamespaceKeyFunc, cache.Indexers{"namespace": cache.MetaNamespaceIndexFunc}) indexer := cache.NewIndexer(cache.MetaNamespaceKeyFunc, cache.Indexers{"namespace": cache.MetaNamespaceIndexFunc})
computeResources := []api.ResourceName{ // create a dummy evaluator so we can trigger quota
api.ResourcePods, podEvaluator := &generic.ObjectCountEvaluator{
api.ResourceCPU, AllowCreateOnUpdate: false,
} InternalGroupKind: api.Kind("Pod"),
ResourceName: api.ResourcePods,
usageFunc := func(object runtime.Object) api.ResourceList {
pod, ok := object.(*api.Pod)
if !ok {
t.Fatalf("Expected pod, got %T", object)
}
if pod.Namespace != namespace {
t.Errorf("Expected pod with different namespace: %q != %q", pod.Namespace, namespace)
}
return core.PodUsageFunc(pod)
} }
podEvaluator := &generic.GenericEvaluator{
Name: "Test-Evaluator.Pod",
InternalGroupKind: api.Kind("Pod"),
InternalOperationResources: map[admission.Operation][]api.ResourceName{
admission.Create: computeResources,
},
ConstraintsFunc: core.PodConstraintsFunc,
MatchedResourceNames: computeResources,
MatchesScopeFunc: core.PodMatchesScopeFunc,
UsageFunc: usageFunc,
}
registry := &generic.GenericRegistry{ registry := &generic.GenericRegistry{
InternalEvaluators: map[schema.GroupKind]quota.Evaluator{ InternalEvaluators: map[schema.GroupKind]quota.Evaluator{
podEvaluator.GroupKind(): podEvaluator, podEvaluator.GroupKind(): podEvaluator,
......
...@@ -327,8 +327,7 @@ func (e *quotaEvaluator) checkRequest(quotas []api.ResourceQuota, a admission.At ...@@ -327,8 +327,7 @@ func (e *quotaEvaluator) checkRequest(quotas []api.ResourceQuota, a admission.At
} }
op := a.GetOperation() op := a.GetOperation()
operationResources := evaluator.OperationResources(op) if !evaluator.Handles(op) {
if len(operationResources) == 0 {
return quotas, nil return quotas, nil
} }
...@@ -340,14 +339,16 @@ func (e *quotaEvaluator) checkRequest(quotas []api.ResourceQuota, a admission.At ...@@ -340,14 +339,16 @@ func (e *quotaEvaluator) checkRequest(quotas []api.ResourceQuota, a admission.At
interestingQuotaIndexes := []int{} interestingQuotaIndexes := []int{}
for i := range quotas { for i := range quotas {
resourceQuota := quotas[i] resourceQuota := quotas[i]
match := evaluator.Matches(&resourceQuota, inputObject) match, err := evaluator.Matches(&resourceQuota, inputObject)
if err != nil {
return quotas, err
}
if !match { if !match {
continue continue
} }
hardResources := quota.ResourceNames(resourceQuota.Status.Hard) hardResources := quota.ResourceNames(resourceQuota.Status.Hard)
evaluatorResources := evaluator.MatchesResources() requiredResources := evaluator.MatchingResources(hardResources)
requiredResources := quota.Intersection(hardResources, evaluatorResources)
if err := evaluator.Constraints(requiredResources, inputObject); err != nil { if err := evaluator.Constraints(requiredResources, inputObject); err != nil {
return nil, admission.NewForbidden(a, fmt.Errorf("failed quota: %s: %v", resourceQuota.Name, err)) return nil, admission.NewForbidden(a, fmt.Errorf("failed quota: %s: %v", resourceQuota.Name, err))
} }
...@@ -375,7 +376,10 @@ func (e *quotaEvaluator) checkRequest(quotas []api.ResourceQuota, a admission.At ...@@ -375,7 +376,10 @@ func (e *quotaEvaluator) checkRequest(quotas []api.ResourceQuota, a admission.At
// as a result, we need to measure the usage of this object for quota // as a result, we need to measure the usage of this object for quota
// on updates, we need to subtract the previous measured usage // on updates, we need to subtract the previous measured usage
// if usage shows no change, just return since it has no impact on quota // if usage shows no change, just return since it has no impact on quota
deltaUsage := evaluator.Usage(inputObject) deltaUsage, err := evaluator.Usage(inputObject)
if err != nil {
return quotas, err
}
// ensure that usage for input object is never negative (this would mean a resource made a negative resource requirement) // ensure that usage for input object is never negative (this would mean a resource made a negative resource requirement)
if negativeUsage := quota.IsNegative(deltaUsage); len(negativeUsage) > 0 { if negativeUsage := quota.IsNegative(deltaUsage); len(negativeUsage) > 0 {
...@@ -392,7 +396,10 @@ func (e *quotaEvaluator) checkRequest(quotas []api.ResourceQuota, a admission.At ...@@ -392,7 +396,10 @@ func (e *quotaEvaluator) checkRequest(quotas []api.ResourceQuota, a admission.At
// then charge based on the delta. Otherwise, bill the maximum // then charge based on the delta. Otherwise, bill the maximum
metadata, err := meta.Accessor(prevItem) metadata, err := meta.Accessor(prevItem)
if err == nil && len(metadata.GetResourceVersion()) > 0 { if err == nil && len(metadata.GetResourceVersion()) > 0 {
prevUsage := evaluator.Usage(prevItem) prevUsage, innerErr := evaluator.Usage(prevItem)
if innerErr != nil {
return quotas, innerErr
}
deltaUsage = quota.Subtract(deltaUsage, prevUsage) deltaUsage = quota.Subtract(deltaUsage, prevUsage)
} }
} }
...@@ -446,8 +453,7 @@ func (e *quotaEvaluator) Evaluate(a admission.Attributes) error { ...@@ -446,8 +453,7 @@ func (e *quotaEvaluator) Evaluate(a admission.Attributes) error {
// for this kind, check if the operation could mutate any quota resources // for this kind, check if the operation could mutate any quota resources
// if no resources tracked by quota are impacted, then just return // if no resources tracked by quota are impacted, then just return
op := a.GetOperation() op := a.GetOperation()
operationResources := evaluator.OperationResources(op) if !evaluator.Handles(op) {
if len(operationResources) == 0 {
return nil return nil
} }
......
...@@ -130,6 +130,7 @@ go_library( ...@@ -130,6 +130,7 @@ go_library(
"//pkg/apis/extensions:go_default_library", "//pkg/apis/extensions:go_default_library",
"//pkg/apis/extensions/v1beta1:go_default_library", "//pkg/apis/extensions/v1beta1:go_default_library",
"//pkg/apis/meta/v1:go_default_library", "//pkg/apis/meta/v1:go_default_library",
"//pkg/apis/storage/util:go_default_library",
"//pkg/apis/storage/v1beta1:go_default_library", "//pkg/apis/storage/v1beta1:go_default_library",
"//pkg/apis/storage/v1beta1/util:go_default_library", "//pkg/apis/storage/v1beta1/util:go_default_library",
"//pkg/client/cache:go_default_library", "//pkg/client/cache:go_default_library",
...@@ -159,6 +160,7 @@ go_library( ...@@ -159,6 +160,7 @@ go_library(
"//pkg/labels:go_default_library", "//pkg/labels:go_default_library",
"//pkg/master/ports:go_default_library", "//pkg/master/ports:go_default_library",
"//pkg/metrics:go_default_library", "//pkg/metrics:go_default_library",
"//pkg/quota/evaluator/core:go_default_library",
"//pkg/registry/generic/registry:go_default_library", "//pkg/registry/generic/registry:go_default_library",
"//pkg/runtime:go_default_library", "//pkg/runtime:go_default_library",
"//pkg/runtime/schema:go_default_library", "//pkg/runtime/schema:go_default_library",
......
...@@ -23,7 +23,9 @@ import ( ...@@ -23,7 +23,9 @@ import (
"k8s.io/kubernetes/pkg/api/resource" "k8s.io/kubernetes/pkg/api/resource"
"k8s.io/kubernetes/pkg/api/v1" "k8s.io/kubernetes/pkg/api/v1"
metav1 "k8s.io/kubernetes/pkg/apis/meta/v1" metav1 "k8s.io/kubernetes/pkg/apis/meta/v1"
"k8s.io/kubernetes/pkg/apis/storage/util"
clientset "k8s.io/kubernetes/pkg/client/clientset_generated/release_1_5" clientset "k8s.io/kubernetes/pkg/client/clientset_generated/release_1_5"
"k8s.io/kubernetes/pkg/quota/evaluator/core"
"k8s.io/kubernetes/pkg/util/intstr" "k8s.io/kubernetes/pkg/util/intstr"
"k8s.io/kubernetes/pkg/util/wait" "k8s.io/kubernetes/pkg/util/wait"
"k8s.io/kubernetes/test/e2e/framework" "k8s.io/kubernetes/test/e2e/framework"
...@@ -288,7 +290,7 @@ var _ = framework.KubeDescribe("ResourceQuota", func() { ...@@ -288,7 +290,7 @@ var _ = framework.KubeDescribe("ResourceQuota", func() {
pvc, err = f.ClientSet.Core().PersistentVolumeClaims(f.Namespace.Name).Create(pvc) pvc, err = f.ClientSet.Core().PersistentVolumeClaims(f.Namespace.Name).Create(pvc)
Expect(err).NotTo(HaveOccurred()) Expect(err).NotTo(HaveOccurred())
By("Ensuring resource quota status captures persistent volume claimcreation") By("Ensuring resource quota status captures persistent volume claim creation")
usedResources = v1.ResourceList{} usedResources = v1.ResourceList{}
usedResources[v1.ResourcePersistentVolumeClaims] = resource.MustParse("1") usedResources[v1.ResourcePersistentVolumeClaims] = resource.MustParse("1")
usedResources[v1.ResourceRequestsStorage] = resource.MustParse("1Gi") usedResources[v1.ResourceRequestsStorage] = resource.MustParse("1Gi")
...@@ -306,6 +308,56 @@ var _ = framework.KubeDescribe("ResourceQuota", func() { ...@@ -306,6 +308,56 @@ var _ = framework.KubeDescribe("ResourceQuota", func() {
Expect(err).NotTo(HaveOccurred()) Expect(err).NotTo(HaveOccurred())
}) })
It("should create a ResourceQuota and capture the life of a persistent volume claim with a storage class.", func() {
By("Creating a ResourceQuota")
quotaName := "test-quota"
resourceQuota := newTestResourceQuota(quotaName)
resourceQuota, err := createResourceQuota(f.ClientSet, f.Namespace.Name, resourceQuota)
Expect(err).NotTo(HaveOccurred())
By("Ensuring resource quota status is calculated")
usedResources := v1.ResourceList{}
usedResources[v1.ResourceQuotas] = resource.MustParse("1")
usedResources[v1.ResourcePersistentVolumeClaims] = resource.MustParse("0")
usedResources[v1.ResourceRequestsStorage] = resource.MustParse("0")
usedResources[core.V1ResourceByStorageClass("gold", v1.ResourcePersistentVolumeClaims)] = resource.MustParse("0")
usedResources[core.V1ResourceByStorageClass("gold", v1.ResourceRequestsStorage)] = resource.MustParse("0")
err = waitForResourceQuota(f.ClientSet, f.Namespace.Name, quotaName, usedResources)
Expect(err).NotTo(HaveOccurred())
By("Creating a PersistentVolumeClaim with storage class")
pvc := newTestPersistentVolumeClaimForQuota("test-claim")
pvc.Annotations = map[string]string{
util.StorageClassAnnotation: "gold",
}
pvc, err = f.ClientSet.Core().PersistentVolumeClaims(f.Namespace.Name).Create(pvc)
Expect(err).NotTo(HaveOccurred())
By("Ensuring resource quota status captures persistent volume claim creation")
usedResources = v1.ResourceList{}
usedResources[v1.ResourcePersistentVolumeClaims] = resource.MustParse("1")
usedResources[v1.ResourceRequestsStorage] = resource.MustParse("1Gi")
usedResources[core.V1ResourceByStorageClass("gold", v1.ResourcePersistentVolumeClaims)] = resource.MustParse("1")
usedResources[core.V1ResourceByStorageClass("gold", v1.ResourceRequestsStorage)] = resource.MustParse("1Gi")
err = waitForResourceQuota(f.ClientSet, f.Namespace.Name, quotaName, usedResources)
Expect(err).NotTo(HaveOccurred())
By("Deleting a PersistentVolumeClaim")
err = f.ClientSet.Core().PersistentVolumeClaims(f.Namespace.Name).Delete(pvc.Name, nil)
Expect(err).NotTo(HaveOccurred())
By("Ensuring resource quota status released usage")
usedResources[v1.ResourcePersistentVolumeClaims] = resource.MustParse("0")
usedResources[v1.ResourceRequestsStorage] = resource.MustParse("0")
usedResources[core.V1ResourceByStorageClass("gold", v1.ResourcePersistentVolumeClaims)] = resource.MustParse("0")
usedResources[core.V1ResourceByStorageClass("gold", v1.ResourceRequestsStorage)] = resource.MustParse("0")
err = waitForResourceQuota(f.ClientSet, f.Namespace.Name, quotaName, usedResources)
Expect(err).NotTo(HaveOccurred())
})
It("should verify ResourceQuota with terminating scopes.", func() { It("should verify ResourceQuota with terminating scopes.", func() {
By("Creating a ResourceQuota with terminating scope") By("Creating a ResourceQuota with terminating scope")
quotaTerminatingName := "quota-terminating" quotaTerminatingName := "quota-terminating"
...@@ -517,6 +569,8 @@ func newTestResourceQuota(name string) *v1.ResourceQuota { ...@@ -517,6 +569,8 @@ func newTestResourceQuota(name string) *v1.ResourceQuota {
hard[v1.ResourceSecrets] = resource.MustParse("10") hard[v1.ResourceSecrets] = resource.MustParse("10")
hard[v1.ResourcePersistentVolumeClaims] = resource.MustParse("10") hard[v1.ResourcePersistentVolumeClaims] = resource.MustParse("10")
hard[v1.ResourceRequestsStorage] = resource.MustParse("10Gi") hard[v1.ResourceRequestsStorage] = resource.MustParse("10Gi")
hard[core.V1ResourceByStorageClass("gold", v1.ResourcePersistentVolumeClaims)] = resource.MustParse("10")
hard[core.V1ResourceByStorageClass("gold", v1.ResourceRequestsStorage)] = resource.MustParse("10Gi")
return &v1.ResourceQuota{ return &v1.ResourceQuota{
ObjectMeta: v1.ObjectMeta{Name: name}, ObjectMeta: v1.ObjectMeta{Name: name},
Spec: v1.ResourceQuotaSpec{Hard: hard}, Spec: v1.ResourceQuotaSpec{Hard: hard},
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment