Merge pull request #54320 from derekwaynecarr/quota-update

Automatic merge from submit-queue (batch tested with PRs 54331, 54655, 54320, 54639, 54288). If you want to cherry-pick this change to another branch, please follow the instructions <a href="https://github.com/kubernetes/community/blob/master/contributors/devel/cherry-picks.md">here</a>.

Ability to do object count quota for all namespaced resources

**What this PR does / why we need it**:
- Defines syntax for generic object count quota `count/<resource>.<group>`
- Migrates existing objects to support new syntax with old syntax
- Adds support to quota all standard namespace resources 
- Updates the controller to do discovery and replenishment on those resources
- Updates unit tests
- Tweaks admission configuration around quota
- Add e2e test for replicasets (demonstrate dynamic generic counting)

```
$  kubectl create quota test --hard=count/deployments.extensions=2,count/replicasets.extensions=4,count/pods=3,count/secrets=4
resourcequota "test" created
$ kubectl run nginx --image=nginx --replicas=2
$ kubectl describe quota
Name:                         test
Namespace:                    default
Resource                      Used  Hard
--------                      ----  ----
count/deployments.extensions  1     2
count/pods                    2     3
count/replicasets.extensions  1     4
count/secrets                 1     4
```

**Special notes for your reviewer**:
- simple object count quotas no longer require writing code
- deferring support for custom resources pending investigation about how to share caches with garbage collector.  in addition, i would like to see how this integrates with downstream quota usage in openshift.

**Release note**:
```release-note
Object count quotas supported on all standard resources using `count/<resource>.<group>` syntax
```
This commit is contained in:
Kubernetes Submit Queue 2017-10-27 15:42:24 -07:00 committed by GitHub
commit 27ef37a32d
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
40 changed files with 1334 additions and 1642 deletions

View File

@ -506,11 +506,9 @@ func BuildAdmissionPluginInitializer(s *options.ServerRunOptions, client interna
// TODO: use a dynamic restmapper. See https://github.com/kubernetes/kubernetes/pull/42615.
restMapper := legacyscheme.Registry.RESTMapper()
// NOTE: we do not provide informers to the quota registry because admission level decisions
// do not require us to open watches for all items tracked by quota.
quotaRegistry := quotainstall.NewRegistry(nil, nil)
quotaConfiguration := quotainstall.NewQuotaConfigurationForAdmission()
pluginInitializer := kubeapiserveradmission.NewPluginInitializer(client, sharedInformers, cloudConfig, restMapper, quotaRegistry, webhookAuthWrapper, serviceResolver)
pluginInitializer := kubeapiserveradmission.NewPluginInitializer(client, sharedInformers, cloudConfig, restMapper, quotaConfiguration, webhookAuthWrapper, serviceResolver)
return pluginInitializer, nil
}

View File

@ -24,7 +24,6 @@ go_library(
importpath = "k8s.io/kubernetes/cmd/kube-controller-manager/app",
deps = [
"//cmd/kube-controller-manager/app/options:go_default_library",
"//pkg/api:go_default_library",
"//pkg/api/install:go_default_library",
"//pkg/api/legacyscheme:go_default_library",
"//pkg/apis/apps/install:go_default_library",
@ -78,6 +77,7 @@ go_library(
"//pkg/controller/volume/expand:go_default_library",
"//pkg/controller/volume/persistentvolume:go_default_library",
"//pkg/features:go_default_library",
"//pkg/quota/generic:go_default_library",
"//pkg/quota/install:go_default_library",
"//pkg/serviceaccount:go_default_library",
"//pkg/util/configz:go_default_library",

View File

@ -36,7 +36,6 @@ import (
cacheddiscovery "k8s.io/client-go/discovery/cached"
"k8s.io/client-go/dynamic"
clientset "k8s.io/client-go/kubernetes"
"k8s.io/kubernetes/pkg/api"
"k8s.io/kubernetes/pkg/api/legacyscheme"
"k8s.io/kubernetes/pkg/controller"
endpointcontroller "k8s.io/kubernetes/pkg/controller/endpoint"
@ -55,6 +54,7 @@ import (
"k8s.io/kubernetes/pkg/controller/volume/expand"
persistentvolumecontroller "k8s.io/kubernetes/pkg/controller/volume/persistentvolume"
"k8s.io/kubernetes/pkg/features"
"k8s.io/kubernetes/pkg/quota/generic"
quotainstall "k8s.io/kubernetes/pkg/quota/install"
"k8s.io/kubernetes/pkg/util/metrics"
)
@ -240,31 +240,34 @@ func startPodGCController(ctx ControllerContext) (bool, error) {
func startResourceQuotaController(ctx ControllerContext) (bool, error) {
resourceQuotaControllerClient := ctx.ClientBuilder.ClientOrDie("resourcequota-controller")
resourceQuotaRegistry := quotainstall.NewRegistry(resourceQuotaControllerClient, ctx.InformerFactory)
groupKindsToReplenish := []schema.GroupKind{
api.Kind("Pod"),
api.Kind("Service"),
api.Kind("ReplicationController"),
api.Kind("PersistentVolumeClaim"),
api.Kind("Secret"),
api.Kind("ConfigMap"),
}
discoveryFunc := resourceQuotaControllerClient.Discovery().ServerPreferredNamespacedResources
listerFuncForResource := generic.ListerFuncForResourceFunc(ctx.InformerFactory.ForResource)
quotaConfiguration := quotainstall.NewQuotaConfigurationForControllers(listerFuncForResource)
resourceQuotaControllerOptions := &resourcequotacontroller.ResourceQuotaControllerOptions{
QuotaClient: resourceQuotaControllerClient.CoreV1(),
ResourceQuotaInformer: ctx.InformerFactory.Core().V1().ResourceQuotas(),
ResyncPeriod: controller.StaticResyncPeriodFunc(ctx.Options.ResourceQuotaSyncPeriod.Duration),
Registry: resourceQuotaRegistry,
ControllerFactory: resourcequotacontroller.NewReplenishmentControllerFactory(ctx.InformerFactory),
InformerFactory: ctx.InformerFactory,
ReplenishmentResyncPeriod: ResyncPeriod(&ctx.Options),
GroupKindsToReplenish: groupKindsToReplenish,
DiscoveryFunc: discoveryFunc,
IgnoredResourcesFunc: quotaConfiguration.IgnoredResources,
InformersStarted: ctx.InformersStarted,
Registry: generic.NewRegistry(quotaConfiguration.Evaluators()),
}
if resourceQuotaControllerClient.CoreV1().RESTClient().GetRateLimiter() != nil {
metrics.RegisterMetricAndTrackRateLimiterUsage("resource_quota_controller", resourceQuotaControllerClient.CoreV1().RESTClient().GetRateLimiter())
}
go resourcequotacontroller.NewResourceQuotaController(
resourceQuotaControllerOptions,
).Run(int(ctx.Options.ConcurrentResourceQuotaSyncs), ctx.Stop)
resourceQuotaController, err := resourcequotacontroller.NewResourceQuotaController(resourceQuotaControllerOptions)
if err != nil {
return false, err
}
go resourceQuotaController.Run(int(ctx.Options.ConcurrentResourceQuotaSyncs), ctx.Stop)
// Periodically the quota controller to detect new resource types
go resourceQuotaController.Sync(discoveryFunc, 30*time.Second, ctx.Stop)
return true, nil
}

View File

@ -235,7 +235,6 @@ pkg/proxy/util
pkg/proxy/winkernel
pkg/proxy/winuserspace
pkg/quota/evaluator/core
pkg/quota/generic
pkg/registry/admissionregistration/externaladmissionhookconfiguration/storage
pkg/registry/admissionregistration/initializerconfiguration/storage
pkg/registry/admissionregistration/rest

View File

@ -10,8 +10,8 @@ go_library(
name = "go_default_library",
srcs = [
"doc.go",
"replenishment_controller.go",
"resource_quota_controller.go",
"resource_quota_monitor.go",
],
importpath = "k8s.io/kubernetes/pkg/controller/resourcequota",
deps = [
@ -20,6 +20,7 @@ go_library(
"//pkg/controller:go_default_library",
"//pkg/quota:go_default_library",
"//pkg/quota/evaluator/core:go_default_library",
"//pkg/quota/generic:go_default_library",
"//vendor/github.com/golang/glog:go_default_library",
"//vendor/k8s.io/api/core/v1:go_default_library",
"//vendor/k8s.io/apimachinery/pkg/api/equality:go_default_library",
@ -27,11 +28,12 @@ go_library(
"//vendor/k8s.io/apimachinery/pkg/api/meta:go_default_library",
"//vendor/k8s.io/apimachinery/pkg/apis/meta/v1:go_default_library",
"//vendor/k8s.io/apimachinery/pkg/labels:go_default_library",
"//vendor/k8s.io/apimachinery/pkg/runtime:go_default_library",
"//vendor/k8s.io/apimachinery/pkg/runtime/schema:go_default_library",
"//vendor/k8s.io/apimachinery/pkg/util/clock:go_default_library",
"//vendor/k8s.io/apimachinery/pkg/util/errors:go_default_library",
"//vendor/k8s.io/apimachinery/pkg/util/runtime:go_default_library",
"//vendor/k8s.io/apimachinery/pkg/util/wait:go_default_library",
"//vendor/k8s.io/client-go/discovery:go_default_library",
"//vendor/k8s.io/client-go/informers:go_default_library",
"//vendor/k8s.io/client-go/informers/core/v1:go_default_library",
"//vendor/k8s.io/client-go/kubernetes/typed/core/v1:go_default_library",
@ -43,15 +45,12 @@ go_library(
go_test(
name = "go_default_test",
srcs = [
"replenishment_controller_test.go",
"resource_quota_controller_test.go",
],
srcs = ["resource_quota_controller_test.go"],
importpath = "k8s.io/kubernetes/pkg/controller/resourcequota",
library = ":go_default_library",
deps = [
"//pkg/api:go_default_library",
"//pkg/controller:go_default_library",
"//pkg/quota:go_default_library",
"//pkg/quota/generic:go_default_library",
"//pkg/quota/install:go_default_library",
"//vendor/k8s.io/api/core/v1:go_default_library",
@ -59,12 +58,12 @@ go_test(
"//vendor/k8s.io/apimachinery/pkg/apis/meta/v1:go_default_library",
"//vendor/k8s.io/apimachinery/pkg/runtime:go_default_library",
"//vendor/k8s.io/apimachinery/pkg/runtime/schema:go_default_library",
"//vendor/k8s.io/apimachinery/pkg/util/clock:go_default_library",
"//vendor/k8s.io/apimachinery/pkg/util/intstr:go_default_library",
"//vendor/k8s.io/apimachinery/pkg/util/sets:go_default_library",
"//vendor/k8s.io/client-go/informers:go_default_library",
"//vendor/k8s.io/client-go/kubernetes:go_default_library",
"//vendor/k8s.io/client-go/kubernetes/fake:go_default_library",
"//vendor/k8s.io/client-go/testing:go_default_library",
"//vendor/k8s.io/client-go/tools/cache:go_default_library",
],
)

View File

@ -1,233 +0,0 @@
/*
Copyright 2015 The Kubernetes Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package resourcequota
import (
"fmt"
"github.com/golang/glog"
"k8s.io/api/core/v1"
"k8s.io/apimachinery/pkg/api/meta"
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/apimachinery/pkg/runtime/schema"
"k8s.io/apimachinery/pkg/util/clock"
utilruntime "k8s.io/apimachinery/pkg/util/runtime"
"k8s.io/client-go/informers"
"k8s.io/client-go/tools/cache"
"k8s.io/kubernetes/pkg/api"
"k8s.io/kubernetes/pkg/controller"
"k8s.io/kubernetes/pkg/quota/evaluator/core"
)
// ReplenishmentFunc is a function that is invoked when controller sees a change
// that may require a quota to be replenished (i.e. object deletion, or object moved to terminal state)
type ReplenishmentFunc func(groupKind schema.GroupKind, namespace string, object runtime.Object)
// ReplenishmentControllerOptions is an options struct that tells a factory
// how to configure a controller that can inform the quota system it should
// replenish quota
type ReplenishmentControllerOptions struct {
// The kind monitored for replenishment
GroupKind schema.GroupKind
// The period that should be used to re-sync the monitored resource
ResyncPeriod controller.ResyncPeriodFunc
// The function to invoke when a change is observed that should trigger
// replenishment
ReplenishmentFunc ReplenishmentFunc
}
// PodReplenishmentUpdateFunc will replenish if the old pod was quota tracked but the new is not
func PodReplenishmentUpdateFunc(options *ReplenishmentControllerOptions, clock clock.Clock) func(oldObj, newObj interface{}) {
return func(oldObj, newObj interface{}) {
oldPod := oldObj.(*v1.Pod)
newPod := newObj.(*v1.Pod)
if core.QuotaV1Pod(oldPod, clock) && !core.QuotaV1Pod(newPod, clock) {
options.ReplenishmentFunc(options.GroupKind, newPod.Namespace, oldPod)
}
}
}
// ObjectReplenishmentDeleteFunc will replenish on every delete
func ObjectReplenishmentDeleteFunc(options *ReplenishmentControllerOptions) func(obj interface{}) {
return func(obj interface{}) {
metaObject, err := meta.Accessor(obj)
if err != nil {
tombstone, ok := obj.(cache.DeletedFinalStateUnknown)
if !ok {
glog.Errorf("replenishment controller could not get object from tombstone %+v, could take up to %v before quota is replenished", obj, options.ResyncPeriod())
utilruntime.HandleError(err)
return
}
metaObject, err = meta.Accessor(tombstone.Obj)
if err != nil {
glog.Errorf("replenishment controller tombstone contained object that is not a meta %+v, could take up to %v before quota is replenished", tombstone.Obj, options.ResyncPeriod())
utilruntime.HandleError(err)
return
}
}
options.ReplenishmentFunc(options.GroupKind, metaObject.GetNamespace(), nil)
}
}
// ReplenishmentControllerFactory knows how to build replenishment controllers
type ReplenishmentControllerFactory interface {
// NewController returns a controller configured with the specified options.
// This method is NOT thread-safe.
NewController(options *ReplenishmentControllerOptions) (cache.Controller, error)
}
// replenishmentControllerFactory implements ReplenishmentControllerFactory
type replenishmentControllerFactory struct {
sharedInformerFactory informers.SharedInformerFactory
}
// NewReplenishmentControllerFactory returns a factory that knows how to build controllers
// to replenish resources when updated or deleted
func NewReplenishmentControllerFactory(f informers.SharedInformerFactory) ReplenishmentControllerFactory {
return &replenishmentControllerFactory{
sharedInformerFactory: f,
}
}
func (r *replenishmentControllerFactory) NewController(options *ReplenishmentControllerOptions) (cache.Controller, error) {
var (
informer informers.GenericInformer
err error
)
switch options.GroupKind {
case api.Kind("Pod"):
informer, err = r.sharedInformerFactory.ForResource(v1.SchemeGroupVersion.WithResource("pods"))
if err != nil {
return nil, err
}
clock := clock.RealClock{}
informer.Informer().AddEventHandlerWithResyncPeriod(
cache.ResourceEventHandlerFuncs{
UpdateFunc: PodReplenishmentUpdateFunc(options, clock),
DeleteFunc: ObjectReplenishmentDeleteFunc(options),
},
options.ResyncPeriod(),
)
case api.Kind("Service"):
informer, err = r.sharedInformerFactory.ForResource(v1.SchemeGroupVersion.WithResource("services"))
if err != nil {
return nil, err
}
informer.Informer().AddEventHandlerWithResyncPeriod(
cache.ResourceEventHandlerFuncs{
UpdateFunc: ServiceReplenishmentUpdateFunc(options),
DeleteFunc: ObjectReplenishmentDeleteFunc(options),
},
options.ResyncPeriod(),
)
case api.Kind("ReplicationController"):
informer, err = r.sharedInformerFactory.ForResource(v1.SchemeGroupVersion.WithResource("replicationcontrollers"))
if err != nil {
return nil, err
}
informer.Informer().AddEventHandlerWithResyncPeriod(
cache.ResourceEventHandlerFuncs{
DeleteFunc: ObjectReplenishmentDeleteFunc(options),
},
options.ResyncPeriod(),
)
case api.Kind("PersistentVolumeClaim"):
informer, err = r.sharedInformerFactory.ForResource(v1.SchemeGroupVersion.WithResource("persistentvolumeclaims"))
if err != nil {
return nil, err
}
informer.Informer().AddEventHandlerWithResyncPeriod(
cache.ResourceEventHandlerFuncs{
DeleteFunc: ObjectReplenishmentDeleteFunc(options),
},
options.ResyncPeriod(),
)
case api.Kind("Secret"):
informer, err = r.sharedInformerFactory.ForResource(v1.SchemeGroupVersion.WithResource("secrets"))
if err != nil {
return nil, err
}
informer.Informer().AddEventHandlerWithResyncPeriod(
cache.ResourceEventHandlerFuncs{
DeleteFunc: ObjectReplenishmentDeleteFunc(options),
},
options.ResyncPeriod(),
)
case api.Kind("ConfigMap"):
informer, err = r.sharedInformerFactory.ForResource(v1.SchemeGroupVersion.WithResource("configmaps"))
if err != nil {
return nil, err
}
informer.Informer().AddEventHandlerWithResyncPeriod(
cache.ResourceEventHandlerFuncs{
DeleteFunc: ObjectReplenishmentDeleteFunc(options),
},
options.ResyncPeriod(),
)
default:
return nil, NewUnhandledGroupKindError(options.GroupKind)
}
return informer.Informer().GetController(), nil
}
// ServiceReplenishmentUpdateFunc will replenish if the service was quota tracked has changed service type
func ServiceReplenishmentUpdateFunc(options *ReplenishmentControllerOptions) func(oldObj, newObj interface{}) {
return func(oldObj, newObj interface{}) {
oldService := oldObj.(*v1.Service)
newService := newObj.(*v1.Service)
if core.GetQuotaServiceType(oldService) != core.GetQuotaServiceType(newService) {
options.ReplenishmentFunc(options.GroupKind, newService.Namespace, nil)
}
}
}
type unhandledKindErr struct {
kind schema.GroupKind
}
func (e unhandledKindErr) Error() string {
return fmt.Sprintf("no replenishment controller available for %s", e.kind)
}
func NewUnhandledGroupKindError(kind schema.GroupKind) error {
return unhandledKindErr{kind: kind}
}
func IsUnhandledGroupKindError(err error) bool {
if err == nil {
return false
}
_, ok := err.(unhandledKindErr)
return ok
}
// UnionReplenishmentControllerFactory iterates through its constituent factories ignoring, UnhandledGroupKindErrors
// returning the first success or failure it hits. If there are no hits either way, it return an UnhandledGroupKind error
type UnionReplenishmentControllerFactory []ReplenishmentControllerFactory
func (f UnionReplenishmentControllerFactory) NewController(options *ReplenishmentControllerOptions) (cache.Controller, error) {
for _, factory := range f {
controller, err := factory.NewController(options)
if !IsUnhandledGroupKindError(err) {
return controller, err
}
}
return nil, NewUnhandledGroupKindError(options.GroupKind)
}

View File

@ -1,160 +0,0 @@
/*
Copyright 2016 The Kubernetes Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package resourcequota
import (
"testing"
"time"
"k8s.io/api/core/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/apimachinery/pkg/runtime/schema"
"k8s.io/apimachinery/pkg/util/clock"
"k8s.io/apimachinery/pkg/util/intstr"
"k8s.io/kubernetes/pkg/api"
"k8s.io/kubernetes/pkg/controller"
)
// testReplenishment lets us test replenishment functions are invoked
type testReplenishment struct {
groupKind schema.GroupKind
namespace string
}
// mock function that holds onto the last kind that was replenished
func (t *testReplenishment) Replenish(groupKind schema.GroupKind, namespace string, object runtime.Object) {
t.groupKind = groupKind
t.namespace = namespace
}
func TestPodReplenishmentUpdateFunc(t *testing.T) {
mockReplenish := &testReplenishment{}
options := ReplenishmentControllerOptions{
GroupKind: api.Kind("Pod"),
ReplenishmentFunc: mockReplenish.Replenish,
ResyncPeriod: controller.NoResyncPeriodFunc,
}
oldPod := &v1.Pod{
ObjectMeta: metav1.ObjectMeta{Namespace: "test", Name: "pod"},
Status: v1.PodStatus{Phase: v1.PodRunning},
}
newPod := &v1.Pod{
ObjectMeta: metav1.ObjectMeta{Namespace: "test", Name: "pod"},
Status: v1.PodStatus{Phase: v1.PodFailed},
}
fakeClock := clock.NewFakeClock(time.Now())
updateFunc := PodReplenishmentUpdateFunc(&options, fakeClock)
updateFunc(oldPod, newPod)
if mockReplenish.groupKind != api.Kind("Pod") {
t.Errorf("Unexpected group kind %v", mockReplenish.groupKind)
}
if mockReplenish.namespace != oldPod.Namespace {
t.Errorf("Unexpected namespace %v", mockReplenish.namespace)
}
}
func TestObjectReplenishmentDeleteFunc(t *testing.T) {
mockReplenish := &testReplenishment{}
options := ReplenishmentControllerOptions{
GroupKind: api.Kind("Pod"),
ReplenishmentFunc: mockReplenish.Replenish,
ResyncPeriod: controller.NoResyncPeriodFunc,
}
oldPod := &v1.Pod{
ObjectMeta: metav1.ObjectMeta{Namespace: "test", Name: "pod"},
Status: v1.PodStatus{Phase: v1.PodRunning},
}
deleteFunc := ObjectReplenishmentDeleteFunc(&options)
deleteFunc(oldPod)
if mockReplenish.groupKind != api.Kind("Pod") {
t.Errorf("Unexpected group kind %v", mockReplenish.groupKind)
}
if mockReplenish.namespace != oldPod.Namespace {
t.Errorf("Unexpected namespace %v", mockReplenish.namespace)
}
}
func TestServiceReplenishmentUpdateFunc(t *testing.T) {
mockReplenish := &testReplenishment{}
options := ReplenishmentControllerOptions{
GroupKind: api.Kind("Service"),
ReplenishmentFunc: mockReplenish.Replenish,
ResyncPeriod: controller.NoResyncPeriodFunc,
}
oldService := &v1.Service{
ObjectMeta: metav1.ObjectMeta{Namespace: "test", Name: "mysvc"},
Spec: v1.ServiceSpec{
Type: v1.ServiceTypeNodePort,
Ports: []v1.ServicePort{{
Port: 80,
TargetPort: intstr.FromInt(80),
}},
},
}
newService := &v1.Service{
ObjectMeta: metav1.ObjectMeta{Namespace: "test", Name: "mysvc"},
Spec: v1.ServiceSpec{
Type: v1.ServiceTypeClusterIP,
Ports: []v1.ServicePort{{
Port: 80,
TargetPort: intstr.FromInt(80),
}}},
}
updateFunc := ServiceReplenishmentUpdateFunc(&options)
updateFunc(oldService, newService)
if mockReplenish.groupKind != api.Kind("Service") {
t.Errorf("Unexpected group kind %v", mockReplenish.groupKind)
}
if mockReplenish.namespace != oldService.Namespace {
t.Errorf("Unexpected namespace %v", mockReplenish.namespace)
}
mockReplenish = &testReplenishment{}
options = ReplenishmentControllerOptions{
GroupKind: api.Kind("Service"),
ReplenishmentFunc: mockReplenish.Replenish,
ResyncPeriod: controller.NoResyncPeriodFunc,
}
oldService = &v1.Service{
ObjectMeta: metav1.ObjectMeta{Namespace: "test", Name: "mysvc"},
Spec: v1.ServiceSpec{
Type: v1.ServiceTypeNodePort,
Ports: []v1.ServicePort{{
Port: 80,
TargetPort: intstr.FromInt(80),
}},
},
}
newService = &v1.Service{
ObjectMeta: metav1.ObjectMeta{Namespace: "test", Name: "mysvc"},
Spec: v1.ServiceSpec{
Type: v1.ServiceTypeNodePort,
Ports: []v1.ServicePort{{
Port: 81,
TargetPort: intstr.FromInt(81),
}}},
}
updateFunc = ServiceReplenishmentUpdateFunc(&options)
updateFunc(oldService, newService)
if mockReplenish.groupKind == api.Kind("Service") {
t.Errorf("Unexpected group kind %v", mockReplenish.groupKind)
}
if mockReplenish.namespace == oldService.Namespace {
t.Errorf("Unexpected namespace %v", mockReplenish.namespace)
}
}

View File

@ -18,6 +18,8 @@ package resourcequota
import (
"fmt"
"reflect"
"sync"
"time"
"github.com/golang/glog"
@ -27,10 +29,11 @@ import (
"k8s.io/apimachinery/pkg/api/errors"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/labels"
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/apimachinery/pkg/runtime/schema"
utilruntime "k8s.io/apimachinery/pkg/util/runtime"
"k8s.io/apimachinery/pkg/util/wait"
"k8s.io/client-go/discovery"
"k8s.io/client-go/informers"
coreinformers "k8s.io/client-go/informers/core/v1"
corev1client "k8s.io/client-go/kubernetes/typed/core/v1"
corelisters "k8s.io/client-go/listers/core/v1"
@ -42,6 +45,19 @@ import (
"k8s.io/kubernetes/pkg/quota"
)
// NamespacedResourcesFunc knows how to discover namespaced resources.
type NamespacedResourcesFunc func() ([]*metav1.APIResourceList, error)
// ReplenishmentFunc is a signal that a resource changed in specified namespace
// that may require quota to be recalculated.
type ReplenishmentFunc func(groupResource schema.GroupResource, namespace string)
// InformerFactory is all the quota system needs to interface with informers.
type InformerFactory interface {
ForResource(resource schema.GroupVersionResource) (informers.GenericInformer, error)
Start(stopCh <-chan struct{})
}
// ResourceQuotaControllerOptions holds options for creating a quota controller
type ResourceQuotaControllerOptions struct {
// Must have authority to list all quotas, and update quota status
@ -50,15 +66,18 @@ type ResourceQuotaControllerOptions struct {
ResourceQuotaInformer coreinformers.ResourceQuotaInformer
// Controls full recalculation of quota usage
ResyncPeriod controller.ResyncPeriodFunc
// Knows how to calculate usage
// Maintains evaluators that know how to calculate usage for group resource
Registry quota.Registry
// Knows how to build controllers that notify replenishment events
ControllerFactory ReplenishmentControllerFactory
// Discover list of supported resources on the server.
DiscoveryFunc NamespacedResourcesFunc
// A function that returns the list of resources to ignore
IgnoredResourcesFunc func() map[schema.GroupResource]struct{}
// InformersStarted knows if informers were started.
InformersStarted <-chan struct{}
// InformerFactory interfaces with informers.
InformerFactory InformerFactory
// Controls full resync of objects monitored for replenishment.
ReplenishmentResyncPeriod controller.ResyncPeriodFunc
// List of GroupKind objects that should be monitored for replenishment at
// a faster frequency than the quota controller recalculation interval
GroupKindsToReplenish []schema.GroupKind
}
// ResourceQuotaController is responsible for tracking quota usage status in the system
@ -79,9 +98,16 @@ type ResourceQuotaController struct {
resyncPeriod controller.ResyncPeriodFunc
// knows how to calculate usage
registry quota.Registry
// knows how to monitor all the resources tracked by quota and trigger replenishment
quotaMonitor *QuotaMonitor
// controls the workers that process quotas
// this lock is acquired to control write access to the monitors and ensures that all
// monitors are synced before the controller can process quotas.
workerLock sync.RWMutex
}
func NewResourceQuotaController(options *ResourceQuotaControllerOptions) *ResourceQuotaController {
// NewResourceQuotaController creates a quota controller with specified options
func NewResourceQuotaController(options *ResourceQuotaControllerOptions) (*ResourceQuotaController, error) {
// build the resource quota controller
rq := &ResourceQuotaController{
rqClient: options.QuotaClient,
@ -122,21 +148,30 @@ func NewResourceQuotaController(options *ResourceQuotaControllerOptions) *Resour
rq.resyncPeriod(),
)
for _, groupKindToReplenish := range options.GroupKindsToReplenish {
controllerOptions := &ReplenishmentControllerOptions{
GroupKind: groupKindToReplenish,
ResyncPeriod: options.ReplenishmentResyncPeriod,
ReplenishmentFunc: rq.replenishQuota,
}
replenishmentController, err := options.ControllerFactory.NewController(controllerOptions)
if err != nil {
glog.Warningf("quota controller unable to replenish %s due to %v, changes only accounted during full resync", groupKindToReplenish, err)
} else {
// make sure we wait for each shared informer's cache to sync
rq.informerSyncedFuncs = append(rq.informerSyncedFuncs, replenishmentController.HasSynced)
}
qm := &QuotaMonitor{
informersStarted: options.InformersStarted,
informerFactory: options.InformerFactory,
ignoredResources: options.IgnoredResourcesFunc(),
resourceChanges: workqueue.NewNamedRateLimitingQueue(workqueue.DefaultControllerRateLimiter(), "resource_quota_controller_resource_changes"),
resyncPeriod: options.ReplenishmentResyncPeriod,
replenishmentFunc: rq.replenishQuota,
registry: rq.registry,
}
return rq
rq.quotaMonitor = qm
// do initial quota monitor setup
resources, err := GetQuotableResources(options.DiscoveryFunc)
if err != nil {
return nil, err
}
if err = qm.syncMonitors(resources); err != nil {
utilruntime.HandleError(fmt.Errorf("initial monitor sync has error: %v", err))
}
// only start quota once all informers synced
rq.informerSyncedFuncs = append(rq.informerSyncedFuncs, qm.IsSynced)
return rq, nil
}
// enqueueAll is called at the fullResyncPeriod interval to force a full recalculation of quota usage statistics
@ -186,7 +221,7 @@ func (rq *ResourceQuotaController) addQuota(obj interface{}) {
for constraint := range resourceQuota.Status.Hard {
if _, usageFound := resourceQuota.Status.Used[constraint]; !usageFound {
matchedResources := []api.ResourceName{api.ResourceName(constraint)}
for _, evaluator := range rq.registry.Evaluators() {
for _, evaluator := range rq.registry.List() {
if intersection := evaluator.MatchingResources(matchedResources); len(intersection) > 0 {
rq.missingUsageQueue.Add(key)
return
@ -202,6 +237,10 @@ func (rq *ResourceQuotaController) addQuota(obj interface{}) {
// worker runs a worker thread that just dequeues items, processes them, and marks them done.
func (rq *ResourceQuotaController) worker(queue workqueue.RateLimitingInterface) func() {
workFunc := func() bool {
rq.workerLock.RLock()
defer rq.workerLock.RUnlock()
key, quit := queue.Get()
if quit {
return true
@ -235,6 +274,8 @@ func (rq *ResourceQuotaController) Run(workers int, stopCh <-chan struct{}) {
glog.Infof("Starting resource quota controller")
defer glog.Infof("Shutting down resource quota controller")
go rq.quotaMonitor.Run(stopCh)
if !controller.WaitForCacheSync("resource quota", stopCh, rq.informerSyncedFuncs...) {
return
}
@ -336,11 +377,10 @@ func (rq *ResourceQuotaController) syncResourceQuota(v1ResourceQuota *v1.Resourc
}
// replenishQuota is a replenishment function invoked by a controller to notify that a quota should be recalculated
func (rq *ResourceQuotaController) replenishQuota(groupKind schema.GroupKind, namespace string, object runtime.Object) {
// check if the quota controller can evaluate this kind, if not, ignore it altogether...
evaluators := rq.registry.Evaluators()
evaluator, found := evaluators[groupKind]
if !found {
func (rq *ResourceQuotaController) replenishQuota(groupResource schema.GroupResource, namespace string) {
// check if the quota controller can evaluate this groupResource, if not, ignore it altogether...
evaluator := rq.registry.Get(groupResource)
if evaluator == nil {
return
}
@ -373,3 +413,66 @@ func (rq *ResourceQuotaController) replenishQuota(groupKind schema.GroupKind, na
}
}
}
// Sync periodically resyncs the controller when new resources are observed from discovery.
func (rq *ResourceQuotaController) Sync(discoveryFunc NamespacedResourcesFunc, period time.Duration, stopCh <-chan struct{}) {
// Something has changed, so track the new state and perform a sync.
oldResources := make(map[schema.GroupVersionResource]struct{})
wait.Until(func() {
// Get the current resource list from discovery.
newResources, err := GetQuotableResources(discoveryFunc)
if err != nil {
utilruntime.HandleError(err)
return
}
// Decide whether discovery has reported a change.
if reflect.DeepEqual(oldResources, newResources) {
glog.V(4).Infof("no resource updates from discovery, skipping resource quota sync")
return
}
// Something has changed, so track the new state and perform a sync.
glog.V(2).Infof("syncing resource quota controller with updated resources from discovery: %v", newResources)
oldResources = newResources
// Ensure workers are paused to avoid processing events before informers
// have resynced.
rq.workerLock.Lock()
defer rq.workerLock.Unlock()
// Perform the monitor resync and wait for controllers to report cache sync.
if err := rq.resyncMonitors(newResources); err != nil {
utilruntime.HandleError(fmt.Errorf("failed to sync resource monitors: %v", err))
return
}
if !controller.WaitForCacheSync("resource quota", stopCh, rq.quotaMonitor.IsSynced) {
utilruntime.HandleError(fmt.Errorf("timed out waiting for quota monitor sync"))
}
}, period, stopCh)
}
// resyncMonitors starts or stops quota monitors as needed to ensure that all
// (and only) those resources present in the map are monitored.
func (rq *ResourceQuotaController) resyncMonitors(resources map[schema.GroupVersionResource]struct{}) error {
if err := rq.quotaMonitor.syncMonitors(resources); err != nil {
return err
}
rq.quotaMonitor.startMonitors()
return nil
}
// GetQuotableResources returns all resources that the quota system should recognize.
// It requires a resource supports the following verbs: 'create','list','delete'
func GetQuotableResources(discoveryFunc NamespacedResourcesFunc) (map[schema.GroupVersionResource]struct{}, error) {
possibleResources, err := discoveryFunc()
if err != nil {
return nil, fmt.Errorf("failed to discover resources: %v", err)
}
quotableResources := discovery.FilteredBy(discovery.SupportsAllVerbs{Verbs: []string{"create", "list", "delete"}}, possibleResources)
quotableGroupVersionResources, err := discovery.GroupVersionResources(quotableResources)
if err != nil {
return nil, fmt.Errorf("Failed to parse resources: %v", err)
}
return quotableGroupVersionResources, nil
}

View File

@ -17,19 +17,23 @@ limitations under the License.
package resourcequota
import (
"fmt"
"strings"
"testing"
"k8s.io/api/core/v1"
"k8s.io/apimachinery/pkg/api/resource"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/apimachinery/pkg/runtime/schema"
"k8s.io/apimachinery/pkg/util/sets"
"k8s.io/client-go/informers"
"k8s.io/client-go/kubernetes"
"k8s.io/client-go/kubernetes/fake"
core "k8s.io/client-go/testing"
"k8s.io/kubernetes/pkg/api"
"k8s.io/client-go/tools/cache"
"k8s.io/kubernetes/pkg/controller"
"k8s.io/kubernetes/pkg/quota"
"k8s.io/kubernetes/pkg/quota/generic"
"k8s.io/kubernetes/pkg/quota/install"
)
@ -52,383 +56,249 @@ func getResourceRequirements(requests, limits v1.ResourceList) v1.ResourceRequir
return res
}
func mockDiscoveryFunc() ([]*metav1.APIResourceList, error) {
return []*metav1.APIResourceList{}, nil
}
func mockListerForResourceFunc(listersForResource map[schema.GroupVersionResource]cache.GenericLister) quota.ListerForResourceFunc {
return func(gvr schema.GroupVersionResource) (cache.GenericLister, error) {
lister, found := listersForResource[gvr]
if !found {
return nil, fmt.Errorf("no lister found for resource")
}
return lister, nil
}
}
func newGenericLister(groupResource schema.GroupResource, items []runtime.Object) cache.GenericLister {
store := cache.NewIndexer(cache.MetaNamespaceKeyFunc, cache.Indexers{"namespace": cache.MetaNamespaceIndexFunc})
for _, item := range items {
store.Add(item)
}
return cache.NewGenericLister(store, groupResource)
}
type quotaController struct {
*ResourceQuotaController
stop chan struct{}
}
func setupQuotaController(t *testing.T, kubeClient kubernetes.Interface, lister quota.ListerForResourceFunc) quotaController {
informerFactory := informers.NewSharedInformerFactory(kubeClient, controller.NoResyncPeriodFunc())
quotaConfiguration := install.NewQuotaConfigurationForControllers(lister)
alwaysStarted := make(chan struct{})
close(alwaysStarted)
resourceQuotaControllerOptions := &ResourceQuotaControllerOptions{
QuotaClient: kubeClient.Core(),
ResourceQuotaInformer: informerFactory.Core().V1().ResourceQuotas(),
ResyncPeriod: controller.NoResyncPeriodFunc,
ReplenishmentResyncPeriod: controller.NoResyncPeriodFunc,
IgnoredResourcesFunc: quotaConfiguration.IgnoredResources,
DiscoveryFunc: mockDiscoveryFunc,
Registry: generic.NewRegistry(quotaConfiguration.Evaluators()),
InformersStarted: alwaysStarted,
}
qc, err := NewResourceQuotaController(resourceQuotaControllerOptions)
if err != nil {
t.Fatal(err)
}
stop := make(chan struct{})
go informerFactory.Start(stop)
return quotaController{qc, stop}
}
func newTestPods() []runtime.Object {
return []runtime.Object{
&v1.Pod{
ObjectMeta: metav1.ObjectMeta{Name: "pod-running", Namespace: "testing"},
Status: v1.PodStatus{Phase: v1.PodRunning},
Spec: v1.PodSpec{
Volumes: []v1.Volume{{Name: "vol"}},
Containers: []v1.Container{{Name: "ctr", Image: "image", Resources: getResourceRequirements(getResourceList("100m", "1Gi"), getResourceList("", ""))}},
},
},
&v1.Pod{
ObjectMeta: metav1.ObjectMeta{Name: "pod-running-2", Namespace: "testing"},
Status: v1.PodStatus{Phase: v1.PodRunning},
Spec: v1.PodSpec{
Volumes: []v1.Volume{{Name: "vol"}},
Containers: []v1.Container{{Name: "ctr", Image: "image", Resources: getResourceRequirements(getResourceList("100m", "1Gi"), getResourceList("", ""))}},
},
},
&v1.Pod{
ObjectMeta: metav1.ObjectMeta{Name: "pod-failed", Namespace: "testing"},
Status: v1.PodStatus{Phase: v1.PodFailed},
Spec: v1.PodSpec{
Volumes: []v1.Volume{{Name: "vol"}},
Containers: []v1.Container{{Name: "ctr", Image: "image", Resources: getResourceRequirements(getResourceList("100m", "1Gi"), getResourceList("", ""))}},
},
},
}
}
func TestSyncResourceQuota(t *testing.T) {
podList := v1.PodList{
Items: []v1.Pod{
{
ObjectMeta: metav1.ObjectMeta{Name: "pod-running", Namespace: "testing"},
Status: v1.PodStatus{Phase: v1.PodRunning},
Spec: v1.PodSpec{
Volumes: []v1.Volume{{Name: "vol"}},
Containers: []v1.Container{{Name: "ctr", Image: "image", Resources: getResourceRequirements(getResourceList("100m", "1Gi"), getResourceList("", ""))}},
testCases := map[string]struct {
gvr schema.GroupVersionResource
items []runtime.Object
quota v1.ResourceQuota
status v1.ResourceQuotaStatus
expectedActionSet sets.String
}{
"pods": {
gvr: v1.SchemeGroupVersion.WithResource("pods"),
quota: v1.ResourceQuota{
ObjectMeta: metav1.ObjectMeta{Name: "quota", Namespace: "testing"},
Spec: v1.ResourceQuotaSpec{
Hard: v1.ResourceList{
v1.ResourceCPU: resource.MustParse("3"),
v1.ResourceMemory: resource.MustParse("100Gi"),
v1.ResourcePods: resource.MustParse("5"),
},
},
},
{
ObjectMeta: metav1.ObjectMeta{Name: "pod-running-2", Namespace: "testing"},
Status: v1.PodStatus{Phase: v1.PodRunning},
Spec: v1.PodSpec{
Volumes: []v1.Volume{{Name: "vol"}},
Containers: []v1.Container{{Name: "ctr", Image: "image", Resources: getResourceRequirements(getResourceList("100m", "1Gi"), getResourceList("", ""))}},
status: v1.ResourceQuotaStatus{
Hard: v1.ResourceList{
v1.ResourceCPU: resource.MustParse("3"),
v1.ResourceMemory: resource.MustParse("100Gi"),
v1.ResourcePods: resource.MustParse("5"),
},
Used: v1.ResourceList{
v1.ResourceCPU: resource.MustParse("200m"),
v1.ResourceMemory: resource.MustParse("2Gi"),
v1.ResourcePods: resource.MustParse("2"),
},
},
{
ObjectMeta: metav1.ObjectMeta{Name: "pod-failed", Namespace: "testing"},
Status: v1.PodStatus{Phase: v1.PodFailed},
Spec: v1.PodSpec{
Volumes: []v1.Volume{{Name: "vol"}},
Containers: []v1.Container{{Name: "ctr", Image: "image", Resources: getResourceRequirements(getResourceList("100m", "1Gi"), getResourceList("", ""))}},
expectedActionSet: sets.NewString(
strings.Join([]string{"update", "resourcequotas", "status"}, "-"),
),
items: newTestPods(),
},
"quota-spec-hard-updated": {
gvr: v1.SchemeGroupVersion.WithResource("pods"),
quota: v1.ResourceQuota{
ObjectMeta: metav1.ObjectMeta{
Namespace: "default",
Name: "rq",
},
Spec: v1.ResourceQuotaSpec{
Hard: v1.ResourceList{
v1.ResourceCPU: resource.MustParse("4"),
},
},
Status: v1.ResourceQuotaStatus{
Hard: v1.ResourceList{
v1.ResourceCPU: resource.MustParse("3"),
},
Used: v1.ResourceList{
v1.ResourceCPU: resource.MustParse("0"),
},
},
},
status: v1.ResourceQuotaStatus{
Hard: v1.ResourceList{
v1.ResourceCPU: resource.MustParse("4"),
},
Used: v1.ResourceList{
v1.ResourceCPU: resource.MustParse("0"),
},
},
expectedActionSet: sets.NewString(
strings.Join([]string{"update", "resourcequotas", "status"}, "-"),
),
items: []runtime.Object{},
},
}
resourceQuota := v1.ResourceQuota{
ObjectMeta: metav1.ObjectMeta{Name: "quota", Namespace: "testing"},
Spec: v1.ResourceQuotaSpec{
Hard: v1.ResourceList{
v1.ResourceCPU: resource.MustParse("3"),
v1.ResourceMemory: resource.MustParse("100Gi"),
v1.ResourcePods: resource.MustParse("5"),
"quota-unchanged": {
gvr: v1.SchemeGroupVersion.WithResource("pods"),
quota: v1.ResourceQuota{
ObjectMeta: metav1.ObjectMeta{
Namespace: "default",
Name: "rq",
},
Spec: v1.ResourceQuotaSpec{
Hard: v1.ResourceList{
v1.ResourceCPU: resource.MustParse("4"),
},
},
Status: v1.ResourceQuotaStatus{
Hard: v1.ResourceList{
v1.ResourceCPU: resource.MustParse("0"),
},
},
},
},
}
expectedUsage := v1.ResourceQuota{
Status: v1.ResourceQuotaStatus{
Hard: v1.ResourceList{
v1.ResourceCPU: resource.MustParse("3"),
v1.ResourceMemory: resource.MustParse("100Gi"),
v1.ResourcePods: resource.MustParse("5"),
},
Used: v1.ResourceList{
v1.ResourceCPU: resource.MustParse("200m"),
v1.ResourceMemory: resource.MustParse("2Gi"),
v1.ResourcePods: resource.MustParse("2"),
status: v1.ResourceQuotaStatus{
Hard: v1.ResourceList{
v1.ResourceCPU: resource.MustParse("4"),
},
Used: v1.ResourceList{
v1.ResourceCPU: resource.MustParse("0"),
},
},
expectedActionSet: sets.NewString(),
items: []runtime.Object{},
},
}
kubeClient := fake.NewSimpleClientset(&podList, &resourceQuota)
informerFactory := informers.NewSharedInformerFactory(kubeClient, controller.NoResyncPeriodFunc())
resourceQuotaControllerOptions := &ResourceQuotaControllerOptions{
QuotaClient: kubeClient.Core(),
ResourceQuotaInformer: informerFactory.Core().V1().ResourceQuotas(),
ResyncPeriod: controller.NoResyncPeriodFunc,
Registry: install.NewRegistry(kubeClient, nil),
GroupKindsToReplenish: []schema.GroupKind{
api.Kind("Pod"),
api.Kind("Service"),
api.Kind("ReplicationController"),
api.Kind("PersistentVolumeClaim"),
},
ControllerFactory: NewReplenishmentControllerFactory(informerFactory),
ReplenishmentResyncPeriod: controller.NoResyncPeriodFunc,
}
quotaController := NewResourceQuotaController(resourceQuotaControllerOptions)
err := quotaController.syncResourceQuota(&resourceQuota)
if err != nil {
t.Fatalf("Unexpected error %v", err)
}
expectedActionSet := sets.NewString(
strings.Join([]string{"list", "pods", ""}, "-"),
strings.Join([]string{"update", "resourcequotas", "status"}, "-"),
)
actionSet := sets.NewString()
for _, action := range kubeClient.Actions() {
actionSet.Insert(strings.Join([]string{action.GetVerb(), action.GetResource().Resource, action.GetSubresource()}, "-"))
}
if !actionSet.HasAll(expectedActionSet.List()...) {
t.Errorf("Expected actions:\n%v\n but got:\n%v\nDifference:\n%v", expectedActionSet, actionSet, expectedActionSet.Difference(actionSet))
}
lastActionIndex := len(kubeClient.Actions()) - 1
usage := kubeClient.Actions()[lastActionIndex].(core.UpdateAction).GetObject().(*v1.ResourceQuota)
// ensure hard and used limits are what we expected
for k, v := range expectedUsage.Status.Hard {
actual := usage.Status.Hard[k]
actualValue := actual.String()
expectedValue := v.String()
if expectedValue != actualValue {
t.Errorf("Usage Hard: Key: %v, Expected: %v, Actual: %v", k, expectedValue, actualValue)
for testName, testCase := range testCases {
kubeClient := fake.NewSimpleClientset(&testCase.quota)
listersForResourceConfig := map[schema.GroupVersionResource]cache.GenericLister{
testCase.gvr: newGenericLister(testCase.gvr.GroupResource(), testCase.items),
}
}
for k, v := range expectedUsage.Status.Used {
actual := usage.Status.Used[k]
actualValue := actual.String()
expectedValue := v.String()
if expectedValue != actualValue {
t.Errorf("Usage Used: Key: %v, Expected: %v, Actual: %v", k, expectedValue, actualValue)
qc := setupQuotaController(t, kubeClient, mockListerForResourceFunc(listersForResourceConfig))
defer close(qc.stop)
if err := qc.syncResourceQuota(&testCase.quota); err != nil {
t.Fatalf("test: %s, unexpected error: %v", testName, err)
}
}
}
func TestSyncResourceQuotaSpecChange(t *testing.T) {
resourceQuota := v1.ResourceQuota{
ObjectMeta: metav1.ObjectMeta{
Namespace: "default",
Name: "rq",
},
Spec: v1.ResourceQuotaSpec{
Hard: v1.ResourceList{
v1.ResourceCPU: resource.MustParse("4"),
},
},
Status: v1.ResourceQuotaStatus{
Hard: v1.ResourceList{
v1.ResourceCPU: resource.MustParse("3"),
},
Used: v1.ResourceList{
v1.ResourceCPU: resource.MustParse("0"),
},
},
}
expectedUsage := v1.ResourceQuota{
Status: v1.ResourceQuotaStatus{
Hard: v1.ResourceList{
v1.ResourceCPU: resource.MustParse("4"),
},
Used: v1.ResourceList{
v1.ResourceCPU: resource.MustParse("0"),
},
},
}
kubeClient := fake.NewSimpleClientset(&resourceQuota)
informerFactory := informers.NewSharedInformerFactory(kubeClient, controller.NoResyncPeriodFunc())
resourceQuotaControllerOptions := &ResourceQuotaControllerOptions{
QuotaClient: kubeClient.Core(),
ResourceQuotaInformer: informerFactory.Core().V1().ResourceQuotas(),
ResyncPeriod: controller.NoResyncPeriodFunc,
Registry: install.NewRegistry(kubeClient, nil),
GroupKindsToReplenish: []schema.GroupKind{
api.Kind("Pod"),
api.Kind("Service"),
api.Kind("ReplicationController"),
api.Kind("PersistentVolumeClaim"),
},
ControllerFactory: NewReplenishmentControllerFactory(informerFactory),
ReplenishmentResyncPeriod: controller.NoResyncPeriodFunc,
}
quotaController := NewResourceQuotaController(resourceQuotaControllerOptions)
err := quotaController.syncResourceQuota(&resourceQuota)
if err != nil {
t.Fatalf("Unexpected error %v", err)
}
expectedActionSet := sets.NewString(
strings.Join([]string{"list", "pods", ""}, "-"),
strings.Join([]string{"update", "resourcequotas", "status"}, "-"),
)
actionSet := sets.NewString()
for _, action := range kubeClient.Actions() {
actionSet.Insert(strings.Join([]string{action.GetVerb(), action.GetResource().Resource, action.GetSubresource()}, "-"))
}
if !actionSet.HasAll(expectedActionSet.List()...) {
t.Errorf("Expected actions:\n%v\n but got:\n%v\nDifference:\n%v", expectedActionSet, actionSet, expectedActionSet.Difference(actionSet))
}
lastActionIndex := len(kubeClient.Actions()) - 1
usage := kubeClient.Actions()[lastActionIndex].(core.UpdateAction).GetObject().(*v1.ResourceQuota)
// ensure hard and used limits are what we expected
for k, v := range expectedUsage.Status.Hard {
actual := usage.Status.Hard[k]
actualValue := actual.String()
expectedValue := v.String()
if expectedValue != actualValue {
t.Errorf("Usage Hard: Key: %v, Expected: %v, Actual: %v", k, expectedValue, actualValue)
actionSet := sets.NewString()
for _, action := range kubeClient.Actions() {
actionSet.Insert(strings.Join([]string{action.GetVerb(), action.GetResource().Resource, action.GetSubresource()}, "-"))
}
}
for k, v := range expectedUsage.Status.Used {
actual := usage.Status.Used[k]
actualValue := actual.String()
expectedValue := v.String()
if expectedValue != actualValue {
t.Errorf("Usage Used: Key: %v, Expected: %v, Actual: %v", k, expectedValue, actualValue)
if !actionSet.HasAll(testCase.expectedActionSet.List()...) {
t.Errorf("test: %s,\nExpected actions:\n%v\n but got:\n%v\nDifference:\n%v", testName, testCase.expectedActionSet, actionSet, testCase.expectedActionSet.Difference(actionSet))
}
}
}
func TestSyncResourceQuotaSpecHardChange(t *testing.T) {
resourceQuota := v1.ResourceQuota{
ObjectMeta: metav1.ObjectMeta{
Namespace: "default",
Name: "rq",
},
Spec: v1.ResourceQuotaSpec{
Hard: v1.ResourceList{
v1.ResourceCPU: resource.MustParse("4"),
},
},
Status: v1.ResourceQuotaStatus{
Hard: v1.ResourceList{
v1.ResourceCPU: resource.MustParse("3"),
v1.ResourceMemory: resource.MustParse("1Gi"),
},
Used: v1.ResourceList{
v1.ResourceCPU: resource.MustParse("0"),
v1.ResourceMemory: resource.MustParse("0"),
},
},
}
lastActionIndex := len(kubeClient.Actions()) - 1
usage := kubeClient.Actions()[lastActionIndex].(core.UpdateAction).GetObject().(*v1.ResourceQuota)
expectedUsage := v1.ResourceQuota{
Status: v1.ResourceQuotaStatus{
Hard: v1.ResourceList{
v1.ResourceCPU: resource.MustParse("4"),
},
Used: v1.ResourceList{
v1.ResourceCPU: resource.MustParse("0"),
},
},
}
kubeClient := fake.NewSimpleClientset(&resourceQuota)
informerFactory := informers.NewSharedInformerFactory(kubeClient, controller.NoResyncPeriodFunc())
resourceQuotaControllerOptions := &ResourceQuotaControllerOptions{
QuotaClient: kubeClient.Core(),
ResourceQuotaInformer: informerFactory.Core().V1().ResourceQuotas(),
ResyncPeriod: controller.NoResyncPeriodFunc,
Registry: install.NewRegistry(kubeClient, nil),
GroupKindsToReplenish: []schema.GroupKind{
api.Kind("Pod"),
api.Kind("Service"),
api.Kind("ReplicationController"),
api.Kind("PersistentVolumeClaim"),
},
ControllerFactory: NewReplenishmentControllerFactory(informerFactory),
ReplenishmentResyncPeriod: controller.NoResyncPeriodFunc,
}
quotaController := NewResourceQuotaController(resourceQuotaControllerOptions)
err := quotaController.syncResourceQuota(&resourceQuota)
if err != nil {
t.Fatalf("Unexpected error %v", err)
}
expectedActionSet := sets.NewString(
strings.Join([]string{"list", "pods", ""}, "-"),
strings.Join([]string{"update", "resourcequotas", "status"}, "-"),
)
actionSet := sets.NewString()
for _, action := range kubeClient.Actions() {
actionSet.Insert(strings.Join([]string{action.GetVerb(), action.GetResource().Resource, action.GetSubresource()}, "-"))
}
if !actionSet.HasAll(expectedActionSet.List()...) {
t.Errorf("Expected actions:\n%v\n but got:\n%v\nDifference:\n%v", expectedActionSet, actionSet, expectedActionSet.Difference(actionSet))
}
lastActionIndex := len(kubeClient.Actions()) - 1
usage := kubeClient.Actions()[lastActionIndex].(core.UpdateAction).GetObject().(*v1.ResourceQuota)
// ensure hard and used limits are what we expected
for k, v := range expectedUsage.Status.Hard {
actual := usage.Status.Hard[k]
actualValue := actual.String()
expectedValue := v.String()
if expectedValue != actualValue {
t.Errorf("Usage Hard: Key: %v, Expected: %v, Actual: %v", k, expectedValue, actualValue)
// ensure usage is as expected
if len(usage.Status.Hard) != len(testCase.status.Hard) {
t.Errorf("test: %s, status hard lengths do not match", testName)
}
}
for k, v := range expectedUsage.Status.Used {
actual := usage.Status.Used[k]
actualValue := actual.String()
expectedValue := v.String()
if expectedValue != actualValue {
t.Errorf("Usage Used: Key: %v, Expected: %v, Actual: %v", k, expectedValue, actualValue)
if len(usage.Status.Used) != len(testCase.status.Used) {
t.Errorf("test: %s, status used lengths do not match", testName)
}
}
// ensure usage hard and used are are synced with spec hard, not have dirty resource
for k, v := range usage.Status.Hard {
if k == v1.ResourceMemory {
t.Errorf("Unexpected Usage Hard: Key: %v, Value: %v", k, v.String())
for k, v := range testCase.status.Hard {
actual := usage.Status.Hard[k]
actualValue := actual.String()
expectedValue := v.String()
if expectedValue != actualValue {
t.Errorf("test: %s, Usage Hard: Key: %v, Expected: %v, Actual: %v", testName, k, expectedValue, actualValue)
}
}
}
for k, v := range usage.Status.Used {
if k == v1.ResourceMemory {
t.Errorf("Unexpected Usage Used: Key: %v, Value: %v", k, v.String())
for k, v := range testCase.status.Used {
actual := usage.Status.Used[k]
actualValue := actual.String()
expectedValue := v.String()
if expectedValue != actualValue {
t.Errorf("test: %s, Usage Used: Key: %v, Expected: %v, Actual: %v", testName, k, expectedValue, actualValue)
}
}
}
}
func TestSyncResourceQuotaNoChange(t *testing.T) {
resourceQuota := v1.ResourceQuota{
ObjectMeta: metav1.ObjectMeta{
Namespace: "default",
Name: "rq",
},
Spec: v1.ResourceQuotaSpec{
Hard: v1.ResourceList{
v1.ResourceCPU: resource.MustParse("4"),
},
},
Status: v1.ResourceQuotaStatus{
Hard: v1.ResourceList{
v1.ResourceCPU: resource.MustParse("4"),
},
Used: v1.ResourceList{
v1.ResourceCPU: resource.MustParse("0"),
},
},
}
kubeClient := fake.NewSimpleClientset(&v1.PodList{}, &resourceQuota)
informerFactory := informers.NewSharedInformerFactory(kubeClient, controller.NoResyncPeriodFunc())
resourceQuotaControllerOptions := &ResourceQuotaControllerOptions{
QuotaClient: kubeClient.Core(),
ResourceQuotaInformer: informerFactory.Core().V1().ResourceQuotas(),
ResyncPeriod: controller.NoResyncPeriodFunc,
Registry: install.NewRegistry(kubeClient, nil),
GroupKindsToReplenish: []schema.GroupKind{
api.Kind("Pod"),
api.Kind("Service"),
api.Kind("ReplicationController"),
api.Kind("PersistentVolumeClaim"),
},
ControllerFactory: NewReplenishmentControllerFactory(informerFactory),
ReplenishmentResyncPeriod: controller.NoResyncPeriodFunc,
}
quotaController := NewResourceQuotaController(resourceQuotaControllerOptions)
err := quotaController.syncResourceQuota(&resourceQuota)
if err != nil {
t.Fatalf("Unexpected error %v", err)
}
expectedActionSet := sets.NewString(
strings.Join([]string{"list", "pods", ""}, "-"),
)
actionSet := sets.NewString()
for _, action := range kubeClient.Actions() {
actionSet.Insert(strings.Join([]string{action.GetVerb(), action.GetResource().Resource, action.GetSubresource()}, "-"))
}
if !actionSet.HasAll(expectedActionSet.List()...) {
t.Errorf("Expected actions:\n%v\n but got:\n%v\nDifference:\n%v", expectedActionSet, actionSet, expectedActionSet.Difference(actionSet))
}
}
func TestAddQuota(t *testing.T) {
kubeClient := fake.NewSimpleClientset()
informerFactory := informers.NewSharedInformerFactory(kubeClient, controller.NoResyncPeriodFunc())
resourceQuotaControllerOptions := &ResourceQuotaControllerOptions{
QuotaClient: kubeClient.Core(),
ResourceQuotaInformer: informerFactory.Core().V1().ResourceQuotas(),
ResyncPeriod: controller.NoResyncPeriodFunc,
Registry: install.NewRegistry(kubeClient, nil),
GroupKindsToReplenish: []schema.GroupKind{
api.Kind("Pod"),
api.Kind("ReplicationController"),
api.Kind("PersistentVolumeClaim"),
},
ControllerFactory: NewReplenishmentControllerFactory(informerFactory),
ReplenishmentResyncPeriod: controller.NoResyncPeriodFunc,
gvr := v1.SchemeGroupVersion.WithResource("pods")
listersForResourceConfig := map[schema.GroupVersionResource]cache.GenericLister{
gvr: newGenericLister(gvr.GroupResource(), newTestPods()),
}
quotaController := NewResourceQuotaController(resourceQuotaControllerOptions)
delete(quotaController.registry.(*generic.GenericRegistry).InternalEvaluators, api.Kind("Service"))
qc := setupQuotaController(t, kubeClient, mockListerForResourceFunc(listersForResourceConfig))
defer close(qc.stop)
testCases := []struct {
name string
name string
quota *v1.ResourceQuota
expectedPriority bool
}{
@ -491,7 +361,7 @@ func TestAddQuota(t *testing.T) {
},
},
{
name: "status, missing usage, but don't care",
name: "status, missing usage, but don't care (no informer)",
expectedPriority: false,
quota: &v1.ResourceQuota{
ObjectMeta: metav1.ObjectMeta{
@ -500,12 +370,12 @@ func TestAddQuota(t *testing.T) {
},
Spec: v1.ResourceQuotaSpec{
Hard: v1.ResourceList{
v1.ResourceServices: resource.MustParse("4"),
"count/foobars.example.com": resource.MustParse("4"),
},
},
Status: v1.ResourceQuotaStatus{
Hard: v1.ResourceList{
v1.ResourceServices: resource.MustParse("4"),
"count/foobars.example.com": resource.MustParse("4"),
},
},
},
@ -536,30 +406,29 @@ func TestAddQuota(t *testing.T) {
}
for _, tc := range testCases {
quotaController.addQuota(tc.quota)
qc.addQuota(tc.quota)
if tc.expectedPriority {
if e, a := 1, quotaController.missingUsageQueue.Len(); e != a {
if e, a := 1, qc.missingUsageQueue.Len(); e != a {
t.Errorf("%s: expected %v, got %v", tc.name, e, a)
}
if e, a := 0, quotaController.queue.Len(); e != a {
if e, a := 0, qc.queue.Len(); e != a {
t.Errorf("%s: expected %v, got %v", tc.name, e, a)
}
} else {
if e, a := 0, quotaController.missingUsageQueue.Len(); e != a {
if e, a := 0, qc.missingUsageQueue.Len(); e != a {
t.Errorf("%s: expected %v, got %v", tc.name, e, a)
}
if e, a := 1, quotaController.queue.Len(); e != a {
if e, a := 1, qc.queue.Len(); e != a {
t.Errorf("%s: expected %v, got %v", tc.name, e, a)
}
}
for quotaController.missingUsageQueue.Len() > 0 {
key, _ := quotaController.missingUsageQueue.Get()
quotaController.missingUsageQueue.Done(key)
for qc.missingUsageQueue.Len() > 0 {
key, _ := qc.missingUsageQueue.Get()
qc.missingUsageQueue.Done(key)
}
for quotaController.queue.Len() > 0 {
key, _ := quotaController.queue.Get()
quotaController.queue.Done(key)
for qc.queue.Len() > 0 {
key, _ := qc.queue.Get()
qc.queue.Done(key)
}
}
}

View File

@ -0,0 +1,341 @@
/*
Copyright 2017 The Kubernetes Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package resourcequota
import (
"fmt"
"sync"
"time"
"github.com/golang/glog"
"k8s.io/api/core/v1"
"k8s.io/apimachinery/pkg/api/meta"
"k8s.io/apimachinery/pkg/runtime/schema"
"k8s.io/apimachinery/pkg/util/clock"
utilerrors "k8s.io/apimachinery/pkg/util/errors"
utilruntime "k8s.io/apimachinery/pkg/util/runtime"
"k8s.io/apimachinery/pkg/util/wait"
"k8s.io/client-go/tools/cache"
"k8s.io/client-go/util/workqueue"
"k8s.io/kubernetes/pkg/controller"
"k8s.io/kubernetes/pkg/quota"
"k8s.io/kubernetes/pkg/quota/evaluator/core"
"k8s.io/kubernetes/pkg/quota/generic"
)
type eventType int
func (e eventType) String() string {
switch e {
case addEvent:
return "add"
case updateEvent:
return "update"
case deleteEvent:
return "delete"
default:
return fmt.Sprintf("unknown(%d)", int(e))
}
}
const (
addEvent eventType = iota
updateEvent
deleteEvent
)
type event struct {
eventType eventType
obj interface{}
oldObj interface{}
gvr schema.GroupVersionResource
}
type QuotaMonitor struct {
// each monitor list/watches a resource and determines if we should replenish quota
monitors monitors
monitorLock sync.Mutex
// informersStarted is closed after after all of the controllers have been initialized and are running.
// After that it is safe to start them here, before that it is not.
informersStarted <-chan struct{}
// stopCh drives shutdown. If it is nil, it indicates that Run() has not been
// called yet. If it is non-nil, then when closed it indicates everything
// should shut down.
//
// This channel is also protected by monitorLock.
stopCh <-chan struct{}
// monitors are the producer of the resourceChanges queue
resourceChanges workqueue.RateLimitingInterface
// interfaces with informers
informerFactory InformerFactory
// list of resources to ignore
ignoredResources map[schema.GroupResource]struct{}
// The period that should be used to re-sync the monitored resource
resyncPeriod controller.ResyncPeriodFunc
// callback to alert that a change may require quota recalculation
replenishmentFunc ReplenishmentFunc
// maintains list of evaluators
registry quota.Registry
}
// monitor runs a Controller with a local stop channel.
type monitor struct {
controller cache.Controller
// stopCh stops Controller. If stopCh is nil, the monitor is considered to be
// not yet started.
stopCh chan struct{}
}
// Run is intended to be called in a goroutine. Multiple calls of this is an
// error.
func (m *monitor) Run() {
m.controller.Run(m.stopCh)
}
type monitors map[schema.GroupVersionResource]*monitor
func (qm *QuotaMonitor) controllerFor(resource schema.GroupVersionResource) (cache.Controller, error) {
// TODO: pass this down
clock := clock.RealClock{}
handlers := cache.ResourceEventHandlerFuncs{
UpdateFunc: func(oldObj, newObj interface{}) {
// TODO: leaky abstraction! live w/ it for now, but should pass down an update filter func.
// we only want to queue the updates we care about though as too much noise will overwhelm queue.
notifyUpdate := false
switch resource.GroupResource() {
case schema.GroupResource{Resource: "pods"}:
oldPod := oldObj.(*v1.Pod)
newPod := newObj.(*v1.Pod)
notifyUpdate = core.QuotaV1Pod(oldPod, clock) && !core.QuotaV1Pod(newPod, clock)
case schema.GroupResource{Resource: "services"}:
oldService := oldObj.(*v1.Service)
newService := newObj.(*v1.Service)
notifyUpdate = core.GetQuotaServiceType(oldService) != core.GetQuotaServiceType(newService)
}
if notifyUpdate {
event := &event{
eventType: updateEvent,
obj: newObj,
oldObj: oldObj,
gvr: resource,
}
qm.resourceChanges.Add(event)
}
},
DeleteFunc: func(obj interface{}) {
// delta fifo may wrap the object in a cache.DeletedFinalStateUnknown, unwrap it
if deletedFinalStateUnknown, ok := obj.(cache.DeletedFinalStateUnknown); ok {
obj = deletedFinalStateUnknown.Obj
}
event := &event{
eventType: deleteEvent,
obj: obj,
gvr: resource,
}
qm.resourceChanges.Add(event)
},
}
shared, err := qm.informerFactory.ForResource(resource)
if err == nil {
glog.V(4).Infof("QuotaMonitor using a shared informer for resource %q", resource.String())
shared.Informer().AddEventHandlerWithResyncPeriod(handlers, qm.resyncPeriod())
return shared.Informer().GetController(), nil
}
glog.V(4).Infof("QuotaMonitor unable to use a shared informer for resource %q: %v", resource.String(), err)
// TODO: if we can share storage with garbage collector, it may make sense to support other resources
// until that time, aggregated api servers will have to run their own controller to reconcile their own quota.
return nil, fmt.Errorf("unable to monitor quota for resource %q", resource.String())
}
// syncMonitors rebuilds the monitor set according to the supplied resources,
// creating or deleting monitors as necessary. It will return any error
// encountered, but will make an attempt to create a monitor for each resource
// instead of immediately exiting on an error. It may be called before or after
// Run. Monitors are NOT started as part of the sync. To ensure all existing
// monitors are started, call startMonitors.
func (qm *QuotaMonitor) syncMonitors(resources map[schema.GroupVersionResource]struct{}) error {
qm.monitorLock.Lock()
defer qm.monitorLock.Unlock()
toRemove := qm.monitors
if toRemove == nil {
toRemove = monitors{}
}
current := monitors{}
errs := []error{}
kept := 0
added := 0
for resource := range resources {
if _, ok := qm.ignoredResources[resource.GroupResource()]; ok {
continue
}
if m, ok := toRemove[resource]; ok {
current[resource] = m
delete(toRemove, resource)
kept++
continue
}
c, err := qm.controllerFor(resource)
if err != nil {
errs = append(errs, fmt.Errorf("couldn't start monitor for resource %q: %v", resource, err))
continue
}
// check if we need to create an evaluator for this resource (if none previously registered)
evaluator := qm.registry.Get(resource.GroupResource())
if evaluator == nil {
listerFunc := generic.ListerFuncForResourceFunc(qm.informerFactory.ForResource)
listResourceFunc := generic.ListResourceUsingListerFunc(listerFunc, resource)
evaluator = generic.NewObjectCountEvaluator(false, resource.GroupResource(), listResourceFunc, "")
qm.registry.Add(evaluator)
glog.Infof("QuotaMonitor created object count evaluator for %s", resource.GroupResource())
}
// track the monitor
current[resource] = &monitor{controller: c}
added++
}
qm.monitors = current
for _, monitor := range toRemove {
if monitor.stopCh != nil {
close(monitor.stopCh)
}
}
glog.V(4).Infof("quota synced monitors; added %d, kept %d, removed %d", added, kept, len(toRemove))
// NewAggregate returns nil if errs is 0-length
return utilerrors.NewAggregate(errs)
}
// startMonitors ensures the current set of monitors are running. Any newly
// started monitors will also cause shared informers to be started.
//
// If called before Run, startMonitors does nothing (as there is no stop channel
// to support monitor/informer execution).
func (qm *QuotaMonitor) startMonitors() {
qm.monitorLock.Lock()
defer qm.monitorLock.Unlock()
if qm.stopCh == nil {
return
}
// we're waiting until after the informer start that happens once all the controllers are initialized. This ensures
// that they don't get unexpected events on their work queues.
<-qm.informersStarted
monitors := qm.monitors
started := 0
for _, monitor := range monitors {
if monitor.stopCh == nil {
monitor.stopCh = make(chan struct{})
qm.informerFactory.Start(qm.stopCh)
go monitor.Run()
started++
}
}
glog.V(4).Infof("QuotaMonitor started %d new monitors, %d currently running", started, len(monitors))
}
// IsSynced returns true if any monitors exist AND all those monitors'
// controllers HasSynced functions return true. This means IsSynced could return
// true at one time, and then later return false if all monitors were
// reconstructed.
func (qm *QuotaMonitor) IsSynced() bool {
qm.monitorLock.Lock()
defer qm.monitorLock.Unlock()
if len(qm.monitors) == 0 {
return false
}
for _, monitor := range qm.monitors {
if !monitor.controller.HasSynced() {
return false
}
}
return true
}
// Run sets the stop channel and starts monitor execution until stopCh is
// closed. Any running monitors will be stopped before Run returns.
func (qm *QuotaMonitor) Run(stopCh <-chan struct{}) {
glog.Infof("QuotaMonitor running")
defer glog.Infof("QuotaMonitor stopping")
// Set up the stop channel.
qm.monitorLock.Lock()
qm.stopCh = stopCh
qm.monitorLock.Unlock()
// Start monitors and begin change processing until the stop channel is
// closed.
qm.startMonitors()
wait.Until(qm.runProcessResourceChanges, 1*time.Second, stopCh)
// Stop any running monitors.
qm.monitorLock.Lock()
defer qm.monitorLock.Unlock()
monitors := qm.monitors
stopped := 0
for _, monitor := range monitors {
if monitor.stopCh != nil {
stopped++
close(monitor.stopCh)
}
}
glog.Infof("QuotaMonitor stopped %d of %d monitors", stopped, len(monitors))
}
func (qm *QuotaMonitor) runProcessResourceChanges() {
for qm.processResourceChanges() {
}
}
// Dequeueing an event from resourceChanges to process
func (qm *QuotaMonitor) processResourceChanges() bool {
item, quit := qm.resourceChanges.Get()
if quit {
return false
}
defer qm.resourceChanges.Done(item)
event, ok := item.(*event)
if !ok {
utilruntime.HandleError(fmt.Errorf("expect a *event, got %v", item))
return true
}
obj := event.obj
accessor, err := meta.Accessor(obj)
if err != nil {
utilruntime.HandleError(fmt.Errorf("cannot access obj: %v", err))
return true
}
glog.V(4).Infof("QuotaMonitor process object: %s, namespace %s, name %s, uid %s, event type %v", event.gvr.String(), accessor.GetNamespace(), accessor.GetName(), string(accessor.GetUID()), event.eventType)
qm.replenishmentFunc(event.gvr.GroupResource(), accessor.GetNamespace())
return true
}

View File

@ -53,9 +53,9 @@ type WantsRESTMapper interface {
SetRESTMapper(meta.RESTMapper)
}
// WantsQuotaRegistry defines a function which sets quota registry for admission plugins that need it.
type WantsQuotaRegistry interface {
SetQuotaRegistry(quota.Registry)
// WantsQuotaConfiguration defines a function which sets quota configuration for admission plugins that need it.
type WantsQuotaConfiguration interface {
SetQuotaConfiguration(quota.Configuration)
admission.Validator
}
@ -85,7 +85,7 @@ type PluginInitializer struct {
authorizer authorizer.Authorizer
cloudConfig []byte
restMapper meta.RESTMapper
quotaRegistry quota.Registry
quotaConfiguration quota.Configuration
serviceResolver webhook.ServiceResolver
authenticationInfoResolverWrapper webhook.AuthenticationInfoResolverWrapper
}
@ -100,7 +100,7 @@ func NewPluginInitializer(
sharedInformers informers.SharedInformerFactory,
cloudConfig []byte,
restMapper meta.RESTMapper,
quotaRegistry quota.Registry,
quotaConfiguration quota.Configuration,
authenticationInfoResolverWrapper webhook.AuthenticationInfoResolverWrapper,
serviceResolver webhook.ServiceResolver,
) *PluginInitializer {
@ -109,7 +109,7 @@ func NewPluginInitializer(
informers: sharedInformers,
cloudConfig: cloudConfig,
restMapper: restMapper,
quotaRegistry: quotaRegistry,
quotaConfiguration: quotaConfiguration,
authenticationInfoResolverWrapper: authenticationInfoResolverWrapper,
serviceResolver: serviceResolver,
}
@ -134,8 +134,8 @@ func (i *PluginInitializer) Initialize(plugin admission.Interface) {
wants.SetRESTMapper(i.restMapper)
}
if wants, ok := plugin.(WantsQuotaRegistry); ok {
wants.SetQuotaRegistry(i.quotaRegistry)
if wants, ok := plugin.(WantsQuotaConfiguration); ok {
wants.SetQuotaConfiguration(i.quotaConfiguration)
}
if wants, ok := plugin.(WantsServiceResolver); ok {

View File

@ -21,6 +21,7 @@ go_library(
"//vendor/k8s.io/apimachinery/pkg/runtime/schema:go_default_library",
"//vendor/k8s.io/apimachinery/pkg/util/sets:go_default_library",
"//vendor/k8s.io/apiserver/pkg/admission:go_default_library",
"//vendor/k8s.io/client-go/tools/cache:go_default_library",
],
)

View File

@ -9,14 +9,10 @@ load(
go_library(
name = "go_default_library",
srcs = [
"configmap.go",
"doc.go",
"persistent_volume_claims.go",
"pods.go",
"registry.go",
"replication_controllers.go",
"resource_quotas.go",
"secrets.go",
"services.go",
],
importpath = "k8s.io/kubernetes/pkg/quota/evaluator/core",
@ -32,7 +28,6 @@ go_library(
"//pkg/quota/generic:go_default_library",
"//vendor/k8s.io/api/core/v1:go_default_library",
"//vendor/k8s.io/apimachinery/pkg/api/resource:go_default_library",
"//vendor/k8s.io/apimachinery/pkg/apis/meta/v1:go_default_library",
"//vendor/k8s.io/apimachinery/pkg/runtime:go_default_library",
"//vendor/k8s.io/apimachinery/pkg/runtime/schema:go_default_library",
"//vendor/k8s.io/apimachinery/pkg/util/clock:go_default_library",
@ -43,8 +38,6 @@ go_library(
"//vendor/k8s.io/apiserver/pkg/admission:go_default_library",
"//vendor/k8s.io/apiserver/pkg/features:go_default_library",
"//vendor/k8s.io/apiserver/pkg/util/feature:go_default_library",
"//vendor/k8s.io/client-go/informers:go_default_library",
"//vendor/k8s.io/client-go/kubernetes:go_default_library",
],
)
@ -60,11 +53,12 @@ go_test(
deps = [
"//pkg/api:go_default_library",
"//pkg/quota:go_default_library",
"//pkg/quota/generic:go_default_library",
"//pkg/util/node:go_default_library",
"//vendor/k8s.io/apimachinery/pkg/api/resource:go_default_library",
"//vendor/k8s.io/apimachinery/pkg/apis/meta/v1:go_default_library",
"//vendor/k8s.io/apimachinery/pkg/runtime/schema:go_default_library",
"//vendor/k8s.io/apimachinery/pkg/util/clock:go_default_library",
"//vendor/k8s.io/client-go/kubernetes/fake:go_default_library",
],
)

View File

@ -1,61 +0,0 @@
/*
Copyright 2016 The Kubernetes Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package core
import (
"k8s.io/api/core/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/client-go/informers"
clientset "k8s.io/client-go/kubernetes"
"k8s.io/kubernetes/pkg/api"
"k8s.io/kubernetes/pkg/quota"
"k8s.io/kubernetes/pkg/quota/generic"
)
// listConfigMapsByNamespaceFuncUsingClient returns a configMap listing function based on the provided client.
func listConfigMapsByNamespaceFuncUsingClient(kubeClient clientset.Interface) generic.ListFuncByNamespace {
// TODO: ideally, we could pass dynamic client pool down into this code, and have one way of doing this.
// unfortunately, dynamic client works with Unstructured objects, and when we calculate Usage, we require
// structured objects.
return func(namespace string, options metav1.ListOptions) ([]runtime.Object, error) {
itemList, err := kubeClient.CoreV1().ConfigMaps(namespace).List(options)
if err != nil {
return nil, err
}
results := make([]runtime.Object, 0, len(itemList.Items))
for i := range itemList.Items {
results = append(results, &itemList.Items[i])
}
return results, nil
}
}
// NewConfigMapEvaluator returns an evaluator that can evaluate configMaps
// if the specified shared informer factory is not nil, evaluator may use it to support listing functions.
func NewConfigMapEvaluator(kubeClient clientset.Interface, f informers.SharedInformerFactory) quota.Evaluator {
listFuncByNamespace := listConfigMapsByNamespaceFuncUsingClient(kubeClient)
if f != nil {
listFuncByNamespace = generic.ListResourceUsingInformerFunc(f, v1.SchemeGroupVersion.WithResource("configmaps"))
}
return &generic.ObjectCountEvaluator{
AllowCreateOnUpdate: false,
InternalGroupKind: api.Kind("ConfigMap"),
ResourceName: api.ResourceConfigMaps,
ListFuncByNamespace: listFuncByNamespace,
}
}

View File

@ -22,17 +22,13 @@ import (
"k8s.io/api/core/v1"
"k8s.io/apimachinery/pkg/api/resource"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/apimachinery/pkg/runtime/schema"
"k8s.io/apimachinery/pkg/util/initialization"
utilruntime "k8s.io/apimachinery/pkg/util/runtime"
"k8s.io/apimachinery/pkg/util/sets"
"k8s.io/apiserver/pkg/admission"
"k8s.io/apiserver/pkg/features"
utilfeature "k8s.io/apiserver/pkg/util/feature"
"k8s.io/client-go/informers"
clientset "k8s.io/client-go/kubernetes"
"k8s.io/kubernetes/pkg/api"
"k8s.io/kubernetes/pkg/api/helper"
k8s_api_v1 "k8s.io/kubernetes/pkg/api/v1"
@ -42,6 +38,9 @@ import (
"k8s.io/kubernetes/pkg/quota/generic"
)
// the name used for object count quota
var pvcObjectCountName = generic.ObjectCountQuotaResourceNameFor(v1.SchemeGroupVersion.WithResource("persistentvolumeclaims").GroupResource())
// 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{
@ -67,34 +66,11 @@ func V1ResourceByStorageClass(storageClass string, resourceName v1.ResourceName)
return v1.ResourceName(string(storageClass + storageClassSuffix + string(resourceName)))
}
// listPersistentVolumeClaimsByNamespaceFuncUsingClient returns a pvc listing function based on the provided client.
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.
// unfortunately, dynamic client works with Unstructured objects, and when we calculate Usage, we require
// structured objects.
return func(namespace string, options metav1.ListOptions) ([]runtime.Object, error) {
itemList, err := kubeClient.CoreV1().PersistentVolumeClaims(namespace).List(options)
if err != nil {
return nil, err
}
results := make([]runtime.Object, 0, len(itemList.Items))
for i := range itemList.Items {
results = append(results, &itemList.Items[i])
}
return results, nil
}
}
// 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.
func NewPersistentVolumeClaimEvaluator(kubeClient clientset.Interface, f informers.SharedInformerFactory) quota.Evaluator {
listFuncByNamespace := listPersistentVolumeClaimsByNamespaceFuncUsingClient(kubeClient)
if f != nil {
listFuncByNamespace = generic.ListResourceUsingInformerFunc(f, v1.SchemeGroupVersion.WithResource("persistentvolumeclaims"))
}
return &pvcEvaluator{
listFuncByNamespace: listFuncByNamespace,
}
func NewPersistentVolumeClaimEvaluator(f quota.ListerForResourceFunc) quota.Evaluator {
listFuncByNamespace := generic.ListResourceUsingListerFunc(f, v1.SchemeGroupVersion.WithResource("persistentvolumeclaims"))
pvcEvaluator := &pvcEvaluator{listFuncByNamespace: listFuncByNamespace}
return pvcEvaluator
}
// pvcEvaluator knows how to evaluate quota usage for persistent volume claims
@ -105,45 +81,13 @@ type pvcEvaluator struct {
// Constraints verifies that all required resources are present on the item.
func (p *pvcEvaluator) Constraints(required []api.ResourceName, item runtime.Object) error {
pvc, ok := item.(*api.PersistentVolumeClaim)
if !ok {
return fmt.Errorf("unexpected input object %v", item)
}
// these are the items that we will be handling based on the objects actual storage-class
pvcRequiredSet := append([]api.ResourceName{}, pvcResources...)
if storageClassRef := helper.GetPersistentVolumeClaimClass(pvc); len(storageClassRef) > 0 {
pvcRequiredSet = append(pvcRequiredSet, ResourceByStorageClass(storageClassRef, api.ResourcePersistentVolumeClaims))
pvcRequiredSet = append(pvcRequiredSet, ResourceByStorageClass(storageClassRef, api.ResourceRequestsStorage))
}
// in effect, this will remove things from the required set that are not tied to this pvcs storage class
// for example, if a quota has bronze and gold storage class items defined, we should not error a bronze pvc for not being gold.
// but we should error a bronze pvc if it doesn't make a storage request size...
requiredResources := quota.Intersection(required, pvcRequiredSet)
requiredSet := quota.ToSet(requiredResources)
// 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
}
// determine what required resources were not tracked by usage.
missingSet := sets.NewString()
pvcSet := quota.ToSet(quota.ResourceNames(pvcUsage))
if diff := requiredSet.Difference(pvcSet); len(diff) > 0 {
missingSet.Insert(diff.List()...)
}
if len(missingSet) == 0 {
return nil
}
return fmt.Errorf("must specify %s", strings.Join(missingSet.List(), ","))
// no-op for persistent volume claims
return nil
}
// GroupKind that this evaluator tracks
func (p *pvcEvaluator) GroupKind() schema.GroupKind {
return api.Kind("PersistentVolumeClaim")
// GroupResource that this evaluator tracks
func (p *pvcEvaluator) GroupResource() schema.GroupResource {
return v1.SchemeGroupVersion.WithResource("persistentvolumeclaims").GroupResource()
}
// Handles returns true if the evaluator should handle the specified operation.
@ -183,6 +127,12 @@ func (p *pvcEvaluator) Matches(resourceQuota *api.ResourceQuota, item runtime.Ob
func (p *pvcEvaluator) MatchingResources(items []api.ResourceName) []api.ResourceName {
result := []api.ResourceName{}
for _, item := range items {
// match object count quota fields
if quota.Contains([]api.ResourceName{pvcObjectCountName}, item) {
result = append(result, item)
continue
}
// match pvc resources
if quota.Contains(pvcResources, item) {
result = append(result, item)
continue
@ -208,7 +158,8 @@ func (p *pvcEvaluator) Usage(item runtime.Object) (api.ResourceList, error) {
}
// charge for claim
result[api.ResourcePersistentVolumeClaims] = resource.MustParse("1")
result[api.ResourcePersistentVolumeClaims] = *(resource.NewQuantity(1, resource.DecimalSI))
result[pvcObjectCountName] = *(resource.NewQuantity(1, resource.DecimalSI))
if utilfeature.DefaultFeatureGate.Enabled(features.Initializers) {
if !initialization.IsInitialized(pvc.Initializers) {
// Only charge pvc count for uninitialized pvc.
@ -218,7 +169,7 @@ func (p *pvcEvaluator) Usage(item runtime.Object) (api.ResourceList, error) {
storageClassRef := helper.GetPersistentVolumeClaimClass(pvc)
if len(storageClassRef) > 0 {
storageClassClaim := api.ResourceName(storageClassRef + storageClassSuffix + string(api.ResourcePersistentVolumeClaims))
result[storageClassClaim] = resource.MustParse("1")
result[storageClassClaim] = *(resource.NewQuantity(1, resource.DecimalSI))
}
// charge for storage

View File

@ -21,9 +21,10 @@ import (
"k8s.io/apimachinery/pkg/api/resource"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/client-go/kubernetes/fake"
"k8s.io/apimachinery/pkg/runtime/schema"
"k8s.io/kubernetes/pkg/api"
"k8s.io/kubernetes/pkg/quota"
"k8s.io/kubernetes/pkg/quota/generic"
)
func testVolumeClaim(name string, namespace string, spec api.PersistentVolumeClaimSpec) *api.PersistentVolumeClaim {
@ -33,168 +34,6 @@ func testVolumeClaim(name string, namespace string, spec api.PersistentVolumeCla
}
}
func TestPersistentVolumeClaimsConstraintsFunc(t *testing.T) {
classGold := "gold"
classBronze := "bronze"
validClaim := 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("10G"),
},
},
})
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"),
},
},
StorageClassName: &classGold,
})
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"),
},
},
StorageClassName: &classBronze,
})
missingStorage := 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 := 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{},
},
StorageClassName: &classGold,
})
testCases := map[string]struct {
pvc *api.PersistentVolumeClaim
required []api.ResourceName
err string
}{
"missing storage": {
pvc: missingStorage,
required: []api.ResourceName{api.ResourceRequestsStorage},
err: `must specify requests.storage`,
},
"missing gold storage": {
pvc: missingGoldStorage,
required: []api.ResourceName{ResourceByStorageClass(classGold, api.ResourceRequestsStorage)},
err: `must specify gold.storageclass.storage.k8s.io/requests.storage`,
},
"valid-claim-quota-storage": {
pvc: validClaim,
required: []api.ResourceName{api.ResourceRequestsStorage},
},
"valid-claim-quota-pvc": {
pvc: validClaim,
required: []api.ResourceName{api.ResourcePersistentVolumeClaims},
},
"valid-claim-quota-storage-and-pvc": {
pvc: validClaim,
required: []api.ResourceName{api.ResourceRequestsStorage, api.ResourcePersistentVolumeClaims},
},
"valid-claim-gold-quota-gold": {
pvc: validClaimGoldStorageClass,
required: []api.ResourceName{
api.ResourceRequestsStorage,
api.ResourcePersistentVolumeClaims,
ResourceByStorageClass(classGold, api.ResourceRequestsStorage),
ResourceByStorageClass(classGold, api.ResourcePersistentVolumeClaims),
},
},
"valid-claim-bronze-with-quota-gold": {
pvc: validClaimBronzeStorageClass,
required: []api.ResourceName{
api.ResourceRequestsStorage,
api.ResourcePersistentVolumeClaims,
ResourceByStorageClass(classGold, api.ResourceRequestsStorage),
ResourceByStorageClass(classGold, api.ResourcePersistentVolumeClaims),
},
},
}
kubeClient := fake.NewSimpleClientset()
evaluator := NewPersistentVolumeClaimEvaluator(kubeClient, nil)
for testName, test := range testCases {
err := evaluator.Constraints(test.required, test.pvc)
switch {
case err != nil && len(test.err) == 0,
err == nil && len(test.err) != 0,
err != nil && test.err != err.Error():
t.Errorf("%s unexpected error: %v", testName, err)
}
}
}
func TestPersistentVolumeClaimEvaluatorUsage(t *testing.T) {
classGold := "gold"
validClaim := testVolumeClaim("foo", "ns", api.PersistentVolumeClaimSpec{
@ -237,8 +76,7 @@ func TestPersistentVolumeClaimEvaluatorUsage(t *testing.T) {
StorageClassName: &classGold,
})
kubeClient := fake.NewSimpleClientset()
evaluator := NewPersistentVolumeClaimEvaluator(kubeClient, nil)
evaluator := NewPersistentVolumeClaimEvaluator(nil)
testCases := map[string]struct {
pvc *api.PersistentVolumeClaim
usage api.ResourceList
@ -246,17 +84,19 @@ func TestPersistentVolumeClaimEvaluatorUsage(t *testing.T) {
"pvc-usage": {
pvc: validClaim,
usage: api.ResourceList{
api.ResourceRequestsStorage: resource.MustParse("10Gi"),
api.ResourcePersistentVolumeClaims: resource.MustParse("1"),
api.ResourceRequestsStorage: resource.MustParse("10Gi"),
api.ResourcePersistentVolumeClaims: resource.MustParse("1"),
generic.ObjectCountQuotaResourceNameFor(schema.GroupResource{Resource: "persistentvolumeclaims"}): resource.MustParse("1"),
},
},
"pvc-usage-by-class": {
pvc: validClaimByStorageClass,
usage: api.ResourceList{
api.ResourceRequestsStorage: resource.MustParse("10Gi"),
api.ResourcePersistentVolumeClaims: resource.MustParse("1"),
ResourceByStorageClass(classGold, api.ResourceRequestsStorage): resource.MustParse("10Gi"),
ResourceByStorageClass(classGold, api.ResourcePersistentVolumeClaims): resource.MustParse("1"),
api.ResourceRequestsStorage: resource.MustParse("10Gi"),
api.ResourcePersistentVolumeClaims: resource.MustParse("1"),
ResourceByStorageClass(classGold, api.ResourceRequestsStorage): resource.MustParse("10Gi"),
ResourceByStorageClass(classGold, api.ResourcePersistentVolumeClaims): resource.MustParse("1"),
generic.ObjectCountQuotaResourceNameFor(schema.GroupResource{Resource: "persistentvolumeclaims"}): resource.MustParse("1"),
},
},
}

View File

@ -23,20 +23,14 @@ import (
"k8s.io/api/core/v1"
"k8s.io/apimachinery/pkg/api/resource"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/apimachinery/pkg/runtime/schema"
"k8s.io/apimachinery/pkg/util/clock"
"k8s.io/apimachinery/pkg/util/initialization"
utilruntime "k8s.io/apimachinery/pkg/util/runtime"
"k8s.io/apimachinery/pkg/util/sets"
"k8s.io/apimachinery/pkg/util/validation/field"
"k8s.io/apiserver/pkg/admission"
"k8s.io/apiserver/pkg/features"
utilfeature "k8s.io/apiserver/pkg/util/feature"
"k8s.io/client-go/informers"
clientset "k8s.io/client-go/kubernetes"
"k8s.io/kubernetes/pkg/api"
"k8s.io/kubernetes/pkg/api/helper/qos"
k8s_api_v1 "k8s.io/kubernetes/pkg/api/v1"
@ -46,8 +40,12 @@ import (
"k8s.io/kubernetes/pkg/quota/generic"
)
// the name used for object count quota
var podObjectCountName = generic.ObjectCountQuotaResourceNameFor(v1.SchemeGroupVersion.WithResource("pods").GroupResource())
// podResources are the set of resources managed by quota associated with pods.
var podResources = []api.ResourceName{
podObjectCountName,
api.ResourceCPU,
api.ResourceMemory,
api.ResourceEphemeralStorage,
@ -60,35 +58,24 @@ var podResources = []api.ResourceName{
api.ResourcePods,
}
// listPodsByNamespaceFuncUsingClient returns a pod listing function based on the provided client.
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.
// unfortunately, dynamic client works with Unstructured objects, and when we calculate Usage, we require
// structured objects.
return func(namespace string, options metav1.ListOptions) ([]runtime.Object, error) {
itemList, err := kubeClient.CoreV1().Pods(namespace).List(options)
if err != nil {
return nil, err
}
results := make([]runtime.Object, 0, len(itemList.Items))
for i := range itemList.Items {
results = append(results, &itemList.Items[i])
}
return results, nil
}
}
// NOTE: it was a mistake, but if a quota tracks cpu or memory related resources,
// the incoming pod is required to have those values set. we should not repeat
// this mistake for other future resources (gpus, ephemeral-storage,etc).
// do not add more resources to this list!
var validationSet = sets.NewString(
string(api.ResourceCPU),
string(api.ResourceMemory),
string(api.ResourceRequestsCPU),
string(api.ResourceRequestsMemory),
string(api.ResourceLimitsCPU),
string(api.ResourceLimitsMemory),
)
// 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.
func NewPodEvaluator(kubeClient clientset.Interface, f informers.SharedInformerFactory, clock clock.Clock) quota.Evaluator {
listFuncByNamespace := listPodsByNamespaceFuncUsingClient(kubeClient)
if f != nil {
listFuncByNamespace = generic.ListResourceUsingInformerFunc(f, v1.SchemeGroupVersion.WithResource("pods"))
}
return &podEvaluator{
listFuncByNamespace: listFuncByNamespace,
clock: clock,
}
func NewPodEvaluator(f quota.ListerForResourceFunc, clock clock.Clock) quota.Evaluator {
listFuncByNamespace := generic.ListResourceUsingListerFunc(f, v1.SchemeGroupVersion.WithResource("pods"))
podEvaluator := &podEvaluator{listFuncByNamespace: listFuncByNamespace, clock: clock}
return podEvaluator
}
// podEvaluator knows how to measure usage of pods.
@ -110,6 +97,7 @@ func (p *podEvaluator) Constraints(required []api.ResourceName, item runtime.Obj
// Pod level resources are often set during admission control
// As a consequence, we want to verify that resources are valid prior
// to ever charging quota prematurely in case they are not.
// TODO remove this entire section when we have a validation step in admission.
allErrs := field.ErrorList{}
fldPath := field.NewPath("spec").Child("containers")
for i, ctr := range pod.Spec.Containers {
@ -123,10 +111,11 @@ func (p *podEvaluator) Constraints(required []api.ResourceName, item runtime.Obj
return allErrs.ToAggregate()
}
// TODO: fix this when we have pod level resource requirements
// 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
requiredSet := quota.ToSet(required)
// BACKWARD COMPATIBILITY REQUIREMENT: if we quota cpu or memory, then each container
// must make an explicit request for the resource. this was a mistake. it coupled
// validation with resource counting, but we did this before QoS was even defined.
// let's not make that mistake again with other resources now that QoS is defined.
requiredSet := quota.ToSet(required).Intersection(validationSet)
missingSet := sets.NewString()
for i := range pod.Spec.Containers {
enforcePodContainerConstraints(&pod.Spec.Containers[i], requiredSet, missingSet)
@ -140,9 +129,9 @@ func (p *podEvaluator) Constraints(required []api.ResourceName, item runtime.Obj
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")
// GroupResource that this evaluator tracks
func (p *podEvaluator) GroupResource() schema.GroupResource {
return v1.SchemeGroupVersion.WithResource("pods").GroupResource()
}
// Handles returns true if the evaluator should handle the specified attributes.
@ -190,7 +179,7 @@ var _ quota.Evaluator = &podEvaluator{}
func enforcePodContainerConstraints(container *api.Container, requiredSet, missingSet sets.String) {
requests := container.Resources.Requests
limits := container.Resources.Limits
containerUsage := podUsageHelper(requests, limits)
containerUsage := podComputeUsageHelper(requests, limits)
containerSet := quota.ToSet(quota.ResourceNames(containerUsage))
if !containerSet.Equal(requiredSet) {
difference := requiredSet.Difference(containerSet)
@ -198,8 +187,8 @@ func enforcePodContainerConstraints(container *api.Container, requiredSet, missi
}
}
// podUsageHelper can summarize the pod quota usage based on requests and limits
func podUsageHelper(requests api.ResourceList, limits api.ResourceList) api.ResourceList {
// podComputeUsageHelper can summarize the pod compute quota usage based on requests and limits
func podComputeUsageHelper(requests api.ResourceList, limits api.ResourceList) api.ResourceList {
result := api.ResourceList{}
result[api.ResourcePods] = resource.MustParse("1")
if request, found := requests[api.ResourceCPU]; found {
@ -269,18 +258,21 @@ func PodUsageFunc(obj runtime.Object, clock clock.Clock) (api.ResourceList, erro
if err != nil {
return api.ResourceList{}, err
}
// by convention, we do not quota pods that have reached end-of life
// always quota the object count (even if the pod is end of life)
// object count quotas track all objects that are in storage.
// where "pods" tracks all pods that have not reached a terminal state,
// count/pods tracks all pods independent of state.
result := api.ResourceList{
podObjectCountName: *(resource.NewQuantity(1, resource.DecimalSI)),
}
// by convention, we do not quota compute resources that have reached end-of life
// note: the "pods" resource is considered a compute resource since it is tied to life-cycle.
if !QuotaPod(pod, clock) {
return api.ResourceList{}, nil
}
// Only charge pod count for uninitialized pod.
if utilfeature.DefaultFeatureGate.Enabled(features.Initializers) {
if !initialization.IsInitialized(pod.Initializers) {
result := api.ResourceList{}
result[api.ResourcePods] = resource.MustParse("1")
return result, nil
}
return result, nil
}
requests := api.ResourceList{}
limits := api.ResourceList{}
// TODO: ideally, we have pod level requests and limits in the future.
@ -296,7 +288,8 @@ func PodUsageFunc(obj runtime.Object, clock clock.Clock) (api.ResourceList, erro
limits = quota.Max(limits, pod.Spec.InitContainers[i].Resources.Limits)
}
return podUsageHelper(requests, limits), nil
result = quota.Add(result, podComputeUsageHelper(requests, limits))
return result, nil
}
func isBestEffort(pod *api.Pod) bool {

View File

@ -22,10 +22,11 @@ import (
"k8s.io/apimachinery/pkg/api/resource"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/runtime/schema"
"k8s.io/apimachinery/pkg/util/clock"
"k8s.io/client-go/kubernetes/fake"
"k8s.io/kubernetes/pkg/api"
"k8s.io/kubernetes/pkg/quota"
"k8s.io/kubernetes/pkg/quota/generic"
"k8s.io/kubernetes/pkg/util/node"
)
@ -90,8 +91,7 @@ func TestPodConstraintsFunc(t *testing.T) {
err: `must specify memory`,
},
}
kubeClient := fake.NewSimpleClientset()
evaluator := NewPodEvaluator(kubeClient, nil, clock.RealClock{})
evaluator := NewPodEvaluator(nil, clock.RealClock{})
for testName, test := range testCases {
err := evaluator.Constraints(test.required, test.pod)
switch {
@ -104,9 +104,8 @@ func TestPodConstraintsFunc(t *testing.T) {
}
func TestPodEvaluatorUsage(t *testing.T) {
kubeClient := fake.NewSimpleClientset()
fakeClock := clock.NewFakeClock(time.Now())
evaluator := NewPodEvaluator(kubeClient, nil, fakeClock)
evaluator := NewPodEvaluator(nil, fakeClock)
// fields use to simulate a pod undergoing termination
// note: we set the deletion time in the past
@ -135,6 +134,7 @@ func TestPodEvaluatorUsage(t *testing.T) {
api.ResourceLimitsCPU: resource.MustParse("2m"),
api.ResourcePods: resource.MustParse("1"),
api.ResourceCPU: resource.MustParse("1m"),
generic.ObjectCountQuotaResourceNameFor(schema.GroupResource{Resource: "pods"}): resource.MustParse("1"),
},
},
"init container MEM": {
@ -153,6 +153,7 @@ func TestPodEvaluatorUsage(t *testing.T) {
api.ResourceLimitsMemory: resource.MustParse("2m"),
api.ResourcePods: resource.MustParse("1"),
api.ResourceMemory: resource.MustParse("1m"),
generic.ObjectCountQuotaResourceNameFor(schema.GroupResource{Resource: "pods"}): resource.MustParse("1"),
},
},
"init container local ephemeral storage": {
@ -171,6 +172,7 @@ func TestPodEvaluatorUsage(t *testing.T) {
api.ResourceRequestsEphemeralStorage: resource.MustParse("32Mi"),
api.ResourceLimitsEphemeralStorage: resource.MustParse("64Mi"),
api.ResourcePods: resource.MustParse("1"),
generic.ObjectCountQuotaResourceNameFor(schema.GroupResource{Resource: "pods"}): resource.MustParse("1"),
},
},
"container CPU": {
@ -189,6 +191,7 @@ func TestPodEvaluatorUsage(t *testing.T) {
api.ResourceLimitsCPU: resource.MustParse("2m"),
api.ResourcePods: resource.MustParse("1"),
api.ResourceCPU: resource.MustParse("1m"),
generic.ObjectCountQuotaResourceNameFor(schema.GroupResource{Resource: "pods"}): resource.MustParse("1"),
},
},
"container MEM": {
@ -207,6 +210,7 @@ func TestPodEvaluatorUsage(t *testing.T) {
api.ResourceLimitsMemory: resource.MustParse("2m"),
api.ResourcePods: resource.MustParse("1"),
api.ResourceMemory: resource.MustParse("1m"),
generic.ObjectCountQuotaResourceNameFor(schema.GroupResource{Resource: "pods"}): resource.MustParse("1"),
},
},
"container local ephemeral storage": {
@ -225,6 +229,7 @@ func TestPodEvaluatorUsage(t *testing.T) {
api.ResourceRequestsEphemeralStorage: resource.MustParse("32Mi"),
api.ResourceLimitsEphemeralStorage: resource.MustParse("64Mi"),
api.ResourcePods: resource.MustParse("1"),
generic.ObjectCountQuotaResourceNameFor(schema.GroupResource{Resource: "pods"}): resource.MustParse("1"),
},
},
"init container maximums override sum of containers": {
@ -292,6 +297,7 @@ func TestPodEvaluatorUsage(t *testing.T) {
api.ResourcePods: resource.MustParse("1"),
api.ResourceCPU: resource.MustParse("4"),
api.ResourceMemory: resource.MustParse("100M"),
generic.ObjectCountQuotaResourceNameFor(schema.GroupResource{Resource: "pods"}): resource.MustParse("1"),
},
},
"pod deletion timestamp exceeded": {
@ -321,7 +327,9 @@ func TestPodEvaluatorUsage(t *testing.T) {
},
},
},
usage: api.ResourceList{},
usage: api.ResourceList{
generic.ObjectCountQuotaResourceNameFor(schema.GroupResource{Resource: "pods"}): resource.MustParse("1"),
},
},
"pod deletion timestamp not exceeded": {
pod: &api.Pod{
@ -352,6 +360,7 @@ func TestPodEvaluatorUsage(t *testing.T) {
api.ResourceLimitsCPU: resource.MustParse("2"),
api.ResourcePods: resource.MustParse("1"),
api.ResourceCPU: resource.MustParse("1"),
generic.ObjectCountQuotaResourceNameFor(schema.GroupResource{Resource: "pods"}): resource.MustParse("1"),
},
},
}

View File

@ -17,33 +17,34 @@ limitations under the License.
package core
import (
"k8s.io/api/core/v1"
"k8s.io/apimachinery/pkg/runtime/schema"
"k8s.io/apimachinery/pkg/util/clock"
"k8s.io/client-go/informers"
clientset "k8s.io/client-go/kubernetes"
"k8s.io/kubernetes/pkg/api"
"k8s.io/kubernetes/pkg/quota"
"k8s.io/kubernetes/pkg/quota/generic"
)
// NewRegistry returns a registry that knows how to deal with core kubernetes resources
// If an informer factory is provided, evaluators will use them.
func NewRegistry(kubeClient clientset.Interface, f informers.SharedInformerFactory) quota.Registry {
pod := NewPodEvaluator(kubeClient, f, clock.RealClock{})
service := NewServiceEvaluator(kubeClient, f)
replicationController := NewReplicationControllerEvaluator(kubeClient, f)
resourceQuota := NewResourceQuotaEvaluator(kubeClient, f)
secret := NewSecretEvaluator(kubeClient, f)
configMap := NewConfigMapEvaluator(kubeClient, f)
persistentVolumeClaim := NewPersistentVolumeClaimEvaluator(kubeClient, f)
return &generic.GenericRegistry{
InternalEvaluators: map[schema.GroupKind]quota.Evaluator{
pod.GroupKind(): pod,
service.GroupKind(): service,
replicationController.GroupKind(): replicationController,
secret.GroupKind(): secret,
configMap.GroupKind(): configMap,
resourceQuota.GroupKind(): resourceQuota,
persistentVolumeClaim.GroupKind(): persistentVolumeClaim,
},
}
// legacyObjectCountAliases are what we used to do simple object counting quota with mapped to alias
var legacyObjectCountAliases = map[schema.GroupVersionResource]api.ResourceName{
v1.SchemeGroupVersion.WithResource("configmaps"): api.ResourceConfigMaps,
v1.SchemeGroupVersion.WithResource("resourcequotas"): api.ResourceQuotas,
v1.SchemeGroupVersion.WithResource("replicationcontrollers"): api.ResourceReplicationControllers,
v1.SchemeGroupVersion.WithResource("secrets"): api.ResourceSecrets,
}
// NewEvaluators returns the list of static evaluators that manage more than counts
func NewEvaluators(f quota.ListerForResourceFunc) []quota.Evaluator {
// these evaluators have special logic
result := []quota.Evaluator{
NewPodEvaluator(f, clock.RealClock{}),
NewServiceEvaluator(f),
NewPersistentVolumeClaimEvaluator(f),
}
// these evaluators require an alias for backwards compatibility
for gvr, alias := range legacyObjectCountAliases {
result = append(result,
generic.NewObjectCountEvaluator(false, gvr.GroupResource(), generic.ListResourceUsingListerFunc(f, gvr), alias))
}
return result
}

View File

@ -1,61 +0,0 @@
/*
Copyright 2016 The Kubernetes Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package core
import (
"k8s.io/api/core/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/client-go/informers"
clientset "k8s.io/client-go/kubernetes"
"k8s.io/kubernetes/pkg/api"
"k8s.io/kubernetes/pkg/quota"
"k8s.io/kubernetes/pkg/quota/generic"
)
// listReplicationControllersByNamespaceFuncUsingClient returns a replicationController listing function based on the provided client.
func listReplicationControllersByNamespaceFuncUsingClient(kubeClient clientset.Interface) generic.ListFuncByNamespace {
// TODO: ideally, we could pass dynamic client pool down into this code, and have one way of doing this.
// unfortunately, dynamic client works with Unstructured objects, and when we calculate Usage, we require
// structured objects.
return func(namespace string, options metav1.ListOptions) ([]runtime.Object, error) {
itemList, err := kubeClient.CoreV1().ReplicationControllers(namespace).List(options)
if err != nil {
return nil, err
}
results := make([]runtime.Object, 0, len(itemList.Items))
for i := range itemList.Items {
results = append(results, &itemList.Items[i])
}
return results, nil
}
}
// NewReplicationControllerEvaluator returns an evaluator that can evaluate replicationControllers
// if the specified shared informer factory is not nil, evaluator may use it to support listing functions.
func NewReplicationControllerEvaluator(kubeClient clientset.Interface, f informers.SharedInformerFactory) quota.Evaluator {
listFuncByNamespace := listReplicationControllersByNamespaceFuncUsingClient(kubeClient)
if f != nil {
listFuncByNamespace = generic.ListResourceUsingInformerFunc(f, v1.SchemeGroupVersion.WithResource("replicationcontrollers"))
}
return &generic.ObjectCountEvaluator{
AllowCreateOnUpdate: false,
InternalGroupKind: api.Kind("ReplicationController"),
ResourceName: api.ResourceReplicationControllers,
ListFuncByNamespace: listFuncByNamespace,
}
}

View File

@ -1,61 +0,0 @@
/*
Copyright 2016 The Kubernetes Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package core
import (
"k8s.io/api/core/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/client-go/informers"
clientset "k8s.io/client-go/kubernetes"
"k8s.io/kubernetes/pkg/api"
"k8s.io/kubernetes/pkg/quota"
"k8s.io/kubernetes/pkg/quota/generic"
)
// listResourceQuotasByNamespaceFuncUsingClient returns a resourceQuota listing function based on the provided client.
func listResourceQuotasByNamespaceFuncUsingClient(kubeClient clientset.Interface) generic.ListFuncByNamespace {
// TODO: ideally, we could pass dynamic client pool down into this code, and have one way of doing this.
// unfortunately, dynamic client works with Unstructured objects, and when we calculate Usage, we require
// structured objects.
return func(namespace string, options metav1.ListOptions) ([]runtime.Object, error) {
itemList, err := kubeClient.CoreV1().ResourceQuotas(namespace).List(options)
if err != nil {
return nil, err
}
results := make([]runtime.Object, 0, len(itemList.Items))
for i := range itemList.Items {
results = append(results, &itemList.Items[i])
}
return results, nil
}
}
// NewResourceQuotaEvaluator returns an evaluator that can evaluate resourceQuotas
// if the specified shared informer factory is not nil, evaluator may use it to support listing functions.
func NewResourceQuotaEvaluator(kubeClient clientset.Interface, f informers.SharedInformerFactory) quota.Evaluator {
listFuncByNamespace := listResourceQuotasByNamespaceFuncUsingClient(kubeClient)
if f != nil {
listFuncByNamespace = generic.ListResourceUsingInformerFunc(f, v1.SchemeGroupVersion.WithResource("resourcequotas"))
}
return &generic.ObjectCountEvaluator{
AllowCreateOnUpdate: false,
InternalGroupKind: api.Kind("ResourceQuota"),
ResourceName: api.ResourceQuotas,
ListFuncByNamespace: listFuncByNamespace,
}
}

View File

@ -1,61 +0,0 @@
/*
Copyright 2016 The Kubernetes Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package core
import (
"k8s.io/api/core/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/client-go/informers"
clientset "k8s.io/client-go/kubernetes"
"k8s.io/kubernetes/pkg/api"
"k8s.io/kubernetes/pkg/quota"
"k8s.io/kubernetes/pkg/quota/generic"
)
// listSecretsByNamespaceFuncUsingClient returns a secret listing function based on the provided client.
func listSecretsByNamespaceFuncUsingClient(kubeClient clientset.Interface) generic.ListFuncByNamespace {
// TODO: ideally, we could pass dynamic client pool down into this code, and have one way of doing this.
// unfortunately, dynamic client works with Unstructured objects, and when we calculate Usage, we require
// structured objects.
return func(namespace string, options metav1.ListOptions) ([]runtime.Object, error) {
itemList, err := kubeClient.CoreV1().Secrets(namespace).List(options)
if err != nil {
return nil, err
}
results := make([]runtime.Object, 0, len(itemList.Items))
for i := range itemList.Items {
results = append(results, &itemList.Items[i])
}
return results, nil
}
}
// NewSecretEvaluator returns an evaluator that can evaluate secrets
// if the specified shared informer factory is not nil, evaluator may use it to support listing functions.
func NewSecretEvaluator(kubeClient clientset.Interface, f informers.SharedInformerFactory) quota.Evaluator {
listFuncByNamespace := listSecretsByNamespaceFuncUsingClient(kubeClient)
if f != nil {
listFuncByNamespace = generic.ListResourceUsingInformerFunc(f, v1.SchemeGroupVersion.WithResource("secrets"))
}
return &generic.ObjectCountEvaluator{
AllowCreateOnUpdate: false,
InternalGroupKind: api.Kind("Secret"),
ResourceName: api.ResourceSecrets,
ListFuncByNamespace: listFuncByNamespace,
}
}

View File

@ -18,58 +18,34 @@ package core
import (
"fmt"
"strings"
"k8s.io/api/core/v1"
"k8s.io/apimachinery/pkg/api/resource"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/apimachinery/pkg/runtime/schema"
"k8s.io/apimachinery/pkg/util/sets"
"k8s.io/apiserver/pkg/admission"
"k8s.io/client-go/informers"
clientset "k8s.io/client-go/kubernetes"
"k8s.io/kubernetes/pkg/api"
k8s_api_v1 "k8s.io/kubernetes/pkg/api/v1"
"k8s.io/kubernetes/pkg/quota"
"k8s.io/kubernetes/pkg/quota/generic"
)
// the name used for object count quota
var serviceObjectCountName = generic.ObjectCountQuotaResourceNameFor(v1.SchemeGroupVersion.WithResource("services").GroupResource())
// serviceResources are the set of resources managed by quota associated with services.
var serviceResources = []api.ResourceName{
serviceObjectCountName,
api.ResourceServices,
api.ResourceServicesNodePorts,
api.ResourceServicesLoadBalancers,
}
// listServicesByNamespaceFuncUsingClient returns a service listing function based on the provided client.
func listServicesByNamespaceFuncUsingClient(kubeClient clientset.Interface) generic.ListFuncByNamespace {
// TODO: ideally, we could pass dynamic client pool down into this code, and have one way of doing this.
// unfortunately, dynamic client works with Unstructured objects, and when we calculate Usage, we require
// structured objects.
return func(namespace string, options metav1.ListOptions) ([]runtime.Object, error) {
itemList, err := kubeClient.CoreV1().Services(namespace).List(options)
if err != nil {
return nil, err
}
results := make([]runtime.Object, 0, len(itemList.Items))
for i := range itemList.Items {
results = append(results, &itemList.Items[i])
}
return results, nil
}
}
// NewServiceEvaluator returns an evaluator that can evaluate services
// if the specified shared informer factory is not nil, evaluator may use it to support listing functions.
func NewServiceEvaluator(kubeClient clientset.Interface, f informers.SharedInformerFactory) quota.Evaluator {
listFuncByNamespace := listServicesByNamespaceFuncUsingClient(kubeClient)
if f != nil {
listFuncByNamespace = generic.ListResourceUsingInformerFunc(f, v1.SchemeGroupVersion.WithResource("services"))
}
return &serviceEvaluator{
listFuncByNamespace: listFuncByNamespace,
}
// NewServiceEvaluator returns an evaluator that can evaluate services.
func NewServiceEvaluator(f quota.ListerForResourceFunc) quota.Evaluator {
listFuncByNamespace := generic.ListResourceUsingListerFunc(f, v1.SchemeGroupVersion.WithResource("services"))
serviceEvaluator := &serviceEvaluator{listFuncByNamespace: listFuncByNamespace}
return serviceEvaluator
}
// serviceEvaluator knows how to measure usage for services.
@ -80,31 +56,13 @@ type serviceEvaluator struct {
// 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(), ","))
// this is a no-op for services
return nil
}
// GroupKind that this evaluator tracks
func (p *serviceEvaluator) GroupKind() schema.GroupKind {
return api.Kind("Service")
// GroupResource that this evaluator tracks
func (p *serviceEvaluator) GroupResource() schema.GroupResource {
return v1.SchemeGroupVersion.WithResource("services").GroupResource()
}
// Handles returns true of the evaluator should handle the specified operation.
@ -149,6 +107,7 @@ func (p *serviceEvaluator) Usage(item runtime.Object) (api.ResourceList, error)
}
ports := len(svc.Spec.Ports)
// default service usage
result[serviceObjectCountName] = *(resource.NewQuantity(1, resource.DecimalSI))
result[api.ResourceServices] = *(resource.NewQuantity(1, resource.DecimalSI))
result[api.ResourceServicesLoadBalancers] = resource.Quantity{Format: resource.DecimalSI}
result[api.ResourceServicesNodePorts] = resource.Quantity{Format: resource.DecimalSI}

View File

@ -20,14 +20,14 @@ import (
"testing"
"k8s.io/apimachinery/pkg/api/resource"
"k8s.io/client-go/kubernetes/fake"
"k8s.io/apimachinery/pkg/runtime/schema"
"k8s.io/kubernetes/pkg/api"
"k8s.io/kubernetes/pkg/quota"
"k8s.io/kubernetes/pkg/quota/generic"
)
func TestServiceEvaluatorMatchesResources(t *testing.T) {
kubeClient := fake.NewSimpleClientset()
evaluator := NewServiceEvaluator(kubeClient, nil)
evaluator := NewServiceEvaluator(nil)
// we give a lot of resources
input := []api.ResourceName{
api.ResourceConfigMaps,
@ -49,8 +49,7 @@ func TestServiceEvaluatorMatchesResources(t *testing.T) {
}
func TestServiceEvaluatorUsage(t *testing.T) {
kubeClient := fake.NewSimpleClientset()
evaluator := NewServiceEvaluator(kubeClient, nil)
evaluator := NewServiceEvaluator(nil)
testCases := map[string]struct {
service *api.Service
usage api.ResourceList
@ -65,6 +64,7 @@ func TestServiceEvaluatorUsage(t *testing.T) {
api.ResourceServicesNodePorts: resource.MustParse("0"),
api.ResourceServicesLoadBalancers: resource.MustParse("1"),
api.ResourceServices: resource.MustParse("1"),
generic.ObjectCountQuotaResourceNameFor(schema.GroupResource{Resource: "services"}): resource.MustParse("1"),
},
},
"loadbalancer_ports": {
@ -82,6 +82,7 @@ func TestServiceEvaluatorUsage(t *testing.T) {
api.ResourceServicesNodePorts: resource.MustParse("1"),
api.ResourceServicesLoadBalancers: resource.MustParse("1"),
api.ResourceServices: resource.MustParse("1"),
generic.ObjectCountQuotaResourceNameFor(schema.GroupResource{Resource: "services"}): resource.MustParse("1"),
},
},
"clusterip": {
@ -91,9 +92,10 @@ func TestServiceEvaluatorUsage(t *testing.T) {
},
},
usage: api.ResourceList{
api.ResourceServices: resource.MustParse("1"),
api.ResourceServicesNodePorts: resource.MustParse("0"),
api.ResourceServicesLoadBalancers: resource.MustParse("0"),
api.ResourceServices: resource.MustParse("1"),
api.ResourceServicesNodePorts: resource.MustParse("0"),
api.ResourceServicesLoadBalancers: resource.MustParse("0"),
generic.ObjectCountQuotaResourceNameFor(schema.GroupResource{Resource: "services"}): resource.MustParse("1"),
},
},
"nodeports": {
@ -108,9 +110,10 @@ func TestServiceEvaluatorUsage(t *testing.T) {
},
},
usage: api.ResourceList{
api.ResourceServices: resource.MustParse("1"),
api.ResourceServicesNodePorts: resource.MustParse("1"),
api.ResourceServicesLoadBalancers: resource.MustParse("0"),
api.ResourceServices: resource.MustParse("1"),
api.ResourceServicesNodePorts: resource.MustParse("1"),
api.ResourceServicesLoadBalancers: resource.MustParse("0"),
generic.ObjectCountQuotaResourceNameFor(schema.GroupResource{Resource: "services"}): resource.MustParse("1"),
},
},
"multi-nodeports": {
@ -128,9 +131,10 @@ func TestServiceEvaluatorUsage(t *testing.T) {
},
},
usage: api.ResourceList{
api.ResourceServices: resource.MustParse("1"),
api.ResourceServicesNodePorts: resource.MustParse("2"),
api.ResourceServicesLoadBalancers: resource.MustParse("0"),
api.ResourceServices: resource.MustParse("1"),
api.ResourceServicesNodePorts: resource.MustParse("2"),
api.ResourceServicesLoadBalancers: resource.MustParse("0"),
generic.ObjectCountQuotaResourceNameFor(schema.GroupResource{Resource: "services"}): resource.MustParse("1"),
},
},
}
@ -198,8 +202,7 @@ func TestServiceConstraintsFunc(t *testing.T) {
},
}
kubeClient := fake.NewSimpleClientset()
evaluator := NewServiceEvaluator(kubeClient, nil)
evaluator := NewServiceEvaluator(nil)
for testName, test := range testCases {
err := evaluator.Constraints(test.required, test.service)
switch {

View File

@ -8,6 +8,7 @@ load(
go_library(
name = "go_default_library",
srcs = [
"configuration.go",
"evaluator.go",
"registry.go",
],
@ -16,12 +17,12 @@ go_library(
"//pkg/api:go_default_library",
"//pkg/quota:go_default_library",
"//vendor/k8s.io/apimachinery/pkg/api/resource:go_default_library",
"//vendor/k8s.io/apimachinery/pkg/apis/meta/v1:go_default_library",
"//vendor/k8s.io/apimachinery/pkg/labels:go_default_library",
"//vendor/k8s.io/apimachinery/pkg/runtime:go_default_library",
"//vendor/k8s.io/apimachinery/pkg/runtime/schema:go_default_library",
"//vendor/k8s.io/apiserver/pkg/admission:go_default_library",
"//vendor/k8s.io/client-go/informers:go_default_library",
"//vendor/k8s.io/client-go/tools/cache:go_default_library",
],
)

View File

@ -0,0 +1,44 @@
/*
Copyright 2017 The Kubernetes Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package generic
import (
"k8s.io/apimachinery/pkg/runtime/schema"
"k8s.io/kubernetes/pkg/quota"
)
// implements a basic configuration
type simpleConfiguration struct {
evaluators []quota.Evaluator
ignoredResources map[schema.GroupResource]struct{}
}
// NewConfiguration creates a quota configuration
func NewConfiguration(evaluators []quota.Evaluator, ignoredResources map[schema.GroupResource]struct{}) quota.Configuration {
return &simpleConfiguration{
evaluators: evaluators,
ignoredResources: ignoredResources,
}
}
func (c *simpleConfiguration) IgnoredResources() map[schema.GroupResource]struct{} {
return c.ignoredResources
}
func (c *simpleConfiguration) Evaluators() []quota.Evaluator {
return c.evaluators
}

View File

@ -20,33 +20,51 @@ import (
"fmt"
"k8s.io/apimachinery/pkg/api/resource"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/labels"
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/apimachinery/pkg/runtime/schema"
"k8s.io/apiserver/pkg/admission"
"k8s.io/client-go/informers"
"k8s.io/client-go/tools/cache"
"k8s.io/kubernetes/pkg/api"
"k8s.io/kubernetes/pkg/quota"
)
// ListResourceUsingInformerFunc returns a listing function based on the shared informer factory for the specified resource.
func ListResourceUsingInformerFunc(f informers.SharedInformerFactory, resource schema.GroupVersionResource) ListFuncByNamespace {
return func(namespace string, options metav1.ListOptions) ([]runtime.Object, error) {
labelSelector, err := labels.Parse(options.LabelSelector)
// InformerForResourceFunc knows how to provision an informer
type InformerForResourceFunc func(schema.GroupVersionResource) (informers.GenericInformer, error)
// ListerFuncForResourceFunc knows how to provision a lister from an informer func
func ListerFuncForResourceFunc(f InformerForResourceFunc) quota.ListerForResourceFunc {
return func(gvr schema.GroupVersionResource) (cache.GenericLister, error) {
informer, err := f(gvr)
if err != nil {
return nil, err
}
informer, err := f.ForResource(resource)
if err != nil {
return nil, err
}
return informer.Lister().ByNamespace(namespace).List(labelSelector)
return informer.Lister(), nil
}
}
// ListResourceUsingListerFunc returns a listing function based on the shared informer factory for the specified resource.
func ListResourceUsingListerFunc(l quota.ListerForResourceFunc, resource schema.GroupVersionResource) ListFuncByNamespace {
return func(namespace string) ([]runtime.Object, error) {
lister, err := l(resource)
if err != nil {
return nil, err
}
return lister.ByNamespace(namespace).List(labels.Everything())
}
}
// ObjectCountQuotaResourceNameFor returns the object count quota name for specified groupResource
func ObjectCountQuotaResourceNameFor(groupResource schema.GroupResource) api.ResourceName {
if len(groupResource.Group) == 0 {
return api.ResourceName("count/" + groupResource.Resource)
}
return api.ResourceName("count/" + groupResource.Resource + "." + groupResource.Group)
}
// ListFuncByNamespace knows how to list resources in a namespace
type ListFuncByNamespace func(namespace string, options metav1.ListOptions) ([]runtime.Object, error)
type ListFuncByNamespace func(namespace string) ([]runtime.Object, error)
// MatchesScopeFunc knows how to evaluate if an object matches a scope
type MatchesScopeFunc func(scope api.ResourceQuotaScope, object runtime.Object) (bool, error)
@ -91,9 +109,7 @@ func CalculateUsageStats(options quota.UsageStatsOptions,
for _, resourceName := range options.Resources {
result.Used[resourceName] = resource.Quantity{Format: resource.DecimalSI}
}
items, err := listFunc(options.Namespace, metav1.ListOptions{
LabelSelector: labels.Everything().String(),
})
items, err := listFunc(options.Namespace)
if err != nil {
return result, fmt.Errorf("failed to list content: %v", err)
}
@ -121,63 +137,86 @@ func CalculateUsageStats(options quota.UsageStatsOptions,
return result, nil
}
// ObjectCountEvaluator provides an implementation for quota.Evaluator
// objectCountEvaluator provides an implementation for quota.Evaluator
// that associates usage of the specified resource based on the number of items
// returned by the specified listing function.
type ObjectCountEvaluator struct {
// AllowCreateOnUpdate if true will ensure the evaluator tracks create
type objectCountEvaluator struct {
// allowCreateOnUpdate if true will ensure the evaluator tracks create
// and update operations.
AllowCreateOnUpdate bool
// GroupKind that this evaluator tracks.
InternalGroupKind schema.GroupKind
allowCreateOnUpdate bool
// GroupResource that this evaluator tracks.
// It is used to construct a generic object count quota name
groupResource schema.GroupResource
// A function that knows how to list resources by namespace.
// TODO move to dynamic client in future
ListFuncByNamespace ListFuncByNamespace
// Name associated with this resource in the quota.
ResourceName api.ResourceName
listFuncByNamespace ListFuncByNamespace
// Names associated with this resource in the quota for generic counting.
resourceNames []api.ResourceName
}
// Constraints returns an error if the configured resource name is not in the required set.
func (o *ObjectCountEvaluator) Constraints(required []api.ResourceName, item runtime.Object) error {
if !quota.Contains(required, o.ResourceName) {
return fmt.Errorf("missing %s", o.ResourceName)
}
func (o *objectCountEvaluator) Constraints(required []api.ResourceName, item runtime.Object) error {
// no-op for object counting
return nil
}
// GroupKind that this evaluator tracks
func (o *ObjectCountEvaluator) GroupKind() schema.GroupKind {
return o.InternalGroupKind
}
// Handles returns true if the object count evaluator needs to track this attributes.
func (o *ObjectCountEvaluator) Handles(a admission.Attributes) bool {
func (o *objectCountEvaluator) Handles(a admission.Attributes) bool {
operation := a.GetOperation()
return operation == admission.Create || (o.AllowCreateOnUpdate && operation == admission.Update)
return operation == admission.Create || (o.allowCreateOnUpdate && operation == admission.Update)
}
// Matches returns true if the evaluator matches the specified quota with the provided input item
func (o *ObjectCountEvaluator) Matches(resourceQuota *api.ResourceQuota, item runtime.Object) (bool, error) {
func (o *objectCountEvaluator) Matches(resourceQuota *api.ResourceQuota, item runtime.Object) (bool, error) {
return Matches(resourceQuota, item, o.MatchingResources, MatchesNoScopeFunc)
}
// MatchingResources takes the input specified list of resources and returns the set of resources it matches.
func (o *ObjectCountEvaluator) MatchingResources(input []api.ResourceName) []api.ResourceName {
return quota.Intersection(input, []api.ResourceName{o.ResourceName})
func (o *objectCountEvaluator) MatchingResources(input []api.ResourceName) []api.ResourceName {
return quota.Intersection(input, o.resourceNames)
}
// Usage returns the resource usage for the specified object
func (o *ObjectCountEvaluator) Usage(object runtime.Object) (api.ResourceList, error) {
func (o *objectCountEvaluator) Usage(object runtime.Object) (api.ResourceList, error) {
quantity := resource.NewQuantity(1, resource.DecimalSI)
return api.ResourceList{
o.ResourceName: *quantity,
}, nil
resourceList := api.ResourceList{}
for _, resourceName := range o.resourceNames {
resourceList[resourceName] = *quantity
}
return resourceList, nil
}
// GroupResource tracked by this evaluator
func (o *objectCountEvaluator) GroupResource() schema.GroupResource {
return o.groupResource
}
// UsageStats calculates aggregate usage for the object.
func (o *ObjectCountEvaluator) UsageStats(options quota.UsageStatsOptions) (quota.UsageStats, error) {
return CalculateUsageStats(options, o.ListFuncByNamespace, MatchesNoScopeFunc, o.Usage)
func (o *objectCountEvaluator) UsageStats(options quota.UsageStatsOptions) (quota.UsageStats, error) {
return CalculateUsageStats(options, o.listFuncByNamespace, MatchesNoScopeFunc, o.Usage)
}
// Verify implementation of interface at compile time.
var _ quota.Evaluator = &ObjectCountEvaluator{}
var _ quota.Evaluator = &objectCountEvaluator{}
// NewObjectCountEvaluator returns an evaluator that can perform generic
// object quota counting. It allows an optional alias for backwards compatibilty
// purposes for the legacy object counting names in quota. Unless its supporting
// backward compatibility, alias should not be used.
func NewObjectCountEvaluator(
allowCreateOnUpdate bool,
groupResource schema.GroupResource, listFuncByNamespace ListFuncByNamespace,
alias api.ResourceName) quota.Evaluator {
resourceNames := []api.ResourceName{ObjectCountQuotaResourceNameFor(groupResource)}
if len(alias) > 0 {
resourceNames = append(resourceNames, alias)
}
return &objectCountEvaluator{
allowCreateOnUpdate: allowCreateOnUpdate,
groupResource: groupResource,
listFuncByNamespace: listFuncByNamespace,
resourceNames: resourceNames,
}
}

View File

@ -17,20 +17,65 @@ limitations under the License.
package generic
import (
"sync"
"k8s.io/apimachinery/pkg/runtime/schema"
"k8s.io/kubernetes/pkg/quota"
)
// Ensure it implements the required interface
var _ quota.Registry = &GenericRegistry{}
// GenericRegistry implements Registry
type GenericRegistry struct {
// internal evaluators by group kind
InternalEvaluators map[schema.GroupKind]quota.Evaluator
// implements a basic registry
type simpleRegistry struct {
lock sync.RWMutex
// evaluators tracked by the registry
evaluators map[schema.GroupResource]quota.Evaluator
}
// Evaluators returns the map of evaluators by groupKind
func (r *GenericRegistry) Evaluators() map[schema.GroupKind]quota.Evaluator {
return r.InternalEvaluators
// NewRegistry creates a simple registry with initial list of evaluators
func NewRegistry(evaluators []quota.Evaluator) quota.Registry {
return &simpleRegistry{
evaluators: evaluatorsByGroupResource(evaluators),
}
}
func (r *simpleRegistry) Add(e quota.Evaluator) {
r.lock.Lock()
defer r.lock.Unlock()
r.evaluators[e.GroupResource()] = e
}
func (r *simpleRegistry) Remove(e quota.Evaluator) {
r.lock.Lock()
defer r.lock.Unlock()
delete(r.evaluators, e.GroupResource())
}
func (r *simpleRegistry) Get(gr schema.GroupResource) quota.Evaluator {
r.lock.RLock()
defer r.lock.RUnlock()
return r.evaluators[gr]
}
func (r *simpleRegistry) List() []quota.Evaluator {
r.lock.RLock()
defer r.lock.RUnlock()
return evaluatorsList(r.evaluators)
}
// evaluatorsByGroupResource converts a list of evaluators to a map by group resource.
func evaluatorsByGroupResource(items []quota.Evaluator) map[schema.GroupResource]quota.Evaluator {
result := map[schema.GroupResource]quota.Evaluator{}
for _, item := range items {
result[item.GroupResource()] = item
}
return result
}
// evaluatorsList converts a map of evaluators to list
func evaluatorsList(input map[schema.GroupResource]quota.Evaluator) []quota.Evaluator {
var result []quota.Evaluator
for _, item := range input {
result = append(result, item)
}
return result
}

View File

@ -12,8 +12,8 @@ go_library(
deps = [
"//pkg/quota:go_default_library",
"//pkg/quota/evaluator/core:go_default_library",
"//vendor/k8s.io/client-go/informers:go_default_library",
"//vendor/k8s.io/client-go/kubernetes:go_default_library",
"//pkg/quota/generic:go_default_library",
"//vendor/k8s.io/apimachinery/pkg/runtime/schema:go_default_library",
],
)

View File

@ -17,15 +17,42 @@ limitations under the License.
package install
import (
"k8s.io/client-go/informers"
clientset "k8s.io/client-go/kubernetes"
"k8s.io/apimachinery/pkg/runtime/schema"
"k8s.io/kubernetes/pkg/quota"
"k8s.io/kubernetes/pkg/quota/evaluator/core"
"k8s.io/kubernetes/pkg/quota/generic"
)
// NewRegistry returns a registry of quota evaluators.
// If a shared informer factory is provided, it is used by evaluators rather than performing direct queries.
func NewRegistry(kubeClient clientset.Interface, f informers.SharedInformerFactory) quota.Registry {
// TODO: when quota supports resources in other api groups, we will need to merge
return core.NewRegistry(kubeClient, f)
// NewQuotaConfigurationForAdmission returns a quota configuration for admission control.
func NewQuotaConfigurationForAdmission() quota.Configuration {
evaluators := core.NewEvaluators(nil)
return generic.NewConfiguration(evaluators, DefaultIgnoredResources())
}
// NewQuotaConfigurationForControllers returns a quota configuration for controllers.
func NewQuotaConfigurationForControllers(f quota.ListerForResourceFunc) quota.Configuration {
evaluators := core.NewEvaluators(f)
return generic.NewConfiguration(evaluators, DefaultIgnoredResources())
}
// ignoredResources are ignored by quota by default
var ignoredResources = map[schema.GroupResource]struct{}{
{Group: "extensions", Resource: "replicationcontrollers"}: {},
{Group: "extensions", Resource: "networkpolicies"}: {},
{Group: "", Resource: "bindings"}: {},
{Group: "", Resource: "componentstatuses"}: {},
{Group: "", Resource: "events"}: {},
{Group: "authentication.k8s.io", Resource: "tokenreviews"}: {},
{Group: "authorization.k8s.io", Resource: "subjectaccessreviews"}: {},
{Group: "authorization.k8s.io", Resource: "selfsubjectaccessreviews"}: {},
{Group: "authorization.k8s.io", Resource: "localsubjectaccessreviews"}: {},
{Group: "authorization.k8s.io", Resource: "selfsubjectrulesreviews"}: {},
{Group: "apiregistration.k8s.io", Resource: "apiservices"}: {},
{Group: "apiextensions.k8s.io", Resource: "customresourcedefinitions"}: {},
}
// DefaultIgnoredResources returns the default set of resources that quota system
// should ignore. This is exposed so downstream integrators can have access to them.
func DefaultIgnoredResources() map[schema.GroupResource]struct{} {
return ignoredResources
}

View File

@ -20,6 +20,7 @@ import (
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/apimachinery/pkg/runtime/schema"
"k8s.io/apiserver/pkg/admission"
"k8s.io/client-go/tools/cache"
"k8s.io/kubernetes/pkg/api"
)
@ -39,12 +40,12 @@ type UsageStats struct {
Used api.ResourceList
}
// Evaluator knows how to evaluate quota usage for a particular group kind
// Evaluator knows how to evaluate quota usage for a particular group resource
type Evaluator interface {
// Constraints ensures that each required resource is present on item
Constraints(required []api.ResourceName, item runtime.Object) error
// GroupKind returns the groupKind that this object knows how to evaluate
GroupKind() schema.GroupKind
// GroupResource returns the groupResource that this object knows how to evaluate
GroupResource() schema.GroupResource
// Handles determines if quota could be impacted by the specified attribute.
// If true, admission control must perform quota processing for the operation, otherwise it is safe to ignore quota.
Handles(operation admission.Attributes) bool
@ -58,25 +59,25 @@ type Evaluator interface {
UsageStats(options UsageStatsOptions) (UsageStats, error)
}
// Registry holds the list of evaluators associated to a particular group kind
// Configuration defines how the quota system is configured.
type Configuration interface {
// IgnoredResources are ignored by quota.
IgnoredResources() map[schema.GroupResource]struct{}
// Evaluators for quota evaluation.
Evaluators() []Evaluator
}
// Registry maintains a list of evaluators
type Registry interface {
// Evaluators returns the set Evaluator objects registered to a groupKind
Evaluators() map[schema.GroupKind]Evaluator
// Add to registry
Add(e Evaluator)
// Remove from registry
Remove(e Evaluator)
// Get by group resource
Get(gr schema.GroupResource) Evaluator
// List from registry
List() []Evaluator
}
// UnionRegistry combines multiple registries. Order matters because first registry to claim a GroupKind
// is the "winner"
type UnionRegistry []Registry
// Evaluators returns a mapping of evaluators by group kind.
func (r UnionRegistry) Evaluators() map[schema.GroupKind]Evaluator {
ret := map[schema.GroupKind]Evaluator{}
for i := len(r) - 1; i >= 0; i-- {
for k, v := range r[i].Evaluators() {
ret[k] = v
}
}
return ret
}
// ListerForResourceFunc knows how to get a lister for a specific resource
type ListerForResourceFunc func(schema.GroupVersionResource) (cache.GenericLister, error)

View File

@ -247,7 +247,7 @@ func CalculateUsage(namespaceName string, scopes []api.ResourceQuotaScope, hardL
// look to measure updated usage stats for
hardResources := ResourceNames(hardLimits)
potentialResources := []api.ResourceName{}
evaluators := registry.Evaluators()
evaluators := registry.List()
for _, evaluator := range evaluators {
potentialResources = append(potentialResources, evaluator.MatchingResources(hardResources)...)
}

View File

@ -23,6 +23,7 @@ go_library(
"//pkg/client/listers/core/internalversion:go_default_library",
"//pkg/kubeapiserver/admission:go_default_library",
"//pkg/quota:go_default_library",
"//pkg/quota/generic:go_default_library",
"//pkg/util/reflector/prometheus:go_default_library",
"//pkg/util/workqueue/prometheus:go_default_library",
"//plugin/pkg/admission/resourcequota/apis/resourcequota:go_default_library",
@ -58,14 +59,11 @@ go_test(
"//pkg/client/clientset_generated/internalclientset/fake:go_default_library",
"//pkg/client/informers/informers_generated/internalversion:go_default_library",
"//pkg/controller:go_default_library",
"//pkg/quota:go_default_library",
"//pkg/quota/generic:go_default_library",
"//pkg/quota/install:go_default_library",
"//plugin/pkg/admission/resourcequota/apis/resourcequota:go_default_library",
"//vendor/github.com/hashicorp/golang-lru:go_default_library",
"//vendor/k8s.io/apimachinery/pkg/api/resource:go_default_library",
"//vendor/k8s.io/apimachinery/pkg/apis/meta/v1:go_default_library",
"//vendor/k8s.io/apimachinery/pkg/runtime/schema:go_default_library",
"//vendor/k8s.io/apimachinery/pkg/util/sets:go_default_library",
"//vendor/k8s.io/apiserver/pkg/admission:go_default_library",
"//vendor/k8s.io/apiserver/pkg/util/feature:go_default_library",

View File

@ -53,16 +53,16 @@ func Register(plugins *admission.Plugins) {
// quotaAdmission implements an admission controller that can enforce quota constraints
type quotaAdmission struct {
*admission.Handler
config *resourcequotaapi.Configuration
stopCh <-chan struct{}
registry quota.Registry
numEvaluators int
quotaAccessor *quotaAccessor
evaluator Evaluator
config *resourcequotaapi.Configuration
stopCh <-chan struct{}
quotaConfiguration quota.Configuration
numEvaluators int
quotaAccessor *quotaAccessor
evaluator Evaluator
}
var _ = kubeapiserveradmission.WantsInternalKubeClientSet(&quotaAdmission{})
var _ = kubeapiserveradmission.WantsQuotaRegistry(&quotaAdmission{})
var _ = kubeapiserveradmission.WantsQuotaConfiguration(&quotaAdmission{})
type liveLookupEntry struct {
expiry time.Time
@ -95,9 +95,9 @@ func (a *quotaAdmission) SetInternalKubeInformerFactory(f informers.SharedInform
a.quotaAccessor.lister = f.Core().InternalVersion().ResourceQuotas().Lister()
}
func (a *quotaAdmission) SetQuotaRegistry(r quota.Registry) {
a.registry = r
a.evaluator = NewQuotaEvaluator(a.quotaAccessor, a.registry, nil, a.config, a.numEvaluators, a.stopCh)
func (a *quotaAdmission) SetQuotaConfiguration(c quota.Configuration) {
a.quotaConfiguration = c
a.evaluator = NewQuotaEvaluator(a.quotaAccessor, a.quotaConfiguration, nil, a.config, a.numEvaluators, a.stopCh)
}
// Validate ensures an authorizer is set.
@ -111,8 +111,8 @@ func (a *quotaAdmission) Validate() error {
if a.quotaAccessor.lister == nil {
return fmt.Errorf("missing quotaAccessor.lister")
}
if a.registry == nil {
return fmt.Errorf("missing registry")
if a.quotaConfiguration == nil {
return fmt.Errorf("missing quotaConfiguration")
}
if a.evaluator == nil {
return fmt.Errorf("missing evaluator")
@ -126,5 +126,9 @@ func (a *quotaAdmission) Admit(attr admission.Attributes) (err error) {
if attr.GetSubresource() != "" {
return nil
}
// ignore all operations that are not namespaced
if attr.GetNamespace() == "" {
return nil
}
return a.evaluator.Evaluate(attr)
}

View File

@ -26,7 +26,6 @@ import (
"k8s.io/apimachinery/pkg/api/resource"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/runtime/schema"
"k8s.io/apimachinery/pkg/util/sets"
"k8s.io/apiserver/pkg/admission"
utilfeature "k8s.io/apiserver/pkg/util/feature"
@ -36,8 +35,6 @@ import (
"k8s.io/kubernetes/pkg/client/clientset_generated/internalclientset/fake"
informers "k8s.io/kubernetes/pkg/client/informers/informers_generated/internalversion"
"k8s.io/kubernetes/pkg/controller"
"k8s.io/kubernetes/pkg/quota"
"k8s.io/kubernetes/pkg/quota/generic"
"k8s.io/kubernetes/pkg/quota/install"
resourcequotaapi "k8s.io/kubernetes/plugin/pkg/admission/resourcequota/apis/resourcequota"
)
@ -135,7 +132,8 @@ func TestAdmissionIgnoresDelete(t *testing.T) {
quotaAccessor.client = kubeClient
quotaAccessor.lister = informerFactory.Core().InternalVersion().ResourceQuotas().Lister()
config := &resourcequotaapi.Configuration{}
evaluator := NewQuotaEvaluator(quotaAccessor, install.NewRegistry(nil, nil), nil, config, 5, stopCh)
quotaConfiguration := install.NewQuotaConfigurationForAdmission()
evaluator := NewQuotaEvaluator(quotaAccessor, quotaConfiguration, nil, config, 5, stopCh)
handler := &quotaAdmission{
Handler: admission.NewHandler(admission.Create, admission.Update),
@ -170,7 +168,8 @@ func TestAdmissionIgnoresSubresources(t *testing.T) {
quotaAccessor.client = kubeClient
quotaAccessor.lister = informerFactory.Core().InternalVersion().ResourceQuotas().Lister()
config := &resourcequotaapi.Configuration{}
evaluator := NewQuotaEvaluator(quotaAccessor, install.NewRegistry(nil, nil), nil, config, 5, stopCh)
quotaConfiguration := install.NewQuotaConfigurationForAdmission()
evaluator := NewQuotaEvaluator(quotaAccessor, quotaConfiguration, nil, config, 5, stopCh)
handler := &quotaAdmission{
Handler: admission.NewHandler(admission.Create, admission.Update),
@ -214,7 +213,8 @@ func TestAdmitBelowQuotaLimit(t *testing.T) {
quotaAccessor.client = kubeClient
quotaAccessor.lister = informerFactory.Core().InternalVersion().ResourceQuotas().Lister()
config := &resourcequotaapi.Configuration{}
evaluator := NewQuotaEvaluator(quotaAccessor, install.NewRegistry(nil, nil), nil, config, 5, stopCh)
quotaConfiguration := install.NewQuotaConfigurationForAdmission()
evaluator := NewQuotaEvaluator(quotaAccessor, quotaConfiguration, nil, config, 5, stopCh)
handler := &quotaAdmission{
Handler: admission.NewHandler(admission.Create, admission.Update),
@ -297,7 +297,8 @@ func TestAdmitHandlesOldObjects(t *testing.T) {
quotaAccessor.client = kubeClient
quotaAccessor.lister = informerFactory.Core().InternalVersion().ResourceQuotas().Lister()
config := &resourcequotaapi.Configuration{}
evaluator := NewQuotaEvaluator(quotaAccessor, install.NewRegistry(nil, nil), nil, config, 5, stopCh)
quotaConfiguration := install.NewQuotaConfigurationForAdmission()
evaluator := NewQuotaEvaluator(quotaAccessor, quotaConfiguration, nil, config, 5, stopCh)
handler := &quotaAdmission{
Handler: admission.NewHandler(admission.Create, admission.Update),
@ -403,7 +404,8 @@ func TestAdmitHandlesNegativePVCUpdates(t *testing.T) {
quotaAccessor.client = kubeClient
quotaAccessor.lister = informerFactory.Core().InternalVersion().ResourceQuotas().Lister()
config := &resourcequotaapi.Configuration{}
evaluator := NewQuotaEvaluator(quotaAccessor, install.NewRegistry(nil, nil), nil, config, 5, stopCh)
quotaConfiguration := install.NewQuotaConfigurationForAdmission()
evaluator := NewQuotaEvaluator(quotaAccessor, quotaConfiguration, nil, config, 5, stopCh)
handler := &quotaAdmission{
Handler: admission.NewHandler(admission.Create, admission.Update),
@ -469,7 +471,8 @@ func TestAdmitHandlesPVCUpdates(t *testing.T) {
quotaAccessor.client = kubeClient
quotaAccessor.lister = informerFactory.Core().InternalVersion().ResourceQuotas().Lister()
config := &resourcequotaapi.Configuration{}
evaluator := NewQuotaEvaluator(quotaAccessor, install.NewRegistry(nil, nil), nil, config, 5, stopCh)
quotaConfiguration := install.NewQuotaConfigurationForAdmission()
evaluator := NewQuotaEvaluator(quotaAccessor, quotaConfiguration, nil, config, 5, stopCh)
handler := &quotaAdmission{
Handler: admission.NewHandler(admission.Create, admission.Update),
@ -567,7 +570,8 @@ func TestAdmitHandlesCreatingUpdates(t *testing.T) {
quotaAccessor.client = kubeClient
quotaAccessor.lister = informerFactory.Core().InternalVersion().ResourceQuotas().Lister()
config := &resourcequotaapi.Configuration{}
evaluator := NewQuotaEvaluator(quotaAccessor, install.NewRegistry(nil, nil), nil, config, 5, stopCh)
quotaConfiguration := install.NewQuotaConfigurationForAdmission()
evaluator := NewQuotaEvaluator(quotaAccessor, quotaConfiguration, nil, config, 5, stopCh)
handler := &quotaAdmission{
Handler: admission.NewHandler(admission.Create, admission.Update),
@ -661,7 +665,8 @@ func TestAdmitExceedQuotaLimit(t *testing.T) {
quotaAccessor.client = kubeClient
quotaAccessor.lister = informerFactory.Core().InternalVersion().ResourceQuotas().Lister()
config := &resourcequotaapi.Configuration{}
evaluator := NewQuotaEvaluator(quotaAccessor, install.NewRegistry(nil, nil), nil, config, 5, stopCh)
quotaConfiguration := install.NewQuotaConfigurationForAdmission()
evaluator := NewQuotaEvaluator(quotaAccessor, quotaConfiguration, nil, config, 5, stopCh)
handler := &quotaAdmission{
Handler: admission.NewHandler(admission.Create, admission.Update),
@ -705,7 +710,8 @@ func TestAdmitEnforceQuotaConstraints(t *testing.T) {
quotaAccessor.client = kubeClient
quotaAccessor.lister = informerFactory.Core().InternalVersion().ResourceQuotas().Lister()
config := &resourcequotaapi.Configuration{}
evaluator := NewQuotaEvaluator(quotaAccessor, install.NewRegistry(nil, nil), nil, config, 5, stopCh)
quotaConfiguration := install.NewQuotaConfigurationForAdmission()
evaluator := NewQuotaEvaluator(quotaAccessor, quotaConfiguration, nil, config, 5, stopCh)
handler := &quotaAdmission{
Handler: admission.NewHandler(admission.Create, admission.Update),
@ -759,7 +765,8 @@ func TestAdmitPodInNamespaceWithoutQuota(t *testing.T) {
quotaAccessor.lister = informerFactory.Core().InternalVersion().ResourceQuotas().Lister()
quotaAccessor.liveLookupCache = liveLookupCache
config := &resourcequotaapi.Configuration{}
evaluator := NewQuotaEvaluator(quotaAccessor, install.NewRegistry(nil, nil), nil, config, 5, stopCh)
quotaConfiguration := install.NewQuotaConfigurationForAdmission()
evaluator := NewQuotaEvaluator(quotaAccessor, quotaConfiguration, nil, config, 5, stopCh)
handler := &quotaAdmission{
Handler: admission.NewHandler(admission.Create, admission.Update),
@ -825,7 +832,8 @@ func TestAdmitBelowTerminatingQuotaLimit(t *testing.T) {
quotaAccessor.client = kubeClient
quotaAccessor.lister = informerFactory.Core().InternalVersion().ResourceQuotas().Lister()
config := &resourcequotaapi.Configuration{}
evaluator := NewQuotaEvaluator(quotaAccessor, install.NewRegistry(nil, nil), nil, config, 5, stopCh)
quotaConfiguration := install.NewQuotaConfigurationForAdmission()
evaluator := NewQuotaEvaluator(quotaAccessor, quotaConfiguration, nil, config, 5, stopCh)
handler := &quotaAdmission{
Handler: admission.NewHandler(admission.Create, admission.Update),
@ -930,7 +938,8 @@ func TestAdmitBelowBestEffortQuotaLimit(t *testing.T) {
quotaAccessor.client = kubeClient
quotaAccessor.lister = informerFactory.Core().InternalVersion().ResourceQuotas().Lister()
config := &resourcequotaapi.Configuration{}
evaluator := NewQuotaEvaluator(quotaAccessor, install.NewRegistry(nil, nil), nil, config, 5, stopCh)
quotaConfiguration := install.NewQuotaConfigurationForAdmission()
evaluator := NewQuotaEvaluator(quotaAccessor, quotaConfiguration, nil, config, 5, stopCh)
handler := &quotaAdmission{
Handler: admission.NewHandler(admission.Create, admission.Update),
@ -1022,7 +1031,8 @@ func TestAdmitBestEffortQuotaLimitIgnoresBurstable(t *testing.T) {
quotaAccessor.client = kubeClient
quotaAccessor.lister = informerFactory.Core().InternalVersion().ResourceQuotas().Lister()
config := &resourcequotaapi.Configuration{}
evaluator := NewQuotaEvaluator(quotaAccessor, install.NewRegistry(nil, nil), nil, config, 5, stopCh)
quotaConfiguration := install.NewQuotaConfigurationForAdmission()
evaluator := NewQuotaEvaluator(quotaAccessor, quotaConfiguration, nil, config, 5, stopCh)
handler := &quotaAdmission{
Handler: admission.NewHandler(admission.Create, admission.Update),
@ -1098,17 +1108,6 @@ func TestAdmissionSetsMissingNamespace(t *testing.T) {
},
}
// create a dummy evaluator so we can trigger quota
podEvaluator := &generic.ObjectCountEvaluator{
AllowCreateOnUpdate: false,
InternalGroupKind: api.Kind("Pod"),
ResourceName: api.ResourcePods,
}
registry := &generic.GenericRegistry{
InternalEvaluators: map[schema.GroupKind]quota.Evaluator{
podEvaluator.GroupKind(): podEvaluator,
},
}
stopCh := make(chan struct{})
defer close(stopCh)
@ -1118,8 +1117,8 @@ func TestAdmissionSetsMissingNamespace(t *testing.T) {
quotaAccessor.client = kubeClient
quotaAccessor.lister = informerFactory.Core().InternalVersion().ResourceQuotas().Lister()
config := &resourcequotaapi.Configuration{}
evaluator := NewQuotaEvaluator(quotaAccessor, install.NewRegistry(nil, nil), nil, config, 5, stopCh)
evaluator.(*quotaEvaluator).registry = registry
quotaConfiguration := install.NewQuotaConfigurationForAdmission()
evaluator := NewQuotaEvaluator(quotaAccessor, quotaConfiguration, nil, config, 5, stopCh)
handler := &quotaAdmission{
Handler: admission.NewHandler(admission.Create, admission.Update),
@ -1164,7 +1163,8 @@ func TestAdmitRejectsNegativeUsage(t *testing.T) {
quotaAccessor.client = kubeClient
quotaAccessor.lister = informerFactory.Core().InternalVersion().ResourceQuotas().Lister()
config := &resourcequotaapi.Configuration{}
evaluator := NewQuotaEvaluator(quotaAccessor, install.NewRegistry(nil, nil), nil, config, 5, stopCh)
quotaConfiguration := install.NewQuotaConfigurationForAdmission()
evaluator := NewQuotaEvaluator(quotaAccessor, quotaConfiguration, nil, config, 5, stopCh)
handler := &quotaAdmission{
Handler: admission.NewHandler(admission.Create, admission.Update),
@ -1210,7 +1210,8 @@ func TestAdmitWhenUnrelatedResourceExceedsQuota(t *testing.T) {
quotaAccessor.client = kubeClient
quotaAccessor.lister = informerFactory.Core().InternalVersion().ResourceQuotas().Lister()
config := &resourcequotaapi.Configuration{}
evaluator := NewQuotaEvaluator(quotaAccessor, install.NewRegistry(nil, nil), nil, config, 5, stopCh)
quotaConfiguration := install.NewQuotaConfigurationForAdmission()
evaluator := NewQuotaEvaluator(quotaAccessor, quotaConfiguration, nil, config, 5, stopCh)
handler := &quotaAdmission{
Handler: admission.NewHandler(admission.Create, admission.Update),
@ -1246,7 +1247,8 @@ func TestAdmitLimitedResourceNoQuota(t *testing.T) {
},
},
}
evaluator := NewQuotaEvaluator(quotaAccessor, install.NewRegistry(nil, nil), nil, config, 5, stopCh)
quotaConfiguration := install.NewQuotaConfigurationForAdmission()
evaluator := NewQuotaEvaluator(quotaAccessor, quotaConfiguration, nil, config, 5, stopCh)
handler := &quotaAdmission{
Handler: admission.NewHandler(admission.Create, admission.Update),
@ -1279,7 +1281,8 @@ func TestAdmitLimitedResourceNoQuotaIgnoresNonMatchingResources(t *testing.T) {
},
},
}
evaluator := NewQuotaEvaluator(quotaAccessor, install.NewRegistry(nil, nil), nil, config, 5, stopCh)
quotaConfiguration := install.NewQuotaConfigurationForAdmission()
evaluator := NewQuotaEvaluator(quotaAccessor, quotaConfiguration, nil, config, 5, stopCh)
handler := &quotaAdmission{
Handler: admission.NewHandler(admission.Create, admission.Update),
@ -1325,7 +1328,8 @@ func TestAdmitLimitedResourceWithQuota(t *testing.T) {
},
},
}
evaluator := NewQuotaEvaluator(quotaAccessor, install.NewRegistry(nil, nil), nil, config, 5, stopCh)
quotaConfiguration := install.NewQuotaConfigurationForAdmission()
evaluator := NewQuotaEvaluator(quotaAccessor, quotaConfiguration, nil, config, 5, stopCh)
handler := &quotaAdmission{
Handler: admission.NewHandler(admission.Create, admission.Update),
@ -1383,7 +1387,8 @@ func TestAdmitLimitedResourceWithMultipleQuota(t *testing.T) {
},
},
}
evaluator := NewQuotaEvaluator(quotaAccessor, install.NewRegistry(nil, nil), nil, config, 5, stopCh)
quotaConfiguration := install.NewQuotaConfigurationForAdmission()
evaluator := NewQuotaEvaluator(quotaAccessor, quotaConfiguration, nil, config, 5, stopCh)
handler := &quotaAdmission{
Handler: admission.NewHandler(admission.Create, admission.Update),
@ -1431,7 +1436,8 @@ func TestAdmitLimitedResourceWithQuotaThatDoesNotCover(t *testing.T) {
},
},
}
evaluator := NewQuotaEvaluator(quotaAccessor, install.NewRegistry(nil, nil), nil, config, 5, stopCh)
quotaConfiguration := install.NewQuotaConfigurationForAdmission()
evaluator := NewQuotaEvaluator(quotaAccessor, quotaConfiguration, nil, config, 5, stopCh)
handler := &quotaAdmission{
Handler: admission.NewHandler(admission.Create, admission.Update),

View File

@ -34,6 +34,7 @@ import (
"k8s.io/client-go/util/workqueue"
"k8s.io/kubernetes/pkg/api"
"k8s.io/kubernetes/pkg/quota"
"k8s.io/kubernetes/pkg/quota/generic"
_ "k8s.io/kubernetes/pkg/util/reflector/prometheus" // for reflector metric registration
_ "k8s.io/kubernetes/pkg/util/workqueue/prometheus" // for workqueue metric registration
resourcequotaapi "k8s.io/kubernetes/plugin/pkg/admission/resourcequota/apis/resourcequota"
@ -51,6 +52,9 @@ type quotaEvaluator struct {
// lockAcquisitionFunc acquires any required locks and returns a cleanup method to defer
lockAcquisitionFunc func([]api.ResourceQuota) func()
// how quota was configured
quotaConfiguration quota.Configuration
// registry that knows how to measure usage for objects
registry quota.Registry
@ -106,16 +110,18 @@ func newAdmissionWaiter(a admission.Attributes) *admissionWaiter {
// NewQuotaEvaluator configures an admission controller that can enforce quota constraints
// using the provided registry. The registry must have the capability to handle group/kinds that
// are persisted by the server this admission controller is intercepting
func NewQuotaEvaluator(quotaAccessor QuotaAccessor, registry quota.Registry, lockAcquisitionFunc func([]api.ResourceQuota) func(), config *resourcequotaapi.Configuration, workers int, stopCh <-chan struct{}) Evaluator {
func NewQuotaEvaluator(quotaAccessor QuotaAccessor, quotaConfiguration quota.Configuration, lockAcquisitionFunc func([]api.ResourceQuota) func(), config *resourcequotaapi.Configuration, workers int, stopCh <-chan struct{}) Evaluator {
// if we get a nil config, just create an empty default.
if config == nil {
config = &resourcequotaapi.Configuration{}
}
return &quotaEvaluator{
quotaAccessor: quotaAccessor,
lockAcquisitionFunc: lockAcquisitionFunc,
registry: registry,
quotaConfiguration: quotaConfiguration,
registry: generic.NewRegistry(quotaConfiguration.Evaluators()),
queue: workqueue.NewNamed("admission_quota_controller"),
work: map[string][]*admissionWaiter{},
@ -365,9 +371,8 @@ func limitedByDefault(usage api.ResourceList, limitedResources []resourcequotaap
// that capture what the usage would be if the request succeeded. It return an error if there is insufficient quota to satisfy the request
func (e *quotaEvaluator) checkRequest(quotas []api.ResourceQuota, a admission.Attributes) ([]api.ResourceQuota, error) {
namespace := a.GetNamespace()
evaluators := e.registry.Evaluators()
evaluator, found := evaluators[a.GetKind().GroupKind()]
if !found {
evaluator := e.registry.Get(a.GetResource().GroupResource())
if evaluator == nil {
return quotas, nil
}
@ -516,18 +521,27 @@ func (e *quotaEvaluator) Evaluate(a admission.Attributes) error {
go e.run()
})
// if we do not know how to evaluate use for this kind, just ignore
evaluators := e.registry.Evaluators()
evaluator, found := evaluators[a.GetKind().GroupKind()]
if !found {
// is this resource ignored?
gvr := a.GetResource()
gr := gvr.GroupResource()
if _, ok := e.quotaConfiguration.IgnoredResources()[gr]; ok {
return nil
}
// if we do not know how to evaluate use for this resource, create an evaluator
evaluator := e.registry.Get(gr)
if evaluator == nil {
// create an object count evaluator if no evaluator previously registered
// note, we do not need aggregate usage here, so we pass a nil infomer func
evaluator = generic.NewObjectCountEvaluator(false, gr, nil, "")
e.registry.Add(evaluator)
glog.Infof("quota admission added evaluator for: %s", gr)
}
// for this kind, check if the operation could mutate any quota resources
// if no resources tracked by quota are impacted, then just return
if !evaluator.Handles(a) {
return nil
}
waiter := newAdmissionWaiter(a)
e.addWork(waiter)

View File

@ -38,6 +38,7 @@ go_library(
"//vendor/github.com/onsi/gomega:go_default_library",
"//vendor/github.com/stretchr/testify/assert:go_default_library",
"//vendor/k8s.io/api/core/v1:go_default_library",
"//vendor/k8s.io/api/extensions/v1beta1:go_default_library",
"//vendor/k8s.io/api/scheduling/v1alpha1:go_default_library",
"//vendor/k8s.io/apimachinery/pkg/api/errors:go_default_library",
"//vendor/k8s.io/apimachinery/pkg/api/resource:go_default_library",

View File

@ -21,6 +21,7 @@ import (
"time"
"k8s.io/api/core/v1"
extensions "k8s.io/api/extensions/v1beta1"
"k8s.io/apimachinery/pkg/api/errors"
"k8s.io/apimachinery/pkg/api/resource"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
@ -409,6 +410,41 @@ var _ = SIGDescribe("ResourceQuota", func() {
Expect(err).NotTo(HaveOccurred())
})
It("should create a ResourceQuota and capture the life of a replica set.", 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.ResourceName("count/replicasets.extensions")] = resource.MustParse("0")
err = waitForResourceQuota(f.ClientSet, f.Namespace.Name, quotaName, usedResources)
Expect(err).NotTo(HaveOccurred())
By("Creating a ReplicaSet")
replicaSet := newTestReplicaSetForQuota("test-rs", "nginx", 0)
replicaSet, err = f.ClientSet.Extensions().ReplicaSets(f.Namespace.Name).Create(replicaSet)
Expect(err).NotTo(HaveOccurred())
By("Ensuring resource quota status captures replicaset creation")
usedResources = v1.ResourceList{}
usedResources[v1.ResourceName("count/replicasets.extensions")] = resource.MustParse("1")
err = waitForResourceQuota(f.ClientSet, f.Namespace.Name, quotaName, usedResources)
Expect(err).NotTo(HaveOccurred())
By("Deleting a ReplicaSet")
err = f.ClientSet.Extensions().ReplicaSets(f.Namespace.Name).Delete(replicaSet.Name, nil)
Expect(err).NotTo(HaveOccurred())
By("Ensuring resource quota status released usage")
usedResources[v1.ResourceName("count/replicasets.extensions")] = resource.MustParse("0")
err = waitForResourceQuota(f.ClientSet, f.Namespace.Name, quotaName, usedResources)
Expect(err).NotTo(HaveOccurred())
})
It("should create a ResourceQuota and capture the life of a persistent volume claim. [sig-storage]", func() {
By("Creating a ResourceQuota")
quotaName := "test-quota"
@ -708,6 +744,8 @@ func newTestResourceQuota(name string) *v1.ResourceQuota {
hard[v1.ResourceRequestsStorage] = resource.MustParse("10Gi")
hard[core.V1ResourceByStorageClass(classGold, v1.ResourcePersistentVolumeClaims)] = resource.MustParse("10")
hard[core.V1ResourceByStorageClass(classGold, v1.ResourceRequestsStorage)] = resource.MustParse("10Gi")
// test quota on discovered resource type
hard[v1.ResourceName("count/replicasets.extensions")] = resource.MustParse("5")
return &v1.ResourceQuota{
ObjectMeta: metav1.ObjectMeta{Name: name},
Spec: v1.ResourceQuotaSpec{Hard: hard},
@ -784,6 +822,33 @@ func newTestReplicationControllerForQuota(name, image string, replicas int32) *v
}
}
// newTestReplicaSetForQuota returns a simple replica set
func newTestReplicaSetForQuota(name, image string, replicas int32) *extensions.ReplicaSet {
zero := int64(0)
return &extensions.ReplicaSet{
ObjectMeta: metav1.ObjectMeta{
Name: name,
},
Spec: extensions.ReplicaSetSpec{
Replicas: &replicas,
Template: v1.PodTemplateSpec{
ObjectMeta: metav1.ObjectMeta{
Labels: map[string]string{"name": name},
},
Spec: v1.PodSpec{
TerminationGracePeriodSeconds: &zero,
Containers: []v1.Container{
{
Name: name,
Image: image,
},
},
},
},
},
}
}
// newTestServiceForQuota returns a simple service
func newTestServiceForQuota(name string, serviceType v1.ServiceType) *v1.Service {
return &v1.Service{

View File

@ -15,7 +15,6 @@ go_test(
importpath = "k8s.io/kubernetes/test/integration/quota",
tags = ["integration"],
deps = [
"//pkg/api:go_default_library",
"//pkg/api/testapi:go_default_library",
"//pkg/client/clientset_generated/internalclientset:go_default_library",
"//pkg/client/informers/informers_generated/internalversion:go_default_library",
@ -23,6 +22,7 @@ go_test(
"//pkg/controller/replication:go_default_library",
"//pkg/controller/resourcequota:go_default_library",
"//pkg/kubeapiserver/admission:go_default_library",
"//pkg/quota/generic:go_default_library",
"//pkg/quota/install:go_default_library",
"//plugin/pkg/admission/resourcequota:go_default_library",
"//plugin/pkg/admission/resourcequota/apis/resourcequota:go_default_library",
@ -32,7 +32,6 @@ go_test(
"//vendor/k8s.io/apimachinery/pkg/apis/meta/v1:go_default_library",
"//vendor/k8s.io/apimachinery/pkg/fields:go_default_library",
"//vendor/k8s.io/apimachinery/pkg/labels:go_default_library",
"//vendor/k8s.io/apimachinery/pkg/runtime/schema:go_default_library",
"//vendor/k8s.io/apimachinery/pkg/util/wait:go_default_library",
"//vendor/k8s.io/apimachinery/pkg/watch:go_default_library",
"//vendor/k8s.io/client-go/informers:go_default_library",

View File

@ -28,14 +28,12 @@ import (
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/fields"
"k8s.io/apimachinery/pkg/labels"
"k8s.io/apimachinery/pkg/runtime/schema"
"k8s.io/apimachinery/pkg/util/wait"
"k8s.io/apimachinery/pkg/watch"
"k8s.io/client-go/informers"
clientset "k8s.io/client-go/kubernetes"
restclient "k8s.io/client-go/rest"
"k8s.io/client-go/tools/record"
"k8s.io/kubernetes/pkg/api"
"k8s.io/kubernetes/pkg/api/testapi"
"k8s.io/kubernetes/pkg/client/clientset_generated/internalclientset"
internalinformers "k8s.io/kubernetes/pkg/client/informers/informers_generated/internalversion"
@ -43,6 +41,7 @@ import (
replicationcontroller "k8s.io/kubernetes/pkg/controller/replication"
resourcequotacontroller "k8s.io/kubernetes/pkg/controller/resourcequota"
kubeadmission "k8s.io/kubernetes/pkg/kubeapiserver/admission"
"k8s.io/kubernetes/pkg/quota/generic"
quotainstall "k8s.io/kubernetes/pkg/quota/install"
"k8s.io/kubernetes/plugin/pkg/admission/resourcequota"
resourcequotaapi "k8s.io/kubernetes/plugin/pkg/admission/resourcequota/apis/resourcequota"
@ -74,8 +73,8 @@ func TestQuota(t *testing.T) {
admission.(kubeadmission.WantsInternalKubeClientSet).SetInternalKubeClientSet(internalClientset)
internalInformers := internalinformers.NewSharedInformerFactory(internalClientset, controller.NoResyncPeriodFunc())
admission.(kubeadmission.WantsInternalKubeInformerFactory).SetInternalKubeInformerFactory(internalInformers)
quotaRegistry := quotainstall.NewRegistry(nil, nil)
admission.(kubeadmission.WantsQuotaRegistry).SetQuotaRegistry(quotaRegistry)
qca := quotainstall.NewQuotaConfigurationForAdmission()
admission.(kubeadmission.WantsQuotaConfiguration).SetQuotaConfiguration(qca)
defer close(admissionCh)
masterConfig := framework.NewIntegrationTestMasterConfig()
@ -101,22 +100,33 @@ func TestQuota(t *testing.T) {
rm.SetEventRecorder(&record.FakeRecorder{})
go rm.Run(3, controllerCh)
resourceQuotaRegistry := quotainstall.NewRegistry(clientset, nil)
groupKindsToReplenish := []schema.GroupKind{
api.Kind("Pod"),
}
discoveryFunc := clientset.Discovery().ServerPreferredNamespacedResources
listerFuncForResource := generic.ListerFuncForResourceFunc(informers.ForResource)
qc := quotainstall.NewQuotaConfigurationForControllers(listerFuncForResource)
informersStarted := make(chan struct{})
resourceQuotaControllerOptions := &resourcequotacontroller.ResourceQuotaControllerOptions{
QuotaClient: clientset.Core(),
ResourceQuotaInformer: informers.Core().V1().ResourceQuotas(),
ResyncPeriod: controller.NoResyncPeriodFunc,
Registry: resourceQuotaRegistry,
GroupKindsToReplenish: groupKindsToReplenish,
InformerFactory: informers,
ReplenishmentResyncPeriod: controller.NoResyncPeriodFunc,
ControllerFactory: resourcequotacontroller.NewReplenishmentControllerFactory(informers),
DiscoveryFunc: discoveryFunc,
IgnoredResourcesFunc: qc.IgnoredResources,
InformersStarted: informersStarted,
Registry: generic.NewRegistry(qc.Evaluators()),
}
go resourcequotacontroller.NewResourceQuotaController(resourceQuotaControllerOptions).Run(2, controllerCh)
resourceQuotaController, err := resourcequotacontroller.NewResourceQuotaController(resourceQuotaControllerOptions)
if err != nil {
t.Fatalf("unexpected err: %v", err)
}
go resourceQuotaController.Run(2, controllerCh)
// Periodically the quota controller to detect new resource types
go resourceQuotaController.Sync(discoveryFunc, 30*time.Second, controllerCh)
internalInformers.Start(controllerCh)
informers.Start(controllerCh)
close(informersStarted)
startTime := time.Now()
scale(t, ns2.Name, clientset)
@ -158,7 +168,6 @@ func waitForQuota(t *testing.T, quota *v1.ResourceQuota, clientset *clientset.Cl
default:
return false, nil
}
switch cast := event.Object.(type) {
case *v1.ResourceQuota:
if len(cast.Status.Hard) > 0 {
@ -254,7 +263,7 @@ func TestQuotaLimitedResourceDenial(t *testing.T) {
},
},
}
quotaRegistry := quotainstall.NewRegistry(nil, nil)
qca := quotainstall.NewQuotaConfigurationForAdmission()
admission, err := resourcequota.NewResourceQuota(config, 5, admissionCh)
if err != nil {
t.Fatalf("unexpected error: %v", err)
@ -262,7 +271,7 @@ func TestQuotaLimitedResourceDenial(t *testing.T) {
admission.(kubeadmission.WantsInternalKubeClientSet).SetInternalKubeClientSet(internalClientset)
internalInformers := internalinformers.NewSharedInformerFactory(internalClientset, controller.NoResyncPeriodFunc())
admission.(kubeadmission.WantsInternalKubeInformerFactory).SetInternalKubeInformerFactory(internalInformers)
admission.(kubeadmission.WantsQuotaRegistry).SetQuotaRegistry(quotaRegistry)
admission.(kubeadmission.WantsQuotaConfiguration).SetQuotaConfiguration(qca)
defer close(admissionCh)
masterConfig := framework.NewIntegrationTestMasterConfig()
@ -286,22 +295,33 @@ func TestQuotaLimitedResourceDenial(t *testing.T) {
rm.SetEventRecorder(&record.FakeRecorder{})
go rm.Run(3, controllerCh)
resourceQuotaRegistry := quotainstall.NewRegistry(clientset, nil)
groupKindsToReplenish := []schema.GroupKind{
api.Kind("Pod"),
}
discoveryFunc := clientset.Discovery().ServerPreferredNamespacedResources
listerFuncForResource := generic.ListerFuncForResourceFunc(informers.ForResource)
qc := quotainstall.NewQuotaConfigurationForControllers(listerFuncForResource)
informersStarted := make(chan struct{})
resourceQuotaControllerOptions := &resourcequotacontroller.ResourceQuotaControllerOptions{
QuotaClient: clientset.Core(),
ResourceQuotaInformer: informers.Core().V1().ResourceQuotas(),
ResyncPeriod: controller.NoResyncPeriodFunc,
Registry: resourceQuotaRegistry,
GroupKindsToReplenish: groupKindsToReplenish,
InformerFactory: informers,
ReplenishmentResyncPeriod: controller.NoResyncPeriodFunc,
ControllerFactory: resourcequotacontroller.NewReplenishmentControllerFactory(informers),
DiscoveryFunc: discoveryFunc,
IgnoredResourcesFunc: qc.IgnoredResources,
InformersStarted: informersStarted,
Registry: generic.NewRegistry(qc.Evaluators()),
}
go resourcequotacontroller.NewResourceQuotaController(resourceQuotaControllerOptions).Run(2, controllerCh)
resourceQuotaController, err := resourcequotacontroller.NewResourceQuotaController(resourceQuotaControllerOptions)
if err != nil {
t.Fatalf("unexpected err: %v", err)
}
go resourceQuotaController.Run(2, controllerCh)
// Periodically the quota controller to detect new resource types
go resourceQuotaController.Sync(discoveryFunc, 30*time.Second, controllerCh)
internalInformers.Start(controllerCh)
informers.Start(controllerCh)
close(informersStarted)
// try to create a pod
pod := &v1.Pod{
@ -323,6 +343,7 @@ func TestQuotaLimitedResourceDenial(t *testing.T) {
}
// now create a covering quota
// note: limited resource does a matchContains, so we now have "pods" matching "pods" and "count/pods"
quota := &v1.ResourceQuota{
ObjectMeta: metav1.ObjectMeta{
Name: "quota",
@ -330,7 +351,8 @@ func TestQuotaLimitedResourceDenial(t *testing.T) {
},
Spec: v1.ResourceQuotaSpec{
Hard: v1.ResourceList{
v1.ResourcePods: resource.MustParse("1000"),
v1.ResourcePods: resource.MustParse("1000"),
v1.ResourceName("count/pods"): resource.MustParse("1000"),
},
},
}